Commit Graph
144 Commits
Author SHA1 Message Date
b91229f487 fix(orchestrator): break the notification-driven respawn loop (#643)
The escalation/approval dispatchers spawn a notification's recipient every
cooldown window for as long as it stays pending. These spawns carry no
task_id, so the PM respawn breaker never sees them — a single wedged
alert/escalation whose recipient never resolves it respawns that recipient
forever. Observed live: fe-pm's unacked alerts kept main-pm/fe-pm spawning
every ~2-3 min for 6+ hours.

Two guards, both gating the spawn after the existing cooldown:
- A hard per-(agent, notification) attempt cap (notification_spawn_max_attempts,
  default 5): once a notification has respawned its target that many times
  without being acknowledged, stop and log once. The count is id-scoped and
  survives map pruning (re-stamp), so a fresh escalation is unaffected.
- A live-work check before spawning: skip when the notification has expired,
  is stale past notification_spawn_max_age_seconds (default 6h — wedged or
  reloaded from before a restart), or its related task is already terminal.
  Fail-open — a failed task fetch or unparseable field never suppresses a
  real escalation.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-22 17:10:55 +02:00
17de29545a [6788ce7f] Silent bug sweep: concurrency, state integrity, engine edge-cases, panel data freshness (#638)
* [943d8c4d] Frontend data freshness and approval-queue reliability audit (#631)

* [233a8b0f] WebSocket reconnect message-loss audit and fix (#625)

* [233a8b0f] fix(panel): add REST catch-up to useNotificationStream on WS reconnect

connection.ts has no message buffering/replay, so a notification published
while the CEO bell's socket was down (disconnected/reconnecting) was lost
forever instead of merely delayed. Add a reconnect-triggered GET
/notifications?unread_only=true catch-up folded into the existing
notification_id dedup so a notification delivered both via catch-up and
live WS is never double-counted, and make clearMessages drop the held
catch-up batch too. use-a2a-live.ts and use-rate-limit-websocket.ts were
audited and already have working reconnect-triggered REST fallbacks
(verified via a2a/page.tsx, rate-limit-banner.tsx, usage-overview-panel.tsx
and their existing F083 tests) so no fix was needed there.

* [233a8b0f] docs(panel): add comprehensive WebSocket hooks reference and reconnect architecture guide

Add panel/docs/frontend/hooks.md with full API reference for useWebSocket, useNotificationStream (with new REST catch-up behavior), useAgentStream, useA2ALiveStream, and useConnectionStatus. Include examples, best practices, and testing guidance.

Add panel/docs/architecture/websocket-reconnect.md documenting the message-loss mitigation pattern: Strategy 1 (REST catch-up for events, used by useNotificationStream) and Strategy 2 (REST invalidation for state, used by A2A/rate-limit consumers), plus the dedup logic ensuring no notification is double-counted on reconnect.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [d5315683] fix(frontend): add distinct toast feedback for silently-swallowed x-post and release-proposal statuses, plus regression tests for all 4 approval queues (#626)

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>

* [cd953838] Data-hook null-guard audit and API client 429 retry-by-method fix (#630)

* [cd953838] fix(panel): gate 429 retry by HTTP method, add hook null-guard regression tests

* [cd953838] chore(conventions): waive test-fixture wrapper in hooks null-guard test

* [cd953838] docs(frontend): document API rate-limit retry behavior and null-guard audit results

Added `docs/frontend/api-rate-limiting.md` to document the 429 retry strategy: GET/PUT auto-retry, POST/PATCH/DELETE require X-Idempotency-Key header. Updated `docs/frontend/hooks.md` to confirm the data-hook null-guard audit found all hooks already have correct `enabled` guards and include a regression test suite for the board-review poll on/off behavior and enabled-guard assertions.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>

* [4534c71a] Backend concurrency, state-machine, and engine audit (#634)

* [41de844a] fix(lifecycle): sync CLAIM_RULES with runtime + clear stale claimant on PM hand-off (#627)

Two confirmed state-machine gaps found while auditing lifecycle.py,
task_lifecycle.py, the _ESCALATABLE_TO_BLOCKED bypass, and every
_REVIEW_QUEUE_STATES entry point:

- lifecycle.py's CLAIM_RULES/claim-ActionSpec/StatusTransition table
  did not grant CELL_PM/MAIN_PM re-claim of AWAITING_PM_REVIEW even
  though task.py's runtime _ROLE_CLAIM_STATUSES already granted it
  and claimed the spec agreed -- the two tables had silently drifted,
  breaking i_will_plan re-claim on an awaiting_pm_review task.

- docs_complete's _maybe_advance_to_pm_review pre-assigns a specific
  owning PM via assigned_to but left claimed_by/active_claimant_id
  pointing at the outgoing documenter, unlike every sibling transition
  into a review-queue state. A stale active_claimant_id makes
  content_actions.py's _active_claim_violation wrongly reject the
  newly-assigned PM's own content writes before it formally claims.
  Reassign claimed_by + active_claimant_id to the owning PM alongside
  assigned_to.

Adds a regression test asserting the documenter's stale claim does not
survive the docs_complete -> awaiting_pm_review hand-off.

Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>

* [0c46f666] Engine dedup race + sequencing.py edge-case audit (#628)

* [0c46f666] fix(sequencing): dedup race audit + collision-edge fallback bug

Audited the list-open-then-originate dedup pattern across six engines:
RoadmapEngine, XEngine.run_cycle, DepUpdateEngine, and CIWatchEngine each
run inside exactly one sequential orchestrator-loop asyncio task (no other
call site invokes run_cycle), so they cannot race with themselves; their
in-cycle dedup sets/keys are correctly built before any commit. SelfHealEngine
is the same shape. VideoEngine.open_video_task is genuinely different: it is
reachable from the release-publish hook, the feature-spotlight hook, and the
on-demand POST /video/request route, so two overlapping calls for the same
occasion can both pass the "no open task yet" check before either commits.
Fixed by wrapping the check+insert in a short-lived Redis mutex (reusing
HeartbeatMutex) keyed by occasion, mirroring XPostService's existing
lock pattern, with a regression test proving only one of two concurrent
calls creates a task.

Verified ReleaseExecutor's half-landed retry path (release_commit_sha):
apply_version_bumps and write_changelog_entry both run as uncommitted
working-tree edits before commit_and_push's single `git add -A` + commit,
so a bumped-version-without-changelog state can never reach origin (and
therefore can never be observed by a fresh retry clone) - confirmed correct
with a real-git-repo regression test, no fix needed.

Fixed sequencing.py's dev_task_collision_edges: the `if edges: return edges`
short-circuit dropped the same-assignee-lane fallback entirely whenever ANY
surfaced sibling pair produced a collision edge, even for a completely
unrelated same-assignee pair with no declared surface. Now the fallback
always runs, skipping only pairs the analyzer already ordered (so the two
mechanisms can never disagree on direction for the same pair).

Verified sequencing.py rule 3 (all-shared batch generates no edges): correct
by inspection (_shared_last_edges skips every pair when both are shared) and
confirmed with a regression test - no fix needed.

* [0c46f666] docs(reference): concurrency audit summary - engine races, fixes, verified patterns

---------

Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [8f7f167a] Redis mutex pre-lock write audit (#629)

* [8f7f167a] Redis mutex pre-lock write audit: add cross-session regression test for XPostService.approve

Audited x_post_service.py, video_post_service.py, release_proposal.py, and
heartbeat_mutex.py for the pre-lock DB-write anti-pattern (a session write
that happens before the SET NX / HeartbeatMutex acquire returns a token,
letting a losing racer's stale write clobber a winner's committed state).

XPostService.approve, VideoPostService.approve, and
ReleaseProposalService.approve/reject already implement the correct
validate-pure-pre-lock, apply-under-lock pattern (the XPostService fix
already shipped per CHANGELOG.md: "X edited_body write deferred into the
single-flight lock (M5)"). HeartbeatMutex holds no AsyncSession at all, so
the anti-pattern is structurally inapplicable there.

Adds a genuine cross-session concurrency regression test to
test_x_post_service.py (a real second DB connection, not an in-process
mock) mirroring VideoPostService's existing cross-session test, proving a
concurrently-committed post survives and the CEO's edited body never lands
on the just-posted row.

* [8f7f167a] Remove redundant inline comments flagged by QA in cross-session regression test

Both comments restated what the surrounding docstrings already say
explicitly, per QA findings F-dbadd8f0 (line 294) and F-27ac051e (line
631) — no behavior change, tests re-verified green against a sandbox
Postgres.

* [8f7f167a] Remove inline trailing comments flagged by QA (correct file this time)

QA findings F-e6f3e6a6 and F-24189858 cited tests/unit/services/
test_x_post_service.py:294 and :631 across 5 revision rounds, but that
file never contained the flagged comment text — a repo-wide grep for
the exact quoted strings shows both comments actually live in the
mirrored tests/unit/services/test_video_post_service.py file, in its
own cross-session concurrency regression tests (the caption-edit and
tiktok-skip tests). Removed both there:
- "# externally visible to the "concurrent" session below" on the
  db_session.commit() call
- "# never attempted without credentials" on the tiktok_poster.calls
  assertion

Both restated what the surrounding docstrings/test names already say;
no behavior change. Verified with the full make quality gate against a
sandbox Postgres/Redis: 13,717 passed, 94.41% coverage, clean except
one pre-existing unrelated failure in tests/unit/api/test_cloud_auth.py
::test_login_route_parses_oauth2_form_not_query_params, which connects
to the app's default localhost:5432 Postgres (not the db_session
sandbox fixture) and is unreachable in this sandboxed environment —
structurally unrelated to the auth subsystem this task never touches.

* [8f7f167a] Redis mutex pre-lock write audit (round 7): add cross-session regression tests for reject() lock protection

Round-7 QA findings F-7eb9fbcb, F-06f39a2e, and F-4d56e49b claim
XPostService.reject(), ReleaseProposalService.reject(), and
release_executor._await_proc() lack lock protection / a CancelledError
handler — but their cited line ranges (255-267, 429-454, 241-257)
describe a pre-fix, shorter version of these functions that predates
commit fb293a787d, already on this branch. At current HEAD:

- x_post_service.py reject() (lines 275-299) acquires _LOCK_PREFIX,
  re-reads under the lock, applies markers.set_x_reject_reason() +
  CANCELLED only inside the critical section, releases in finally.
- release_proposal.py reject() (lines 460-486) does the identical
  dance with _RELEASE_LOCK_PREFIX.
- release_executor.py _await_proc() (lines 257-265) already has an
  `except asyncio.CancelledError` block that kills + reaps the child
  and re-raises, mirroring the TimeoutError handler, with an existing
  dedicated regression test
  (test_await_proc_kills_child_on_outer_cancellation).

The one genuine gap: neither reject() path had a cross-session
(real second DB connection, not an in-process mock) regression test
proving the in-lock re-read catches a concurrent approve/publish that
completes mid-lock-wait — only approve() had one. Added
test_reject_concurrent_approve_completes_during_lock_wait to both
test_x_post_service.py and test_release_proposal_status_guards.py,
mirroring the existing approve() cross-session test: a second engine
commits COMPLETED between reject's pre-lock read and lock acquisition,
and the test asserts the CANCELLED write / reject-reason marker never
lands on the just-completed row.

No production code changed — verified via 103 targeted tests green
against a sandbox Postgres/Redis, plus `make -o sync gate` clean.

* [8f7f167a] Regenerate stale lifecycle artifacts (restore auditor waive_finding)

foundation-check was the only failing gate: the committed lifecycle artifacts
were missing the auditor's waive_finding verb that the lifecycle source
defines, so make quality regenerated them and failed on the diff — nothing to
do with the mutex fix (which passes ruff/mypy/tests/coverage/bandit clean).
make lifecycle restores the drift; this is what the 8 revision rounds kept
missing.

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>

* [d615f2e3] fix(tests): sync stale CLAIM_RULES pinning assertions with lifecycle.py (#635)

test_claim_rules_match_pre_gateway_table still asserted the pre-audit
two-member frozenset for CELL_PM/MAIN_PM claim rules. CLAIM_RULES in
lifecycle.py already grants both roles claim rights on
Status.AWAITING_PM_REVIEW (added by the state-machine exhaustiveness
audit) so a PM can re-claim its own review-queue task after a respawn.
Updated both assertions to include AWAITING_PM_REVIEW, matching the
actual dict. Grepped the repo for sibling stale copies of the old
literal; found none beyond this test.

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [3c4e7a35] fix(quality-gate): reflow CONCURRENCY_AUDIT.md and stub occasion lock in video tests (#636)

Root cause: PR #634's CI failed at the markdown-reflow check (make quality
Makefile:285) on CONCURRENCY_AUDIT.md — a hard-wrapped audit doc left over
from the merged "Engine dedup race + sequencing.py edge-case audit" unit
(PR #628). Fixed with `make reflow-docs` (the exact remedy the CI output
itself named).

Running the full local `make quality` (with a sandbox Postgres/Redis to
get past DB-gated skips) surfaced a second real regression from that same
PR #628 unit: it added a Redis-backed HeartbeatMutex occasion lock to
VideoEngine.open_video_task, but two pre-existing test files
(tests/unit/runtime/test_video_render_loop.py and
tests/integration/test_video_routes.py) call open_video_task without
stubbing that lock, so they failed closed against the suite's
deliberately-unreachable test Redis (_no_live_redis). Fixed by applying
the same lock-stub pattern tests/unit/services/test_video_engine.py
already uses for its own occasion-lock tests: an autouse HeartbeatMutex
stand-in fixture in test_video_render_loop.py, and wrapping the two
route-level video-request tests in test_video_routes.py with the file's
existing _LOCKED patch pair (already used by every other lock-dependent
test in that file).

The one remaining local failure,
test_cloud_auth.py::test_login_route_parses_oauth2_form_not_query_params,
is a pre-existing environment gap unrelated to this branch: it needs a
real Postgres reachable at localhost:5432 (which .github/workflows/ci.yml
provides as a service container) but this dev sandbox has no such binding
— confirmed unrelated to any of the four merged audit units.

make quality now passes clean: 13729 passed, 0 regressions, 94.49% coverage.

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [ddc8121f] regenerate lifecycle artifacts for awaiting_pm_review claim rules and waive_finding intent (#637)

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

---------

Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>

* fix(sequencing): drop lane-fallback edges that would cycle against analyzer edges

The dev-task collision fallback unioned the analyzer's authoritative edges
with same-assignee lane edges, deduping only the direct pair. A lane chain
through an unsurfaced middle sibling could still contradict an analyzer edge
transitively (the shared-last migration order inverts plain priority order),
closing a 3-cycle that made add_dependency raise ConflictError and wedged
every later delegate to that parent. Fallback edges are now accepted only
when they can't close a cycle against the edges already kept; a regression
test reproduces the exact scenario.

Also strip pre-merge cruft: remove the root CONCURRENCY_AUDIT.md working
report, delete the near-duplicate websocket-reconnect.md doc, fix the stale
a2a/page.tsx doc citation, correct the api-rate-limiting doc to state
idempotency-key retry is unimplemented, and fix two lifecycle.py comments
that referenced a guard function which never existed.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-22 08:11:28 +02:00
c8f55be904 fix(notifications): task titles and agent slugs replace raw UUIDs (#616)
* fix(notifications): task titles and agent slugs replace raw UUIDs

Notification producers interpolated raw task/agent UUIDs into subjects and
bodies ('Task 68e1e4db-... unblocked', 'handed back to 00000000-...-0004').
A tiny notification_text helper (task_display: title-first with a #id8
fallback; agent_display: identity-map slug first, DB lookup fallback) now
feeds every producer: all 13 NotificationService methods, the 7
delivery-service bodies whose subjects were already title-based, the
substitute-PM ad-hoc insert, and the orchestrator/choreographer callers,
which thread the task row's title one call deeper. Fixes the literal
'cell_pm' role string sent as an agent slug in the merge-conflict
notification. Tool-call examples like unblock('<uuid>') keep the raw id on
purpose — agents need it.

* test(notifications): board-review subject assertion matches the humanized format

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-20 20:59:36 +02:00
5b27a443e9 chore(agnosticism): close the audit residue — B6/B8/B10 + three MAJORs (#587)
Thread the deployer's product name through the X reply + feature-spotlight
prompts (B6 leftover; release/video paths shipped in #570); make the
docs-site repo/URL config (ROBOCO_DOCS_SITE_*, defaults unchanged) instead
of a roboco-website hardcode (B8); de-assert our repo from the Main PM
prompt (B10); derive PR labels from the real target branch instead of
literal to-master/to-slave; drop the stale headcount from base.md; and
make the bash-guard's Makefile check require an actual quality/gate/lint/
test target before denying raw package-manager commands (no more
false-remediation loop on Go/Rust Makefiles).

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-19 17:52:17 +02:00
Renn F 0f1e60fb95 fix(gateway): no model self-attribution in agent commits
Two layers: generated agent settings now set includeCoAuthoredBy: false
(never set anywhere before, so the CLI nudged models into appending
'Co-Authored-By: Claude ...' to commit messages), and the commit verb
strips AI-attribution lines deterministically at the chokepoint every
provider routes through.
2026-07-18 15:55:48 +02:00
Renn F 796ab05d3d test(runtime): drain fixture uses the real engine-slot initializer, not a stale copy 2026-07-18 00:56:04 +02:00
9fbec78126 feat: Telegram V2 — inbound commands + actionable approve/reject from chat (#551)
* feat(telegram): V2 inbound — command router, actionable approve/reject keyboards, chat-gated poll loop

* fix(release,x,video,telegram): terminal-state guards on approve/reject; sender-identity check

* docs(map,rag): Telegram V2 inbound surfaces and terminal-state approve/reject guards

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-18 00:47:09 +02:00
fd621f0dba feat(motion,runtime): wire the three craft capabilities to the video-authoring dev (#544)
Playwright, the taste-skill design bar, and hyperframes were all
implemented — and each stopped one hop short of the hands doing video
work: playwright reached only fe-qa/ux-qa, the design bar's web-UI dials
actively pointed a video task at 'dense product UI -> motion 2-3', and
hyperframes' agent-facing doctrine never reached any agent. Three wires:

- vendor the official HyperFrames agent skills (hyperframes-core,
  -keyframes, -creative) under motion/skills/ at a pinned upstream
  commit (Apache-2.0, attribution headers; prose reflowed to house
  style, re-vendor note in each header); README and the dev video
  prompt block point at them
- register the playwright MCP for a ux-dev spawned onto a source=video
  task (_is_video_authoring_spawn: fail-closed role/team/task-source
  probe) so the composition author can watch their HTML live in a real
  browser between renders — gating-only, agent-ux already bakes the
  browser; QA gating unchanged, be-qa/ordinary ux-dev still excluded
- design bar video-mode override in the ux_ui team prompt: video tasks
  are films, the web dials do not apply — use the cinematography bar
  and the vendored doctrine instead

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-17 05:26:42 +02:00
aa15dc40cc Feature/video artifact verification (#537)
* fix(release): CI wait polls the prod rung; escape the header tooltip apostrophe

get_latest_ci_conclusion defaults to the ladder's head rung, so
wait_for_ci searched slave for a release commit that lives on master
and timed out after 40 minutes with the run already green. The wait
now passes the prod branch explicitly. Also fixes the
react/no-unescaped-entities error that turned master's Panel CI red.

* fix(panel,video): dead dialog triggers behind tooltips; dotted composition ids render

HelpTip nested inside a Dialog/AlertDialog trigger puts the trigger's
click handler on the Tooltip root, which renders no DOM — the agents
Spawn item and the KB Reindex-All / Delete-index confirms were dead.
Tooltips now wrap the triggers. The video renderer accepts interior
single dots in composition ids (release-0.25.0) with '..' still
unrepresentable, and propose_video refuses an unrenderable id at
authoring time.

* fix(dispatch): restart-safe PM review turns

A leaf task in awaiting_pm_review had no periodic pickup: the closure
dispatcher bailed on childless tasks and skipped PR-bearing review
tasks as already-promoted, assuming the submit-time PM session was
still alive — an assumption every restart breaks. Proven live on the
docs-sync leaf after the 0.25.0 redeploy, which also dependency-blocked
its sibling dev task. Childless awaiting_pm_review tasks now flow to
the PM's review turn, and the merge turn respawns its PM when none is
active.

* feat(video): verify the rendered artifact, not the source

The 14s release-0.25.0 cut shipped with only one of four scenes visibly
registering: the dev authored DOM, the smoke asserted DOM, QA read code —
nobody consumed the rendered MP4 before the CEO did. Close that loop, and
the reject loop behind it:

- sidecar frames mode: POST /render with frames=1..32 renders the cut,
  ffprobes the REAL duration, extracts midpoint-sampled keyframe PNGs
  (timestamps in filenames), streams a tar.gz back with X-Video-Duration
- request_render do-verb (developer/QA, request_sandbox's shape): renders
  the caller's ACTUAL composition — dev's own worktree (head_sha/dirty
  provenance), QA a read-only git-archive export of the assembled branch —
  extracts frames to the container-shared .previews/ path, stamps the
  render_preview marker, returns the paths as envelope evidence
- gate: i_am_done on a source=video task refuses without a stamped
  render_preview (Requirement.RENDER_VERIFIED; canonical source string
  moved to foundation as markers.VIDEO_TASK_SOURCE; mirrored in the
  possibilities-matrix fast path so it cannot bypass the check)
- QA claim_review evidence carries video_context (composition id, the
  dev's preview, a re-render instruction) so review checks output
- dev spawn prompt block + a 4th authoring AC order Read-every-frame
  verification before submitting
- reject -> re-author: a CEO reject with a reason opens a fresh authoring
  task carrying the verbatim feedback + a revise-in-place pointer at the
  existing composition (best-effort, never fails the reject) — rejection
  feedback no longer dies on the cancelled draft

E2E: rendered the committed release-0.25.0 composition through the new
frames mode locally — the returned keyframes show exactly the reported
failure (blank frame at 5.8s, only 'Env ladder' by 12.8s), the check the
fleet was missing.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-16 19:49:26 +02:00
797847e379 [1dae04a7] Video: release 0.25.0 (revision) (#536)
* fix(release): CI wait polls the prod rung; escape the header tooltip apostrophe

get_latest_ci_conclusion defaults to the ladder's head rung, so
wait_for_ci searched slave for a release commit that lives on master
and timed out after 40 minutes with the run already green. The wait
now passes the prod branch explicitly. Also fixes the
react/no-unescaped-entities error that turned master's Panel CI red.

* fix(panel,video): dead dialog triggers behind tooltips; dotted composition ids render

HelpTip nested inside a Dialog/AlertDialog trigger puts the trigger's
click handler on the Tooltip root, which renders no DOM — the agents
Spawn item and the KB Reindex-All / Delete-index confirms were dead.
Tooltips now wrap the triggers. The video renderer accepts interior
single dots in composition ids (release-0.25.0) with '..' still
unrepresentable, and propose_video refuses an unrenderable id at
authoring time.

* fix(dispatch): restart-safe PM review turns

A leaf task in awaiting_pm_review had no periodic pickup: the closure
dispatcher bailed on childless tasks and skipped PR-bearing review
tasks as already-promoted, assuming the submit-time PM session was
still alive — an assumption every restart breaks. Proven live on the
docs-sync leaf after the 0.25.0 redeploy, which also dependency-blocked
its sibling dev task. Childless awaiting_pm_review tasks now flow to
the PM's review turn, and the merge turn respawns its PM when none is
active.

* [1dae04a7] Revise release-0.25.0 composition to 40s scene-based pacing with four feature cards

* [1dae04a7] docs(motion): update release-0.25.0 README section for 40s four-card revision

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>
2026-07-16 17:15:12 +02:00
Renn F 85ac6422ff fix(gateway): possibilities-matrix fast-path hardening + collision-context guards
The W7 fast path now rejects empty/trivial notes (its sole compensating
control for the skipped journal gates), pushes the branch before the
behind-base check, and pairs the local-gate fallback with the toolchain
guard; the WORK_ALREADY_DONE prompt no longer promises a fast path to
verifying tasks the gate routes elsewhere. build_collision_context now
degrades gracefully at all three call sites instead of breaking the
gate review, PM briefing, or collision-map route.
2026-07-15 08:25:06 +02:00
d80dfb8bbe feat(env-branches): per-project ordered environment ladder (replaces default_branch) (#534)
* [env-bran] EnvSyncEngine: orchestrator-side prod→dev cascade (default-off)

- EnvSyncEngine mirrors CiWatchEngine: cascade ladder_pairs top-down via
  GitHub merges API; clean→auto-push lower rung, conflict→one sync PR +
  tracked MAIN_PM task + stop. Never pushes prod (lower rung is never prod
  by construction).
- GitService.sync_env_branch (merges API) + open_sync_pr (idempotent) +
  _env_merge_status/_post_sync_pr helpers (constants for 201/204/409).
- TaskService.ENV_SYNC_SOURCE + list_open_env_sync_tasks (per-repo dedup).
- config env_sync_enabled/_interval_seconds(1800)/_max_open_tasks(3)/_max_per_cycle(1).
- Orchestrator 4-touch registration + _load_env_sync_set (ladder+token opt-in).
- Feature-flags card + settings FEATURE_FLAGS entry for ROBOCO_ENV_SYNC_ENABLED.

* [env-bran] Panel: environment ladder editor + types + validation

- EnvironmentRung type + environments on Project/ProjectCreate/ProjectUpdate.
- EnvironmentLadderEditor (plain useState, add/remove/up-down reorder, head/
  prod labels) reused by create + edit project dialogs.
- validateLadder (non-empty name+branch, no duplicate branches) shared,
  toast.error on submit; empty editor => null => inherits default_branch shim.
- default_branch input kept with override-hint; API client passthrough.
- 6 unit tests for validateLadder.

* [env-bran] Tests + gate green: env ladder, EnvSyncEngine, promotion chain

- tests/unit/models/test_env_branches.py: shim, head/prod, ladder_pairs,
  promotion_chain, normalize (20 tests)
- tests/integration/services/test_env_sync_engine.py: cascade clean/conflict/
  missing_ref/tokenless/degenerate/caps/dedup/disabled (9 tests, DB)
- tests/integration/test_migration_env_branches.py: 073 defaults null + round-trip
- tests/unit/services/test_release_executor*.py: add env_chain=[] to
  _ReleaseContext constructions (promotion_chain field is now required)
- tests/unit/runtime/test_orchestrator_shutdown_drain.py: register _env_sync_task
  in the stop()-drain fixture (new named background loop)
- roboco/services/git.py: revert _project_head_branch rename back to
  _project_default_branch (modify-in-place per plan); the rename in the
  consumers commit broke ~15 unit-test mocks that bind the original name
- roboco/services/env_sync_engine.py + models/env_branches.py: ruff format
- roboco/api/schemas/project.py: trailing-newline format

Backend gate green (13013 passed / 439 skipped), mypy clean, ruff clean.
Panel gate green (typecheck/lint/522 tests).

* [env-bran] fix: add env_chain to _ReleaseContext in e2e smoke (CI red)

The release-executor promotion_chain change made _ReleaseContext.env_chain
required. I fixed the three unit/release test files but missed the
construction in tests/e2e_smoke/test_background_engines.py:98 — my local
gate ran 'mypy roboco/' (excludes tests/) and I skipped 'make e2e-smoke',
so CI's mypy-on-tests + the e2e runtime job caught it instead of me.

Verified locally with the CI-equivalent gates:
  uv run mypy roboco/ tests/   -> 1170 files, clean
  ROBOCO_E2E_SMOKE=1 uv run pytest tests/e2e_smoke -> 50 passed, 1 skipped

* [env-bran] fix: extract _ensure_prod_fetched to clear xenon rank C (CI red)

_production_assess grew past xenon --max-absolute B (rank C) when the
env-branches prod-tip fetch added an if/try/except branch. Extracted the
fetch-with-fallback into _ensure_prod_fetched (degan+fetch paths), moved
_run_git to the module-level import. Local make quality green (all gates
incl xenon/vulture/deptry/import-linter/foundation-check).

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-15 04:43:37 +02:00
f2e5676198 W7: Possibilities matrix (work-already-done fast path) (#522)
* [W7] Add possibilities_matrix_enabled feature flag (default off)

* [W7] Add _work_appears_done predicate (status+commits+PR+ACs+no-open-findings)

* [W7] Add CI-green quality proxy for the fast path (local fallback on no-CI)

* [W7] Add work-already-done fast path in i_am_done (slimmed gates, no rich plan)

* [W7] Add WORK_ALREADY_DONE prompt state

* [W7] Make fast path mypy-clean (cast to helpers for _resolve_ci_status; typed mock locals)

* [W7] Extract _all_criteria_addressed to bring _work_appears_done under xenon B

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-15 04:33:24 +02:00
ba9c9d69d8 fix(dispatch): prefilter sequence-held dev tasks before spawn (#519)
* fix(dispatch): prefilter sequence-held dev tasks before spawn

_spawn_pending_dev booted a full dev container for a pre-assigned pending task
that the assignee-blind sequence guard would refuse at the claim chokepoint (a
non-terminal lower-sequence same-parent sibling — not a declared dependency).
_blocked_by_earlier_lane_sibling is narrower (same dev's lane) and
_validate_task_for_spawn checks declared deps, not sequence siblings, so the
container spawned, the first claim hit _claim_blocked_by_sequence and was
refused, and the agent exited only to be re-spawned next tick — pure churn
until the predecessor went terminal.

Mirror the PM path's _pending_claim_blocked prefilter (the exact claim-gate
predicate, fails open) at the top of _spawn_pending_dev, before the narrower
per-dev lane probe. Reuses the helper so it can't drift from the chokepoint.

* fix(dispatch): prefilter sequence-held dev tasks before spawn

_spawn_pending_dev booted a full dev container for a pre-assigned pending task
that the assignee-blind sequence guard would refuse at the claim chokepoint (a
non-terminal lower-sequence same-parent sibling — not a declared dependency).
_blocked_by_earlier_lane_sibling is narrower (same dev's lane) and
_validate_task_for_spawn checks declared deps, not sequence siblings, so the
container spawned, the first claim hit _claim_blocked_by_sequence and was
refused, and the agent exited only to be re-spawned next tick — pure churn
until the predecessor went terminal.

Mirror the PM path's _pending_claim_blocked prefilter (the exact claim-gate
predicate, fails open) at the top of _spawn_pending_dev, before the narrower
per-dev lane probe. Reuses the helper so it can't drift from the chokepoint.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-15 04:33:06 +02:00
a9dee3b34e feat(agents): force agents to the Makefile — deny raw uv/pip/conda/poetry (CEO #15) (#518)
* fix(prompts): point agents at Makefile, drop raw uv run instructions

backend.md:23-26 literally instructed raw uv run ruff/mypy/pytest (copied from
the human-facing CLAUDE.md), so agents bypassed the Makefile's UV_NO_SYNC=1 +
private UV_CACHE_DIR venv-corruption guard. Replace with make targets across
backend/developer/qa/cell_pm + a universal rule in base.md. Regenerate verbs.md
from the updated regen script (baked instruction now make foundation-check) and
align the Makefile drift message. Ships with the bash-guard deny in the next
commit so agents don't loop fighting the guard.

* feat(bash-guard): deny raw uv/pip/conda/poetry, point at Makefile

When a Makefile is present, deny raw uv run/uv pip/uv lock/add/remove, pip/pip3
install/uninstall, conda install/create/run, poetry run/install/add and remediate
to make quality/gate/lint/test. Skipped when no Makefile (Makefile-less projects
not blocked). ROBOCO_GUARD_SKIP_PM=1 (grok path) nudges exit 0 instead of the
run-canceling exit 2. Overrides the prior bare-uv-run-allowed stance by CEO
direction; the /app-targeted blocks above keep priority.

* feat(grok): deny raw uv/pip/conda/poetry via native --deny + PM-skip nudge

Add _RAW_PM_DENY (uv run/pip install/lock/add/remove, pip/pip3 install, conda
install/create/run, poetry run/install/add) to _deny_rules so grok's graceful
native --deny blocks raw package-manager commands (model adapts to make, run
continues — unlike a hook deny which cancels the run). The bash-guard hook
keeps the compound-command fallback (cd x && uv run) and nudges exit 0 there via
ROBOCO_GUARD_SKIP_PM=1 in the grok hook env, never canceling.

* test(bash-guard): align existing tests with W1 Makefile-gate policy

Raw uv run / pip install are now Makefile-gated (W1, CEO item #15), so two
existing bash-guard invariants reverse:

- test_allows_pytest_even_if_suite_uses_requests keeps its HTTP-injection
  allow-path intent but uses bare `python -m pytest` (raw `uv run` is now
  denied); the deny case is covered by test_bash_guard_makefile_guardrail.
- test_allows_pip_install_in_workspace -> test_denies_pip_install_when_makefile_
  present: a workspace clone carries a Makefile, so bare pip install is now
  denied -> agents use `make` / `uv sync --extra dev`. Makefile-less skips
  stay covered.

Gate: 12994 passed, 439 skipped, 94.81% cov (DB env :55432 user renzof);
the lone flaky integration error passes in isolation (DB-state race, not W1).

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-15 04:32:27 +02:00
Renn FandRenzo F 1c63c88cbf test(audit): guard await_args against None for mypy union-attr
CI mypy (which checks tests/, unlike the targeted source-only run that
missed it) flagged ack_mock.await_args.args[1] — await_args is
_Call | None. Assert it is not None first, matching the spawn_call
pattern above.
2026-07-14 06:14:27 +02:00
Renn FandRenzo F 6fe0067f73 fix(orchestrator): stop auditor alert-spawn rotation — ack as auditor on dispatch
The auditor respawned every ~3 min on the same stale rework alerts.

Root cause: _dispatch_audit_work's alert path fetched the SYSTEM-wide
"not fully acked" view (list_system_notifications), but the auditor is
read-only (no ack verb) and auditor_triage never acks — so once an alert
existed the CEO was the only party who could clear it, and the CEO hadn't
acked. The per-alert cooldown (PR #499) only paced a rotation through the
N un-acked alerts; it was a damper, not a fix.

Fix: fetch the auditor's OWN pending-ack view (GET /notifications authed
as the auditor -> list_for_agent, which filters acked_by for the auditor)
and ack the alert as the auditor on dispatch. Each alert is now a
one-shot, DB-persistent: the next tick cannot respawn on an alert the
auditor already observed — even one the CEO hasn't acked. Authed as the
auditor (not the system identity) so the route selects the per-recipient
view; HTTP rather than DB-direct so it shares the orchestrator's loop in
prod and stays loop-safe in the e2e harness (which runs _dispatch_audit_work
in its own asyncio.run loop, away from the app's DB engine).

e2e now asserts the alert is in acked_by for the auditor after dispatch —
the rotation-stopper itself, not just the spawn.
2026-07-14 06:14:27 +02:00
Renn FandRenzo F 1f769f6315 [sandbox-ext] fix: drop dynamic verify SQL (bandit B608) — static query + Python membership
CI bandit -ll flagged B608 at sandbox.py:264 (f-string ANY(ARRAY[...])
with interpolated feature names). Root-cause fix: verify_step now runs a
static 'SELECT extname FROM pg_extension' and verify_ok checks set
membership against the installed extnames — no interpolation, no string-
built-SQL surface, and a more correct check (membership vs count). The
enable_step CREATE EXTENSION stays (identifiers can't be parameterized;
allowlist-validated upstream, the containment). Tests updated from the
count-based exec_out to the extname-list exec_out.
2026-07-13 20:05:45 +02:00
Renn FandRenzo F e7d7311636 [sandbox-ext] Phase 3: parameter surface — schema + project field + verb override + cache-by-features
Migration 072 adds projects.sandbox_extensions (jsonb null): a per-service
extension/module map a venture declares up front (e.g. {"postgres":
["vector","postgis"],"redis":["search"]}). Additive + nullable so
existing opted-in projects stay byte-for-byte bare — no default set, opters
set the extensions they need explicitly (TimescaleDB out unless asked).

Project model validates the map against SANDBOX_ENGINE_FEATURES: unknown
service keys and unallowed features are rejected at the model boundary with
the allowlist named (plpython3u — superuser-RCE — excluded by construction),
empty feature lists drop to bare, order normalized + deduped. The allowlist
is the security containment, not privilege. Mirrors sandbox_services: not on
ProjectCreate, only Project + ProjectUpdate.

request_sandbox gains an extensions arg; _sandbox_features_scope unions a
per-call override with the project's standing set (trusted), bounds it to the
opted set + allowlist, rejects a non-opted service or unallowed feature with
the allowlist named in remediate — scope-first priority preserved by
rej_scope or rej_features. ensure_sandbox threads features through to
provision(); cache-by-features: a cached entry satisfies a new call iff
services are a subset AND every requested feature per service is already
cached — a feature superset re-provisions (rotates creds), mirroring the
services-superset case. available_extensions rides the evidence payload so an
agent doesn't guess what was activated.

Gate: ruff clean, mypy clean (9 modules), 51 tests pass (incl. migration
round-trip).
2026-07-13 20:05:45 +02:00
Renn FandRenzo F 3838d64eaa sandbox: kitchen-sink images, feature-aware selection (Phase 2)
Phase 1 made the provisioner able to activate allowlisted extensions
post-ready but kept the bare upstream images. Phase 2 ships the images that
actually carry the extension/module files, and selects them only when a
venture requests features — bare sandboxes stay on the light upstream image
(no heavier pull, honoring the 'existing opters stay bare' decision).

- _PostgresEngine / _RedisEngine gain kitchen_sink_image + image_for(features):
  bare (no features) -> the light image; features requested -> the kitchen-sink
  image. The provisioner runs engine.image_for(features), not engine.image, so
  the bare path is byte-for-byte unchanged. Mongo inherits the base image_for
  (returns its image regardless — no activatable features).
- docker/sandbox-pg.Dockerfile: pgvector/pgvector:pg16 (ships vector) + postgis
  apt install; contrib (pg_trgm/citext/uuid-ossp) inherited from the official
  postgres base. Built at deploy via the sandbox-pg-image compose one-shot
  (mirrors the agent-image builders); the provisioner's _ensure_image finds the
  local tag and never pulls. Published by release.yml; pulled in registry
  compose. The verify step fails loudly if an extension's files are missing.
- _RedisEngine kitchen-sink image: redis/redis-stack-server:latest (headless;
  ships search/json/bloom as loadable-but-unloaded modules — no custom build).
- Extended the sandbox image-tag ghost-tag guard (the mongo:8-alpine regression
  test) to also cover kitchen_sink_image: skips locally-built roboco-* images,
  uses the namespaced Docker Hub endpoint for redis/redis-stack-server.

Image-specific package names / module .so paths are verified at the CEO's NAS
deploy (the spec's NAS smoke); the unit tests with the fake runner remain the
CI bar, and the verify step is the fail-loud safety net for a wrong build.
2026-07-13 20:05:45 +02:00
Renn FandRenzo F b015cde9ad sandbox: post-ready extension/module activation + allowlist (Phase 1)
Parameterized sandbox dev DBs — groundwork for 'extensions on the fly'
(docs/internal/specs/2026-07-13-sandbox-extensions-on-the-fly.md). A
venture declares the extensions/modules it needs; the provisioner activates
them post-ready via docker exec, never via bind-mounts or initdb scripts.

Phase 1 (behavior-preserving scaffolding — no image, no schema, no caller
passes features yet):

- Allowlists SANDBOX_PG_EXTENSIONS / SANDBOX_REDIS_MODULES are the ONLY
  extensions/modules the system will ever activate — the security
  containment, not privilege. plpython3u & co. (superuser-RCE vectors)
  are excluded by construction.
- SandboxEngine ABC gains enable_step / verify_step / verify_ok. pg:
  CREATE EXTENSION IF NOT EXISTS via psql, verified by a pg_extension
  count. redis: MODULE LOAD per module, verified by MODULE LIST. mongo:
  no-op (server is batteries-included).
- SandboxProvisioner.provision takes features={service: [names]},
  allowlist-validates before any container runs, runs enable then verify
  after the base readiness probe; a failed enable or a short verify (image
  missing the extension files) is fatal — an agent never receives creds
  for a db missing what it asked for. Empty features = bare = the
  existing path, byte-for-byte unchanged.
- SandboxConnection gains features; as_payload surfaces
  available_extensions / available_modules so the agent doesn't guess.

13 new unit tests (fake docker runner): enable/verify argv per engine,
allowlist rejection of plpython3u before any run, failed-enable + failed-
verify fatality, bare-provision unchanged, payload surfacing.
2026-07-13 20:05:45 +02:00
a3524da5f8 [90c9474c] Auditor revival: scheduled audit trigger and reactive alert producers (#499)
* [927e64d5] Backend slice: auditor scheduled trigger and reactive alert producers (#496)

* [1f2cdb4b] Reactive alert producers at QA-fail and rework (#492)

* [1f2cdb4b] feat(services): add auditor-targeted rework alert producers at QA-fail and rework chokepoints

* [1f2cdb4b] test(services): fix mypy typing in auditor alert producer unit tests

* [1f2cdb4b] docs(backend): document reactive auditor rework alert producers in map and role docs

---------

Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [5173415f] Scheduled audit trigger, config, and sweep prompt (#493)

* [5173415f] Add scheduled audit trigger, interval config, sweep prompt, and focused tests

* [5173415f] Allow ROBOCO_AUDIT_INTERVAL_SECONDS=0 to disable scheduled sweeps

* [5173415f] docs(audit): document scheduled auditor sweeps and ROBOCO_AUDIT_INTERVAL_SECONDS

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [a26c18b9] E2E smoke test for auditor triggers (#495)

* [a26c18b9] Add e2e smoke test for auditor scheduled and reactive triggers

* [a26c18b9] docs(tests): add e2e smoke test catalog and changelog entry for auditor triggers

---------

Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [3bc47cdc] Fix _fresh_orchestrator state for auditor trigger e2e tests (#497)

* [3bc47cdc] fix(tests): initialize orchestrator state in _fresh_orchestrator helper

* [3bc47cdc] docs(changelog): add _fresh_orchestrator test harness fix entry

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [8323cd50] Fix e2e smoke regression on assembled cell PR #496 (#498)

* [8323cd50] fix(e2e_smoke): repair auditor-trigger smoke tests and harden harness

* [8323cd50] docs(tests): document e2e smoke harness hardening for PR #498

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

---------

Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [37e6d999] Backend: repair failing CI checks on auditor revival PR #499 (#503)

* [6e79bada] Triage and fix Python quality gate and Analyze (python) failures (#501)

* [6e79bada] fix(task): replace type ignore with forward-reference cast for SQLAlchemy Mapped UUID in get_all_descendants

* [6e79bada] fix(notification_delivery): add generic type arguments to dict return types in get_ack_status and get_delivery_summary

* [6e79bada] docs(changelog): add Python quality gate type-hygiene fixes to Unreleased

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [50e7e104] Triage Analyze (javascript-typescript) failure on backend-only diff (#500)

* [50e7e104] Split CodeQL workflow so JS/TS analyzer only runs on panel changes

* [50e7e104] docs(backend): document split CodeQL workflow triggers and branch protection notes

---------

Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [203c426b] Triage and fix e2e lifecycle smoke (scripted agents) failure (#502)

* [203c426b] fix(orchestrator): pre-initialize _instances in __new__ so __init__-bypass tests survive _dispatch_audit_work; allow audit_interval_seconds=0; mount /api/notifications in e2e harness

* [203c426b] fix(e2e_smoke): restore ROBOCO_AGENT_TOKEN isolation and clarify /api/notifications mount comment

* [203c426b] docs(map): document orchestrator __new__ pre-init and e2e harness token isolation for auditor-revival smoke fix

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [48cb05c2] Fix remaining e2e lifecycle smoke (scripted agents) failure on auditor-revival PR #503 (#504)

* [48cb05c2] Harden AgentOrchestrator __new__ pre-init for auditor dispatch state

* [48cb05c2] Document auditor-dispatch pre-init rationale in AgentOrchestrator __new__

* [48cb05c2] docs(orchestrator): extend __new__ pre-init docs for auditor-dispatch state

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>

* [90c9474c] intake: ambient workspace note + dedupe scope clones by git_url

Two intake follow-ups folded into 90c9474c's spec Notes:

(a) _resolve_intake_ambient now prepends a workspace note so the intake
    agent knows its cwd holds clones of every project in the scope (the
    primary at cwd, siblings alongside under /data/workspaces) and drafts
    against the real trees via Grep/Glob/Read, not from memory.

(b) _clone_intake_scope dedupes slugs by git_url before cloning. A
    multi-project scope can list several projects pointing at one repo
    (a monorepo's cell-projects share a git_url); cloning each produced
    redundant identical workspaces. Mirrors CI-watch's per-git_url dedupe:
    keep the first slug per non-empty git_url; a project with no/empty
    git_url is never collapsed onto another so distinct local repos still
    clone. The dedupe is a pure static helper (_dedupe_slugs_by_git_url)
    with unit coverage.

---------

Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-13 15:41:00 +02:00
ba7135ba50 feat(gateway): carry intake technical depth down the chain + widen review coherence scope (#491)
Two structural issues flagged by the CEO:

1. Task technical-depth dilution — intake's rich analysis (file:line
   targets, code examples, rationale) was getting lost as it traveled
   umbrella -> root-subtask -> cell -> dev. The detail IS preserved in
   Task.description; the dilution was in delegation (PMs re-authoring)
   and the intake prompt not demanding depth.

   Fixes:
   - evidence_repo: ancestor_context_for_task walks the parent chain
     (cycle-guarded, depth-capped 16, desc-clipped 1500) and surfaces it
     as parent_context in the evidence payload, so a leaf dev finally
     sees the upstream intake analysis instead of a bare title.
   - evidence_builder: Task.description now rides in the payload;
     EvidencePayload gains description + parent_context (omit-when-empty
     so no null noise).
   - orchestrator: _description_body (capped 4000) injects the
     description into the dev spawn prompt + SessionStart briefing.
   - role prompts (main_pm/cell_pm/developer/prompter): teach pass-the-
     torch, don't-dim-it; prompter now demands file:line/code-examples
     in the_work/notes (reconciled with the no-code-level-ACs-on-roots
     rule). main_pm's brief-not-a-spec scoped: not-a-spec applies to the
     solution only, facts forward verbatim.

2. PR-review/QA scope too narrow — they only checked the AC checklist,
   not whether the change is coherent with project structure/intent.

   Fixes:
   - qa.md + pr_reviewer.md: Coherence & intent rule (intent via
     description+parent_context, coherence with project patterns,
     standards). Criterion-less major findings allowed for intent drift
     (Finding.criterion is optional).
   - parent_context + description wired into the gate/QA/inbound-PR
     evidence builders (fail-open, logged).

Skipped per YAGNI: a technical_spec JSONB column (detail is already in
description) and a criterion_kind enum (criterion is already optional).

All gates green: ruff, mypy (1152), pytest (12883 passed, 94.82% cov),
xenon, vulture, bandit, pip-audit, deptry, alembic, import-linter,
foundation-check.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-13 08:06:57 +02:00
cea3e56628 feat(lifecycle): revision findings ledger — structured failure feedback, persisted and delivered down the chain (#486)
* feat(lifecycle): revision findings ledger — structured QA/PR/PM/CEO failure feedback, persisted and delivered down the chain

Every bounce used to survive only as flattened prose: rounds overwrote each
other in notes_structured, request_changes persisted nothing, two raw
dev_notes appends were silently destroyed by the next handoff note, and the
dev prompt pointed at fields (qa_notes via evidence(), pm_notes) the API
never delivered. Agents re-interpreted and re-discovered every failure
before they could start fixing it.

- task_review_findings (migration 071, append-only): file/line/severity/
  criterion(AC-id-validated)/expected/actual/fix/evidence per finding, with
  origin (qa|pr_gate|pm|ceo), round, and an open->addressed->verified
  lifecycle (waived reserved); new tasks.pm_notes + PmReviewContent give
  request_changes a structured home
- producers: fail_review/pr_fail/request_changes take findings=[...] (prose
  issues shimmed+merged for one release, deprecation-logged); ceo_reject
  validates its reason (no 500), lands an origin=ceo finding, and bumps
  round+audit on branchless coordination roots; guardrails at the verb
  chokepoint (nudge >5, hard reject >10, field caps, traversal-safe file);
  the dev_notes data-loss appends are removed; new task.request_changes +
  task.ceo_reject audit events close rework attribution
- delivery: qa_notes/pr_reviewer_notes/pm_notes carry the deterministic
  [F-id8] rendering; claim briefings, evidence(), the REVISION_REQUIRED
  spawn prompt, PM triage bounced-blocks, and A2A bodies deliver open
  findings; round-N+1 QA and gate reviewers get the full prior ledger;
  panel Findings tab + bounced-xN chip; metrics pm_rejects/ceo_rejects +
  findings counts; vault task notes render a Findings section (fail-open)
- resolution closes for every origin: i_am_done and submit_up/submit_root
  take resolved_findings gated by FINDINGS_ADDRESSED (owner-gated so a
  stale non-owner PM can never mutate the ledger); pass_review/pr_pass/
  complete verify-stamp same-transaction; ceo_approve stamps best-effort
- 24 real-DB integration tests drive the full loop through the real
  choreographer; full suite 12856 green

* docs: revision findings ledger sweep — CLAUDE.md, map, RAG corpus

- CLAUDE.md: new ledger section + corrected request_changes row
- docs/map/review-findings.md (new subsystem map) + surgical updates to
  task-service/pr-gate-review/metrics-observability/vault/panel maps
- docs/rag: producers' findings contract across qa/pr-reviewer/developer/
  cell-pm/main-pm/ceo role docs (the PM docs were missing request_changes
  entirely), verb references, and a new architecture/review-findings.md
  disambiguating ledger findings from convention findings

* test(e2e): resubmit resolves the pr_fail finding per the ledger contract

The scripted pr_fail revision loop resubmitted submit_up without
resolved_findings — correctly rejected now that FINDINGS_ADDRESSED gates
the PM resubmit verbs (green locally, red only in CI since the e2e suite
skips without ROBOCO_E2E_SMOKE=1). The scripted PM now reads the open
ledger row pr_fail persisted (new open_finding_ids arc helper) and
resolves it on resubmit, asserting the open set drains — exercising the
coordinator half of the new contract end to end.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-11 22:54:42 +02:00
d03181ab48 feat(vault): Obsidian vault V2 — janitor, archival, weekly report, KB ingest, Bases + sync runbook (#482)
* feat(vault): V2 — create-seam + drift janitor, archival, weekly org-report, KB ingest, Bases views + sync runbook

Implements the vault V2 canonical spec end to end (the splice guard shipped
separately and is reused at KB-ingest time):

- materialize-on-create: TaskService.create writes each task's note best-effort
  from the moment it exists; the transition-touch stops no-oping on live work
- drift janitor (services/vault_janitor.py + hourly _vault_janitor_loop): daily
  changed-task re-projection, random drift sample, archival pass — restart-proof
  via RoboCo/_meta/.janitor_state.json, 200/cycle caps, per-item isolation,
  processed-only resume markers, self-repairing state file
- archival: vault_archive_days (30, 0=off) moves old terminal tasks' notes to
  RoboCo/Archive/<year>/Tasks/<project>/ — one write_task code path for janitor
  and rebuild, id8 lookup across Tasks/+Archive/, alias links keep moves safe
- weekly org-report: VaultWriter.write_org_report renders Reports/<ISO-week>.md
  from MetricsService/UsageService (numbers duplicated into frontmatter for
  trend queries), once per ISO week, with a best-effort CEO notification
- KB ingest: IndexType.VAULT_NOTES + VaultNotesIndexPlugin + _vault_kb_loop
  embed the CEO's RoboCo/Notes into the RAG corpus — injection guard as a hard
  gate (flagged notes quarantined with an idempotent callout), traversal- and
  symlink-contained at both config and engine layers, content-hash dedup,
  50-ingest/cycle cap, frontmatter stripped; reaches roboco_kb_search, the
  mentor default domain, claim-time briefings (kind vault_note), and the panel
  KB browser; no migration (chunks table auto-creates; migration 030's
  CHUNK_TABLES tuple appended per the chunks_playbooks precedent)
- Bases views (Task Board.base, Reports.base — schema verified against the
  Obsidian docs) + the Mac sync runbook vault asset
- config/flags/compose: vault_archive_days, vault_report_enabled (flags card),
  vault_kb_enabled (flags card; NAS compose arms it, registry ships it off),
  vault_kb_dirs (+ overlap/traversal validator), vault_kb_interval_seconds
- e2e smoke (tests/e2e_smoke/test_vault_v2.py): real create-seam, real janitor
  cycle incl. archival + state, real KB engine + real guard

* docs: vault V2 sweep — map, RAG corpus, CLAUDE.md

- docs/map/vault.md: V1+V2 — janitor/archival/report/KB data flows, new files,
  config, health posture
- docs/map/orchestrator.md + task-service.md: the two new loops, the create
  seam, the three janitor queries
- docs/rag/architecture/obsidian-vault.md: agent-facing what-changed (notes
  from creation, archive link-safety, CEO notes retrievable, weekly report)
- docs/rag/architecture/config-reference.md: the five new settings
- CLAUDE.md: vault paragraph covers V1+V2; flags-card list mentions the vault
  report/KB flags

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-11 15:51:19 +02:00
20110debab fix(ci): fleet-branch push triggers + dispatcher claim prefilter (#463)
* fix(ci): fleet-branch push triggers close the absent-check gap; dispatcher claim prefilter

PROVEN with API receipts: when the PM squash-merges a subtask PR into a
branch that is itself another PR's head (GitService.merge_pull_request →
GitHub's Merge API), the pull_request synchronize webhook fires
unreliably (1 of 3 in the live sample) while plain push events fired
100% — so PR heads sat with ABSENT required checks that three review
rounds mistook for green. CI, CodeQL, e2e-smoke, and panel-ci now also
trigger on push to the fleet's branch types, deduped by a concurrency
group keyed on head_ref||ref_name so a branch that is also a PR head
never double-runs.

Dispatcher churn: _route_unassigned_pm_task consults the claim guards'
own predicate (TaskService.is_pending_claim_blocked, a public wrapper —
no duplicated SQL) before routing, so dependency- or sequence-held
tasks skip the tick with zero HTTP claim round-trips; fails open so a
DB hiccup degrades to the old behavior.

* chore(docs): reflow hard-wrapped prose inherited from the six-PR merge train

* chore(foundation): regenerate lifecycle artifacts; reflow inherited prose

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-11 09:21:15 +02:00
f0f09b2204 [1197c975] Re-add Playwright chromium to FE/UX QA images and add browser-verification prompt guidance (#406)
* [3e552255] Re-add Playwright chromium to QA images + prompt guidance (#395) (#405)

* [3e552255] feat(docker): re-add Playwright chromium-headless-shell to QA images

* [3e552255] ci(docker): add Playwright QA image build + headless smoke check workflow

* [3e552255] fix(ci): scope agent-image-smoke.yml trigger to paths only, add PR comment

The workflow was gated by `branches: [master]` on both push and
pull_request, but this repo's task-hierarchy PRs open against nested
parent feature branches, not master, until root->master assembly - so
the workflow never fired on a dev-level PR and produced zero evidence.
Drop the branch filter (path scoping is sufficient) and post the
size-delta table + smoke-check output as a PR comment via
actions/github-script, since no agent role has gh CLI or GitHub API
read access to pull check-run output directly.

* [3e552255] fix(ci): post agent-image-smoke PR comment even on step failure

The 'Post results as a PR comment' step only had
`if: github.event_name == 'pull_request'`, which GitHub implicitly ANDs
with success() — so if the docker build or headless-launch smoke check
failed, the PR comment (the only evidence-delivery path QA/PM has, since
no agent role can read the Checks tab) silently never posted. Added
always() so a partial report always lands on the PR.

This commit also re-lands the branches-filter removal + PR-comment step
from 747e4d74 to make sure this fix actually reaches the remote PR
branch — QA's needs_revision at 05:48 came after that commit's local
timestamp (05:43) but still saw the pre-fix workflow, indicating the
prior push never reached GitHub.

* [3e552255] docs(qa): add browser verification guide and CHANGELOG entry for Playwright chromium

- Added comprehensive QA browser verification guide at docs/backend/qa/browser-verification.md covering setup, examples for fe-qa/ux-qa, limitations, and troubleshooting
- Updated CHANGELOG.md with Unreleased entry documenting Playwright chromium-headless-shell re-add to agent-qa-fe and agent-ux images, CI smoke workflow, and links to QA guidance
- Guide explains when to use browser verification (rendered output, computed styles, a11y, visual design), how to launch headless chromium, and provides practical examples for both FE and UX QA use cases
- References built-in guidance in fe-qa.md/ux-qa.md identity prompts and CI smoke workflow verification

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [9aafe8f5] Revision: fix reflow-check CI failure and resolve orchestrator Playwright-allowance gap (#421)

* [9973237c] fix(docs): separate reflow-joined metadata lines in browser-verification.md (#419)

scripts/reflow_md.py --check treats two adjacent non-blank lines as one
paragraph and flags it as hard-wrapped prose needing a join. Insert a
blank line between the `**For:**` and `**Purpose:**` metadata lines so
each stays its own single-line paragraph; verified the reflow is now a
no-op and the non-whitespace token sequence is unchanged.

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [cc355a2b] docs(qa): add no-op analysis for orchestrator.py Playwright allowance (#422)

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [a506cc10] docs(qa): quote real Dockerfile Playwright snippets in no-op analysis (#424)

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [2b35dd4e] Resolve merge conflict, confirm green CI, add Playwright MCP registration, re-verify 5 ACs (#447) (#450)

* [2b35dd4e] docs(changelog): resolve Unreleased/0.22.0 ordering conflict, keep Playwright entry

* [2b35dd4e] feat(runtime): register Playwright MCP server for fe-qa/ux-qa, per CEO round-3 note

Adds @playwright/mcp to agent-qa-fe and agent-ux images, wired via a wrapper
entrypoint that points the server at each image's already-baked
chromium-headless-shell instead of downloading a second browser. The
orchestrator registers the `playwright` MCP server only for the qa role on
the frontend/ux_ui teams, so be-qa and ux-dev never see it. Updates the QA
identity prompts and docs/backend/qa/browser-verification.md to document the
structured mcp__playwright__* tools in place of hand-scripted Bash+Python,
adds CI smoke coverage (binary + baked-chromium resolution + a real
panel-page screenshot from inside the ux-qa image), and records the change
in CHANGELOG.md.

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [1197c975] type the mcpServers extraction so mypy's no-any-return passes

* [1197c975] extract role-scoped MCP registration — orchestrator back under the complexity budget

The playwright branch pushed _generate_mcp_config to rank C in the merge
ref; docs/research/playwright registrations move to one helper, behavior
identical.

* [1197c975] basename-sanitize the MCP config filename

CodeQL's path-injection query re-fired on the (moved, unchanged) config
write; agent ids are orchestrator-issued, but the filename now rides the
same os.path.basename sanitizer _grok_usage_json established.

* [1197c975] basename the agent id variable itself — the sanitizer shape CodeQL models

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-11 07:39:53 +02:00
7f138d3bf5 [e4ed92d6] Video pipeline per-project requests, re-render action, composition preview (#403)
* [7f2c881a] Project-scope video pipeline + re-render + preview proxy (#386) (#396)

* [7f2c881a] feat(video): scope on-demand video requests + render loop to project_id

Require project_id on VideoRequestBody (404 when unresolvable or not
opted into the video engine), thread it through VideoEngine.open_video_task
via a shared resolve_authoring_project helper, and resolve the render
loop's motion/ workspace from the authoring task's own project_id instead
of the hardcoded self_heal_project_slug.

* [7f2c881a] fix(video): cast task.id to UUID before VideoEngine.rerender calls

mypy flagged task.id as sqlalchemy.sql.sqltypes.UUID[Any] rather than
uuid.UUID in the three rerender tests; cast to UUID per the codebase's
established idiom (cast("UUID", obj.id)) used elsewhere for the same
SQLAlchemy Mapped-attribute inference gap.

* [7f2c881a] docs(video): API endpoints for project-scoped requests, re-render, and preview proxy

Add comprehensive API documentation for the new project-scoped video engine endpoints:
- POST /api/video/request: on-demand video authoring scoped to project_id (breaking change)
- POST /api/video/pipeline/{task_id}/rerender: CEO-triggered re-render with idempotency key clearing
- GET /api/video/preview/{task_id}/{file_path}: CEO preview proxy with path-traversal confinement

Document project-scoping architecture: authoring tasks and render loop now resolve from task's own project_id instead of hardcoded self_heal_project_slug.

Add migration guide covering breaking change to VideoRequestBody schema (project_id now required), error handling changes (404 on unresolvable/non-opted-in projects), and client migration steps.

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [8f959c3b] docs(ux_ui): add project picker, re-render control, and composition preview panel spec (#381) (#398)

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>

* [1fb5b5cb] Project picker, re-render button, and composition preview panel (#397) (#402)

* [1fb5b5cb] feat(video): project picker, re-render button, and composition preview panel

* [1fb5b5cb] docs(video): add comprehensive guide for project picker, re-render button, and composition preview panel

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [a512f364] Add video_engine_enabled to ProjectSummaryResponse (#412) (#414)

* [a512f364] feat(api): surface video_engine_enabled on ProjectSummaryResponse

* [a512f364] docs(api): document video_engine_enabled on ProjectSummaryResponse

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [03607ab9] Fix re-render control gating/placement and project picker filter (#434)

* [f2f3e89f] Fix RerenderControl gating/placement across queue and strip views (#431)

* [f2f3e89f] feat(video): widen RerenderControl gating and share it across queue/strip views

Extracts RerenderControl into a shared panel/src/components/dashboard/
video-rerender-control.tsx component, widens its gate from
render_status === 'failed' to source_task_id + composition_id present
(matching what the backend rerender endpoint actually requires), adds a
confirm dialog before firing the mutation, and wires the same component
into video-pipeline-strip.tsx for still-in-flight rendering/render_failed
rows.

* [f2f3e89f] docs(video): enhance RerenderControl JSDoc with gating logic and usage examples

Add comprehensive JSDoc to the RerenderControl component covering its
purpose, gating logic (render for any source_task_id + composition_id,
regardless of render_status), three visual button states (idle/loading/
error), confirm-dialog guard behavior, and usage examples for both
video-post-queue.tsx and video-pipeline-strip.tsx contexts. Explains
why the backend's rerender endpoint doesn't require a failed render and
how the component prevents accidental re-renders.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [404d8ed3] Filter project picker to video-engine-enabled projects (#432)

* [404d8ed3] feat(panel): filter video-request project picker to opted-in projects

Add video_engine_enabled to the client ProjectSummary type, give
ProjectSelector a videoEngineOnly filter prop, default RequestVideoDialog's
picker to the current video-enabled project with a friendly empty-state
when none exist, and cover the filter with a new project-selector test.

* [404d8ed3] docs(panel): add ProjectSelector component API reference with videoEngineOnly filter

Document the reusable ProjectSelector component with its props, filtering behavior,
and new videoEngineOnly filter for video-engine-enabled projects. Follows the
existing component documentation pattern from page-refresh-provider.

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [519a4088] fix(panel): import missing RerenderControl in video-post-queue and correct stale doc (#436)

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>

* [8e912c3e] Reflow hard-wrapped video UX design doc to pass quality gate (#439)

* [ccfe2015] docs(ux_ui): reflow video request composition-controls spec to one line per paragraph (#438)

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>

* [99c3ed9c] docs(backend): reflow hard-wrapped prose in video-engine-endpoints.md and video-project-scoping.md (#442)

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>

* [c2e98fc0] docs(backend): strip stray trailing whitespace in video-engine-endpoints.md fence (#451)

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>

* [9c7bc11a] Reflow all 3 hard-wrapped docs on this branch and verify quality gate (#457)

* [9c7bc11a] test(scripts): guard reflow_md.py --check wiring into make quality

* [9c7bc11a] docs(standards): document markdown reflow quality gate workflow and verification

Added comprehensive guide explaining the one-logical-unit-per-line markdown prose standard, how the reflow check integrates into make quality, the three reflowed files (video-engine-endpoints.md, video-project-scoping.md, composition-controls spec), and the regression test added to ensure wiring stability. This task verifies all three ACs are satisfied: reflow_md.py --check exits 0, make quality passes (non-DB portions), and the three files are whitespace-only reflowed.

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>

* [002f0cdd] docs(rag): document reflow-check zero-diff troubleshooting path (#459) (#460)

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [e4ed92d6] fix rerender missing-task test — assert the empty queue it creates

The test never seeds; the trailing assertion expected a phantom video
post. Broken since the branch's first commit but never executed — every
earlier CI run short-circuited at a pre-pytest gate step.

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-11 07:39:17 +02:00
15a3e87a2f feat(vault): Obsidian vault V1 — projection core, Auditor narrative, input loop (#458)
* fix(gateway): gate review diffs against the task's real parent branch (#444)

The in-path PR-review gate's evidence diff (claim_gate_review) and the
pr_pass conventions guard derived their diff base via parent_branch_for
string surgery, which reuses the child branch's own team segment — wrong
for every cross-team hop (a frontend child of a main_pm root derives a
ref that never existed) and silently falls back to the repo default
branch, so the reviewer judged the entire inherited base-branch content
as the task's own work and failed acceptance criteria the task never
touched. Bounced a live goals-tab fix three times, unfixable by branch
surgery.

The gate now resolves the base via resolve_parent_branch (the parent
task's recorded branch_name, cross-team correct) and threads it as a new
preferred_parent override through git.diff / list_changed_files /
conventions_check_for_task — consulted only when no explicit base is
given, so the pinned literal-base contract (base="HEAD~1") and every
other diff caller (QA, doc, content) are byte-identical. Parent lookup
fails open (derived-base fallback) like the other resolve_parent_branch
call sites, and is skipped entirely while the conventions flag is off.

Also excludes .uv-cache/ and .claude/ (agent worktrees, private uv
cache) from the markdown prose scanner — both are repo-local tool dirs
whose vendored/generated files tripped make reflow-check.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>

* feat(vault): Obsidian vault V1 — projection core + input loop

The vault is a rebuildable projection of the DB (never a source of
truth), default-off behind ROBOCO_OBSIDIAN_VAULT_ENABLED + ROBOCO_VAULT_PATH.

Projection core: VaultWriter materializes tasks/journals/A2A/agents as
wikilinked markdown (id-suffixed stable filenames, alias-based links so
renames never break, is_private journals excluded like the RAG corpus);
event seams materialize on journal write and A2A send and touch task-note
frontmatter at the status-transition chokepoint — all best-effort, a
vault failure never blocks a verb. Shipped .obsidian config (Dataview,
Kanban, team/status graph groups) + _meta dashboards; python -m
roboco.vault rebuild/relocate (rebuild preserves the narrative section).

Auditor narrative duty: curate_vault content verb (auditor-only,
playbook-curation pattern) spawned by a dedicated root-completion hook
with its own cooldown — fully separate from _dispatch_audit_work, whose
scheduled-sweep/alert-producer revival belongs to the queued fleet task.

Input loop: VaultIntakeEngine (ROBOCO_VAULT_INTAKE_*) watches the
intake folder for #roboco-tagged notes and materializes each as ONE held
draft (confirmed_by_human=False, Secretary-owned, source=vault_note,
excluded by the dispatchers via _is_held_ceo_source) with local-model
extraction and a deterministic fallback; vault_seen_notes ledger
(migration 070) keyed on path+content-hash (the CEO-feedback callout is
stripped before hashing so the engine's own append never self-triggers);
per-cycle and open-draft caps. Nothing auto-starts.

* fix(vault): integrate with master — re-chain migration 070 onto 069, mypy-clean tests

The vault branch was cut from slave before the sequence-gate promotion,
so migration 070 chained from 068 while master already carried 069 —
two heads on merge. Master is now merged in and 070 revises 069.
Vault test files also get mypy-clean mock idioms (monkeypatch.setattr
over method assignment; await_args narrowed before access).

* fix(scripts): dedupe SKIP_DIRS again after the master merge

Master still carries the twin-merge duplicate (its dedupe hotfix is an
unmerged PR); the merge re-imported it here.

* fix(vault): reflow hard-wrapped prose in the vault asset templates

* chore(config): exclude .uv-cache and .claude from deptry's scan scope

Same repo-local tool dirs the prose scanner skips; the standalone deptry
target walks the repo root and drowned in the cache's unpacked wheels.

* fix(vault): board-review activation path for vault drafts + relocate graft

The input loop's held-artifact posture was a dead end: vault_note drafts
were unconditionally held by _is_held_ceo_source, owned by the verbless
Secretary, hidden from the panel's approval surfaces by team, and the
open-drafts cap counted them forever — the engine self-bricked after ten
notes. Vault drafts now ride the intake board-review path instead: a
tagged note becomes a PENDING Product-Owner-assigned Board draft (the
exact confirm_live_draft board shape), the board reviews it, and only
the CEO's approve_and_start makes it deliverable — never-auto-starts now
rests on the board gate, proven by tests against the real dispatchers.
The cap counts only drafts still awaiting the CEO (team==BOARD,
non-terminal), so approval and cancellation both free it.

relocate into an existing personal vault now grafts old_root/RoboCo as a
direct child (refusing loudly if RoboCo/ already exists there) and adds
only absent .obsidian/_meta files — a personal vault's config is never
clobbered. An absent destination keeps the whole-tree move.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-11 03:17:59 +02:00
8f3f4236c0 feat(tasks): sequence is the bar — strict sibling ordering at the claim chokepoint (#452)
* feat(tasks): enforce sibling sequence order at the claim chokepoint

A task with a parent and effective sequence N (COALESCE(sequence, 0))
can no longer be claimed while any sibling with a strictly lower
effective sequence is non-terminal — assignee-blind, independent of and
stricter than dependency_ids, enforced in _validate_claim_preconditions
so both claim paths (gateway verbs and the dispatcher's raw REST claim)
cross it. Ties run parallel; cancelled siblings never block; sequence 0
and parentless tasks are unaffected. Live failure this guards: a PM
delegated revision subtasks sequenced 0..3 with no dependency edges and
seq 2 started alongside seq 0 — sequence was advisory-only.

set_sequence's contract updated accordingly. New e2e smoke case drives
the refusal and the post-completion claim through the real gateway.

* chore(scripts): skip .uv-cache and .claude in the prose scanner

Repo-local tool dirs (private uv cache, agent worktrees) carry vendored
and generated markdown that tripped make reflow-check.

* fix(tasks): wave-derived delegation sequences + claim-gate hardening

Three fixes from the adversarial review of the sequence claim gate:

Delegation no longer stamps a raw per-sibling ordinal (deterministic
merge-order bookkeeping) as sequence — under the strict gate that
serialized ALL delegated work, including fully independent cross-dev and
cross-cell siblings. Sequences are now wave-derived post-wiring
(stamp_wave_sequence: 1 + max same-parent dependency sequence, 0 when
independent), so independent siblings tie and run parallel while
colliding/ordered work ascends. The cross-cell UX wiring restamps
instead of writing relative ux+1 values (a relative write could invert
a collision-derived stamp), and the dispatch merge/lane barriers gain a
created_at tiebreak for wave-tied siblings so shared-branch merge order
stays deterministic. PM-authored sequences are never rewritten.

The guard now also fires on reclaims from needs_revision (a lower-
sequence sibling delegated after the first claim was invisible), and
tasks.parent_task_id gains an index (migration 069) — the guard's
sibling probe ran as a Seq Scan on the hottest verb.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-10 22:55:00 +02:00
bba20a3917 feat(prompter): board-review → redraft loop for MegaTask batches (#411)
Batch parity with the single-draft keep-alive redraft loop. A first
board-route confirm-batch parks the intake session against the umbrella
(instead of the unconditional reap), so the existing board-completion
injection reaches the still-live chat — now with a batch-aware brief
(compose_batch_redraft_message: live root-subtask snapshots + board
notes + a one-propose_batch re-proposal instruction). The re-confirm
carries BatchConfirmRequest.task_id and routes to the new
PrompterService.update_live_batch: in-place umbrella + root-subtask
update (positional patch of live children, cancel+recreate on scope
change, create/cancel on count change, dependency edges rewired to the
fresh wave plan) gated by the same _validate_batch_scope as create.
Readers use the CANCELLED-excluding get_live_subtasks view so
multi-round redrafts survive earlier cancels.

Cold path: re-interview now handles a branchless umbrella by recovering
its multi-repo scope from live children (distinct_projects_for_batch)
and returning project_ids — fixes the live 400 behind the task-detail
redraft button on umbrellas. Panel: confirmBatch board branch keeps the
chat open, threads batchRedraftTaskIdRef (persisted) into the
re-confirm, treats a redraft re-confirm as terminal on both routes, and
surfaces the server's real validation message on confirm failure.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-10 09:38:10 +02:00
3787d15524 fix(agents): block subagent spawning at the Claude Code level (disallow Task) (#377)
The fleet-wide subagent ban was implemented as an allowlist omission, but Task
is a default-permitted Claude Code built-in — an allowlist auto-approves, it
does not restrict. Under permission_mode="dontAsk" (intake/secretary SDK) and
defaultMode="bypassPermissions" (fleet), Task ran regardless and can_use_tool
was never invoked for it, so every Claude-path agent could still spawn
subagents despite allows_subagent=False. Only the grok path blocked it.

Explicitly disallow the subagent tool at every Claude-path spawn point:
disallowed_tools=["Task"] on the intake and secretary SDK drivers, and "Task"
in the fleet settings.json base_deny (an explicit deny applies even under
bypassPermissions). This mirrors the grok path's --disallowed-tools Agent.

Pins the ban in test_cc_lockdown.py (fleet settings deny Task) and a new
test_sdk_driver_subagent_ban.py (intake + secretary options disallow Task).

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-10 02:37:12 +02:00
0450ec9e89 feat(x-engine): smart spotlight cadence — daily when there's news, quiet when there isn't (#374)
CEO verdict on the blind 3-day timer: 'It should default to 1 day and
be more like... smart.' Now: interval defaults to 1 day; the cycle
skips (with logged reasons) while a spotlight draft is still awaiting
the CEO, and stretches to 3x the interval when nothing has shipped
(CHANGELOG sections via the read clone) since the last spotlight
activity — where activity is a materialized draft's seen_at or a
completed exploration's updated_at, deliberately excluding the stale-
cycle janitor's cancels. The HoM gains an explicit skip exit
(propose_feature_spotlight skip=true + reason: completes the
exploration, no draft, no seen-slug, still counts as activity), and
its spawn prompt now carries the seen ledger WITH dates, what shipped
since the last spotlight, and recently rejected drafts with the CEO's
reasons — fresh-but-unspotlighted first. Fail-open on changelog read
errors so a signal outage never starves the engine.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-09 20:59:53 +02:00
7bc42ec114 fix(runtime): fleet-wide subagent ban — no role fans out, not one (#372)
CEO directive 2026-07-09: every allows_subagent in role_config flips to
False (was True for cell_pm, main_pm, product_owner, head_marketing,
prompter, secretary); the grok path's drifted _SUBAGENT_ALLOWED_ROLES
allowlist empties to match. The spawn manifest already consumes the
flag, so Claude-path agents lose the Agent tool and grok-path agents
get it in disallowed-tools by construction. New invariant test iterates
every role config so a single role can't quietly regain it.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-09 17:14:13 +02:00
Renn F 5e7c498d00 fix(runtime): auto-submit is unconditional; refusals brief the fallback PM
The PR-gate turn cut (#295) already auto-submitted assembled tasks, but
its refusals fell back to a PM spawn silently -- in production the PM
turns the cut was meant to remove kept happening with no visible cause
(live case: an AC-coverage refusal). The flag is gone (the fallback is
the safety net), the umbrella/branchless exclusion uses the canonical
batch predicates, and every refusal reason now rides into the spawned
PM's prompt so the fallback starts informed.
2026-07-08 22:16:50 +02:00
f48d088c08 fix(gateway): working exits for wedged agents + declare_coverage roll-up unblock (#341)
A live task burned 5+ hours because every exit was locked. unclaim now
works from verifying and needs_revision (service guard + lifecycle edge);
the circuit breaker and the i_am_done push-failure remediate name the
working chain ending in unclaim(); sync_branch(stash=true) clears the
DIRTY_WORKSPACE dead-end (pop-conflict preserves the stash); blocking a
task QA already owns now says to idle instead of listing states; the
orchestrator auto-block logs real errors and skips states where blocking
is meaningless instead of force-blocking them.

declare_coverage (cell/main PM) retroactively stamps parent-AC refs on a
child that implements them -- closing the roll-up deadlock where the
declaring child was cancelled and its re-delegated replacement completed
the work uncredited. Cancelling a ref-declaring child now warns and
surfaces the orphaned criteria.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-08 21:40:22 +02:00
47d78f50ee feat(sandbox): on-demand provisioning via request_sandbox verb (#338)
* feat(sandbox): on-demand request_sandbox verb replaces eager provisioning

Sandboxes were provisioned at every agent spawn for opted-in projects,
so every role paid the sidecar spin-up and a provisioning failure
refused the spawn. Provisioning now happens when an agent asks: the
request_sandbox do-verb (dev + QA) reaches the orchestrator through
ContentActionsDeps, ensure_sandbox provisions idempotently with an
in-memory per-agent cache (evicted at teardown and janitor sweep), and
creds return in the envelope payload including ready-to-export
ROBOCO_TEST_* values. Spawn now only injects a marker env naming the
available services plus a briefing line; sandbox failures can no longer
refuse a spawn. Teardown lifecycle unchanged.

* feat(sandbox): harden request_sandbox + Phase 3 wiring proof and docs

Hardening from adversarial review: ensure_sandbox now provisions the
project's full opted-in set on first request (a later superset can
never tear down a live sandbox mid-use), serializes per-agent behind an
asyncio lock (a client timeout-retry no longer races its own in-flight
provision), and verifies container liveness on every cache hit (a dead
sandbox evicts and re-provisions instead of serving dead creds). MCP
client budget 720->1080s for the full-set cold case. Phase 3: e2e smoke
wiring test (manifest grants + guard-chain envelopes over the real
API), sandbox-db/tools/map docs and CLAUDE.md rewritten for on-demand.

* feat(sandbox): release sandboxes when the agent's work ends

CEO directive: sidecars must not dangle once the agent is done. The six
work-ending verbs (i_am_done, unclaim, i_am_idle, pass_review,
fail_review, i_documented) now release the caller's sandbox best-effort
on their success path via release_sandbox (lock + teardown + cache
evict; a no-sandbox agent costs a dict lookup). Container removal and
the janitor remain the backstop; a re-request provisions fresh.

* test(sandbox): monkeypatch the release hook instead of method assignment

mypy method-assign rejected the direct AsyncMock assignments; the prior
static gate ran before this test file landed.

* test(sandbox): guard envelope evidence for mypy in verb tests

* chore(prompts): regenerate verb tables for request_sandbox

* chore: resolve merge with master (breadcrumbs + statement budget)

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-08 16:40:02 +02:00
0e9f21de69 fix(api): default event loop to asyncio + cancellation-safe commit — kills the CI segfault (#340)
* fix(api): default the event loop to asyncio + cancellation-safe commit

The recurring CI e2e segfault traced to uvloop: the harness's
uvicorn.run() auto-selected it while production's serve() path never
consulted Config.loop (stock asyncio, accidentally safe). Every launch
site now resolves ROBOCO_UVICORN_LOOP (default asyncio; uvloop opt-in),
and DbCommitMiddleware's commit-in-send can no longer be interrupted
mid-wire: on cancellation it gets a bounded grace to finish (committed
data survives the 504), else invalidate-and-reraise.

* feat(runtime): expected-stop breadcrumbs attribute container deaths

Two production exit-143s had no attributable source: every orchestrator
kill path now records a short reason breadcrumb, and the exit monitor
consumes it -- an expected stop logs its reason at info, a genuinely
unexpected one logs none_recorded plus docker-inspect diagnostics
(OOMKilled, timestamps) so the next mystery SIGTERM self-identifies.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-08 16:01:01 +02:00
312ec990dd fix: prod triage 2026-07-08 — MCP auth residue, gateway envelopes, verb-loop cap, A2A interjection, manual spawn UX (#334)
* fix(auth): pass agent UUID to CLI-arg MCP servers (optimal/docs/search)

The container token is HMAC-signed over the agent UUID (#314), but the
optimal/docs/search MCP servers received the slug as their CLI arg and
sent X-Agent-ID=<slug>, so every research/RAG/docs call 401ed with
signature mismatch under enforced auth. Pass the already-computed
agent_uuid in the three args lists instead.

* fix(gateway): include remediate in gateway.rejected audit details

Conventions-gate rejections carry the offending file:line listing only
in the envelope's remediate field, which the audit row dropped -- ops
logs showed just the violation count with no way to see what blocked.

* fix(gateway): return envelope on do/commit git failure

A GitError from the commit verb propagated to the generic middleware
handler, so agents got a raw error blob with no remediate/next. Catch
it and return an error envelope; 'no changes added to commit' with an
explicit files list now names the mismatch and the omit-files fallback.

* fix(agent-sdk): absolute rejection cap breaks slow-drip verb loops

The verb circuit breaker only counted rejections inside a 60s sliding
window, so an agent retrying i_am_done every 3-4 minutes looped for 30+
minutes without tripping it. Add a session-scoped cumulative per-(verb,
task) cap at 3x the windowed limit that trips regardless of pacing.

* feat(a2a): CEO chime-in interjects into the viewed conversation

Previously reply_as_ceo re-homed the message into a canonical CEO<->target
conversation with no panel surface, so a chime-in reported success but was
invisible and only opportunistically delivered. interject_as_ceo now inserts
the message into the conversation being viewed (from_agent=ceo, directed via
an @target content prefix), bumps that conversation's counters with the
unread ping keyed to the addressed participant, and both participants see it
in transcript and read_a2a.

* feat(panel): manual spawn carries task + message, surfaces refusals

The agent detail page spawned with no request body (task/message impossible),
the spawn button could double-fire (2.5ms double-POST seen live), and refusal
reasons never reached the UI: readiness refusals were generic 500s and the
already-running no-op looked like success. Detail page now uses
SpawnAgentDialog, a synchronous ref guard blocks re-entry, AgentReadinessError
maps to 409 with its reason shown, already_running is signalled and toasted,
and a task_id builds a task-aware prompt instructing the claim (task_id alone
never did), with the CEO's message appended as a note.

* test(panel): align a2a page test with the interjection footer copy

The chime-in rebuild changed the composer footer; the page-level test
asserting the old copy was outside the rebuild's scoped vitest run.

* fix(api): commit the request DB session before the response is sent

FastAPI unwinds yield-dependencies after the response bytes go out, so
get_db's post-yield commit raced the client's next request -- a verb
could return ok while its claim/status write was still uncommitted (the
e2e ok-without-effect flake family), and a failed commit was silently
lost behind an already-sent 200. DbCommitMiddleware (innermost, pure
ASGI) commits the session stashed by get_db_committed before forwarding
http.response.start; commit failure now surfaces as a 5xx. get_db is
untouched for its direct non-request callers.

* fix(db): invalidate, not rollback, the session on request cancellation

With the commit moved into the send path, the flow-verb timeout can
cancel mid-commit; rolling back then issues another command over an
asyncpg connection stranded mid-wire-protocol, and the poisoned
connection segfaults uvloop/asyncpg when a later checkout recycles it
(3/3 identical CI faulthandler dumps). On CancelledError discard the
connection via session.invalidate() -- SQLAlchemy's documented handling
for a timeout during commit -- and keep rollback for plain exceptions.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-08 10:41:02 +02:00
0bf0cd69b3 fix(release): close the 0.19.0 scan findings — sandbox mongo tag, flow-verb timeout walls, video hardening (#329)
- mongo:8-alpine → mongo:8 (tag never existed; a mongo-opted project could spawn no agents) + a Docker Hub tag-existence e2e guard for every sandbox engine
- flow-verb timeouts at both walls: shared SLOW_VERBS policy (i_am_done / submit_up / submit_root / open_pr / i_will_work_on get the 900s server budget); the MCP client now outlasts the server budget (+10s headroom, orchestrator-injected env) so agents receive the middleware's clean 504 envelope instead of dying at the old flat 30s client timeout
- cancellation safety: the quality gate kills+reaps its child on CancelledError; create_pr records the PR via a shield-with-wait-out helper so the write can neither be skipped nor race get_db's rollback
- video engine: renderer sidecar isolated on a render-only network, 2g/2cpu caps, 570s render watchdog with exit-on-hang, 512MB tar decompression cap, CEO notification on terminal render failure, reject under the approve mutex (fail-closed on Redis-down)
- dead python-jose dependency removed (drops ecdsa and its unfixable Minerva advisory PYSEC-2026-1325); panel --font-mono now a real monospace stack

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-08 03:26:12 +02:00
8f6dde9a50 feat(sandbox): pluggable per-engine registry (postgres/redis/mongo) (#324)
* feat(sandbox): pluggable per-engine registry (postgres/redis/mongo)

Replaces the hardcoded postgres+redis branches in the provisioner and the
env emitter with a registry of SandboxEngine specs (image, run args,
readiness probe, connection, ROBOCO_TEST_* env) in a pure low module
(roboco/models/sandbox.py). VALID_SANDBOX_SERVICES is derived from the
registry — single source of truth — and the provisioner + orchestrator
iterate it, so adding an engine is one class + one registry line, not
another branch. Adds a mongo:8-alpine engine (ROBOCO_TEST_MONGO_*) as the
third service alongside postgres/redis.

Also fixes the cold-pull loop that stranded v0.19.0 board agents with
empty error strings: docker run pulled inline under a 20s deadline, so a
NAS cold pull was killed, cancelled, and re-pulled from scratch forever.
_ensure_image now inspects + pulls (300s) before run; provisioning errors
log type+message so a bare TimeoutError no longer shows as "".

Panel edit-project dialog: postgres/redis toggles -> a Set<string>
multi-select driven by a SANDBOX_SERVICES catalog, so new engines appear
in the UI by adding to the catalog.

Tests: engine parity (allowlist==registry, unique slugs/images, no None
leak in env, SandboxInfo aggregates every engine), mongo provision + env
injection, plus the existing postgres/redis provision/env/spawn/janitor
suite updated to the registry shape. 821 unit / 5 skip green; ruff + mypy
(360 files) clean.

* docs(sandbox): reflect pluggable engine registry + mongo across docs

CHANGELOG (0.19.0): Added entry for the pluggable sandbox engine registry
(postgres/redis/mongo) + Fixed entry for the cold-pull loop/empty-error
strand that boarded v0.19.0 board agents.

docs/map (9 files): sandbox subsystem blurbs, SandboxProvisioner rows,
_maybe_provision_sandbox/_append_sandbox_env rows, feature-flag rows, the
migration-057 row + v0.17.0 delta, and the models.md VALID_SANDBOX_SERVICES
note — all retitled to DB/Redis/Mongo via the engine registry
(roboco/models/sandbox.py), with the one-class-one-line extension story and
the _ensure_image cold-pull fix. Production-network (roboco_data) lines left
as postgres+redis — mongo is sandbox-only, not a prod service.

docs/rag (3 files): sandbox-db.md rewritten around the registry (engine list,
generic _provision_engine, image pre-pull, ROBOCO_TEST_DB_*/REDIS_*/MONGO_*
incl. MONGO_AUTH_DB=admin, single emit_env); config-reference sandbox flag
row + subsection retitled; db-network-isolation framing broadened to
postgres/redis/mongo. preconditions-and-rejections left untouched (its hit
was an unrelated gateway see-also link).

* test(e2e): harden umbrella close terminal reads with bounded wait-for-state

The MegaTask umbrella close test flaked once on CI (ceo-approve returned
200 but the re-fetch saw awaiting_pm_review) then passed on re-run. The
production path is deterministic: complete -> main_pm_complete ->
submit_pm_review -> escalate_to_ceo -> ceo_approve -> commit, all on one
session, all awaited; the fire-and-forget completion hooks are isolated
(own session, best-effort, never touch task.status or the request session).
20 local runs could not reproduce it.

The one real surface is the read pattern: the e2e stack commits on the
uvicorn thread's loop and reads via a separate loop (run_db -> asyncio.run
with a fresh engine), so a terminal single point-read can race a
still-draining completion hook on a contended runner. Replace the two
terminal point-reads with a bounded wait_for_status poll. Strictly better
than a one-shot read: absorbs the transient, and a genuine state bug still
surfaces via the timeout branch asserting against the last-read state.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-07 13:59:53 +02:00
Renn F 49bff15c78 fix(sandbox): pre-pull images before run + capture timeout type in spawn-refusal log
`docker run postgres:16-alpine` pulled inline under a 20s deadline; on a cold
NAS the pull exceeded it, the run was killed, the pull cancelled, and every
retry re-pulled from scratch — a persistent spawn-refusal loop that
deadlocked board reviews on opted-in projects. `postgres:16-alpine` is
referenced nowhere in compose (the main service uses pgvector/pgvector:pg16),
so it was always a cold pull.

- sandbox: `_ensure_image` pulls with a 300s deadline when `image inspect`
  reports absent, before `docker run` (postgres + redis). Pull failure raises
  before any run, so it self-diagnoses instead of looping.
- orchestrator: log + raise `f"{type(e).__name__}: {e}"` — `str(TimeoutError())`
  is `""`, which made the failure mode invisible in the logs.
- tests: fake runner covers image/pull verbs; 3 new tests for skip/pull/fail.
2026-07-07 10:22:21 +02:00
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
e9d0e0bd48 feat(video): 0.19.0 video engine (Remotion) + preview auth + render persistence (#307)
* feat(video): Phase A — VideoEngine origination spine + held-source gates

New default-off engine skeleton: opens a UX/UI authoring task (source=video, assigned to a ux-dev, LOW complexity to clear the dev-needs-subtasks guard) and materializes a held CEO-approval draft (source=video_post). Excludes video_post from all three held-source skip sites; adds the video_draft marker, six config flags, and the feature-flag entries. Origination + gate behavior unit-tested.

* refactor(orchestrator): fold _dispatch_dev_work skip chain into a helper

The per-source if/continue chain grew past xenon's --max-absolute B when the video_post held source joined it. Extract _is_non_dev_dispatch_source (every held-CEO source plus the two Board exploration sources) so the dev loop's skip is one flat call. Behavior-identical.

* feat(video): Phase B — propose_video do-tool (metadata-only, team-gated)

UX/UI dev records a video's composition ref + per-platform captions onto the authoring task's video_draft marker. Team-gated at runtime via _caller_team (Role.DEVELOPER can't tell a ux-dev from a be-dev). Resolves the caller's ACTIVE task via get_active_task_for_agent, not an oldest-first scan that would clobber a second open video task. Metadata only, no render. Wired through do_server + route + schema; added to _DEV_DO.

* feat(video): Phase D — render loop + RemotionRenderer client

Orchestrator-async _video_render_loop renders a completed authoring task's merged composition to MP4 (vertical + square) via the remotion-renderer sidecar and materializes the held video_post draft. RemotionRenderer tars the read-clone's motion/ source, POSTs it, and saves the returned MP4 bytes to a TASK-scoped local path (no shared volume; a composition is reused across videos so a composition-scoped path would clobber an earlier draft). Render failures bounded-retry (read-clone catch-up window, transient sidecar) up to a cap, then terminal-fail. Client tested vs a mock transport; loop vs a mock renderer + real DB.

* feat(video): Phase C — release / spotlight / on-demand video triggers

Three entry points open a UX/UI video-authoring task via VideoEngine.open_video_task: (1) a published release drafts a companion video — best-effort in ReleaseProposalService.approve, never fails the publish; script from the CHANGELOG via the local model with a template fallback. (2) propose_feature_spotlight gains optional wants_video/video_script — best-effort, gated on video_on_spotlight, default-off leaves the spotlight flow byte-for-byte unchanged. (3) POST /video/request (CEO-only) for an on-demand brief, with clean disabled/not_opened responses. All gated on video_engine_enabled.

* fix(video): savepoint-isolate video-task inserts (F042 poisoned session)

The best-effort try/except around open_video_task (release-publish + spotlight hooks) swallowed the Python exception, but a DBAPI error at the insert flush left the shared session must-rollback — so the caller's next commit (release finalize / request boundary) threw PendingRollbackError: the release stuck 'pending' after actually publishing, or the spotlight draft + HTTP response were lost. Wrap both inserts (open_video_task, _originate_video_post) in a begin_nested savepoint (the repo's established F042 pattern) so a DB error rolls back only the insert. open_video_task returns None (every caller already handles it); _originate_video_post propagates to the render loop's handler. Regression test: an insert FK error returns None with the session left usable. Dormant while the flags were off; armed on the NAS.

* feat(video): Phase G — motion/ package + remotion-renderer sidecar + compose

In-repo Remotion v4 motion/ package (ReleaseAnnouncement composition; calculateMetadata returns 1080x1920 vertical / 1080x1080 square from inputProps.orientation) + a credential-free remotion-renderer sidecar: untar the POSTed motion/ source, bundle (LRU-cached per source sha), selectComposition + renderMedia h264, stream the MP4 bytes back — matching the RemotionRenderer client contract. docker/remotion.Dockerfile on Debian (Chrome apt deps, build-time Chrome pre-warm, ffmpeg bundled in @remotion/renderer). Wired into both compose files (roboco_default only, shm_size 1gb, /health check) + the release publish matrix. Verified via a real local render of both cuts; the Debian docker build is the CEO's to run.

* chore(video): D-hardening — video_post source_task_id + render-loop docstring

Add a source_task_id back-reference to the video_post held-draft marker (traceability from a draft to its authoring task; also makes the render loop's two-key idempotency check wireable later). Fix the render-loop test's stale docstring ('never retried' -> bounded-retry). Both from the Phase D critic's non-blocking follow-ups.

* feat(video): Phase E1 — VideoPostService + heartbeat mutex (approve->post)

CEO-approve->post service: heartbeat-renewed Redis mutex (fail-closed, grace=ttl-2*heartbeat), re-read-in-lock double-post guard, per-platform durable commits (asyncio.shield-ed, settle-before-rollback on lock-loss), all writes inside the lock (captions validated pre-lock, applied in-lock — no stale whole-column clobber), idempotent, per-platform retry-skip. Poster interfaces (X/TikTok, mocked here). Reject + list-held-drafts. Survived 3 adversarial rounds; residual = a crash in the poster->commit window (CEO-gated low-freq, documented).

* fix(video): G-hardening — renderer leaks + Share Tech Mono brand font

Sidecar: give bundle() an explicit outDir tracked + deleted on LRU eviction (was leaking ~19MB remotion-webpack-bundle-* per source); res.on('close') cleanup so an aborted/retried download no longer leaks its remotion-out-* MP4 dir. Fonts: vendor Share Tech Mono (roboco-website brand font) as the display face (self-hosted woff2, 400-weight, headline fontWeight 700->400 to avoid faux-bold) + self-hosted Inter body — no gstatic fetch at render time (lsof-verified). Extras: composition_id whitelist (400) + Multer error middleware (400/413).

* feat(video): Phase E2 — X v2 + TikTok posters, tiktok_credentials, routes

LiveXVideoPoster (X v2 chunked media upload: init/append/finalize/STATUS-poll -> tweet w/ media_ids, OAuth1 signer reused). LiveTikTokPoster (OAuth2 inbox: init -> chunked PUT with asymmetric final chunk -> status-fetch; 401 -> refresh_token grant, rotated token persisted). tiktok_credentials Fernet singleton + migration 062 (single head). Routes: CEO approve/reject + list held drafts + write-only tiktok creds, wiring real posters into VideoPostService. Residual: a lock-loss right after a token-refresh flush can discard the rotated token (same rare CEO-gated class as the documented post->commit window).

* feat(video): Phase F — panel video-post queue + TikTok creds card + flags

video-post-queue.tsx: <video> MP4 preview with 9:16/1:1 cut switch, per-platform editable captions (280/2200 counters, over-limit disables approve), approve/reject, Request-a-video dialog. tiktok-credentials-card.tsx (4 write-only OAuth2 fields). feature-flags-card inlines TikTokCredentialsForm under video_engine_enabled. Mounted in command-center. tsc/eslint clean, 273 panel tests green. NOTE: needs the GET /video/posts/{id}/media route + mp4_paths on VideoPostResponse (folded into H) for the preview source.

* feat(video): Phase H — media route + e2e smoke + NAS arming + docs

GET /video/posts/{id}/media?cut= (CEO-gated FileResponse of the rendered MP4; closes the panel preview gap) + mp4_paths on VideoPostResponse. e2e smoke tests/e2e_smoke/test_video_pipeline.py (full flow, sidecar+X/TikTok mocked; asserts dispatcher skips, render-loop materialize, propose_video team-gate, approve idempotency). NAS arming: docker-compose.yml/.yaml ROBOCO_VIDEO_ENGINE_ENABLED/ON_RELEASE/ON_SPOTLIGHT default-on (.yaml resynced to .yml); registry stays off. CLAUDE.md video-engine section + CHANGELOG. Fixed 2 pre-existing route-test pollution leaks. Full suite 11763 passed.

* fix(video): auth-carrying preview, media route confinement, VideoPost type drift

Three fixes along the video preview path:

1. panel video preview auth: the <video> element was pointed straight at
   GET /video/posts/{id}/media, but a native <video src> GET carries none
   of axios's X-Agent-ID/X-Agent-Role headers — so in the default
   header-trust deployment the request 401s. Fetch the cut via
   videoApi.getMediaBlob (axios, responseType: blob) and drive <video>
   off a URL.createObjectURL result instead. The object URL is revoked
   on cut-change (the previous cut's URL) and on unmount, so neither
   cut switches nor row teardown leak blob URLs.

2. backend media route confinement: GET /video/posts/{id}/media now
   resolves mp4_path and refuses it with 404 when it falls outside
   settings.video_output_dir. Defense-in-depth against any future
   writer of mp4_paths serving files from arbitrary disk locations.

3. panel VideoPost type/comment drift: added mp4_paths to the
   VideoPost interface (the committed VideoPostResponse already
   carries it), and corrected the stale comment on videoMediaUrl
   that claimed no route served the rendered bytes — the route has
   existed since the media endpoint landed; the comment now describes
   why getMediaBlob exists instead of a direct <video src>.

* Persist rendered videos to data in physical storage.

* ++

* docs(video): 0.18.0 CHANGELOG entry + RAG + map reference for video engine

- Move the video engine bullet from [Unreleased] into [0.18.0] and note
  the ROBOCO_VIDEO_OUTPUT_DIR bind-mount persistence.
- Add docs/rag/architecture/video-engine.md (mirrors x-engine.md shape:
  enable/disable, three triggers, render loop + sidecar, CEO gate, media
  route confinement, credentials).
- Reference the video render loop in docs/map/orchestrator.md's engine list.

* chore(video): re-bump to 0.19.0 + sync registry compose defaults

Version was wrongly bumped to 0.18.0; 0.18.0 is an already-released
section. Restore its 2026-07-04 date and move the video-engine CHANGELOG
bullet into a new [0.19.0] - 2026-07-05 section above it. Bump
pyproject.toml, roboco/__init__.py, roboco/config.py (app_version),
panel/package.json, and the motion/README inputProps example to 0.19.0.

docker-compose.registry.yml: add ROBOCO_VIDEO_ENGINE_ENABLED /
_VIDEO_ON_RELEASE / _VIDEO_ON_SPOTLIGHT defaulted false (NAS arms them
true), and comment out the video-renders bind mount with a short note
so the public registry image ships video off by default. Structural
sync with docker-compose.yml maintained.

* fix(video): rate-limit /render + reflow motion/README

CodeQL flagged js/missing-rate-limiting on the renderer /render route.
The sidecar is container-network-only with one trusted caller (the
orchestrator, which renders cuts serially), so this limiter is a
retry-storm ceiling (30/min, well above legit render rate), not the
primary control. Also reflows motion/README.md hard-wrapped prose that
failed the markdown quality gate.

* fix(build): finish pnpm 11 migration + regen verb tables

The panel Docker image build failed on `pnpm install --frozen-lockfile`:
node:22-alpine's corepack resolved to its bundled pnpm 11, but
panel/package.json pinned packageManager to pnpm@10.25.0, and pnpm 11
refuses to run against that pin. The Dockerfiles were already written for
pnpm 11 (comments, CI=true, strictDepBuilds); the package.json pin was the
stale outlier. Finish the migration instead of working around it:

- panel/package.json: packageManager pnpm@10.25.0 -> pnpm@11.10.0; drop the
  `pnpm` field (pnpm 11 ignores it — build approval lives in
  panel/pnpm-workspace.yaml's allowBuilds). Lockfile unchanged (pnpm 11
  accepts it as-is); frozen-lockfile verified.
- remotion-renderer/package.json: pin packageManager pnpm@11.10.0 for
  determinism (was relying on corepack's implicit default); engines.node
  >=22.13 (pnpm 11 requirement).
- docker/panel.Dockerfile + docker/remotion.Dockerfile: `corepack prepare
  pnpm@11.10.0 --activate` so the build uses the pinned version explicitly
  instead of trusting corepack's bundled default (which a future
  node:22-alpine could change).
- .github/workflows/panel-ci.yml: Node 20 -> 22 (pnpm 11 requires
  Node >=22.13; Node 20 fails the engines check).

Also regenerate agents/prompts/_generated/{developer,head_marketing,verbs}.md
— the video engine added propose_video and extended propose_feature_spotlight
(wants_video, video_script) but the verb tables weren't refreshed, failing
the foundation-check quality gate.

* chore(build): approve esbuild build script in remotion pnpm-workspace.yaml

pnpm 11 generated this file with a placeholder ('set this to true or false')
during install; resolve it to true so local dev of the renderer doesn't
re-prompt. esbuild's postinstall only verifies the prebuilt platform binary
(@esbuild/<platform> is installed as an optional dep), so approving it is
safe and silences the ERR_PNPM_IGNORED_BUILDS warning.

* fix(build): copy pnpm-workspace.yaml into panel + remotion images

pnpm 11 hard-errors with [ERR_PNPM_IGNORED_BUILDS] (exit 1) when a
dependency ships a postinstall script that isn't approved in
allowBuilds. Both Dockerfiles copied only package.json + pnpm-lock.yaml,
so the build-approval map in pnpm-workspace.yaml never made it into the
image — the remotion image build died on esbuild@0.28.1's postinstall.

Copy pnpm-workspace.yaml alongside the manifests in both images. In
panel, this also drops the --config.strictDepBuilds=false workaround:
with sharp and unrs-resolver now approved, their postinstalls run and
install the platform-specific binaries (previously skipped, leaving
sharp without its @img/sharp-* binary at runtime).

Verified locally: remotion + panel `pnpm install --frozen-lockfile`
exit 0 with the workspace file present; both exit 1 without it.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-05 13:37:17 +02:00
Renn F da17c49f2d feat(marketing): HoM feature-spotlight X drafts + brand-voice charter (v0.18.0 B)
The Head of Marketing now markets features, not just releases: a default-off
x_feature_spotlight loop periodically spawns the HoM to investigate what shipped
(CHANGELOG, feature flags, docs/map, KB) and draft ONE held marketing post via
propose_feature_spotlight, reviewed in the X post queue.

- New x_feature source (distinct from x_post, fixing panel mislabeling) + a
  panel Feature-spotlight branch.
- brand_voice column on company_goals (migration 061, single head) as the
  CEO-editable voice source, surfaced in Settings and injected into the HoM
  briefing; a VOICE GUIDE baseline in head-marketing.md.
- propose_feature_spotlight verb (HoM-only), mirroring propose_roadmap.

Gated by x_feature_spotlight_enabled (default off; flag-off dormancy proven).
Also fixed two real bugs found mid-build: company_goals API schemas dropped
brand_voice on GET/PUT; the live charter UI is goals-tab.tsx, not the unmounted
company-goals-card.tsx. Full suite green (2935); migration single-head verified.
2026-07-04 07:34:40 +02:00
Renn F 7716830322 feat(fleet): opus-fable adoption — doctrine + discipline hooks (v0.18.0 A)
Fleet behaves more like Fable 5 on existing model tiers, behind
ROBOCO_FABLE_MODE_ENABLED (config default off; armed :-true on the NAS compose,
absent from the registry compose).

- Doctrine: vendored agents/prompts/doctrine/fable.md composed into every
  agent's system prompt via fable_doctrine_layer() after base.md.
- Hooks (Claude Code): 4 non-overlapping hooks (stop-gate/bash-discipline/
  honesty-nudge/precompact) appended per-agent via _fable_hook_groups(). The
  make-quality + lint-suppression duplicates are deliberately NOT added (already
  gate-enforced); session-start skipped.
- Hooks (grok): conservative V1 — only the non-denying honesty-nudge, since a
  grok hook deny cancels the whole run.
- Flag on the feature-flags card; hook scripts shipped into the agent image.

Flag-off spawn path proven byte-identical (worktree diff, sha256 match); full
suite green (2074 unit + e2e-smoke + hook harness), mypy/xenon/ruff clean.
Fixed a real stdin bug in the vendored stop-gate hook (heredoc + pipe both
claimed stdin). Distilled from rennf93/opus-fable-playbook (MIT).
2026-07-04 06:44:40 +02:00
3ccc723cd4 v0.17.0 — Wave 3: sandbox DB, DB isolation, mobile UI, cloud auth, X account, roadmap engine (#303)
* feat(sandbox): throwaway per-agent Postgres/Redis sandbox containers

Orchestrator-provisioned sibling containers per agent spawn
(SandboxProvisioner, roboco/runtime/sandbox.py). Per-project opt-in via
projects.sandbox_services (migration 057); master switch
ROBOCO_SANDBOX_DB_ENABLED, default-off, armed in the NAS compose only.

When active, ROBOCO_TEST_DB_* / ROBOCO_TEST_REDIS_* point at the sandbox
and the prod-creds gate-env injection is suppressed (sandbox replaces,
never coexists). Sandbox lifetime tracks the agent container: teardown at
every removal path, orphan janitor at startup + each reaper tick with a
grace window for mid-flight spawns. The pre-spawn stale-clear spares the
just-provisioned sandbox; provision pre-clears stale same-named
containers from a crash-missed teardown.

Panel: per-project sandbox-service switches in the edit dialog + feature
flag card entry.

* docs: CLAUDE.md entry for the sandboxed dev DB/Redis subsystem

* feat(security): isolate prod Postgres/Redis from agent containers (roboco_data network)

Second user-defined bridge roboco_data carries postgres+redis only; the
orchestrator is multi-homed (default + data). Spawned agents and their
sandbox sidecars stay on roboco_default and can no longer resolve or
reach roboco-postgres:5432 / roboco-redis:6379 (redis has no auth —
membership is its only containment). Normal bridge, so host-published
ports (15432/16379) keep working. Applied to both build composes and
the registry compose; docker-compose.yml re-synced byte-identical with
docker-compose.yaml (it had drifted by the sandbox flag block).

ROBOCO_DB_NETWORK_ISOLATED (config default false, armed alongside the
topology) suppresses the legacy _append_gate_env prod-creds injection:
under isolation those creds dead-end, and unreachable creds are worse
than none. DB-needing projects opt into sandbox_services instead. The
flag is deliberately not a panel feature flag - it must travel with the
compose networks: stanzas.

Preserved by construction: agent<->agent A2A and orchestrator->agent SDK
polls on :9000, MCP->orchestrator on :8000, ollama reachability, docker
exec/inspect (daemon socket), host port publishing.

* feat(panel): full mobile responsiveness pass

Shared primitives: useIsMobile (useSyncExternalStore, hydration-safe,
memoized matchMedia subscribe), ResponsiveTable table->card switch below
md (single subtree mounted, no duplicated interactive rows), scrollable
snap TabsList in the base primitive (justify-center-safe so the first
tab stays reachable on overflow), persistent md:hidden bottom tab bar
(Overview/Tasks/Kanban/Chat, safe-area padded).

Applied: card lists for tasks/projects/products/work-sessions/sessions
+ the three raw metrics tables; CEO approval queue / release proposal /
playbook review action rows stack on narrow; command-center reorders
approvals above the fold on mobile; task-header metadata wraps;
Communications + A2A become URL-driven single-pane drill-downs below lg
(fixes the unconstrained-height ScrollArea bug) with dvh heights;
recharts label density/radius adapts via useIsMobile; git diff viewer
gets mobile font + wrap toggle; vh->dvh sweep; chat composers get
safe-area-inset padding; dashboard main p-4 md:p-6 + pb-20 for the bar.

Verified at 375px on the built app: bottom bar, drawer, approval-first
overview, swipeable kanban tab strip. All gates green (eslint, tsc,
vitest 249, next build 24/24 routes).

* feat(auth): cloud auth via FastAPI Users (default-off, single-user cookie session)

ROBOCO_CLOUD_AUTH_ENABLED (default off) lets the panel/API be exposed
beyond localhost without changing the CEO's local no-login flow while
off — get_agent_context and the WS gate are byte-for-byte unchanged in
off-mode. On: header-trust dies for humans — any agent-role claim (ceo
or a privileged PM/board role) with no valid HMAC token or session
cookie is 401, closing the header-spoof hole on the host-published
:8000 port for every role. The agent-fleet HMAC path and the system
self-PATCH keep working unmodified in both modes.

Single seeded CEO user (migration 058 users table, UserTable), no
registration router — idempotent env-driven upsert at startup by PK.
Cookie transport (httponly/secure/samesite=lax) + a JWTStrategy bound
to a fingerprint of the current password hash (rotating the password
invalidates every prior session). Sliding 30-day session: every
authenticated request re-mints the cookie, so an active session never
expires — no unexpected logouts.

Panel: (auth)/login page + proxy.ts (Next 16 rename of middleware; probes
/auth/status over the docker-internal URL, fails open to off) gate the
dashboard; client.ts gets withCredentials + 401->/login. nginx unchanged.

Review hardening: broadened the on-mode rejection from ceo-only to every
non-CEO role without a valid token (was only closed when
ROBOCO_AGENT_AUTH_REQUIRED was also armed); Next-16 proxy.ts rename to
clear the middleware deprecation warning.

* feat(x): RoboCo X account engine — HoM drafts, per-post CEO approval (default-off)

ROBOCO_X_ENGINE_ENABLED (default off, inert without creds). Mirrors the
ReleaseManagerEngine held-artifact shape: XEngine drafts a post when a
release publishes (via a draft_release_post seam on ReleaseProposalService
.approve) and drafts replies to meaningful mentions (dedicated poll loop,
x_seen_mentions dedup ledger, per-cycle/open caps). Drafting is
local-model-only, clamped to 280 chars. Nothing auto-posts — every tweet
is a held task (source x_post/x_reply, confirmed_by_human=False,
Secretary-owned, dispatcher-skipped) the CEO edits/approves/rejects in a
panel queue.

The four OAuth 1.0a secrets live Fernet-encrypted in a singleton
x_credentials row (migration 059, all-or-nothing, API returns only
has_credentials); decryption is server-side, agents never hold creds or
egress. Hand-rolled OAuth 1.0a HMAC-SHA1 signer, no new dependency;
NullXClient makes the unconfigured path a graceful no-op.

XPostService.approve (CEO-only) is the sole caller of post_tweet.

Review hardening: closed a double-post race — the approve path now
re-reads committed task state inside the Redis lock and commits COMPLETED
before releasing, so a concurrent approve that acquires the lock after the
winner released can't re-post (SET-NX is non-waiting, and the route-level
commit landed after the lock dropped). Added a regression test.

* feat(roadmap): board roadmap engine — PO proposes themed cycles, CEO approves per-item (default-off)

ROBOCO_ROADMAP_ENGINE_ENABLED (default off). Weekly, RoadmapEngine opens
ONE held exploration task (source=board_roadmap, confirmed_by_human=False,
Product-Owner-assigned), deduped to one open cycle. A dedicated one-shot
_dispatch_roadmap_exploration spawns the PO solo (not the two-reviewer
board path, which would also spawn HoM + fire Approve-&-Start). The PO
explores read-only (git/KB/metrics/releases/charter/web) and makes one
propose_roadmap call (PO-only content verb) authoring a themed cycle —
goal + 3-7 item drafts — persisted as a roadmap_cycle marker (no table,
no migration; head stays 059).

The CEO acts per-item in the panel roadmap queue: approve materializes a
BACKLOG task (source=roadmap, no assignee — never auto-starts), reject
records a reason; all-items-terminal completes the exploration task.
RoadmapService is idempotent per item. Dispatchers skip board_roadmap.

Includes a real SQLAlchemy dirty-check fix (deep-copy the JSON marker
before mutating, or the in-place edit + reassign compares equal to its
own baseline and the UPDATE is skipped).

Review hardening: create_task_from_draft now honors a draft-declared
source only from a {prompter, roadmap} whitelist — drafts are
LLM-authored, so an unbounded source could impersonate a privileged
origin (release_manager would even wedge that engine's dedup).

* chore(release): 0.17.0

Wave 3 — six default-off subsystems: sandboxed dev DB/Redis, prod
Postgres/Redis network isolation, full mobile UI pass, cloud auth
(FastAPI Users), the RoboCo X account engine, and the board roadmap
engine. Plus the waves 1+2 work already on master since 0.16.0.

Version bumped across the canonical set (config.py, __init__.py,
pyproject.toml, panel/package.json, uv.lock); CHANGELOG [Unreleased]
cut to [0.17.0]; docs/map delta added.

Compose: every optional feature armed :-true in the NAS composes, OFF
in the user-facing registry compose. Two opt-in exceptions default off
(CLOUD_AUTH — needs email/password/secret + TLS, would otherwise fail
startup; ROUTING_STRICT — fail-closed spawning). DB_NETWORK_ISOLATED
stays on in both (coupled to the roboco_data topology).

* chore(compose): arm cloud_auth + routing_strict ON in the NAS composes

Every feature defaults ON in the NAS composes per policy — these two
were wrongly left off. Both keep the ${VAR:-true} form so the operator
controls the real runtime via .env: cloud auth needs
ROBOCO_CLOUD_AUTH_EMAIL/_PASSWORD/_SECRET + TLS set there before a boot
(else startup fails loud), and routing_strict is fail-closed. Registry
compose keeps both off.

* fix(ci): reflow board.md prose (quality gate) + document v0.17.0 env creds

The roadmap section added hard-wrapped prose that failed the markdown
prose gate; reflowed (token-invariant). Also brought .env.example
current: cloud auth (now armed — needs SECRET or startup fails), routing
strict, the X engine (panel-entered OAuth), and web research.

* fix(ci): reduce cyclomatic complexity of five wave-3 blocks (xenon gate)

The wave-3 subagents introduced C-rank functions the CI xenon gate
rejects (my per-item reviews ran ruff/mypy/pytest but not xenon):
- sandbox.janitor_sweep -> extract _list_labeled_sandboxes /
  _list_live_agent_containers / _prune_grace
- x_client.fetch_mentions -> extract _parse_mention_items
- x_engine.run_cycle -> extract _process_mentions
- orchestrator._dispatch_pm_work -> extract the source-skip into a
  MODULE-level _is_held_ceo_source (module, not method, so the
  wholesale-mocked dispatcher unit tests exercise the real logic)
- auth/seed.ensure_seed_user -> extract _apply_seed_updates (module avg -> A)

Behavior-preserving; full suite green (11902), xenon clean.

* fix(ci): declare pyjwt + fastapi-users-db-sqlalchemy as direct deps (deptry)

The cloud-auth code imports jwt and fastapi_users_db_sqlalchemy directly
but they were only transitive deps (via fastapi-users), which deptry
(quality gate, DEP003) rejects. Declared explicitly; deptry roboco/ clean.
Missed originally because local make quality stopped at earlier gates
before reaching deptry.

* feat(x): gate mention replies behind ROBOCO_X_REPLIES_ENABLED (default off)

Per CEO decision: the X engine should only post about releases by
default. Reading mentions needs a paid X API tier, so the mention-reply
half is now a deliberate opt-in on top of release posting.

New default-off flag x_replies_enabled gates the mentions poll loop
(_x_mentions_poll_loop) and XEngine.run_cycle; release-post drafting
(the release-proposal approve hook) is unaffected and still runs when
x_engine_enabled + credentials are set. Added to FEATURE_FLAGS + the
panel card. Tests: release posting works with replies off; run_cycle +
the poll loop are no-ops with replies off.

* fix: 401 only redirects to /login when cloud auth is on; panel-token strips .env quotes

Two bugs that together dead-ended login in secure mode:
- client.ts redirected to /login on ANY 401, so a mismatched panel
  token (header-trust/secure mode, cloud auth off) bounced the user to a
  login page whose backend route isn't mounted -> 404. Now it probes
  /auth/status (bare fetch, no interceptor re-entry) and only redirects
  when cloud_auth_enabled.
- make panel-token read the .env secret with grep|cut without stripping
  surrounding quotes, so a quoted ROBOCO_AGENT_AUTH_SECRET produced a
  token signed with the quotes included — which never verifies against
  the orchestrator (docker-compose/pydantic unquote the secret). Now
  strips surrounding single/double quotes.

* fix: git-log 500 on '|' in commit message; X queue shows an empty state

- GET /api/git/log 500'd (ValueError: Invalid isoformat) when a commit
  SUBJECT contained a '|' (e.g. the 'curl|sh' lockdown commit): the
  fixed '|' field delimiter let the subject's pipe shift the split so
  author+date collapsed into one field. Switched to \x1f (Unit
  Separator), which can't appear in commit content. Regression test with
  a piped subject.
- The X Post Queue returned null when empty, so there was no visible
  place for the X drafts. It now renders a discoverable empty state
  pointing at Settings -> X credentials.

* docs: bring docs/rag + docs/map current for v0.17.0 (waves 1-3)

Agent-facing RAG corpus and codebase map updated for every feature in
the 0.17.0 span, code-verified:
- wave 3: sandbox DB, DB network isolation, cloud auth, X engine
  (+ x_replies_enabled sub-flag), board roadmap engine — new RAG
  architecture pages + role/tool/config-reference updates; new symbols,
  migrations 057-059, panel surfaces, and the get_agent_context
  dual-path across the map slices.
- waves 1-2: A2A live view + switchboard, prompter memory
  (search_past_tasks), Secretary edit access + PM-lighter scope, the
  PR-gate auto-submit turn cut (ROBOCO_PR_GATE_AUTO_SUBMIT_ENABLED).
- correctness fix: api-routes-schemas.md no longer claims the A2A admin
  routes are reachable by any authenticated agent — they carry a
  _require_ceo gate (wave 2c).

docs/internal, _front.md deltas, and the frozen _complete_map.md
snapshot untouched.

* fix(rag): atomic upsert for indexed-doc tracking (kills e2e segfault)

The indexed-document tracking write used check-then-insert in two paths
(IndexedDocumentRepository.upsert_batch and the file-source
_upsert_doc_record). Under concurrent indexing both callers saw no row
and both inserted, so the second violated uq_indexed_doc_source and
poisoned its transaction — surfacing in CI as the intermittent
_checkin_failed SIGSEGV on the failed connection's pool checkin.

Both paths now use INSERT ... ON CONFLICT DO UPDATE against the
constraint: coalesce keeps an existing title/preview when the new value
is empty (matching the old guards) and metadata is jsonb-merged. The
batch dedupes within itself first (ON CONFLICT can't touch a row twice
in one statement). expire_all after the Core upsert keeps same-session
ORM reads consistent with the merged DB row.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-03 19:24:00 +02:00
12745352aa CC capability lockdown: shared credential mount + curl|sh RCE closed (+5 hardenings spec'd) (#302)
* fix(security): lock down shared Claude Code credential mount + curl|sh RCE

Audit of Claude Code capabilities reachable inside a spawned agent
container turned up two live gaps against the shared harness state:

- Every agent container bind-mounts the host's ~/.claude (OAuth store) and
  ~/.claude.json read-write (_build_mount_args) — the shared subscription
  auth used by the whole fleet. Nothing denied the native Read tool or the
  bash-guard hook from reading .credentials.json / .claude.json, so any
  role could exfiltrate the harness's own Claude Code auth. Deny both at
  the settings.json layer (absolute // form, per the #167 gotcha) and in
  the bash-guard hook's credential-exfil checks (cat/grep/source/base64/
  interpreter one-liners), mirroring the existing .netrc/.git-credentials
  treatment.
- The bash-guard hook only blocked curl/wget to github.com or internal
  hosts; `curl <any other host>/install.sh | bash` (or `bash <(curl ...)`,
  `eval "$(curl ...)"`) executed untrusted remote code unchecked. New
  checks deny piping a fetch into an actual shell (sh/bash/zsh/dash/ksh)
  while leaving non-executing consumers (tar, jq, -o file) untouched.

Also add --disable-slash-commands to every container agent spawn: skills
resolve independently of the --tools allowlist, so a contaminated shared
~/.claude could otherwise leak host skills/plugins into an agent session.
No RoboCo role's workflow uses a Claude Code skill.

64 -> 78 shell bash-guard cases, 54 -> 71 pytest bash-guard cases, plus a
new 5-case settings/CLI test module. ruff/mypy/xenon B clean.

* docs: changelog for the CC capability lockdown

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-03 03:05:37 +02:00
876e19b389 A2A switchboard (pair cards), Secretary/PM task access + closed over-permission hole, MegaTask conventions fix (#298)
* feat(tasks): Secretary full task access; PM lighter editing — and a closed over-permission hole

Secretary: the CEO-gated edit directive covers the full content surface
(title/description/AC/priority/team/complexity/nature + claim-aware
reassignment through the real reassign paths, enum coercion, slug or
UUID assignees), and read_task returns full detail (notes, plan,
bounded progress, PR refs). The submit_directive tool docs never
mentioned edit at all — fixed, it was undiscoverable.

PMs: scouted the PATCH route and found has_higher_perms gave PM
identities UNRESTRICTED admin (ASSIGN is not team-scoped) — wider than
'not that much'. Now: cell PMs hard-403 outside their team, and both PM
roles are capped to the content allowlist (title/description/AC/
priority) with zero status changes via this surface. CEO/Board/Auditor
keep full admin. Built subagent-driven (Sonnet 5), reviewed.

* feat(a2a): the switchboard — org-chart pair cards with live activity

70 permission-matrix-derived pair cards (cells/pm-chain/board/cross),
lighting on either direction's a2a.message frames with a 45s fade —
A2A only, never verbs, per CEO ruling. Click-through reuses the v1
transcript + chime-in drawer; v1 list stays as the mobile fallback.
One CEO-gated /a2a/chat/admin/pairs route joins the static matrix
against conversations in a single bulk query. Built subagent-driven
(Sonnet 5), reviewed; pre-existing agent-utils slug-map gap flagged.

* fix(runtime): conventions ambient covers the MegaTask project_ids scope

_resolve_intake_ambient forwarded project_ids only to the history-digest
resolver — a MegaTask intake got no architectural-conventions block even
with the flag on. The conventions resolver now takes project_ids first
(mirroring the history resolver), both share one order-preserving
_projects_by_ids helper, and a regression test pins the threading to
both sub-resolvers. Built subagent-driven (Sonnet 5), reviewed.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-03 01:58:38 +02:00
da563487b8 Wave 2 features: A2A live view (CEO chime-in + reply budget) and prompter memory (#297)
* feat(a2a): live view — watch fleet conversations, CEO chime-in, reply budget

A2A_MESSAGE_SENT published from A2AService.send (excerpt-capped) and
fanned through the existing /ws/system bridge; CEO-only admin REST for
conversations/messages + a reply route on the publish-bearing send path;
panel /a2a page with live transcript and a composer gated on task-linked
conversations. The matrix gains its one asymmetric rule: CEO may message
anyone, nobody may target the CEO — and agent replies inside a
CEO-opened conversation are hard-budgeted to one per CEO message
(per conversation, per agent), rejected with wait-don't-retry guidance.
Built subagent-driven (Sonnet 5), reviewed; v1 seams documented in the
map delta.

* feat(prompter): intake remembers the task history

Intake spawns now carry a per-project chronological digest of recent
tasks (capped: 15 lines/project, 4000 chars total — ~300-1000 tokens)
merged into the ambient layer, and the interviewer gets a bounded
search_past_tasks tool (one shared implementation behind the grok MCP
tool and the Claude SDK in-process tool) to check precedent
mid-conversation. Informational memory only — the sequencing analyzer
keeps ownership of ordering. Built subagent-driven (Sonnet 5), reviewed;
pre-existing conventions-ambient MegaTask-scope gap flagged, untouched.

---------

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