* feat(x): redraft loop on CEO reject — feedback re-enters the draft flow
A rejected X draft's reason used to die with the cancel. reject() with a
non-blank reason now schedules a redraft after its commit
(defer_after_commit; fresh session; never blocks or fails the HTTP
response): XEngine.redraft_from_rejection re-drafts the same source kind
via the local model with the reason and rejected body folded in as
revision guidance, originating ONE fresh held draft — mirroring the
video pipeline's reauthor_from_rejection. Local-model failure or empty
output originates nothing (no degraded copies); markers carry forward
whole so a redrafted reply/spotlight stays fully functional downstream;
bodies ride the same 280 clamp; the open-posts cap holds.
Hardened per adversarial review: reject() is now idempotent on an
already-CANCELLED target at both check sites (mirroring approve's
already_rejected guard — a replayed reject schedules nothing), and the
dedup check+originate runs under a non-blocking identity-keyed Redis
lock (SET NX + compare-and-del, matching the approve/reject mutex
style) so racing rejects can't stack duplicate drafts — lock held or
Redis down skips the redraft, which is always safe. Tests pin the
fresh-session contract by session identity, the replay no-op, the
lock-skip, and clean up their own committed rows.
* fix(tests): runtime UUID import + typed task-id coercion in x cleanup helper
CI's quality gate runs mypy over tests/ (the local pass covered only
roboco/): the _delete_tasks calls handed ORM-typed ids where uuid.UUID
was expected. Coercing at the call sites then exposed that UUID was
imported under TYPE_CHECKING only — a runtime NameError. Import moved
to runtime; both call sites coerce explicitly.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Root cause first: no viewport export existed anywhere, so viewport-fit
was never 'cover' and every env(safe-area-inset-*) resolved to 0 on
notched iPhones — content under the status bar, dock without real
home-indicator clearance. The export lives on the (tg) group layout
(server component), NOT app-wide: the dashboard shell has no safe-area
padding and must not inherit cover.
Also: min-w-0 on four truncating flex children that overflowed their
justify-between rows (chat names/previews, fleet task titles);
object-contain on the approvals video (letterbox instead of distort on
short phones); break-words on the changelog pre / task description /
quoted mention; touch targets bumped to >=36px (sheet close, segmented
controls, ack button, chips, back button, bell, jump-to-latest,
cut-toggle); overflow-x-hidden backstop on the (tg) main scroller; fleet
avatar strip sliced to 3 with a +N badge instead of silent clipping.
Verified: pnpm typecheck clean, lint 0 errors, panel suite 870/870.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
fastapi-guard peels a fixed trusted_proxy_depth=1 from X-Forwarded-For
(the rightmost entry, which nginx itself recorded). That is correct for
every chain except host-proxied tailnet traffic (Tailscale Serve →
nginx), which arrives as [tailnet-client, loopback-or-bridge-gateway] —
depth-1 resolves it to a whitelisted hop IP, leaving WAF/ban/rate-limit
inert for the whole /tg surface (the documented ceiling).
ClientIpResolutionMiddleware (pure ASGI, wraps SecurityMiddleware so it
runs first) stamps guard_core's request.state.client_ip cache — its
supported pre-resolution seam — for EXACTLY that shape: peel known local
hops (loopback + docker bridge pool) from the right, stamp only when at
least one hop was peeled AND the candidate is in the tailnet CGNAT range
(100.64.0.0/10). Every other shape abstains, so direct LAN clients,
agent containers relaying through nginx (even with forged public-IP
prefixes), and all-hops operator traffic resolve byte-for-byte as
before. Documented residual: a same-bridge container forging a CGNAT
prefix only DE-privileges itself (loses its whitelist exemption). XFF is
read first-occurrence to match Starlette's own header semantics, and a
wiring test pins the middleware mount ORDER, not just presence.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The pg_dump sidecar wrote its dumps to the same disk it protects — one
disk failure lost both. Setting ROBOCO_BACKUP_MIRROR_DIR in .env to a
path on a different disk (external/remote mount) arms a mirror step after
every successful dump: tmp+rename copy, mirror pruned to the same
BACKUP_KEEP, unwritable mirror logs-and-skips without blocking the
primary. Unset, the script never attempts a copy — no fake off-disk
copies on the same disk. Docs gain the mirror setup and a quarterly
restore drill (throwaway pgvector container, pg_restore, row-count
sanity check).
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(lifecycle): inherit advanced upstream base on work re-claims
A re-claim reused a branch cut at an earlier claim, so upstream work
merged since (UX/UI landing on the root after the cell branch was cut)
never reached BE/FE branches — divergence and avoidable conflicts.
_finalize_claim now merges the advanced base into the pre-existing branch
via the dependency-lineage merge: already-ancestor is a no-op, a conflict
aborts at the cut point and leaves a transition note steering the agent
to sync_branch, a clean merge logs an audit trail; never fails the claim.
Double-gated: by role (developer/cell_pm/main_pm — QA/documenter/gate
claims review the branch as pushed and never move it) AND by pre-claim
status (pending/needs_revision only — a PM's i_will_plan re-claim of its
own awaiting_pm_review task must not move a branch that already passed
QA + the PR gate). Fresh cuts already branch from the live remote base.
Cell-PM prompt now orders reading the upstream design docs before
planning.
* fix(lifecycle): extract base-inheritance gate predicate for xenon budget
The four-condition inline gate pushed _finalize_claim to cyclomatic
rank C; the quality gate caps blocks at B. The decision moves to a pure
module-level predicate, byte-for-byte the same logic.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
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>
approve() flushed the published proposal's COMPLETED status but left the
durable commit to the background caller (_run_approve_background), which runs
after approve()'s finally has already released the Redis lock. In that window
a concurrent reject() could acquire the freed lock, re-read a row whose
COMPLETED write was only flushed (invisible to its own session under READ
COMMITTED), pass its guard, and flip the just-published proposal to CANCELLED
— last writer winning the row. The bug-sweep (#638) fixed reject()'s side of
this but left approve()'s, so reject()'s 'fails closed' guarantee didn't hold
end to end.
Commit COMPLETED while still holding the lock (mirroring XPostService._post),
so it's durable before release and a racing reject sees it and refuses. A
cross-session regression test proves a fresh connection sees COMPLETED the
moment approve returns (it read 'pending' before the fix).
* [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>
CEO A2A mutations now invalidate the Mine-list query key so the list
refreshes without the socket; the tg Metrics tab renders explicit error
notes instead of confident zero stats when a section's fetch fails; the
tg Secretary chat checks a new registry-backed /secretary/live/active
route before auto-starting, showing a Take Over button instead of
silently killing a session live on another device; the AI-routing card
surfaces roster fetch errors instead of an empty grid; the shared
acceptance-criteria editor caps at the backend's 7-item limit; the
board-tab comment no longer calls the task sheet read-only. Tests:
task-sheet approve/reject interactions (not just visibility), a
non-demo metrics error-state test, secretary take-over branches, and
dashboard-router auth-gate coverage (the e2e harness now mounts the
dashboard router so the gate is actually exercised).
mint_installation_token now wraps JWT build + HTTP + parsing so raw
httpx/jwt failures surface as GitHubAppError and the existing PAT
fallback catches them all (a GitHub outage no longer crashes git
operations for App-bound projects). The PEM is validated at
credential-set time instead of first mint, and the installation-token
cache is cleared when credentials are deleted. Also: the video
preview root resolves symlinks like its sibling route (frames under a
symlinked workspaces_root no longer false-404), the tracked dangling
motion/node_modules symlink is removed and the gitignore gains a
slash-less entry that actually matches symlinks, changelog caption
input strips GHSA refs like PR refs, and the edit-project dialog hides
the GitHub App section for auto-detected gitlab.com projects and warns
before a save that would clear both auth sources.
The ceo_name setting only reached the panel header — the CEO agent row
kept its seeded name, so agent selectors (create-flag, assignee pickers,
kanban) still showed the old name after a rename. Setting ceo_name now
updates the CEO row's name in the same transaction, resolved via the
shared duplicate-tolerant get_agent_by_role lookup. The seed path never
overwrites existing rows, so a custom name survives restarts. The
agent-facing docs/map entries drop the personal name. Write-through
tests pin ancient created_at on fixture rows so they stay deterministic
against CEO rows other suite tests may have committed.
The duplicate-CEO-row fix landed on one call site but three identical
bare 'select(...).where(role == ...)' + scalar_one_or_none() lookups
remained in the orchestrator (auditor + two CEO), each still raising
MultipleResultsFound under the same condition. All five sites now route
through a shared get_agent_by_role helper (earliest-created row wins).
The five event-bus notification handlers also thread task_title so a
revived producer renders titles instead of #id8 (protocol signatures
updated to match the service), the pull_request CI trigger mirrors the
push trigger's paths so a fork PR touching only those paths still runs
CI, and a stale a2a comment about auditor/pr_reviewer lacking read_a2a
is corrected.
The pre-push codegen auto-commit staged with a blanket 'git add -A', so
any pre-existing dirty file in the worktree (e.g. crash-orphaned edits a
resume never cleans) was silently swept into the 'regenerate generated
artifacts' commit. Now the porcelain state is snapshotted before and
after the codegen run and only newly-dirty paths are staged with
'git add --'. The commit is also recorded via _link_commit_to_task with
the pushing agent's id threaded from both push call sites, so it shows
up in the task's commit history like every other commit.
The WAF/IP-ban/rate-limit exemption whitelisted all of RFC1918, so any
LAN client reaching the host-published nginx port resolved to its real
192.168.x.x and rode the exemption. Only the docker agent mesh needs it:
keep 127.0.0.1/::1 + 172.16.0.0/12, drop 10.0.0.0/8 and 192.168.0.0/16.
New middleware tests drive the real XFF resolution path (forwarded LAN
client blocked, direct bridge peer exempt); the remaining ceiling
(host-proxied traffic resolving to loopback/bridge-gateway under
depth-1 XFF trust) is documented instead of claimed away.
* fix(panel): A2A transcript/list poll as fallback when the socket drops
The desktop A2A view refreshed ONLY on /ws/system a2a.message frames —
refetchInterval was off (only the /tg mini app polled). So when the socket
flaps (the NAS stack flaps often), the open transcript froze: agent replies
never landed and the thread stuck on the last frame received, even though the
messages persisted server-side. useA2AMessages and useA2AConversations now
take a 10s REST poll gated on the live-stream connection — polls only while
disconnected, never when the WS is healthy, so it's a true fallback with no
wasted requests. Backend was fine (get_messages_admin returns the full
transcript incl. CEO interjects; the frame carries the right conversation_id).
* fix(panel): make the A2A poll an unconditional backstop, not disconnect-gated
Live-checked the NAS: /ws/system connections stay open (10 opens / 0 closes in
30m) and events publish — the socket is NOT flapping, so a disconnect-gated
poll wouldn't fire. The real freeze mode is a silent-dead / half-open WS that
still reports readyState OPEN (no client keepalive ping detects it), where
isConnected stays true. So poll unconditionally: 20s while the socket claims
up, 8s once known-down. Guarantees liveness regardless of why a frame didn't
land.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
#621 only let a NEW project bind to a GitHub App installation (the create
dialog's Select repo picker). An already-imported project on a PAT had no way
to re-route to the App. The Edit Project dialog now carries a GitHub App
section: when App creds are configured, it shows the current binding (App
installation vs PAT), reuses the same SelectRepoPicker to bind, and an Unbind
button to revert to PAT (sends explicit null). Hidden/disabled for non-GitHub
providers. Once bound, git ops (commits, PR reviews) are attributed to the
App bot instead of the operator's account.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
A project that checks in generated artifacts (RoboCo's lifecycle renders,
verb tables) drifts whenever their source changes. The agent pre-submit gate
(make gate) omits foundation-check, so drift is invisible at the desk and only
fails on CI's drift gate — a failure with no link back to the task, which made
one live task thrash 8 revision rounds.
New per-project codegen_command (migration 078): run in the task's worktree
right before push, and any drift committed into the same push, so CI never
sees stale artifacts. Fail-open — a broken/timeout codegen command logs and
lets the push proceed (CI's drift gate is the safety net); a null command
(every project without checked-in codegen) is a pure no-op. Hooked at both
push_branch (open_pr's first push, the PR head CI grades) and push_task_branch
(later re-pushes). RoboCo sets codegen_command='make codegen' (a new Makefile
target — the write counterpart to foundation-check's read) via the panel.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The Auditor and PR reviewers get the real DM button (this branch). Secretary
and Intake aren't A2A-DMable — they run their conversation over a live-session
bridge — so they now carry the same chat icon but it navigates to their own
screen instead: Intake -> /prompter, Secretary -> /business?tab=secretary.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(a2a): CEO can DM the Auditor and PR reviewers
A mid-flight PR reviewer or Auditor that's stuck was unreachable — the CEO
had no way to DM them. Both roles now carry dm/read_a2a, so the CEO can open
a 1:1 and they can reply in-thread through the existing CEO-reply path.
Scoped deliberately: the Auditor stays a silent observer to its peers — it
gains no peer-initiation surface (can_a2a_direct routes it through
_check_auditor_a2a, which refuses every initiation target; it can only reply
inside a CEO-opened DM). PR reviewers keep their owning-PM scope. Intake and
Secretary stay excluded — they have their own dedicated chat pages.
NO_COMMS_ROLES drops to {prompter, secretary}; the panel's EXCLUDE_NON_DM_ROLES
matches. KB/docs updated so the 'auditor/pr_reviewer have no dm' claim isn't
left stale.
* test(a2a): smoke guard checks _NO_COMMS_ROLES, not a hardcoded 'auditor'
The dm() runtime guard no longer names the auditor (it now carries dm to
reply to the CEO); it refuses the canonical _NO_COMMS_ROLES set. Assert on
that set so the smoke test tracks the guard, not a stale role name.
* chore(foundation): regenerate verb tables for auditor/pr_reviewer dm+read_a2a
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
formatBucket guessed hourly vs daily from the timestamp string
(bucket.endsWith('T00:00:00.000Z')) and then rendered LOCAL getHours(), so
every daily midnight-UTC bucket rendered as the viewer's local hour — '02:00'
at UTC+2 — across the 7d/30d/90d windows. Granularity is now derived from the
data's own bucket spacing (bucketGranularity: min gap >2h = daily) and daily
buckets render as a short UTC date. The 24h/hourly view is unchanged. Added
minTickGap so the 90d axis doesn't crowd.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(github-app): App credentials, installation tokens, and a Select repo picker
RoboCo was 100% PAT-based. A singleton Fernet-encrypted github_app_credentials
row (migration 077, telegram-credentials pattern) now stores the App id +
private key; github_app_auth mints RS256 app JWTs and caches installation
tokens until 5 minutes before expiry. Projects can bind an installation
(projects.github_installation_id): get_decrypted_token returns a minted
installation token for bound projects and falls back to the stored PAT on
any minting failure, so all ten token consumers work unchanged.
CEO-gated routes expose credentials CRUD plus installation/repo listing, and
the New Project dialog gains a Select repo picker (disabled with a HelpTip
until the App is configured) that fills the git URL and binds the
installation; manual URL + PAT stays the default path.
* test(panel): mock the GitHub App credentials card in the settings page test
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
#615 merged on a false green: the CI paths filter excludes motion/**, so a
motion-only commit fired no quality run and the merge landed two latent
breakages on slave.
- _get_ceo_agent used scalar_one_or_none on role==CEO, which raises
MultipleResultsFound once a second CEO-role row exists. It now pins to the
earliest-created CEO, mirroring the sibling _get_auditor_agent. This is what
made test_brand_voice_nudge_fires_once fail under the full-suite ordering.
- _process_mentions tipped to xenon rank C when the skip-drafting branch was
added; extracted the cap check and the skip filter into two small helpers.
- ci.yml push paths now include motion/** so a motion-only commit can't
false-green the quality gate again.
Regression test: two CEO rows no longer break the lookup.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(x): real voice guide, slop ban, and caption craft for drafted content
Release posts and mention replies were drafted from a one-sentence voice
stub while the reasoning-backed Head-of-Marketing voice guide only reached
the off-by-default spotlight path. The drafting prompts now carry the full
voice rules, a banned AI-slop list, three style exemplars, hook-first
structure, and an under-240-char budget so the 280 clamp never truncates
mid-sentence.
An empty brand_voice now nudges the CEO exactly once (durable
system_settings marker) instead of silently shipping baseline voice forever.
A failed reply draft skips origination instead of shipping 'Thanks for the
mention!'. Video dev prompts and motion/README gain per-platform caption
templates (X: hook + specifics + outro; TikTok: hook + short lines + few
niche hashtags).
* docs(motion): reflow the new Captions section to satisfy the prose gate
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* 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>
* fix(git): branch list classifies remote refs correctly and prunes stale ones
The branches route detected remote-tracking refs via a 'remotes/' prefix
that --format=%(refname:short) never emits, so every origin/* ref rendered
under LOCAL and origin/HEAD surfaced as a fake branch. Listing now uses the
full %(refname) and classifies on refs/heads/ vs refs/remotes/.
Cleanup's remote deletion worked, but no code path ever pruned the viewing
clone's remote-tracking refs, so deleted branches persisted in the UI
forever. The branches route now runs a best-effort 'git remote prune origin'
before listing remote refs, and the manual Fetch fetches with --prune.
* refactor(git): extract branch-line classifier to satisfy the complexity gate
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The header chip and the Settings User Info card rendered a literal 'Renzo'.
The name now lives in the system_settings store under ceo_name (validated:
trimmed, non-empty, max 60 chars) with the same client-served default the
transcript-retention card uses, editable inline from the User Info card.
Agent prompts already refer to 'the CEO' generically, so no prompt rewiring;
the two agent-facing RAG docs drop the name too. License/CLA copyright is
untouched.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
New Project claimed GitLab/Gitea were 'planned' while both providers are
fully shipped, showed a hardcoded GitHub badge, and never sent git_provider
at all — a non-GitHub project could not be created without an immediate
edit. It now carries the same forge Select the edit dialog has; the edit
dialog's own 'GitLab support is planned' tooltip is corrected too.
Also: the git actions panel's hardcoded 'main' (wrong PR-eligibility and
target label for master-default and env-ladder projects) is replaced by the
project's resolved head branch; acceptance criteria become editable in the
edit-task dialog; three feature flags get their missing descriptions; and
three forms swap raw-UUID text inputs for the existing Task/Agent selectors.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The per-agent override grid rendered a hand-maintained AGENT_GROUPS literal
that had drifted: ux-dev-2 and all four PR reviewers were absent, so their
model overrides could not be viewed or edited at all (the backend accepts
any slug). The grid now derives its sections from useAgentDefinitions()
with the same team helpers the Fleet page uses, org-ranked ordering, a
loading skeleton, and group HelpTips. The static offline-fallback maps
(agent-utils, use-agents, mock-data) get the missing agents too.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Every chart hand-rolled its own k-only formatter (22596k-style ticks, left-
clipped y labels), used recharts' default white tooltip (invisible header on
the dark theme), and two charts pinned a numeric XAxis interval that collapses
short series to a single tick. The agent Token Activity chart had no axes at
all and blanked its tooltip date on purpose.
One shared formatTokens/formatBucket (lib/format.ts) and one shared themed
tooltip style (components/charts/chart-tooltip.tsx) now feed all 10 charts;
axis widths/margins sized to the labels; preserveStartEnd tick intervals.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(tg): Mini App V6 — premium overhaul (design system, Chat parity, Metrics drilldown, CEO verbs)
Design system: native type with tabular-numeral heroes (mono demoted to
the wordmark), borderless elevated cards, floating dock, Telegram
window-chrome painting via the theme bridge; Inbox moves behind a header
bell with humanized notifications (UUIDs resolve to task names).
Chat: honest Mine/Fleet split — participant-scoped CEO threads with real
unread counts and mark-read, watched fleet threads with reply-as-CEO on
task-linked conversations (watch-only otherwise), markdown transcripts,
live pulse flashes, and a pinned Secretary live chat on the panel's SSE
session runtime.
Metrics: new tab with period-segmented spend hero, by-agent/team/model
breakdowns, delivery + efficiency health, and a per-agent drilldown over
usage time-series (agent_slug) + member scorecard.
Board: tg-native grouped pipeline replacing the MobileTaskBoard wrapper;
task sheet gains the CEO decide verbs (approve / request changes /
unblock).
Security: /api/dashboard router now require_panel_token-gated at router
level (mirrors /api/usage), closing unauthenticated metrics exposure.
* fix(tg): restore Share Tech Mono brand voice, Phosphor icon set, borderless avatars
The mono returns as the numeral/brand voice (.tg-display — heroes, stat
values, wordmark) while labels stay native sentence case. The hand-drawn
duotone glyphs and lucide feature icons are replaced by Phosphor (MIT):
duotone at rest via an IconContext at the shell, filled weight on the
dock's active tab; row glyph maps (board statuses, inbox kinds, approval
kinds, quick actions) all move over. Team avatar tiles drop their borders
— tint-only squircles.
* fix(tg): fleet avatar strip breathes — spaced tiles instead of overlap
* polish(tg): taste-skill audit pass — em-dash purge, one icon family, separator rationing
Applied the design-taste audit against the cockpit: every em-dash in
visible UI copy rewritten (periods/commas/colons), the remaining lucide
chrome (carets, arrows, send, close, spinners) moved to Phosphor so the
tg tree ships one icon family (send is the native paper-plane, carets
bold), the hand-rolled chevron SVG deleted, and metadata lines rationed
to a single middle-dot separator.
* polish(tg): pipeline chip strip scrolls without a visible scrollbar
* fix(tests): metrics observability fixture uses a relative timestamp
The hardcoded _T0 (2026-06-20) aged out of the service's 30-day window
exactly 30 days later, detonating the suite on every branch. Two days
back from now() stays inside every window (30d metrics, 7d scorecards)
permanently.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* [68e1e4db] feat(video): author release-0.26.0 marketing clip in the panel-demo kit register
* [68e1e4db] fix(video): extend pk-frame clip window to full length to kill black-gap render bug
* [68e1e4db] docs(video): document release-0.26.0 composition structure in motion/README
Added a comprehensive "Release-specific example: release-0.26.0" section documenting the v0.26.0 marketing video composition: a 40-second panel-demo kit clip showcasing four key features (orchestrator security hardening, three-forge support, Telegram Mini App V4, active guard mode) with choreographed panel elements, stats overlay, and dual-format captions. Includes preview/test instructions, props.js shape, captions schema with verified character counts, and smoke-test invariants matching the pattern established by release-0.25.0.
---------
Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Documenter <ux-doc@roboco.tech>
A source=video authoring task reaches awaiting_ceo_approval with no MP4
yet — rendering only happens after it completes — so the CEO had nothing
to review. Two CEO-gated routes now serve the request_render preview
frames: GET /video/preview-frames/{task_id} lists them per orientation
(parsed from the self-describing .previews/{task8}/{orientation}/ filenames
rather than the render_preview marker, which only holds the last call's
single orientation), and .../{orientation}/{filename} streams a frame's
PNG behind the existing path-confinement guard. The task-detail Overview
gains a Video preview card — a 9:16/1:1 toggle + prev/next/scrubber frame
stepper with composition id, duration, and a dirty badge — shown for a
video task with preview frames or awaiting CEO approval.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
draft_release_post was fed highlights=list(report.change_summary) — raw
per-commit subjects — so the announcement model parroted the top commit
('RoboCo API v0.26.0 is out: docs: curate the full Unreleased body #601').
New pure changelog_highlights() extracts the bold feature leads from the
curated release entry (report.drafted_changelog), stripping PR refs and
trailing periods; approve() prefers those and falls back to change_summary
only when the changelog yields nothing. The video captions were already
good because the authoring dev read the changelog — same source now.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
With the guard active on the NAS, a documenter's journal-entry POST body
tripped a WAF signature and the guard banned its docker-bridge IP
(172.18.0.7) — after which EVERY gateway verb from that agent (dm,
i_am_idle, claim_review) was blocked by ip_security, wedging the agent
into a respawn loop. Confirmed live: roboco:guard:banned_ips:172.18.0.7
in redis with passive=False. The guard's threat-ban targets the external
attack surface arriving via nginx; internal HMAC-authenticated agents
reach the orchestrator DIRECTLY on the docker bridge and must not be
subject to it. build_security_config now sets whitelist to the RFC1918 +
loopback ranges. External traffic keeps its real client IP (XFF, trusted-
proxy depth 1 — un-spoofable into a private range), so the WAF still
fires on genuine attackers; the middleware tests model that with a
public TEST-NET-3 IP.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The release engine's bell notification for a just-originated proposal
inserted through a fresh session while the proposal task sat uncommitted
in the engine's own transaction — the related_task_id FK rejected the
row and the ping was silently lost (caught live in the postgres log; the
DB-free Telegram DM still went out). send_ack_notification now accepts
db_session, forwarded to _create_notification so the insert joins the
caller's transaction, and the release engine passes its session. The
other five callers pass no task_id or reference committed tasks.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The release proposal's drafted changelog carried only the five entries
PRs had remembered to write, and its own gaps list flagged ~53 missing
items — the entire feature story (Mini App V4+V5, the forge program,
Telegram V2/V3, Agents hub, Workstation, video craft, the panel-perf and
hermetic-test-suite work). The Unreleased body now tells the whole
release: Security amended for #599's calibration (the stale
placeholder-trips-the-guard warning was inverted by the fix) + adm-zip
and registry-auth entries; Added carries the thirteen feature programs;
Changed covers perf/agnosticism/test-hermeticity/docs; Fixed absorbs the
remaining baskets. The readiness drafter prefers this curated body, so
the re-originated proposal ships it verbatim.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The app-level agent-roster sync fires /api/agents on every surface; on
/tg it races the initData sign-in, takes the cloud-auth 401, and the
interceptor bounced the Telegram webview to the password /login page it
cannot complete — hijacking the cockpit into the dashboard. The Mini App
owns its auth UX (initData sign-in + its own wall), so the redirect now
exempts /tg via an exported, tested path predicate.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The 2026-07-19 outage root cause: enforce_https keyed off
environment==production, but nginx is the single entry point — the app
only ever sees proxy-HTTP, and the production NAS terminates no TLS at
all — so the moment the guard went active, https_enforcement blocked
the entire request stream. It is now hardcoded off at the guard-config
level (TLS and http->https redirects belong to nginx, not the app).
Same calibration pass for the two prose validators the flip armed: the
secret-exfil key pattern requires a real b64-shaped value so the
documented placeholder lines can't block, and the injection override
pattern requires the second-person 'your' so neutral engineering prose
about the guard subsystem passes.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* perf(panel): kanban virtualization + row memoization + scorecards batch endpoint
The audit's remaining phases: the kanban card lists render through
@tanstack/react-virtual windows (columns are the dnd drop targets, cards
only drag — no sortable conflict) with memoized columns/cards and a
stabilized handleAction; the task table's desktop row and mobile card are
extracted and memoized (the table itself already client-paginates to 100).
Backend: the per-member scorecard N+1 (~20 requests x 3 queries per poll)
collapses into GET /dashboard/metrics/members backed by
get_all_member_scorecards with grouped rollup/overlay SQL shared with the
single-agent path.
* test(metrics): shared-DB-safe scorecard tests — unique seeds, delta assertions
The two new batch-scorecard tests assumed a private DB: fixed ceo/system
slugs collided with other tests' seeds (ix_agents_slug) and a global
exactly-one CEO lookup + exact roster count broke in the one-process
suite. Unique slugs, subset/disjoint assertions, dead count constant
dropped.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
An operator .env with deploy-flavored values skewed local runs three
ways: environment=production flipped the GHSA-4f7g fail-closed auth gate
(header-trust tests 401), an armed cloud_auth 401'd every agent request,
an armed guard rate-limited the suite, and a missing encryption key
failed every crypto path. The autouse fixture pins environment,
encryption_key (per-process Fernet), cloud_auth_enabled, and
guard_enabled to schema defaults; suites exercising those postures arm
them per-test. Full suite locally: 13641 passed, 0 failed — was 174
failures under an armed .env.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Slice updates cover the forge package + provider parity, Telegram cockpit
V4/V5 surfaces, panel perf (virtualized kanban, scorecards batch route),
close_task_pr_best_effort + dependents guard, NO_COMMS_ROLES + CEO A2A
refusal at conversation creation, notification ack-TTL, docs-site config,
pr_labels base-branch signature, taste-skill prompt layers, prompter
history exclusions, live forge e2e suites, and migrations 075/076.
_complete_map.md is regenerated from the slices — the committed concat
had drifted ~8KB behind its own sources.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Seventeen-page sweep of the agent-facing KB against shipped behavior:
required covers_parent_criteria and per-AC criteria_verified reach the
QA/PM/task-tools pages (the QA docs also named non-callable pass_review/
fail_review — the MCP tools are pass/fail); collision_context lands in the
QA/gate/planning evidence docs; the possibilities matrix gets its own
architecture page + config entry; the auditor page gains its missing
waive_finding and playbook-curation verbs; git-pr-types.md is rewritten
off the long-dead is_root_pr model; PR/workspace/git-error pages stop
assuming GitHub (forge-agnostic + env-ladder semantics).
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(tg): Mini App V5 — brand typography, icon depth, motion, detail sheets
Share Tech Mono (the vendored motion-brand face) becomes the cockpit's
display voice via next/font/local scoped to #tg-shell; icon tiles, circle
actions, and avatars get gradient/ring depth; a dependency-free motion
vocabulary lands (spend count-up, tab rise-in, staggered sections,
sparkline draw-in, sheet slide-up with native BackButton dismiss); the
Board tab gains a tap-through task sheet (ACs, open findings, PR link),
Today's fleet opens a full-roster sheet, and Board/Inbox join the
/tg?demo=1 fixtures.
* feat(tg): custom RoboCo icon set + operations ring
The cockpit stops using stock lucide on its hero surfaces: a hand-drawn
duotone icon set (speedometer, seal, bell, kanban, brand-cursor bubble,
rocket, double-check, broom, robot head) covers the tab bar and the Today
ring. The ring itself stops duplicating the tab bar and becomes real
operations: Ship deep-focuses the release proposal in Approvals, Ack all
bulk-acknowledges pending notifications, Sweep runs the stale-branch
cleanup across every git-configured project behind a confirm sheet, and
Fleet opens the roster.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* fix(security): guard goes active; CEO A2A respects no-comms roles; ack notifications expire
ROBOCO_GUARD_PASSIVE_MODE defaults to false in both compose files — the
deferred post-calibration flip; fail_secure stays off and the env override
remains the rollback. can_a2a_direct no longer short-circuits the CEO past
the no-comms set (auditor/pr_reviewer/prompter/secretary), now canonical
in foundation.policy.communications.NO_COMMS_ROLES and shared with the
content-actions gate; the A2A service refuses at conversation creation
instead of silently suppressing the wake. Ack-required notifications get
expires_at stamped from ROBOCO_NOTIFICATION_ACK_TTL_HOURS (default 48,
0 disables), so the re-escalation sweeper's expires_at query matches rows
for the first time.
* refactor(notification): extract _ack_and_expiry — xenon rank back under B
The expires_at stamping pushed _create_notification_with_session to
rank C; the requires_ack + expiry derivation moves into a helper with
the same semantics and comments.
* test(conftest): dispose the global DB engine after every test
Production code reaching get_db_context()/get_engine() lazily creates the
process-global engine bound to the current event loop; with per-test
function-scoped loops, any later test touching the global path inherits a
dead-loop engine and dies with 'Future attached to a different loop' —
the order-dependent class that has been wandering the suite (cloud_auth
login, metrics, tasks-routes, full-lifecycle) whenever collection order
shifts. An autouse fixture now close_db()s after every test, keeping the
global path loop-local; no-op when untouched.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
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>
Task cancellation left the task's PR open on the forge forever: cancel()
now best-effort-closes the recorded PR for the task and its cascaded
descendants (close_task_pr_best_effort resolves owner/repo off git_url —
no clone needed; never raises into the cancel). The bulk stale-branch
sweep gains a dependents guard: a branch still recorded by a non-terminal
task, or serving as a live child's resolve_parent_branch base, is excluded
from the candidate window — mirroring the existing env-ladder-rung skip.
Scoped to the sweep, not delete_task_branch, so the BFS cascade-cancel
can't falsely block a parent's branch on its own about-to-cancel child.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The history-digest pipeline (PR #297) injected past-task data but nothing
told the intake agent what to do with it: prompter.md now carries an
explicit don't-re-propose section (cite duplicates by short id, reference
precedent in notes, let history inform depends_on sequencing), and
list_recent_for_project excludes cancelled tasks so dead work can't pose
as precedent.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>