task_to_response (the builder behind the task list + detail endpoints the panel
reads) set dev_notes/qa_notes/quick_context but dropped pr_reviewer_notes,
doc_notes, and notes_structured, and TaskResponse didn't declare
notes_structured at all — so the PR-reviewer notes, documenter notes, and the
structured PR-review verdict were always blank in the panel regardless of what
agents persisted. The structured-content write-path + obligation gates were
fine; the data just wasn't serialized. Now returns every note section + the
structured source of truth. Regression test asserts all three round-trip.
The documenter writes and commits docs onto the task branch in its own
workspace clone, but i_documented had no push step — so the commit stayed
local and the PM merged the already-open PR without the docs, which then
vanished on merge. i_documented now pushes the task branch before handoff,
mirroring the developer's _ensure_branch_pushed; a push failure holds the
task in awaiting_documentation for a retry instead of silently dropping the
docs. Extract _finalize_documented to keep the verb under the return-count
ceiling.
Use cast("ProjectTable", project) at the 3 _resolve() call sites where
SimpleNamespace was passed as a ProjectTable argument, satisfying mypy
without any # type: ignore suppressions. ProjectTable import is kept
under TYPE_CHECKING since the quoted cast form requires no runtime
symbol; cast is imported from typing for the runtime call.
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
A self-heal fix task was opened confirmed_by_human=false and held out of
dispatch until "Approve & Start" — but that button only renders for a
board-reviewed Intake task (pending + board_review_complete + team != main_pm),
never for a self-heal task (team=main_pm, no board review). So there was no way
to start it: it sat in pending forever and the Main PM never picked it up.
Self-heal is RoboCo healing itself, not an Intake draft — it shouldn't need a
manual Approve & Start. Origination now opens the fix task confirmed + assigned
to the Main PM agent, the PM dispatcher's self-heal hold is dropped, and the
now-dead approve_and_start special-case is removed. The fix still ships through
the normal gates (dev -> QA -> PR review -> the CEO's merge); the loop never
starts, merges, or deploys.
The read-clone refresh reused the orchestrator's token-less best-effort fetch,
but _clone_repo scrubs the token from the remote URL — so on a PRIVATE repo the
refresh fetch failed silently and the clone stayed frozen at clone-time, never
seeing commits merged afterwards. The panel then showed "auto-derived defaults"
for a project whose .roboco/conventions.yml was already on the default branch.
Replace the refresh with a token-authenticated fetch + hard-reset to the default
branch (the token is injected transiently into the fetch argv, mirroring the
clone; the read clone is orchestrator-side and never mounted into an agent
container). A public repo with no token still refreshes unauthenticated.
Harden the architectural-conventions standard so it works out-of-the-box on
any project and resolves for projects that predate it, and make RoboCo pass
its own gate.
General defaults (apply to every project, not just one with a tuned file):
- The auto-scan excludes test and documentation trees (tests/, docs/) — those
legitimately define fixtures and aren't enforced code.
- Helper placement seeds at warn, not block: `helper` matches any top-level
function, too blunt a signal to hard-block a route file's small private glue.
Misplaced model/route/component stay block; the body-level thin_routes check
remains the real fat-handler guard.
- thin_routes no longer counts transaction-lifecycle calls (commit/flush/
refresh) as data access — an explicit `db.commit()` after delegating to a
service is a valid pattern.
- no_lint_suppressions exempts a small allowlist of structurally-unavoidable
framework codes (ruff TC001-TC003, pydantic prop-decorator); bare or other
suppressions still flag.
- CLAUDE.md rule-lifting skips bare common-word tokens that would match
everywhere (e.g. "commit"), keeping only specific identifiers.
- The ambient prompt block lists only constrained modules and truncates at a
line boundary with a "+N more" pointer instead of cutting mid-line.
Backfill: the standard previously read the committed file + repo scan from
project.workspace_path, a field only a manual API call set — so an older
project (or one whose workspace was cleared) showed an empty "missing" map no
matter what was pushed. The service now ensures a dedicated, default-branch
read clone on demand (WorkspaceService.ensure_read_clone) and resolves from
it, persisting the resolved path + real HEAD. The panel tab, the spawn-time
ambient block, and the per-task constraints all resolve the committed standard
with no manual setup.
Adopt in-repo: relocate the inline request/response models from the system and
*_live route modules into roboco/api/schemas/ so the codebase passes its own
placement gate, and ship a canonical .roboco/conventions.yml. no_models_in_routes
and modular_cohesion are now clean and enforced at block.
Docs updated across the user guide, the agent-facing RAG standard, the
developer and pr_reviewer role prompts, CLAUDE.md, and the changelog. New unit
tests cover the scan exclusions, helper-warn, the suppression allowlist, the
commit exemption, and the resolve/backfill path; the conventions + project
integration suites pass against Postgres.
Documenter docs only ever reached DOCS_BASE_PATH=/app/docs (a host-mounted, RAG-indexed knowledge store) and were never committed to the project's git repository — so the documenter's deliverable never landed in the repo. write_doc now also writes the doc into the agent's workspace clone under docs/<type>/<file> and commits it onto the task branch via GitService.commit, so it rides the existing PR into the repository on merge. Best-effort: a documenter without a cloned workspace or task branch still succeeds (logged, never fatal); the /app/docs knowledge store + RAG indexing are unchanged. Adds tests that the doc is written into the workspace and committed onto the task branch, and that it no-ops cleanly without a branch.
The baseline-constraint and conventions-map integration tests asserted 'no models in routers' — the old UNIVERSAL block default. Now that placement rules are scan-derived (only seeded where the target module exists), an empty-scan test project carries no_models_in_routers no longer; the universal block rule is no_lint_suppressions. Updated the five assertions accordingly; behaviour (baseline attaches, isn't suppressed, is idempotent) is unchanged.
The standard was architectural LINTING (placement + hygiene) — things ruff/eslint already do — and it forced backend rules onto frontend projects. This makes it enforce MODULARIZATION, the separation-of-concerns a senior demands that linters are blind to:
- modular_cohesion: a file that mixes architectural concerns (a model defined in a router, a schema in a component) is a monolith — split it. One concern per file.
- thin_routes (Python): a route handler that runs its own DB access instead of delegating to a service.
- thin_components (TypeScript/React): a component that fetches data in its body instead of using a hook.
- god_class: a class past a method-count threshold (single responsibility).
The checks inspect a definition's BODY and a file's COMPOSITION via tree-sitter, precision-over-recall (fire only on a confident structural signal). Rules are now scan-derived and language-aware: hygiene seeds universally, placement only for modules that exist, and modularity per stack — so a frontend project carries no_models_in_components + thin_components, never a backend no_models_in_routers. BUILTIN_RULES is reduced to language-agnostic hygiene.
An agent's make quality runs against no Postgres, so the conftest skips every integration test and coverage collapses far below the 80% threshold — a self-hosted PM read 71% on a suite that is ~96% with a DB and chased it as a code regression. _append_gate_env now injects ROBOCO_TEST_DB_* (host/port/user/password/admin-db) from the orchestrator's own DB settings into each spawn; agents share the Docker network so the host resolves, and the conftest creates throwaway test databases isolated from the live one. The app runtime reads ROBOCO_DATABASE_*, never ROBOCO_TEST_DB_*, so this only feeds the test harness. Gated on toolchain_match_enabled, the faithful-gate flag.
The toolchain guard fails open on a recorded 'unknown' status (precision over recall — never strand a task on an inconclusive smoke). But an 'unknown' means provisioning ran yet the smoke could not confirm the suite is collectable under the interpreter, so the gate was proceeding blind with no trace — a silent hollow pass. It now emits a 'toolchain.unverified_gate_pass' warning with the agent and task ids when proceeding past 'unknown', while still not blocking; a missing marker (None) stays silent so the warning carries signal.
test_crypto's round-trip tests called the real encrypt/decrypt path, which needs settings.encryption_key configured — so they silently depended on ROBOCO_ENCRYPTION_KEY being present in the environment. In agent gate containers it is not, so four tests failed there and the agent mis-read it as a code regression. An autouse fixture now monkeypatches a valid generated Fernet key, so the tests pass without any ambient secret and agents never need the production key injected to gate.
RoboCo's code imports tomllib (3.11+) and the stack runs on 3.13, but requires-python declared >=3.10. The toolchain resolver picks the lowest satisfying version, so it provisioned agent workspaces of the self-hosted roboco-api project at Python 3.10 — an interpreter the suite cannot even be collected under, leaving the workspace .venv unusable and the gate running in an ad-hoc fallback env. Raising the floor to >=3.13 makes resolve_target_python return 3.13, matching the agent image. Re-locks to drop the now-unreachable 3.10-3.12 backports; a guard test pins the repo's own resolution to 3.13.
* feat(conventions): standard schema models + effective-map merge
* feat(conventions): tree-sitter Python classifier + placement checks
* feat(conventions): TS classifier, hygiene/custom checks, runner + CLI
* feat(conventions): ROBOCO_CONVENTIONS_ENABLED flag + cache table + migration
* feat(conventions): repo auto-scan + scaffold draft renderer
* feat(conventions): ConventionsService (cache/baseline/ambient/scaffold/restore)
* feat(conventions): auto-scaffold on project registration (flag-gated)
* feat(conventions): TaskDescription.constraints + auto-baseline attach
* feat(conventions): ambient architecture-map injection at spawn
* test(conventions): subprocess CLI smoke for the agent-image entrypoint
* feat(conventions): block i_am_done on block-level convention violations
* feat(conventions): block pr_pass on unresolved convention violations
* feat(conventions): surface convention findings into QA evidence
* docs(prompts): convention awareness for PO/Intake/Dev/QA/PR-reviewer
* feat(conventions): panel Conventions tab + flag toggle + parity
* test(conventions): end-to-end block, fix, and waiver through the gate
* refactor(conventions): extract pr_pass guards to keep pr_gate under the gate
* style(conventions): format the baseline-constraints attach in task.create
* test(conventions): type-annotate test helpers for the full mypy gate
* build(conventions): ignore types-PyYAML in deptry (mypy-only type stub)
* docs(conventions): document the standard in CLAUDE.md + PM prompt awareness
* fix(conventions): baseline constraints are non-suppressible (dedup-append)
* feat(conventions): scaffold on first workspace clone (threaded workspace)
* feat(conventions): multi-project ambient map for PO/Intake (per-product)
* feat(conventions): persist findings + violations-feed route (migration 044)
* feat(conventions): panel violations feed in the Conventions tab
* test(conventions): intake-spawn mock accepts the ambient layer kwarg
* fix(docker): ollama-init best-effort pull, gate startup on cached models present
A degraded/slow ollama registry made the model manifest re-check fail under
set -e, so ollama-init exited 1 and blocked the orchestrator's
service_completed_successfully gate — taking the whole stack down even though
both models were already cached. Pulls are now best-effort; success is gated on
the models being present, so a flaky registry can't down a cached deployment.
* refactor(content): drop dead TaskDescription.with_baseline_constraints
The structured baseline-merge helper had zero production callers. Project-task baseline constraints are attached by the wired string backstop (TaskService._attach_baseline_constraints), and a real task is free-form prose that cannot form a valid TaskDescription (requires a non-trivial objective + non-empty the_work), so the helper was unreachable from any live path — a leftover from the structured-merge -> string-append design pivot. Removing it leaves a single enforcement path. The constraints field itself stays: it is a member of the well-formed-spec schema (Objective / What This Builds / The Work / Notes / Constraints / Acceptance Criteria), rendered by render_markdown and unit-tested.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(conventions): standard schema models + effective-map merge
* feat(orchestrator): park provider on persistent server overload (529/500)
A 429 rate limit already parks a provider — queue its spawns, probe until it
recovers — but a persistent 529/500/503 overload had no such break: the run
died and the orchestrator crash-retried straight back into the overload,
burning tokens in a respawn loop.
Generalize the park to provider-unavailability. On a non-graceful Anthropic
agent exit, match the API's overload markers (overloaded_error /
internal_server_error / "API Error: 5xx") against the dead container's own
output and park the provider with kind="overloaded"; the existing spawn gate
already queues any parked provider, and the probe-resume loop revives the task
when it recovers. Grok keeps its exit-75 path; both now route through one
_park_provider_unavailable helper. Markers are kept specific so an agent that
merely writes about HTTP 500/529 can't trip the break.
Fix the recovery probe to require a 2xx: it treated any non-429 as recovered,
so a probe that itself got a 529 would have resumed agents straight back into
the overload — wrong for the new path and for a 429 that lifts into a 5xx.
Gated by ROBOCO_OVERLOAD_BREAK_ENABLED (default on; off => crash-retry).
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
make quality runs mypy over tests/ too; the toolchain test fixtures
(monkeypatch params, the fake-subprocess factories) were missing annotations.
No behavior change.
Ties the pieces at the logic level: a target whose .python-version (3.13)
conflicts with requires-python (>=3.14) provisions against 3.14, a collection
error records 'broken', and the gate guard then refuses the pass. Flag-off
provisions exactly as today and never blocks.
Adds _toolchain_broken_guard: when toolchain matching is on and the acting
agent's workspace recorded a 'broken' status (the project's suite cannot be
collected under the provisioned interpreter), i_am_done, pass_review, and
pr_pass refuse with an i_am_blocked remediation instead of letting the role
"pass" on a source read. fail_review / pr_fail stay available. git gains
toolchain_status_for_task (resolves the actor's workspace, reads the marker,
fail-open None). Inert when the flag is off; a missing/unknown status never
strands a task.
When toolchain matching is enabled, install_dev_deps resolves the target's
Python and provisions the venv against it (uv sync --extra dev --python <v>,
uv auto-downloads), then runs a runnability smoke (uv run pytest --collect-only)
and records {python, status} in a .git/.roboco-toolchain marker the gates read.
Precision over recall: only a pytest collection error (the interpreter-mismatch
signature) reports 'broken'; pytest-absent / timeout report 'unknown' so a gate
never blocks on an inconclusive smoke. Flag off => provisioning is unchanged.
ROBOCO_TOOLCHAIN_MATCH_ENABLED (default-off) gates the whole subsystem.
Adds work_sessions.toolchain_python / toolchain_status (nullable VARCHAR(20))
recording the interpreter the workspace was provisioned with and whether the
target's suite can be executed (ok | broken | unknown). Migration 042 verified
with a real upgrade head + downgrade -1 + re-upgrade on a throwaway Postgres.
Pure resolver (services/toolchain.py) that derives the Python version an agent
should provision a target workspace with. Defends against uv's resolution
order — a .python-version file overrides requires-python during interpreter
selection — by honoring the pin only when it satisfies requires-python, else
resolving a concrete version from requires-python for the caller to pass via
--python. This is the root cause behind the live guard-core-app failure (pin
3.13 vs packages needing 3.14). Promotes packaging to a direct dependency.
Completes the note(scope='handoff') write-path (WIP 23e6ee57): every role
with a dedicated note section is now obligated to populate it, the same way
journals are obligated.
Obligations (foundation.policy.tracing):
- DEV_NOTES / PR_REVIEWER_NOTES / QUICK_CONTEXT_MIN_CHARS requirements +
checkers, wired onto i_am_done (dev_notes), delegate (quick_context), and
pr_pass / pr_fail / post_pr_review (pr_reviewer_notes).
- Fixes a latent bug: the docs-notes checker read dev_notes instead of
doc_notes (the documenter's section); the i_documented shim now feeds
doc_notes to match.
Auditor: a session-scoped note obligation on i_am_idle — the auditor owns no
delivery task and has no delivery verb, so it must have recorded an
observation within the window before going idle (JournalService.has_recent_entry).
Write-then-gate: persisted sections (dev_notes / quick_context) are
pre-written by the agent's note(scope='handoff') before the gated verb;
argument-borne sections (doc_notes / pr_reviewer_notes) are checked through a
SimpleNamespace shim, the same pattern qa_notes already uses.
Config: dev/pr_reviewer/quick_context min-chars (40/40/30), panel-tunable.
Plus per-gap remediation hints and full coverage (write-path routing,
ownership, validation->remediation, each obligation, the doc_notes fix).
Full make-quality green: 9777 passed, 95.6% coverage.
Three integration tests still asserted the old quick_context soup format for
completion_notes / escalation_notes — update them to read the new
orchestration_markers transition_note (the source moved in the prior commit).
Sweep found pitch slug was the one content-tool free-text field still outside
the anti-soup guard (a product could be slugged 'wip'/'asdf'). Add it to the
guard (min 2, so real short slugs like 'ui' pass). Also fix two pitch tests
that were false-passing on a 1-char title (the title guard rejected first):
give them substantive fields so they actually exercise the role gate
(not_authorized) and the non-cell-target rejection (invalid_state).
The structure-everything sweep found four more writers packing key:value soup
into quick_context (the human ResumptionNote field), same anti-pattern as the
already-fixed approve_and_start_notes:
- _record_completion_notes -> completion_notes:<text>
- escalate_to_ceo -> escalation_notes:<text>
- ceo_approve -> ceo_approval_notes:<text>
- ceo_reject -> ceo_rejection:<reason>
Route them through a unified orchestration_markers['transition_notes'] dict
(keyed by event) via markers.set_transition_note, so quick_context carries only
the structured ResumptionNote and the panel never shows raw <event>:<text> soup.
Adds the typed accessor + a roundtrip test.
The in-path PR-review gate created a deadlock: when an assembled cell→root /
root→master PR fails the gate (pr_fail) — or qa_fail / ceo_reject fires — the
PM-owned coordination task lands in needs_revision, which was developer-claim-
only. So the task had no actor and no exit but cancel, and the cell PM escalated
in a loop (8KB of [ESCALATED] dev_notes on one task). Pre-gate, the PM simply
re-delegated from in_progress; the gate routed the failure through the PM's own
task instead.
Add NEEDS_REVISION to the CELL_PM / MAIN_PM claim rules so the PM re-claims via
i_will_plan, revises the plan, and re-delegates the fixes — pr_fail/qa_fail
already reassign the failed task to its owning PM and the revision dispatcher
re-spawns it; the claim rule was the only missing piece.
Scope is by give_me_work routing (offers only the caller's own assigned tasks),
the same mechanism that scopes a developer's leaf-revision — NOT a gateway-only
ownership gate, which would violate the spec=gateway parity invariant and can't
use task_type anyway (main-PM coordination roots can be code-typed). Regenerates
panel/lib/lifecycle.json.
Two agent-authored fields were leaking non-developer content into the
human note columns the panel renders:
- apply_escalation appended '[ESCALATED] From X to Y\nReason: ...' to
dev_notes (the developer's space). On a re-escalation loop a stuck cell
PM grew one task's dev_notes to ~8KB across 5 escalations. It now writes
a structured orchestration_markers['escalation'] record; the target
still learns the reason from the escalate notification.
- approve_and_start string-packed 'approve_and_start_notes:<text>' into
quick_context (raw key:value soup). It now writes
orchestration_markers['approve_and_start_notes'], leaving quick_context
for the human ResumptionNote only.
Adds typed marker accessors (get/set_escalation, get/set_approve_and_start_notes)
and refactors _record_pr_review under the complexity bound by extracting
_compose_review_body. Documents update_task_with_message as the legacy
A2A-protocol log (dev_notes is intentional there, not pollution).
Extends structured-content enforcement from the content tools to every
flow verb that carries agent free-text, closing the last hole where a
dev/PM could pass word soup: i_am_blocked(reason), i_am_done(notes),
submit_up/submit_root/complete(notes), escalate_up/escalate_to_ceo(reason),
pass_review(notes), fail_review/pr_fail(issues), pr_pass(notes),
i_documented(notes), delegate(title/description). Plans (i_will_plan /
i_will_work_on) keep their existing >=150-char approach + sub_task gates
and are skipped here so recovery re-entry with thin values still works.
Shared helpers on the choreographer: _free_text_soup (bare envelope, list
aware) and _soup_or_decision_env (folds the soup check into a verb's
existing spec-gate return so no verb gains a return or tips the xenon
bound). reject_trivial now also catches all-filler multi-token strings.
base.md documents the broadened rule for agents.
The universal guard covered say/dm/note-text/progress/notify/pitch
problem+solution. Close the remaining content-tool fields the agent
authors: pitch title, open_session topic, pr_update title/body (when
supplied), and the decision/reflect narrative sub-fields of note
(rationale/context/what_done/...). Narrative fields are only checked
when the agent fills them — an omitted field keeps its tolerant
'(not provided)' placeholder so a thin note still records and never
trips the do-server circuit breaker. Fold pr_update's no-fields + soup
checks into one helper to stay under the return-count bound.
The single non-empty/non-placeholder gate caught a lone banned token
(wip) and below-floor strings, but multi-token soup made entirely of
placeholders (wip wip, tbd / na, todo todo todo) slipped through both
checks. Add an all-tokens-filler test that strips edge punctuation per
token and rejects when every meaningful token is banned — without
flagging real prose that merely contains a filler word (none of the
tests failed). Strengthens every content model + gateway anti-soup
guard that composes reject_trivial.
The PR-review gate's pr_fail sends a cell->root / root->master PR back to
needs_revision, still owned by the cell/main PM. The dispatch side
(_dispatch_revision_coordination_roots) re-spawns the owning PM for any
PM-owned needs_revision task, but the readiness gate
(_readiness_check_role_for_status) only waived the dev/doc role restriction for
coordination tasks — and a gate-failed assembled PR has a project + branch, so
it is NOT coordination. Result: dispatch routed the cell PM, readiness refused
it ("state=needs_revision requires role in {developer,documenter} but agent
be-pm is cell_pm"), and the task deadlocked. Pass owner_is_pm (derived from the
task's assignee, like the dispatch side) so the readiness waiver also fires for
a PM-owned revision, not just coordination roots. QA stays excluded.
The stale-claim reaper skips a task whose assignee holds an ACTIVE instance,
but that check reads the in-memory _instances registry — lost on an
orchestrator restart while the agent's container keeps running. The reaper then
released a task out from under a live agent it had merely forgotten (registry
amnesia). On a registry MISS, fall back to asking Docker directly
(_inspect_container_state); a still-running container is spared. A known
instance (active or stopped) stays authoritative, and an uninitialised registry
(unit-test harness) keeps the prior behaviour.
An agent that ran `uv sync` / `pip install` against /app rebuilt the
orchestrator's MCP-gateway venv (/app/.venv) — breaking its own gateway tools
(every roboco-flow/-do/-git verb), stranding it, and getting its task reaped.
Deny package-mutation verbs (uv sync/lock/add/remove, uv pip, pip install)
whose target resolves to /app (cd /app, --project/--directory /app, /app/.venv,
UV_PROJECT_ENVIRONMENT=/app). Placed outside the ROBOCO_GUARD_SKIP_GIT gate so
it fires for every provider — the Claude PreToolUse hook and the grok exfil
hook alike. Reads of /app and workspace installs are untouched.
Origination created the task with team=main_pm but no assignee, so after
the CEO's Approve-&-Start it fell to the unassigned-team routing, which
the orchestrator picks up slowly or never. Assign the Main PM agent up
front (the seeded foundation uuid) so the dispatcher routes it straight
to that agent via the assigned-PM path once confirmed. The
confirmed_by_human hold is unaffected — the dispatcher's self-heal skip
sits before the assigned/unassigned split, so the task stays inert until
the CEO approves it (now covered by a test).
The loop read the latest completed Actions run with per_page=1 and an
empty default workflow scope, so the conclusion flickered: on RoboCo's
8-workflow repo a green run from an unrelated workflow (or a green run on
an older commit) masked a red ci.yml run, and a single transient GitHub
error silently skipped the whole cycle — so self-heal sometimes fired on
a real regression and sometimes did not.
- Default self_heal_ci_workflow to "ci.yml" so the signal is scoped to
the gate workflow, not "latest run across all workflows".
- Fetch a window of recent completed runs and resolve the conclusion
against the branch's current HEAD (newest commit's latest attempt), so
a stale/unrelated green run can't mask the HEAD failure and a green
re-run supersedes the original failure.
- Retry transient network / 429 / 5xx errors within the cycle instead of
treating one blip as "all green".
* feat(lifecycle): add the in-path PR-review gate status + reviewer verbs
Insert awaiting_pr_review between the assembled-PR submit and the PM merge,
giving the merge level the rejection capability it structurally lacks — today
only qa_fail and ceo_reject ever reach needs_revision, so a PM review is a
merge button with no teeth.
- New Status awaiting_pr_review + submit_for_review / pr_pass / pr_fail actions
(pr_pass -> awaiting_pm_review, pr_fail -> needs_revision, mirroring the QA gate).
- Reviewer verbs claim_gate_review / pr_pass / pr_fail, and a main-PM submit_root
verb (the root analogue of the cell PM's submit_up; opens the root->master PR).
- Extend the self-review-symmetry validator to the new sign-off actions.
- Mirror the value into the ORM TaskStatus enum + the A2A state map, and add the
postgres taskstatus enum value (migration 040, forward-only like 037).
- Regenerate the per-role verb tables; add gate spec tests.
Spec surface only; the gateway methods + dispatch are wired in follow-ups, so the
verbs are advertised but dormant (flow_server tolerates unregistered verbs).
* feat(identity): add the three cell PR-review-gate reviewers
The in-path gate needs a reviewer per cell so each cell's assembled cell->root
PR is reviewed by a stack-specialized agent, while pr-reviewer-1 serves the
root->master gate (and keeps doing inbound external PRs).
- be/fe/ux-pr-reviewer: PR_REVIEWER role, team-scoped (so dispatch routes each
cell's gate to its own reviewer); seeded identities + ROLE_TEAM_RULES + names.
AI agent count 22 -> 25.
- They reuse the existing roboco-agent-pr-reviewer image (AGENT_IMAGES maps the
three slugs to it, as be-dev-1/-2 share one image) — no new image.
- Tracing table: pr_pass/pr_fail require a learning entry (parity with
post_pr_review), submit_root mirrors submit_up, claim_gate_review is waived
(its tracing applies on pr_pass/pr_fail) — completes the verb surface added
in the prior commit.
- Update the roster-pinning identity tests.
* feat(gateway): wire the in-path PR-review gate end to end
Make the assembled-PR review gate operational across the choreographer, the
TaskService transitions, and the v1 flow surface.
- TaskService: submit_for_review (in_progress→awaiting_pr_review), pr_gate_claim
(no-transition reviewer claim), pr_pass (→awaiting_pm_review), pr_fail
(→needs_revision); mirror qa_pass/qa_fail (clear claim, actor-mismatch warn,
issues appended for the PM's revision). VerbRunner gains the matching atomic
handlers + a create_root_pr side effect.
- Repoint submit_up to compose submit_for_review (cell→root PR enters the gate),
and add a main-PM submit_root verb (opens the root→master PR, enters the gate).
- Split main_pm_complete: a code root must pass the gate first (requires
awaiting_pm_review; rejects an in_progress code root toward submit_root and no
longer reopens the PR), while a branchless coordination root still walks
straight through, ungated.
- PRGateMixin (claim_gate_review / pr_pass / pr_fail) composed onto the
Choreographer; flow_server forwarders + v1 routes (pr_reviewer + main_pm) +
request schemas.
- Tests: gate spec + the updated submit_up / main_pm_complete expectations + new
real-DB integration tests driving submit_for_review→pr_gate_claim→pr_pass and
pr_fail through the real enforcement layer.
* feat(orchestrator): dispatch the in-path PR-review gate
Make the gate live in the dispatch loop.
- _dispatch_pr_gate_work: route awaiting_pr_review tasks to reviewers by level —
a cell→root task to its cell reviewer (be/fe/ux-pr-reviewer), the root→master
task to pr-reviewer-1. The reviewer self-claims via claim_gate_review (no
pre-claim, mirroring the external-PR dispatcher); registered in
_dispatch_all_work. _select_agent_for_cell learns the pr_reviewer role.
- _build_pr_gate_prompt: anchors the reviewer to the parent objective + full
acceptance criteria + the FE<->BE contract, then pr_pass / pr_fail.
- _readiness_check_role_for_status: awaiting_pr_review -> pr_reviewer.
- Fail routing: pr_fail reassigns the failed assembled task to its PM
(_revision_pm_for_task: cell PM for a cell team, Main PM for the root), and the
revision dispatcher is generalized from coordination-roots-only to any
PM-owned needs_revision task so the gate-failed task is re-coordinated instead
of deadlocking.
* docs: document the in-path PR-review gate + the cell reviewers (22→25)
Reflect the shipped gate across the canonical + RAG docs.
- CLAUDE.md: agent count 22→25, the cell reviewers in the org chart, an
awaiting_pr_review state + the gate transitions + a gate note in the lifecycle
section, and submit_root / claim_gate_review / pr_pass / pr_fail in the verb
surface table.
- docs/rag/architecture: org-structure (count, cell-reviewer roster, cells
table), agent-uuids (be/fe/ux-pr-reviewer rows), agent-model (role + team
rows).
- docs/rag/roles/pr-reviewer: the in-path gate section + the gate verbs.
- Wrap reviewer.id with UUID(str(...)) in the gate DB tests for mypy.
* docs: finish the gate doc sweep across README + RAG + generated artifacts
Catch the remaining surfaces beyond the canonical docs.
- README + how-to: agent count 22→25, the 6-agent cells (+ PR Reviewer), the
main reviewer's root→master gate role.
- RAG: permissions + tool-permissions + task-tools list the gate verbs
(claim_gate_review / pr_pass / pr_fail) for pr_reviewer; regenerate the
lifecycle artifacts (intent-verbs, status-transitions, the per-role
lifecycle-*.md prompts, panel lifecycle.json) from the spec via
build_lifecycle_artifacts.py so they carry the new status + verbs.
* fix(migration): shorten the 040 revision id to fit alembic_version VARCHAR(32)
The revision id '040_taskstatus_awaiting_pr_review' is 33 chars; alembic's
alembic_version.version_num column is VARCHAR(32), so recording the migration on
a real 'alembic upgrade head' failed with 'value too long for type character
varying(32)' (surfaced on the NAS deploy). The test suite missed it: the test DB
is built via Base.metadata.create_all and the parity test only renders SQL
offline, so nothing actually applied the migration chain.
- Rename to '040_awaiting_pr_review' (22 chars).
- Add a guard test asserting every revision id fits the VARCHAR(32) column.
- Verified by applying the full chain 001->040 against real Postgres: it now
reaches head and records '040_awaiting_pr_review' without truncation.
* fix(migration): land the actual 040 revision-id shortening + guard test
The prior commit captured only the file rename (git add aborted on the deleted
old path), leaving the long revision id and missing the guard test. This commit
carries the real content: revision id '040_awaiting_pr_review' (22 chars) and the
revision-id length guard. Re-verified against real Postgres — the full chain
reaches head and records the short id without truncation.
* fix(product): flush cell deletes before inserts when re-mapping projects
Editing a product's cell->project map (PATCH /api/products/{id}) 409'd with
'duplicate key value violates unique constraint uq_product_projects_product_team'
whenever a team already had a mapping. _replace_cells clears the old rows and
appends the new ones, but within a single flush SQLAlchemy orders INSERTs before
DELETEs for the same table, so the new (product_id, team) rows collided with the
not-yet-deleted old ones. Flush the deletes first.
Pre-existing bug (unrelated to the PR-review gate); surfaced on the NAS. New
real-Postgres regression test re-maps all three cells to different projects —
it fails with the unique violation without the fix and passes with it. The
existing update test only changed WHICH team was mapped, so it never collided.
* fix(gateway): let main_pm submit_root past the shared submit-up guard
submit_root reused the cell PM's _submit_up_ownership_guard, which
hardcoded agent.role != cell_pm and rejected the Main PM with
"submit_up is reserved for cell_pm". A branch-bearing code root could
then never close: submit_root bounced to complete, while complete
required awaiting_pm_review (reachable only via submit_root) and bounced
back — a circular rejection.
Both callers already run the spec gate (can_invoke_intent), which
enforces submit_up→cell_pm and submit_root→main_pm, so the guard's role
re-check was redundant for submit_up and wrong for submit_root. Broaden
it to accept either PM role as a defense-in-depth non-PM reject.
Adds the first choreographer-level submit_root test (the gap that let
this ship).
* fix(gateway): proactively steer both PMs to their bubble-up verb
The submit_root deadlock had a sibling steering gap: nothing told a PM
which verb opens the gate. The delegate next-hint said only 'i_am_idle
when done', and complete's in_progress rejection named submit_root for
the Main PM but left the Cell PM with a bare 'not ready for completion'
— no submit_up pointer, the same guess-the-verb trap.
- delegate hint now names the role-correct verb (root → submit_root,
cell parent → submit_up) proactively, before any rejection.
- cell_pm_complete's in_progress rejection now steers to submit_up,
mirroring the Main PM's submit_root gate hint.
Tests cover both the cell-PM steer and the role-aware delegate hint.
* docs: correct who-merges-which-PR across the gate docs + complete description
Audit of the gate docs found the merge actors mis-stated in several
places — the exact ambiguity that risks 'the reviewer/PM merges the root
PR' confusion:
- complete IntentSpec description said 'Main PM merges root PR' — false;
main_pm_complete escalates and the CEO merges root→master. Corrected
(propagated to intent-verbs.md, lifecycle.json, generated role prompts
via build_lifecycle_artifacts.py).
- task-tools.md: submit_up target was awaiting_pm_review (should be
awaiting_pr_review); Main PM flow had no submit_root — added it.
- README.md: lifecycle diagram now shows the awaiting_pr_review gate.
- cell-pm.md / main-pm.md: dropped the stale 'submit_up hands work to the
Main PM who merges your cell branch' model — the cell PM merges its own
gated cell→root PR; the Main PM owns the root + submit_root; the CEO
merges master. Added submit_root to the main-pm manifest.
- git-commits.md, pr-creation.md, tool-permissions.md, git-tools.md:
stopped attributing root→master PR opening to complete (it's submit_root).
No behavior change; verb wiring + state machine verified gap-free this
session (the pr_fail→needs_revision→PM respawn loop closes correctly).
* fix(orchestrator): stop closure respawn waiting the reaper window
A PM that finished its subtasks and idled left its parent 'paused' with a
fresh last_heartbeat_at. _is_recently_paused gated closure respawn on
_claim_heartbeat_ttl — the REAPER window (stale_claim_reap_seconds: 600s
default, 1800s on the NAS) — so the parent sat untouched for up to 10-30
minutes before its PM was respawned to close it. The whole chain stalled
behind it.
The race that guard actually protects against (i_am_idle auto-pauses, then
the agent is marked IDLE + its container tears down) is seconds, and the
live-session case is already covered by _is_agent_active. Introduce a
dedicated short debounce (pm_closure_recently_paused_seconds, default 45s)
and gate closure on that instead.
The existing test fixture masked this by setting _claim_heartbeat_ttl to
claim_stale_seconds (180s), not the production reaper value. Fixture now
mirrors production; adds a regression test that a parent paused past the
debounce but within the reaper window respawns immediately.
* feat(gate): post the in-path review verdict on the assembled PR
The in-path gate previously left no trace on the PR it gated — pr_pass /
pr_fail were pure status transitions. Now each verdict is posted as a
GitHub review on the assembled PR itself (server-side, bot account), so
the decision is visible on the very PR the PM merges.
- pr_pass → APPROVE, pr_fail → REQUEST_CHANGES on a cell→root PR.
- The root→master PR ALWAYS gets a plain COMMENT, never APPROVE/REQUEST_
CHANGES: only the CEO acts on master, so the gate must never leave an
approval that could satisfy branch protection (letting someone else
merge) nor a blocking review that could impede the CEO's merge.
- Best-effort and AFTER the DB transition — a GitHub failure is logged,
never rolls back the gate decision. Reuses git.post_pr_review's existing
self-review→COMMENT downgrade for the org's own PRs.
Adds _project_slug_for to the ChoreographerHelpers protocol (mypy) and a
unit suite covering event selection, the master-bound COMMENT rule, the
no-PR skip, and failure-swallowing. Docs updated (pr-reviewer, task-tools).
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The v1 flow role guards (_require_roles) are the sole gate for /api/v1/flow/*
but checked only the X-Agent-Role string — unlike get_agent_context, which
verifies the token. So a forged role header passed, and in strict mode
(ROBOCO_AGENT_AUTH_REQUIRED) the token was never required on these routes.
Call _check_agent_auth_token before the role membership test (deferred import
to avoid the cycle): missing token stays a no-op in header-trust mode, any
presented token is verified, and strict mode now requires it.
Supersedes #222 (antfleet-ops flagged the bypass); re-done as our own change so
it lands without the contributor CLA, and adds the positive valid-token test
their PR was missing.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
is_agent_owned_dir used a bare startswith on the encoded workspaces root, so a
sibling root that shares the prefix (e.g. -data-workspaces2 vs -data-workspaces)
was misclassified as agent-owned and its operator transcripts pruned. Require an
exact match or a '-' path-boundary prefix.
Supersedes #221 (the same fix from antfleet-ops, who flagged the over-match);
re-done here as our own change so it can land without the contributor CLA.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>