Files
roboco/docs/map/prompts-roles-taxonomy.md
T
3849c1737e feat(video): switch 0.19.0 renderer Remotion → HyperFrames (HTML-native, Apache-2.0) (#314)
* feat(video): rewrite sidecar render core to HyperFrames (in place)

* feat(video): convert motion compositions from Remotion TSX to HyperFrames HTML

* refactor(video): rename render client to video_renderer_client (renderer-agnostic)

* chore(video): rename remotion-renderer prose in test_video_pipeline docstrings

* chore(video): rename sidecar to video-renderer + add system ffmpeg for HyperFrames

* chore(video): rename stray remotion-renderer refs in sidecar + py docstrings (controller cleanup)

* chore(video): fix stale Remotion API names in Dockerfile comment (controller cleanup)

* docs(video): rewrite video-engine prose for HyperFrames + add map entry + folded prose fixes

* docs(video): add trailing newline to docs/map/video-engine.md (controller cleanup)

* chore(video): drop internal spec refs + minio/test suppressions (folded hygiene)

* fix(video): reclaim outDir on createRenderJob throw + hide empty 4th highlight

Final whole-branch review (Opus) triaged two FIX items from the SDD nits
ledger; the rest ship as-is.

- render.js: a synchronous throw from createRenderJob (post-mkdtemp, not
  awaited) left an empty outDir on disk — the outer catch only reclaimed
  extractDir. Reclaim outDir too when it exists, and correct the stale
  comment that claimed the out dir was never created.
- {vertical,square}.html: the 4th highlights <li> lived in the DOM hidden
  only by JS, so a no-JS / failed-script render would show an empty bullet.
  Start it style="display:none" and reveal on populate, so an unscripted
  render shows nothing instead.

Vitest smoke (release-announcement.test.js) 4/4 green; render.js syntax
checked. Python suite untouched by this fix (JS/HTML only).

* fix(video): type _override_db yield as AsyncSession | None

T7 widened _build_app's db_session param to AsyncSession | None (to drop the
4x # type: ignore[arg-type] on the DB-independent _build_app(None, ...) calls)
but left the inner _override_db fixture typed AsyncIterator[AsyncSession] —
so 'yield db_session' yielded AsyncSession | None into a declared AsyncSession,
and mypy failed at test_video_routes.py:177 ('Incompatible types in yield').

The DB-independent media tests pass db_session=None deliberately: their route
uses a monkeypatched task service and never awaits the session, so yielding
None is safe at runtime. Type the override's yield as AsyncSession | None to
match — no cast, no # type: ignore, no assert, runtime behavior unchanged.
The 3 media tests (3 passed) and the 19 db-gated tests (skipped locally) hold.

* chore(gate): skip .superpowers scratch in markdown prose gate

reflow_md.py walks the filesystem via rglob('*.md') and skips tooling dirs
(.venv, .mypy_cache, .pytest_cache, ...) but not .superpowers/ — the
superpowers SDD workflow's scratch dir (briefs, reports, progress ledger,
all gitignored). A dev running SDD locally would hit a false markdown-prose
gate failure on those transient files. Add .superpowers to SKIP_DIRS,
consistent with the existing tooling-scratch exclusions.

* fix(video): validate composition_id to close path traversal (CodeQL)

compositionId flowed unvalidated from the POST body into path.join
under extractDir/motion/compositions/, so a '../..'-style value could
escape the composition dir (CodeQL: Uncontrolled data used in path
expression). Validate at the trust boundary in server.js
(/^[A-Za-z0-9_-]+$/) and add a path.resolve + startsWith containment
check in render.js so it stays safe regardless of caller.

* fix(mcp): send X-Agent-Token + X-Agent-Team from flow/do servers

flow_server._build_headers and do_server._build_headers constructed
only X-Agent-ID/Role/Correlation-ID, omitting X-Agent-Token and
X-Agent-Team (unlike ApiClient._get_agent_headers used by the other
MCP servers). Latent since the gateway refactor — surfaced when
ROBOCO_AGENT_AUTH_REQUIRED=true was armed on the NAS, 401-ing every
flow/do verb with 'Missing X-Agent-Token header'. Add both headers
(mirroring ApiClient) so the HMAC gate passes. Tests assert the
headers are now injected.

* [video-engine] Per-project video_engine_enabled opt-in toggle

Mirrors ci_watch_enabled (migration 048): the global
ROBOCO_VIDEO_ENGINE_ENABLED flag arms the subsystem; the new
projects.video_engine_enabled column (migration 063) opts a repo into
authoring against its motion/ dir. VideoEngine._opted_in_project no-ops
open_video_task at the single chokepoint covering all three trigger
paths (on-release, on-spotlight, CEO on-demand) until the operator
flips it in the panel edit-project dialog. Existing projects stay
opted out (server_default=false).

* fix(auth): send X-Agent-Token + X-Agent-Team from all agent->API call sites

The prior fix (6ed4e139) covered the flow/do MCP servers but missed four
other agent->orchestrator call sites that built the header dict by hand
and omitted X-Agent-Token and/or X-Agent-Team. With ROBOCO_AGENT_AUTH_REQUIRED
armed on the NAS, every one 401s:

- agent_sdk/server.py: the session-end post-mortem flush
  (/api/journals/me/entries), A2A persistence + offline fallback
  (/api/a2a/*), and the stopped-without-transition auto-substitute
  (/api/tasks/auto-substitute) — all sent only X-Agent-ID/Role, so each
  401'd 'Missing X-Agent-Token'. Add a shared _agent_headers() helper
  (mirroring flow_server._build_headers) and route all four through it.
- agent_sdk/secretary_driver.py: _headers() sent the token but not the
  team, so the HMAC gate 401'd with signature mismatch (secretary is
  board-team; token signed with team='board', verified with team='').
  Add the team header.
- mcp/git_readonly.py: the read-only git MCP sent only X-Agent-ID/Role
  — no token, no team — so /api/git/* 401'd once auth was armed. Convert
  the static _HEADERS to a _headers() helper with team + token.
- runtime/orchestrator.py: the cell-PM auto-submit self-API call acted
  as a PM with a hand-built {X-Agent-ID, X-Agent-Role} dict — no token,
  no team — 401ing under auth-required. Add _agent_api_headers(uuid,
  role) mirroring _system_api_headers, and use it.

Tests: _agent_headers round-trip (token + team, team-omitted when None),
_agent_api_headers carries a signed PM token + team.

* [auth] Omit UNSIGNED self-call token in dev mode + video-engine test mypy fix

_agent_api_headers sent the UNSIGNED sentinel when ROBOCO_AGENT_AUTH_SECRET
was unset, but the dev-mode middleware rejects a presented-but-unverifiable
token with 401 signature mismatch (while accepting a missing one). The
cell-PM auto-submit self-call 401'd in every dev run, regressing
test_auto_submit_cuts_the_pm_turn. Attach the token only when a secret is
set. Also fix the FromClause.update mypy error in the per-project
video-engine opt-out test (ORM row load + flush).

* [auth] Omit UNSIGNED agent token at every agent->API call site

The orchestrator injects ROBOCO_AGENT_TOKEN=UNSIGNED when the HMAC secret
is unset at spawn. The API middleware rejects a presented-but-unverifiable
token with 401 'signature mismatch' even in dev mode (auth not required),
so forwarding UNSIGNED turned every flow/do/SDK/secretary/git verb into a
401 — the live pr_reviewer/i_am_idle signature-mismatch loop. Omit the
header when the token is the UNSIGNED sentinel at all five agent-side
header builders; dev accepts a missing token, prod 401s with 'Missing
X-Agent-Token' (the clear respawn-with-secret signal). Add a structlog
diagnostic on the middleware reject path so the next mismatch logs the
exact (id, role, team, token_unsigned, auth_required) inputs.

* [auth] Self-heal stale agent tokens at orchestrator startup

A token is signed once at spawn. If ROBOCO_AGENT_AUTH_SECRET drifts
afterwards (a .env change, a compose recreate that reloads the
orchestrator's env without recreating agent containers, an image
redeploy), the surviving agent keeps sending its old token and the
middleware 401s every verb with 'signature mismatch'. The container
stays alive heartbeating, so the reaper never reclaims it and no fresh
agent spawns: the fleet stalls.

_heal_stale_agent_tokens runs at startup (before _readopt_running_agents)
and kills each running agent container whose baked-in token no longer
verifies against the current secret, so normal dispatch re-spawns it
with a freshly signed token. Inert when the secret is unset (dev):
verify fails for every token without a secret, so the heal would kill
the whole fleet without this gate. Best-effort: a probe failure leaves
the container alone (the reaper still covers it).

* [auth] Sign agent token over the UUID, not the slug (pr_reviewer 401 root cause)

The token was signed over the agent slug (_append_agent_auth_env) while the
MCP servers send X-Agent-ID as the agent UUID (_generate_mcp_config, since
453a7ae2 — gateway v1 parses X-Agent-ID as Annotated[UUID]). The middleware
verified HMAC(uuid:role:team) against a slug-signed token → 'signature
mismatch', token_unsigned=false. Latent for 2 months until 6ed4e139/53391f22
made the MCP servers forward the token.

The c0328971 startup heal missed it: docker exec printenv reads the
container-level ROBOCO_AGENT_ID (the slug), so the heal verified the
slug-signed token against the slug → matched → didn't kill the stale
container, which kept 401ing (its MCP server sends the UUID).

Fix: sign the token over the UUID, set the container ROBOCO_AGENT_ID to the
UUID too (so the SDK server — which inherits container env, not the MCP
manifest env — sends UUID consistently), and resolve the container-env id to
its UUID in _heal_stale_agent_tokens so pre-fix stale containers are evicted
on next restart. Regression test: test_heal_kills_slug_env_container_with_slug_signed_token.

* [scan] gate A2A/notification/stream agent-id deps under cloud auth (C1)

* [scan] omit UNSIGNED agent token from MCP server headers (H1)

* [scan] fail loud when cloud auth and nginx CEO-token are both armed (H2)

* [scan] cache last-known-good auth-probe result in panel proxy (C2)

* [respawn] Tripped breaker self-heals after a cooldown

A DB-durable PM-respawn counter (migration 051 / e2f7097a) wedges forever
once tripped: the only reset was a task status change, which can't happen
while the breaker blocks the spawn. So a deploy that fixes the underlying
loop (auth/prompt/schema) couldn't clear the wedge without manual DELETE
surgery on respawn_tracker — the 2026-07-06 pr-reviewer-1 loop, where the
auth fix cleared the 401 but count=63 survived restart and kept skipping
the dispatcher spawn for an external-PR task.

Freeze last_check at the trip tick and, after pm_respawn_trip_cooldown_seconds
(default 300), let ONE spawn through. A still-wedged task re-trips after the
threshold (bounded re-burn ~3 spawns per window); a fixed one advances and
the status-change path fully resets. Restore re-stamps last_check to now, so
a freshly restored row still trips immediately — durability preserved, which
is why the migration-051 persistence tests still pass.

* [scan] fix test_deps callsites for cloud-auth-gate signature change (C1 followup)

* [scan] per-IP rate limit on /auth/login under cloud auth (L31)

* [scan] Phase 1 auth/security fixes under 0.19.0 CHANGELOG

* [scan] secretary token signs over real team (board) not empty — fixes /api/secretary/* 401 (L31-class)

* [scan] LoginRateLimiter: key off X-Forwarded-For first hop + redis-down fail-open test

nginx is the single entry point; request.client.host is the nginx peer IP,
collapsing every external client into one limiter bucket (self-DoS amp).
Read the downstream client IP from X-Forwarded-For (first hop) / X-Real-IP,
falling back to the peer. Adds coverage for the XFF keying, the redis-down
fail-open branch, and drops a redundant asyncio marker on a sync-TestClient
test.

* [scan] nits: describe login_max_attempts + replace cast with assert in get_current_agent_slug

login_max_attempts was the only bare cloud-auth field; add a Field
description matching the surrounding idiom. Replace cast('str', ctx.slug)
with a runtime assert that fails loud if the cloud-auth ctx invariant
breaks, and drop the now-unused cast import.

* [scan] secretary token: use get_agent_team resolver + complete spawn-shutdown mock team (0dfd45ca followup)

* [scan] require agent HMAC token under cloud_auth (close v1 flow/do header-trust)

* [scan] _require_ceo accepts CEO session cookie under cloud_auth

* [scan] HTTP require_panel_token accepts session cookie under cloud_auth

* [scan] gate /api/settings behind panel token

* [scan] gate unauthenticated /api read routes (agents/a2a-tasks/kanban/usage/rate-limits)

* [scan] hoist deferred test imports to top-level (clear PLC0415)

* [scan] Phase 1b e2e smoke + CHANGELOG

* [scan] add_dependency rejects self-reference + cycle (M18)

* [scan] WorkSessionService.create translates IntegrityError to ConflictError (H10)

* [scan] _qa_or_doc_claim locks the task row FOR UPDATE (M19)

* [scan] docs_complete + mark_pr_created lock the task row FOR UPDATE (H4)

* [scan] gate complete() IN_PROGRESS on leaf/branchless only (H3)

* [scan] _unclaim_from_blocked clears stale pre-block snapshot (H5)

* [scan] admin_set_status terminal guard + skip revision bump under force (M20)

* [scan] cell_pm_complete idempotent pre-check before merge (H7)

* [scan] wrap gateway post-runner side effects in try/except (H6)

* [scan] pass_qa/fail_qa accept AWAITING_QA only (L29)

* [scan] mark_pr_created passes audit_agent_id (L30)

* [scan] phase 2 e2e smoke - one scenario per finding

* [scan] phase 2 quality gate

ruff format + check: green
mypy roboco/: green (357 files)
pytest unit+integration: 6905 passed, 10 pre-existing DB-contamination
  failures (pass in isolation)
e2e smoke: 11 passed, 4 cross-scenario workspace-contamination failures
  (all 6 state-machine scenarios pass individually)

Quality-gate fixes:
- move function-local imports to module top (PLC0415)
- fix M19 regression: submit_for_qa clears active_claimant_id so the
  competing-claimant guard lets the QA claim through
- fix H7 regression: _StubGit gains is_pr_merged_for_task
- fix M19 unit tests: mock session.execute for the FOR UPDATE lock
- e2e H3: notes >= 20 chars; e2e H5: rich i_will_work_on inputs +
  PM unclaims (block reassigns to PM)

* [scan] move active_claimant_id clear into pass_qa/fail_qa + admin_set_status (M19 follow-on)

Phase 2 opus whole-branch review found the M19 follow-on clear lived in
the gateway wrappers (qa_pass/qa_fail) not the transition methods
(pass_qa/fail_qa) themselves. The direct REST routes POST /pass-qa and
POST /fail-qa call the transitions directly, bypassing the wrappers and
leaving the QA's stale active_claimant_id set in AWAITING_DOCUMENTATION
/ NEEDS_REVISION — the competing-claimant guard then rejects the next
legitimate documenter/QA claim. admin_set_status had the same gap for a
non-blocked override into a review/queue state (IN_PROGRESS->AWAITING_QA
left the dev's id, blocking qa_claim).

Root-cause fix: move the clear INTO pass_qa and fail_qa (mirroring
submit_for_qa), add a clear in admin_set_status when
new_status in _REVIEW_QUEUE_STATES and from_status != BLOCKED, and drop
the now-redundant clears + flushes from the qa_pass/qa_fail wrappers.
Every caller is covered; the wrappers keep their actor-mismatch warnings.

Covering tests: test_pass_qa_clears_active_claimant_for_doc_claim
(asserts a subsequent doc_claim succeeds), test_fail_qa_clears_active_claimant,
test_admin_set_status_into_review_queue_clears_active_claimant,
test_admin_set_status_non_review_queue_keeps_active_claimant. Updated
the two wrapper unit tests that asserted the wrapper clears (now the
transition's job).

* [C3] unindex_journal_entry + call from delete_entry

JournalService.delete_entry deleted the DB row but never de-indexed the
RAG chunks, so deleted/private journal content bled forever into RAG
answers and claim-time briefings. Add OptimalService.unindex_journal_entry
mirroring unindex_playbook (vector-store delete_by_source + tracking-row
delete via get_db_context, both idempotent + best-effort), and call it
from delete_entry after the row commit inside a try/except so a de-index
failure never errors the delete.

* [M25] learning_id hashes full content to avoid collision

The memory distiller emits lessons with a fixed 'Problem: …' opening
shape, so two distinct lessons whose first 100 chars match collided on
learning_id = f"lrn-{md5(content[:100])[:12]}". replace_on_reingest then
routed both to the same source URI and the second ingest's replace_chunks
DELETE wiped the first lesson's chunks — silent data loss.

Hash the full content (widening the hex slice 12→16) so distinct bodies
get distinct ids and each retains its chunks.

* [H13] reject non-internal local_llm_base_url at config load

* [M28] bulk-insert learning broadcast instead of N+1

* [M27] mark_read/mark_all_read stamp only the unread rows seen at call time

mark_read and mark_all_read used to zero the unread counter FIRST, then run
a bulk UPDATE … WHERE read_at IS NULL that stamped every inbound unread row.
A send_chat_message committing between the counter-zero and the UPDATE
inserted a new read_at NULL row that the UPDATE then stamped as read — the
new message was silently consumed while the counter stayed 0.

Mirrors get_unread_messages (same file): SELECT the unread message IDs at
call time, UPDATE exactly those IDs, then recompute the unread counter from
the DB via the existing _reset_unread_counter helper. A message arriving
mid-call is not in the selected ID set, so the UPDATE skips it and the
recomputed counter keeps it unread.

* [H12] dedup: exact to_agents predicate + purpose discriminator + ack DEL

* [M23] playbook indexed_ok/indexed_at + startup reconcile of unindexed approved

* [M24] RAG indexing dead-letter + janitor reclaim + failed_index_count health

* [L23] institutional_memory_status sentinel distinguishes below-floor/empty/error/disabled

* [L26] sweep_expired_notifications re-escalates stale unacked ack-required

* [phase3] e2e smoke + CHANGELOG for 0.19.0

* [M24] _reindex_journal_entry honors is_private (C1 review fix)

Dead-letter replay mirrors the original journal._schedule_rag_index path:
a private entry is never indexed into the shared JOURNALS corpus, and a
private learning is still recorded into LEARNINGS as non-shareable.
Previously the replay always called index_journal_entry and skipped
record_learning for private learnings, leaking private content on replay
and dropping the legitimate non-shared learning. Three regression tests.

* [H11] clone via git -c http.extraheader, not URL-embedded PAT

* [H11] _sync_read_clone fetch via http.extraheader, not URL-embedded PAT

Sibling site to the clone fix: the conventions read-clone refresh ran
'git fetch --tags <https://TOKEN@host> <branch>', exposing the PAT in the
fetch argv on the orchestrator host. Mirrors the clone site's per-call
'-c http.extraheader=Authorization: Basic …' prefix + bare URL. SSH URLs
and tokenless public repos unchanged.

* [H11] release_executor clone+push via http.extraheader; delete _inject_token_into_url

* [H8] rebase_onto_base gates on clean tree like pull

* [H9] _link_commit_to_task flushes, doesn't commit out-of-band

* [M38] _pr_is_merged returns None on HTTPError; caller assumes merged

* [M39] _cherry_unmerged_entry marker grep anchored to commit-prefix

* [L1] thread actor_agent_id through update_pr_for_task

* [H8] fix rebase test mocks for clean-tree gate

H8 inserted a 'git status --porcelain' dirty-tree gate at the top of
rebase_onto_base (mirroring pull). The 3 rebase control-flow tests mocked
_run_git with a side_effect list matching the OLD call sequence (no
leading status call), so every call shifted by one and the assertions
missed. Prepend a clean-status result to each list so the gate passes
and the fetch/checkout/reset/rebase/diff/abort/push sequence aligns.
Verified: 16 passed (was 3 failed/13 passed post-H8, 16 passed pre-H8).

* [L2] push --force-with-lease instead of bare --force

* [L1] refresh stale workspace-resolution docstrings

pr_target and _workspace_for_branch still documented the actor →
assigned_to → created_by fallback chain that L1 removed from
_resolve_workspace_agent_id. Update both to the post-L1 actor →
assigned_to → None resolver (project.workspace_path as the final
fallback) so a future reader doesn't rely on a fallback that no
longer exists.

* [M37] merge_pr locks the work_session row FOR UPDATE

* [phase4] e2e smoke + CHANGELOG for 0.19.0

* [phase4] fix M37 test flake + document H8 skip

The opus whole-branch review flagged the M37 concurrency tests as
~50% flaky: both asserted caller A wins the FOR UPDATE race, but
which caller wins the lock is non-deterministic. When B won, the
'assert a_row.merged_by == a_merger' branch flipped false even
though the production code (M37) was correct — exactly one merger
recorded, audit trail intact. Assert the invariant instead: both
rows COMPLETED, both report the same merged_by, value in
{a_merger, b_merger}. Applied to both the unit test and the e2e
twin. Also documents the H8 e2e skip in the module docstring (the
report claimed it was documented there but it wasn't) and drops
the internal 'Phase 4' label from the docstring header in favor of
the public '0.19.0' version anchor.

* [H24] wait_for_ci polls through the window on non-success

* [H25,L34] release mutex orphan-sweep on start + shared redis client

* [M1] tiktok _refresh commits rotated tokens in an independent session

* [H25] drop new type:ignore in orphan-sweep test (constraint cleanup)

* [M2] feature-spotlight re-arms when exploration stale past 2x interval with no live HoM spawn

* [M6,M7] mark_seen after meaningful+project; persist since_id cursor in redis

* [M3,M5] reject() guards COMPLETED; edited_body deferred into the single-flight lock

* [M4] bound list_completed_video_tasks + ix_tasks_source_status_created index (migration 066)

* [M8,M9,L9] pass head_sha to CI gate; _run_git 30s timeout; _commits_since split maxsplit 2

* [M10,L35] dedupe dep_update by (git_url, command); fold redundant per-project queries

* [L36] gather ci_watch telemetry sweep instead of sequential iteration

* [L11] document self_heal fingerprint is stable per-signal by design

* [M11] engine-loop liveness watchdog: heartbeat + 2x-interval staleness alert

* [M21] video render loop commits per-task, not one trailing commit

* [M22] _detect_stuck_tasks skips held-CEO-source tasks

* [L6] video_renderer_client._save writes temp + atomic rename

* [phase5] e2e smoke + CHANGELOG for 0.19.0

* [M11] instrument x_mentions + roadmap engine loops with liveness heartbeats

* [phase5] fix-wave: correct e2e M11 unit-test filename + strengthen failed-cycle heartbeat assertion

* [C4] panel WS: shared /ws/system socket + long-tail retry + pong watchdog

* [H15] video-post-queue caption derived per render (mirror x-post-queue)

* [C4-fix] panel WS: discriminating long-tail tests + drop dead freeze block + evict dead shared conn on manual disconnect

Finding 1 (Critical, websocket.test.ts): the two long-tail-retry tests fired onopen between close cycles, which reset reconnectAttempts to 0 each cycle, so they passed under the pre-fix 3-attempt gate. Rewrote both to NEVER fire onopen between closes, so attempts accumulates: test 1 asserts state stays 'reconnecting' past attempt 3 (old gate would flip 'disconnected' terminal); test 2 asserts a new socket is constructed within 30000ms at attempt 7 where uncapped 5000*1.5^7 ~= 85s (old uncapped code would leave the timer unexpired). Verified both FAIL on a reverted old-shape connection.ts and PASS on the fixed code.

Finding 2 (Important, connection.ts): the 'if (raw >= cap) this.reconnectAttempts = exp' block was a no-op (exp was just read from the same field) and the unconditional increment afterwards grew the counter regardless. Deleted the dead block; kept the Math.min cap on the delay. Replaced the misleading ponytail comment with an accurate one: delay is capped, counter grows unbounded but delay is bounded.

Finding 3 (Important, use-websocket.ts): manual disconnect() tore down the shared conn for all subscribers but left the dead (manualClose=true, never reconnects) entry in _sharedSockets, so a later mount hit the reuse branch, attached a subscriber, replayed 'disconnected', and never called connect(). Added a urlRef and _sharedSockets.delete(url) in the manual disconnect callback so a later mount reopens a fresh conn.

* [H16] settings Save wired to settingsApi (persist + read back)

* [H17] tasks page passes status/team/limit to useTasks (server-side filter)

* [H18] useAgents roster re-derives on live-status change (statusEpoch in queryKey)

* [M40] useMetrics reads agent counts from useAgentStatus cache (dedupe poll)

* [H18] tighten useAgents statusEpoch comment (drop spec ref)

* [M40] drop spec ref + tighten useMetrics comment

* [M41] scorecard refetchInterval 60s -> 5min (25 req/min -> 5)

* [M42] feature-flag off-transition confirm + pending-keys Set

* [M43] X/TikTok credentials clear-behind confirm dialog

* [M44] rate-limit syncFromApi merges (keep fresher hitAt) + A2A reconnect invalidation

* [phase6] proxy.ts cookie-check comment + CHANGELOG Fixed entries

* [phase6] drop stale WS pin-attempts comment + fix tasks-page lead-in

* [H21] type DelegateRequest.estimated_complexity as Complexity (reject critical)

* [H22] type SoftBlockRequest.resolver_type as BlockerResolverType (no silent AGENT fallback)

* [H23] serialize TaskTable.documents into TaskResponse (DocRefResponse)

* [L27] delete SubstituteRequest phantom suggested_role/suggested_team fields

* [L14] Envelope.not_found defaults remediate (guide re-fetch + re-issue)

* [L28] delete unused ListResponse generic (dead code; pagination deferred)

* [H19] _delegate_static_guards allow cell_projects roots (cross-cell MegaTask)

* [M13] MegaTask confirm-batch idempotency key from session_id (SETNX guard + result sidecar)

* [M14] strip assigned_to from MegaTask drafts (no board-owned root-subtask deadlock)

* [H20] thin_routes receiver-gate add/add_all/merge (no false block on set/cache.add)

* [M16] tighten noqa code-capture to [A-Z0-9, ]+ (no false block on natural prose)

* [M45] conventions read-clone force-refetch on read (no 30s stale map window)

* [L25] conventions._resolve returns (root, sha); ORM mutated on the event loop

* [M15] open_conventions_pr force-pushes disposable scaffold branch (no silent None)

* [L24] roadmap cycle completion emits status-transition audit

* [Phase7] CHANGELOG: 15 schema/conventions/MegaTask/API fixed (H21-H23,L27,L14,L28,H19,M13,M14,H20,M16,M45,L25,M15,L24)

* [Phase7] lint gate hygiene: shorten docstring (E501), sort imports (I001), hoist AuditLogTable import (PLC0415)

* [H14] Enable the GROK provider row in _apply_grok so routing reaches the GrokCliProvider

* [M31] Route GROK active-token resolution to usage.json so live usage reflects grok agents

* [M32] Pass cache read/write tokens to calculate_cost in the usage sweep so live cost reflects Anthropic cache spend

* [M33] Park Ollama-Cloud rate limits via a marker map so a glm-5.2:cloud 429 parks instead of crash-respawning

* [M34] Sweep orphan agent_spawn_sessions at startup so crashed-run tokens roll into usage/cost summaries

* [L12] Persist revisit_resets (migration 067) so the PM-respawn breaker's revisit counter survives a restart

* [L18] Date-gate the Sonnet-5 promo revert so billing returns to list rates after 2026-08-31

* [L20] Warn when ROBOCO_GROK_RUN_LOG yields no session id instead of silently falling back to a zero-usage env id

* [phase8] CHANGELOG: LLM provider routing, usage capture, billing fixes

* [phase8] Trailing ruff format hygiene (orchestrator marker tuples, token-sweep test signatures)

* [phase8] Fix mypy: rename GROK-branch tokens var so transcript fallback stays reachable

* [M35] Add an expiring agent-token format (iat/exp) with backward-compatible verify

* [M35] Wire agent-token TTL at spawn (config + orchestrator + grok) so tokens are bounded

* [M36] Add JWT jti claim and re-mint the sliding cookie only near expiry so a stolen cookie's exp is fixed

* [M36] Redis jti revocation: read_token rejects revoked jtis and logout revokes the current jti

* [phase9] CHANGELOG: bound agent tokens + sliding-cookie re-mint window + jti revocation

* [scan-fix] mypy: type-annotate test files for make-quality gate

CI's make quality runs mypy roboco/ tests/; the scan-fix program's local
gate ran mypy roboco/ only, so test files were never type-checked. Fix all
67 errors across 23 test files with real annotations/casts/asserts/dead-code
removal — no # type: ignore / # noqa added.

* [e2e] Per-test DB isolation + dispatcher re-claim before PM complete

* [scan] Regenerate verb tables for delegate Complexity type

* [scan] Reduce 9 xenon C-ranks to B (auth, orchestrator, gateway, services)

* [scan] Restore short-circuit time.time() in verify_agent_token (security path)

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-07 10:09:23 +02:00

48 KiB

Purpose

This slice is the prompt-composition pipeline and the role/team/permission taxonomy that feeds it. At agent spawn the orchestrator resolves an agent's role+team from the canonical foundation-derived maps in agents_config.py, then compose_prompt layers (in order) a tool-load directive, the autogenerated lifecycle verb surface, the universal base rules, an optional fable-mode doctrine layer (fable_mode_enabled), an optional ponytail build-laziness doctrine layer (bundled with Fable, role-scoped — developers get the full ladder, other roles get the ethos-only cut), the role prompt, the autogenerated per-role verb-signature table, the team prompt, the agent identity, and an optional architectural-conventions ambient block — writing the result to a per-agent .md file the runtime mounts. A separate prompt-injection guard (prompt_guard.py) denies poisoned incoming turns at the interactive input boundary for both Claude-SDK and Grok sessions, mirroring the bash UserPromptSubmit hook. agents_config.py is also the MCP-layer permission taxonomy (HMAC agent tokens, role/team helpers, escalation chain, A2A routing) that gates tool visibility and identity-binding at spawn.

Files

Path Role LOC
roboco/agents/factories/_base.py Layered prompt composer: loads/concatenates tool-directive + lifecycle + base + an optional fable-mode doctrine + an optional ponytail build-laziness doctrine + role + autogen-verbs + team + identity + ambient layers; exports PROMPTS_BASE_PATH, role/team/builtin-tool maps, compose_prompt, fable_doctrine_layer, ponytail_doctrine_layer, conventions_ambient_layer, make_slug 298
roboco/agents/factories/__init__.py One-line re-export shim pointing to _base 1
roboco/agents_config.py 691-line MCP-layer permission/taxonomy module: HMAC agent-token issue/verify, role+team+cell maps derived from foundation, escalation chain, ROLE_PERMISSION_LEVELS, ROLE_SKILLS, A2A routing helpers 691
roboco/agent_sdk/prompt_guard.py Reusable Python port of the bash injection guard: _PATTERNS regex list, detect_injection, refusal_message, CLI main (exit 1 on injection) for the grok entrypoint 93
agents/prompts/base.md Universal base layer: identity separation, gateway-verb-only action, envelope shapes, missing-key cheatsheet, resume-from-briefing, charter alignment, todo rules, ground rules 93
agents/prompts/doctrine/fable.md Vendored Fable-5 behavioral doctrine (from github.com/rennf93/opus-fable-playbook, MIT, YAML frontmatter stripped): communication/turn-discipline/autonomy-calibration/honesty/code-discipline/delegation/precedence sections; loaded only when fable_mode_enabled, injected right after base.md via fable_doctrine_layer() 47
agents/prompts/doctrine/ponytail.md Vendored Ponytail build-laziness doctrine for developers (from the ponytail plugin, MIT, Copyright (c) 2026 DietrichGebert, trimmed, YAML frontmatter stripped): the ladder (YAGNI → reuse-in-this-codebase → stdlib → native-platform → installed-dep → one-line → minimal), the rules, the Intensity table (lite/full/ultra), a 5-point RoboCo preamble that makes the ladder yield to placement / coverage gate / design bar / task hygiene / reviewer feedback, and the ponytail: comment convention; loaded only when fable_mode_enabled, injected right after the Fable doctrine via ponytail_doctrine_layer() for AgentRole.DEVELOPER only, with a trailing **Operative intensity: {ponytail_intensity}.** directive 88
agents/prompts/doctrine/ponytail-ethos.md Vendored Ponytail ethos-only doctrine for non-developer roles (same source/attribution, trimmed): the ethos rules and the RoboCo preamble (the 6th point guards free-text field obligations), with the code-mechanics rungs (the ladder) and the Intensity table removed so they can't leak into prose artifacts; loaded by ponytail_doctrine_layer() for every role except DEVELOPER; no intensity directive (ethos runs a fixed restrained stance) 40
agents/prompts/roles/developer.md Developer role prompt: implement-only identity, verb table (give_me_work/i_will_work_on/commit/open_pr/i_am_done/sync_branch/...), workspace path, behind-base -> sync_branch guidance, conventions waiver path, a pointer scoping frontend/ux_ui's ## Design bar to those teams only 25176
agents/prompts/roles/qa.md QA role prompt: review-only identity, claim_review/pass/fail/i_am_blocked verbs, ac_verdicts per criterion, circuit-breaker guidance 13355
agents/prompts/roles/documenter.md Documenter role prompt: docs-on-same-branch identity, claim_doc_task/commit/i_documented/i_am_blocked verbs, circuit-breaker guidance 10739
agents/prompts/roles/cell_pm.md Cell PM role prompt: coordinator identity, i_will_plan/delegate/complete/submit_up/unblock verbs, AC coverage gate, collision-surface declaration (intends_to_touch/adds_migration/touches_shared/depends_on), behind-base escalation 35682
agents/prompts/roles/main_pm.md Main PM role prompt: org-level coordinator, delegate/complete/submit_root/triage_all/unblock/escalate_to_ceo verbs, upstream-handoff precondition, branch-bearing vs branchless root gate 32363
agents/prompts/roles/pr_reviewer.md PR Reviewer role prompt: external-PR review (claim_pr_review/post_pr_review) + in-path gate (claim_gate_review/pr_pass/pr_fail), trust gate, conventions strictness 8129
agents/prompts/roles/board.md Board role prompt (PO/HoM/Auditor): strategic overseer, triage/escalate_to_ceo, no unblock verb, Auditor silent 12253
agents/prompts/roles/prompter.md Intake interviewer role prompt: CEO-only chat, propose_draft/propose_batch, MegaTask batch + per-cell-project-map drafting, root-subtask coordination-level AC guidance 13825
agents/prompts/roles/secretary.md Secretary role prompt: CEO chief-of-staff, gated-action confirm protocol, reading-free/prepare-direct/high-impact-bounce discipline 4338
agents/prompts/teams/backend.md Backend team layer: Python/FastAPI/Postgres stack, teammates, uv quality commands 983
agents/prompts/teams/frontend.md Frontend team layer: TS/Next.js stack, pnpm quality commands, ## Design bar section (taste-skill-distilled layout/typography/motion/spacing rules + 3 tuning dials + an "AI tells to avoid" list) 976
agents/prompts/teams/ux_ui.md UX/UI team layer: design-system focus areas, teammates, ## Design bar section (same taste-skill basis as frontend.md, plus a design-artifact-to-code handoff bullet) 1009
agents/prompts/identities/ 19 per-agent identity files (be-dev-1/2, fe-dev-1/2, ux-dev-1/2, be/fe/ux -qa/-doc/-pm, main-pm, product-owner, head-marketing, auditor): YAML id/name/role/team/cell/reports_to + scope blurb; loaded by slug 0
agents/prompts/_generated/lifecycle-developer.md Autogenerated lifecycle verb list for developer (regenerated from lifecycle spec by make lifecycle); lists sync_branch etc. 1498
agents/prompts/_generated/lifecycle-main_pm.md Autogenerated lifecycle verbs for main_pm; submit_root now branch-keyed not task_type-keyed 2034
agents/prompts/_generated/lifecycle-cell_pm.md Autogenerated lifecycle verbs for cell_pm 1839
agents/prompts/_generated/lifecycle-qa.md Autogenerated lifecycle verbs for qa 818
agents/prompts/_generated/lifecycle-documenter.md Autogenerated lifecycle verbs for documenter 736
agents/prompts/_generated/lifecycle-pr_reviewer.md Autogenerated lifecycle verbs for pr_reviewer 1071
agents/prompts/_generated/lifecycle-product_owner.md Autogenerated lifecycle verbs for product_owner 421
agents/prompts/_generated/lifecycle-head_marketing.md Autogenerated lifecycle verbs for head_marketing 422
agents/prompts/_generated/lifecycle-auditor.md Autogenerated lifecycle verbs for auditor (triage + i_am_idle) 325
agents/prompts/_generated/lifecycle-prompter.md Autogenerated lifecycle verbs for prompter (i_am_idle only — driver-based) 275
agents/prompts/_generated/lifecycle-secretary.md Autogenerated lifecycle verbs for secretary (i_am_idle only — driver-based) 276
agents/prompts/_generated/lifecycle-ceo.md Autogenerated lifecycle verbs for ceo (empty — human, not spawned) 181
agents/prompts/_generated/lifecycle-system.md Autogenerated lifecycle verbs for system sentinel (empty) 184
agents/prompts/_generated/developer.md Per-role autogenerated verb-signature table (Flow + Content tools) for developer, regenerated by scripts/regenerate_verb_tables.py from Pydantic schemas + role_config 2384
agents/prompts/_generated/qa.md Per-role autogenerated verb-signature table for qa (pass_review ac_verdicts carries BeforeValidator) 2040
agents/prompts/_generated/documenter.md Per-role autogenerated verb-signature table for documenter 2083
agents/prompts/_generated/cell_pm.md Per-role autogenerated verb-signature table for cell_pm; delegate signature includes intends_to_touch/adds_migration/touches_shared/depends_on 3270
agents/prompts/_generated/main_pm.md Per-role autogenerated verb-signature table for main_pm; delegate signature includes collision-surface fields 3316
agents/prompts/_generated/pr_reviewer.md Per-role autogenerated verb-signature table for pr_reviewer (claim_gate_review/pr_pass/pr_fail + claim_pr_review/post_pr_review) 1461
agents/prompts/_generated/product_owner.md Per-role autogenerated verb-signature table for product_owner (triage/escalate_to_ceo + pitch/notify) 1765
agents/prompts/_generated/head_marketing.md Per-role autogenerated verb-signature table for head_marketing 1765
agents/prompts/_generated/auditor.md Per-role autogenerated verb-signature table for auditor (triage/i_am_idle + note/evidence + approve/reject/archive_playbook) 1273
agents/prompts/_generated/verbs.md Aggregate reference doc of all per-role verb shapes (NOT injected at spawn; _base.py loads the per-role file instead); notes driver-based roles omitted 18302

Key Symbols

Name Kind File:Line Responsibility
_get_prompts_base_path function roboco/agents/factories/_base.py:17 Resolve project_root/agents/prompts/ from this file's location with a cwd-relative fallback
PROMPTS_BASE_PATH constant roboco/agents/factories/_base.py:37 Module-level cached prompts base path used by default in compose_prompt
_load_layer function roboco/agents/factories/_base.py:40 Read a prompt layer file, return '' if missing (graceful fallback)
_ROLE_LAYER_MAP dict roboco/agents/factories/_base.py:55 Maps role string -> roles/*.md filename; board roles all share board.md; prompter/secretary/pr_reviewer have own files
_TEAM_LAYER_MAP dict roboco/agents/factories/_base.py:77 Maps team string (backend/frontend/ux_ui) -> teams/*.md filename
_role_layer function roboco/agents/factories/_base.py:84 Load the role-specific prompt layer or None if role unknown
_team_layer function roboco/agents/factories/_base.py:93 Load the team prompt layer or None if unset/unknown
_autogen_verbs_layer function roboco/agents/factories/_base.py:104 Load _generated/.md autogenerated verb-signature table for the role
_BUILTIN_TOOLS_COMMON tuple roboco/agents/factories/_base.py:127 Built-in Claude Code tools every role gets: Read,Bash,Grep,Glob,TodoWrite
_BUILTIN_TOOLS_AUTHORS tuple roboco/agents/factories/_base.py:134 Authors set (developer/documenter) adds Edit,Write to the common set
_ROLE_BUILTIN_TOOLS dict roboco/agents/factories/_base.py:136 Per-role builtin-tool grant map; non-authors get common-only
_tool_load_directive_layer function roboco/agents/factories/_base.py:149 Build top-of-prompt 'your tools are ready' block; steers away from ToolSearch and shell-redirect rewrites
_lifecycle_layer function roboco/agents/factories/_base.py:187 Load _generated/lifecycle-.md canonical verb-surface fragment (from lifecycle spec, CI-gated)
fable_doctrine_layer function roboco/agents/factories/_base.py:203 Return the vendored doctrine/fable.md doctrine text, or None when fable_mode_enabled is off / the file is missing; only caller is compose_prompt, inserted right after base.md
ponytail_doctrine_layer function roboco/agents/factories/_base.py Return the vendored Ponytail build-laziness doctrine, role-scoped and gated on the same fable_mode_enabled flag (no separate flag — ponytail is Fable's complementary build-doctrine). Developers → doctrine/ponytail.md (full ladder) with a trailing **Operative intensity: {settings.ponytail_intensity}.** directive; every other role → doctrine/ponytail-ethos.md (ethos-only, no dial). None when the flag is off / the file is missing; only caller is compose_prompt, inserted immediately after the Fable doctrine layer
compose_prompt function roboco/agents/factories/_base.py:203 Compose the full system prompt by concatenating tool-directive, lifecycle, base, role, autogen-verbs, team, identity, ambient layers with '---' separators, skipping empty layers
_AMBIENT_TOTAL_CAP constant roboco/agents/factories/_base.py:256 3000-char cap on the concatenated conventions ambient block
conventions_ambient_layer async function roboco/agents/factories/_base.py:259 Render per-project architectural-standard ambient block(s), multi-project headed, capped; None when conventions off / no projects
make_slug function roboco/agents/factories/_base.py:296 Lowercase + dash slug helper
_AUTH_SECRET_ENV constant roboco/agents_config.py:42 Env var name ROBOCO_AGENT_AUTH_SECRET for the HMAC signing key
_auth_secret function roboco/agents_config.py:45 Return HMAC secret bytes or None when unset
_signing_payload function roboco/agents_config.py:51 Canonical lowercase stripped agent_id:role:team HMAC message
issue_agent_token function roboco/agents_config.py:61 Mint hex HMAC-SHA256 token binding agent identity to role+team; returns UNSIGNED sentinel if secret unset
verify_agent_token function roboco/agents_config.py:79 Constant-time HMAC verification; fail-closed on unset secret / UNSIGNED
issue_panel_token function roboco/agents_config.py:94 Mint the CEO-identity token the panel presents (signed for CEO_AGENT_ID/ceo/empty team)
_UUID_TO_SLUG dict roboco/agents_config.py:109 Reverse map UUID->slug from AGENT_UUIDS seeds
_resolve_to_slug function roboco/agents_config.py:114 Resolve UUID or slug input to slug
AGENT_ROLE_MAP dict roboco/agents_config.py:127 slug->role.value for every non-SYSTEM agent (derived from foundation.AGENTS)
AGENT_TEAM_MAP dict roboco/agents_config.py:133 slug->team.value derived from foundation
CELL_MEMBERS dict roboco/agents_config.py:139 team.value -> sorted slug list per cell
ALL_AGENTS list roboco/agents_config.py:146 All agent slugs
BOARD_MEMBERS list roboco/agents_config.py:149 product-owner, head-marketing, auditor
ALL_DOCS list roboco/agents_config.py:152 Cross-cell documenter slugs for docs workspace perms
TASK_CREATOR_ROLES frozenset roboco/agents_config.py:159 Roles that can call task.create (cell_pm, main_pm, product_owner, head_marketing, ceo)
ESCALATION_CHAIN dict roboco/agents_config.py:170 slug -> escalation target slug (dev/qa/doc -> cell PM -> main-pm -> product-owner -> ceo)
get_agent_role function roboco/agents_config.py:204 Role string for an agent (UUID or slug); 'unknown' if missing
get_agent_team function roboco/agents_config.py:210 Team string for an agent or None
get_agent_cell function roboco/agents_config.py:216 Alias of get_agent_team
get_cell_members function roboco/agents_config.py:221 Slugs for a cell
is_pm function roboco/agents_config.py:226 cell_pm or main_pm predicate
is_board_member function roboco/agents_config.py:232 Board membership predicate by slug
is_management function roboco/agents_config.py:237 PM/Board/CEO predicate
is_ceo function roboco/agents_config.py:250 CEO predicate (full permission bypass)
can_send_notifications function roboco/agents_config.py:255 Role in foundation NOTIFY_SENDER_ROLES
can_create_tasks function roboco/agents_config.py:263 Role in TASK_CREATOR_ROLES
can_assign_tasks function roboco/agents_config.py:269 Same set as can_create_tasks
_CANCEL_ROLES set roboco/agents_config.py:276 Roles that may cancel (cell_pm/main_pm/product_owner/head_marketing — NOT ceo/auditor)
can_cancel_tasks function roboco/agents_config.py:284 Role in _CANCEL_ROLES
get_escalation_target function roboco/agents_config.py:290 Next escalation slug from ESCALATION_CHAIN
get_pm_for_team function roboco/agents_config.py:295 Cell PM slug for a team
get_pm_for_agent function roboco/agents_config.py:305 Responsible PM: cell PM for members, main-pm for cell PMs, product-owner for main PM
_slugs_for_role_set function roboco/agents_config.py:355 Expand a role-set to sorted slugs honoring optional team_scope; excludes system sentinel
ROLE_PERMISSION_LEVELS dict roboco/agents_config.py:404 Single source of truth role->permission-level (CEO/BOARD/AUDITOR/MAIN_PM/CELL_PM/CELL_MEMBER) used by PermissionService
VALID_NOTIFICATION_TYPES frozenset roboco/agents_config.py:425 Valid NotificationType values
VALID_NOTIFICATION_PRIORITIES frozenset roboco/agents_config.py:428 Valid NotificationPriority values
ROLE_SKILLS dict roboco/agents_config.py:438 Role -> A2A skill descriptor list for Agent Cards
get_agent_skills function roboco/agents_config.py:566 A2A skills for an agent by role
_BOARD_ROLES frozenset roboco/agents_config.py:584 Foundation board roles (PO/HoM/Auditor; main_pm intentionally excluded)
_MAIN_PM_TARGETS frozenset roboco/agents_config.py:585 Roles a main PM may A2A directly
_check_cell_pm_a2a function roboco/agents_config.py:590 A2A permission for cell PM (own cell / other PMs / main-pm allowed; board escalated)
_check_cell_member_a2a function roboco/agents_config.py:604 A2A permission for cell members (same-cell allowed; cross-cell via PMs)
_check_main_pm_a2a function roboco/agents_config.py:624 A2A permission for main PM (_MAIN_PM_TARGETS allowed)
can_a2a_direct function roboco/agents_config.py:632 (allowed, error) for direct A2A from one agent to another; routes CEO via notify, board/main_pm/cell-member via handlers
get_a2a_route_hint function roboco/agents_config.py:670 Human-readable routing hint for an A2A message
_PATTERNS list roboco/agent_sdk/prompt_guard.py:28 Five (regex, reason) injection patterns: ignore-previous, role-override, fake role prefix, control-token mimicry, fake executive-order
detect_injection function roboco/agent_sdk/prompt_guard.py:63 Return deny reason if text matches an injection pattern (lowercased), else None
refusal_message function roboco/agent_sdk/prompt_guard.py:72 Guidance string shown on denial (mirrors bash hook text)
main function roboco/agent_sdk/prompt_guard.py:82 CLI entry: exit 1 if argv[1] is an injection (used by grok entrypoint)

Data Flow

Spawn-time composition (synchronous, per agent): orchestrator._generate_prompt(role/team/agent_id, ambient) calls agents_config.get_agent_role/get_agent_team to resolve the canonical strings from foundation-derived AGENT_ROLE_MAP/AGENT_TEAM_MAP, converts to AgentRole/Team enums, then calls compose_prompt. compose_prompt resolves prompts_path (PROMPTS_BASE_PATH = project_root/agents/prompts) and builds an ordered list: _tool_load_directive_layer(role) (inline), _lifecycle_layer (reads _generated/lifecycle-.md), base.md, _role_layer (roles/.md via _ROLE_LAYER_MAP), _autogen_verbs_layer (_generated/.md), _team_layer (teams/.md via _TEAM_LAYER_MAP, None for board/main-pm), identities/<agent_slug>.md, then the optional ambient string. Empty/None layers are dropped; the rest are joined with "\n\n---\n\n". The composed string is written to /app/prompts-generated/<agent_id>-prompt.md (container) or $TMPDIR/roboco-prompts/ (host) and the path returned to the spawn path that mounts it as the agent's system prompt.

Ambient resolution (async, best-effort): orchestrator._resolve_conventions_ambient gates on settings.conventions_enabled, opens a DB session, resolves in-scope projects (single project_slug for delivery roles, or per-cell projects from a task's product_id for PO/Intake), and calls conventions_ambient_layer -> ConventionsService.render_ambient_block per project (ensuring a read clone), multi-project-headed, capped to 3000 chars. Any exception is caught and returns None so a compose is never blocked by conventions.

Identity binding at spawn: agents_config.issue_agent_token(agent_id, role, team) HMAC-signs the canonical lowercase agent_id:role:team with ROBOCO_AGENT_AUTH_SECRET and the orchestrator injects the token into the agent env; verify_agent_token (called server-side on X-Agent-Token headers) fail-closes on unset secret or UNSIGNED. The panel gets issue_panel_token() signed for the CEO identity.

Injection guard (runtime, per turn): IntakeDriver.send_turn calls detect_injection(text) before sending to the model; on a match it emits an error chunk with refusal_message(reason) and returns without forwarding. The grok one-shot entrypoint runs python -m roboco.agent_sdk.prompt_guard <text> and refuses start (exit 1) on a match. The same five patterns run in docker/scripts/user-prompt-hook.sh for non-SDK Claude sessions.

Callers: orchestrator.py:3238 (compose_prompt), orchestrator.py:3265/3299 (conventions_ambient_layer), intake_driver.py:379-382 (detect_injection/refusal_message). Callees from this slice: roboco.foundation.identity (AGENTS/Role/Team/slugs_for_team), roboco.foundation.policy.communications (NOTIFY_SENDER_ROLES), roboco.seeds.initial_data (AGENT_UUIDS/CEO_AGENT_ID), roboco.services.conventions (get_conventions_service), roboco.config.settings, roboco.models.base (NotificationType/Priority).

Mermaid

graph TD
    subgraph "Spawn-time prompt composition"
      O[orchestrator._generate_prompt] --> AC[agents_config.get_agent_role/team]
      AC --> FOUND[foundation.identity.AGENTS]
      O --> CP[compose_prompt]
      CP --> TLD[_tool_load_directive_layer role]
      CP --> LL[_lifecycle_layer _generated/lifecycle-role.md]
      CP --> BASE[base.md]
      CP --> RL[_role_layer roles/file.md]
      CP --> AVL[_autogen_verbs_layer _generated/role.md]
      CP --> TL[_team_layer teams/file.md]
      CP --> ID[identities/agent_slug.md]
      CP --> AMB[ambient string]
      TLD --> OUT["/app/prompts-generated/agent_id-prompt.md"]
      LL --> OUT
      BASE --> OUT
      RL --> OUT
      AVL --> OUT
      TL --> OUT
      ID --> OUT
      AMB --> OUT
    end
    subgraph "Ambient (async, best-effort)"
      OA[orchestrator._resolve_conventions_ambient] -->|settings.conventions_enabled| CAL[conventions_ambient_layer]
      CAL --> CS[ConventionsService.render_ambient_block]
      CS --> RC[ensure read clone]
      CAL -->|cap 3000| AMB
    end
    subgraph "Identity binding"
      IAT[issue_agent_token] --> HMAC[HMAC-SHA256 agent_id:role:team]
      VAT[verify_agent_token] --> HMAC
      IPT[issue_panel_token] --> IAT
    end
    subgraph "Injection guard (per turn)"
      IDV[IntakeDriver.send_turn] --> DI[detect_injection]
      DI -->|match| RM[refusal_message -> error chunk, return]
      DI -->|clean| MODEL[forward to model]
      GE[grok entrypoint] -->|CLI main exit 1| DI
      BASH[user-prompt-hook.sh] -.same 5 patterns.-> DI
    end

Logical Tree

prompts-roles-taxonomy slice
├── Prompt composition (roboco/agents/factories/)
│   ├── _base.py
│   │   ├── PROMPTS_BASE_PATH resolver
│   │   ├── _load_layer (file -> str|'')
│   │   ├── Layer maps: _ROLE_LAYER_MAP, _TEAM_LAYER_MAP
│   │   ├── Layer loaders: _role_layer, _team_layer, _autogen_verbs_layer, _lifecycle_layer
│   │   ├── Builtin-tool grant: _BUILTIN_TOOLS_COMMON/AUTHORS, _ROLE_BUILTIN_TOOLS, _tool_load_directive_layer
│   │   ├── compose_prompt (ordered join with '---')
│   │   └── conventions_ambient_layer (async, multi-project, 3000-char cap) + _AMBIENT_TOTAL_CAP
│   └── __init__.py (shim)
├── Permission taxonomy (roboco/agents_config.py)
│   ├── HMAC token layer: _auth_secret, _signing_payload, issue_agent_token, verify_agent_token, issue_panel_token
│   ├── UUID<->slug: _UUID_TO_SLUG, _resolve_to_slug
│   ├── Derived maps: AGENT_ROLE_MAP, AGENT_TEAM_MAP, CELL_MEMBERS, ALL_AGENTS, BOARD_MEMBERS, ALL_DOCS
│   ├── Role sets: TASK_CREATOR_ROLES, _CANCEL_ROLES, ROLE_PERMISSION_LEVELS, _BOARD_ROLES, _MAIN_PM_TARGETS
│   ├── Escalation: ESCALATION_CHAIN, get_escalation_target, get_pm_for_team, get_pm_for_agent
│   ├── Helpers: get_agent_role/team/cell, get_cell_members, is_pm/board_member/management/ceo, can_send_notifications/create/assign/cancel_tasks
│   ├── Notification enums: VALID_NOTIFICATION_TYPES/PRIORITIES
│   ├── A2A skills: ROLE_SKILLS, get_agent_skills
│   └── A2A routing: _check_cell_pm/cell_member/main_pm_a2a, can_a2a_direct, get_a2a_route_hint
├── Injection guard (roboco/agent_sdk/prompt_guard.py)
│   ├── _PATTERNS (5 regexes mirroring user-prompt-hook.sh)
│   ├── detect_injection, refusal_message
│   └── main (CLI for grok entrypoint)
└── Prompt corpus (agents/prompts/)
    ├── base.md (universal rules)
    ├── roles/ (9 files: developer, qa, documenter, cell_pm, main_pm, pr_reviewer, board, prompter, secretary)
    ├── teams/ (3 files: backend, frontend, ux_ui)
    ├── identities/ (19 per-agent YAML+blurb files)
    └── _generated/ (regenerated artifacts)
        ├── lifecycle-<role>.md (x14; from lifecycle spec via make lifecycle; CI-gated no-drift)
        ├── <role>.md verb-signature tables (x12; from schemas + role_config via regenerate_verb_tables.py)
        └── verbs.md (aggregate reference doc; NOT injected at spawn)

Dependencies

  • Internal: roboco.foundation.identity (AGENTS, Role, Team, CELL_TEAMS, BOARD_ROLES, slugs_for_team), roboco.foundation.policy.communications (NOTIFY_SENDER_ROLES), roboco.seeds.initial_data (AGENT_UUIDS, CEO_AGENT_ID), roboco.models.base (AgentRole, Team, NotificationType, NotificationPriority), roboco.config.settings (conventions_enabled), roboco.services.conventions (get_conventions_service, ConventionsService.render_ambient_block/resolve_workspace), roboco.db.base (get_session_factory), roboco.db.tables (ProjectTable), roboco.runtime.orchestrator (_generate_prompt, _resolve_conventions_ambient, _resolve_ambient_projects), roboco.agent_sdk.intake_driver (IntakeDriver.send_turn consumer), scripts/regenerate_verb_tables.py (regenerates _generated/.md + verbs.md), scripts/build_lifecycle_artifacts.py (regenerates _generated/lifecycle-.md; make lifecycle), docker/scripts/user-prompt-hook.sh (canonical bash guard mirrored by prompt_guard.py), roboco.api.schemas.v1 (Pydantic verb schemas the autogen tables derive from), roboco.services.gateway.role_config (role->verb config the autogen tables derive from)
  • External: pathlib.Path, hmac / hashlib (HMAC-SHA256 tokens), os.environ (ROBOCO_AGENT_AUTH_SECRET), re (injection regexes), sys (prompt_guard CLI), sqlalchemy.ext.asyncio.AsyncSession (ambient layer), typing.Final, argparse-less sys.argv CLI

Entry Points

Name File Trigger
orchestrator._generate_prompt roboco/runtime/orchestrator.py Called per agent spawn to compose + write the system-prompt .md file; calls compose_prompt (line 3238)
orchestrator._resolve_conventions_ambient roboco/runtime/orchestrator.py Async, called from the spawn path before _generate_prompt to resolve the optional ambient block; calls conventions_ambient_layer (line 3299)
IntakeDriver.send_turn roboco/agent_sdk/intake_driver.py Per interactive turn (Intake/Secretary Claude-SDK and Grok sessions); calls detect_injection before forwarding to the model (line 379)
python -m roboco.agent_sdk.prompt_guard roboco/agent_sdk/prompt_guard.py CLI invoked by the grok one-shot entrypoint on ROBOCO_INITIAL_PROMPT; exit 1 denies start
make lifecycle scripts/build_lifecycle_artifacts.py Developer/CI target regenerating _generated/lifecycle-*.md; CI gates on git diff --exit-code
scripts/regenerate_verb_tables.py scripts/regenerate_verb_tables.py Developer target regenerating _generated/.md + verbs.md after role_config/schema changes

Config Flags

  • ROBOCO_AGENT_AUTH_SECRET (env) — HMAC signing secret for agent/panel tokens; unset => verify_agent_token fail-closes (rejects every token), issue_*_token returns UNSIGNED
  • ROBOCO_CONVENTIONS_ENABLED — gates whether conventions_ambient_layer resolves + injects the architectural-standard ambient block; off => compose_prompt omits the ambient layer entirely
  • ROBOCO_FABLE_MODE_ENABLED (default off) — gates fable_doctrine_layer AND ponytail_doctrine_layer (bundled — no separate ponytail flag); off => compose_prompt omits both doctrine layers entirely (byte-for-byte unchanged prompt)
  • ROBOCO_PONYTAIL_INTENSITY (default full) — string value (lite/full/ultra), NOT a feature flag; selects the operative intensity the developer ponytail doctrine runs at (appended as a **Operative intensity: ...** directive for developers only; non-developers run a fixed restrained ethos regardless). roboco/config.py ponytail_intensity, validated as Literal["lite","full","ultra"] at Settings instantiation
  • ROBOCO_SDK_URL (env, default http://localhost:9000) — used by the bash user-prompt-hook.sh (sister guard), not prompt_guard.py directly
  • ROBOCO_INITIAL_PROMPT (env) — the one-shot prompt the grok entrypoint hands to prompt_guard CLI main
  • PROJECT_HOST_PATH (orchestrator) — selects container (/app/prompts-generated) vs host ($TMPDIR/roboco-prompts) output dir for composed prompts

Gotchas

  • Layer ORDER matters and is load-bearing: tool-directive FIRST, then lifecycle, then base, then an optional fable-mode doctrine layer (fable_mode_enabled, agents/prompts/doctrine/fable.md), then an optional ponytail build-laziness doctrine layer (fable_mode_enabled, agents/prompts/doctrine/ponytail.md for developers / ponytail-ethos.md for other roles — bundled with Fable, no separate flag, role-scoped), then role, then autogen-verbs, then team, then identity, then ambient. The lifecycle fragment is intentionally before base so the agent reads its allowed verb surface before any other instruction. Reordering would change model attention priority.
  • Empty/missing layers are silently dropped (compose_prompt skips falsy layers). An unknown role yields _role_layer=None AND _autogen_verbs_layer=None AND _lifecycle_layer=None — the agent would still spawn with just tool-directive + base + identity + ambient, missing its entire role+verb surface. The orchestrator guards upstream (raises ValueError on unknown role), but a typo in _ROLE_LAYER_MAP silently degrades to a roleless prompt rather than failing.
  • _ROLE_LAYER_MAP maps all three board roles (product_owner/head_marketing/auditor) to the SAME board.md file. The per-role distinction (PO vs HoM vs Auditor) comes only from the identity file + the _generated/.md verb table, not from the role layer. A board role missing its identity file would lose its role-specific scope.
  • verbs.md is the aggregate reference doc but is NOT injected at spawn — _base.py loads the per-role _generated/.md file instead. Editing verbs.md has zero prompt effect; it is a documentation/CI artifact only. The per-role files are the load-bearing ones.
  • Identity YAML files carry a stale role: label (e.g. main-pm.md says role: pm, product-owner.md says role: board) that does NOT match the AgentRole enum values (main_pm/product_owner). The composition pipeline ignores this field entirely (loads identity by slug only); the real role comes from agents_config.AGENT_ROLE_MAP. Do not trust the identity YAML role label for enforcement.
  • Board members (product_owner/head_marketing/auditor) have team=None, so _team_layer returns None for them — they get no team layer. main-pm likewise. Only cell members (dev/qa/doc/cell_pm) get a team layer.
  • The autogen verb tables (_generated/.md) and lifecycle fragments are REGENERATED artifacts (make lifecycle / regenerate_verb_tables.py) gated on CI (git diff --exit-code). Hand-editing them is futile and will fail CI; change the source (lifecycle spec / Pydantic schemas / role_config) and regenerate.
  • prompt_guard.py mirrors user-prompt-hook.sh patterns but is a separate implementation. The two must be kept in sync manually — there is no shared source. Drift between them means Claude (bash hook) and Grok/SDK (Python guard) apply different deny rules.
  • detect_injection lowercases the text and uses loose anchoring (^|[\s>]) so injected content mid-message is caught, but the regexes are intentionally narrow (5 patterns). False negatives are expected by design — this is a classic-jailbreak denylist, not a comprehensive classifier; content that doesn't match still reaches the model.
  • conventions_ambient_layer is best-effort and wraps the whole resolution in a try/except in the orchestrator (_resolve_conventions_ambient). A conventions resolution failure degrades silently to no ambient layer — a compose is never blocked by conventions. This means a conventions regression could quietly stop injecting the standard with no error surface.
  • _AMBIENT_TOTAL_CAP (3000) truncates the ambient block with a trailing ellipsis. A large multi-project spawn (PO spanning several cells) can have its architectural standard silently truncated mid-block, leaving the agent with a partial standard.
  • HMAC token verification fail-closes when ROBOCO_AGENT_AUTH_SECRET is unset — every agent token is rejected. issue_agent_token returns the literal sentinel 'UNSIGNED' which verify_agent_token also rejects. Deploying without the secret bricks all agent API auth (by design).

Drift from CLAUDE.md

  • CLAUDE.md Project Overview states '25 AI agents + 1 human CEO', but agents/prompts/base.md line 3 says '22 AI agents + 1 human CEO'. The base prompt agent count is stale relative to CLAUDE.md (memory notes a 20->22 update on 2026-06-16; CLAUDE.md later moved to 25). A spawned agent reads '22' in its system prompt while the org chart it sees has 25.
  • CLAUDE.md's verb-surface table lists i_am_blocked for the qa and documenter roles. The role prompts qa.md and documenter.md ONLY added the i_am_blocked verb row in commit 15effce0 (this slice's baseline diff) — before that the role prompts omitted it even though the gateway accepted it. The prompts are now aligned, but the documenter.md circuit-breaker section previously explicitly said 'you don't have an i_am_blocked verb', which was false vs the gateway and vs CLAUDE.md. Fixed in 15effce0.
  • CLAUDE.md says the lifecycle is defined in roboco/foundation/policy/lifecycle.py with a shim at roboco/enforcement/task_lifecycle.py. _base.py:_lifecycle_layer (line 187-200) docstring says the lifecycle fragment is regenerated from roboco/lifecycle/spec.py by make lifecycle. The actual source path the regenerator uses is roboco/lifecycle/spec.py (per the docstring), which is not mentioned in CLAUDE.md's lifecycle section — minor doc-path drift, not behavioral.
  • CLAUDE.md describes the prompt composition as 'base + role + team + identity prompts' and an ambient 'Architectural Standard' block at spawn. The actual compose_prompt order (line 295-306) is tool-directive + lifecycle + base + fable + ponytail + role + autogen-verbs + team + identity + ambient — i.e. FIVE additional layers (tool-directive, lifecycle, fable, ponytail, autogen-verbs) not named in CLAUDE.md's composition description. CLAUDE.md undersells the actual layer stack.
  • Identity files declare role: pm / role: board (e.g. identities/main-pm.md, identities/product-owner.md) which do not match the canonical AgentRole enum values (main_pm, cell_pm, product_owner, head_marketing, auditor) that CLAUDE.md and agents_config use. The pipeline ignores this label so it is cosmetic, but it is inconsistent with the canonical taxonomy CLAUDE.md documents.

Changes Since Baseline

SHA Subject Impact
15effce0 Chore: 141 Gaps fill-in (#283) — sole commit touching this slice since fd10cc86 Prompt-surface alignment with gateway/spec: (1) developer.md + lifecycle-developer.md + verbs.md add the new sync_branch verb and rewrite the behind-base guidance on developer/cell_pm/main_pm to point devs at sync_branch instead of i_am_blocked/escalate_up (cell/root integration branches still escalate). (2) qa.md and documenter.md add the i_am_blocked verb row and rewrite the circuit-breaker section to use i_am_blocked instead of 'you don't have an i_am_blocked verb -> unclaim' — corrects a false prompt claim. (3) cell_pm.md delegate signature + verbs.md add the collision-surface fields intends_to_touch/adds_migration/touches_shared/depends_on and a new 'Collision surface' section instructing the PM to declare them on every code subtask so siblings sequence. (4) pr_reviewer.md adds the in-path gate verbs claim_gate_review/pr_pass/pr_fail + an 'In-path gate review' section. (5) prompter.md adds MegaTask root-subtask coordination-level AC guidance (task_type=planning, coordination-level ACs). (6) lifecycle-main_pm.md submit_root description changes from 'Only for code roots' to 'branch-bearing roots; gate is branch-keyed not task_type-keyed'. (7) note verb schema in all autogen tables gains done/next/where_to_look top-level string params; pass_review ac_verdicts and delegate covers_parent_criteria/intends_to_touch now show BeforeValidator in the signature.

Post-snapshot updates (since 2026-06-29): 536bbb64 (Chore/all/logical gaps sweep #286) — (a) agents_config.py: _TEAM_SCOPED_ROLES deduped: was inline-defined, now re-exported as _comms.TEAM_SCOPED_ROLES from foundation.policy.communications (values unchanged: dev/qa/doc/cell_pm); (b) _generated/cell_pm.md, main_pm.md, qa.md, verbs.md: BeforeValidator repr cleaned from delegate/pass_review signatures — now renders list[str] | None = None instead of the memory-address-bearing BeforeValidator literal; (c) lifecycle spec: PRECONDITION_ROOT_NOT_CODE added to submit_root extra_preconditions, backing the branch-keyed / planning-typed claim the prompt asserts. aba57359 ([chore] lifecycle artifacts regenerate, foundation-check) — lifecycle-cell_pm.md, lifecycle-developer.md, lifecycle-documenter.md, lifecycle-main_pm.md, lifecycle-qa.md: unclaim description expanded with "A PR reviewer who claimed an external/gate review and cannot finish releases the claim here rather than wedging the lane"; lifecycle-cell_pm.md + lifecycle-main_pm.md: complete description clarified "The merge runs BEFORE the complete transition" ordering.

v0.18.0 (2026-07-04): Fable mode adds a 9th conditional compose_prompt layer — fable_doctrine_layer() (_base.py:203) injects agents/prompts/doctrine/fable.md right after base.md, gated by fable_mode_enabled (default off; off = byte-for-byte unchanged prompt). FE/UX-UI design bar: ## Design bar sections added to teams/frontend.md + teams/ux_ui.md (taste-skill-distilled dials + rules), plus a scoping pointer in roles/developer.md — doc-only, no flag, no compose_prompt change (team/role layers already existed; only their file contents grew).

v0.19.0 (2026-07-05): Ponytail build-laziness doctrine bundled with Fable — ponytail_doctrine_layer(prompts_path, role) in roboco/agents/factories/_base.py, gated on the same fable_mode_enabled flag (no separate flag — ponytail is Fable's complementary build-doctrine), slotted into compose_prompt immediately after fable_doctrine_layer. Role-scoped: developers (AgentRole.DEVELOPER) → agents/prompts/doctrine/ponytail.md (the full ladder: YAGNI → reuse-in-this-codebase → stdlib → native-platform → installed-dep → one-line → minimal, the rules, the Intensity table, the ponytail: comment convention) plus a trailing **Operative intensity: {settings.ponytail_intensity}.** directive; every other role → agents/prompts/doctrine/ponytail-ethos.md (ethos-only — the code-mechanics rungs and the Intensity table are dropped so they can't leak into prose artifacts like task plans / review notes / docs). Both files vendored from the ponytail plugin (MIT, Copyright (c) 2026 DietrichGebert), trimmed, YAML frontmatter stripped, and carry a 5-point RoboCo preamble that makes the ladder yield to the Architectural Conventions Standard (placement), the 80% coverage gate + QA review + self-verification, the per-team design bar, task hygiene, and reviewer feedback — overlap mitigated by scoping, not deletion. ROBOCO_PONYTAIL_INTENSITY (lite/full/ultra, default full; roboco/config.py ponytail_intensity, a string value — NOT a feature flag) selects the developer's operative intensity; non-developers get no dial. Prompt-only — no hooks, no grok-path changes; a flag-off spawn is byte-for-byte unchanged.

Regression Risks

Title File:Line Claim Severity
Collision-surface declaration is prompt-only, not gate-enforced agents/prompts/roles/cell_pm.md:141 The new 'Collision surface' section tells the cell PM to fill intends_to_touch/adds_migration/touches_shared on every code subtask so the analyzer can sequence colliding siblings, but the section explicitly says 'leave it empty only for a research/design subtask' — there is no documented gate that REFUSES a code delegate with empty intends_to_touch. If a PM omits it on a code subtask, two siblings editing the same file run in parallel and collide (the exact 2026-06-27 out-of-order break this was added to prevent). The protection hinges on agent compliance with prompt prose, not a hard gate. medium
sync_branch guidance contradicting the i_am_done gate for behind-base branches agents/prompts/roles/developer.md:126 developer.md now says 'call sync_branch as soon as roboco_git_status shows your branch behind, OR when i_am_done refuses with your branch is N behind'. If the i_am_done gate's behind-base check and sync_branch's rebase disagree on what 'base' means (e.g. base resolved from the recorded branch vs the parent task's head), a dev could sync_branch successfully and still hit the i_am_done behind-base refusal, looping. The prompt assumes both use the same base resolution; a divergence there would trap the dev. No fallback to i_am_blocked is offered anymore (the prompt explicitly forbids it for a plain behind-base condition), removing the previous escape hatch. medium
documenter/qa circuit-breaker now directs to i_am_blocked — verify the verb is actually granted to those roles VERIFIED OK agents/prompts/roles/documenter.md:100 The circuit-breaker section was rewritten from 'you don't have an i_am_blocked verb -> unclaim' to 'i_am_blocked(task_id, reason=...) to escalate'. This relies on i_am_blocked being genuinely callable by qa and documenter at the gateway. The autogen verbs.md shows i_am_blocked for qa but the documenter section in verbs.md (and _generated/documenter.md) must also list it — if the role_config does not grant i_am_blocked to documenter, the new prompt guidance sends the agent to a verb that will return not_authorized, trapping them on a circuit_open with no documented fallback (the old unclaim-only path was removed). **Verified 2026-07-01: lifecycle spec `i_am_blocked.allowed_roles = frozenset(_DEV_ROLES _QA_ROLES
submit_root description changed from code-root to branch-bearing-root semantics RESOLVED (536bbb64) agents/prompts/_generated/lifecycle-main_pm.md:14 The lifecycle fragment now says submit_root is 'For branch-bearing roots' and 'The gate is branch-keyed, not task_type-keyed — a Main-PM root is planning-typed, never code'. If the gateway's submit_root implementation still keys off task_type=code (the old contract), a planning-typed branch-bearing root would be rejected by the gate while the prompt tells the Main PM to call submit_root on it — a loop. The prompt now asserts a behavior the gateway must match; mismatch breaks Main-PM root submission. Fixed 2026-06-30: 536bbb64 added PRECONDITION_ROOT_NOT_CODE (_p_root_not_code: checks task_type != code) to submit_root.extra_preconditions in the lifecycle spec, and the spec description was updated to match; prompt and gateway are now aligned. high
note schema advertises done/next/where_to_look the gateway must accept agents/prompts/_generated/developer.md:25 All autogen verb tables now show note(...) with done/next/where_to_look top-level params. If the Pydantic note schema (the regenerator source) was updated but the gateway's note handler / DB journal model does not persist these fields, agents will pass them, the schema accepts them, but they are silently dropped — the handoff/quick_context fields the meltdown #1 fix intended to surface top-level would never reach downstream briefings. The prompt advertises params that may be no-ops at the persistence layer. medium
BeforeValidator rendering in verb signatures leaks into the prompt RESOLVED (536bbb64) agents/prompts/_generated/verbs.md:58 The regenerated verb tables now render BeforeValidator(func=<function coerce_str_list at 0x109dbdee0>, json_schema_input_type=PydanticUndefined) literally into the agent's system prompt for pass_review.ac_verdicts, delegate.covers_parent_criteria and delegate.intends_to_touch. This is a memory-address-bearing repr of an internal Pydantic validator injected into every qa/cell_pm/main_pm prompt. It is noise the model must parse around and the address is non-deterministic across runs, which could in principle perturb caching/reprompt determinism. Not a correctness bug but a prompt-hygiene regression introduced by the regenerator. **Fixed 2026-06-30: 536bbb64 cleaned the regenerated tables; all three fields now render `list[str] None = None`.**
cell_pm.md behind-base guidance split between dev sync_branch and cell-branch escalate_up agents/prompts/roles/cell_pm.md:178 The rewritten behind-base section tells the PM to direct devs to sync_branch for their leaf but to escalate_up for the cell integration branch. If a PM mis-classifies a behind-base condition (tells a dev to escalate_up instead of sync_branch, or calls escalate_up on a dev's leaf), the dev waits on a platform action that won't come (sync_branch is the dev's own verb). The split is correct but easy to misapply; a misroute strands the dev. low

Health

The composition pipeline is well-structured and deterministic: a single ordered join over gracefully-degrading layers, with CI-gated autogenerated artifacts (lifecycle + verb tables) that cannot silently drift from the spec, and a clean separation between the prompt corpus (markdown), the taxonomy (agents_config.py, derived from foundation so it cannot drift from the org chart), and the guard (prompt_guard.py, mirroring the bash hook). The main open integrity risks are (a) the prompt-only enforcement of the new collision-surface declaration on delegate — the 2026-06-27 out-of-order break this was added to fix can still recur if a PM omits intends_to_touch on a code subtask; (b) the stale agent-count in base.md (22 vs CLAUDE.md's 25) and the cosmetic but inconsistent role: labels in identity YAML. Previously flagged risks (b/c as of 2026-06-29 snapshot) have been closed: documenter/qa i_am_blocked is confirmed granted by the lifecycle spec (_DEV_ROLES|_QA_ROLES|_DOC_ROLES), and submit_root's branch-keyed claim is now backed by PRECONDITION_ROOT_NOT_CODE in the spec gate (536bbb64). BeforeValidator repr in prompt tables also cleaned (536bbb64).