pr_reviewer was absent from GATEWAY_ENABLED_ROLES, so the spawn mounted no
tool-manifest and set ROBOCO_GATEWAY_ENABLED=false. The reviewer booted with
no flow verbs, could never claim its external-PR task, exited, and was
respawned on the same task every tick — an endless loop that burned tokens.
Add pr_reviewer to the set, plus a regression invariant asserting every
spawnable seeded role has a manifest (only the never-spawned roles —
prompter/secretary/ceo/system — may be absent) and a direct pr-reviewer-1
manifest test checking its claim_pr_review/post_pr_review verbs are present.
The panel half of the PR-review gate. A PrReviewQueue card on the Command
Center lists external PRs the org has reviewed and awaiting the CEO's call,
each with Supersede / Dismiss / View-on-GitHub. Hidden when empty.
- tasksApi.getExternalPrReviews / supersedeExternalPr / dismissExternalPr,
typed to the exact backend response shapes (GET /tasks/external-pr-reviews,
POST /tasks/{id}/supersede-external-pr, POST .../dismiss-external-pr).
- Mock mode returns [] so the queue hides and the actions are unreachable —
no mock-masked contract (the #194 trap avoided; verified FE paths/shapes
against the real routes + route ordering).
A notification can't be the gate: the reviewer is read-only and the CEO
decides what happens next. Backend for a real PR-review decision queue:
- post_pr_review now notifies the CEO (send_external_pr_reviewed_notification,
APPROVAL/HIGH, related_task_id) the moment a review lands — server-side
best-effort (the reviewer has no notify verb).
- TaskService.list_external_pr_reviews_awaiting_decision(): completed
external_pr reviews the CEO has neither superseded (confirmed_by_human) nor
dismissed (quick_context dismissed=1 marker).
- TaskService.dismiss_external_pr_review(): CEO declines → marker → leaves queue.
- GET /api/tasks/external-pr-reviews (PM+/CEO) and CEO-only
POST /api/tasks/{id}/dismiss-external-pr. Supersede already exists.
Panel queue wiring follows in the next commit.
The reviewer was one-shot: external_review_task_exists deduped on
(project, pr_number) only, so an external PR was reviewed exactly once ever —
a contributor pushing a fix never triggered a re-review (the review went
stale). Drive re-review off the PR's head commit instead:
- list_open_prs now returns head_sha (the change signal).
- ingest records the reviewed SHA as an external_pr_head=<sha> marker in the
review task's quick_context.
- external_review_task_exists is head-SHA aware: same SHA -> skip (unchanged);
new SHA -> open a fresh review (changed); no task yet -> first review;
legacy/markerless task or unknown SHA -> skip (never re-review on a guess,
so existing reviews don't re-fire after deploy).
No migration — reuses quick_context, like the supersede markers.
Multiple projects can map to ONE repo — a monorepo product's backend/
frontend/ux cells each have their own Project pointing at the same git_url.
The poll ingested per-project with a per-(project,pr) dedup, so one external
PR (e.g. #170 on github.com/rennf93/roboco) created one review task per cell
project — three identical reviews for the same PR.
Collapse active projects to one canonical project per distinct repo before
polling (_projects_one_per_repo, deterministic by slug so the pick is stable
across polls). A monorepo product now yields ONE review per external PR;
genuinely separate repos (multi-repo) each still get polled.
The build compose got ROBOCO_EXTERNAL_PR_ENABLED (default true) but the
registry compose's orchestrator env was missed, so a pull-based deploy would
have defaulted it off — inconsistent. Add the same external-PR block so both
deploy paths behave identically.
Wire the external-PR settings into the orchestrator env in both compose
files: ROBOCO_EXTERNAL_PR_ENABLED defaults to true (overridable via .env),
with require_human_confirm kept true and the poll-interval/allowlist knobs
documented inline. The config default stays off (safe for library use); the
deployment compose turns the read-only review on. The code default is
unchanged — only the shipped compose flips it.
Read-only: enabling this only discovers inbound external/fork PRs and posts
one change-request. The supersede that builds contributor code is still
CEO-triggered and human-confirm gated.
pr-reviewer-1 was the one agent with no dedicated image and no compose
builder service — it reused roboco-agent-base, which left it absent from the
compose files entirely (so it looked like the PR reviewer simply was not
there). Make it first-class for parity: add docker/agent-pr-reviewer.Dockerfile
(FROM the base — read-only reviewer, no extra toolchain), an
agent-pr-reviewer-image builder service in both compose files and the registry
compose, the image in the release workflow's publish list, and map
pr-reviewer-1 -> roboco-agent-pr-reviewer in the orchestrator plus its
lazy-build dockerfile map. Supersedes the earlier base-reuse mapping.
scripts/reflow_md.py joins hard-wrapped markdown prose to one line per
paragraph, leaving code fences, tables, headings, blockquotes, and front
matter byte-identical, and refusing to touch any file whose non-whitespace
token sequence would change (so it can only ever edit whitespace). Add
`make reflow-docs` (apply) and `make reflow-check` (fail if any in-scope doc
is hard-wrapped), and wire reflow-check into `make quality` so CI keeps the
manual ~75-char line breaks from creeping back in.
Markdown and editors soft-wrap on their own, so the manual ~75-char line
breaks across the docs added nothing but noise. Join wrapped prose, list
items, and paragraphs into single lines across 67 docs — README, CLAUDE.md,
deployment, usage, the RAG knowledge base, and the agent role prompts.
Whitespace-only: code fences, tables, and blockquote alerts are byte-identical
and the change is token-verified (no content altered). Applied with a
deterministic reflow tool (committed separately).
Also lands two doc edits that were awaiting commit: the measured under-load
resource numbers in usage.md and the pr_reviewer additions to the
org-structure RAG doc.
pr-reviewer-1 was the only role without an AGENT_IMAGES entry, relying on
the silent base-image fallback — which reads as a missing image. It is a
read-only reviewer (diff via API, grep, post one change-request; never runs
code), so the base image is the right fit. Map it explicitly to
AGENT_BASE_IMAGE so the choice is visible; no new image is built or
published. Behavior unchanged (get_agent_image still returns base).
The verb table predates both roles. Add pr_reviewer (give_me_work,
claim_pr_review, post_pr_review — read-only reviewer) and secretary
(human-only, i_am_idle only), note their content-tool restrictions, and
correct the canonical-source reference to lifecycle.intents_for_role.
A panel-wide UI task that asked for the UX/UI and Frontend cells was
decomposed into Backend + Frontend only — the Main PM judged the UI work
collapsible into Frontend and dropped UX/UI entirely. Its prompt gave full
cell discretion ('you decide which cells', 'most roots only touch one
cell') with no rule to honor cells the brief names.
Add a rule: when the brief, acceptance criteria, or PO/HoM handoff
explicitly call for a cell, the Main PM must create a subtask for each
named cell and never collapse one into a neighbour — its discretion covers
only un-named scope; a genuinely-unnecessary named cell must be confirmed
via escalate_up/dm, not silently dropped. Matching anti-pattern added.
The companion finding (cross-dev sequencing) needed no change: the Cell PM
prompt already keeps dependent units in one dev's lane and splits only
independent units, and cross-cell order is enforced by the dependency_ids
gate, not sequence.
README + deployment guide now show three ways to run RoboCo — pull the
pre-built images (docker-compose.registry.yml), build from source, or the
local-dev flow — instead of only the dev path. Add the ROBOCO_REGISTRY /
ROBOCO_VERSION knobs and the required ROBOCO_ENCRYPTION_KEY (which was
missing) to .env.example, and fix a stale piragi reference there.
usage.md drops the unmeasured per-agent RAM ceiling and records the figures
measured on the live stack: RAM is low and Ollama-dominated, storage is the
real cost (images share the agent-base layer). Idle numbers — peak under an
active task is best read live with docker stats.
The orchestrator spawned agents only by bare image names and built any
missing image from source on the host, so a deployment had to carry the
build context and a toolchain — there was no way to just pull and run the
images the release workflow publishes.
Add two settings (default empty = unchanged local-build behavior):
ROBOCO_AGENT_IMAGE_REGISTRY and ROBOCO_AGENT_IMAGE_TAG. When a registry is
set, the orchestrator spawns and ensures {registry}/roboco-agent-*[:tag] and
pulls (never builds) any image it lacks. Also adds the previously-missing
agent-secretary image to the lazy-build map.
Ship docker-compose.registry.yml: a standalone compose that pulls every
published image (GHCR or Docker Hub, pinnable version) and wires the
orchestrator to spawn the matching pre-built agent images. The existing
build compose files are unchanged.
The secretary agent image landed with the company layer (0.4.0) and is
built by docker-compose + referenced by the orchestrator, but the release
workflow's publish list was never updated, so it was absent from GHCR and
Docker Hub. Add it so a pull-based deploy has the full image set.
Add the 2.5-hour 'Working with RoboCo' build session (a conversation to a
shipped feature) as a second hero thumbnail beside the 26-min intro.
Update the agent count from 20 to 22 across the README, CLAUDE.md, usage,
the base agent prompt, the how-to guide, and the org-structure RAG doc:
the standing org gains the PR Reviewer (board-level, read-only), and the
on-demand Intake and Secretary are now counted. The org-structure doc
gains the PR Reviewer in the hierarchy, count table, board team, and
communication matrix. The historical 0.1.0 changelog entry is left as-is.
Rewrite the resource-usage section: drop the unmeasured per-agent RAM
ceiling (RAM is low and agents run few-at-a-time) and lead with storage —
the image set's shared base layer — which is what docker prune reclaims.
Scope close_pull_request repo resolution by project_id and thread the
umbrella's project into close-on-land, so a contributor PR is never
resolved (or closed) against a same-numbered PR in another project's
repo. Skip the comment + close PATCH when the PR is already closed, so a
retried sweep never re-posts the 'superseded' comment.
Require a non-cancelled descendant that actually landed a PR before
retiring the contributor PR, so an umbrella force-completed over a
cancelled code subtask leaves the contributor's still-valid PR open.
Run close-on-land from the always-on sweeper rather than the default-off
poll loop, so a supersede that lands after the feature is toggled off is
still reconciled. Serialize concurrent supersede triggers under a lock so
a double-click can't cut two branches / spawn two umbrellas. Anchor the
supersede marker checks to the marker line so appended CEO notes can't be
mistaken for the closed/dedup tokens. Make the fork-head branch cut
idempotent (forced refspec) so a commit-fail retry converges.
Also drop an importlib.reload(roboco.config) in a unit test that rebound
the settings singleton and leaked into the PM decision-window test.
When a supersede umbrella reaches COMPLETED (our own PR merged), close-on-land
retires the contributor's PR with a linking thank-you comment:
- TaskService.supersede_umbrellas_pending_close() finds landed umbrellas not yet
marked closed=1; mark_supersede_pr_closed() records the close (idempotent).
- orchestrator._close_superseded_prs runs in the external-PR poll tick: parses
the contributor PR# from the umbrella's quick_context and calls
GitService.close_pull_request(delete_branch=False) — we never touch the
contributor's fork branch. _parse_supersede_pr is unit-tested.
Completes the supersede flow: CEO authorizes -> fork branch -> Main PM -> cell
-> our PR -> CEO merge -> contributor PR closed + linked. ruff + mypy clean
(279); foundation + gateway suites green (5208).
A second adversarial review of the supersede flow found a feature-breaking HIGH
plus correctness gaps:
- HIGH: parenting the umbrella to the review task burned a MAX_TASK_DEPTH level
(review->umbrella->cell-PM->dev = depth 4 > 3), so the dev code task could
never be created and the work never reached a cell. Fix: the umbrella is now a
ROOT task; the contributor PR# + review link ride quick_context (also
simplifies close-on-land — no parent walk).
- MED: no dedup — a repeat CEO trigger created duplicate umbrellas / two racing
PRs. Fix: find_supersede_umbrella() makes the trigger idempotent (returns the
existing umbrella).
- LOW: supersede worked on un-reviewed/cancelled review tasks. Fix: require
review.status == COMPLETED (review-first).
- LOW: a partial failure could orphan a pushed fork branch. Fix: create the
umbrella before the push, and log the branch so any orphan is discoverable.
ruff + mypy clean (279).
The org takes over a reviewed external PR and finishes it itself:
- POST /api/tasks/{id}/supersede-external-pr (CEO-only) -> orchestrator
.supersede_external_pr: confirms the review task (this CEO action authorizes
running the contributor's code), cuts a roboco-owned branch off the fork head
(create_branch_from_pr_head — the only point untrusted code enters a roboco
branch), and creates the supersede umbrella.
- TaskService.create_supersede_umbrella: a planning task on the same repo,
parented to the review task (contributor PR# stays reachable for close-on-land),
carrying the pre-cut fork branch, handed to Main PM to delegate to a cell.
confirmed_by_human=True.
From there the work rides the normal lifecycle (Main PM -> cell -> our PR ->
QA/doc/PM -> CEO). ruff + mypy clean (279); app builds with the new route.
Follow-up: close-on-land of the contributor PR + an adversarial review pass.
An adversarial review of the feature found two blocking defects (both would
surface the moment external_pr_enabled is turned on) plus hardening gaps:
- HIGH: the enforcement legacy role-gate overlay OVERWROTE spec-derived roles,
so pr_reviewer was erased from the (in_progress->completed) edge it shares
with the PM self-complete gate — the review task could never complete. Fix:
UNION legacy + spec roles instead of overwriting (also preserves the legacy
'add roles' intent on every shared edge).
- HIGH: claim_pr_review routed claim+start through the verb runner, which hit
start()'s plan gate (planless review task -> None -> crash/respawn loop) and
auto-created+pushed a stray branch (violating the read-only/branchless
invariant). Fix: mirror QA's claim_review — a verb-body TaskService.pr_review_claim
does pending->in_progress with no plan and no branch.
- MED: add the pr_reviewer Write(*)/Edit(*) deny at the permission layer (it
ingests untrusted PR diffs — make read-only explicit, not implicit).
- MED: regenerate the verb-table artifacts (the schemas existed but the
generator had not been re-run; the agent prompt showed 'unknown' signatures).
ruff + mypy clean (279 files); foundation + gateway suites green (5205 passed).
The safe core of supersede: fetch a contributor PR's head via the GitHub
special ref refs/pull/{n}/head into a roboco-owned local branch and push it to
origin, so a dev cell can finish the work on a branch WE own and merge. We never
push to the contributor's fork. First point untrusted code enters a roboco
branch — callers invoke it only for a human-confirmed supersede.
At ingest, a non-empty external_pr_author_allowlist restricts which external
PRs are reviewed to those GitHub logins (case-insensitive). An empty allowlist
(default) reviews every external PR — safe because the review is read-only; the
confirmed_by_human gate still guards any later supersede that runs fork code.
Unit-tested (_pr_author_allowed).
Make the reviewer verbs reachable and dispatched:
- flow_server: claim_pr_review / post_pr_review MCP tools + registry entries.
- flow API: a flow_pr_reviewer router (/api/v1/flow/pr_reviewer/{verb}) with
give_me_work / claim_pr_review / post_pr_review / i_am_idle, gated by a new
require_pr_reviewer dependency; request schemas ClaimPrReviewRequest /
PostPrReviewRequest; router registered in the app.
- orchestrator: _dispatch_pr_review_work routes PENDING source='external_pr'
tasks to the single global pr-reviewer-1 (no pre-claim — the reviewer claims
via claim_pr_review, which needs the task PENDING; guarded on is_agent_active)
+ _build_pr_review_prompt (read-only, trust-boundary framing) + the builder
map entry. _dispatch_pm_work and _dispatch_dev_work now skip
source='external_pr' so only the reviewer ever handles review tasks.
ruff + mypy clean (279 files); app builds with all four pr_reviewer flow routes;
foundation + flow-mapping suites green.
Implement the choreographer + service layer for inbound external-PR review:
- PRReviewerMixin (claim_pr_review, post_pr_review) composed into the
Choreographer MRO. claim_pr_review runs claim+start (pending->in_progress) and
returns the PR's unified diff INLINE; post_pr_review runs pr_review_done
(in_progress->completed) then posts ONE change-request to GitHub from the verb
body (a2a.send pattern), gated on a journal:learning entry.
- VerbRunner: _do_pr_review_done atomic handler -> TaskService.complete_review.
- TaskService.complete_review: validated in_progress->completed for the review
task, attributed to the reviewer.
- GitService.get_pr_diff: read-only unified diff via the GitHub API (the fork
code is never checked out or run).
- Enforcement: review tasks (source='external_pr') are branch-gate exempt for
claimed->in_progress (they do no git of their own, like coordination tasks).
ruff + mypy clean (278 files); composed choreographer imports with both verbs.
GitService.post_pr_review posts a single review via POST /pulls/{n}/reviews
(REQUEST_CHANGES by default; APPROVE/COMMENT supported) — the first /reviews
call in the codebase. Resolves owner/repo/token from the project slug,
authenticates as the PAT owner (Bearer), and raises GitError on any token or
GitHub failure so the calling side-effect can surface it. This is the capability
the pr_reviewer's post_pr_review verb invokes after its DB commit. httpx fully
mocked in tests (request shape, auth, error paths).
Add the dormant inbound path for external-PR review (gated by external_pr_enabled,
off by default):
- GitService.list_open_prs lists a project's open PRs, normalized with fork /
author-association classification (the inbound counterpart to the org's
outbound, head-filtered PR calls).
- TaskService.ingest_external_pr + external_review_task_exists create one
de-duped review task per newly-seen external PR (source='external_pr',
confirmed_by_human=False) — a gate so no agent fetches or runs contributor
code until a human confirms the PR.
- A poll loop in the orchestrator, mirroring the strategy-engine loop: only when
enabled it lists each active project's open PRs, ingests the external ones, and
wakes the dispatcher.
The trust-critical author/fork classifier is unit-tested; the GitHub-list and
DB-ingest paths are exercised by the integration gate.
Introduce the configuration contract for inbound external-PR review, mirroring
the strategy-engine block: external_pr_enabled (master switch, off),
external_pr_poll_interval_seconds (>=60), external_pr_author_allowlist, and
external_pr_require_human_confirm (default true). All inert by default — no
inbound GitHub call and no untrusted-code execution until the CEO opts in and a
human confirms an ingested PR.
Add an architecture doc for the company-in-a-box layer (charter, pitches,
strategy engine, cockpit signals, feature toggles) — none of it was in the KB
the agents query — and note the on-demand human-facing roles (prompter,
secretary) in org-structure so they are no longer invisible.
The agents' runtime KB had drifted three releases behind the gateway. Add the
two missing role docs (prompter, secretary — both live-session SDK chat roles,
human-only) and fold the AC/decomposition guardrails and per-dev code queues
into the high-traffic PM docs:
- task-model: acceptance_criteria_ids + parent_ac_refs fields and how the
child->parent AC link works.
- task-planning: covers_parent_criteria on delegate, the parent_ac_coverage /
unclaimed_parent_acs briefing fields, the decomposition-floor and roll-up
gates (safe-by-construction), and per-dev sequenced code queues.
- task-tools: same coverage note + correct stale verbs — QA is
pass_review/fail_review (not pass/fail), cell_pm gains reassign, and main_pm
no longer claims submit_up/reassign it does not have.
Promote the Unreleased section to [0.5.0] - 2026-06-16 — AC/decomposition
guardrails, per-dev sequenced code queues, the unified Business page, the 26
panel UI fixes, and the spawn/PR/ownership firefight fixes — and add the 0.5.0
compare link.
Correct the Removed note: the /cockpit, /company-goals, /secretary, and
/pitches panel routes are deleted (404), not redirected; the relocated strategy
signals are served by the new GET /api/cockpit/signals endpoint.
Bump the version 0.2.0 -> 0.5.0 across pyproject, __init__, config app_version,
panel package.json, and the uv.lock self-entry — these had drifted unbumped
since 0.3.0.
* [0c66b856] Frontend: Build tabbed Business page consolidating Goals/Secretary/Pitches (#183)
* [c9f00d0d] feat(business): add /business tabbed page consolidating Goals, Secretary, Pitches (#182)
- Create src/app/(dashboard)/business/page.tsx with URL-driven Tabs (goals|secretary|pitches), reading ?tab= via useSearchParams; defaults to 'goals'
- Create src/components/business/goals-tab.tsx: key-introspected form fields for objectives items and operating_policy (no raw JSON textareas), updated_at/updated_by metadata, skeleton loading, OfflineState on error
- Create src/components/business/secretary-tab.tsx: ReactMarkdown (GFM) chat bubbles, structured directive cards with labeled key-value rows, RequiredNotesDialog for reject, skeleton loading, OfflineState on error
- Create src/components/business/pitches-tab.tsx: sub-header Refresh button, PitchCard skeleton loading, OfflineState on error (not empty-state text), RequiredNotesDialog for both Approve and Reject
- Create src/components/ui/required-notes-dialog.tsx: Submit disabled on empty/whitespace, Cancel closes without action, state resets on each open via key pattern
- Update sidebar.tsx: remove Cockpit/Company Goals/Secretary/Pitches entries, add single Business entry (Building2 icon, /business)
- Replace company-goals/page.tsx, secretary/page.tsx, pitches/page.tsx with server-side redirect() to /business?tab=X
- Replace cockpit/page.tsx with notFound() (404)
- All tabs: shadcn Card + Skeleton, sonner toast for success/error
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
* [e3e5ff9b] feat(dashboard): add StrategySignalsPanel next to CeoApprovalQueue in a 2-column grid layout (#181)
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
---------
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
* refactor(panel): delete the consolidated old routes instead of stubbing them
cockpit/company-goals/secretary/pitches are fully consolidated into /business,
so the old route pages are dead code. Remove the four page.tsx files outright
rather than keep redirect/404 stubs — the clean move is to delete, not add.
The sidebar already points only at /business; no internal links reference the
old routes (the remaining /company-goals|/secretary|/pitches|/cockpit strings
are backend API paths the API clients call, unaffected). Old bookmarks now
resolve to Next's default 404, which is correct for a removed route.
* perf(cockpit): light /cockpit/signals endpoint for the Dashboard panel
The relocated Strategy Signals panel was calling /cockpit/summary, which runs
the whole fan-out (company goals + usage/spend + task-counts + pitches +
strategy assess) just to read the signals. Add CockpitService.signals() +
GET /api/cockpit/signals (CockpitSignals schema, same _COCKPIT_ROLES gate) that
runs only StrategyEngine.assess(), and repoint the panel (+ cockpitApi.signals()
client method, CockpitSignal type). Now the Dashboard fetches only what it
shows. Backend gated: ruff + full mypy + 6 cockpit tests green (live DB).
---------
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* [41219301] Fix 26 verified UI bugs in the panel dashboard (#174)
* [115788ef] API/state bugs batch 1 — PATCH fix, WebSocket reconnect, agent roster, sessions export, timestamp (#173)
* fix(orchestrator): launch agent MCP servers with uv run --no-sync
Agent MCP servers (flow/do/git-readonly/optimal/docs/search) are launched as
`uv run python -m roboco.mcp.<server>` with cwd = the agent's workspace clone.
When that clone's uv.lock drifts from the baked image, `uv run` re-syncs the
dependency set mid-spawn and the servers never reach "connected" — they sit at
status="pending", so the agent gets zero gateway verbs. It then can't claim,
commit, or even i_am_idle (all MCP verbs), so its Stop is rejected and it
respawns in a loop, re-doing work it can't submit.
UV_PROJECT_ENVIRONMENT pins the venv location but does not stop the cwd-relative
resolve/sync; --no-sync does, so the servers reuse the baked /app/.venv as-is
and start instantly. PMs were unaffected only because they run from /app where
the env already matches the lock.
* fix(agent): launch agent uv-run subprocesses with --no-sync
Agents with a write workspace (developer/product_owner/head_marketing/documenter)
run with cwd = their git workspace clone. Claude Code launches each MCP server
(flow/do/git-readonly/optimal/docs/search) and the SDK server as
`uv run python -m ...` from that cwd. When the clone's uv.lock drifts from the
baked image, `uv run` re-resolves and re-syncs /app/.venv against the clone's
lock — a multi-minute stall on a cold wheel cache — so the servers never reach
"connected": they sit at status="pending" and the agent gets ZERO gateway
verbs. It then can't claim/commit/idle (all MCP verbs), its Stop is rejected,
and it respawns in a loop redoing work it can't submit.
UV_PROJECT_ENVIRONMENT pins the venv location but does NOT stop the cwd-relative
resolve/sync (confirmed empirically on uv 0.11.1); `--no-sync` does, so the
servers reuse the baked /app/.venv as-is and start instantly. The /app-cwd roles
(qa/cell_pm/main_pm/auditor) were unaffected because their env already matches.
- orchestrator.py: --no-sync on all 6 generated MCP servers
- docker/scripts/sdk-startup-hook.sh: --no-sync on the agent_sdk.server launch
- test_spawn_strict_mcp.py: assert every server's args start with run,--no-sync
* [115788ef] fix(api): use PATCH not PUT in tasksApi.update(), remove WS double-increment, fix staleTime/roster id, remove sessions groupsApi dup, add < 1h ago label
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
* [db2c341f] UI/visual bugs batch 1 — priority labels, QA columns, DnD prompt, CEO dialog, dark mode (#172)
* fix(orchestrator): launch agent MCP servers with uv run --no-sync
Agent MCP servers (flow/do/git-readonly/optimal/docs/search) are launched as
`uv run python -m roboco.mcp.<server>` with cwd = the agent's workspace clone.
When that clone's uv.lock drifts from the baked image, `uv run` re-syncs the
dependency set mid-spawn and the servers never reach "connected" — they sit at
status="pending", so the agent gets zero gateway verbs. It then can't claim,
commit, or even i_am_idle (all MCP verbs), so its Stop is rejected and it
respawns in a loop, re-doing work it can't submit.
UV_PROJECT_ENVIRONMENT pins the venv location but does not stop the cwd-relative
resolve/sync; --no-sync does, so the servers reuse the baked /app/.venv as-is
and start instantly. PMs were unaffected only because they run from /app where
the env already matches the lock.
* fix(agent): launch agent uv-run subprocesses with --no-sync
Agents with a write workspace (developer/product_owner/head_marketing/documenter)
run with cwd = their git workspace clone. Claude Code launches each MCP server
(flow/do/git-readonly/optimal/docs/search) and the SDK server as
`uv run python -m ...` from that cwd. When the clone's uv.lock drifts from the
baked image, `uv run` re-resolves and re-syncs /app/.venv against the clone's
lock — a multi-minute stall on a cold wheel cache — so the servers never reach
"connected": they sit at status="pending" and the agent gets ZERO gateway
verbs. It then can't claim/commit/idle (all MCP verbs), its Stop is rejected,
and it respawns in a loop redoing work it can't submit.
UV_PROJECT_ENVIRONMENT pins the venv location but does NOT stop the cwd-relative
resolve/sync (confirmed empirically on uv 0.11.1); `--no-sync` does, so the
servers reuse the baked /app/.venv as-is and start instantly. The /app-cwd roles
(qa/cell_pm/main_pm/auditor) were unaffected because their env already matches.
- orchestrator.py: --no-sync on all 6 generated MCP servers
- docker/scripts/sdk-startup-hook.sh: --no-sync on the agent_sdk.server launch
- test_spawn_strict_mcp.py: assert every server's args start with run,--no-sync
* [db2c341f] fix(ui): priority badges, QA columns, DnD dialog, CEO label, dark mode
- priority-indicator.tsx: update labels P0→P0-Highest etc, add text-xs to all color strings, fix className operator precedence bug
- task-table.tsx: match priority label format and add text-xs to badge className
- kanban-column.tsx: show QA Pass/Fail buttons in VERIFYING column alongside AWAITING_QA
- kanban-board.tsx: intercept DnD drops onto NEEDS_REVISION/AWAITING_DOCUMENTATION to show notes dialog when showQaActions is true
- task-action-dialogs.tsx: change CeoApproveDialog Label from 'Approval notes' to 'Notes required'; fix all Cancel buttons to call handleOpenChange(false) so state is cleared on dismiss
- create-task-dialog.tsx: reset form when dialog is closed without submitting
- active-blockers-panel.tsx: add dark:border-red-900 dark:bg-red-950 dark:hover:bg-red-900 to blocker items
- notifications/page.tsx: add dark: Tailwind variants for NORMAL, HIGH, URGENT priority badge colors
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
* [123e2ec2] Fix remaining 15 UI bugs — revision pass after CEO rejection (#178)
* [cdb9b22a] fix(ui): task-header BACKLOG/NEEDS_REVISION actions, P0 priority label, chat-composer safe clear, and inline-edit double-mutation guards (#176)
- task-header.tsx: add BACKLOG ('Activate Task') and NEEDS_REVISION ('Start Revision') cases to getAvailableActions() switch so the Actions dropdown is never empty for those statuses
- draft-proposal-card.tsx: PRIORITY_LABELS[0] changed from 'Urgent' to 'Highest' to match backend contract
- chat-composer.tsx: move setValue('') inside try-block after onSend resolves; a failed send now preserves the textarea text
- acceptance-criteria.tsx: onMouseDown={(e)=>e.preventDefault()} on inline-edit save button to prevent onBlur+onClick double API mutation
- tab-dependencies.tsx: same onMouseDown guard on parent-task inline-edit save button
- tab-plan.tsx: onMouseDown guards on all inline-edit/add save buttons (ApproachSection, SubTasks, TechConsiderations, Risks, OpenQuestions)
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
* [01852d69] fix(dashboard): wire real agent status, refetch all 4 queries, error indicator, Coming Soon tooltip on search, sentinel div auto-scroll in message-list and mentor-chat, and New Report / Generate Report button mutations in reports-panel and auditor-dashboard — all 9 files fixed (#177)
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
---------
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
* [3f35f502] feat(tasks): add case activate and case start-revision to handleAction switch in task detail page (#179) (#180)
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
* fix(tasks): route Start Revision through the operator status override
The new "Start Revision" action on a NEEDS_REVISION task called
lifecycle.start (POST /tasks/{id}/start), which is assignee-only — so an
operator/CEO clicking it from the task detail page got a 403 ("Only the
assigned agent can start this task") instead of a transition.
Route it through useUpdateTask (PATCH /tasks/{id} with status=in_progress)
instead. The backend treats status as an audited admin override applied via
admin_set_status and gated on elevated (ASSIGN) permissions — the same
god-mode path the kanban board uses for operator status changes — so the
operator can nudge a needs_revision task back into progress for its assignee
to rework. Mirrors the existing kanban updateTask.mutateAsync shape.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
Per-dev sequenced queues (the prior commit) have a PM delegate a dev's whole
code queue up front, so a dev owns several pending, assigned-but-unclaimed code
leaves at once (seq0 + seq2). The orchestrator's lane barrier holds the seq2
SPAWN while seq0 is non-terminal — but _pending_assignment_guard rejected
i_am_idle for ANY pending assigned task, with no lane awareness. So a dev whose
current leaf just moved to QA (awaiting_qa) could neither idle (guard rejects)
nor proceed cleanly: it was steered to claim seq2 early (the claim path has no
lane/sequence check, since delegate sets `sequence` not `dependency_ids`),
jumping its own queue order, or it looped on the rejection. An adversarial
review of the queue work surfaced this; it is latent until PMs actually
delegate multi-item per-dev queues, so the green suite hid it.
Fix: TaskService.has_earlier_incomplete_code_sibling mirrors the orchestrator's
lane barrier in the service layer; _pending_assignment_guard now drops a dev's
lane-held pending code leaves (via _pending_blocking_idle / _pending_not_lane_held)
so the dev idles cleanly and the orchestrator spawns the next queue item when
the lane clears — preserving one-leaf-at-a-time, in order. `is not True` keeps
it inert under partial test mocks. Tests cover the service primitive (live /
terminal / higher-seq / non-code / missing-field) and the guard (dev idles when
lane-held; still blocks a non-lane-held pending leaf). Full mypy + xenon green.
The spec-2 change added `include_ac_coverage` to Choreographer._briefing_for
(_impl.py) but not to the typed stub in _protocol.py that the role mixins
inherit for static analysis. The composed Choreographer (board/doc/qa mixins +
_LegacyChoreographer) then had two incompatible _briefing_for signatures in its
MRO — caught by full `mypy roboco/ tests/` (not by a per-file check). Add the
same keyword to the stub. mypy clean at CI scope (648 files).
regenerate_verb_tables.py looped every role in ROLE_CONFIGS, emitting a
_generated/<role>.md for prompter and secretary too. Both intentionally keep
only note+evidence in role_config — their real tools live in their agent_sdk
drivers (intake: propose_draft; secretary: read_state/read_task/
submit_directive, the last gated through the backend /directives), and neither
uses the _generated/<role>.md prompt-composition path. So the generated tables
understated those roles and showed up as perpetually-untracked noise.
Skip the driver-based roles (_DRIVER_BASED_ROLES) in both the aggregate verbs.md
and the per-role file output, with a comment pointing at the real surfaces.
Regenerated verbs.md drops the two misleading sections.
True two-dev parallelism: a cell PM delegates the FULL set of code units up
front — each dev gets its own queue, both build at the same time, each works
its queue one task at a time in order. Replaces the old ceiling (≤2 code
subtasks per parent, one per dev) which structurally forced under-decomposition.
- Cap: `code` removed from `_SPINE_TYPE_CAPS` — no per-parent code cap (total
fan-out still bounded by `_SUBTASK_HARD_CAP=12`); `planning`/`documentation`
stay sequential at 1. `_same_assignee_rejection` exempts `code` so a dev may
own a queue, but still rejects an exact same-title duplicate (the accidental
re-delegation bug). `_spine_type_dup_envelope` simplified to the sequential
spine it now only serves.
- Dispatch barrier: `_blocked_by_earlier_lane_sibling` holds a dev's
higher-sequence pending code leaf while it still has an earlier non-terminal
code sibling under the same parent (keyed on assignee, gates only code) — the
dev works its queue in order. Wired into `_spawn_pending_dev`. Loop-free
(skip the tick, no reject/respawn) and best-effort (lookup failure → dispatch),
mirroring the existing merge barrier. The merge barrier is unchanged: leaf
PRs still merge serially in sequence order into the shared cell branch, so the
independent build lanes never wedge it.
- Prompt: cell_pm role guidance rewritten from the two-subtask-cap model to the
per-dev-queue model (delegate all units now; dependent units go in one dev's
queue, upstream first).
Independent per-dev queues (each lane advances at its own pace) rather than
strict cross-dev wave-sync, by design — more parallel and leaves the
wedge-prone merge barrier untouched. Pairs with the spec-2 idle coverage gate:
removing the code cap lets a PM claim every criterion up front, so that gate is
always satisfiable.
The decomposition floor that pairs with the roll-up gate (spec 4): a PM
cannot finish decomposing a parent while one of its acceptance criteria has
no subtask responsible for it — the "two leaves, half the ACs silently
dropped" pattern. Three parts:
- Gate: i_am_idle is rejected for a cell_pm/main_pm whose owned parent still
has criteria in unclaimed_parent_acceptance_criteria (claimed = referenced
by any live, non-cancelled child). Distinct from the roll-up gate, which
fires at submit_up/complete and demands a *completed* child; this fires
earlier and asks only that every criterion be *claimed*. Safe-by-
construction: inert until a PM declares coverage, so legacy / not-yet-
adopted decompositions are never blocked.
- Visibility: PM-facing briefings (give_me_work, i_will_plan, submit_up) and
every delegate response now carry parent_ac_coverage ({id,text,claimed,
verified} per criterion) + unclaimed_parent_acs, so a PM can map subtasks
to criterion ids via covers_parent_criteria and see what is still
uncovered after each delegate. Off for leaf roles, so a developer's own
criteria never surface as bogus "unclaimed" noise.
- Prompts: cell_pm / main_pm role prompts document covers_parent_criteria and
the new idle enforcement in the existing Coverage discipline.
TaskService.{parent_ac_coverage,unclaimed_parent_acceptance_criteria} added
beside uncovered_parent_acceptance_criteria; all three refactored onto a
shared _parent_ac_ref_sets helper (keeps each under the xenon B ceiling,
preserves the committed roll-up behavior). Verb tables regenerated for the
new delegate param — the regen also syncs pre-existing table drift that was
never regenerated after earlier merges (read_messages, pass_review
ac_verdicts, board pitch). Two brand-new generated tables (prompter,
secretary) are left untracked pending a separate decision.
The AC coverage + roll-up gates (specs 1, 4) are inert until PMs declare which
parent criteria each subtask covers. Thread covers_parent_criteria end-to-end so
they can: MCP delegate tool -> DelegateRequest schema -> cell_pm + main_pm routes
-> DelegateInputs -> child.parent_ac_refs. Additive/optional — no behavior change
until a PM populates it; the tool docstring instructs splitting the parent's
criteria across subtasks so their union covers all of them.
ruff + mypy clean; 107 delegate/flow_server tests green.
A parent could complete / submit_up / escalate_to_ceo once its subtasks were
merely terminal — never checking whether the parent's acceptance criteria were
actually satisfied. That's how PR #175's half-built umbrella sailed to CEO
approval (escalate_to_ceo had no subtask/AC check at all).
- TaskService.uncovered_parent_acceptance_criteria(parent): parent ACs not
covered by a COMPLETED child (via parent_ac_refs). Safe-by-construction —
returns [] unless a child declares coverage, so it is INERT for tasks
decomposed before coverage tracking and activates only once a PM maps
children to parent criteria. Cancelled children do not count.
- _parent_acs_covered_envelope wired into all four roll-up gates: cell_pm_complete,
main_pm_complete, submit_up, and escalate_to_ceo (the weakest — previously
only journal:decision). isinstance guard keeps it inert under partial mocks.
- 4 new tests; 57 task + 89 gateway tests green.
Pairs with spec 2 (coverage at decompose-time forces the linkage this enforces).
Foundation for the decomposition-coverage and roll-up AC-verification gates.
Acceptance criteria were a flat list[str] with no per-criterion identity, so
nothing could relate a child task's criteria to the parent's — letting a PM drop
half a parent's ACs unnoticed (PR #175).
- migration 036: additive acceptance_criteria_ids + parent_ac_refs array columns;
backfills stable md5(task_id:index) ids for existing rows.
- Task model + TaskCreateRequest + db table: the two fields.
- TaskService.create generates one stable id per criterion (1:1) when absent.
- DelegateInputs.covers_parent_criteria -> child.parent_ac_refs (the linkage),
propagated through create_subtask.
- regression-safe (53 task tests green) + 1 new test.
Coverage gate (spec 2), roll-up AC gate (spec 4), per-dev sequenced queues
(spec 3) build on this. Design: docs/SPEC_AC_GUARDRAILS_2026-06-16.md.
A developer that hits a wall calls i_am_blocked, which escalates the code task
to its cell PM (assigned_to=PM, BLOCKED) and snapshots the dev as
pre_block_assignee — the intended dev->cell-PM triage handoff. The in-band
recovery (unblock(restore=True)) hands ownership back to the dev. But the
OUT-OF-BAND paths — the operator PATCH /tasks/{id} status override and the
orchestrator's own _auto_recover_blocked_parent / _auto_resume_paused_parent —
go through admin_set_status, which set only status and never restored the owner.
The task re-entered pending/in_progress still owned by the PM, and the dispatcher
then execute-spawned the PM on a code task it cannot do ('break this down and
delegate' against the task itself) -> respawn loop.
admin_set_status now, when taking a task out of 'blocked' into pending/in_progress
with a pre-block snapshot present, routes through the existing
_apply_pre_block_restore primitive (the same one unblock(restore=True) uses) to
hand ownership back to the executor. Every other override is unchanged, and the
escalate/apply_escalation/block-down path is untouched, so the dev->cell-PM
handoff still works. + 2 regression tests.
open_pr -> GitService.create_pr posted "base": parent straight to GitHub, so
when the parent (an ancestor task's integration branch) was never pushed — a PM
paused before its first push, or the workspace was wiped — GitHub 422'd "base
field invalid" and stranded every child PR. The base-existence fallback added
in 3d9dd298 lived only in create_pull_request, which open_pr never calls.
Add _ensure_base_on_remote and call it in create_pr: if the base branch is
absent on origin, create it off the default branch's tip (preserving the
integration hierarchy) instead of failing; fall back to the default branch only
if that create push itself fails. Covered by 3 new tests.
Agents with a write workspace (developer/product_owner/head_marketing/documenter)
run with cwd = their git workspace clone. Claude Code launches each MCP server
(flow/do/git-readonly/optimal/docs/search) and the SDK server as
`uv run python -m ...` from that cwd. When the clone's uv.lock drifts from the
baked image, `uv run` re-resolves and re-syncs /app/.venv against the clone's
lock — a multi-minute stall on a cold wheel cache — so the servers never reach
"connected": they sit at status="pending" and the agent gets ZERO gateway
verbs. It then can't claim/commit/idle (all MCP verbs), its Stop is rejected,
and it respawns in a loop redoing work it can't submit.
UV_PROJECT_ENVIRONMENT pins the venv location but does NOT stop the cwd-relative
resolve/sync (confirmed empirically on uv 0.11.1); `--no-sync` does, so the
servers reuse the baked /app/.venv as-is and start instantly. The /app-cwd roles
(qa/cell_pm/main_pm/auditor) were unaffected because their env already matches.
- orchestrator.py: --no-sync on all 6 generated MCP servers
- docker/scripts/sdk-startup-hook.sh: --no-sync on the agent_sdk.server launch
- test_spawn_strict_mcp.py: assert every server's args start with run,--no-sync
Agent MCP servers (flow/do/git-readonly/optimal/docs/search) are launched as
`uv run python -m roboco.mcp.<server>` with cwd = the agent's workspace clone.
When that clone's uv.lock drifts from the baked image, `uv run` re-syncs the
dependency set mid-spawn and the servers never reach "connected" — they sit at
status="pending", so the agent gets zero gateway verbs. It then can't claim,
commit, or even i_am_idle (all MCP verbs), so its Stop is rejected and it
respawns in a loop, re-doing work it can't submit.
UV_PROJECT_ENVIRONMENT pins the venv location but does not stop the cwd-relative
resolve/sync; --no-sync does, so the servers reuse the baked /app/.venv as-is
and start instantly. PMs were unaffected only because they run from /app where
the env already matches the lock.