* feat(motion): demo-register visual design bar, vendored craft references, catalog vocabulary index
* fix(motion): correct design-bar composition claims after adversarial fact-check
* docs(map,rag): design-bar, vendored references, and catalog index on the video-engine surfaces
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(panel): Workstation page — Products and Projects as tabs behind one sidebar entry
* docs(map): workstation page, extracted views, and the local-filter scroll trade-off
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(panel): session-start, 7d overview spend, and 30d business spend charts
* feat(panel): surface work sessions as a Git page tab (route was orphaned)
* feat(git): reap spent task branches and render previews at lifecycle chokepoints; guarded stale-branch sweep
* feat(panel): stale-branch cleanup button on the Git page
* fix(git,panel): cursor-resumable sweep, force-delete spent refs, local filter state
* docs(map,rag): branch/preview reaping, cleanup sweep, git-tab work sessions, wave-2 charts
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
A draft targeting a platform with no credentials could never complete:
the X leg posted and committed its id, the TikTok leg failed as
'transient', and the card sat pending in the CEO queue with retry
semantics that structurally cannot succeed. Unconfigured platforms are
now an explicit skip: the draft COMPLETES when every configured
platform has posted (detail names what was skipped), an approve whose
targets are all unconfigured refuses loudly instead of silently
completing, and genuine post failures keep their partial/retry
semantics. Re-approving a parked card clears it without re-posting
(the already-posted guard is covered by a dedicated test).
Verified: 32/32 test_video_post_service (3 new: skip-and-complete,
all-unconfigured refusal, re-approve recovery), ruff/mypy/xenon clean.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The 0.7.36 exact-pin landed via an npm install, which wrote a stray
package-lock.json and left pnpm-lock.yaml stale — the image build's
pnpm install --frozen-lockfile then correctly refused the spec
mismatch, blocking deploys. Lockfile regenerated with the Dockerfile's
own pinned pnpm@11.10.0 (resolves @hyperframes/producer@0.7.36 exactly,
frozen-lockfile install and the sidecar's node --test 15/15 verified
locally); the npm lockfile is removed — this package is pnpm-managed.
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>
The vendor publishes seek-safe authoring doctrine as installable agent
skills (hyperframes-keyframes / hyperframes-creative / hyperframes-core);
its primary seek-safe primitive is GSAP tweens on window.__timelines —
the kit's CSS register is a house pattern and the clip-window rule is
its empirically-derived companion.
The release cuts read as static screen recordings: one 6s cursor glide
that then vanished, a locked-off camera, metronomic flat card fades. And
two renderer defects were silently wrecking every cut:
- @hyperframes/producer floated (^0.7.36, no lockfile — image builds get
whatever is latest): 0.7.60 fails EVERY render with "Cannot access 'rt'
before initialization". Pinned 0.7.36 exact and committed
package-lock.json so sidecar builds are reproducible.
- the producer's per-clip visibility scheduler runs on a clock that lags
~50% behind the encoded timeline on a 40s cut — the final frame showed
the authored ~18s state, so the tail scenes (bell card, toast, outro)
were silently missing from the MP4. This, not authoring, is why
rendered cuts kept losing their late scenes. Fix: clip windows are for
structural layers only (hero, panel frame, status); every beat rides
base-hidden styles + delayed CSS animations, which run on the true
clock. Verified frame-by-frame: all four cards, stats, toast, and
outro now land exactly on schedule in both cuts.
Craft, made reusable in the kit instead of per-composition heroics:
- kit.js choreographCursor: data-waypoints="t x y [click]; ..." generates
a multi-leg eased path with fade in/out, an idle-hand sway, and click
rings + glyph press dips — the cursor behaves like a hand, never pops
in, freezes, or blinks out
- kit.js choreographCamera: data-shots="t x y scale; ..." — push-ins
toward each beat's focal point, pull-backs for reveals, settle to end
- release-0.25.0 both cuts re-choreographed: the cursor is the CEO's
hand (settle on the intake while it types, ONE submit click, witness
each card completing, acknowledge the toast, exit off-frame); the
camera lives on every beat; springy card entrances replace flat fades
- motion/README.md gains 'Cinematography & rhythm' (shot-list-first, no
locked-off camera, verify motion with frame PAIRS) + the clip-window
rule; kit/README.md documents both engines; the dev video prompt block
carries the craft bar
Verified end to end through the real sidecar: both cuts render green,
32-frame strips read visually — cursor travels and clicks on schedule,
camera moves, every scene present. motion pnpm test 15/15.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(gateway): delegation detail-fidelity — details survive hand-off, both directions
Details thinned out at every delegation hop: a PM child task mapped to no
parent criterion was legal (coverage only surfaced at submit_up, after the
whole wave ran — a 12-subtask docs tree grew through 8 review rounds that
way, one child titled 'docs page and route wrapper' shipping only the
page), and QA could pass work on a gestalt read (a 4-scene video brief
shipped 3 scenes past every gate because the features existed only in
prose). Three chokepoint gates:
- delegate (down): every child must declare covers_parent_criteria
resolving against the parent's real acceptance criteria — no mapping or
an unresolvable ref rejects naming every offending child and the valid
criteria; the success envelope carries parent_ac_coverage
{covered, uncovered} so a wave-planning PM sees remaining gaps in the
same turn. Full coverage stays enforced at submit_up (waves stay legal).
- pass_review (up): mandatory criteria_verified — one {criterion,
evidence} entry per task AC, matched by the findings ledger's
id-or-exact-text matcher, evidence soup-checked and capped; rejects
naming the unverified criteria; entries render deterministically into
qa_notes as '[AC] <criterion> — verified: <evidence>' lines. The old
count-only ac_verdicts gate is superseded (arg kept for back-compat).
- video briefs (structured detail at origination): an enumerable feature
list (release highlights, or input_props.highlights carried onto a
reject re-author) becomes its own scene acceptance criterion, bounded to
the AC caps; a re-author without highlights carries the
feedback-addressed criterion instead.
Extracted findings.py's criterion matcher into shared unmatched_criteria /
uncovered_acceptance_criteria instead of duplicating it; criteria_verified
joins the WAF free-text exclusion set like findings/issues.
* fix(gateway): break the block/unblock wedge — four hardening fixes from the live PM loop
A cell task looped fe-pm/main-pm block/unblock for hours (10 cycles, 43
spawns): a transient GitHub API error resolving CI became an unwaivable
blocker finding whose own fix text said no code change was required, the
submit freshness guard then demanded a commit no finding called for,
escalate_up auto-blocked, and main-pm's correct recovery plan 422'd on
the approach length cap, degrading it to a bare unblock. Four fixes:
- pr_pass CI-unresolvable refusal is now explicitly transient-worded:
retry pr_pass shortly, do NOT pr_fail over a CI-status lookup error —
a platform blip is not a code finding
- submit freshness guard grants ONE unchanged-head resubmission per
head sha when the findings ledger has zero open rows (all addressed
without code changes) — stamped via the resubmit_unchanged_head
marker so the same head can never loop a second time
- unblock carries a flip breaker: block_flip_count marker, and at the
third flip a one-shot CEO notification flags the task as structurally
wedged (unblock itself still succeeds — the breaker signals, it does
not wedge recovery)
- i_will_plan's approach cap truncates at 800 chars instead of
rejecting — an over-detailed plan must never cost the PM its turn
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* 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>
* 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>
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.
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(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.
* [2f806123] feat(motion): add release-0.25.0 composition extending panel-demo register
* [2f806123] fix(scope): revert out-of-scope backend and panel changes from video branch
* [2f806123] fix(scope): revert out-of-scope backend and panel changes from video branch
* [2f806123] fix(scope): restore out-of-scope files from current origin/master after stale-master revert
* [2f806123] fix(scope): restore motion/pnpm-workspace.yaml from origin/master
* [2f806123] docs(motion): add release-0.25.0 composition example to README
---------
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>
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.
The fresh release clone in the orchestrator container has no git
identity, so the release commit refused with 'Author identity unknown'
— and the unconditional -S would have failed next on the keyless
container. commit_and_push now sets a configurable bot identity on the
clone and signs only when ROBOCO_RELEASE_SIGN_COMMITS is armed with a
mounted key.
make quality inside the production orchestrator container fails on
~1000 clean-env assumptions (armed compose flags, live Redis, host
mounts) for a tree that is green in CI — proven live on the first
org-proposed release. The execute-time gate now re-verifies the head
rung's CI conclusion, fail-closed on absent or red with branch, sha,
and conclusion in the failure detail; the pushed release commit keeps
its own CI wait before publish.
The readiness drafter transcribed raw commit subjects even when
[Unreleased] carried curated prose, and the executor inserted its entry
below a still-populated [Unreleased] — shipping the same content twice
in two qualities. The drafter now uses the curated body as the release
entry when present (transcription stays the fallback; completeness gaps
still police curation), and the executor empties [Unreleased] as it
stamps the entry. [Unreleased] itself catches up with the feedback
round, the dense tooltip passes, and the slave-CI fix.
The readiness sweep reads the head rung's CI conclusion and treats a
missing run as unknown, which silently blocked every release proposal.
Slave joins the push triggers and the paths filter covers the
docs/panel-only commits that land there.
Task lifecycle actions get a 25-action tip map, every dialog field and
git panel control annotated; fixes two round-1 disabled-button tips
that could never fire and a double data-state stamp on tab-progress.
Every sidebar destination, metric derivation, sortable column, queue
action, and dialog control explains itself; mobile tab bar shares the
sidebar's descriptions so the two nav surfaces can't drift.
Key band gets a 14-unit gutter between the key column and self-hosted;
mix-mode rows gain horizontal separation, vertical rhythm, and taller
row padding.
Density bar: every dialog field, column header, filter, badge, and
status pill explains itself — grounded in the backend (ladder
normalization, rate-limit park/probe, self-hosted fallback, reindex
scope). Disabled buttons span-wrap so tips fire; tabs re-assert
data-state.
First rung badges 'PRs land', last badges 'release', middles drop the
fake rung-N names, and a 'promotes to' connector shows the direction
between rows; helper copy and the dialogs' default-branch hint follow
the same vocabulary.
repeat(auto-fill, minmax(17rem, 1fr)) replaces the breakpoint ladder:
one column on a phone through eight on a 27", consistent card width
across every team section at any viewport. Cards gain padding and a
base-size title.
Derivation tips on every key-metric, scorecard, and quality-metrics
figure; the cryptic member-scorecard headers get full decodes; avatar
initials, truncated ids, and toggle buttons gain accessible names;
title-only hints upgrade to the HelpTip idiom throughout.
All 34 feature flags get verified one-line tips; secret inputs state
the write-only contract; KB index types get canonical descriptions;
switch tips ride Labels so Radix data-state stays intact. Also fixes
the scorecard SectionLabel swallowing props, which made tooltips on it
silently inert.
Full import-graph walk per page: filter-chip and plan-section icon
buttons gain their first accessible names, per-state description maps
for git/docs/finding/priority badges, truncation-gated full-value tips,
disabled buttons explain their gate. Stateful Radix triggers use the
task-tabs data-state re-assertion pattern.
by_state.running / a.state 'running'/'stopped' never exist in the
orchestrator enum (offline/starting/active/waiting/idle/stopping), so
these counters were structurally zero — same class as the agents-page
Active bug, remaining two sites.
Total Agents now counts the roster (backend total_agents is live
containers only) and Active reads by_state.active — running/ready never
existed in the backend enum, so the counter was structurally zero.
Board + Main PM fold into one leadership band, cards compress to a
status dot + single detail line, and the grid densifies with per-team
count badges.
Five near-empty stat cards become a 2x3 tile band sharing the row with
the status donut. Journal entry cards and the entry page render full
ids with the shared CopyButton and a /tasks quick-link badge, with the
entry-card anchor restructured so links no longer nest.
Per-agent mix rows group under Board / Main PM / cell / intake headers
in a two-column grid with compact selects; Grok + Ollama key cards
stack in the left column with the self-hosted section to their right.
Save-mix and inherit semantics untouched.
TooltipTrigger asChild spreads its own data-state (closed) through the
Slot merge onto the TabsTrigger, overriding Tabs' active/inactive
attribute, so no tab ever matched the data-[state=active] styles.
Re-asserting data-state in the trigger's own props survives the merge;
derived from the same activeTab that drives the controlled Tabs.
GET /orchestrator/agents/{id} 404s for any non-running container, and
the page's error early-return unmounted the DB-backed header/activity
content #529 had placed behind it. The fatal card now gates on the
roster identity lookup; a live-status error degrades in place to a
not-running banner + spawn dialog, with retry disabled on the
deterministic 404.
CLAUDE.md gains the five undocumented subsystems (env-branches ladder +
EnvSyncEngine, Telegram bridge, possibilities matrix, collision map,
PR labeler) and their flags; docs/map and the pr-creation workflow now
describe head/prod ladder resolution instead of single default_branch;
CHANGELOG's [Unreleased] covers all sixteen merged PRs plus this
hardening basket.
Renames the smoke-replay loop variable mypy 2.3's stricter narrowing
rejects (the uv.lock bump made this the promotion blocker), renames
_ReleaseContext.default_branch to prod_branch to match what it holds
since the env ladder, extends the Makefile-gated guard to bare uv sync
on both runtimes (shared-cache poisoning is the race the guard exists
for), and lists ROBOCO_TELEGRAM_ENABLED in both compose files
(byte-identical).
_compute_file_range now caps any resolved window at _FILE_MAX_LINES
instead of only the exact whole-file shape, closing the near-whole-file
bypass. Telegram sends ride a generalized after-commit outbox
(defer_after_commit over the F107 machinery) so a slow Bot API can no
longer hold the caller's transaction open; TelegramClient grows an
abstract close(). The KB update loop iterates AUTO_INDEX_DIRS so
docs/map edits re-index without a restart. PR-label application
catches all exceptions per its never-raises contract, and pr_merge's
CEO-only message names the resolved branch.
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.
Pure derive_pr_labels (foundation/policy/pr_labels.py) maps a PR's shape
to a stable org-structure label set: to master/to slave (is_root_pr
discriminator), root, MegaTask, and the owning layer (main-pm /
cell/{team} / subtask/{team}). Mirrors batch.py: object|None inputs,
enum-or-string normalization, no DB/I/O. Full slave-targeting semantics
(base_branch vs default_branch) land with the slave/master wiring (W-H);
YAGNI now.
GitService._apply_pr_labels posts the result to the GitHub labels API
best-effort (create-before-add, swallow 422/409, never raises) so a label
failure can never block PR creation. Wired at all three PR-opening sites:
create_pr (gateway path), create_pull_request (REST/task path), and
_push_and_open_conventions_pr (static chore label). Existing PR tests
mock _apply_pr_labels so they never hit the real labels API.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Reject cancelled the proposal's status but never moved it out of the
held-proposal set, so the one-open-proposal dedup blocked the release
manager from ever re-assessing — a rejected proposal deadlocked the cycle.
reject() now sets CANCELLED (mirroring video_post_service), which
list_open_release_proposals already excludes, so a fresh proposal can
originate next cycle.
A failed ~40min background execute (gate red, CI red, or an unexpected
crash) left the proposal silently PENDING with no signal to the CEO.
_run_approve_background now writes a release_execute_outcome marker
(status + detail) on every terminal outcome, and an 'error' marker on an
unhandled exception. GET /proposal surfaces execute_status / execute_detail
/ execute_in_flight (derived from the in-memory _INFLIGHT_APPROVES registry)
so the panel can show a running badge, a failure block with the reason, and
a Retry-approve label instead of a silent wait.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(gateway): reviewer/PM collision map (W5)
The collision surface (intends_to_touch / adds_migration / touches_shared)
is authored at delegate time, consumed once by SequencingService to wire
dependency edges, then never shown to a reviewer again. This surfaces it:
- Pure builder (services/gateway/choreographer/collision.py): for a task
under review, the surfaced siblings (same parent) that would collide —
file-overlap globs or a shared migration chain (both adds_migration) —
with the overlapping globs and a declared-vs-actual drift check. No
DB/IO; callers fetch siblings (one indexed get_subtasks query, mig 069)
+ actual files (git). Caps: 10 siblings, 5 globs.
- Evidence envelopes: collision_context block injected into QA
claim_review, PR-gate claim_gate_review (both carry real touched files
so drift is populated), and the PM i_will_plan briefing (no actual
files at plan time, drift omitted). Best-effort — a failure omits the
block, never breaks the verb/briefing. Empty block omitted (zero token
cost via _EVIDENCE_OMIT_WHEN_EMPTY).
- Panel: GET /api/tasks/{id}/collision-map (declared surface + sibling
overlap; no drift — the panel route resolves no workspace) + a Collision
tab on the task detail (8th tab). Mock-mode returns an empty map.
- docs/map added to the RAG auto-index dirs so the collision-map concept
is fleet-retrievable; skipped gracefully if the dir is absent.
19 new tests (15 unit on the pure builder + 4 integration on the route).
Gate green: ruff/mypy/xenon (module rank A)/pytest 13000/coverage 94.81%,
panel typecheck/lint/516 tests.
* [w6-telegram] Add Telegram notifications bridge (V1)
CEO-facing Telegram DM bridge, flag-gated off by default
(ROBOCO_TELEGRAM_ENABLED). Mirrors the X-credentials / X-client pattern:
- TelegramCredentialsTable (migration 073) — singleton Fernet-encrypted
bot_token + chat_id, all-or-nothing set/clear; API never returns plaintext.
- TelegramClient ABC / NullTelegramClient (no-op, configured->False, never
raises) / LiveTelegramClient (httpx POST sendMessage) / build_telegram_client
factory (Null when creds unset).
- /telegram/credentials CEO-only routes (write-only, guard-decorated).
- Best-effort _notify_telegram fan-out from the two CEO-notify producers
(notify_ceo_of_escalation, notify_ceo_of_completion) — guarded by the flag,
never raises into the producer, carries a panel deep-link when
panel_base_url is set.
- panel credentials card (2 fields) nested in the Telegram feature-flag row.
- panel_base_url + telegram_timeout_seconds config fields.
V1 scope only: credentials + flag + panel card + client + one-line fan-out.
Out of scope (V2): inbound commands, a TelegramEngine background loop, a
dedup ledger, a bus subscription.
* [w6-telegram] fix: slave mypy/xenon regression (product tests + helper extract)
Pre-existing on slave from prior session's merges — no PR's CI caught them
(squash merges don't re-CI the result; each branch was based on older slave).
- test_product: _product helper returned MagicMock -> list invariant error;
cast to ProductTable, move import under TYPE_CHECKING.
- test_usage: svc.session.execute (AsyncSession) has no call_args_list;
cast to MagicMock at the two call sites.
- product.progress_for_products: xenon rank C -> extract module-level
_project_to_products_map helper (repo pattern: helper-extract).
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* [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>
* [W9-5a] Tooltip sweep foundation: HelpTip helper + shared cryptic badges
HelpTip: DRY wrapper over the verbose 3-element Radix Tooltip pattern so a
broad sweep stays a one-line wrap per site (falsy label short-circuits to the
bare child). Unit-tested (3 cases).
TaskStatusBadge + AgentStateBadge: the panel's most cryptic, most-frequent
elements (15 task lifecycle states, 11 agent states) had no explanation
anywhere. Add a per-state tooltip via HelpTip, with the canonical text in one
description map and exported as taskStatusDescription / agentStateDescription
so the per-view inline renderers (kanban, task header) reuse it instead of
re-declaring. This is part 1 of the W9-5 tooltip sweep; the per-view inline
surfaces follow in subsequent PRs.
* [W9-5b] Per-view tooltip sweep: decode cryptic badges, icon-only buttons, status dots
35 HelpTip additions across 22 panel components, reusing the W9-5a
helper plus taskStatusDescription/agentStateDescription. Tipped:
task-id/commit-hash/branch/PR badges, severity/origin/status badges,
priority (P0-P3), migration/shared flags, MegaTask umbrella badge,
Review Gate / For Resumption / Confidential note badges, icon-only
view/delete/edit/clear/show-hide buttons, semver bump + gate-state
badges, ahead-not-pushed badge. Skipped self-explanatory labeled
buttons and elements already carrying title=.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Backend: GET /git/file reads a file at a branch tip (read_file_at_branch)
and slices it to a line window — explicit start/end, a line+context center,
or the whole file capped at 2000 lines. _compute_file_range is the pure
helper (unit-tested).
Frontend: useGitFile hook + CodeSnippet (styled <pre>, line numbers, active-
line highlight — matches git-diff-viewer, no shiki). Wired into FindingCard
so each file:line finding shows the surrounding source. Fail-open: a missing
file renders a muted hint, never breaks the card.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Backend: ProjectSummaryResponse gains task_counts (done/active/blocked) + ci_watch_enabled. ProjectService.task_counts_for_projects does one GROUP BY project_id over TaskTable for every distinct project_id in the list (a project with no tasks is absent — route falls back to None). ci_watch_enabled is read straight off the Project row (already a column) — a 0-cost schema extension, honest signal that CI-watch is armed, no live-conclusion fan-out. project_to_summary takes an optional task_counts. No migration.
Frontend: ProjectTable gains a Tasks column (done/active/blocked + health dot, amber at-risk when blocked>0) and a CI-Watch badge under the project name when ci_watch_enabled. Both desktop Table and mobile ResponsiveTableCard variants. Mock projects carry the new shape (two sample repos).
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Backend: ProductSummaryResponse gains cells: [{team, project_id, project_name}] and progress: {done, active, blocked}. ProductService.progress_for_products does one grouped query over tasks for every distinct project_id any product references, summed per product (monorepo case dedups a project once per product via a seen set). list_all eager-loads cells + each cell's project (selectinload + joinedload) so product_to_summary reads project.name without an N+1. No migration — reads existing tasks.status + product_projects.
Frontend: ProductTable renders a Cells column (team badges + project names, Unmapped when empty) and a Progress column (done/active/blocked counts + a health dot: amber at-risk when blocked>0). Both desktop Table and mobile ResponsiveTableCard variants. Mock products carry the new shape.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Backend: optional agent_slug filter on GET /usage/time-series + UsageService.get_time_series (AgentSpawnSessionTable.agent_slug column already exists — no migration).
Frontend: AgentActivityPanel on the agent detail page — a 7d per-agent token sparkline (recharts AreaChart) + a merged work-session/journal activity timeline. Work-sessions filter by the agent UUID (WorkSessionTable.agent_id is a UUID FK to agents.id), journals by slug. List grid left as-is (avoids 25-agent fan-out). Card last_active deferred (no live hook populates AgentMetrics).
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Backend: widen usage _PeriodType to 24h/7d/30d/90d and add a 90d branch to
_parse_period (daily buckets already cover it). TestParsePeriod pins the
contract per window.
Frontend: UsagePeriod += 90d with a scaleFor helper (replacing 6 inline
ternaries) and 90 daily mock points. One generic SegmentedControl primitive
(reuses Radix Tabs) drives both the metrics time-window selector
(24h/7d/30d/90d) and the per-chart Chart/Table view toggle — one file, two
roles. The Token Usage & Costs tab drops 8 hardcoded '24h' hooks for a
single period state + selector; the stale '(24h)' cost-card parenthetical
goes too. The Performance landing tab gains a TaskStatusChart donut fed by
the status counts already on the page (no new hook). Agent/team bar charts
gain an inline table view.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The bell showed a transient WebSocket-stream buffer count with no
read/ack actions; the persisted unread/ack state and the mark-read /
acknowledge / mark-all-read mutation hooks already existed
(use-notifications.ts) and powered the notifications page, but the bell
ignored them.
The bell now derives its badge from useNotifications().unread_count (the
real DB count, not the stream buffer), renders the recent items with
per-item Mark Read + Acknowledge + a header Mark all read, shows the
pending-ack count, and keeps the WS stream only for the connection
indicator (the NotificationAlerts sibling still owns the toast/chime).
The stream buffer is cleared on popover close so it can't grow unbounded
now that it's no longer displayed.
The mutations self-invalidate notificationKeys.all on success, so the
badge + popover refresh immediately after each action; useNotifications
also refetches every 30s.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* [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>
docs/map is the agent-facing exhaustive codebase map (CLAUDE.md) but was never
RAG-indexed — only docs/rag was. Add it to OptimalService._auto_index_dirs so
every docs/map/*.md rides index_documentation (the generic _index_docs_directory
rglobs *.md and routes only the 'standards' subdir to the standards indexer) and
becomes roboco_kb_search-able. No map-specific branch needed.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(panel): add pickTab helper for validated URL tab params
* fix(panel): validate kanban view + KB tab params via pickTab
The bare `as T || default` cast only guarded null — a typo/invalid value
(?view=deev, ?tab=foo) passed through as an out-of-set TabValue, blanking the
active tab highlight and the content pane. pickTab validates against the
known set and falls back to the default on null/empty/invalid.
* fix(panel): highlight active sidebar footer link (exact match)
SidebarFooter had no isActive branch (SidebarNav does), so footer links
never highlighted. Added with EXACT match (pathname === href) — not
startsWith — so /settings does not also highlight on /settings/ai-providers.
Main nav keeps startsWith (longer hrefs need prefix matching); the two
intentionally differ.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* 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>
* 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>
PR target, workspace clone, read-clone pin, branch base, merge-target
resolver and the default spawn git context now resolve the head rung
(head_branch) instead of the raw default_branch. The release executor
targets the prod rung (prod_branch) for clone/commit/tag (W-H decouple)
and runs a full-chain head->...->prod promotion before the bump, fail-
closed on a divergent rung (promotion_failed outcome). release_readiness
switches the diff baseline to prod..head with a tag_drift cross-check
(last tag != prod tip => hotfix-on-prod). release_manager_engine fetches
the prod rung into the head-pinned read clone so origin/<prod> resolves.
Replace the single default_branch with an ordered environments ladder
(list[{name,branch}]; first=head=PR target, last=prod=release target,
middle=intermediates). Migration 073 adds nullable JSONB projects.environments;
a read-time shim (roboco/models/env_branches.py) synthesizes a degenerate
single-branch ladder from default_branch when environments is null, so
behavior is unchanged until the operator declares a real split in the panel.
Wired through Project/ProjectCreate/ProjectUpdate models (normalize_environments
validator: rejects empty name/branch + dup branches), ProjectTable column,
API request/response schemas, create+update routes + service. EnvSyncEngine
+ consumer rewires follow.
Wire the long-unwired mark_waived repo method to a new auditor-only
flow verb waive_finding, severity-scoped to minor/nit (blocker/major
must be fixed, never waived), requiring a note, with a task.finding_waived
audit event and no task status change. Add the verb to the IntentSpec
table (auto-derived into the auditor manifest), the flow_auditor route,
and the flow_server MCP tool.
Surface open review findings (cross-task, blocking-first) on the
auditor dashboard via ReviewFindingsRepository.list_open_findings and a
new findings field on AuditorDashboard. Restore the panel's 4-card
auditor layout with a new read-only FindingsQueuePanel as the 4th card.
The push-event e2e smoke flaked ~1/50 with
``RuntimeError: Future ... attached to a different loop`` in
test_create_seam_materializes_note_flag_on_and_off (and the janitor test
shares the same helper). Root cause: _fresh_factory returned the app's
SHARED get_session_factory() (_DbHolder engine), so the test's session
shared a connection pool with the uvicorn server thread (loop B). A
lingering app handler from a prior test could check out a connection on
loop B; asyncpg's pool is not loop-affinity-aware, so it then handed the
vault test a connection created on loop B, awaited on the test's
function-scoped loop A → cross-loop. _reset_lazy_db_holder only resets
at teardown, so it can't stop a lingering handler contaminating the
fresh pool mid-test.
Fix: _fresh_factory builds a PRIVATE engine from e2e_stack.db_url and
returns (factory, engine); the caller disposes it in finally. The
create/janitor seams use only the passed session (assemble_task_note_data,
get_project_service, VaultJanitor never call get_session_factory), so a
private engine against the same e2e DB exercises the real wiring while
keeping its pool loop-pure — the app can't reach it.
This is the e2e-suite cross-loop flake that was blocking PR #516's
push-event e2e check (the pull_request run passed, the push run hit this
unrelated vault test). Pre-existing; not introduced by the auditor fix.
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.
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.
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.
CI xenon --max-absolute B failed on _normalize_sandbox_extensions
(project.py), _sandbox_features_scope + request_sandbox (content_actions).
Extracted _validate_one_sandbox_extension, _validate_per_call_extensions,
_sandbox_provision_or_reject — same behavior, rank B. ruff/mypy/xenon/51 tests green.
CI caught: AttributeError: module 'sqlalchemy' has no attribute 'JSONB'.
Repo convention (mig 043/010): from sqlalchemy.dialects import postgresql;
postgresql.JSONB(). Integration test now applies cleanly.
Project edit dialog (Sandbox section) exposes a per-service extension
picker — Switches from the allowlist grouped under each enabled service
(postgres: pgvector/PostGIS/pg_trgm/citext/uuid-ossp; redis: RediSearch/
RedisJSON/RedisBloom; mongo has none), mirroring the backend
SANDBOX_ENGINE_FEATURES allowlist. State holds a per-service Set; payload
builds sandbox_extensions only for enabled services with non-empty picks
(empty {} clears the column, mirroring sandbox_services' always-send —
exclude_unset + no exclude_none means an explicit {} writes NULL). The
picker renders only for opted-in services with activatable features.
Types: Project.sandbox_extensions (Record<string,string[]> | null),
ProjectUpdate.sandbox_extensions? (not on ProjectCreate, mirroring
sandbox_services). Mock create seeds null.
Docs name the allowlist (the security containment — no plpython3u), the
no-default-set rule (opters set explicitly, existing opters stay bare), the
standing-vs-per-call union, cache-by-features, kitchen-sink image selection,
and the recommendation to set the full set in project settings so agents
request subsets. sandbox-db.md gains an Extensions section; task-tools.md
and config-reference.md updated; CLAUDE.md sandbox paragraph extended.
Gate: panel typecheck + lint + prettier clean, 516 tests pass.
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).
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.
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.
A branch_name set on a task was treated as proof the ref existed on origin,
so _finalize_claim skipped _ensure_branch_for_task and create_branch/push
never ran. A manual field write (or a prior failed create_branch whose
rollback didn't restore branch_name) left the field set while the branch was
never pushed; descendants then ls-remote'd the name, found it empty, and cut
from master via create_branch's silent fallback — breaking the cell->root
branch hierarchy (MegaTask f7d0a61a root-branch 404).
Defect A:
- _ensure_branch_for_task trust-but-verifies a pre-set branch_name: probe
origin, and when the ref is confirmed missing run the full create to push
it. An inconclusive probe (network error) fails soft so a transient glitch
can't fail a normal resume claim. Gated on project_id so branchless
coordination/umbrella tasks are untouched.
- _finalize_claim always runs _ensure_branch_for_task (the single chokepoint
that ensures the branch exists) and snapshots+restores branch_name on
rollback, so a failed first attempt can't leave the field half-set and
short-circuit a retry.
- GitService.branch_exists_on_remote: ls-remote probe returning True (present)
/ False (absent) / None (probe errored, fail soft).
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>
* [13d03d5c] Add 5 coordination-event notification producers + wire at chokepoints (#472) (#474)
* [13d03d5c] Add 5 coordination-event notification producer methods
* [13d03d5c] Wire reassignment/collision/unblock/dependency-revival notifications
* [13d03d5c] Wire stale-claim-reaped notification into orchestrator reaper
* [13d03d5c] fix(runtime): guard reaper's UUID annotation + defensive attr access
The stale-claim-reaped notification hook added a runtime-unquoted
`UUID` type annotation (only imported under TYPE_CHECKING, so the
module raised NameError on import) and a direct `t.assigned_to`
attribute access that crashes against the minimal test doubles the
existing reaper test suite uses. Quote the annotation and switch to
getattr-defensive access, matching `_assignee_is_provider_parked`'s
existing convention in the same file.
* [13d03d5c] test(notification): unit coverage for 5 coordination-event producers
One test per new send_* method (reassignment, collision-sequencing,
unblock, dependency-revival, stale-claim-reaped) following the
existing _FakeDb/_patch_db_context pattern, asserting subject/body/
related_task_id/priority/recipient-count, plus a no-recipients no-op
case for reassignment.
* [13d03d5c] test(task): prove reassign + unblock don't double-fire notifications
Two chokepoint-level tests mocking NotificationService at its defining
module: a repeated reassign() to the same already-current target skips
the notification (guarded by comparing against the pre-mutation
assignee), and a repeated unblock() on the same task only notifies
once since the second call short-circuits on the status!=BLOCKED
guard.
* [13d03d5c] style(task): ruff format the collision-sequencing wiring block
No behavior change — reflows the newly-added _notify_collision_sequencing
call site to satisfy ruff format's line-length rules.
* [13d03d5c] docs(backend): add coordination-event notification producers guide
Documented the 5 new NotificationService producers (reassignment, collision-sequencing,
unblock, dependency-revival, stale-claim-reaped) with fire conditions, double-fire
prevention mechanisms, and implementation patterns. Updated backend README to link the
new services guide for developers integrating new coordination events.
---------
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
* [3ee8150b] Frontend: render coordination-event notifications + e2e smoke coverage (#475)
* [69777c3a] test(e2e-smoke): add coverage for soft-block + unblock coordination notifications (#471)
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
* [8eb82639] Render 5 coordination-event notification types with task deep-links (#470)
* [8eb82639] feat(notifications): add APPROVAL type icon and deep-link component test
Add missing APPROVAL member to the frontend NotificationType enum to
match backend roboco/models/base.py, wire its icon into the existing
typeIcons Record in the notifications page, and add a component test
covering type rendering and the task deep-link.
* [8eb82639] docs(notifications): document 5 coordination-event types and APPROVAL enum addition
Added comprehensive reference guide explaining the 5 notification types
(TASK_ASSIGNMENT, BLOCKER_ESCALATION, REVIEW_REQUEST, DOCUMENTATION_REQUEST,
APPROVAL), their visual identities (icon + color), use cases, and
deep-linking behavior to related tasks. Updated panel README with quick
reference table. TypeScript Record pattern ensures exhaustive type coverage
at build time.
---------
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: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
* [a27de2a8] fix(docs): reflow hard-wrapped notification-types.md to pass markdown gate (#479) (#481)
The Python quality gate on assembled PR #477 was red because the newly
added docs/frontend/components/notification-types.md (introduced by the
frontend coordination-event rendering commit) had manually wrapped prose
paragraphs, which scripts/reflow_md.py --check rejects as part of make
quality. Reflowed the file with scripts/reflow_md.py --apply (whitespace
only, no content change) so the check passes. ruff format/check, mypy,
xenon, vulture, bandit, and the full pytest suite (10284 passed) all
confirmed green on this commit; notification.py, task.py, and
orchestrator.py are untouched.
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
* [705419d5] Remove duplicate unblock notification and fix its dependent tests (#485) (#488)
* [705419d5] fix(notifications): remove duplicate unblock notification, fix its tests
The /unblock route was still calling delivery.notify_assignee_of_unblock()
(TASK_ASSIGNMENT) after TaskService.unblock() already sent the
send_unblock_notification() ALERT wired in by an earlier task — a real
duplicate notification on every unblock. Delete the route-layer call and
the now-dead NotificationDeliveryService.notify_assignee_of_unblock
method, fix the integration test that mocked it, and fix/extend the e2e
notification-coordination-events test to assert the persisted ALERT rows
(exact subjects) for both the direct-unblock and dependency-revival
producers instead of the old TASK_ASSIGNMENT assertion.
* [705419d5] docs(backend): update coordination-events doc for unblock duplicate removal
---------
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
* [6c142a73] docs(changelog): document restored coordination-event notification producers and add collision-sequencing double-fire test (#489) (#490)
Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
* [77719d3f] Seed system agent in e2e harness to fix unblock/dependency-revival notifications
The e2e harness's seed_company omitted the system sentinel agent that
production seeds via initial_data.py. The unblock and dependency-revival
notification producers default to from_agent="system", which
_resolve_agent_uuid looks up by slug in the DB. With no system row the
resolver returns None and _create_notification silently skips the
notification, so the two ALERT assertions got 0 rows instead of 1.
The soft-block test passed because it uses NotificationDeliveryService
which creates the notification directly with a real agent UUID as
from_agent, bypassing the slug resolution path entirely.
* [77719d3f] Use foundation UUID for system agent to avoid slug collision
The first attempt seeded the system agent with a random UUID. Other
tests (_seed_system_and_secretary, _seed_video_agents) check by the
fixed foundation UUID via session.get(AgentTable, uuid); not finding
it they INSERT their own system row, hitting ix_agents_slug. Using the
foundation UUID makes their check find the seed_company row and skip.
* [77719d3f] Fix dependency-revival notification event loop mismatch
The dependency-revival test calls _unblock_dependents directly via
stack.run_db, which creates a new asyncio event loop. Inside,
_notify_dependency_revival -> NotificationService._create_notification
opened its own session via get_db_context(), which reuses the singleton
_DbHolder engine — bound to the FastAPI server's event loop. The
asyncpg connection raised 'Future attached to a different loop' and the
exception was silently caught + logged as a warning, so the notification
never persisted and the test saw 0 rows.
Fix: add an optional db_session parameter to _create_notification and
the two send methods. When provided, use the caller's session directly
and skip the internal commit (the caller owns the transaction). The
TaskService's _notify_unblock and _notify_dependency_revival now pass
self.session, keeping the notification in the same event loop + session
as the task transition.
* [77719d3f] Scope system-agent seeding to notification tests only
Seeding the system sentinel in seed_company (commits 3bba7b32/617b7890)
fixed the 0-notification bug but caused 3 i_documented gateway_timeout
failures: every e2e test now paid notification-creation latency for
system-origin notifications that were previously silently skipped,
pushing the already-slow i_documented verb past its 120s timeout.
Move system-agent seeding out of seed_company and into a scoped
_seed_system_agent helper called only by the two coordination-event
tests that exercise send_unblock_notification /
send_dependency_revival_notification (both resolve from_agent='system'
via DB lookup). dev_lifecycle and state_machine tests revert to the
pre-fix behavior (system-origin notifications silently skipped, no extra
latency).
The event-loop fix (commit 7b95d77d: pass db_session=self.session to
_create_notification) is unchanged — dependency_revival still needs it
because stack.run_db creates a new event loop while _DbHolder.engine is
bound to the FastAPI server loop.
* [77719d3f] Fix reassignment notification deadlock + suppressed-notification commit regression
Two fixes in notification.py / task.py:
1. Cross-session self-deadlock in send_reassignment_notification:
TaskService.reassign() flushes an uncommitted row lock on the task,
then calls _notify_reassignment -> send_reassignment_notification ->
_create_notification(db_session=None) which opens a SEPARATE session
via get_db_context() and INSERTs a notification with related_task_id
FK -> tasks.id. The FK key-share lock blocks on the request session's
uncommitted exclusive lock, but the request can't commit until the
notify returns -> 120s verb hard-cut. Fix: pass db_session=self.session
so the notification joins the verb's own transaction, same pattern as
the unblock/dependency-revival fix in 7b95d77d.
2. Suppressed-notification commit regression: the 7b95d77d refactor moved
await db.commit() out of _create_notification_with_session into
_create_notification's db_session=None branch, where it ran
unconditionally — even when _create_notification_with_session returned
early (suppressed: unresolvable from_agent / no recipients /
refire-guard / dedup-hit). Fix: _create_notification_with_session now
returns bool (False at each early return, True after delivery);
_create_notification commits only when created is True.
---------
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@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: Backend Developer 2 <be-dev-2@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The Settings page PUT four keys (notifications_enabled, sound_enabled,
auto_refresh, refresh_interval) the backend's settings allowlist never
accepted — Save died on the first 422 and had never persisted these
cards. Worse, nothing consumed the prefs anywhere: no auto-refresh timer,
no notification toast, no sound system existed. Pure theater.
- the four prefs move into the persisted UI store (client-only, same
idiom as theme/sidebar) and the cards apply instantly; the dead server
plumbing and the global Save button are gone — the backend allowlist
stays strict and untouched
- AutoRefreshDriver (new): when Auto Refresh is on, ticks the page-refresh
registry every N seconds — skips while nothing is registered or a
refresh is in flight; default-off so no background poller starts unasked
- NotificationAlerts (new): toasts each newly-arrived WS notification
(subject + priority) when notifications are enabled, with an optional
~120ms Web-Audio chime — initial backlog on connect never toasts, one
chime per batch, autoplay blocks never throw
- tests: settings page rewritten store-driven; fake-timer coverage for
the driver; stream/store/sonner/AudioContext-mocked coverage for alerts
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* 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>
* 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>