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

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

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

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

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

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

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

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

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

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

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

Auditor spawn failures are swallowed into a WARNING log so they cannot
block the underlying event's processing chain. The auditor is a silent
observer — its absence must have no side effects on the lifecycle.
2026-05-12 05:39:41 +02:00
Renn F 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.
2026-05-12 05:32:25 +02:00
Renn F 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).
2026-05-12 05:22:44 +02:00
Renn F 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.
2026-05-12 05:10:32 +02:00
Renn F 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.
2026-05-12 05:03:12 +02:00
Renn F 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.
2026-05-12 04:52:38 +02:00
Renn F 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.
2026-05-12 04:44:03 +02:00
Renn F 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.
2026-05-12 04:37:01 +02:00
Renn F 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.
2026-05-12 04:35:13 +02:00
Renn F 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.
2026-05-12 04:22:03 +02:00
Renn F 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.
2026-05-12 04:05:44 +02:00
Renn F 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).
2026-05-12 04:03:21 +02:00
Renn F 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.
2026-05-12 03:48:47 +02:00
Renn F 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.
2026-05-12 03:39:17 +02:00
Renn F 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).
2026-05-12 03:35:13 +02:00
Renn F 10be97fd5a refactor(orchestrator): A2+A3 follow-ups — extract workspace-path helpers
Fixes 2 important + 1 minor issue from the code-quality review of 5adb4ff:

1. Formula duplication: the workspace path string was inlined at two
   sites in orchestrator.py (the canonical _prepare_agent_spawn and the
   new _build_mount_args -w logic). Extracted to module-level helpers
   _agent_workspace_path(project, team, agent_id) and
   _cell_workspace_path(project, team) so both callers share the same
   formula. Future path changes only land in one place.

   Also extracted _resolve_project_slug_from_git_context() as the
   module-level counterpart to the instance method, called by the static
   _build_mount_args site that cannot access self.

2. Test consistency: test_workdir_matches_edit_allowlist_path now
   extracts the Edit(<prefix>/**) value from _get_role_permissions and
   asserts the spawn cmd's -w value equals that prefix. The test would
   actually catch a drift where _build_mount_args and _get_role_permissions
   use different formulas — previously it just compared two copies of
   the same string.

3. Test coverage: added test cases for product_owner and head_marketing
   spawns (both share the per-agent workspace path), so all roles that
   _get_role_permissions distinguishes are covered.

Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md
A2+A3 (re-scoped 2026-05-12).
2026-05-12 03:13:11 +02:00
Renn F 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).
2026-05-12 03:03:17 +02:00
Renn F cfb7424c80 fix(gateway): A1 review-fixes — re-entry ordering, gate unit-coverage, approach check
Three fixes from the code-quality review of a1009c0:

1. Critical: _pm_sub_tasks_gate ran before _handle_pm_reentry, breaking
   idempotent re-entry for PMs whose containers crashed mid-run. Moved
   the gate to after the re-entry short-circuit so initial-claim is the
   only path that hits the gate.

2. Critical: gate had no direct unit test (the HTTP-layer test mocked
   the choreographer). Added tests/unit/gateway/test_i_will_plan_sub_tasks_gate.py
   with six tests: empty sub_tasks → incomplete_input, missing rich_plan
   → incomplete_input, filled sub_tasks → gate passes, developer with
   empty sub_tasks → gate passes (devs don't decompose), sub_tasks filled
   but approach empty → incomplete_input, in_progress re-entry short-
   circuits before gate even with no sub_tasks.

3. Important: approach was only enforced at the HTTP Pydantic boundary.
   Direct service-layer callers (MCP, test fixtures, orchestrator-
   internal) could persist a plan with no approach. Gate now also checks
   approach >= 20 chars (_PM_APPROACH_MIN_LEN constant) and includes it
   in the rejection's missing list when absent.

Plus: stale docstring on i_will_plan corrected; _handle_pm_reentry
docstring rewritten to lead with the domain reason (re-entry contracts)
not the PLR0911 linter justification; unused pytest import removed from
test_i_will_plan_rich_required.py; pre-existing PLR2004/PLC0415 issues
in that file fixed.
2026-05-12 02:45:48 +02:00
Renn F 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.
2026-05-12 02:30:00 +02:00
Renn F 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.
2026-05-11 08:41:46 +02:00
Renn F 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.
2026-05-11 06:05:02 +02:00
Renn F 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
2026-05-11 06:00:58 +02:00
Renn F 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
2026-05-11 05:57:57 +02:00
Renn F 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
2026-05-11 05:43:01 +02:00
Renn F 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.
2026-05-11 05:03:06 +02:00
Renn F 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>'`.
2026-05-11 04:59:03 +02:00
Renn F 92badfe6a0 docs(prompts): teach all roles the structured verb shapes (pre-gateway parity)
Counterpart to bcc748c. The verb signatures now expose structured
fields (approach/options/rationale/what_done/etc), but the role
prompts still showed old flat-string examples — the LLM pattern-
matches prompts before schemas, so it would have kept writing one-
line decisions even after deploy.

Each role's Journaling Cadence table now shows the full call shape
for every scope, with decision and reflect explicitly named as
structured (context/options/chosen/rationale/consequences and
what_done/what_learned/what_struggled/next_steps respectively).

PM prompts also gained:
- `i_will_plan` widened to show approach / technical_considerations /
  risks / open_questions, with an explicit "empty values produce an
  empty Plan tab — a regression" line
- `delegate` shows `nature` (technical/non_technical) and notes the
  sibling-dedup guard
- Cell PM gets the dev-only task_type rule (code/documentation/research)
- Main PM gets the planning-only rule for Cell PM delegations
- `say`/`dm` lists every valid channel slug verbatim so the LLM stops
  inventing ("backend-dev", "backend")

Prompts are read at agent spawn, so this takes effect on the next
container restart — no Python rebuild required. Combined with bcc748c
this is the full pre-gateway-parity restoration.
2026-05-11 04:30:36 +02:00
Renn F 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.
2026-05-11 03:45:09 +02:00
Renn F 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.
2026-05-11 02:46:54 +02:00