mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
0bafbedb30aa2dccf0fb8c261103d798fe240149
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0bafbedb30 |
fix(orchestrator): auto-recover blocked parent at PM closure respawn (#177)
#170 made the closure dispatcher auto-resume a `paused` parent before respawning its PM, but only `paused`. A parent that is `blocked` at closure (every descendant already terminal) is an errant/stale block — a child's i_am_blocked propagated, or a PM blocked it and never unblocked — the real dependency is already done. #170 left it as-is, so the respawned PM landed on a blocked parent it cannot submit_up / complete and had to manually `unblock` it first (needs journal:decision) — which models do not reliably do, wedging the whole closure chain forever (observed end-to-end this run: leaf stuck awaiting_pm_review, cell parent blocked, root paused, PMs cycling indefinitely). Add `_auto_recover_blocked_parent` (mirrors `_auto_resume_paused_parent`) and recover `blocked` symmetrically to `paused` in `_maybe_spawn_pm_closure`. `blocked -> in_progress` is lifecycle-valid — it is exactly what `unblock(restore=True)` performs. Scoped to the closure-spawn point (descendants terminal) so a live dependency block is never auto-cleared. Best-effort, like the paused path. 4 new tests mirror the #170 suite (recovered-before-spawn, mutual exclusivity with paused, patch shape, error-swallowing). make quality green. |
||
|
|
caa4fc1969 |
fix(gateway): unclaim releases a pending-assigned task — escape trap (#176)
An agent assigned a `pending` task it never claimed was structurally
trapped: from pending-assigned, unclaim returned None ("cannot unclaim
from status pending"), i_am_idle rejected ("assigned but never claimed"),
i_am_blocked rejected ("block requires in_progress"). Any persistent
claim-time rejection (a gate the agent cannot satisfy, a transient
validation error) therefore looped the agent until budget-reap AND left
the task orphaned (pending, assigned, no progress). Observed in smoke-16
and smoke-17.
unclaim_for_agent now releases a pending task assigned to the caller:
no status change (already pending → no lifecycle transition, no
WorkSession to abandon since it was never claimed), just clear
assigned_to/active_claimant_id so the dispatcher can reassign. The
choreographer spec gate already permits unclaim from pending (composes=()
— role-only), so the service branch is the whole fix. Updated the now-
stale unclaim remediate string; rewrote the test that encoded the buggy
trap and added a paused-status negative case.
|
||
|
|
1d02b09fe0 |
fix(bash-guard): deny interpreter/library HTTP to internal hosts (#175)
The internal-API rule only fired when the FIRST shell token was an HTTP
CLI (curl/wget/http/https/httpie). smoke-17 showed an agent reach the
orchestrator with hand-forged X-Agent-ID/X-Agent-Role headers via:
python3 << 'EOF'
import httpx
httpx.post("http://roboco-orchestrator:8000/api/v2/flow/developer/i_will_work_on",
headers={"X-Agent-ID": "<self>", "X-Agent-Role": "developer"})
EOF
The binary is python3 (slips the CLI check) and it imports httpx, not
roboco.* (slips the #164 import check). Only minimax's wrong endpoint
path prevented a real gateway bypass under a forged identity.
Add a language-agnostic rule: deny when the command pairs an HTTP-client
token (httpx/requests/urllib/aiohttp/http.client/net::http/fetch(/
node-fetch/axios/...) with a forbidden internal host, consistent with
the curl/wget sibling (inspects full $low incl. heredoc body). External
HTTP (pypi/docs/github) has no internal host so it still passes. The
stale "interpreter one-liners — out of scope" KNOWN GAP comment is
corrected; the variable-expansion gap remains documented.
11 new tests incl. the exact smoke-17 heredoc, requests/urllib/aiohttp/
node-fetch/Net::HTTP variants, and allow-cases (external host, client
import w/o host, pytest runner). make quality green.
|
||
|
|
251d1c36a2 |
fix(mcp): flow_server i_will_work_on forwards steps — completes #172
#172 (
|
||
|
|
3d34fc2677 |
feat(progress): plan-driven progress — % derived from the plan checklist (#173)
Progress was only the synthetic milestone entry (auto-emitted at open_pr/i_am_done); agents never deliberately reported and the % was an ungated free-form guess. Now the plan's sub_tasks ARE the progress skeleton: - progress() gains optional `plan_step` (a sub_task id or its 1-based order). With it, that step is marked completed and the percentage is DERIVED as completed/total (equal weight) via new TaskService.record_plan_progress — the agent cannot set/game it. - A narrative entry WITHOUT plan_step is allowed for important mid-step documentation and carries the current derived % (the bar never regresses). No hard anti-spam gate (would loop minimax) — prompt guidance steers "meaningful moments, not every tool call". - `percentage` is now an optional fallback, used only for tasks with no sub_task checklist (back-compat). v2 ProgressRequest, the do.py route, and the do_server MCP tool updated accordingly. - An unmatched plan_step returns invalid_state listing the valid step refs (resolve by id / order / 1-based index). - developer + documenter prompts updated to the plan_step workflow. - Helpers extracted (_plan_subtasks/_derive_plan_pct/_valid_step_refs/ _mark_subtask_complete) to keep record_plan_progress within the cyclomatic gate. Commit 3 of 3 for the plan/progress quality work (#171/#172/#173). |
||
|
|
4c397e1768 |
feat(gateway): developer i_will_work_on takes a substantive step checklist (#172)
The dev plan was a free string with only a presence gate, so the executing dev had no checklist for plan-driven progress (#173). - IWillWorkOnRequest gains `steps` (same SubTask shape as a PM's sub_tasks); flow_dev route threads it through. - i_will_work_on layers steps onto the narrative plan via the same panel-shaped path PMs use, so task.plan.sub_tasks is populated (panel render + #173 progress). - New _dev_steps_gate (mirrors _pm_sub_tasks_gate, runs after the spec gate): a developer FRESH claim must supply a non-empty steps list with every description >= _PM_SUBTASK_DESC_MIN_LEN. Re-entry/recovery short-circuit before the gate (extracted _dev_reentry + _fresh_dev_claim keep i_will_work_on within the return-count + cyclomatic gates). - developer role prompt: steps template + "thin steps rejected" + the progress(plan_step=...) handoff. - Updated every dev-fresh-claim test fixture across the suite to pass substantive steps; added dedicated _dev_steps_gate coverage. Commit 2 of 3 for the plan/progress quality work (#171/#172/#173). |
||
|
|
ed828a719b |
feat(gateway): substantive-plan gate — approach >=150 + real sub_task descriptions (#171)
Plans were vague because the gate accepted the bare minimum: PM approach >=20 chars and title-only sub_tasks. minimax wrote exactly the minimum. - IWillPlanRequest.approach min_length 20 -> 150 (kept in sync with _PM_APPROACH_MIN_LEN; the gate enforces it at the choreographer layer too so direct/MCP callers can't bypass the HTTP boundary). - New _thin_subtask_hint: every PM sub_task must have a title and a description >= _PM_SUBTASK_DESC_MIN_LEN (60) saying what the step does — each sub_task is both a delegate target AND a progress-checklist item, so a title alone is not a plan. - cell_pm/main_pm role prompts: explicit "the gate REJECTS thin plans" framing + concrete sub_task example + the new minimums. - Updated all affected test fixtures across the suite to use substantive approaches/descriptions; added thin-sub_task rejection coverage. Commit 1 of 3 for the plan/progress quality work (#171/#172/#173). |
||
|
|
e94159dce8 |
fix(orchestrator): auto-resume paused parent before PM closure respawn (#170)
A PM auto-pauses its owned parent on i_am_idle (by design — so the
closure dispatcher knows to respawn it when subtasks finish).
Pre-gateway the parent was resumed at respawn so the PM landed
actionable; the gateway refactor dropped that, so the respawned PM had
to issue resume() itself. minimax reliably failed to (called resume on
the leaf / unblock on the paused root), wedging smoke-15 — the leaf
stayed awaiting_pm_review and the chain never completed.
Restore the pre-gateway behaviour: _maybe_spawn_pm_closure now calls
new _auto_resume_paused_parent (paused -> in_progress via the same
PATCH path _auto_block_task uses) immediately before spawning the PM,
but only when the parent is actually `paused` (awaiting_pm_review /
in_progress parents untouched). Best-effort: a resume failure is
logged and swallowed so it never blocks the spawn (the PM can still
resume manually). The parent stays assigned to the PM, so it lands on
its own in_progress task able to submit_up / complete / escalate
directly — no reliance on the weak model issuing resume().
Combined with
|
||
|
|
4090397cea |
fix(gateway): rejected PM is told the exact complete() call (#170, partial)
Smoke-15 wedge: leaf 1533ce56 sat at awaiting_pm_review owned by
be-pm, but the PMs looped firing complete/unblock at the wrong
(parent) task_ids — every rejection was generic ("not assigned to
you" / "not ready for completion"), so minimax never discovered it
just needed `complete(1533ce56)`.
New _own_review_hint: on a cell_pm/main_pm complete-guard rejection
(not-owner, wrong-state, or main-pm-on-non-root), if the PM owns a
DIFFERENT task that is awaiting_pm_review, append a remediate suffix
naming it and the exact `complete(task_id='<id>', notes='...')` call.
Best-effort (never raises into the rejection path), pure guidance —
no control-flow or state-machine change.
Scope: this is the bounded, low-risk slice of #170 (fix b). The
parent-state corruption + missing recovery transition (root->paused /
cell->blocked from earlier mis-targeted verbs, fix a/c) is a lifecycle
state-machine change deferred for explicit design alignment — tracked
in #170.
|
||
|
|
0737cc0143 |
fix(git): authenticate diff-path fetches so QA's diff base is current (#168)
Smoke-15: QA's claim_review diff was `origin/master...origin/<branch>`
but origin/master in QA's clone was the STALE clone-time tip
(
|
||
|
|
12672b94ed |
fix(docs): i_documented persists DocRef dicts, not bare strings (#169)
Smoke-15: be-doc i_documented(files=["README.md"]) → choreographer
doc.py stamped `existing.documents = files` (a list[str]) onto
Task.documents. Task.documents is list[DocRef] persisted as dicts —
list_docs does DocRef(**d), _get_existing_doc_ref does d.get("path"),
the RAG indexer does d.get("path"). A bare string 500'd GET /docs
("TypeError: DocRef() argument after ** must be a mapping, not str")
during be-pm's PR review and would AttributeError the indexer.
Fix at source: new _doc_refs_for builds proper DocRef dicts
(path, title=filename, doc_type, created_by/at, updated_by/at) at the
i_documented stamp. Defensive read: new _coerce_doc_ref tolerates
dict / DocRef / bare-string / rejects unknown — applied at
list_docs and _get_existing_doc_ref so legacy/corrupted rows can't
500. _add_doc_to_task (docs-route write path, not the gateway flow
that broke) left as-is per scope.
|
||
|
|
8158eb37ef | unpinning claude clode version | ||
|
|
38dba74837 |
fix(agents): stop instructing agents to ToolSearch built-in tools (#167)
The system-prompt directive layer and the briefing block both opened
with "FIRST ACTION REQUIRED: run ToolSearch to activate deferred
Edit/Write". That premise is false: per Claude Code 2.1.114, ToolSearch
gates only deferred MCP tools, never built-ins — and it is not even a
callable tool in the agent runtime. Built-ins are loaded at spawn via
the `--tools` flag and gated solely by the per-role permission rules
(the actual Edit/Write breakage was the global Write(*)/Edit(*) deny +
single-slash path, fixed in
|
||
|
|
c0ba335470 |
fix(runtime): agents can finally Edit/Write — drop global deny + fix abs path syntax (#167)
Smoke-10..14: every agent (developers included) got "Edit exists but is not enabled in this context" and fell back to destructive bash redirection (a 207-line README rewritten to a 3-line stub, which QA correctly failed). Two coordinated defects in _generate_agent_settings / _get_role_permissions: 1. base_deny carried a GLOBAL Write(*)/Edit(*). Claude Code evaluates permission rules deny -> ask -> allow, first match wins — a deny ALWAYS beats a more-specific allow and the glob syntax has no negation. So the global deny unconditionally shadowed every per-role workspace-scoped Write/Edit allow. Removed it; the security denies that legitimately rely on deny-always-wins (Bash(git:*), credential Read denies, curl github, env) stay. Roles that must not author (qa, cell_pm, main_pm, auditor) keep their OWN Write(*)/Edit(*) deny. 2. The workspace allow used a single leading slash (Write(/data/...)). Claude Code resolves a single / against the settings.json project root, not the container filesystem root, so the allow silently never matched even without defect #1. Emit the // absolute-filesystem form. defaultMode stays bypassPermissions (switching to dontAsk would require re-deriving the full allow-list and risks wedging agents elsewhere — out of scope). Verified against Claude Code 2.1.114 permission docs. |
||
|
|
954acff911 |
fix(git): resolve diff HEAD ref per-workspace so QA/doc/PM see real diffs (#161 facet)
Smoke-14: QA's claim_review evidence had pr_diff_summary="" and files_changed=[] on a PR with a real README change. Root cause: diff() and list_changed_files() diffed against the bare local <branch_name>. That ref only exists in the clone where the dev ran `git checkout -b` at claim. QA / documenter / PM inspect from their OWN clones, where a bare <branch_name> resolves refs/heads then refs/remotes/<name> but NEVER refs/remotes/origin/<name> — so `git diff base...<branch>` had an unresolvable HEAD and silently returned an empty diff (run with check=False). #161 previously fixed the BASE side (cell-PM parent never pushed → fall back to default branch). This is the symmetric HEAD-side facet. open_pr pushes the leaf branch, so origin/<branch> is the workspace-independent source of truth. New _resolve_head_ref fetches the branch and prefers the local branch (dev's own clone, unchanged behaviour), falling back to origin/<branch> (QA/doc/PM clones), then the bare name so the command stays well-formed. diff() and list_changed_files() route through it; explicit base (incremental dev path, base=HEAD~1) is preserved. |
||
|
|
1605d187f1 |
fix(security): bash-guard git-ops check inspects commands, not file content (#165)
The git network/auth deny rule matched its regex against the whole command string, so heredoc bodies and echo/printf arguments that merely documented git verbs (a README, a notes file) were treated as git invocations and denied. This wedged smoke-13's dev: after wiping the README via an Edit/Write fallback it could not restore it because every `cat > README.md << EOF ... git commit ... EOF` was blocked. The git-ops check now runs against a skeleton of the command with heredoc bodies and echo/printf literal args stripped (both are data the shell writes, never executed). Quoted args to a shell interpreter (`bash -c "... && git fetch"`) ARE executed, are not echo/printf/heredoc bodies, and so survive untouched — the hook's core purpose is preserved. A sentinel prefix distinguishes a legitimately-empty skeleton from a python failure (fail closed on failure). All other rules, including the #164 import-bypass rule, still inspect the full command. |
||
|
|
81f5655d48 |
fix(security): block gateway-internals import + agent-id forgery (#164)
Smoke-12: be-dev-1 (minimax-m2.7) bypassed the entire MCP boundary by
running `uv run python3 -c "import os;
os.environ['ROBOCO_AGENT_ID']='...'; from roboco.mcp.flow_server
import open_pr; open_pr(...)"` from the Bash tool. This voided the
per-role tool manifest (role-scoping is meaningless if the agent can
import any server module in-process), forged agent identity via an
env-var rewrite, and ran choreographer code outside the gateway's
tracing + auth.
bash-guard-hook.sh now adds two deny rules:
1. Any python/uv/poetry/pipenv/pdm/hatch invocation that imports or
`-m`-runs roboco.* internals (mcp/services/runtime/foundation/
api/enforcement). The whole command string — heredoc body
included — is matched, so quoting/heredoc forms are covered.
2. Any assignment or export of ROBOCO_AGENT_ID (identity forgery).
Reading roboco source for context (cat/grep) is still allowed — the
block is on *executing* internals, not viewing them. Normal python
one-liners without roboco imports still pass.
19 bash-guard tests pass (10 prior + 9 new). Note: takes effect on
agent-image rebuild (hook ships in the agent container).
|
||
|
|
c18ad34530 |
fix(gateway): reject PM-created documentation subtasks (#163)
Smoke-12: be-pm delegated TWO subtasks under one cell parent — a code subtask (be-dev-1, spawned) and a documentation subtask (be-dev-2). The orchestrator dev-dispatch refuses to spawn a developer for task_type=documentation, so the doc subtask became a permanent orphan that loops dev-dispatch forever and would deadlock submit_up (all subtasks must be terminal). The spine-cap is per-type so code + documentation both passed sibling-dedup — the PM never saw the anti-pattern warning. _delegate_static_guards now rejects task_type='documentation' with a remediate explaining the lifecycle auto-creates the documentation phase (awaiting_documentation → documenter spawned) after the code subtask passes QA, and that the PM should delegate ONLY the code subtask. |
||
|
|
aa2e6bc5ed |
fix: panel logo (#160), diff base fallback (#161), doc branch checkout (#162)
#160 — panel /roboco-logo.png "received null": next/image optimizer fails for static public assets in Next.js standalone mode. Added `unoptimized` to the sidebar logo Image so it serves the static file directly (validated on panel rebuild). #161 — QA/doc evidence pr_diff_summary empty: A leaf dev branch's parent_branch_for is the cell-PM branch, which is never pushed (only devs push their leaf branch). diff against a non-existent origin/<parent> returned empty. Added GitService._resolve_diff_base + _default_branch_ref + _ref_exists: diff/list_changed_files fall back to the repo default branch (origin/HEAD → master/main) when origin/<parent> is absent. #162 — claim_doc_task BRANCH_MISMATCH loop: The documenter's clone is separate from the dev's; the task branch already existed (dev created it) so no checkout ran in the doc workspace — roboco_docs_write / commit failed BRANCH_MISMATCH and the doc looped. Fixes: (a) new GitService.checkout_branch_in_agent_workspace; claim_doc_task checks out the task branch into the doc clone (best-effort — a checkout hiccup never fails the claim). (b) BRANCH_MISMATCH remediate now lists all four role claim verbs (i_will_work_on / i_will_plan / claim_doc_task / claim_review). (d) give_me_work next-hint is role+status aware via _claim_verb_hint (doc→claim_doc_task, qa→claim_review, pm→i_will_plan, else dev). Facet (c) (i_am_blocked "Not Found" for doc) was only reachable via the stuck-without-checkout path; primary fix removes it. Smoke-11 reached dev→QA→doc (deepest ever) and validated the prior 6 fixes (panel flood gone, #158/#159/#157 confirmed). These three clear the doc-phase blockers found in that run. |
||
|
|
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
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
0c60d0bf7d | ++ | ||
|
|
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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
|
||
|
|
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. |
||
|
|
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
|
||
|
|
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.
|
||
|
|
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". |
||
|
|
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.
|
||
|
|
3171e1e192 | ++ | ||
|
|
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. |
||
|
|
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. |
||
|
|
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). |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
7a9ab1945f | style: ruff format leftover line-wrap fixups from CCN refactors | ||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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', ...) |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
f38c15b966 |
feat(gateway): C5 write acceptance_criteria_status on i_am_done
Pre-gateway parity. evidence(task_id).acceptance_criteria_status was
always [] because the gateway's i_am_done gate validated each
criterion against the dev's journal:reflect but didn't persist the
per-criterion verdict. The panel + audit log couldn't show
per-criterion checkmarks.
Now the gate writes a list of {criterion, addressed, artifact_ref,
checked_at} entries to task.acceptance_criteria_status. The existing
matching logic surfaces which artifact (commit sha / reflect-note)
addressed each criterion; entries that aren't addressed get
addressed=False so the panel can flag them.
Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md
section C5.
|
||
|
|
b53d8fe194 |
feat(gateway): C4 auto-create WorkSession on claim
Pre-gateway parity. Smoke run 3 showed task.work_session_id null on every task — the choreographer's claim/plan/start path didn't create the row that downstream subsystems (panel, PR tracking, merge chain) need to track agent-per-task git activity. Add TaskService.ensure_work_session(task_id, agent_id) as a public wrapper around the existing _create_work_session_if_needed logic. Role restriction lifted to None so both developers and PMs get a session (pre-gateway always created sessions for all claimants). Built-in re-entry guard prevents duplicate rows on re-claim. Wire the call into both _claim_plan_start_run and _resume_from_claimed immediately before _touch, so every successful in_progress transition (including the stuck-claimed recovery path) creates the row. Spec ref: Wave C task C4 (2026-05-12). |
||
|
|
a47237416e |
feat(runtime): C3 tunable reaper threshold + heartbeat on every verb dispatch
Smoke run 3 showed agents reaped at the 3-min stale-claim window while they were actively retrying rejected verbs. Two causes: 1. The reaper threshold was hardcoded at 180s via claim_stale_seconds. LLM inference + retry loops routinely take longer than that between verb-successes. Added settings.stale_claim_reap_seconds (default 600s); override via ROBOCO_STALE_CLAIM_REAP_SECONDS env var. claim_stale_seconds (spawn-filter cutoff) is unchanged at 180s. 2. last_heartbeat_at only refreshed on verb SUCCESS. A verb stuck in a rejection loop (e.g. tracing_gap missing journal:decision) showed no heartbeat updates even though the agent was alive. Added a best-effort heartbeat refresh inside _emit_rejection so EVERY verb dispatch — success or rejection — counts as activity. Heartbeat approach: option (b) — touch inside _emit_rejection (single centralized rejection path). Requires no middleware layer, no HTTP body parsing, and no new files. The _touch guard for task_id=None means agent-level rejections (no task context) are a safe no-op. Net effect: agents stop being reaped mid-retry. Genuinely-stuck containers (no verb dispatch at all) still reap normally at 600s. Spec ref: Wave C Task C3. |
||
|
|
eb9cd93e09 |
fix(workspace): C2 cache refresh fetch for 30s per workspace path
Smoke run 3 fired 'ensure_workspace: refresh fetch returned non-zero' 9 times per run because each evidence(task_id) call triggered ensure_workspace -> fetch. The workspace doesn't change in subseconds. Added a 30s TTL cache keyed by workspace path. ensure_workspace(force=True) bypasses the cache for callers that genuinely need a fresh fetch. Net effect: log noise drops from 9 entries to 1-2 per run; orchestrator spends less time waiting on redundant git fetches. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section C2. |
||
|
|
cdb4a6edeb |
fix(mcp): C1 per-verb circuit breaker trips on incomplete_input too
Smoke run 3 showed Main PM hitting 7 incomplete_input rejections on the decision-note required-fields gate before finally succeeding. The per-verb breaker tracks repeated rejections of the same verb in a 60s window and returns circuit_open after the 3rd strike — but its classification set only included tracing_gap. incomplete_input was added in Wave 1 (pre-gateway parity for decision/reflect structured fields) and should have been added to the breaker at the same time. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section C1. |
||
|
|
d73e86044b |
fix(gateway): B6 give_me_work returns pre-assigned pending tasks first
Smoke run 3 showed Main PM's first give_me_work() returning
{status: idle, next: 'no Main PM work'} even though c7935d2c was
pending and assigned to Main PM. The filter only walked
list_assigned_for_agent (ordered by priority/updated_at — pending
could rank behind in_progress rows) and the PM path fell through
to idle because the pre-assigned pending case was not checked first.
Pre-pended a list_pending_for_agent check in both give_me_work and
pm_give_me_work: tasks where assigned_to=agent_id AND status=pending
take priority over all other lookups. Added TaskService.list_pending_for_agent
for the query (ordered by sequence, priority, created_at).
Updated existing tests in test_choreographer_dev, test_choreographer_pm_extras,
and test_heartbeat_wired to set list_pending_for_agent.return_value=[]
where they were not testing the pre-assigned path.
Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md
section B6.
|
||
|
|
85e20e6a2a |
fix(docker): B5 tighten bash-guard denial message to 2 lines
Smoke run 3 showed the bash-guard hook emitting 8+ lines on every blocked shell-git op — enumerating every alternative MCP verb across roboco-flow / roboco-do / roboco-git-readonly. That's repeated token spend on every refused retry; the LLM doesn't need the full alt-list inline, it has the role prompt + the MCP tool schema for that. Trimmed to 2 lines: denial reason + a one-line pointer to the role's State→Verb table. Test asserts <= 3 echo lines in any denial block. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section B5. |
||
|
|
6550d69b75 |
fix(gateway): B4 decision/reflect remediate includes literal call example
Smoke run 3 showed Main PM taking 7 attempts to satisfy the decision-note required-fields contract — the remediate listed which fields were missing but didn't show what a fully-formed call looks like. The LLM pattern-matches examples better than field-list prose; each retry it dropped a different field. Added a literal note(scope='decision', ...) / note(scope='reflect', ...) call template to the rejection remediate so the agent sees the canonical shape with named-keyword args and example values. The missing-fields list stays — both pieces of information are useful, but the example is what actually drives convergence. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section B4. |
||
|
|
ce92829385 |
fix: clear 55 pre-existing test failures uncovered after Wave A landed
Three classes of failure, all surfaced once Wave A's plan-required gate and the migration 013 went in. Per project standing rule: pre-existing errors are not a free pass — fix them. 1. Wave A1 ordering (32 lifecycle parity failures + 1 full-pipe test). _pm_sub_tasks_gate fired BEFORE _claim_plan_start_gate, so wrong-state PMs got `incomplete_input` (the gate's verdict) when the spec's lifecycle gate should have returned `invalid_state` first. Swapped: re-entry check → spec lifecycle gate → sub_tasks gate → claim_plan_run. Parity test now sees the spec's verdict as expected. 2. E2 enum naming (2 migration_013 failures + ripple). _str_enum in roboco/db/tables.py didn't pass name=… to SQLAlchemy Enum(...), so Base.metadata.create_all in test setup inferred `role` from the Python class `Role` while the alembic migrations declare `agentrole`. Tests saw two enums for the same class and hit `agentrole = role` operator errors. Fixed: default name to lower(class_name) (matches every migration), override `Role` → `agentrole`. One dict entry; no class-by-class registration needed. 3. _MockContentActions.note() signature drift. Wave 2 G4 added `structured` kwarg to ContentActions.note(). The integration mock at tests/integration/v2/test_full_pending_to_completed.py didn't accept the new kwarg → 1 test failed on the very first call from the v2 do/note route. Added `structured: object = None` and left it unused (the test asserts lifecycle, not journal rendering). Plus three ruff E501 line-length fixes in the test files I touched. Quality: ruff + mypy clean. pytest 6690 passed / 0 failed / 274 skipped. |
||
|
|
f680db34c6 |
fix(gateway): B3 canonical say() return status — always 'posted'
Smoke run 3 showed inconsistent return strings — main-pm got status='sent', be-pm got status='posted' for the same verb. Confirmed say() already returns 'posted' at its sole success exit. Added test_say_status.py to pin the canonical past-tense pattern (note->'noted', say->'posted', notify_ack->'acked') and prevent regression. dm() and notify() retain 'sent' — different verbs, different semantics. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section B3. |
||
|
|
eed4551497 |
feat(alembic): B2 drop unused pm_approvals Task column
Smoke run analysis initially flagged three Task fields as unused (pm_approvals, quick_context, proactive_context). A follow-up audit found quick_context (stores original_developer marker + doc notes + PR creator + escalation notes) and proactive_context (RAG injection) are actively used. Only pm_approvals is truly orphaned. Migration 014 drops pm_approvals; downgrade() recreates it if ever needed. The two false-positive fields stay untouched. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section B2 (re-scoped 2026-05-12). |
||
|
|
7254ceee50 |
fix(docker): B1 update shell hooks to gateway verb names
Smoke run 3 showed stop-hook.sh complaining 'Denied: you stopped without calling a terminal tool' AFTER agents successfully called i_am_idle() — because the hook listed 9 pre-gateway verb names (roboco_agent_idle, roboco_task_substitute, etc.) that no longer exist. Same staleness in bash-guard-hook.sh. Both hooks now reference current gateway verbs only. stop-hook branches its suggestion by ROBOCO_AGENT_ROLE so devs see i_am_done/i_am_blocked, QAs see pass/fail, PMs see complete/escalate_up. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section B1. |
||
|
|
fadc05966e |
feat(alembic): A5 migration 013 drops stray role postgres enum
Smoke run 2 (2026-05-11) produced 'UndefinedFunctionError: operator does not exist: agentrole = role' because postgres had two enums (role, agentrole) for the same Python class. Information_schema check confirms no column uses role; migration drops it. Upgrade() raises if that ever stops being true. Downgrade() recreates the enum with the foundation's Role values. Investigation of WHY a second enum got created is tracked in spec E2; this migration handles the symptom. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section A5. |
||
|
|
d5a40086f4 |
fix(workspace): A4 downgrade expected refresh-fetch auth-fail to DEBUG
Smoke run 3 fired the same workspace.py warning ~9x per run: 'ensure_workspace: refresh fetch returned non-zero' stderr: 'fatal: could not read Username for https://github.com' This is EXPECTED behavior, not a bug. The docstring on _fetch_origin_best_effort explains that credentials are deliberately scrubbed from .git/config after the initial clone (part of the secret- exfiltration mitigation) and refresh fetches are best-effort. For private repos the auth-fail is the documented outcome. The original A4 spec proposed re-injecting the PAT -- that would have violated _assert_no_pat_leak and the URL-scrub mitigation. Re-scoped to: silence the known-benign signature at DEBUG, keep WARNING for genuine failures (network errors, broken remotes, repo-not-found). No behavior change. No security boundary touched. Just log level. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md A4 (re-scoped 2026-05-12 after investigation showed the original spec proposed reintroducing a documented security regression). |
||
|
|
10be97fd5a |
refactor(orchestrator): A2+A3 follow-ups — extract workspace-path helpers
Fixes 2 important + 1 minor issue from the code-quality review of
|
||
|
|
5adb4ff272 |
fix(orchestrator): A2+A3 set agent container cwd to workspace path
Smoke run 3 surfaced two bugs that share a root cause:
- Edit(/app/README.md) → 'Edit exists but is not enabled in this context'
- commit(files=['/app/README.md']) → 'outside repository at <workspace>'
Both happened because the container's WORKDIR is /app (roboco package
source) while the agent's task workspace is bind-mounted at
/data/workspaces/<project>/<team>/<agent>/. The Dev role's
Edit/Write permission allowlist scopes to the workspace, so any Edit
call from /app fails the path match.
Adds '-w {workspace_path}' to the docker run command so the container
starts with cwd = task workspace. Edit(README.md) and git add README.md
now resolve inside the workspace clone.
Mirrors _get_role_permissions path selection exactly:
- developer / product_owner / head_marketing: per-agent workspace
- documenter: cell workspace (matches its Write/Edit allowlist)
- qa / cell_pm / main_pm / auditor: omit -w, fall back to /app
Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md
sections A2 + A3 (re-scoped per investigation 2026-05-12).
|
||
|
|
cfb7424c80 |
fix(gateway): A1 review-fixes — re-entry ordering, gate unit-coverage, approach check
Three fixes from the code-quality review of
|
||
|
|
a1009c05e8 |
feat(gateway): A1 plan-required-at-claim gate
i_will_plan now requires approach (min_length=20) at the schema and non-empty sub_tasks at the gateway when the caller is a PM role. Restores pre-gateway parity for _validate_claimed_start — agents could not transition claimed -> in_progress without filling the rich plan. Smoke run 3 (2026-05-11) showed PMs calling i_will_plan with just plan='paragraph' and the gateway accepting it; Plan tab stayed empty because no agent filled approach/sub_tasks/risks/open_questions. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section A1. |
||
|
|
62d1084a0c |
fix(gateway): notify_list/get/ack call NotificationDeliveryService (not Service)
Wave 1 wired notify_list/get/ack into ContentActions but pointed them at `self.notifications` (which is NotificationService — sender side, with send_blocker_notification / send_qa_ready_notification / etc.). The read methods (list_for_agent, get_for_recipient_and_mark_read, acknowledge) live on `NotificationDeliveryService` instead. Smoke run 2026-05-11 surfaced this immediately: AttributeError: 'NotificationService' object has no attribute 'list_for_agent' Fixes: - roboco/api/deps.py — import NotificationDeliveryService and wire it in as a new ContentActionsDeps field `notification_delivery`. - roboco/services/gateway/content_actions.py — add notification_delivery to ContentActionsDeps (Optional with `None` default for back-compat with any tests that don't supply it). Point notify_list, notify_get, notify_ack at self._deps.notification_delivery. - tests/unit/gateway/test_content_actions.py — _make_deps adds a default AsyncMock for notification_delivery so existing tests continue to pass. Quality: ruff + mypy clean. 505 unit tests pass. |
||
|
|
dc9c49e1e4 |
feat(gateway): G8 part b — typed blocker_type + what_needed on i_am_blocked
Pre-gateway parity (G8 part b of the 2026-05-11 design). The pre-gateway TaskBlockInput at 254cc93:roboco/mcp/schemas/__init__.py required blocker_type (external|internal|question|dependency) and what_needed so PMs could triage their inbox by class. Current i_am_blocked dropped both fields — every blocked task looked the same to the PM. Now i_am_blocked accepts both as optional kwargs: - Back-compat: callers that omit them still work (blocker_type defaults to None → rendered as flat reason in the struggle entry). - New: when supplied, the struggle journal entry body is structured markdown (## Blocker Type / ## What Needed sections) so the panel's journal view renders named blocks instead of one flat sentence. Validator on blocker_type enforces the enum at the Pydantic boundary with a clear "must be one of: ..." error if the agent invents a value (same pattern as the Wave 3 G7 validators). G8 part a — typed `pause(checkpoint_summary, remaining_work)` — defers. That gap needs a new IntentSpec in foundation/policy/lifecycle.py (currently pause is an ActionSpec only; agents auto-pause via i_am_idle) plus checkpoint wiring through TaskService.add_checkpoint. Material work, deferred until after the user has deployed and verified G7 + G8b lands cleanly. Wired: - roboco/api/schemas/v2/flow.py — IAmBlockedRequest gains optional blocker_type + what_needed; @field_validator enforces the enum - roboco/api/routes/v2/flow_dev.py — passes the new fields through - roboco/services/gateway/choreographer/_impl.py — i_am_blocked signature + structured struggle-entry rendering - roboco/mcp/flow_server.py — typed wrapper with the kwargs - agents/prompts/roles/developer.md — updated verb table - tests/unit/mcp_servers/test_flow_server.py — updated to expect the new optional kwargs as None when omitted Quality: ruff + mypy clean. 505 tests pass. |
||
|
|
bd52e3d0c3 |
fix(schemas): pre-gateway-style cross-field validators on DelegateRequest
Pre-gateway parity for G7 of the 2026-05-11 design. The pre-gateway
TaskCreateInput at 254cc93:roboco/mcp/schemas/__init__.py:210-235 had
@field_validator hooks that caught the most common LLM-vs-schema
confusions with helpful "did you mean X?" hints. Those validators were
lost in the gateway refactor.
Three validators added to DelegateRequest:
- estimated_complexity: rejects ints (some agents send 1/2/3 thinking
it's a priority), enforces enum {low|medium|high|critical}. Hint
steers them to drop priority (which isn't a delegate parameter).
- nature: rejects invented values like the 2026-05-11 'standard'
regression. Enum is {technical|non_technical}. Hint explicitly cites
the regression so the LLM knows why this is enforced.
- task_type: rejects invented task_type values. Enum is {code,
documentation, research, planning, design, administrative}.
Fail-fast at the Pydantic boundary returns a 422 with the structured
hint inline, so the agent loops a single retry instead of leaking a
TaskCompletenessError up the stack.
Existing tests in tests/unit/api/routes/v2/test_flow_*.py used
"nature": "feature" — a value that the gateway's TaskNature enum
never accepted, so it would have been rejected at completeness check
anyway. Updated both to "technical".
Spec ref: docs/superpowers/specs/2026-05-11-pre-gateway-parity-design.md
|
||
|
|
72e01a7f13 |
feat(gateway): Wave 2 pre-gateway parity — structured note, sub_tasks, channels
Three Wave 2 gaps from the 2026-05-11 pre-gateway parity design:
G4 — note() decision/reflect now require structured fields at the gateway
(pre-gateway `Field(...)` parity). Returns `incomplete_input` envelope
with field-by-field hints when any required field is missing.
- decision: context (str), options (list[{name,pros,cons}] min len 2),
chosen (str), rationale (str). `consequences` and `next_steps` are
now list[str] (was str). Renderer emits each option as a "### Name
+ Pros / Cons" block instead of a bullet — matches the pre-gateway
DecisionOption sub-shape exposed in `roboco/mcp/schemas/__init__.py`
at `254cc93`.
- reflect: what_done, what_learned, what_struggled (each non-empty
str). next_steps stays optional.
- Bumped tests/unit/gateway/test_content_actions.py with explicit
pass-with-N-options coverage (≥2 floor; 3-option case green).
G5 — i_will_plan now persists sub_tasks alongside approach / risks /
open_questions / technical_considerations. The Plan tab's Sub-Tasks
section was empty because the verb didn't accept the field. Choreographer
server-assigns id + order to each sub_task (pre-gateway build_plan_data
parity) and normalizes every list entry to the EXACT shape
`panel/src/types/index.ts::TaskPlan` consumes:
- SubTask: {id, title, description, completed:false, order,
estimated_hours:null, notes:null}
- Risk: {description, mitigation, severity:null} — accepts the
{risk, mitigation} pre-gateway shape too
- OpenQuestion: {question, answer:null, answered_by:null,
answered_at:null} — accepts a bare string fallback
The normalization lives in three small module-level helpers
(_normalize_sub_task / _normalize_risk / _normalize_open_question)
called from _build_panel_shaped_plan, keeping i_will_plan's branch
count under PLR0912.
G6 — new `channels()` verb returns the agent's readable + writable
channel slugs from foundation.policy.communications. Stops invented
slugs ("backend-dev", "backend") that we kept seeing in smoke runs.
Added to every role's manifest including auditor (read-only access).
Wired through:
- roboco/api/schemas/v2/do.py — list-typed consequences/next_steps,
dict-typed options, ChannelsRequest
- roboco/api/schemas/v2/flow.py — IWillPlanRequest.sub_tasks
- roboco/api/routes/v2/do.py — /channels endpoint
- roboco/api/routes/v2/flow_*.py — pass sub_tasks through
- roboco/services/gateway/content_actions.py — channels() method;
_check_scope_required_fields enforces decision/reflect structure;
_render_option_block emits per-option markdown blocks
- roboco/services/gateway/choreographer/_impl.py — _build_panel_shaped_plan
helper used by i_will_plan
- roboco/services/gateway/role_config.py — _CHANNEL_DISCOVERY tuple
on every role
- roboco/mcp/do_server.py — channels() tool + note() signature with
options as list[dict[str,str]]
- roboco/mcp/flow_server.py — i_will_plan signature with sub_tasks
Frontend: no code change. panel/src/types/index.ts already declares
the exact shape we now write; panel/src/components/tasks/task-detail/
{tab-plan,tab-progress,tab-sessions,tab-notes}.tsx already reads it.
The empty panels we observed were a backend write-side problem, not
a frontend read-side problem — Wave 1 + Wave 2 close it.
Quality: ruff + mypy clean. 505 unit tests pass (added 2 new tests on
decision-scope requirements, updated 3 existing tests to fit the
pre-gateway-parity contract).
Spec ref: docs/superpowers/specs/2026-05-11-pre-gateway-parity-design.md
|
||
|
|
8408d761ca |
feat(gateway): Wave 1 pre-gateway parity — sessions, progress, notify inbox
Closes empty-panel complaints (Sessions, Progress) and the i_am_idle notification-inbox deadlock identified in the 2026-05-11 gap analysis. All backend service methods already exist; this is pure MCP-surface widening on top of the existing choreographer + ContentActions. New MCP tools (roboco-do): - progress(task_id, message, percentage) — Progress tab writer - open_session(task_id, channel, topic, ...) — Sessions tab writer (PM+) - link_session(session_id, task_id, ...) — Idempotent task↔session - notify_list(unread_only, pending_ack_only, limit) - notify_get(notification_id) - notify_ack(notification_id) Wired through: - roboco/api/schemas/v2/do.py — six new request schemas with Field constraints (Progress.percentage: ge=0, le=100; OpenSession.topic: max_length=200; etc.) - roboco/api/routes/v2/do.py — six new POST routes, thin dispatchers - roboco/services/gateway/content_actions.py — six new ContentActions methods forwarding to TaskService.add_progress, MessagingService.create_session_for_tasks /link_session_to_task, NotificationDeliveryService.list_for_agent / get_for_ recipient_and_mark_read / acknowledge - roboco/mcp/do_server.py — six new typed tool wrappers + registered in _TOOLS - roboco/services/gateway/role_config.py — receivers (list/get/ack) added to every role except auditor (who gets list/get, no ack). Session verbs to PM-or-up. Progress to dev + doc. - agents/prompts/roles/*.md — verb tables updated for developer / QA / documenter / cell_pm / main_pm. i_am_idle line points to notify_list as the deadlock resolution path. Authorization: - progress: assignee + active status (in_progress / verifying / awaiting_qa / awaiting_documentation) - open_session: cell_pm / main_pm / product_owner / head_marketing / ceo - link_session: caller must own the task - notify_ack: caller must be a recipient (ValueError from service maps to not_authorized envelope) Per-file ignore extended: - roboco/services/gateway/**/*.py = [PLC0415, PLR0913] — same rationale as roboco/mcp/**: typed verb signatures are the agent-facing contract; bundling into dataclasses hides field-level schema the LLM needs at the tool layer. Quality: ruff + mypy clean. 503 unit tests pass on touched surfaces. Spec ref: docs/superpowers/specs/2026-05-11-pre-gateway-parity-design.md |
||
|
|
60bd9b175d |
fix(gateway): tighten sibling-dedup to cap spine-type concurrency
Smoke run 2026-05-11 (3rd attempt) caught the runaway-decomposition
pattern again, but with TWO different dev assignees so the old
same-assignee-same-type check missed it. Cell PM split one workflow
into "Execute Git Workflow: Branch, Edit, Commit, Push" (be-dev-1)
+ "Create PR with Task ID Linked to Parent Task" (be-dev-2), then
respawned and added a 3rd ("Commit and push smoke test change",
planning) — five tasks for what should be one dev hop.
The cell_pm.md prompt already forbids this pattern. The agents ignore
it. So we add the rule at the gate:
Rule 1 (spine-type concurrency cap): for task_type ∈
{code, planning, documentation}, a parent may have at most ONE
non-terminal subtask of that type — regardless of assignee. These
types are the spine of the lifecycle (dev → QA → doc → PM); the
chain is sequential and there's no merge story for parallel
siblings of the same spine type. PM must complete the existing
child first, or restructure into independent parents.
Rule 2 (same-assignee fallback): unchanged behavior for non-spine
types (research / design / administrative) — same assignee +
same type still rejects.
Error message names the existing sibling id so the PM doesn't
need to query separately, and remediate suggests either
"complete the existing one" or "split parent into two parents
for genuinely parallel work".
Quality: ruff + mypy clean, 417 unit tests pass.
|
||
|
|
6369184b72 |
docs(prompts): mark i_will_work_on plan param as required, not optional
Smoke run showed be-dev-1 repeatedly calling `i_will_work_on(task_id)` without `plan`, hitting `tracing_gap missing=['plan']` and retrying with the same payload. Root cause: prompt's verb table showed the signature as `plan=None` (optional default) while the gateway requires plan on every claim — including first claim. The dev followed the signature line, missed the workflow-table line that pairs it with `plan='...'`. Tightening the signature to `plan` (no default), explicit "REQUIRED even on first claim" callout, and a note that resume calls use `plan='resume: <next step>'`. |
||
|
|
92badfe6a0 |
docs(prompts): teach all roles the structured verb shapes (pre-gateway parity)
Counterpart to |
||
|
|
bcc748c8a3 |
fix: restore pre-gateway structured verb surfaces (5 fixes)
Smoke run 2026-05-11 showed five regressions stemming from the gateway consolidating multiple typed endpoints into thin verbs with collapsed signatures. The choreography is fine; the verb signatures lost the structured shape that pre-gateway forced agents to fill. Each fix restores a structured surface so the LLM's tool schema again carries the constraints that prevent the observed bugs. A) do_server: list valid channel slugs in say()/dm() docstrings. Stops invented channels (`backend`, `backend-dev`) — the LLM now sees the closed set in the tool schema. B) choreographer: add _delegate_sibling_dedup_guard. Rejects a delegate that would create a non-terminal sibling with the same assigned_to + task_type under the same parent — the dupe shape observed on smoke (Main PM creating two planning tasks for be-pm; Cell PM creating two code tasks for be-dev-1). C) choreographer: extend _validate_assignee_task_type to all roles. Devs may only get code|documentation|research (not planning/design/ administrative). QA gets code only. Documenters get documentation only. Catches the misroute observed on smoke (Cell PM gave be-dev-2 a 'research' coordination task that should have stayed with the PM). D) i_will_plan: thread approach / technical_considerations / risks / open_questions from MCP through to TaskService.set_plan as a TaskPlan-shaped dict. Empty default keeps back-compat. Panel's Plan tab now renders Approach / Sub-Tasks / Technical Considerations / Risks / Open Questions instead of an empty pane. E) note(): scope-specific structured fields restored. For 'decision' scope: context, options[], chosen, rationale, consequences. For 'reflect' scope: what_done, what_learned, what_struggled, next_steps. Rendered as markdown sections into the journal entry content so the Decisions and Reflections views show named blocks instead of a one-line phrase. Pre-gateway parity. Files changed: - roboco/mcp/do_server.py (A, E) - roboco/mcp/flow_server.py (D) - roboco/services/gateway/choreographer/_impl.py (B, C, D) - roboco/services/gateway/content_actions.py (E) - roboco/api/schemas/v2/flow.py (D) - roboco/api/schemas/v2/do.py (E) - roboco/api/routes/v2/flow_main_pm.py (D) - roboco/api/routes/v2/flow_cell_pm.py (D) - roboco/api/routes/v2/do.py (E) Quality: ruff + mypy clean. 89 unit tests pass on the touched surfaces. |
||
|
|
229797ffe3 |
fix: unblock smoke run (gateway envelope + alembic + redis + MCP)
Four bugs surfaced by the 2026-05-11 smoke run, all on the path from Main PM's first delegate to the cell PM accepting a subtask: - gateway: TaskCompletenessError from _create_subtask_from_inputs leaked through Starlette as a 500; agents retried in a tight loop because they never saw field_hints. Wrap the call in _create_subtask_and_envelope, catch the error, return Envelope.incomplete_input with the interrogation-pattern reply the upfront completeness check produces. - alembic: migration 012 used a 40-char revision id which exceeds alembic_version.version_num varchar(32). Upgrade fell back to create_all on every boot, silently skipping the migration. Rename to 012_align_agentrole_foundation (30 chars). File rename + revision string. - events/stream_bus: external Redis FLUSHALL while orchestrator is running (e.g. reset_runtime_state.sh) drops the consumer group; the listen loop then spams NOGROUP every block-cycle forever. Catch ResponseError with NOGROUP in the message and rebootstrap the group via _ensure_consumer_group, then continue. Self-heals without restart. - mcp/flow_server: delegate took body: dict with no schema, so the LLM invented values like nature='standard' and the SDK threw 'unhashable type: dict' on nested args. Flatten to typed top-level parameters with docstring listing valid enum values for team / task_type / nature / estimated_complexity. PLR0913 per-file ignore added for roboco/mcp/** because MCP tool signatures ARE the LLM contract — bundling into a dataclass would hide the enum hints that prevent the invention bug. |
||
|
|
207aaecd72 |
Feature: lifecycle canonical spec (#14)
* chore: clean make quality baseline on feature/lifecycle-canonical-spec
Three classes of pre-existing issues blocking `make quality`:
1. Alembic migrations 002/009/011 used runtime introspection
(op.get_bind() + inspect / bind.execute) without guarding for
offline (--sql) mode. `alembic upgrade head --sql` is part of
`make quality`; in offline mode `op.get_bind()` returns a
MockConnection with no inspection system, so the migrations
crashed before emitting their SQL stubs. Each migration now
short-circuits or simplifies in `context.is_offline_mode()` —
live-DB behavior is unchanged.
2. ruff format drift on three files left over from prior in-flight
edits (choreographer/_impl.py, content_actions.py, and one test
file). `ruff format` applied.
3. vulture flagged two unused `tb` parameters in async __aexit__
stubs in test_task_service_lifecycle_misc.py. The parameter is
protocol-required but unused by the body — renamed to `_tb`
(vulture treats underscore-prefixed names as intentionally unused).
`make quality` is now green from this branch's HEAD; subsequent
lifecycle-spec work can use it as the per-task gate.
* feat(lifecycle): canonical spec package + Role/Status/TaskType enums
Foundation for the canonical lifecycle/permissions module. Enums
mirror docs/internal/old/workflows/STATUS_TRANSITIONS.md +
PERMISSIONS.md. Tests pin enum membership against both the
predecessor canon and roboco.models.base.TaskType.
* feat(lifecycle): Decision dataclass with allow/reject/tracing_gap constructors
Single rejection shape every consumer maps to its native format
(Envelope, HTTP code, prompt hint). __post_init__ enforces the
allowed/rejection_kind invariants so a malformed Decision can't reach
a consumer.
* fix(lifecycle): tighten Decision invariants per Task 2 review
Two reviewer findings on the Task 2 Decision dataclass, addressed
in one commit:
1. The docstring promised `allowed=True ⇒ rejection_kind is None
AND missing == [] AND remediate is None`, but __post_init__ only
checked the rejection_kind half. A caller could construct an
allow-shaped Decision with stale missing/remediate fields and
sneak it past validation. Tighten __post_init__ to enforce the
full invariant. Add a regression test.
2. tracing_gap defensively copies the missing list (`list(missing)`)
to isolate the stored list from later caller-side mutation, but
no test pinned this. Add a regression test that mutates the source
list after construction and asserts the stored list is unchanged.
Issue 2 from the same review (mutable list vs tuple for `missing`)
is a broader design call deferred until consumers exist; the
defensive copy is sufficient until then.
* feat(lifecycle): Precondition/ActionSpec/IntentSpec/StatusTransition dataclasses
The four dataclasses that hold the canonical tables. ActionSpec and
StatusTransition are direct ports of pre-gateway PERMISSIONS.md +
STATUS_TRANSITIONS.md rows. IntentSpec is the gateway-only addition:
each gateway intent verb declares which atomic actions it composes.
* feat(lifecycle): _STATUS_TRANSITIONS table + STATUS_GRAPH view
Direct port of STATUS_TRANSITIONS.md. Every transition records its
trigger action and (optionally) a role constraint. STATUS_GRAPH is
the precomputed source→{targets} view callers use for reachability
checks.
* fix(lifecycle): pin role_constraint values + clarify Task-5 handoff
Two reviewer findings on Task 4 _STATUS_TRANSITIONS, addressed in
one commit:
1. The original Task-4 tests verified (source, target) pairs but
not role_constraint contents. A typo in a single role name (e.g.
forgetting MAIN_PM from escalate_to_ceo) would have slipped past
them silently. Add test_status_transitions_role_constraints_match_canon
pinning every non-None constraint and the cancel-block invariant.
2. role_constraint=None on the `claim` rows from PENDING and
NEEDS_REVISION was load-bearing — it is the explicit handoff
point between the StatusTransition table (state machine layer)
and CLAIM_RULES (per-role claim authority, lands in Task 5).
The original inline comment said this in passing; expand it so
the design choice is unmissable for a stranger reading just
spec.py.
* feat(lifecycle): _ATOMIC_ACTIONS + CLAIM_RULES + ROLE_TEAM_RULES tables
Direct port of PERMISSIONS.md. Every task management tool gets an
ActionSpec with allowed_roles, source_statuses, target_status,
self_review_block, and needs_team_match flags. CLAIM_RULES maps each
Role to the statuses they can claim from. ROLE_TEAM_RULES is the
per-slug team restriction.
* fix(lifecycle): tighten ActionSpec contracts per Task 5 review
Three reviewer findings on Task 5's _ATOMIC_ACTIONS table, addressed
in one commit:
1. set_plan.source_statuses widened to {CLAIMED, IN_PROGRESS} but
every existing caller (i_will_work_on / i_will_plan compositions)
runs set_plan while CLAIMED, between claim and start. Narrow to
{CLAIMED} only. If a future "edit plan mid-flight" feature lands,
widen explicitly with test coverage at that time.
2. needs_team_match was set True only on claim/qa_pass/qa_fail/
docs_complete. Defense-in-depth says every role-scoped task
action should re-assert team match (don't rely on the inheritance
chain through assigned_to alone). Flip to True on: start,
set_plan, block, pause, submit_verification, submit_qa,
submit_pm_review, complete, create_subtask. Leave False on
board/CEO actions and PM cross-cell interventions (unblock,
resume, cancel) where the cross-cell semantics are intentional.
3. claim.source_statuses is intentionally a SUPERSET of any single
role's CLAIM_RULES allowance (the table holds the union; CLAIM_RULES
holds the per-role authority). Add an inline comment above the
claim ActionSpec so a future reader doesn't conclude the two
tables disagree — they don't, they encode overlapping facts at
different grains.
* feat(lifecycle): _INTENT_VERBS table — every gateway verb declared
Each gateway intent verb is now a named composition of atomic actions
plus optional side effects. i_will_work_on = (claim, set_plan, start);
i_am_done = (submit_verification, submit_qa); open_pr is pure side
effects (push_branch, create_pr); etc.
* fix(lifecycle): widen block.allowed_roles to include QA + Documenter
Task 6 review caught a role-set inconsistency: i_am_blocked.allowed_roles
admits dev/QA/doc, but the underlying block.allowed_roles only allowed
dev+PM. Result: a QA or documenter calling i_am_blocked would pass the
IntentSpec gate and then be rejected by the composed ActionSpec gate
when Task 7 wires can_invoke_intent.
Widen block to include QA + Documenter. The semantic case is sound: a
QA reviewing a task can discover an external blocker; a documenter
writing docs may need PM intervention. Predecessor PERMISSIONS.md
restricted block to dev+PM, but with the gateway exposing i_am_blocked
to all worker roles, the underlying atomic must agree.
The deeper unclaim/escalate_up "imperative verb" concern from the same
review (composes=() but mutates state) is deferred to Task 8 where the
validator design lands.
* feat(lifecycle): public lookup functions + Context + preconditions
can_claim, can_invoke_action, can_invoke_intent, valid_next_verbs,
composed_actions_for, intents_for_role, status_after — the entire
public surface every consumer will use. Context carries the
caller-supplied state preconditions need (plan, journal-decision
flag, etc.). Preconditions for plan/commits/no_pr/ownership are
declared once and wired into the relevant IntentSpecs.
* fix(lifecycle): wire PRECONDITION_OWNERSHIP through Context.actor_id
Task 7 review found _p_owns_task reads agent.id but every call site
passes None for the agent arg. Result: getattr(None, "id", object())
returns a fresh sentinel, task.assigned_to == <sentinel> is always
False, and open_pr / i_am_done would reject every owner the moment
Task 9 wires consumers.
Fix: thread identity through Context.actor_id (new UUID field) and
rewrite _p_owns_task to read from the context. Both call sites already
pass the Context — no signature changes elsewhere. Add green-path
test exercising the owner-can-open-pr case the existing tests
missed (the Task 7 plan only tested precondition-failure paths,
which masked the bug).
Plus surface hygiene: STATUS_GRAPH, CLAIM_RULES, ROLE_TEAM_RULES,
and the four PRECONDITION_* constants are now in
roboco.lifecycle.__init__.__all__ so consumers in Tasks 8/9 don't
depend on the implicit `from roboco.lifecycle.spec import ...`
backdoor.
* feat(lifecycle): import-time self-consistency validators
10 validators run at module import; first failure raises
LifecycleSpecError and prevents the package from loading. Covers
status enum coverage, reachability, terminal exits, intent
compositions, status chain consistency, claim-rule role/status
coverage, self-review symmetry, team-rule slug existence, and
StatusTransition action references.
* fix(lifecycle): close validator gaps; resolve BACKLOG-claim and submit_qa IN_PROGRESS-shortcut ambiguity
Three reviewer follow-ups on Task 8's _validate.py, plus two real
data corrections the new action-target-reachability validator
surfaced.
1. Design spec §9 calls for "every ActionSpec.target_status, when
set, is reachable from each source_status via STATUS_GRAPH" —
missing from Task 8's 10 validators. Add
_check_action_target_reachable_from_source.
2. _check_role_team_rules_slugs verified slug existence in
AGENT_UUIDS but NOT that the cell team in ROLE_TEAM_RULES
matches the seed. Add _check_role_team_rules_team_match,
scoped to non-None entries only — None means "exempt from
team-match enforcement" (cross-cell roles), not "no team in
org chart".
3. test_validators_pass_on_real_spec was ceremonial. Add
test_run_all_validators_raises_on_unknown_intent_action,
a deliberate-break regression that monkeypatches _INTENT_VERBS
to inject a fake action and asserts LifecycleSpecError raises.
The new action-target-reachability validator caught two real
data inconsistencies between the predecessor canon docs and the
spec tables:
A. claim.source_statuses listed BACKLOG and CLAIM_RULES[*PM]
listed BACKLOG, but STATUS_GRAPH[BACKLOG] = {PENDING, CANCELLED}
only. Resolution: PMs use the explicit \`activate\` action to
move BACKLOG → PENDING, then claim from PENDING. Drop BACKLOG
from claim.source_statuses and CLAIM_RULES.
B. submit_qa.source_statuses listed IN_PROGRESS, but
STATUS_GRAPH[IN_PROGRESS] does NOT include AWAITING_QA. The
intent verb i_am_done composes (submit_verification, submit_qa)
which forces IN_PROGRESS → VERIFYING → AWAITING_QA — no
shortcut. Drop the stale IN_PROGRESS entry from
submit_qa.source_statuses.
Both corrections tighten the canonical state machine to a strict
no-skip transition graph. Pre-gateway PERMISSIONS.md/STATUS_TRANSITIONS.md
disagreements are resolved here; spec.py is the canon now.
* feat(gateway): Envelope.from_decision maps lifecycle Decisions to envelopes
Single shape adapter so verb bodies stop hand-composing rejection
envelopes. Each rejection_kind maps to a specific envelope flavor;
'self_review' folds into 'not_authorized' with a parenthetical hint;
constructing from an allow Decision raises (programmer error).
* feat(gateway): VerbRunner for atomic composed-action dispatch
Wraps spec.composed_actions_for(intent) in session.begin_nested()
so mid-sequence failures roll the DB back. Side effects run AFTER
the savepoint commits. Each atomic action name dispatches to a
TaskService method via a single, exhaustive _dispatch_atomic
mapping. New verbs slot in by adding an IntentSpec entry + a
_dispatch_atomic case if a new atomic is needed.
* refactor(gateway): i_will_work_on uses spec.can_invoke_intent + VerbRunner
Replace the bespoke status-branch dispatcher in i_will_work_on with the
spec-driven flow: load task -> load agent -> build spec.Context ->
spec.can_invoke_intent (and spec.can_claim for per-role status authority)
-> Envelope.from_decision on rejection -> VerbRunner.run_intent on success.
The _i_will_work_on_pending, _i_will_work_on_claimed,
_i_will_work_on_needs_revision, and _start_failed_envelope helpers are
removed; the runner replaces them. Two narrow verb-body re-entry blocks
remain for behaviors the spec does not yet model:
1. in_progress + same agent -> idempotent heartbeat-only return
2. claimed + same agent -> _resume_from_claimed (set_plan + start)
to recover from a stuck mid-claim crash without re-running claim
against a state the spec excludes.
The behavioral claim guards (already_active / paused / sibling_sequence)
also stay imperative for now -- they're not in the spec yet and migrate
into spec.extra_preconditions in a later task. Per-role claim authority
is enforced via spec.can_claim because the atomic claim action's
source_statuses are the union across roles; CLAIM_RULES narrows.
Parity test in tests/lifecycle/test_consumer_parity.py runs the verb
against every (role x status x task_type='code') combo (112 rows) and
asserts the envelope error matches the spec's Decision (or can_claim's
Decision when the intent gate passes but per-role claim authority does
not). This is the contract that makes spec/verb drift impossible.
Existing tests updated where rejection-message text changed (the spec
now produces the messages, e.g. "role 'cell_pm' may not call
'i_will_work_on'" instead of "PM cannot execute code") or where the
spec's stricter view ("invalid_state" -> "not_authorized" for a dev
trying to claim awaiting_qa) is more accurate. Test fixtures were
updated to wire task.session.begin_nested as a proper async context
manager (required by VerbRunner) and to set agent_for().id so runner-
driven calls line up with assert_awaited_with(task_id, agent_id).
* refactor(lifecycle): push CLAIM_RULES enforcement into can_invoke_action
Task 11's i_will_work_on migration had to call spec.can_claim()
separately after spec.can_invoke_intent() because the claim action's
source_statuses is the union across all claim-eligible roles —
can_invoke_intent alone would let a developer pass for claiming
awaiting_qa (a QA-only state).
The retrofit pattern would repeat in every claim-composing verb
(i_will_plan, claim_review, claim_doc_task). Push the per-role
narrowing inside can_invoke_action when the action is "claim",
using the same not_authorized vs invalid_state disambiguation
can_claim already implemented (status-reserved-for-another-role
returns not_authorized; status-no-role-can-claim returns
invalid_state). Extracted the body to _check_claim_rules_narrow
to keep can_invoke_action under xenon's complexity threshold.
Update _i_will_work_on_gate to drop the redundant spec.can_claim
call. Update test_consumer_parity.py to assert only against
can_invoke_intent's Decision.
Tasks 12-22 will inherit the cleaner pattern: spec.can_invoke_intent
is the single gate; verb bodies don't need per-action retrofits.
* refactor(gateway): i_will_plan uses spec.can_invoke_intent + VerbRunner
Migrates i_will_plan to the spec-driven pattern Task 11 set up for
i_will_work_on. The verb body now: (1) loads task + agent, (2) builds
Context, (3) checks idempotent/recovery re-entry, (4) calls
spec.can_invoke_intent, (5) returns Envelope.from_decision on
rejection, (6) delegates composition to VerbRunner. The
_i_will_plan_* helpers are removed — the runner replaces them.
Parity test in tests/lifecycle/test_consumer_parity.py runs the verb
against every (role × status × task_type) combo and asserts the
envelope matches spec.Decision.
* refactor(gateway): delegate uses spec.can_invoke_intent for role/state gate
Migrates delegate to the spec-driven role/state gate. The chain
validation (main_pm->cell_pm, cell_pm->its team's devs), the
assignee-vs-task_type rule (Cell PMs receive planning-typed only),
the enum coercion, and the parent-lifecycle/cap guards STAY in the
verb body — they encode delegate-specific semantics the spec
doesn't model.
Parity test in tests/lifecycle/test_consumer_parity.py asserts the
spec's role+state rejection is correctly surfaced. Chain/assignee
rejections continue to be tested in test_choreographer_pm_extras.
* refactor(gateway): open_pr uses spec.can_invoke_intent + VerbRunner
Migrates open_pr to spec-driven gating. The spec's
extra_preconditions (PRECONDITION_OWNERSHIP, PRECONDITION_COMMITS,
PRECONDITION_NO_PR) handle all three precondition checks; the verb
body delegates side-effect dispatch (push_branch, create_pr) to
VerbRunner.
Idempotent re-entry retained: an open_pr call against a task that
already has a PR (and the caller owns it) returns OK without
re-opening, rather than the tracing_gap the spec would otherwise
produce. This preserves agent ergonomics — two calls in a row
shouldn't surface a misleading "no_prior_pr" hint.
Parity test in tests/lifecycle/test_consumer_parity.py runs the verb
against representative (status x commits x pr_number) combos and
asserts the envelope matches spec.Decision.
* refactor(gateway): i_am_done uses spec.can_invoke_intent + VerbRunner
Migrates i_am_done to spec-driven gating. The spec's
extra_preconditions (PRECONDITION_OWNERSHIP, PRECONDITION_COMMITS)
handle ownership and commit-count checks; VerbRunner dispatches
the (submit_verification, submit_qa) atomic chain.
The tracing-gate preconditions (progress entry, journal:reflect,
acceptance criteria) and the field-level submit-qa gates stay in
the verb body — they model gates the spec doesn't yet cover.
Defense-in-depth: those gates run after the spec accepts the
ownership/commits checks.
Parity test in tests/lifecycle/test_consumer_parity.py runs the
verb against (role × status × ownership × commits) and asserts
the envelope matches spec.Decision.
* refactor(gateway): i_am_blocked uses spec.can_invoke_intent + VerbRunner
Migrates i_am_blocked to spec-driven gating. The journal:struggle
write stays in the verb body (it's a side effect outside the
lifecycle action). VerbRunner dispatches the `block` atomic action
via task_service.escalate.
Parity test in tests/lifecycle/test_consumer_parity.py.
* refactor(gateway): unclaim and resume use spec.can_invoke_intent
Migrates both verbs to the spec-driven gate. unclaim's verb body
keeps its dispatch (task.unclaim_for_agent) because composes=();
resume goes through VerbRunner with composes=("resume",).
The reassignment-rejection branch (introduced in
|
||
|
|
091e4076a2 |
fix(gateway): reject Cell-PM-assigned subtasks that aren't task_type=planning
Bug B from the 2026-05-09 smoke run. main-pm called delegate(assigned_to='be-pm', task_type='code'). The chain validator let it through (be-pm IS in main-pm's allowed targets), the schema let it through (task_type='code' is a valid enum value), and the subtask got created mis-typed. Task 0 made it cosmetically work because PMs can now plan code-typed parents — but the model is wrong: a Cell PM owns the PLANNING of the slice; the code execution is what they delegate to devs. New gate in _delegate_static_guards: when assignee is a Cell PM (be-pm/fe-pm/ux-pm), task_type MUST be 'planning'. Returns invalid_state with a remediate hint pointing at the right type. Devs are unrestricted (could be code OR documentation, depending on the slice). Tests: 3139 passing (+ 2 regression tests pinning the rule), 100% coverage, ruff clean. |
||
|
|
73e1e96851 | Many fixes and cleanups |