* 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>
The deferred half of the Leonxlnx/taste-skill (MIT) adoption: an
industrial-brutalist / minimalist-editorial / premium-agency aesthetic
vocabulary keyed onto the existing design-bar dials for both FE and UX/UI
team prompts, and a ux_ui-only image-direction section (composition,
palette discipline, anti-slop imagery, mockup conventions) with a pointer
from frontend.md. Layer tests pin presence and team scoping.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(tg): premium Today — spend hero, trend, quick actions, fleet avatars
The cockpit home stops being flat cards and becomes a real app surface:
- Spend HERO: the day's cost at 40px with a signed delta-vs-yesterday
chip and a live 7-day amber area sparkline (hand-rolled inline SVG, no
charting lib in the Mini App bundle).
- Quick-action ring: circular Approve (amber + needs-you badge) / Board /
Inbox / Chat, the wallet-style primary-verb row.
- Needs-you as a rich amber gradient banner (top items + draft chips)
instead of a plain section.
- Fleet as live avatar tokens (stable per-name hue, pulse dot) over the
working list.
- "Shipped this week" day-bars (today emphasized) + week total.
Backend: /telegram/today gains spend.series (7-day cost) + delta_pct and
a velocity series (per-day completed tasks) — two cheap grouped-by-day
queries, same DB-only ethos, degrading to zeros on error.
* feat(tg): color-code approval rows by kind
TgRowIcon gains a tone prop; the approvals list tints each tile per kind
(amber Release / sky X post / violet Video / emerald Roadmap) so a mixed
queue reads as color-coded instead of a monochrome column.
* feat(tg): sender/peer avatars on Inbox + Chat
Inbox notifications and Chat conversation rows adopt the fleet-avatar
language: a per-name-hued initials token leads each card, unread inbox
items carry a subtle primary tint, and both cards move to the rounded-2xl
surface — so every tab now shares one visual system. Board keeps the
shared MobileTaskBoard (already grouped/pill-styled, and reused outside
the cockpit).
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
GitLab's create_org_repo (services/forge/gitlab.py) replaces the Phase-3
synthetic 501 with a real implementation: resolves org (a group's full
path, subgroups included) to a numeric namespace id via GET
/groups/{path}, falling back to the token's own namespace on a 404
(personal-namespace projects); POSTs /projects with the
name/path/description/visibility/initialize_with_readme payload
(visibility mapped private->"private"/"internal"); reshapes the 201
onto the GitHub fields callers read (full_name/clone_url/html_url) and
GitLab's duplicate-path 400 "has already been taken" onto GitHub's 422
shape, text preserved. Gitea's create_org_repo was already real but
untested — added transport-level coverage.
GitHubProvisioningService (services/github_provisioning.py) is now
provider-aware: ROBOCO_PROVISIONING_PROVIDER (github default / gitlab /
gitea) and ROBOCO_PROVISIONING_HOST (self-hosted instance, required for
gitlab/gitea or the service stays disabled exactly like a missing
token/org) pick the target forge; the class/factory names stay
GitHub-flavored for backward compatibility (pitch.py and existing
imports untouched). A shared _is_already_exists() helper recognizes
GitHub's "already exists" (422), Gitea's (409/422 "already exists"),
and GitLab's reshaped "has already been taken" (422). The existing-repo
re-fetch now builds a provider-aware RepoRef (GitLab packs org/name
into the owner field; GitHub/Gitea keep the owner,repo pair). Default
behavior (no new env set) is byte-for-byte the Phase-1 GitHub path,
pinned by a regression test.
Gates: ruff format/check clean, mypy roboco/+tests/ clean (1235 files),
xenon A/A/B clean, targeted suite (forge + provisioning + pitch) 79/79
green.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Caught live on the NAS: every cloud-auth login failed with
422 {"loc": ["query", "db"]} regardless of credentials. Root cause:
auth/manager.py used postponed annotations with a TYPE_CHECKING-only
AsyncSession import, so FastAPI could not resolve get_user_db's
Annotated[AsyncSession, Depends(get_db)] at runtime and silently
demoted `db` to a required query parameter.
The module now evaluates annotations eagerly (no future-annotations,
runtime imports) so an unresolvable annotation is loud instead of a
silent contract change. Regression test mounts the REAL login router —
no dependency overrides, which is exactly why the existing suite never
caught this — and asserts wrong credentials yield 400
LOGIN_BAD_CREDENTIALS, never a 422.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(forge): Phase 2 — Gitea provider, per-call routing, host registry
Gitea support lands behind the Phase-1 seam:
- GiteaProvider (services/forge/gitea.py): Gitea v1 transport addressed
by instance host (api base from the project's git_url). Where Gitea's
wire contract diverges from GitHub's, the provider adapts responses
back into the shapes GitService already classifies (ShapedResponse):
`token` auth scheme, duplicate-PR 409→422 with the "already exists"
text GitService keys on, commit statuses reshaped into check_runs /
workflow_runs envelopes, APPROVE→APPROVED review mapping, Do-keyed
POST merge, merge-method repo keys, label-color '#' prefix,
client-side head/base PR filtering. Deliberate postures per the spec:
zero-workflows fail-open (statuses-free repo → no_ci_configured) and
merge_branch as a shaped 501 (env-sync cascade lands on missing_ref;
the shared local-git fallback is Phase-2.1).
- ForgeRouter (services/forge/router.py): GitService._forge now routes
per call from RepoRef.host — every existing call site unchanged in
shape. RepoRef gains an optional host; _parse_git_url returns the
host-stamped ref and it is threaded through GitService/release
executor instead of being rebuilt from strings (helpers re-signatured
to take RepoRef).
- Host registry (services/forge/registry.py): in-memory host→provider
map, self-healing — ProjectService.get/get_by_slug re-register on
every read; provider_for resolves gitea projects by git_url host.
- Registration validation now accepts git_provider="gitea"; GitLab
remains recognized-but-rejected. Panel: the read-only Forge badge
becomes a real picker (Auto-detect / GitHub-GHE / Gitea / GitLab
disabled).
Plain git (clone/fetch/push) needs no changes — the Basic-auth
extraheader works on Gitea unchanged. Gates: mypy 392 files, xenon A,
full unit suite 6356 green, integration suite 2257 green.
* feat(forge): live-Gitea contract suite + scheme support + slash-safe refs
Hardening from running the provider against a real dockerized Gitea
1.22.6 (the spec's Phase-2 contract suite, now committed as the
env-gated tests/e2e_smoke/test_gitea_live.py — self-seeding: creates its
own repo, pushes real commits, and drives PR open → duplicate reshape →
list/filter → diff → review → labels → commit-status CI reshapes →
squash merge → branch delete → release, plus a live verification of the
x-access-token Basic-auth git-CLI claim).
Two real findings fixed:
- Branch refs weren't URL-encoded — every RoboCo branch carries slashes
(feature/backend/...), and Gitea's router 404s on the extra path
segments. list_ci_runs + delete_branch_ref now quote the ref
(regression-pinned in the unit suite).
- The API base hardcoded https; a LAN instance serving plain http is a
real deployment shape. GiteaProvider gains a scheme (recorded per host
by the registry from the project's git_url).
ShapedResponse moves to forge/shaping.py (shared by the upcoming GitLab
transport, which needs its text override for diff reassembly).
* feat(forge): Phase 3 GitLab provider + Phase 2.1 local-merge fallback
GitLabProvider (services/forge/gitlab.py): GitLab v4 transport addressed
by host+scheme, subgroup-safe (the MR project path packs into
RepoRef.owner, URL-encoded per call). Adapters translate MR semantics
into the GitHub shapes GitService classifies: iid→number,
source/target_branch→head/base with a merged bool, per-file diffs
reassembled into unified-diff text (ShapedResponse text override,
3-page cap), approve-vs-note review routing (GitLab has no
request-changes verb), pipelines/statuses reshaped into
workflow_runs/check_runs, merge-method repo-key mapping, duplicate-MR
409→422. Reviewer mirroring is skipped (needs numeric ids RoboCo
doesn't store); provisioning stays Phase 4. gitlab.com now auto-detects
at registration like github.com; self-hosted GitLab sets the provider
explicitly (panel picker enabled).
Phase 2.1: neither Gitea nor GitLab has GitHub's server-side merges
API — their merge_branch returns a shaped 501 and
GitService.sync_env_branch now runs the shared local-git fallback
(_local_merge_branch: throwaway clone → ancestor check → merge → push;
a conflict aborts with the remote untouched; same status vocabulary as
the merges-API path).
Also aligns the whole tree with the full gate's tests/-scoped mypy
(provider-test responder typing, e2e_smoke's stale owner/repo shapes).
Gates: mypy 1229 files clean, xenon A, unit suite 6393 green, forge
suites 85 green, panel typecheck/lint clean.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
The full-suite CI run executes every test tree in one process against one
database, so other suites' committed rows are visible to the Today-brief
queries and the "empty company" / absolute-count assertions failed
(assert 4 == 0). The suite now snapshots a pre-seed baseline and asserts
deltas, seeds at priority 0 so its rows stay inside the brief's item
caps, and uses a unique agent slug (the fixed be-dev-1 could collide
with leaked rows). Verified by co-running with the known-polluting
integration suites in one process.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
CI runs `mypy roboco/ tests/`; the bridge/cockpit suites were only gated
against `mypy roboco/` locally. Real credentials dataclass instead of
SimpleNamespace, AsyncMock casts where mocks sit behind typed fields, a
return annotation on the fake stream, a None-guard on the consumer task,
and a fresh registry lookup where mypy's literal narrowing read an
assert as always-false.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(tg): P0 — dev mock bridge + Telegram-native foundations
Mini App V4 phase 0. The (tg) shell gains the groundwork every later
phase builds on:
- Dev mock bridge: outside Telegram, a development build falls back to a
no-op WebApp object and skips the webapp-auth POST (the regular panel
session cookie authorizes API calls), so the cockpit is workable in a
plain browser. Production keeps the "Open from Telegram" wall.
- Telegram theme adoption: themeParams map onto the shadcn CSS variables
scoped to #tg-shell (desktop dashboard untouched), colorScheme drives
the dark class, themeChanged re-applies live. Non-hex values are
dropped at the trust boundary.
- Viewport/swipe correctness: shell height rides Telegram's own
--tg-viewport-stable-height (100dvh fallback), vertical swipe-to-close
disabled so list scrolling can't dismiss the app.
- Native chrome bindings: TgWebAppProvider context plus useMainButton /
useBackButton declarative hooks and a null-safe haptics helper —
consumers never touch window.Telegram directly.
* feat(tg): P1 — Today home tab + one-round-trip /telegram/today brief
Mini App V4 phase 1: the cockpit now opens on a "Today" brief answering
"does anything need me?" in one glance.
Backend: GET /api/telegram/today (CEO-gated, rate-limited) returns the
whole brief in one round trip via the new TgCockpitService — needs-you
items (awaiting-CEO + blocked tasks capped for a phone screen, held-draft
counts across release/X/video/roadmap queues), a fleet snapshot with
per-agent current-task titles, today's spend from the day rollup
(degrading to zeros on a usage hiccup, mirroring the CEO overview), and
ship state. Deliberately DB-only: no live GitHub calls, no readiness
snapshot (that path clones), no orchestrator singleton — the CI
red/green proxy is the set of open ci_watch fix tasks.
Panel: TgTodayTab is the new default tab (Gauge icon) — needs-you rows
and draft chips deep-link into the tab that acts on them (with a haptic
tap), fleet/spend/ship render as dense cards, 45s refetch until the P3
WebSocket wiring lands.
* fix(tg): dev mock engages when the CDN bridge loads outside Telegram
Live browser smoke caught it: a bare tab still loads telegram-web-app.js,
so window.Telegram.WebApp EXISTS outside Telegram — just with empty
initData. The dev fallback keyed on a null bridge only, so a dev browser
went down the real-auth path and posted empty initData instead of
mounting the mock. The fallback now treats bridge-with-no-initData the
same as no bridge (a real Telegram launch always carries initData);
production behavior is unchanged.
* feat(tg): P2 — native approvals card stack
Mini App V4 phase 2: the Approvals tab stops stacking the four desktop
queue cards and becomes a phone-native flow — one normalized list across
release proposal / X drafts / video drafts / proposed roadmap items, and
a full-context detail per item:
- Release: version/bump/gate badges, changelog draft, gaps, migration
notes, in-flight + failed-execute banners; approve runs the fail-closed
executor, reject requires a substantive change request (10 chars).
- X: editable body with the live 280 counter, replied-to mention quoted;
approve sends the edited body only when actually edited.
- Video: cut-toggled player (blob-fetched through the authed client — a
bare <video src> would 401), per-platform caption edits with 280/2200
counters; approve sends only checked-in edits.
- Roadmap: the PO's full pitch (description, rationale, ACs); approve
materializes into the backlog per item.
The detail's primary action rides Telegram's native MainButton and back
navigation rides the BackButton, with visible fallbacks outside Telegram
(dev mock, old clients). Haptics fire on outcomes. An acted-on item
vanishes from the refetched queue, popping back to the list by
construction. A failed queue source is surfaced ("list may be
incomplete" / "couldn't load") instead of masquerading as an empty
queue — caught live in the browser smoke.
* feat(tg): dev demo mode — /tg?demo=1 renders canned cockpit data
Development-only: with the flag param present, the Today brief and the
four approval queues resolve typed fixtures (dynamically imported, so
production bundles never carry them) instead of hitting the backend —
the cockpit is fully browsable with zero stack running. Mutations still
go to the real API and fail loudly; it's a showroom, not a simulator.
* feat(tg): P3 — cockpit rides /ws/system live
Chat adopts the desktop A2A invalidate-on-frame idiom over the shared
ref-counted /ws/system socket: every a2a.message frame refreshes the
conversation list and the affected thread, missed-frame gaps are healed
by a reconnect refetch, and the 10s thread poll turns off entirely while
the socket is up (it remains the fallback). The Today brief refreshes on
each USAGE_SNAPSHOT push so the spend line tracks the sweeper live, with
the 45s poll as the socket-down fallback. No new sockets, no backend
changes — the WS gate already accepts the cloud-auth session cookie.
* feat(tg): P4 — deterministic bot command tier + self-syncing menu
Mini App V4 phase 4 (deterministic half): three new bot commands beside
/status /queue /task —
- /agents: who's mid-task right now, from the same TgCockpitService
fleet snapshot the Today brief renders (now public `fleet()`).
- /usage: today's spend from the day rollup.
- /blocked: awaiting-you + blocked tasks, deep-linked into the panel,
capped per section, titles HTML-escaped.
BOT_COMMANDS is the single registry driving /help AND a once-per-process
Bot API setMyCommands sync on the first poll cycle (new client method,
best-effort), so the Telegram command menu can never drift from what the
code implements. The interactive tier (/secretary, /newtask riding a
live Intake interview in-thread) is specced but not in this commit.
* feat(tg): direction-C styling pass — Telegram palette, RoboCo voice
The cockpit stops wearing default-shadcn and gets its own visual
language on top of the P0 themeParams bridge (colors stay CSS-variable
driven, so inside Telegram everything still adopts the user's theme):
- Shared primitives (components/tg/ui.tsx): TgSection grouped cards with
tracked micro-label headers, TgRow list rows (44px targets, press
feedback, 1/2-line clamp), TgRowIcon glyph tiles, TgStat tabular-nums
figures. Every tab composes the same three, so density and rhythm are
identical across the surface.
- Shell renders a centered 430px column (sm:border-x) — the phone UI no
longer stretches across a desktop dev browser.
- Tab bar: tighter type, active stroke-weight shift, backdrop blur.
- Today: needs-you count badge, divided task rows with inline blocked
marker, fleet as mono-named rows, spend/ship as stat tiles.
- Approvals rows as icon-tile cards; detail header gains the kind glyph.
- Inbox/Chat rows aligned to the same card language.
* feat(tg): P5 — /secretary and /newtask live-chat bridges
The bot's interactive tier: both commands bridge the CEO's Telegram chat
into the same in-process runtimes the panel drives — the persistent
Secretary container and the scoped Intake interview.
There is no synchronous send→reply seam (replies land on the session's
single-consumer relay queue), so each bridged session runs one long-lived
consumer task (roboco/services/telegram_bridge.py) that drains
PrompterLiveRegistry.stream and pushes one Telegram message per completed
turn. While a session is live, plain chat text IS the conversation;
/end closes it.
/newtask resolves the intake scope (single project auto-picked, multiple
offered as a tap-to-pick keyboard holding the initial text), and the
interview happens in-thread. A draft proposal renders as a card with
Send-to-Board / Discard buttons: confirm routes through the normal
board-review path (PrompterService.confirm_live_draft, route=board) and
PARKS the session — board feedback later streams straight back into the
same thread, closing the redraft loop from the phone. MegaTask batches
still confirm in the panel only.
The consumer's open stream arms the registry's 60s keepalive, so the
bridge runs its own idle TTL (same setting, parked sessions exempt).
State is per-process in-memory by design (the _PENDING_REPLIES posture);
intake/secretary containers are process-wide singletons, so a bridged
session preempts a live panel session of the same kind by construction.
* feat(tg): cockpit skin — RoboCo dark deck with a constant amber accent
The cockpit no longer inherits the dashboard's white default outside
Telegram: #tg-shell carries its own standing skin (deep slate surfaces,
amber primary) so the Mini App looks like RoboCo everywhere. Inside
Telegram the themeParams bridge now overrides SURFACE tokens only —
background/card/text/hint/border repaint to the user's Telegram theme
while --primary/--ring stay RoboCo amber: Telegram's surfaces, RoboCo's
voice. Demo fixtures also rewritten to neutral content (they previously
depicted unbuilt forge work and already-shipped roadmap items as live).
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
_sync_target_branch ran a bare `git pull` after checkout; on a workspace
clone whose local target branch diverged from origin (inevitable once
remote history is rewritten) modern git fatals with "Need to specify how
to reconcile divergent branches". On the CEO approve-and-merge path the
GitHub merge had already landed, so the failure surfaced as a spurious
400 "Merge failed" and every retry re-hit the same wedged clone.
Sync is now fetch + `reset --hard origin/<branch>` — remote-authoritative,
matching the helper's intent and self-healing the divergence for all
call sites (CEO merge, PM 409-retry sync, post-merge best-effort sync).
Item B+C of the video/X per-project targeting spec, plus the
company_goals.company_name field they depend on (migration 075).
- CompanyGoalsService.resolve_product_name is the single fallback chain
(project name -> charter company_name -> RoboCo); XEngine and
VideoEngine both call it and their prompt builders are pure functions
taking product_name — release posts/videos stop hardcoding RoboCo.
- The X and video queue responses carry project_slug/project_name via one
shared unloaded-guard helper (api/schemas/project_fields.py); both
panel queues render a shared ProjectBadge so multi-project drafts are
tellable apart.
- Business -> Goals editor gains the company-name input.
- Fixes a pre-existing test-isolation leak: the company-goals routes test
commits the charter singleton into the session-scoped test DB and
polluted later suites; it now deletes the row on teardown.
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
* feat(forge): Phase 0 — git_provider column + registration-time forge validation
Pointing a project at a GitLab/Gitea git_url used to fail silently, several
steps deep, at first PR. New pure policy module (foundation/policy/forge.py)
detects the provider from the git_url host and validates at the
ProjectService create/update chokepoint: github auto-detects and
auto-stamps, explicit git_provider=github is the GitHub Enterprise escape
hatch, gitlab/gitea are recognized-but-not-yet-supported, unknown hosts get
a loud rejection with guidance. An update changing git_url does NOT inherit
a stored auto-stamped provider (restating the override is required), so a
host swap can't smuggle the escape hatch past validation. Migration 075
adds the nullable projects.git_provider column; the panel project dialogs
show the detected forge. Phase 0 of the forge-providers spec.
* fix(panel): mock-mode forge detection extracts the real host
CodeQL js/incomplete-url-substring-sanitization: the substring check
matched github.com anywhere in the URL. Extract the hostname (URL parse
or scp-form regex, mirroring forge.py) and require an exact match.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
GHSA-4f7g-w95g-5q2c (CVSS 9.8) — the default registry deploy ran in
header-trust mode: with ROBOCO_AGENT_AUTH_REQUIRED unset and cloud auth
off, require_panel_token / _check_agent_auth_token returned without
verifying a credential, so any client reaching the API could write
settings and claim X-Agent-Role: ceo with no token. Binding :8000 to
loopback (c4053d5f) closed the direct path but not nginx :3000, which
proxies /api/ to the orchestrator and passes client X-Agent-* through.
Root cause: header-trust is the default even in production. _auth_required
now fails closed when settings.environment == production (the registry
compose already declares it) — an explicit false still opts out for a
trusted private network. The registry compose arms auth by default and
requires ROBOCO_PANEL_AGENT_TOKEN so nginx injects a valid CEO token and
the panel keeps working. The CEO's NAS deploy is unaffected: it runs
cloud auth, which already enforced tokens on every role.
The env-ladder change makes a parentless root consult the project head
rung before string derivation; on a bare AsyncMock the resolver probe
auto-materializes and returns a truthy mock. Stub it to None so the test
exercises the no-project string-derivation fallback its name promises,
matching test_merge_chain's pattern.
draft_release_post / draft_release_video accept a project_id and the
release-proposal approve hooks pass the proposal task's own project;
the spotlight companion video forwards the spotlight draft's project
into open_video_task — previously it always authored against the
deployment-anchor project's motion/ tree regardless of which project
the spotlight was about. Omitted project_id keeps the anchor-project
fallback, so single-project deployments are unchanged.
_pyproject_version read pyproject.toml with no error handling, so a
non-Python project failed the release-manager cycle every interval
forever. _project_version now probes pyproject.toml, package.json,
Cargo.toml, then a bare VERSION file; missing or unparseable manifests
are skipped and no manifest degrades to an empty version.
A parentless root's PR base / merge target now resolves through
resolve_parent_branch to the project's panel-configured head rung —
submit_root passed a hardcoded 'master', which on a main-default repo made
_ensure_base_on_remote silently create a spurious master branch and land
the assembled root PR there. Literal master survives only as the
no-project string-derivation fallback.
The #568 tests unpacked AsyncMock.await_args without narrowing its
Optional type and passed a SimpleNamespace where _format_task_detail
is annotated TaskTable. Narrow with the repo-standard 'assert
await_args is not None' and cast the fake task at the call sites.
* [e530aa5e] Diagnose and fix roboco-api CI failure (run 29629255153) (#561) (#562)
* [e530aa5e] fix(tests): narrow None before indexing validate_init_data() result in telegram_initdata self-check
CI run 29629255153 failed on mypy, not the historical pydantic-settings
issue (uv.lock already pins 2.14.2). The __main__ self-check block in
test_telegram_initdata.py indexed the dict[str, object] | None return
of validate_init_data() without narrowing away None first.
* [e530aa5e] docs(qa): document CI fix for mypy type narrowing in telegram_initdata test
Explains the root cause (mypy type error in __main__ block), the solution (None narrowing before indexing), and the safe pattern for future test self-checks that call functions returning optional types.
---------
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
* [0884b737] Diagnose and fix Python quality gate + e2e lifecycle smoke CI failures on PR #563 (#564) (#565)
* [0884b737] fix(tests): isolate ROBOCO_SDK_URL for scripted e2e-smoke agents
tests/e2e_smoke/harness.py already isolates ROBOCO_AGENT_TOKEN from the
host environment (the #503/#504 fix) but left ROBOCO_SDK_URL leaking
through. flow_server/do_server both default it to
http://localhost:9000 and forward every rejection there for the
per-verb circuit breaker; inside a real spawned agent container that
port is a live SDK loopback, so the breaker records genuine attempts
for the ephemeral test-agent IDs and trips circuit_open mid-test
(test_sandbox_on_demand.py::test_request_sandbox_guard_chain_over_real_api,
which deliberately causes 3 rejections in a row). Point it at a
guaranteed-refused loopback address so every environment gets the same
fail-open bypass a bare CI runner already gets by having nothing
listening on 9000 at all.
* [0884b737] docs(changelog): document e2e-smoke harness ROBOCO_SDK_URL isolation fix
Document the fix that isolates ROBOCO_SDK_URL in the ScriptedAgent harness to prevent the per-verb circuit breaker from leaking state into ephemeral test-agent identities when the e2e-smoke suite runs inside a live agent container. This ensures the suite passes consistently regardless of whether it runs on bare CI or inside a spawned agent.
---------
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
* [3b9a1771] Diagnose and fix ALL make quality + e2e-smoke stage failures on PR #563; confirm real CI green (round 3) (#566) (#567)
* [3b9a1771] fix(e2e-smoke): match real embedding dimension when seeding fake journal chunk
test_c3_deleted_journal_unindexed inserted a 4-dim placeholder vector
into chunks_journals, but the e2e stack's app lifespan eagerly creates
that table with the real settings.embedding_dimensions (1024) before
the test runs, so the insert failed with "expected 1024 dimensions,
not 4". Derive _SMOKE_DIM from settings.embedding_dimensions instead
of a hardcoded constant so the seeded vector always matches the
table's actual column width.
* [3b9a1771] docs(qa): document e2e-smoke embedding dimension fix in round 3 CI diagnosis
Recorded the root cause, solution, and pattern for the final e2e-smoke test failure found in comprehensive sandbox testing: the test seeded a 4-dim placeholder vector but the app's eager lifespan init created chunks_journals with the real 1024-dim embedding column. Updated _SMOKE_DIM to derive from settings.embedding_dimensions instead of a hardcoded constant.
---------
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
---------
Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
_classify_check_runs counted any completed cancelled check-run as failing.
The push + pull_request double-trigger leaves cancelled same-name
check-runs on the same head SHA next to the surviving run's green, so the
pr_pass gate saw permanent red on a genuinely green PR. Keep only the
newest (highest-id) run per check name before classifying.
Two layers: generated agent settings now set includeCoAuthoredBy: false
(never set anywhere before, so the CLI nudged models into appending
'Co-Authored-By: Claude ...' to commit messages), and the commit verb
strips AI-attribution lines deterministically at the chokepoint every
provider routes through.
_SWITCHBOARD_SLUGS reused is_human_only_role (spawn semantics) and dropped
the CEO before can_a2a_direct — which allows CEO -> anyone — ever ran, so
the static pair matrix had no CEO pairs and a Renzo filter emptied the
switchboard. Only prompter/secretary/system are excluded now; CEO pairs
get their own 'CEO Direct' section (matrix 70 -> 93). Every switchboard
section header is now a collapse toggle (Radix Collapsible, default open).
* 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>
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>
* 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>
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.
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>
* 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>
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>