Commit Graph
100 Commits
Author SHA1 Message Date
Renn F d819c28893 fix(gateway): make i_will_work_on plan-precondition atomic; recover claimed-no-plan
Bug A from the 2026-05-09 smoke run. be-dev-1 called i_will_work_on
without `plan` on a pending task; claim() ran first (transitioned to
`claimed`), then the plan check failed → tracing_gap. The natural
retry path then dispatched to `_i_will_work_on_claimed`, which had no
plan-recovery logic and called start() against a still-plan-less task,
returning `start failed` forever. The dev kept looping; the parent
escalated up; the whole slice ended `blocked`.

Two changes (Task-5 atomicity pattern applied to i_will_work_on):

1. `_i_will_work_on_pending`: move the plan precondition BEFORE
   `claim()`. A missing-plan first call now returns tracing_gap with
   the task untouched in `pending`, so the agent's retry-with-plan
   succeeds cleanly.

2. `_i_will_work_on_claimed`: now accepts `plan` and calls set_plan
   before start() if the task has no plan yet. Recovery path for any
   already-stuck task (e.g. left over from the earlier image, or an
   orchestrator restart that left a partial claim).

Also wires `plan` through the dispatcher to the claimed branch.

Tests: 3137 passing (3135 + 2 regression tests pinning the atomic
invariant), 100% coverage, ruff clean.
2026-05-09 03:15:02 +02:00
Renn F b601441da7 fix(audit): record actor's actual role from agents.role at write time
The 2026-05-08 trace caught an audit row with actor=main-pm but
agent_role=cell_pm — the caller had supplied the verb's *expected*
role rather than the actor's actual role. Forensics work that joined
audit_log on agent_role would silently miscategorize the row.

Fix: AuditService now reads the actor's role directly from
agents.role at write time via the new _resolve_actor_role_from_db
helper. Wired into log_task_action_denial,
log_state_transition_denial, and log_notification_denial. The
caller-supplied role param is kept as a best-effort fallback for the
case where the DB lookup fails (singleton-without-DB paths,
permission errors, etc.) so audit writes never block the operation
being audited.

Coverage:
  - 3 unit tests (test_audit.py) for the no-DB / invalid-id paths
  - 1 unit-with-real-DB test (test_audit_real_query.py) verifying
    the persisted row's agent_role is read from DB even when the
    caller passes a deliberately-wrong role
  - 1 unit-with-real-DB test for the no-row case

Tests: 3135 passing, 100% coverage, ruff clean.
2026-05-08 12:18:00 +02:00
Renn F 19f27b4f88 fix(gateway): improve unclaim/resume rejection when task was reassigned
Investigation finding for Task 6 of the gateway introspection plan:
the 2026-05-08 trace's "not your claim" rejection at 02:51:22 / 02:52:48
was NOT caused by a UUID-comparator bug. AGENT_UUIDS in
roboco/seeds/initial_data.py are static so identity is stable across
restarts, and SQLAlchemy + Pydantic both round-trip UUIDs cleanly
(pinned by two new regression tests in test_task_service_misc.py).

The actual cause: the task was REASSIGNED out from under main-pm by
an upstream verb between when main-pm last touched it and when it
tried to unclaim/resume. Common triggers:
  - cell_pm_complete propagates up via _maybe_advance_parent_to_pm_review,
    which reassigns the parent to the cell PM for the team
  - main_pm_complete clears assigned_to to None (CEO acts via UI)
  - unblock with restore=True flips assigned_to back to pre_block_state

Pre-fix the rejection said only "not your claim" — agents can't tell
whether they hit a transient race or whether the task was legitimately
moved on. Fix: surface the current_owner UUID and hint that an
upstream verb did this, telling the agent to call give_me_work() to
find its current work.
2026-05-08 12:03:37 +02:00
Renn F 6806516015 refactor(gateway): rename submit_for_qa to open_pr; pin atomic preconditions
Pre-fix, submit_for_qa opened a PR (side effect) and returned OK with
next='call i_am_done' — agents read the verb name, assumed they were
done with QA handoff, never called i_am_done, and PRs ended up
orphaned (PR #12 in the 2026-05-08 trace).

Two changes:

1. Rename submit_for_qa -> open_pr so the verb name matches the
   semantic. The PR opens here; the actual QA handoff happens at
   i_am_done. Renamed across:
   - choreographer/_impl.py (method)
   - mcp/flow_server.py (tool registration + _TOOLS dict)
   - api/routes/v2/flow_dev.py (route + handler)
   - api/schemas/v2/flow.py (OpenPrRequest)
   - services/gateway/verb_gates.py (_STATE_VERBS)
   - services/gateway/role_config.py (developer flow manifest)
   - services/gateway/content_actions.py (commit-success next= hint)
   - agent_sdk/server.py (post-tool guidance map)
   - runtime/orchestrator.py (developer prompt)
   - agents/prompts/{base,roles/developer,_generated/*}.md
   - tests/unit/gateway/test_submit_for_qa.py -> test_open_pr.py
   - tests/unit/api/routes/v2/test_flow_dev.py
   - tests/unit/gateway/test_verb_gates.py
   - tests/unit/api/test_correlation_id.py
   - tests/unit/mcp_servers/test_flow_server.py
   - tests/integration/test_full_lifecycle_real_db.py

2. New regression test (test_open_pr_does_not_create_pr_if_no_commits)
   pins the atomic invariant: preconditions (assignee, commits,
   no-prior-PR) must be checked BEFORE git.create_pr/push_branch run.
   Any future re-ordering breaks the test.

Tests: 3128 passing (3127 + 1 new), 100% coverage, ruff clean.

Note: TaskService.submit_for_qa() (the v1-layer service method) is
INTENTIONALLY not renamed — it's a different layer used by the v1
routes. The rename here is only the gateway verb surface.
2026-05-08 11:54:31 +02:00
Renn F 2eeefb2ee1 refactor(gateway): consolidate commit + notify role gates into verb_gates
Replaces hardcoded role-string-constants in content_actions.py
(_COMMIT_ALLOWED_ROLES, _NOTIFY_ALLOWED_ROLES) with calls into
verb_gates.is_verb_allowed against a synthetic in-progress task probe.
Pre-fix the same role lists lived in both content_actions and
verb_gates; if one drifted the other would mask it. Now there's one
table.

Adds `notify` to verb_gates._ALWAYS_AVAILABLE for cell_pm, main_pm,
product_owner, head_marketing.

Note: i_will_plan / delegate role checks INTENTIONALLY stay as
explicit `role not in (cell_pm, main_pm)` checks, not is_verb_allowed.
Their state checks must surface as `invalid_state` (a different
agent-side error code) — conflating them with role-state combo
checks breaks the rejection-code semantics agents rely on.

Tests: 3127 passing, 100% coverage, ruff clean.
2026-05-08 11:42:04 +02:00
Renn F ebdbd7fc47 feat(gateway): wire envelope introspection into qa.py + doc.py role mixins
Closes the Task 3 wiring loop: every Envelope construction site in the
QA and Documenter mixins now stamps current_state + valid_next_verbs.
Refactored doc._check_i_documented_inputs to take the loaded task as a
parameter so it can pass through to .with_introspection() without
re-fetching.

Task 3 of the 2026-05-08 gateway introspection plan is now complete
across _impl.py, qa.py, and doc.py.
2026-05-08 11:27:54 +02:00
Renn F 17630bf3fc feat(gateway): wire envelope introspection into i_am_blocked, unclaim, resume,
escalate_up, escalate_to_ceo, submit_up, unblock

Continues Task 3 of the gateway introspection plan. Every developer-
and PM-facing lifecycle verb in _impl.py now stamps current_state +
valid_next_verbs on both the success path and (where the task was
loaded successfully) on rejection paths.

The qa.py / doc.py role mixins still need wiring; that's a follow-up
since their structure mirrors what's already been done here.
2026-05-08 08:25:32 +02:00
Renn F 6092df59b2 feat(gateway): wire envelope introspection into complete + main_pm/cell_pm complete
Trace-driven priority: the 2026-05-08 audit log showed PMs spamming
`complete` against tasks in the wrong status (claimed/in_progress
rather than awaiting_pm_review). Introspection on the rejection path
now tells the PM the actual current_state and which verbs are valid
right now (typically `delegate` / `escalate_up`), shrinking the
trial-and-error loop.

Continues Task 3 of the gateway introspection plan.
2026-05-08 08:18:40 +02:00
Renn F 56fbb97a80 feat(gateway): wire envelope introspection into submit_for_qa, i_am_done
submit_for_qa: ok envelopes (PR-already-open + new-PR) and
not-assigned / no-commits rejections now stamp current_state +
valid_next_verbs.

i_am_done: not-assigned rejection plus tracing-gap and field-gate
rejections all stamp introspection. Success path inherits via the
shared _build_i_am_done_ok helper.

Continues Task 3 of the gateway introspection plan.
2026-05-08 08:15:10 +02:00
Renn F 93c66fa07a feat(gateway): wire envelope introspection into give_me_work, i_will_plan, delegate
Continues Task 3 of the gateway introspection plan: every successful
or rejected envelope from these three verbs now carries current_state +
valid_next_verbs sourced from verb_gates.valid_next_verbs(role, task).

Remaining verbs (submit_for_qa, i_am_done, i_am_blocked, unclaim,
resume, unblock, complete, main_pm_complete, escalate_up,
escalate_to_ceo, submit_up, plus the qa.py / doc.py role-mixins) to be
wired in subsequent commits — the choke-point pattern at each call
site is `.with_introspection(task=t, role=role)` on the constructed
Envelope, so it stays mechanical from here.
2026-05-08 08:09:16 +02:00
Renn F fd344511a5 feat(gateway): add Envelope introspection (current_state + valid_next_verbs)
Pre-fix, agents had no way to introspect what verbs were valid from a
task's current state — the 2026-05-08 trace showed them spamming
escalate_to_ceo/complete/unblock/resume against a `claimed` task and
racking up rejections. Pre-gateway agents could ground reasoning in
VALID_TRANSITIONS[status] from a doc; the gateway hid that.

Now every Envelope carries:
  - current_state: the task's status string (or None for tool-discovery
    envelopes that aren't task-bound)
  - valid_next_verbs: the verbs the caller can usefully call right now,
    sourced from verb_gates.valid_next_verbs(role, task)

Wired into i_will_work_on (highest-traffic verb) for both the OK path
and the wrong-state rejection path. Remaining lifecycle verbs to be
wired in subsequent commits (Task 3.9).
2026-05-08 08:03:32 +02:00
Renn F b4ec19ca9c feat(gateway): add verb_gates single source of truth for role x state
Pre-2026-05-08, role checks lived in three places that could disagree
silently:
  1. roboco/services/gateway/role_config.py — verb allow-list per
     role (used by spawn manifest)
  2. roboco/services/gateway/claim_guards.py — pm_cannot_execute_code,
     role_typed_claim
  3. Choreographer string constants in _impl.py / qa.py / doc.py /
     content_actions.py

verb_gates.valid_next_verbs(role, task) collapses them into one
declarative table mapping (role, task_status) -> tuple of valid verbs,
plus a per-role set of always-available verbs. Will be wired into
Envelope.valid_next_verbs in the next task so agents stop
trial-and-erroring against the gateway, and into the choreographer
guards in Task 4.
2026-05-08 07:49:22 +02:00
Renn F 01ff44b83f fix(gateway): unblock PM planning + drop magic delegate task_type
Two coupled fixes from the 2026-05-08 smoke-test trace:

1. pm_cannot_execute_code is now scoped to i_will_work_on (the
   EXECUTION verb) only. Pre-fix it also fired on i_will_plan, which
   deadlocked any code-typed parent: cell_pm couldn't plan, so couldn't
   transition parent to in_progress, so couldn't delegate. PMs PLAN
   code-typed parents and DELEGATE the work — that's exactly the verb
   we were blocking.

2. delegate.task_type is now REQUIRED at both the HTTP boundary
   (DelegateRequest) and the choreographer dataclass (DelegateInputs).
   The pre-fix default of 'code' silently changed semantics whenever a
   caller forgot the field — main-pm's call in the smoke trace omitted
   it, schema defaulted to 'code', and the cell PM downstream was
   wedged. Also drops the choreographer's task.task_type fallback
   (the DB column is NOT NULL anyway).

Plus middleware coverage tests for the parallel ServiceError →
4xx handler hierarchy added in the prior session, restoring 100%
coverage across the touched files.

Tests: 3101 passing, 100% coverage, ruff clean.
2026-05-08 07:45:18 +02:00
Renn F f0eec854d1 Code quality 2026-05-06 22:25:39 +02:00
Renn F 9aa30fb945 100% Coverage 2026-05-06 21:02:31 +02:00
Renn F 64c48356d0 test: lift coverage 41% → 76% (+1068 tests across 36 files)
Service-level tests now exercise provider, permissions, project, journal,
messaging, work_session, metrics, kanban, extraction, learning, notification,
dashboard, llm_routing, a2a, task, repository_base, audit, db_seed,
branch_name, indexed_document, query_helpers, agent. API route tests cover
provider, journal, project, sessions, dashboard, work_session, tasks, a2a,
groups, notifications, agents, channels, messages, kanban, api_resources.
Pure-function helpers covered: handlers, deps_helpers, middleware,
middleware_docs, transcription, pr templates, agents_config, errors,
logging, journal/notification/channel/a2a access, task_lifecycle,
streaming, converters, crypto, schemas (common + websocket), events,
permissions extras.

pyproject ruff per-file-ignores extended for tests so PLR2004 (status code
magic values), PLC0415 (lazy imports), PLR0913 (fixture params), ARG001
(unused fixture deps), SIM105, and E501 don't fight test idioms.
2026-05-06 00:32:52 +02:00
Renn F b6903490f1 + tests 2026-05-05 05:50:01 +02:00
Renn F 85ef124c8f Quality Gates 2026-05-05 03:19:43 +02:00
Renn F 4829f93a68 fix(gateway): unblock task claim; full Phase 0/1/2 remediation
Resolves the 100% claim-failure rate introduced by the gateway rewrite
  (commit 62bda0c plus 78 follow-ups). Live smoke runs hit
  `404 /api/v2/flow/developer/...` on every dev verb plus a manifest
  fallback that silently exposed off-role verbs to PMs — confirmed
  firing simultaneously in NAS agent logs (be-dev-1, be-pm, main-pm).

  Audit reports under docs/internal/audit_2026_05_04/ catalogue 49
  defects across gateway, services, prompts, MCP transport, substrate,
  and tests (8 detail reports + master synthesis). Six smoking guns;
  three proven in production logs.

  Phase 0 — unblock claim:
  - URL prefix /api/v2/flow/dev → /developer; slug-map board roles
    (product_owner, head_marketing) → /board (D-01)
  - _i_will_work_on AttributeError on None across pending /
    needs_revision / claimed re-entry branches (D-02)
  - Seed last_heartbeat_at in _qa_or_doc_claim (D-03)
  - Drop misleading i_have_committed verb; dev flow uses commit() (D-04)
  - Manifest mount via compose; flow_server + do_server fail loud
    instead of exposing all-verbs fallback (D-12)
  - MCP _post() surfaces envelope body on 4xx so agents see remediate
    hints (D-13)
    on git failure so retries aren't blocked by half-state (S-01)

  Phase 1 — lifecycle stability:
  - _resolve_skill falls back to AgentTable.capabilities (D-06)
  - main_pm_complete uses kwargs for escalate_to_ceo (D-07)
  - i_am_done auto-runs submit_verification when in_progress (D-08)
  - active_claimant_id wired in claim/unclaim paths — single-claimant
    invariant now functional (D-05)
  - qa_pass/qa_fail assert claimed_by parity with qa_agent_id (D-18)
  - Prompt-drift sweep: fail() shape, i_am_done(task_id, notes),
    subtask cap (12 hard / 8 soft), error-code symbology rewritten in
    base.md + per-role anti-patterns (D-10/11/29/30/31, D-37)

  Phase 2 — invariants + architecture:
  - Real-DB integration test exercising claim → in_progress → commit
    → submit_for_qa → i_am_done → awaiting_qa (P2-1)
  - choreographer.py → package; 3 of 6 role mixins extracted
    (board, doc, qa). _impl.py 2,526 → 2,080 lines (-18%). Continuation
    plan in docs/internal/audit_2026_05_04/p2_2_decompose_plan.md (P2-2)
  - Closure guards consolidated via _subtasks_not_terminal_envelope (P2-3)
  - TaskService.unclaim_for_reaper routed through canonical
    _validate_and_set_status; in_progress → pending added to
    VALID_TRANSITIONS (P2-4)
  - Dead code removed: i_am_done_with_catchup verb, _run_catch_up helper
    (P2-5)
  - 6 state-machine invariants asserted via property test (P2-6)
  - attempt_id (uuid4) stamped on every gateway.rejected audit row (P2-7)
  - _reconcile_orphan_claims_on_startup rolls back tasks left CLAIMED
    with branch_name=NULL from prior crashes (P2-8)
  - scripts/regenerate_verb_tables.py introspects Pydantic schemas +
    role_config; compose_prompt injects per-role tables as a layer.
    Eliminates the prompt-drift class structurally (P2-9)

  Other:
  - D-48: orchestrator mounts host's ~/.claude.json when present so
    agents don't boot from backup recovery on every spawn
  - D-49: dev dispatcher rejects role-mismatched spawns (e.g. doc task
    assigned to dev agent)

  Tests: 553 pass · ruff + mypy clean. Live NAS smoke verification
  pending — needs the stack brought back up.
2026-05-04 23:43:55 +02:00
Renn F 0a3c963923 fix(gateway): make i_will_plan / i_will_work_on idempotent on re-entry
Smoke 2026-05-04 captured the cycle the prior 63d0adf fix didn't close:
- Spawn 1: i_will_plan succeeds, task pending → claimed → in_progress.
- Agent goes idle (LLM thinking, container exits, respawned).
- Spawn 2: i_will_plan called again. _i_will_plan_preflight rejects
  'task in in_progress, expected pending'. Agent has no recovery path.
- Heartbeat eventually goes stale, reaper drops claim back to pending,
  spawn 3 fires, loop repeats indefinitely.

Fix: when a respawned PM/dev re-enters the verb on a task they already
own in claimed/in_progress, return OK with current state and refresh
the heartbeat instead of rejecting. The verb is now genuinely
idempotent for the caller, which matches what 'I will plan' should
mean — record intent + advance state, regardless of how many times
the agent says it. Different-caller contention still rejects.

Refactored i_will_work_on's pending branch into _i_will_work_on_pending
helper to satisfy PLR0911 after the new branch raised return count.
2026-05-04 02:04:39 +02:00
Renn F cfdd389c43 test(gateway): regression for ContentActions.commit role gate
Pin: PM, QA, Board cannot call commit; developer + documenter can.
Smoke 2026-05-03 saw main-pm reach the git layer with a 'commit' call
attempting to author a fix to the very gateway bug we were hitting.
2026-05-03 23:01:44 +02:00
Renn F c61a3bf81e fix(gateway): role-gate ContentActions.commit (developers + documenters only)
Smoke 2026-05-03 saw main-pm reach the git layer with a 'commit' call,
trying to author 'fix(gateway): allow claimed status in i_will_plan
preflight'. That should never have been possible — main_pm/cell_pm/
board/auditor/qa manifests all exclude commit. Reaching the verb body
means either the MCP manifest filter mis-routed, or the agent hit the
v2 do.py route directly.

Mirror Task 16 notify pattern: server-side role check in the verb body
rejects with not_authorized + 'PMs delegate, do not commit' remediate.
Defense-in-depth — manifest is still the primary gate.
2026-05-03 22:57:28 +02:00
Renn F ee743ffc7a fix(git): pass actor_agent_id to pr_merge for workspace resolution
pr_merge falls back to task.assigned_to for workspace resolution, but
that field is None at merge time (submit_qa/pass_qa cleared it during
prior transitions). When project.workspace_path is unset, the resolver
raises ValidationError 'no workspace configured and no agent_id
provided' — surfaces as 500 from cell_pm_complete.

Add actor_agent_id parameter (the PM doing the merge) and use it as
the primary workspace owner. Falls back to task.assigned_to, then
created_by, before raising. cell_pm_complete now threads pm_agent_id
through.
2026-05-03 22:49:12 +02:00
Renn F 24279fbee4 test(task): pin claim() seeds last_heartbeat_at (regression for 931eb0a)
Real-DB integration test: pending pre-assigned task → claim() →
status=CLAIMED AND last_heartbeat_at populated. Without the seed
(reverted in unit testing), the reaper would interpret NULL as
stale and reap the freshly-claimed task on the next dispatch tick.
2026-05-03 22:27:53 +02:00
Renn F 638e06910f fix(orchestrator/prompts): enumerate task_type/complexity values explicitly
Smoke run agents picked invalid enums ('development', 'small') because
spawn prompts used 'code|documentation|...' format with literal '...'
that LLMs interpret as 'fill in something creative'. Replace with
explicit '<one of "a" / "b" / ...>' enumerations matching the
TaskType + Complexity enum members, plus an explicit reminder that
the gateway rejects invented values.
2026-05-03 22:18:12 +02:00
Renn F 931eb0a40f fix(task): seed last_heartbeat_at on claim to stop reaper tight-loop
Smoke 2026-05-03: reaper fired ~12 times/sec against a freshly-claimed
task because _finalize_claim never seeded last_heartbeat_at. Reaper's
'NULL means stale' rule then reaped the claim within ~250ms of the
claim landing, agent re-claimed via i_will_plan, reaper reaped again.

Set last_heartbeat_at = claimed_at in _finalize_claim so the freshly-
claimed task carries an authentic heartbeat from second zero. Reaper
logic stays untouched.
2026-05-03 22:14:31 +02:00
Renn F 809f169bb0 test(gateway): regression tests for i_will_plan claim/start bugs
Pin the smoke-2026-05-03 bugs that motivated 63d0adf:
- pre-assigned-and-pending: claim must still fire (was skipped when
  task.assigned_to already matched pm_agent_id)
- start-returns-None: must surface invalid_state (was silently returning
  Envelope.ok with fabricated status='in_progress')
2026-05-03 22:09:59 +02:00
Renn F 63d0adfaa5 fix(gateway): always claim() pending tasks in i_will_plan/i_will_work_on
Smoke run revealed CEO-pre-assigned root tasks stay stuck in pending
because both verbs skipped claim() when task.assigned_to already
matched the caller. Claim is what transitions pending → claimed; without
it, start() refused the claimed → in_progress transition and silently
returned None. i_will_plan then returned an OK envelope with a
fabricated 'in_progress' status string while the DB stayed pending,
causing the next delegate() call to correctly reject with
PARENT_NOT_CLAIMED. Agent looped on delegate retries, never figuring
out claim was the missing step.

Fix:
- Drive claim() on status == pending (idempotent for same assignee),
  not on assigned_to mismatch.
- If start() returns None, surface invalid_state envelope instead of
  fabricating success.
2026-05-03 22:07:17 +02:00
Renn F 9310d66508 chore(post-audit): document notify verb + remove # type: ignore
Audit followups before pushing the gateway-restoration batch:
- Add notify() row to cell_pm.md, main_pm.md, board.md verb tables.
  Manifests + routes + MCP + tests all wired in 3a2498a but agents
  had no prompt-level cue.
- Replace # type: ignore[attr-defined] in test_pm_respawn_reset.py
  with cast('Any', orch) — matches project's no-suppress standard.
2026-05-03 18:04:26 +02:00
Renn F 9bae446cbe Linting/Formatting 2026-05-03 17:51:33 +02:00
Renn F 95c96d4ce3 chore(work_session): delete dead generate_branch_name helper
Used '/' separator while live build_branch_name uses '--'. Footgun
for future contributors. No live callers; safe to remove.
2026-05-03 10:39:04 +02:00
Renn F f8e07d47e3 fix(optimal): make IndexJournalEntryParams.entry_id required
Task 22 added a runtime ValueError when entry_id is None, but the
dataclass still typed it Optional. Contract-vs-runtime split — callers
get no IDE/mypy hint about the required field. Tighten the type to
UUID (required) and remove the misleading "can be None for system
events" docstring note. Lifecycle events already use their synthetic
uuid5 path, so no real caller is broken.
2026-05-03 10:38:59 +02:00
Renn F b0107f8c12 fix(optimal): raise instead of falling back to 'unknown' doc source
Silent fallback hid an upstream bug where journal_entry.id wasn't
flushed before indexing. Raise so we see the regression.
2026-05-03 10:31:59 +02:00
Renn F 279aceab3d chore(docker): set ROBOCO_PUBLIC_BASE_URL for commit-link rendering
Default 127.0.0.1 produced unusable links in commit message bodies.
Set to the NAS LAN IP.
2026-05-03 10:21:43 +02:00
Renn F d6f64f4b9d fix(audit): populate agent_id on task.* and agent.* events
task.awaiting_qa fired after submit_qa cleared claimed_by; agent.*
events stored slug-only. Capture claimed_by before mutation and add
slug→UUID resolver in orchestrator audit path.
2026-05-03 10:16:25 +02:00
Renn F bf44d5aade fix(orchestrator): skip closure spawn if PM just paused via i_am_idle
Tiny race: dispatcher decided to spawn for closure between agent's
heartbeat and idle-pause. Spawn would land against an already-paused
parent. Gate spawn on (status != PAUSED OR last_heartbeat older than
cutoff).
2026-05-03 10:03:46 +02:00
Renn F 3cabee155e chore(lifecycle): remove quarantined state (phantom)
State existed in the lifecycle table and the enum but no verb, route,
or service path ever set it. Removing dead state. If we need
problem-task isolation later we'll add it explicitly with a verb.
2026-05-03 09:52:07 +02:00
Renn F 5c742e6aec fix(git): serialize concurrent merges to the same parent branch
Two PMs completing different subtasks of the same parent could race
on gh API merge calls. GitHub returns 409 to one but local DB write
ordering wasn't guaranteed. Take row-level lock on parent task before
merge; retry once on 409.
2026-05-03 09:38:03 +02:00
Renn F 3a2498a609 feat(gateway): notify(target, text, scope?) for PMs and Board
Pre-gateway PMs/Board sent formal notifications (require ack); gateway
had say/dm only. Now PMs and Board can issue ack-required notifications
via NotificationService through the standard envelope path.
2026-05-03 09:19:08 +02:00
Renn F 1ed1317a35 fix(workspace): re-apply agent ownership after refresh fetch
I1: _fetch_origin_best_effort runs as root and writes new pack files
+ ref updates that land root-owned, undoing _ensure_agent_owned that
ran before. Subsequent spawns hit Permission denied. Mirror the
fetch_branch_for_inspection pattern: re-run _ensure_agent_owned AFTER
the fetch.

I2: separate workspace_refresh_fetch_timeout_seconds (default 60s)
from workspace_clone_timeout (300s). Refresh transfers small deltas;
300s of blocking on every spawn against a hung remote is operationally
bad. 60s is enough for any sane refresh.
2026-05-03 09:06:40 +02:00
Renn F e21ecd000b feat(workspace): fetch refs on healthy-clone re-entry
ensure_workspace short-circuited when the clone existed, so a
respawned PM/Doc could be reviewing arbitrarily stale diffs. Add
a best-effort 'git fetch origin' on every entry; checkout
unchanged.
2026-05-03 08:53:14 +02:00
Renn F 074f47a2f9 feat(observability): propagate correlation_id end-to-end
X-Correlation-ID was bound to structlog but lost on the MCP→API hop.
MCP shims now forward it; Envelope carries it back to the agent;
audit_log row records it for forensic joins.
2026-05-03 08:37:25 +02:00
Renn F 99eac69aab fix(audit): migrate audit_log.details to JSONB so .astext works
Task 13's has_recent_tracing_gap query used .astext on a generic JSON
column, which raises AttributeError at runtime. The choreographer's
exception handler swallowed it, leaving the strike-count reset
permanently inert in production. Migrate the column to JSONB (which
supports .astext + GIN indexing for future audit queries), update the
ORM, and add a real-DB integration test that would have caught this.
2026-05-03 08:19:50 +02:00
Renn F 44784293c7 fix(orchestrator): respect tracing-gap as forward progress
_pm_respawn_should_gate counted PARENT_NOT_CLAIMED rejections as
no-progress and killed PMs after 3 strikes — even when the new prompts
told them to call i_will_plan first. Reset counter when last response
was a tracing_gap (rule-following retry, not stuck).
2026-05-03 08:04:28 +02:00
Renn F 87ef42bf09 chore(orchestrator): enable gateway cooldown logic in production
ROBOCO_GATEWAY_ENABLED defaulted to False, leaving trigger_filter's
spawn cooldown / role-rate logic dormant. Flip to true and add the
gateway_triggers table migration if missing. Without this, respawn
rate has no server-side limit besides _pm_respawn_should_gate.
2026-05-03 07:43:05 +02:00
Renn F 00b385c019 fix(orchestrator): retry parent branch lookup to close PM race
When a PM's i_will_plan and a child dev's spawn fire in the same tick,
the dev sometimes saw branch_name=None because the PM's transaction
hadn't committed yet. Auto-block fired and the dev sat blocked until
the next 30s tick. Add 3x250ms retries when parent is mid-claim.
2026-05-03 07:30:48 +02:00
Renn F 41d7e8295e fix(task): resume_for_agent delegates to resume() for observability
I2: resume_for_agent did its own _validate_and_set_status call,
skipping resume()'s structlog 'Task resumed' event and the fire-and-
forget RAG lifecycle-event indexing. Gateway-driven resumes were
invisible to logs and the RAG corpus. Delegate to resume() after
the gateway-specific ownership pre-checks (mirrors pause_for_agent's
pattern).

I1: documented why Choreographer.unclaim does NOT call _touch on
its OK path - assigned_to is cleared, no claimant heartbeat to
refresh. Prevents future readers from 'fixing' the asymmetry.
2026-05-03 07:24:59 +02:00
Renn F cd9f999533 feat(gateway): add resume verb for paused -> in_progress
i_am_idle auto-pauses owned in_progress tasks; lifecycle table allows
paused -> in_progress; no verb implemented it. Adds resume so an agent
respawned for a paused task can continue. Routes through
_validate_and_set_status mirroring the unclaim pattern (Task 9 fix).
2026-05-03 07:05:16 +02:00
Renn F bf3de1ab2e fix(task): route unclaim_for_agent through _validate_and_set_status
unclaim_for_agent did direct attribute assignment, bypassing the
single point of truth for status transitions. Choreographer's pre-check
made the runtime correct today, but a future change to VALID_TRANSITIONS
or ROLE_RESTRICTED_TRANSITIONS would silently miss this path. Route
through _validate_and_set_status; assigned_to is still cleared
explicitly as the unclaim's specific side effect.
2026-05-03 06:48:15 +02:00
Renn F 1f7c9adaba feat(gateway): add unclaim verb
Choreographer remediate strings already pointed agents at unclaim;
the verb didn't exist. Now it does — claimed/in_progress -> pending,
clears assigned_to, available to dev/qa/doc/cell_pm/main_pm.
2026-05-03 06:38:27 +02:00
Renn F 68466e9bf0 fix(gateway): enforce [task-id] prefix on every commit
ContentActions.commit was stripping user-supplied prefixes but never
re-adding the canonical one. Dev prompt promised auto-prefix; code
delivered nothing. Now every gateway commit lands with [task-id-short].

Format choice: simple [task_id[:8]] (8-char), matching the dev prompt
("Auto-prefixes [task-id]") and CLAUDE.md's documented commit format.
The legacy templates/git/commit.py uses the richer
[root_short:task_short] for the commit_for_task API path with full
traceability metadata; the gateway commit path is intentionally simpler
and stays aligned with the prompt-level promise.
2026-05-03 06:23:43 +02:00
Renn F c0c5838baa fix(bash-guard): close scheme-less curl bypass
Previous regex used a greedy [^|]* and required at least one / before
the host, so 'curl roboco-orchestrator:8000/api' (no scheme, no slash)
slipped through. Split into two simpler checks: (a) line starts with
curl/wget/http/https/httpie, AND (b) line contains a forbidden host.
Probe-verified: scheme-ful, scheme-less, and protocol-relative forms
all denied; external URLs still allowed; GitHub-specific deny still
fires first.
2026-05-03 06:17:37 +02:00
Renn F 8381ade3ce fix(bash-guard): deny internal curl to orchestrator/localhost
Prompts told agents internal API calls were denied; the guard only
denied GitHub. Combined with task 4 (X-Agent-Role enforcement) this
closes the manifest-bypass loophole.
2026-05-03 06:11:38 +02:00
Renn F 38246050d6 feat(gateway): audit-log every gate rejection
Choreographer takes an audit dep but never used it. Now every rejection
Envelope (invalid_state, not_authorized, tracing_gap, not_found) writes a
gateway.rejected row with verb + reason + missing fields. Forensic
signal for stuck flows; no behaviour change for happy path.

Adds AuditService.log_event() — generic free-form event_type write so
the gateway doesn't have to extend AuditEventType for every new surface.
Audit writes are best-effort: a failure logs a warning but never
propagates, since the agent's response Envelope is the contract.
2026-05-03 06:00:36 +02:00
Renn F 8b43ed98be fix(messaging): close fail-open when agent_slug lookup returns None
I1: post_to_channel raised no error if get_agent_slug returned None
(unknown/deleted agent), and send_message's 'if agent_slug' skipped
the validate_channel_access call. Same fail-open class the prior
commit was fixing, just narrower window. Now post_to_channel raises
ChannelAccessDeniedError directly when the slug lookup fails — say()
already converts that to a clean not_authorized Envelope.

I2: 'writable channels for your role' wording was misleading because
get_agent_channels resolves by slug, not role. Replaced with
'channels you may write to' for clarity.
2026-05-03 05:46:02 +02:00
Renn F 7907988e52 fix(messaging): restore channel-access RBAC in gateway say()
post_to_channel was calling send_message without agent_slug, which
disabled the validate_channel_access check. Forward the slug; convert
ChannelAccessDeniedError to a friendly not_authorized Envelope with
the agent's writable-channel list (pre-gateway behaviour).
2026-05-03 05:37:12 +02:00
Renn F 92294f90fb feat(api/v2): enforce X-Agent-Role on every flow router
Route layer now rejects 403 if the role doesn't match the router's
allowed set. Choreographer still re-checks role per verb where needed,
but defense in depth means a future verb that forgets the role check
doesn't leak. Auditor router also gated.
2026-05-03 05:27:31 +02:00
Renn F 3d5f14815d feat(gateway): add submit_for_qa verb so devs can open PRs
Gate E made i_am_done strict (requires pr_number set), but the only
verb that opened PRs was i_am_done_with_catchup which lives off the
dev manifest. Devs hit NO_PR with no escape. Adds submit_for_qa as
the explicit push+PR step, leaving i_am_done to do the strict submit.
2026-05-03 05:14:07 +02:00
Renn F 6643b4c375 feat(gateway): heartbeat on every hot verb
i_will_work_on, i_have_committed, i_am_done, i_am_blocked, i_will_plan,
and pm_give_me_work now call task.heartbeat() so the reaper has fresh
data. Closes the loop opened by tasks 2a + 2b.
2026-05-03 05:01:50 +02:00
Renn F c12aad3005 fix(orchestrator): consolidate stale-heartbeat config + drop dead _task_svc slot
I1: claim_heartbeat_ttl_seconds (300s) overlapped semantically with the
pre-existing claim_stale_seconds (180s). Between 180-300s of silence,
trigger_filter queued duplicate spawns while the reaper hadn't yet
released the claim — exactly the dispatcher churn the reaper was
supposed to close. Collapse to one field (claim_stale_seconds, 180s);
reaper now consumes the same setting trigger_filter uses, so both
agree on 'stale' on the same tick and the reaper runs first.

I2: _task_svc injection slot on AgentOrchestrator.__init__ was
production-dead (always None) and only used by __new__-based test
instances. Drop the __init__ slot + the production branch in
_reap_stale_claims that read it. Tests still pre-bind on __new__
instances; the attribute exists per-instance, not per-class.
2026-05-03 04:57:25 +02:00
Renn F b301020398 feat(orchestrator): reap stale claims via last_heartbeat_at
Dispatch loop now releases tasks whose holder has gone silent past
ROBOCO_CLAIM_HEARTBEAT_TTL_SECONDS (default 300s). Closes the
'dead container squats task forever' failure mode that the schema
hinted at but no code enforced.
2026-05-03 04:47:51 +02:00
Renn F 37bc4e58ed feat(task): add heartbeat() to touch last_heartbeat_at
Foundation for stale-claim recovery. Schema column existed since
migration 006; no writer until now. Idempotent UPDATE — no select
roundtrip — so callable from any hot verb without extra DB cost.
2026-05-03 04:37:19 +02:00
Renn F c33340bda1 fix(gateway): correct claim/start argument order in choreographer
Service signatures are (task_id, agent_id, ...) but choreographer was
calling (agent_id, task_id). Production claim path silently returned
None; unit tests pinned the buggy order so the bug was invisible. Swap
all 7 call sites and update test assertions. Add a regression pin
that locks in the correct order.
2026-05-03 04:27:14 +02:00
Renn FandClaude Opus 4.7 4c9b7c4210 feat(gateway): restore Gate Set F completion-time guards
cell_pm_complete, main_pm_complete, and submit_up already had subtask-
terminality gates inherited from the pre-gateway closure check at
roboco/services/task.py. This commit verifies the gate is preserved
and improves the remediation hint to actually NAME the non-terminal
subtasks instead of telling the PM to "call triage()".

The improvement uses a new private helper
``Choreographer._non_terminal_subtask_ids`` that queries get_subtasks
and filters to non-terminal statuses, returning a comma-separated
list of "<id> (<status>)" pairs. The PM now sees exactly which
subtasks are blocking the parent's completion.

Pre-gateway reference: roboco/services/task.py closure check
(documented in PRE_GATEWAY_LIFECYCLE.md §4.3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:37:17 +02:00
Renn FandClaude Opus 4.7 4cb47afdb2 feat(gateway): restore Gate Set E submit-qa field-level gates
i_am_done now strictly enforces the four pre-gateway field-level gates
restored from roboco/api/routes/tasks.py:903-940 at commit 254cc93:

- NOT_SELF_VERIFIED: task.self_verified must be true.
- NO_COMMITS: task.commits must be non-empty.
- NO_PR: task.pr_number must be set.
- NO_PROGRESS: task.progress_updates must have at least one entry.

Each missing field surfaces as a tracing_gap with the matching pre-
gateway error code in the missing list, and a remediation hint that
tells the dev exactly what to do.

The previous silent-catch-up behavior is preserved as a separate
opt-in verb i_am_done_with_catchup. Existing tests that asserted the
catch-up behavior have been migrated to the new verb.

This fixes the failure mode where a dev could call i_am_done with no
commits and the gateway would silently try to push nothing, open an
empty PR, etc. — now the dev sees an explicit error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:34:57 +02:00
Renn FandClaude Opus 4.7 855cd24477 feat(gateway): restore Gate Set D content-tool ownership guards
When a caller passes an explicit task_id to commit / note / say / dm /
evidence, ContentActions now verifies task.assigned_to == caller_agent_id
before allowing the side effect. Auto-fill from get_active_task_for_agent
is implicitly self-owned and does not need a re-check.

evidence() additionally allows assigned_to=None (post-handoff transient
state) so QA / documenter can inspect tasks between reassignments.

Pre-gateway, agents could not even see tasks they didn't own because the
MCP handlers resolved task from session context. The gateway exposes
task_id parameters across multiple verbs, so the explicit ownership
gate is required.

Exception: say() and dm() with NO task_id are exempt — used for channel
announcements and off-task A2A. The strict guard only applies when the
agent supplies a task_id parameter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:31:10 +02:00
Renn FandClaude Opus 4.7 197b0f22dc feat(gateway): restore Gate Set C exit-time guards
Choreographer.i_am_idle now refuses with INVALID_STATE when the caller
has any pending (assigned but never claimed) task. Pre-gateway this
was implicit because the orchestrator's auto-respawn would re-spawn
the agent for the assignment, leading to a tight respawn loop. The
explicit refusal lets the agent fix the state via i_will_work_on
(dev/qa/doc) or i_will_plan (pm) first.

Existing auto-pause for in_progress tasks is preserved (Gate Set C
spec calls this out as still required) — it runs AFTER the pending
guard, so an agent with a mix of pending+in_progress is told about
the pending task first instead of silently pausing in_progress and
then looping on the pending one.

Pre-gateway reference: roboco/runtime/orchestrator.py
auto-respawn loop guards (already preserved at HEAD); the explicit
agent-facing gate is new.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:27:54 +02:00
Renn FandClaude Opus 4.7 466cc8d8f7 feat(gateway): restore Gate Set B delegation-time guards
PARENT_NOT_CLAIMED: Choreographer.delegate now enforces that the parent
task is in_progress AND assigned to the calling PM before allowing
subtask creation. Pre-gateway this was implicit (orchestrator only
spawned PMs after they claimed their parent); the gateway exposes
delegate as a first-class verb so the gate must be explicit.

SUBTASK_CAP: hard-blocks delegation when the parent already has 12
subtasks. Pre-gateway never had this cap because PMs naturally never
created more than a handful per spawn cycle; with delegate as a verb
agents can loop, so a cap is needed.

The _delegate_guard helper was split into _delegate_role_guards,
_delegate_static_guards, and _delegate_lifecycle_guards to keep each
piece below the PLR0911 return-count threshold and make the layered
gating explicit.

Pre-gateway reference: implicit in roboco/runtime/orchestrator.py
spawn flow; restored here as explicit server-side enforcement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:26:00 +02:00
Renn FandClaude Opus 4.7 5c0011c90b feat(gateway): restore Gate Set A claim-time guards
Ports five pre-gateway predicates that were dropped when the gateway
displaced the MCP claim handler. Predicates restored from
roboco/mcp/tasks/handlers/_helpers.py:124-204 and
roboco/mcp/tasks/handlers/claim.py:121-180 at commit 254cc93:

- SEQUENCE_ORDER_VIOLATION: a sibling task with sequence < N must be
  in completed/cancelled before sibling N can be claimed.
- ALREADY_ACTIVE: agent cannot claim while owning an in_progress /
  claimed / verifying task other than the one being resumed.
- PAUSED_TASKS_EXIST: agent cannot claim while paused tasks exist.
- PM_CANNOT_EXECUTE_CODE: cell_pm/main_pm cannot claim task_type=code.
- ROLE_TYPED_CLAIM: developer claim is restricted to
  code/research/design; qa/documenter must use claim_review /
  claim_doc_task.

All five guards run inside Choreographer._run_claim_guards before
i_will_work_on / i_will_plan / claim_review / claim_doc_task mutate
state. Skip flags isolate guards that don't apply to a verb (e.g.,
PM-code skipped on QA verb, role-typed skipped on PM verb).

The guards live in roboco/services/gateway/claim_guards.py so
choreographer.py stays focused on orchestration.

Existing tests updated to provide the new mock primings; the
permissive auto-mock behavior they relied on no longer applies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:23:04 +02:00
Renn FandClaude Opus 4.7 1da4ac4b2e chore(prompts): restore role identity framing + anti-patterns
Each role file now has a six-section structure (Identity / Inputs / Your
verbs / Workflow / Anti-patterns / When the gateway returns an error).
The Identity section gives a hard role prior with explicit negative space
("you do NOT write code", "you do NOT merge"), recovering the framing
that was lost in the gateway slim-down. The Anti-patterns section names
the gateway error codes the role will hit if they step out
(PARENT_NOT_CLAIMED, SUBTASK_CAP, PM_CANNOT_EXECUTE_CODE, NO_COMMITS,
NO_PR, NOT_SELF_VERIFIED, etc.), so agents expect the system to catch
them. base.md now centralizes the envelope contract, ground rules, and
channel-slug convention so per-role files stay focused.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:17:11 +02:00
Renn FandClaude Opus 4.7 27dccc7215 fix(gateway): four PM-lifecycle smoke regressions
* choreographer.escalate_up: AttributeError when target lookup returned
  None. Switched from role-based escalate_up_to_role (which mishandled
  slug-shaped escalation_target like "main-pm" because AgentRole only
  accepts underscore form) to slug-based task.escalate, with explicit
  None-handling that returns invalid_state instead of 500.

* prompts/roles/cell_pm.md + main_pm.md: enumerate the new lifecycle
  verbs (i_will_plan, delegate, submit_up, give_me_work, i_am_idle).
  Without this, PM agents fell back to calling i_will_work_on (the dev
  verb) and 404'd at /api/v2/flow/cell_pm/i_will_work_on. Workflow
  walkthroughs included.

* messaging.get_channel_by_slug + get_or_create_channel_by_slug: strip
  leading "#" so "#main-pm-board" resolves to the row stored as
  "main-pm-board". Agents follow Slack convention; gateway must accept
  it.

* agent_sdk session-end post-mortem hook: corrected payload shape from
  {content, kind:"reflect"} to {type:"task_reflection", title, content}
  so /api/journals/me/entries validates. Added pad-to-min-length so the
  50-char content gate doesn't reject thin post-mortems.

* test_choreographer_pm: updated escalate_up test to assert task.escalate
  is awaited, plus regression test for the None-target invalid_state path.

380 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:54:05 +02:00
Renn F 4520293def feat(gateway): wire PM lifecycle verbs through API + MCP + tests
* api/schemas/v2/flow.py: IWillPlanRequest, DelegateRequest,
  SubmitUpRequest with min_length=1 validators where appropriate.

* api/routes/v2/flow_cell_pm.py: give_me_work routes to
  pm_give_me_work; new endpoints i_will_plan, delegate, submit_up.
* api/routes/v2/flow_main_pm.py: new endpoints give_me_work,
  i_will_plan, delegate.

* mcp/flow_server.py: Python wrappers for i_will_plan, delegate,
  submit_up registered in _TOOLS so manifest-scoped agents can call
  them.

* tests/unit/gateway/test_choreographer_pm_extras.py: 22 tests
  covering happy + reject paths for each new verb plus i_am_idle's
  auto-pause behavior.
* tests/unit/api/routes/v2/test_flow_cell_pm.py +
  test_flow_main_pm.py: route-level tests for the new endpoints.

Test count: 352 → 381 (+29). make quality-fast green.
2026-05-03 00:06:50 +02:00
Renn F 4e5cdd0891 feat(gateway): restore PM lifecycle verbs (i_will_plan/delegate/submit_up)
Adds the missing gateway verbs that PMs need to drive their parent task
through the lifecycle. Pre-Phase-4, PMs could only triage/unblock/
complete/escalate/idle — they had no way to claim+start their parent
task, no way to create subtasks (told to curl raw), no way to bubble
cell-level PRs up to the Main PM. The lifecycle stalled at pending.

This commit lands the choreographer + service layer:

* TaskService gains: list_in_progress_for_agent, pause_for_agent,
  submit_pm_review (gateway alias of submit_for_pm_review),
  create_subtask (TaskCreateRequest-based, infers status from
  assigned_to), and main_pm_agent.

* Choreographer gains:
  - i_will_plan(task_id, plan): claim + set_plan + start for PMs.
    Mirrors i_will_work_on, scoped to cell_pm/main_pm.
  - delegate(parent_task_id, DelegateInputs): create a subtask with
    delegation-chain validation (Main PM → cell PMs; Cell PM → its
    own team's devs). Resolves slug → UUID via AGENT_UUIDS.
  - submit_up(task_id, notes): cell PM bubbles a finished cell-scope
    task up to Main PM. Opens a cell-level PR into the parent (Main
    PM) branch, transitions to awaiting_pm_review, reassigns to
    Main PM. Tracing gates: notes>=20, journal:decision, all
    subtasks terminal, branch present.
  - pm_give_me_work: returns the PM's first assigned task in any
    active status (not just dev-active states).
  - i_am_idle: now auto-pauses every in_progress task this agent
    owns before marking idle. Restores the pre-Phase-4 behavior the
    closure dispatcher relies on.

* role_config: cell_pm gains give_me_work + i_will_plan + delegate
  + submit_up; main_pm gains give_me_work + i_will_plan + delegate.
2026-05-03 00:06:29 +02:00
Renn F 736bd57f30 fix(orchestrator/prompts): rewrite stale spawn prompts to use gateway verbs
5 spawn prompt builders (_build_dev_prompt + _get_workflow_instructions,
_build_qa_prompt, _build_doc_prompt, _build_pm_review_prompt,
_build_pm_closure_prompt) plus the recently-rewritten triage prompts
referenced MCP tools deleted in Phase 4 T9. Replace with gateway verbs
(give_me_work, i_will_work_on, claim_review, claim_doc_task,
i_will_plan, delegate, complete, submit_up, etc). Drops the curl-POST
recipe in favor of the new delegate verb. Surfaced live during NAS
smoke.
2026-05-02 23:53:02 +02:00
Renn F 249e9c2c59 fix(gateway): align content_actions with actual service method names
content_actions.note/dm/say/commit/evidence called write_entry/send/
post_to_channel/git.commit/git.diff(base=)/fetch_branch_for_inspection,
none of which existed on JournalService/A2AService/MessagingService/
GitService/WorkspaceService. Live smoke threw AttributeError on every
content tool. Add the matching gateway-shaped adapters on each service
(scope-string -> JournalEntryType for note; channel-by-slug -> default
group -> active session for say; UUID-or-slug recipient resolution for
dm; branch-name commit + diff(base=) for commit/evidence; project-aware
fetch_branch_for_inspection on workspace). Surfaced live.
2026-05-02 21:48:07 +02:00
Renn F 54e19f88e6 fix(orchestrator): rewrite PM spawn prompts with gateway verbs + curl for delegation
Both _build_main_pm_triage_prompt and _build_pm_triage_prompt referenced MCP tools deleted in Phase 4 T9 (roboco_task_get/plan/start/create/activate/journal_decision/agent_idle). Main_pm + cell_pm spawned with these and tried the dead tools — minimax-m2.7 then improvised, ending up implementing the task itself instead of breaking it down.

New prompts: gateway verbs (evidence, note, say, dm, complete, escalate_*, i_am_idle) for transitions/journal/comms; explicit POST /api/tasks curl recipe for breakdown + delegation (gateway has no create_subtask verb yet). Both prompts now reinforce: PMs do not implement, do not run git, must delegate. Main PM hands to Cell PMs; Cell PMs hand to devs.

Surfaced live during NAS smoke.
2026-05-02 19:04:57 +02:00
Renn F 7089d78428 Revert "fix(orchestrator): default code tasks route to dev, not cell_pm"
This reverts commit 8d689e3eec.
2026-05-02 18:59:49 +02:00
Renn F 8d689e3eec fix(orchestrator): default code tasks route to dev, not cell_pm
_classify_code_task routed every default-complexity (medium) code task to cell_pm even when the description named no coordination work. The PM then re-delegated back to a developer, adding a useless hop and producing the smoke-test pattern where main_pm/cell_pm tried to do every lifecycle stage themselves.

Drop the complexity==medium → cell_pm branch. Cell PM now lights up only when the description carries an actual coordination keyword (coordinate, integration, cross-team, sync, planning, milestone, dependencies, review). High/critical complexity, cross-cell keywords, missing team, and team=all still route to main_pm. Adds 11 unit tests; full suite 363 passing.

Surfaced live during NAS smoke.
2026-05-02 18:57:47 +02:00
Renn F 4a3da479d1 test(gateway): cover per-transition reassignment
Adds:
- TaskService.reassign unit tests (set, clear, missing-task)
- New test_choreographer_reassignment.py covering:
    i_am_done -> qa
    pass_review -> documenter
    i_documented -> cell_pm
    main_pm_complete -> None (CEO via UI)
    escalate_to_ceo (board) -> None
    cell_pm_complete -> walks up to parent and reassigns when all
                        siblings terminal, skips otherwise
    fail_review -> does NOT issue an explicit reassign (qa_fail
                   already restores the original developer via
                   quick_context)
2026-05-02 05:25:07 +02:00
Renn F 3b02a72e05 feat(gateway): reassign task.assigned_to on every lifecycle transition
The orchestrator polls per-agent for actionable tasks. Without
updating assigned_to as the lifecycle hands a task to the next role,
the orchestrator kept respawning the previous-stage agent (often
main-pm) for every stage, hitting role-permission rejections and
looping. Each Choreographer transition now writes the new assignee
alongside the existing A2A notification.

- Adds TaskService.reassign(task_id, new_assignee | None)
- _notify_qa: dev → qa
- pass_review: qa → documenter
- i_documented: doc → cell_pm
- main_pm_complete + escalate_to_ceo: clear assignment (CEO acts via UI)
- cell_pm_complete: walks up to parent; if all subtasks terminal,
  hands the parent off to the cell PM for that team via a new
  _maybe_advance_parent_to_pm_review helper

fail_review still leans on qa_fail's existing original_developer
recovery via quick_context, and unblock_with_restore restores
pre_block_assignee — both already correct, no changes needed.
2026-05-02 05:24:54 +02:00
Renn F 55d2fae564 fix(errors): replace stale roboco_task_* MCP refs in error messages with gateway verbs
10 stale remediation hints across api/routes/tasks.py + exceptions.py pointed at MCP tools deleted in Phase 4 T9 (roboco_task_start, roboco_task_qa_pass, roboco_task_qa_fail, roboco_task_progress, roboco_task_unblock, roboco_task_submit_verification, roboco_task_submit_qa, roboco_task_complete, roboco_task_claim, roboco_task_activate). Each now mentions both the gateway verb (i_will_work_on, pass, fail, complete, unblock, etc.) and the panel REST equivalent. Surfaced live during NAS smoke.
2026-05-02 05:13:28 +02:00
Renn F 453a7ae22a fix(orchestrator): forward agent UUID (not slug) as ROBOCO_AGENT_ID to MCP env
Gateway v2 endpoints declare X-Agent-ID as Annotated[UUID, Header(...)]. The MCP servers (flow_server, do_server) read os.environ['ROBOCO_AGENT_ID'] verbatim and put it in the header. We were exporting the slug ('main-pm', 'be-dev-1', ...) so every gateway call from a containerized agent 422'd on header UUID parse. Resolve to UUID via AGENT_UUIDS lookup in seeds/initial_data.py. Surfaced by the new 422 logger (de54c3b) during NAS smoke.
2026-05-02 05:06:32 +02:00
Renn F de54c3b52d feat(api/middleware): log request body + per-field errors on 422 validation failures
FastAPI's default RequestValidationError returns details to the client but nothing to server logs. Smoke test hit a 422 on /api/v2/flow/main_pm/complete with no way to tell which field failed. Add a handler that logs path/method/body/errors on every 422 so the next failure is debuggable in one log scan.
2026-05-02 04:55:21 +02:00
Renn F 33464a207a docs(prompts): forbid Bash curl/git for gateway-covered ops; PMs don't implement
Live smoke runs showed agents reaching for `Bash curl /api/...` and
`Bash git ...` even though the slim role prompts named the gateway
verbs, and main_pm took ownership of an implementation task and tried
to commit code from the PM seat.

- base.md and every role prompt: explicit ground rule that direct
  curl-to-orchestrator and raw-git invocations are forbidden — every
  commit/push/PR/transition/journal/comms call goes through the gateway
  verbs. The deny-list line in base.md now also covers curl/wget to the
  orchestrator's /api/... (was GitHub-only before).
- main_pm.md and cell_pm.md: explicit "you do not implement tasks
  yourself" rule. Implementation belongs to developers; PMs delegate.
2026-05-02 04:45:23 +02:00
Renn F f2211a15a9 refactor(mcp): scope do_server tool registration to per-agent manifest
Same change as flow_server: read the spawn manifest's do_tools list and
register only those names on this MCP server, so e.g. a Cell PM agent
no longer sees commit() in its tool palette (Cell PMs don't write code,
they only coordinate). Falls back to registering all do tools when the
manifest is missing, with a warning, so test runs without the bind
mount keep working.
2026-05-02 04:45:11 +02:00
Renn F 3d6028b036 refactor(mcp): scope flow_server tool registration to per-agent manifest
The MCP server previously registered every flow verb unconditionally, so
Claude Code surfaced off-role verbs (e.g. main_pm seeing i_am_blocked) in
the model's tool palette and agents called them, only to get 404s back
from the role-scoped orchestrator API.

Refactor each verb into a plain function and register them through a
manifest-driven loop: read /app/tool-manifest.json, register only the
verbs in flow_tools. When the manifest is absent or malformed, register
the full set as a failsafe (covers local test runs without the bind
mount) and log a warning. Drop _validate_role_compatibility — it's
superseded by the new registration loop.
2026-05-02 04:45:04 +02:00
Renn F 6bae4ad69f fix(gateway): add escalate_to_ceo endpoint + unblock to main_pm role
Smoke surfaced: main-pm hit 404 on /api/v2/flow/main_pm/escalate_to_ceo and /i_am_blocked. role_config promised neither escalate_to_ceo nor unblock for main_pm; the router exposed unblock but not escalate_to_ceo. Aligned both: role_config now lists triage_all/unblock/complete/escalate_up/escalate_to_ceo/i_am_idle, and the router has the matching escalate_to_ceo endpoint.

Note: i_am_blocked stays absent (PMs don't get blocked, they unblock others) — agent hallucinated it from the unscoped MCP tool list. Tightening MCP visibility to the manifest is a separate followup.
2026-05-02 04:37:14 +02:00
Renn F 25c51941f5 fix(api/tasks): coerce malformed sub_task ids in convert_plan response
Agents (esp. minimax-m2.7) PUT plans with sub_tasks like {id: '1', ...}. The write succeeds (DB stores raw JSON), but the response model SubTaskResponse.id requires a UUID — first read crashes the endpoint. Coerce non-UUID ids to a fresh UUID at serialization time so a single bad write doesn't brick the read path. Surfaced during NAS smoke.
2026-05-02 04:17:06 +02:00
Renn F dc147f41db fix(api/tasks): NO_PLAN remediation hint references gateway, not deleted MCP
Old hint pointed agents at roboco_task_plan() / roboco_task_start() — both deleted in Phase 4 T9. New hint covers both callers: panel via PATCH, agents via gateway i_will_work_on(plan=...).
2026-05-02 04:15:00 +02:00
Renn F 0ecf47ad77 style(notification_delivery): re-format after subagent dead-code cleanup 2026-05-02 03:51:52 +02:00
Renn F 2c7b1aed27 refactor(tasks): remove dead /tasks routes + service code
Phase 4 left these task-route endpoints with no callers:
- POST /tasks/{id}/unclaim — no panel button, no orchestrator call,
  no agent_sdk path. CLAIMED→PENDING transition stays defined in
  enforcement/task_lifecycle.py for any future re-introduction.
- POST /tasks/{id}/pm-reject — orchestrator's PM-closure briefing
  prompt references roboco_task_pm_reject (an MCP tool that does not
  exist). Real PM rework path is escalate or cancel-and-recreate.

Cascading service cleanup (now-unreferenced):
- TaskService.unclaim, TaskService.pm_reject
- NotificationDeliveryService.notify_developer_of_pm_reject
- PMRejectDetails dataclass
- EventType.TASK_PM_REJECTED enum value (never emitted)

Panel and orchestrator paths preserved:
- claim, start, block/unblock, pause/resume, verify, submit-qa,
  pass-qa, fail-qa, complete, cancel, activate, docs-complete,
  ceo-approve/reject, escalate, escalate-to-ceo, substitute,
  progress, checkpoint, commit, soft-block, submit-pm-review.

Tests: 336 unit tests still passing.
2026-05-02 03:50:13 +02:00
Renn F 27e0b2c689 refactor(notifications): remove dead /notifications routes + service code
Phase 4 left these endpoints with no callers:
- POST /notifications (send_notification)
- GET /notifications/pending-a2a (check_pending_a2a)
- POST /notifications/ack-a2a (ack_a2a_notifications)

Panel only uses GET (list), GET/{id}, POST/{id}/read, POST/{id}/ack.
Orchestrator only calls GET /notifications. Agent_sdk does not call
notifications at all (uses A2A through different paths).

Cascading service cleanup (now-unreferenced):
- ApiNotificationCreate dataclass
- send_from_api, _assert_content
- has_pending_a2a, auto_ack_a2a
- NotificationCreateRequest schema + __init__ re-export

Also resolves smoketest issue #9 (Missing X-Agent-ID on
/notifications/pending-a2a) — endpoint no longer exists.
2026-05-02 03:47:20 +02:00
Renn F 702c14eb2f fix(alembic/009): make enum reconcile dynamic — handles ALL postgres enums, not just 3
Original 009 only reconciled agentrole/team/taskstatus. Production hit LookupError: 'CELL' is not among defined channeltype values during seed bootstrap — channeltype (and ~16 other enums declared in 001) had uppercase members from the original create_all bootstrap that 009 never touched.

Rewrite queries pg_enum at migration time to find every enum with uppercase members, then for each: read current members, lowercase them, add desired_additions for the 3 enums with new ORM members, find every (table, column) referencing the enum via pg_attribute, RENAME-old/CREATE-new/ALTER-USING-lower/DROP-old.
2026-05-02 03:38:58 +02:00
Renn F 1d688a302c refactor(mcp): trim dead exports from utils + schemas
After Phase 4 T9 deleted task/journal/notify/a2a/message/project MCP
servers, only optimal_server and docs_server consume mcp/utils + mcp/schemas.

mcp/utils.py: removed 7 dead exports — get_agent_headers (made private as
_get_agent_headers, only kept as ApiClient internal helper),
format_success_response, resolve_agent_uuid, resolve_agent_uuid_cached,
clear_agent_uuid_cache, get_cached_agent_uuid, _agent_uuid_cache, plus
unused _UUID_LENGTH/_UUID_HYPHEN_COUNT/_HTTP_OK constants. Live exports
(format_error_response, ApiClient, ApiResponse) preserved.

mcp/schemas/__init__.py: removed 21 dead Pydantic schemas (JournalEntryInput,
TaskReflectionInput, DecisionOption, DecisionLogInput, LearningInput,
StruggleInput, SendMessageInput, AskQuestionInput, ReportBlockerInput,
SendNotificationInput, TaskCreateInput, TaskAssignInput, TaskEscalateInput,
TaskBlockInput, TaskPauseInput, SessionCreateForTasksInput,
SessionLinkTaskInput, GroupCreateInput, UpdateDocInput, ProjectCreateInput,
ProjectUpdateInput). Only WriteDocInput (used by docs_server) preserved.

Net: -598 LOC, +22 LOC across the two files.
2026-05-02 03:29:35 +02:00
Renn F c3d325fe4b refactor: remove dead Test/CI route stack
Trashed roboco/api/routes/test.py, roboco/api/schemas/test.py, and roboco/services/test_runner.py — 744 LOC across 3 files. The routes (/test/run, /test/lint, /test/format, /test/typecheck, /test/build) were the API surface for the deleted roboco/mcp/test/test_server.py MCP. With that MCP server gone (Phase 4 T9) no caller remains: panel never used /test/*, and no Python service imports TestRunnerService outside the deleted MCP.
2026-05-02 03:25:02 +02:00
Renn F a82a4f9fd4 fix(.gitignore): anchor Python build artifacts; recover panel/src/lib (28 files)
The 'lib/' rule (intended for Python virtualenv at repo root) was matching panel/src/lib/, hiding the entire panel API client + utility tree from git. Anchored Python build-artifact rules to the repo root with a leading slash so they only match at the top level. Adds 28 panel/src/lib files that should have been tracked from day one.
2026-05-02 03:18:47 +02:00
62bda0c497 Gateway/full (#9)
* chore(gateway): scaffold gateway package and test layout

* feat(config): add gateway feature flags, coordination thresholds, commit-validator settings

* feat(gateway): add standardized response envelope with ok/error variants

* feat(gateway): add remediation hint catalog for tracing-gap and invalid-state errors

* feat(gateway): add per-role flow/do tool catalog with developer, qa, doc, pm, board configs

* feat(db): add gateway columns — active_claimant_id, heartbeat, pre_block snapshot, acceptance_criteria_status, qa_evidence_inspected

* feat(db): create gateway_triggers table for dispatcher decision logging

* feat(db): align canonical skill set; substitute qa_review -> code_review across agent seeds

* fix(db/008): make skill alignment in-place + idempotent; preserve column and existing custom skills

* feat(gateway): add claimant_lock for single-active-agent invariant with heartbeat staleness

* feat(gateway): add trigger_filter with stale-cleanup, claimant-queue, and cooldown rules

* feat(gateway): add tracing_gate with plan, progress, journal, acceptance_criteria, qa requirements

* refactor(gateway): drop per-file ruff ignores; refactor tracing_gate with dispatch table + GateContext

* feat(gateway): add merge_chain to resolve PR target by branch hierarchy depth

* feat(gateway): add commit_validator with min-length, banned-words, and conventional-shape hints

* feat(gateway): add evidence_builder for verb-response evidence and capped context_briefing

* feat(gateway): add Choreographer skeleton with per-phase verb signatures and DI protocols

* feat(runtime): add spawn_manifest builder for per-role pre-loaded tool registration

Introduces SpawnInputs dataclass + build_for_role(inputs) + write_manifest()
in roboco/runtime/spawn_manifest.py; reads role_config for allowed verbs/tools,
emits JSON manifest that SDK shim reads at container startup to eliminate ToolSearch.

* feat(runtime): wire gateway pre-spawn check (trigger_filter + claimant_lock) into orchestrator behind ROBOCO_GATEWAY_ENABLED flag

- Add GatewayTriggerTable SQLAlchemy ORM model to roboco/db/tables.py
  (matches existing table from migration 007_gateway_triggers_table)
- Add module-level gateway_pre_spawn_check() + helpers to orchestrator.py
  (gated: returns ("spawn", "gateway disabled") immediately when flag is False)
- Wire gateway check into _safe_spawn() — the single dispatcher choke-point
  for all agent spawns; QUEUE or DROP outcome logs and returns None (no spawn)
- ROBOCO_GATEWAY_ENABLED defaults to False; legacy behaviour is unchanged

* feat(agent_sdk): load tool-manifest.json at startup behind ROBOCO_GATEWAY_ENABLED flag (no agent-visible change yet)

Adds load_tool_manifest() to the SDK server that reads env at call-time
so gateway-enabled agents can obtain their pre-registered tool list at
startup; returns None when the flag is off, leaving the legacy briefing
path completely unchanged.

* fix(optimal_brain): skip indexing when source ID is None to eliminate roboco://journals/None spam

- Add `build_doc_source(kind, id_)` module-level helper in indexes/base.py that
  returns None when id_ is None instead of producing a "roboco://journals/None" URI
- Update abstract `build_source_uri` return type to `str | None` so subclasses
  can legitimately signal a missing ID
- Short-circuit `ingest()` and `_prepare_docs_for_batch()` in BaseIndexPlugin
  when `build_source_uri` returns None (debug log, no push to vector store)
- Fix JournalsIndexPlugin.build_source_uri: `kwargs.get("entry_id")` returns the
  kwarg value even when it is None, so fall back to doc_id before calling
  build_doc_source
- Fix ConversationsIndexPlugin.build_source_uri: return None when session_id is
  None rather than producing "roboco://conversations/None-unknown"
- Add 9 unit tests with a piragi-free conftest that stubs sys.modules

* fix(agent_sdk): inject X-Agent-ID header on notification-poller requests

Both `_check_pending_a2a` and `_auto_ack_a2a_notifications` in
`roboco/mcp/a2a_server.py` were calling the main API without identity
headers, causing orchestrator `Missing X-Agent-ID header` warnings on
`GET /api/v1/notifications/pending-a2a` and the ack-a2a POST.

Add module-level `AGENT_ROLE` constant (mirrors the existing `AGENT_ID`
pattern) and pass `{"X-Agent-ID": AGENT_ID, "X-Agent-Role": AGENT_ROLE}`
on both requests.

* fix(git): use ROBOCO_PUBLIC_BASE_URL for commit-trailer Links instead of hardcoded localhost

* fix(test_runner): call uv run pytest/ruff directly; add make to orchestrator Dockerfile as backstop

FileNotFoundError was propagating as a raw 500 when a project had `make test`
configured but make was not installed in the orchestrator container.

Two fixes:
1. Catch FileNotFoundError in _run_command and re-raise as ValidationError (400)
   with a clear message telling the operator to reconfigure the project command
   (e.g. replace 'make test' with 'uv run pytest').
2. Add `make` to the orchestrator Dockerfile runner-stage apt-get so projects
   that legitimately use make targets continue to work without reconfiguration.

* fix(api/git): resolve project by slug or UUID in git_log endpoint

Add _resolve_project_slug() helper to git routes that tries UUID
lookup first and falls back to slug, matching the pattern already
used in project routes. Apply to all four read-only git endpoints:
status, log, branches, diff.

* fix(a2a): auto-create conversation when conversation_id absent; reject empty IDs in URL builder

* fix(agent_sdk): default subagent model to parent agent's model from spawn manifest, not hardcoded haiku

Inject CLAUDE_CODE_SUBAGENT_MODEL env var into every agent container at
spawn time.  Claude Code ≥2.1.x reads this variable to override the
default Task (Agent) subagent model, which otherwise hard-codes
claude-haiku-4-5-20251001.  When the parent runs on a non-Anthropic
provider (e.g. Ollama Cloud / minimax-m2.7:cloud) that Anthropic model
is unreachable, so subagent dispatch fails.

The value follows the same provider-aware translation already used for
the --model CLI flag: Anthropic short names go through MODEL_MAP, and
non-Anthropic identifiers are passed verbatim.  To avoid calling the
class by name inside a @staticmethod, the shared translation logic is
extracted to the module-level _resolve_agent_cli_model() helper;
_resolve_cli_model() now delegates to it.

Verified: CLAUDE_CODE_SUBAGENT_MODEL is present and honoured in the
Claude Code 2.1.123 binary (grep confirmed the env-var lookup pattern
`if(process.env.CLAUDE_CODE_SUBAGENT_MODEL) return KK(…)`).

* chore(makefile): add quality and quality-fast targets composing every PR gate

* chore(quality): add import-linter dependency and gateway boundary contract

* test(property): scaffold tracing-completeness assertion (filled in Phase 4)

* Format test file to pass ruff check

* fix(gateway): drop Protocol scaffolding from choreographer skeleton; del-statements on unused stub args; clear vulture whitelist

* linting

* feat(gateway): Phase 1 dev cutover — ChoreographerDeps + give_me_work

Add ChoreographerDeps frozen dataclass (7 deps: task, work_session, git,
a2a, journal, audit, evidence_repo), refactor Choreographer.__init__ to
accept the bundle, implement give_me_work + _briefing_for via
evidence_builder.build_context_briefing, and add property accessors for
all deps. All Phase 2-4 stubs gain del-statements and still raise
NotImplementedError. 3 tests added and passing; mypy/ruff/vulture clean.

* feat(gateway): implement i_will_work_on handling pending, claimed, and needs_revision recovery

* feat(gateway): implement i_have_committed with plan-required precondition

Replaces the NotImplementedError stub with the real implementation: looks up
the agent's active task, enforces plan presence before recording, calls
task.add_progress, and returns a structured Envelope. Adds 3 unit tests
(records progress, no active task → invalid_state, no plan → tracing_gap).

* feat(gateway): implement i_am_done with smart catch-up and skill resolution

* feat(gateway): implement i_am_blocked (struggle + escalate) and i_am_idle (with unread soft-block)

* feat(gateway): add ContentActions for commit, note, say, dm, evidence with auto-inject and validation

* feat(api/v2): add /api/v2/flow/dev/* endpoints delegating to Choreographer

Six intent-verb endpoints (give_me_work, i_will_work_on, i_have_committed,
i_am_done, i_am_blocked, i_am_idle) under /api/v2/flow/dev/, each a thin
handler that delegates to Choreographer. Includes Pydantic request schemas,
EvidenceRepo Phase 1 stub (all methods return []), get_choreographer FastAPI
dep wired with all 7 service deps, and 8 unit tests (all passing).

* feat(api/v2): add /api/v2/do/* endpoints for commit, note, say, dm, evidence

* feat(mcp): add roboco-flow MCP server for intent verbs (Phase 1: dev verbs implemented)

* feat(mcp): add roboco-do MCP server for smart-wrapped content tools

* feat(runtime): mount per-agent tool-manifest.json on developer-container spawn; gateway flag enabled for devs only

* docs(prompts): rewrite developer role prompt for gateway-only verbs (~15 lines vs 49)

* chore(mcp): confirm dev manifest excludes legacy task/journal/notify/a2a tools (Phase 1 cutover; servers retired in Phase 4)

* feat(gateway): implement claim_review with inline evidence (kills #15) and qa_evidence_inspected tracking

* feat(gateway): implement pass_review with qa_notes/learning/evidence tracing gates

* feat(gateway): implement fail_review with issue list, tracing gates, and dev A2A handoff

* feat(api/v2): add /api/v2/flow/qa/* endpoints (claim_review, pass, fail, give_me_work, i_am_idle)

* feat(mcp): add QA verbs (claim_review, pass, fail) to roboco-flow MCP server

* docs(prompts): rewrite QA role prompt for gateway verbs; explicitly warn against grep-the-commit anti-pattern

* feat(runtime): enable gateway flag for QA-role spawns (Phase 2 cutover)

* feat(gateway): implement claim_doc_task and i_documented with file-list and notes-min-chars gates

* feat(gateway): implement triage (cell PM) and triage_all (main PM) with priority order

* feat(gateway): implement unblock with pre_block_state restoration (kills #23)

* feat(gateway): implement cell_pm_complete with auto-merge to parent branch (kills #22 for cell scope)

* feat(gateway): implement main_pm_complete (open master PR + escalate to CEO)

* feat(gateway): add complete() dispatcher routing to cell_pm_complete or main_pm_complete by role

* feat(gateway): implement escalate_up routing by role.escalation_target

* feat(api/v2): add /api/v2/flow/{documenter,cell_pm,main_pm}/* endpoints

* feat(mcp): add Doc + PM verbs to roboco-flow MCP server (claim_doc_task, i_documented, triage, triage_all, unblock, complete, escalate_up)

* docs(prompts): rewrite Doc, Cell PM, and Main PM role prompts for gateway verbs

* feat(runtime): enable gateway flag for Doc, Cell PM, and Main PM roles (Phase 3 cutover)

* test(integration): full pending->awaiting_ceo_approval test through dev/QA/doc/cell-PM/main-PM gateway path

* chore(tests): rename unused args to _args in flow_server tests

Cleared RUF059 lint blocker for Phase 3 closeout. The destructured args was only consumed in URL-asserting tests; one variant only checks kwargs["json"], so its args is now _args.

* chore: untrack docs/superpowers/ + add to .gitignore

Plans + spec were inadvertently swept into commits 5d41a4b and de0c5b5 by subagent 'git add -A' calls. Removed from index and gitignored going forward; files remain on disk for ongoing reference. They still exist in history of those two commits — invoke a follow-up filter-repo if a full purge is desired.

* feat(gateway): implement Board escalate_to_ceo with role allow-list

Allows main_pm, product_owner, and head_marketing to escalate tasks to
CEO. Enforces awaiting_pm_review state and journal:decision tracing gate.
Closes Phase 4 Task 1.

* feat(gateway): implement board_triage prioritizing strategic root tasks

Adds Choreographer.board_triage and TaskService.list_strategic_for_board.
PO and Head Marketing get curated lists of strategic-nature root tasks
in awaiting_pm_review. Closes Phase 4 Task 2.

* feat(gateway): implement auditor_triage surfacing long-running blocked-task anomalies

Adds Choreographer.auditor_triage and TaskService.list_long_running_blocked.
The Auditor surfaces tasks blocked >30min as anomalies for reflect-note
observation. Closes Phase 4 Task 3.

* chore(tests): add return + arg type annotations to gateway tests

All gateway test functions now have -> None and parameter annotations. Cleared 63 mypy [no-untyped-def] errors that pre-existed since Phase 1. Mypy now clean across tests/unit/gateway/.

* feat(api/v2): add /api/v2/flow/{board,auditor}/* endpoints

Board: triage, escalate_to_ceo, i_am_idle.
Auditor: triage, i_am_idle (read-only role).
Adds EscalateToCeoRequest schema with reason min_length validation.
Closes Phase 4 Task 4.

* feat(mcp): add Board + Auditor verbs to roboco-flow MCP server

Adds escalate_to_ceo MCP tool used by Board (PO + Head Marketing) and Main PM. Updates the implemented set in _validate_role_compatibility. Auditor uses the existing triage tool with role-routing in URL.

Closes Phase 4 Task 5.

* docs(prompts): rewrite Board (PO, Head-Marketing, Auditor) prompts for gateway verbs

All 3 board identity files + roles/board.md now use the slim, gateway-aware shape (no ToolSearch directive, no state-tool table). Auditor is explicit about its read-only scope. Closes Phase 4 Task 6.

* feat(runtime): enable gateway manifest for ALL roles (Phase 4 cutover)

Adds product_owner, head_marketing, auditor to GATEWAY_ENABLED_ROLES. Every spawned agent now gets a gateway manifest mounted at /app/tool-manifest.json. The legacy briefing path is dead. Closes Phase 4 Task 8.

* test(property): implement tracing-completeness assertion across smoke-test batch

Replaces Phase 0 stub. Asserts the 6 tracing-contract requirements on every
completed task: audit_log agent_id non-null per state-transition row,
DEVELOPER:TASK_REFLECTION journal entry, QA:LEARNING journal entry,
CELL_PM/MAIN_PM:DECISION_LOG journal entry, acceptance_criteria_status
covering every criterion with a referencing_artifact_id, and
qa_evidence_inspected = true.

Uses an in-memory ephemeral Postgres test DB (`roboco_test_<pid>_<rand>`)
provisioned per pytest session, not SQLite — the production schema relies
on Postgres-only types (UUID, ARRAY) the SQLite dialect cannot compile.
Tests requesting db_session/smoke_test_batch are auto-skipped when no
Postgres is reachable on localhost:5432; ROBOCO_TEST_DB_HOST/PORT/USER
override the endpoint.

Schema is built via Base.metadata.create_all + manual ALTER for the
acceptance_criteria_status / qa_evidence_inspected columns, NOT via
`alembic upgrade head`. This sidesteps two pre-existing layer-drift items
that block any fresh migration run today:

  1. Migration 001 declares the agentrole Postgres enum with lowercase
     values (qa, developer, ...) but the SQLAlchemy ORM binds
     Enum(AgentRole) to the StrEnum's uppercase NAMES — production DBs
     mask this by being bootstrapped via create_all and stamped at 001.
  2. Migration 008 runs UPDATE agents SET skills WHERE id over an
     agents.skills column that no migration in this chain ever creates.

Documented in conftest.py so a future migrations cleanup can find them.
Also notes that acceptance_criteria_status/qa_evidence_inspected are in
the DB schema (per migration 006) but are NOT mapped on the ORM TaskTable
nor on the Pydantic Task model — services that read them via
`task.qa_evidence_inspected` rely on those values being set on raw rows.
The property test uses raw SQL to read the columns directly, matching the
DB-level contract.

Closes Phase 4 Task 11.

Side change: pyproject.toml — adds asyncpg.* to the existing
[[tool.mypy.overrides]] ignore_missing_imports list (asyncpg ships no
py.typed marker), matching the convention used for redis, anthropic,
piragi, etc.

Test count: 1; backend: Postgres (localhost test DB).

* style(mcp/flow_server): single-line _post call after format pass

* fix(db): map 7 gateway columns from migration 006 to TaskTable + Task model

active_claimant_id, last_heartbeat_at, pre_block_state, pre_block_assignee, pre_block_metadata, acceptance_criteria_status, qa_evidence_inspected: present in DB since migration 006 but absent from the ORM mapping. Gateway code (tracing_gate, choreographer, claimant_lock) reads these via task.<attr>; without the mapping, runtime would AttributeError. Closes PHASE4-BUG-A.

* fix(db): repair alembic chain — neutralize 008, add 009 enum reconcile, ORM uses values_callable

Three coordinated changes that close PHASE4-BUG-B:

1. roboco/db/tables.py — introduce _str_enum() helper that wraps Enum() with values_callable=lambda obj: [m.value for m in obj]. Apply to all 23 StrEnum-typed mapped columns. ORM now serializes by .value (lowercase) to match alembic 001's declared enum values; default Enum() was using .name (uppercase) which never matched.

2. alembic/versions/008_align_skills.py — replace with documented no-op. The original migration referenced agents.skills, a column that has never existed in any migration (the agents table has capabilities, not skills). The substitution intent (qa_review -> code_review) was already satisfied statically in roboco/agents_config.py.

3. alembic/versions/009_enum_reconcile.py — new migration that:
   - Adds missing enum values: agentrole.system, team.fullstack, taskstatus.quarantined.
   - Detects uppercase drift from a Base.metadata.create_all bootstrap and rebuilds agentrole/team/taskstatus enums with lowercase members + USING lower(col::text)::enum on every column referenced. No-op if already lowercase.

Tests stay green: 281 passed.

* feat(services): backfill 36 gateway-shaped methods for Choreographer

The gateway Choreographer was wired to call methods that the underlying
services did not expose. This adds them as thin wrappers + queries (most
alias canonical methods; a handful are gateway-specific variants).

TaskService — 26 methods: aliases (submit_verification, submit_qa,
list_blocked_for_team, list_blocked_all_teams,
list_awaiting_pm_review_for_team, list_assigned_for_agent), agent
queries (agent_for, qa_agent_for_team, documenter_for_team,
cell_pm_for_team, get_active_task_for_agent, list_paused_for_agent),
triage queries (list_awaiting_main_pm_all, all_subtasks_terminal),
state setters (set_plan, mark_evidence_inspected, mark_agent_idle),
QA/Doc claim variants (qa_claim, doc_claim, qa_pass, qa_fail),
PM completion (cell_pm_complete with merge_commit), unblock with
state restore (unblock_with_restore), and escalation
(escalate, escalate_up_to_role). Also adds GatewayAgentView
dataclass that unifies DB and config-derived agent attributes.

JournalService — 4 methods: existence checks (has_decision_for_task,
has_learning_for_task, has_reflect_for_task) + write_struggle.

GitService — 4 methods: branch-keyed entry points (create_pr,
pr_merge, pr_target, diff) plus push_branch helper. Each derives
project + workspace from the task that owns the branch / PR.

WorkSessionService — 2 methods: files_changed + has_unpushed_commits.
PR existence is the proxy for pushed (no per-commit push column).

Choreographer: switched git.push(branch_name) call to push_branch()
to dispatch to the new gateway-shaped helper.

* test(services): unit tests for 36 gateway-backfill methods

Adds happy-path + edge tests for every method added in the prior
backfill commit. Total 61 new tests across:
  - tests/unit/services/test_task.py (36)
  - tests/unit/services/test_journal.py (8)
  - tests/unit/services/test_git.py (10)
  - tests/unit/services/test_work_session.py (7)

Each test mocks at the session boundary (no DB) and stubs adjacent
service methods via a dynamic _bind helper to avoid mypy
[method-assign] noise without resorting to type:ignore comments.

* test(gateway): switch dev catch-up assertion to push_branch

The Choreographer's catch-up sequence was renamed from git.push(branch)
to git.push_branch(branch) when GitService got a gateway-shaped helper
in the prior commit. This updates the existing assertion to match.

* feat(mcp): add roboco-git-readonly server with status/log/diff/branches

Slim FastMCP server exposing the four read-only git tools every role
needs (status, log, diff, branch_list) by forwarding to /api/v1/git/*
on the orchestrator. Replaces the read-only half of the legacy
roboco-git server; write operations now go through gateway verbs in
roboco-flow / roboco-do.

The endpoint shapes mirror the panel-facing API (project_slug,
include_remote, staged/file_path) so the same backend handlers serve
both human and agent traffic.

* refactor(mcp): delete legacy task/journal/notify/a2a/message/project servers

Phase 4 cutover: agents now reach every state-changing surface through
the gateway (roboco-flow intent verbs + roboco-do content tools), with
roboco-git-readonly + roboco-optimal + roboco-docs covering reads. The
seven legacy MCP servers + their handler trees are dead code from the
agent side, so they're removed:

  roboco/mcp/task_server.py            (1020 LOC)
  roboco/mcp/journal_server.py         (512 LOC)
  roboco/mcp/notify_server.py          (440 LOC)
  roboco/mcp/a2a_server.py             (790 LOC)
  roboco/mcp/message_server.py         (682 LOC)
  roboco/mcp/project_server.py         (667 LOC)
  roboco/mcp/tasks/  (handlers+utils)  (~4300 LOC)
  roboco/mcp/test/   (in-container runner; replaced by gateway evidence
                      + manual smoke)
  roboco/mcp/git/    (full server; read-only half migrates to the new
                      slim roboco-git-readonly module, write half is
                      owned by gateway verbs)

Orchestrator updates:
  - _generate_mcp_config registers only roboco-flow, roboco-do,
    roboco-git-readonly, roboco-optimal, and (for docs roles) roboco-docs.
    No more per-role legacy fan-out.
  - base_allow flips to mcp__roboco-flow__*, mcp__roboco-do__*,
    mcp__roboco-optimal__*, mcp__roboco-git-readonly__*. Role-specific
    allow lists are reduced to file IO scoping, since gateway verbs
    enforce role policy server-side.
  - TRACEABILITY_TRIGGER_TOOLS rewritten in terms of the gateway servers
    (mcp__roboco-flow__* / mcp__roboco-do__*) instead of the now-deleted
    per-tool list.

Test fix: tests/unit/services/test_a2a.py imported _handle_send_chat_message
from the deleted a2a_server. The four MCP-layer URL-builder tests (empty
conversation_id guard) are dropped — the equivalent boundary now lives
in /api/v2/do/* which has its own integration coverage. The two
service-layer nil-UUID guard tests are kept; they exercise A2AService
directly and remain meaningful (the panel still uses the v1 chat surface,
where a buggy caller could pass the nil UUID).

Net: ~9600 LOC removed from roboco/mcp/. quality-fast green:
338 tests pass, mypy clean, ruff clean. No /api/v1/* router changes —
those endpoints stay live for the panel UI which still uses every
lifecycle action; agents have no prompts that name them so the path is
dead code from the agent side.

* docs(claude.md): replace legacy MCP listing with gateway/verb-surface section

Phase 4 cutover: agents go through roboco-flow + roboco-do (gateway), not the deleted task/journal/notify/a2a/message/project servers. Document the verb surface per role + the Envelope response shape so future Claude Code sessions land in the correct mental model. Closes Phase 4 Task 13.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-05-02 03:11:49 +02:00
Renn F 254cc93fd5 It gets to documenter but QA fails false positive then it gets blocked because it needs to be awaiting_pm_review to be able to merge. It's almost there. 2026-05-01 05:41:10 +02:00
Renn F 7970020021 AI Providers 2026-05-01 03:09:11 +02:00
Renn F d15b7ae561 Enforcements, hooks and code quality 2026-04-21 17:48:45 +02:00