Commit Graph
371 Commits
Author SHA1 Message Date
Renn F 5da909d9d7 fix(gateway): cross-team planning fanout + complete tracing-gap hints
Task #157 — spine-cap allows planning fanout across cells:
    main-pm's pattern is to delegate planning to be-pm / fe-pm / ux-pm
    in parallel — each on a different team. The previous spine-cap
    rejected all planning siblings under one parent as
    over-decomposition. New helper _is_cross_team_planning skips the
    cap for planning when both teams are non-empty and distinct. Code
    / documentation stay capped regardless (single repo on one branch
    shouldn't have two simultaneous code subtasks).

Task #159 — tracing-gap remediate hints every requirement:
    journal:during_work>=1, journal:struggle, commits>=1, pr_open,
    and self_verified had no entries in _hint_for_missing_key, so when
    they were missing the agent saw the token in `missing[]` but the
    `remediate` text had no instruction for how to satisfy them.
    Smoke-10's be-dev-1 burned multiple turns retrying i_am_done not
    knowing scope='reflect' doesn't count toward during_work. Now
    every token has a hint, the during_work hint warns that reflect
    doesn't satisfy it, and multi-hint remediate uses a numbered list
    so the model treats each requirement as a distinct step instead
    of a semicolon-blob.

Also coerce convert_plan._coerce_risk formatting (ruff-format follow-up
to 9cd73d0).
2026-05-15 08:21:08 +02:00
Renn F 9cd73d0902 fix(panel): coerce risk.severity to default str on read + write
Bug:
    Smoke-10's main-pm submitted a rich plan with risks omitting
    severity. _normalize_risk persisted severity=None into the DB.
    Every panel poll of /tasks/{id} then 500'd because
    TaskPlanResponse.risks declares list[dict[str, str]] and Pydantic
    rejects None for a str field. Result: panel single-task page broken
    end-to-end every ~1-3s as panel reloads.

Fix:
    Write side (_normalize_risk): default missing/None severity to
    "medium" so new writes never persist None.
    Read side (convert_plan._coerce_risk): defensively coerce any
    existing DB row with severity=None to "medium" so old bad data
    doesn't continue bricking the read path.
2026-05-15 07:25:52 +02:00
Renn F 2c838c2a9e feat(gateway): propagate sessions to subtasks + auto-emit milestone progress
Task #156 (sessions): pre-gateway flow created a session for the whole
task tree at once, so subtasks were visible in the PM's group chat the
moment they existed. The gateway creates subtasks one-by-one via
delegate(), losing that wiring. Added
MessagingService.propagate_sessions_to_subtask and threaded it through
the choreographer's _create_subtask_from_inputs. ChoreographerDeps grew
an optional `messaging` field so existing test wirings keep working.

Task #155 (progress): smoke-9 had zero progress entries because the dev
never called progress() explicitly. Added _record_milestone_progress and
fire it server-side from two natural milestones — open_pr ("opened PR
#N", 70%) and i_am_done ("submitted for QA review", 90%). Best-effort
write (contextlib.suppress) so a progress failure cannot break the verb
path. Extracted _open_pr_success_envelope to keep cyclomatic rank ≤ B.
2026-05-15 06:54:35 +02:00
Renn F 4fdde2b082 fix(gateway): evidence/QA/doc paths populate files_changed from git
Bug:
    ContentActions.evidence() hard-coded files_changed=[] and diffed
    against HEAD~1 instead of the branch's parent. QA's _build_qa_claim_
    evidence (and doc/_impl mirrors) sourced files_changed from
    work_session.files_modified, which the gateway commit() never
    populates (no add_files_modified plumbing). Result: QA / docs / PM
    reviewers saw an empty change list on real PRs and only the latest
    commit's delta — flagged in smoke-9 when PR #20 showed the README
    change on GitHub but evidence() reported empty.

Fix:
    Added GitService.list_changed_files (git diff --name-only against
    parent branch). evidence(), _build_qa_claim_evidence,
    _claim_doc_evidence, and _build_i_am_done_ok all source files_changed
    from this — git is the authoritative source. evidence() also drops
    the HEAD~1 base so the diff is the full PR.

    Wired EvidenceRepo into ContentActionsDeps so evidence() returns
    journal_highlights too, matching the QA/doc shape.
2026-05-15 06:31:38 +02:00
Renn F d5ff8c7b13 fix(gateway): i_will_plan persists rich plan dict, not raw string
Bug:
    Choreographer.i_will_plan() built spec_ctx with the raw plan string
    while ctx (_ClaimPlanStartContext) got the resolved (panel-shaped)
    dict. The verb runner uses spec_ctx — so the rich shape never reached
    TaskService.set_plan. The panel's Plan tab stayed empty even when
    PMs supplied approach / sub_tasks / risks.

Fix:
    Pass the resolved (possibly-dict) plan into spec_ctx too. Widened
    lifecycle.Context.plan to `str | dict[str, Any] | None` to match.
    Tightened _resolve_effective_plan to require a non-empty narrative
    paragraph — rich structure layers on top of prose, not in place of it.
2026-05-15 06:15:44 +02:00
Renn F e3570b444f fix(orchestrator): briefing renders ToolSearch directive + current verb names
Smoke-8 follow-up. Two issues in _write_agent_briefing:

1. _build_tool_load_block was scraping role prompts for a "## Load on
   spawn" section that doesn't exist in any role file. Returned "" for
   every role → no ToolSearch directive in the briefing. Combined with
   weak models skipping the system-prompt-layer directive (#144), the
   agent's first action was Edit → "not enabled in this context."

   Fix: per-role tool list lives in the orchestrator (mirrors
   factories._base.py). Pre-renders the directive directly. developer
   and documenter get Edit + Write; QA/PMs/board get the common
   read-only set. 7 tests pin the contract.

2. The briefing's "Terminal tools (how to exit cleanly)" section still
   listed pre-gateway verb names: roboco_agent_idle,
   roboco_task_substitute, roboco_task_escalate,
   roboco_task_submit_qa, _qa_pass/fail, _docs_complete, _complete.
   Same rename pattern as #145's _TERMINAL_TOOLS set. Updated to:
   i_am_idle, i_am_blocked, unclaim, i_am_done, pass, fail,
   i_documented, complete, submit_up, escalate_up, escalate_to_ceo.

The agent now reads the same directive in two places (system prompt +
session briefing) — the second touch point catches weak models that
skip the first.
2026-05-15 05:01:18 +02:00
Renn F 47c674d70e fix(hooks): post-tool-budget-hook records terminal tool to SDK
Smoke-8: the stop-hook still nagged after a successful i_am_idle even
after #145's _TERMINAL_TOOLS rename. Root cause was upstream — nothing
was POSTing to /terminal/tool_recorded, so the SDK's recent_tools
deque stayed empty and had_terminal_recently() always returned False.

The PostToolUse hooks already record every tool call to
/budget/tool_called for the budget/loop tracker. Added a parallel call
to /terminal/tool_recorded so the terminal-tracker sees the same
stream. Fire-and-forget; never blocks Claude.

After the SDK suffix-strip (line ~798 in agent_sdk/server.py),
mcp__roboco-flow__i_am_idle becomes i_am_idle which is in
_TERMINAL_TOOLS (per #145). Stop-hook reads /terminal/stop_attempt
and now sees had_terminal_recently=true on the first attempt → exits 0.
2026-05-15 04:54:08 +02:00
Renn F 0c60d0bf7d ++ 2026-05-15 04:49:07 +02:00
Renn F 64d89fbd93 docs(prompts): teach all roles the roboco-git-readonly verbs
Smoke-8: QA fell back to Bash for git inspection (git log, git branch,
git show) — bash-guard correctly blocked most of it. The
roboco-git-readonly MCP server WAS registered for every agent (per
orchestrator.py:1897) with roboco_git_status/log/diff/branches, but
no role prompt mentioned them, so agents never tried them.

Added the four verbs to the verb tables in: developer.md, documenter.md,
qa.md, cell_pm.md, main_pm.md, board.md. Each entry notes "use these,
NOT raw `Bash git ...`" so agents reach for the right tool first.

No code change — these MCP tools have existed all along. This is a
prompt visibility fix.
2026-05-15 04:48:59 +02:00
Renn F ef29d663fa docs(prompts): teach PMs the gateway's auto-naming conventions
Smoke-8: QA correctly failed a PR because the PM wrote acceptance
criteria the gateway can never satisfy:
- "branch named feature/backend/<full-uuid>" — gateway generates
  hierarchical 8-char IDs with `--` separator
- "commit prefix [<root-id>]" — gateway prefixes with the leaf
  (dev's) task ID, not the root

The dev did the right work (timestamp added to README, PR opened) but
the literal criteria were unreachable.

Both cell_pm.md and main_pm.md now have an "How to write
acceptance_criteria" block explaining:
- Gateway-controlled outputs: branch name, commit prefix
- Examples of bad criteria (implementation/identifier-based) and good
  criteria (outcome/file-content/PR-state)
- When you must reference a task ID, use the leaf (dev) ID — not
  the root.

Next smoke run: PMs should write outcome criteria, dev work should
clear QA on first review (assuming the work itself is correct).
2026-05-15 04:42:54 +02:00
Renn F cfefe85f87 fix(orchestrator): don't auto-restart on graceful exit; tighten role-status
Smoke-8 surfaced a tight respawn loop: QA failed a PR cleanly, container
exited 0, then _check_health bumped error_count and respawned QA with
the same task_id. But by then the task was in needs_revision (dev's
state), so QA's claim_review was rejected — and the cycle repeated on
the next health tick. Token-burning loop.

Two layers:

1. _check_health now reads docker's exit code. exit_code == 0 →
   graceful (intentional handoff via i_am_idle / clean shutdown) →
   reset error_count, do NOT auto-restart. Non-zero → keep the
   existing crash-retry behavior. Refactored into
   _inspect_container_state + _handle_stopped_container to keep
   xenon's complexity check happy.

2. _readiness_check_role_for_status now includes the dev-owned
   states (needs_revision, verifying) so a misrouted spawn for QA /
   PM / board on these statuses fails the readiness gate before the
   gateway has to reject it. Defense in depth — the right path is
   #1 (don't respawn on clean exit at all), but if some other code
   path tries to spawn QA on needs_revision the gate now catches it.

Tests: 12 new (5 for _check_health graceful/crash matrix + 7 for the
expanded role-status table). Pre-gateway names (none of which were
needed here) untouched.
2026-05-15 04:36:08 +02:00
Renn F 87b18bc64f fix(gateway): spine-cap remediate forbids task_type workaround
Smoke-7: be-pm got the expected spine-cap rejection on a second
delegate. The remediate said "drive the existing sibling to
completion / cancel it, OR split this parent into two sibling
parents". The model read that, decided neither applied, and
"adapted" by re-delegating with task_type='documentation' as a
"verification subtask". The gateway accepted it (different type =
no cap collision) but the orphan subtask had no claimant — it
blocked submit_up forever with "subtasks not all terminal".

Two-layer fix:

1. _spine_type_dup_envelope remediate now explicitly forbids the
   workaround: "DO NOT work around this by delegating again with a
   different task_type (e.g. 'documentation' or 'research' as a
   'verification' subtask). The lifecycle handles QA, documentation,
   and PM-review automatically after the code subtask finishes —
   you do not create auxiliary subtasks for those roles. Call
   i_am_idle() now and wait for the existing child to come back."

2. cell_pm.md workflow step 6 strengthened to name the anti-pattern
   explicitly: no verification subtask (QA is the verification step);
   never re-delegate with a different task_type as a workaround.

3 new tests pin the remediate text: forbids workaround, names the
verification anti-pattern, retains invalid_state error kind.
2026-05-15 03:26:04 +02:00
Renn F d4126ffb7f fix(sdk): _TERMINAL_TOOLS uses current gateway verb names
Smoke-7 evidence: every agent's first successful i_am_idle was followed
by a stop-hook error "you stopped without calling a terminal tool"
even though i_am_idle had just succeeded.

Root cause: agent_sdk._TERMINAL_TOOLS still held pre-gateway names
(roboco_agent_idle, roboco_task_submit_qa, ...). /terminal/tool_recorded
strips the `mcp__roboco-flow__` prefix and stores 'i_am_idle' — the
membership check against {'roboco_agent_idle', ...} never matched, so
had_terminal_recently() always returned False, and the stop hook nagged
every clean exit. Wasted ~2-3 turns per agent + burned stop_allowance.

Fix: rebuilt _TERMINAL_TOOLS with the current gateway verb names:
i_am_idle, i_am_done, i_am_blocked, i_documented, unclaim, pass, fail,
complete, submit_up, escalate_up, escalate_to_ceo.

7 new tests pin:
- every role's terminal verbs are recognized
- pre-gateway names are NOT in the set
- _SessionState.had_terminal_recently() returns True after i_am_idle
2026-05-15 03:20:43 +02:00
Renn F 197b1576c3 fix(prompts): hoist ToolSearch activation to top of system prompt
Smoke-7: be-dev-1 hit "Edit exists but is not enabled in this context."
Claude Code v2.1.69+ defers built-in tools (Edit, Write, Read, etc.)
behind a ToolSearch call. Weak models (minimax-m2.7) skip soft
directives buried in the briefing.

Also: 4 role prompts (developer, cell_pm, main_pm, board) claimed
"no ToolSearch needed" — a lie that compounds the problem. The
manifest registers MCP tools; built-in tools are still deferred.

Fix: compose_prompt now prepends a tool-load directive layer as the
FIRST block in the system prompt. It names the exact ToolSearch call
the role needs:
- developer/documenter: Read, Bash, Grep, Glob, Task, TodoWrite, Edit, Write
- qa/pm/board: Read, Bash, Grep, Glob, Task, TodoWrite (no Edit/Write)

The directive includes the failure mode it prevents so the model
understands what skipping the call causes.

Updated role-prompt lines that lied about ToolSearch.

7 new tests pin: directive is the first block; developer/documenter
get Edit/Write; qa/pm don't; failure-mode message is present.
2026-05-15 03:14:43 +02:00
Renn F 417b8c5f29 fix(gateway): dm catches A2AAccessDeniedError; circuit breakers handle dict errors
Smoke-7 surfaced: be-qa called dm(recipient='qa-all', ...) — 'qa-all'
is a channel slug, not an agent. A2A enforcement raised
A2AAccessDeniedError. It propagated past dm(), past content_actions,
got caught by FastAPI middleware which renders RobocoError.to_dict()
as {'error': {'code': ..., 'message': ..., 'details': ...}} — a
DICT-shaped 'error' field.

do_server's circuit-breaker check (and flow_server's mirror) did
`payload.get('error') in _CIRCUIT_REJECTION_KINDS` — trying to hash
a dict against a frozenset → `TypeError: unhashable type: 'dict'`.
The agent saw "Error executing tool dm: unhashable type: 'dict'"
and got stuck calling dm in a loop.

Two-layer fix:

1. content_actions.dm now catches A2AAccessDeniedError and returns
   Envelope.not_authorized with the original reason + route_hint as
   remediate. This is the right shape — content tools always emit
   Envelopes; RobocoErrors escaping to the middleware is a bug.

2. Defense-in-depth: do_server._record_and_check_circuit and
   flow_server._record_and_check_circuit now guard against non-string
   error fields. Any future RobocoError-leak that bypasses (1) will
   pass through untouched instead of crashing the tool call.

3 new tests pin the contracts:
- dm A2A denial returns Envelope.not_authorized (not propagated)
- do_server circuit-breaker doesn't crash on dict-shaped errors
2026-05-15 03:03:53 +02:00
Renn F b90ce83946 fix(mcp): expose pass/fail to QA via IntentSpec→public name mapping
Smoke-7 surfaced this: QA spawned, claim_review succeeded, but every
attempt to call `pass()` fell through to dm/say workarounds. The MCP
tool 'pass' never existed.

Root cause: foundation.policy.lifecycle declares the intent verbs as
`pass_review`/`fail_review` (Python-friendly names — `pass`/`fail` are
keywords). intents_for_role(Role.QA) returns those names, the spawn
manifest carries them, and flow_server reads them. But flow_server's
_TOOLS dict has keys 'pass'/'fail' — the manifest's pass_review keys
didn't match and got silently dropped from the registration.

Fix: add _INTENT_TO_PUBLIC = {'pass_review': 'pass', 'fail_review':
'fail'} in flow_server. _register_tools transforms manifest names
through it before _TOOLS lookup. Manifest entries map to the public
MCP tool names the prompts advertise.

Also fixed _VERB_RETRY_LIMITS keys in foundation.agent_loop — they
used the IntentSpec names too, but the SDK receives the public name
from /verb/attempted (derived from the flow URL path), so the limit
entries never matched real rejections. Renamed to 'pass'/'fail'.

3 regression tests pin: pass/fail register under public names;
IntentSpec names don't leak through; the registered tool POSTs to
the correct orchestrator path.
2026-05-15 02:54:39 +02:00
Renn F 3bbaf0d645 fix(gateway): reject null + DO-NOT-PASS-NULL remediate for decision/reflect
Smoke-6 found the agent calling note(scope='decision', context=null,
chosen=null, rationale=null) eight times in a row. Root cause split
across two surfaces:

1. The MCP tool schema declared these fields as `str | None = None`,
   producing a JSON schema of `anyOf [string, null]`. minimax-m2.7 read
   that and decided null was a valid value — passed it on every retry.

2. The remediate text used `<placeholder>` syntax for the example
   without telling the agent "don't pass null" explicitly.

Fixes:
- roboco/api/schemas/v2/do.py NoteRequest: context, chosen, rationale,
  what_done, what_learned, what_struggled now typed `str = ""` (no None).
  Pydantic on the route rejects literal null with 422 BEFORE the gateway
  sees it. Empty string still counts as missing at the gate.
- roboco/mcp/do_server.py note(): matching signature changes so the
  MCP tool schema declares the fields as `string` not `anyOf[string,null]`.
- roboco/services/gateway/content_actions.py: remediate text now opens
  with "DO NOT pass null" and the example uses concrete values (redis vs
  postgres) instead of <angle bracket> placeholders. Reflect remediate
  also gets the don't-pass-null intro and concrete values.

8 new tests pin "schema rejects null for each of the 6 string fields"
plus "empty defaults work for unscoped notes".
2026-05-14 05:27:45 +02:00
Renn F 21007e122f fix(mcp): do_server per-verb circuit breaker mirrors flow_server
Smoke-6 surfaced the gap. main-pm called note(scope='decision') with
context: null 8 times in a row — every one returned incomplete_input
and the agent kept retrying. The flow_server had a breaker (C1) but
do_server didn't, so content-tool rejections went uncapped.

Mirror the flow_server pattern:
- _CIRCUIT_REJECTION_KINDS = {tracing_gap, invalid_state,
  not_authorized, incomplete_input} (same set)
- _record_and_check_circuit posts to the SDK's /verb/attempted on each
  counted rejection
- When the SDK reports open=true, the original rejection envelope is
  REPLACED with the circuit_open envelope so the agent stops retrying

The SDK side (agent_sdk/server.py) already accepts arbitrary verb
names; no changes there. note hits the default cap of 3 retries / 60s
from foundation.agent_loop. After the third incomplete_input the
agent gets circuit_open and the loop ends.

9 new tests pinning the contract.
2026-05-14 05:21:33 +02:00
Renn F 3171e1e192 ++ 2026-05-14 04:51:04 +02:00
Renn F 4f7f992c91 refactor(gateway): extract pr_update auth check to keep xenon at B
xenon flagged ContentActions.pr_update as rank C — the three-branch
PM-or-assignee guard inlined with the precondition checks pushed it
over the cyclomatic-complexity bound. Extracted the authorization
check into a static helper _pr_update_is_authorized so the verb
itself stays at rank B and the helper carries the role-string +
team-equality branches.

Behavioural no-op; existing tests cover both the assignee path and
the cell_pm-same-team / cell_pm-other-team / main_pm paths.
2026-05-14 04:37:55 +02:00
Renn F b4fe0f13fa feat(roles): expose pr_update to dev/doc/cell_pm/main_pm + prompt updates
Adds pr_update to _DEV_DO, _DOC_DO, _CELL_PM_DO, _MAIN_PM_DO so the
spawn manifest builder registers it on those roles' do-servers. QA,
auditor, and Board roles do not get it — QA reviews PRs but does not
edit them; Board operates above the PR layer; auditor is silent.

developer.md and documenter.md grow a verb-table row and a note next
to the open_pr workflow step calling out that pr_update — not bash-
shimmed `gh pr edit` — is the way to fix PR metadata.
2026-05-14 04:34:05 +02:00
Renn F 74b7c39612 feat(gateway): wire pr_update verb — ContentActions + route + MCP
ContentActions.pr_update enforces:
- task.pr_number is set (else invalid_state, remediate 'call open_pr')
- at least one of title/body/reviewers is non-None (else invalid_state)
- caller is task assignee OR PM on team (cell_pm same-team / main_pm
  cross-team), else not_authorized
- GitError raised by the underlying service maps to invalid_state with
  the upstream message preserved

The route at POST /api/v2/do/pr_update binds PRUpdateRequest, whose
model_validator returns 422 on all-None bodies so the verb layer
never sees them. The MCP tool registry adds 'pr_update' so manifest-
scoped do-servers can expose it to roles that opt in (next commit).
2026-05-14 04:32:46 +02:00
Renn F b5d3d13346 feat(git): add update_pr_for_task + PRUpdateRequest schema
Smoke-5 surfaced that be-dev-1 had no gateway-native way to fix a
PR's title/body or request a reviewer after open_pr; `gh pr edit`
is bash-shimmed and the dev correctly escalated rather than bypass
the guard. This adds the GitService primitive: PATCH /pulls/{n}
for title/body and POST /pulls/{n}/requested_reviewers for the
reviewer list, with NotFound + 422 mapped to typed GitError. The
PRUpdateRequest schema enforces 'at least one field' via a
model_validator so the route returns 422 before reaching the verb.
2026-05-14 04:28:37 +02:00
Renn F 1bd6eb3372 fix(gateway): journal task_id auto-injection works from blocked/paused
Smoke-5 root cause. Agents wrote 5 decisions / 8 reflections / 1 struggle
during the run — every single entry persisted with task_id=NULL. The C8
tracing gate then never saw them and PMs spiraled forever on
'missing: journal:decision' while their decisions sat orphaned.

Cause: ContentActions.note/say/dm/notify called
TaskService.get_active_task_for_agent for task_id auto-injection. That
helper filters to _DEV_ACTIVE_STATUSES = {claimed, in_progress,
verifying, awaiting_qa, awaiting_documentation}. BLOCKED, PAUSED, and
NEEDS_REVISION fall outside that set — so the moment an agent gets
stuck (which is exactly when they journal), auto-injection returns None
and the entry persists without task_id.

Fix:
- New TaskService.get_journal_context_task_for_agent — same shape as
  get_active_task_for_agent but the status set
  _JOURNAL_CONTEXT_STATUSES adds BLOCKED, PAUSED, NEEDS_REVISION.
- ContentActions.note/say/dm/notify use the new lookup.
- ContentActions.commit keeps the narrow get_active_task_for_agent —
  can't commit from blocked, so the dev-active set is correct there.

Tests:
- tests/unit/services/test_journal_context_lookup.py — 5 tests pinning
  the two queries: journal-context INCLUDES blocked/paused/needs_revision,
  dev-active EXCLUDES them.
- Existing content-actions tests updated to stub the new method
  alongside the old one.

This alone may be 70% of what was killing smoke runs end-to-end.
2026-05-14 04:21:07 +02:00
Renn F 7430c88f63 fix(gateway): open_session passes model schema, not API schema
Smoke run 4 crashed with AttributeError: 'SessionForTasksCreateRequest'
object has no attribute 'config' when main-pm called open_session.

The service _build_session_request reads req.config.max_message_count
(model has nested config). The gateway was passing the API schema
SessionForTasksCreateRequest (flat fields, no config attribute at all)
— so `req.config` blew up with AttributeError, not the safer None.

Fix: gateway now constructs SessionForTasksCreate (the model) with the
enum-typed relationship_type, matching what the route at
roboco/api/routes/sessions.py:230 does. Unknown relationship strings
fall back to DISCUSSION.

Two regression tests pin the contract: service receives the model;
invalid relationship_type defaults to DISCUSSION.
2026-05-14 03:28:38 +02:00
Renn F 4dfd1daf1e style: ruff format leftovers from Wave A-D sessions
Pure whitespace / line-wrap reformats accumulated when ruff format
ran during earlier waves but weren't included in their commits. No
semantic changes — collection literals reflowed, with-statement
context managers regrouped via PEP 617 parens.
2026-05-12 22:40:15 +02:00
Renn F 717b7895d0 refactor(optimal_brain): split _process_and_store to drop closure CCN 13 → 0
The inner closure inside BaseIndexPlugin.ingest() packed chunk
filtering, metadata merge, embedding, and aborted-transaction retry
into one block (CCN 13). Xenon's --max-absolute B ignores closures
(only top-level callables count), but radon flagged it as the last
rank-C block. Now zero rank-C blocks in the whole codebase.

Extracted:
- _filter_quality_chunks(raw_chunks) — module helper for the
  tiny / mostly-markdown chunk filter
- _reset_store_connection(store) — module helper for piragi's
  force-close + _init_schema after an aborted transaction
- _chunk_filter_embed_store(doc, metadata) — method that orchestrates
  chunk → filter → embed → store (called via asyncio.to_thread)
- _store_with_transaction_retry(store, chunks, count) — method with
  the retry loop, using early-return guard instead of nested ifs

ingest() now does: await asyncio.to_thread(self._chunk_filter_embed_store, ...).
No more nonlocal capture; chunk_count flows back through the return value.
2026-05-12 07:19:30 +02:00
Renn F 7a9ab1945f style: ruff format leftover line-wrap fixups from CCN refactors 2026-05-12 06:56:15 +02:00
Renn F 20940fe26a refactor(gateway): split _delegate_sibling_dedup_guard to drop CCN 11 → 4
Lift terminal-status and spine-task-type sets to class constants. Pull each
rule's rejection envelope into its own builder (_spine_type_dup_envelope,
_same_assignee_dup_envelope) and combine them under _sibling_dup_envelope.
The guard body is now a flat scan: skip terminal siblings, delegate the
per-sibling verdict to the helper, return the first non-None envelope.
2026-05-12 06:55:39 +02:00
Renn F 60ef901367 refactor(gateway): split i_will_plan to drop CCN 11 → 7
Lift the rich-plan field list to a class constant (_RICH_PLAN_FIELDS) and
extract _resolve_effective_plan — the any()-over-5-keys decision between
the raw string plan and the panel-shaped dict. The verb body keeps its
spec-gate / re-entry / sub_tasks-gate sequence but no longer carries the
panel-shape branch.
2026-05-12 06:54:31 +02:00
Renn F 7f19616c7b refactor(gateway): split i_am_blocked to drop CCN 12 → 7
Extract _build_struggle_body (the reason + optional Blocker Type / What Needed
markdown assembly) and _run_i_am_blocked_intent (the verb-runner dispatch +
try/except → rejection envelope). The verb body keeps its setup / spec-gate
shape but the structured-body branching and runner-exception branching no
longer count against it.
2026-05-12 06:52:33 +02:00
Renn F 657c92cda5 refactor(gateway): split _write_criteria_status to drop CCN 19 → 6
Extract four helpers: _extract_first_commit_sha (dict/model-tolerant sha read),
_already_addressed_criteria (set comprehension over existing status),
_find_existing_entry (preserved-entry lookup) and _new_criterion_entry (build
one fresh row). The main function is now a flat sequence: early-return on
empty criteria, early-return when all already addressed, then one loop with
two cases that each delegate to a helper.
2026-05-12 06:51:12 +02:00
Renn F b49e6ade78 refactor(runtime): split AgentOrchestrator._build_mount_args to drop CCN 14 → 3
Extract each conditional -v/-e block into a focused helper:
_append_claude_json_mount (claude.json file mount), _append_optional_host_mounts
(settings + briefing), _core_volume_and_env_args (the always-on block),
_append_provider_env (Anthropic-base/token), _append_manifest_args (spawn
manifest + gateway flag), _append_workspace_cwd (role-based -w). Two role
membership sets are lifted to class constants. The top-level function is
now a flat sequence of calls — no nested conditionals.
2026-05-12 06:50:07 +02:00
Renn F 51458a02df refactor(events): split StreamEventBus._listen_loop to drop CCN 11 → 6
Extract the per-cycle XREADGROUP + dispatch into _listen_tick and the NOGROUP
self-heal branch into _handle_response_error. The outer loop is now a flat
while/try/except sequence: cancel breaks, response-error delegates the
recover-or-sleep decision to the helper, generic exceptions sleep. No
behavior change; the two new helpers preserve identical log messages and
ordering.
2026-05-12 06:48:18 +02:00
Renn F 25afc2960f refactor(gateway): split _check_scope_required_fields to drop CCN 12 → 3
Lift the two scope-required field tables to module-level constants and route
through a shared _collect_required helper. The options-specific minimum-count
check and the generic scalar-empty check are each a one-line predicate
(_options_field_missing / _scalar_field_missing). The outer function is now a
dict lookup plus one call.
2026-05-12 06:47:23 +02:00
Renn F 39f709e761 refactor(gateway): split _render_journal_content to drop CCN 16 → 5
Lift the scope→sections lookup into a module-level dict (_SCOPE_SECTIONS) and
extract per-value rendering (options list / generic list / scalar) into
_render_section_value. The outer loop is now a flat dispatch with one early
continue per branch; the chained ternary and the list/dict branch ladder
that drove the CCN to 16 are gone.
2026-05-12 06:46:34 +02:00
Renn F 4f7dd7a336 docs(prompts): E4 clarify TodoWrite vs progress() distinction
TodoWrite is Anthropic's private session-local scratchpad — agents use
it to track their own immediate next steps. It does NOT surface to the
panel's Progress tab and is NOT a substitute for
progress(task_id, message, percentage). Smoke run 3 didn't show this
conflation yet, but Wave D's new progress() directive risks it.

- base.md gets the canonical "TodoWrite vs progress()" callout
- developer.md + documenter.md (the two roles with progress()) get
  inline reminders in their verb tables: "NOT TodoWrite"

Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md
section E4.
2026-05-12 06:39:18 +02:00
Renn F f2551c0bdc fix(orchestrator): E3 disable builtin Claude.ai MCP connectors via --strict-mcp-config
Smoke run 3 showed agents loading builtin Anthropic connectors
(mcp__claude_ai_Gmail__authenticate, Google Calendar, Notion, Drive)
alongside our 5 roboco MCP servers. The connectors bloat the tool
surface and give the LLM 'discover' targets it shouldn't have.

The Claude Code CLI's --strict-mcp-config flag tells it to load ONLY
the servers from --mcp-config, ignoring all builtin defaults. Added
to _append_image_and_claude_args next to --mcp-config.

Note: the existing --tools allowlist (Read,Write,Edit,Bash,Grep,Glob,
Task,TodoWrite) only filters builtin tools, not MCP-prefixed ones —
that's why the connectors slipped through.

Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md
section E3.
2026-05-12 06:38:00 +02:00
Renn F db4ac0c1c8 test(integration): E2 anti-regression — agents.role binds to 'agentrole' enum
Pin the SQLAlchemy enum-naming invariant: every column typed Role
(aliased as AgentRole) MUST bind to the postgres enum named
'agentrole', not 'role'. Wave A5's smoke regression came from the
inferred 'role' type colliding with migration 001's 'agentrole'.

The fix already shipped (roboco/db/tables.py::_PG_ENUM_NAME_OVERRIDES);
this test locks it in. Two assertions: agents.role.udt_name == agentrole;
no stray 'role' enum type exists in pg_type.
2026-05-12 06:35:43 +02:00
Renn F ce5761701d docs(prompts): D1 fixup — role-correct circuit-breaker escalation paths
QA / Documenter / Board don't have i_am_blocked in their manifests. The
D1 snippet's "escalate via i_am_blocked" line is now role-correct:
- QA / Documenter: unclaim(task_id) + dm(cell-pm, ...) with rejection
- Board: dm(ceo, ...) for PO/HoM; Auditor uses note(scope='reflect', ...)
2026-05-12 06:34:24 +02:00
Renn F df993befe7 docs(prompts): D4 compel channels() before invented say() slugs
All role prompts now mention channels() as the way to list valid
channel slugs. Smoke run 3 showed agents inventing slugs ('backend-dev',
'backend') and getting Channel not found. The channels() verb was
added in Wave 2 G6 but unused — making the directive explicit in
every prompt.
2026-05-12 06:32:41 +02:00
Renn F dc11ca23f8 docs(prompts): D4 compel triage() first on respawn in cell_pm.md
Cell PM prompt now mandates triage() as the first call on every
respawn, before re-decomposing. Smoke run 3 showed PMs re-decomposing
blindly and hitting spine-cap; triage shows them existing children
and prevents the over-decomposition pattern.
2026-05-12 06:31:56 +02:00
Renn F 517aa4b16b docs(prompts): D4 compel progress() after each commit in developer.md
Developer prompt now mandates progress(task_id, message, percentage)
after each commit. Wave 1's progress verb was added but no agent
called it. The Progress tab stays empty without it.
2026-05-12 06:31:43 +02:00
Renn F 02c241e3a5 docs(prompts): D4 compel open_session in PM prompts
PM prompts now include open_session(task_id, channel, topic) in the
State→Verb table for the "just claimed" state. Without this, the
Sessions tab stays empty — Wave 1's session verb was added but agents
never called it because the prompt didn't directive it.
2026-05-12 06:31:30 +02:00
Renn F 129504a51d docs(prompts): D3 document journal:during_work>=1 in developer.md
Smoke run 3 showed be-dev-1 writing reflect but no mid-work entry,
hitting tracing_gap on i_am_done with missing: ['journal:during_work>=1'].
The reflect note does not satisfy this gate — it's an end-of-work
artifact. The prompt now shows the 5-step cadence explicitly:
i_will_work_on → decision → work → reflect → i_am_done.
2026-05-12 06:30:23 +02:00
Renn F 68d52be4a7 docs(prompts): D2 post-first-delegate reasoning for Main PM + Cell PM
Smoke run 3 showed Main PM seeing the spine-cap reject on its 2nd
delegate attempt (its 1st succeeded) and concluding 'I cannot delegate'
→ escalated to product-owner. The new anti-pattern tells PMs that
spine-cap or role-guard rejections AFTER a successful delegate mean
over-decomposition, not delegation impossibility — verify with triage()
and idle instead.
2026-05-12 06:30:05 +02:00
Renn F 314d829172 docs(prompts): D1 circuit-recovery instruction in all 6 role prompts
Smoke run 3 showed be-dev-1 hitting circuit_open on i_am_done and
escalating via i_am_blocked instead of writing the missing journal
entry and retrying. The prompts now name circuit_open explicitly,
tell agents to read the remediate, fix the one piece, retry once,
and only escalate if the breaker fires again.
2026-05-12 06:29:45 +02:00
Renn F 41ef7f6b4e feat(gateway): C8 PM-decision gate windowed satisfaction
_check_pm_decision_required now requires the latest journal:decision
within pm_decision_window_seconds (default 300). Older decisions no
longer satisfy the gate. Adds JournalService.latest_decision_at.

Future-tighten (out of scope): per-verb-group consumption tracking
would need persistent state — Choreographer is per-request today.
2026-05-12 06:27:33 +02:00
Renn F 89eacf028e feat(gateway): C7 synthetic checkpoint on auto-pause
Smoke run 3 showed agents auto-pausing on i_am_idle (correct behavior
for non-terminal tasks) but capturing no checkpoint — panel's
Checkpoints column stayed empty. Pre-gateway parity: the auto-pause
path now writes a synthetic checkpoint summarizing state at pause-time
so the panel reflects reality.

Manual i_will_pause (G8a, deferred) will eventually let agents pass
their own checkpoint_summary; for now this synthetic write covers the
bare i_am_idle case which is what all current agents do.

Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md
section C7.
2026-05-12 05:45:31 +02:00
Renn F 1ab9ccabd8 feat(events): C6 spawn auditor on escalation/block/cancel events
The auditor's role is 'silent observer' — read every channel and emit
a reflect note when something notable happens. Smoke run 3 never
spawned auditor because no event-subscription registered it. Added
handler handle_auditor_spawn() wired to:
  - task.blocked          (EventType.TASK_BLOCKED)
  - task.cancelled        (EventType.TASK_CANCELLED)
  - task.awaiting_ceo_approval (EventType.TASK_AWAITING_CEO_APPROVAL)

Routine events (task.claimed, task.started, task.created) deliberately
do NOT trigger auditor — those are progress, not exceptions. The
auditor's container is one-shot: i_am_idle() exits after logging its
reflect note.

Auditor spawn failures are swallowed into a WARNING log so they cannot
block the underlying event's processing chain. The auditor is a silent
observer — its absence must have no side effects on the lifecycle.
2026-05-12 05:39:41 +02:00