mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
docs(release): 0.27.0 prep — changelog through Wave C, map + rag + CLAUDE.md current (#697)
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -25,6 +25,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
- **Codegen drift heals itself before push (#632).** The push path auto-regenerates generated artifacts, stages only the codegen-produced drift, and links the auto-commit to the task (stage only codegen-produced drift and link the auto-commit).
|
||||
- **One-command quickstart (#653).** `make quickstart` runs the idempotent bootstrap: a fresh `.env` from the example with the three required secrets injected using the documented formulas, registry pull + up, and a doctor-style readiness sweep grounded in real surfaces — each stage failing loud with the exact next command; the release workflow's new pull-smoke job literally pulls the registry compose against every just-published tag.
|
||||
- **Off-disk backup mirror (#645).** The backup sidecar can mirror each dump to a second disk (`ROBOCO_BACKUP_MIRROR_DIR`, env-gated), with a quarterly restore-drill doc.
|
||||
- **Panel consistency & UX wave (#694).** A global Cmd+K command palette (`components/layout/command-palette.tsx`) searches tasks/agents/projects/pages with recents + fuzzy match, wired into the header search box. The Tasks page gains top-level List\|Kanban tabs sharing the page's URL filter state, and the standalone `/kanban` route now redirects there — its sidebar entry is gone and the bottom tab bar's kanban slot swaps to Agents. A forms-consistency audit synced the create/edit project and task dialogs against their backend schemas (`docs/frontend/forms/forms-audit.md`), and a touch-target/responsiveness sweep bumped every sub-36px icon control across the panel to the accessible floor, fixed `AlertDialog` clipping at short viewport heights, and wrapped the Scorecards Members table in `ResponsiveTable`.
|
||||
- **Project settings is a full page, not a dialog.** The edit-project dialog carried ~30 fields across 7 concerns in one flat scroll with a per-tab width swap — outgrown. Project settings now live at `/projects/[id]/settings` as a card-per-concern grid (the settings page's own pattern) with per-card save and Conventions as a page-level tab at natural width; the list Edit action routes there, and a slim quick-edit dialog (name/cell/active) replaces the kitchen-sink (promote project settings to a full page).
|
||||
|
||||
### Changed
|
||||
|
||||
- **Panel dialogs converge on one shape: one disclosure primitive, DialogFooter everywhere, three dialog widths.** `collapsible-section` becomes the single sectioned-disclosure primitive — task dialogs' raw `Collapsible` and create-project's ad-hoc `showAdvanced` both move onto it; every hand-rolled dialog footer becomes `DialogFooter`; dialog widths collapse from ten ad-hoc classes to three named sizes, with deliberate outliers annotated. No behavioral change.
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -36,6 +42,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
- **Silent bug sweep across concurrency, state integrity, engine edge-cases, and panel data freshness (#638),** plus the V6 review-gap fixes (V6 review gaps — honest errors, safe secretary start, real tests) and the release-approve write committing under the release lock (commit the approve() COMPLETED write under the release lock).
|
||||
- **Panel accuracy basket.** Branch lists classify remote refs correctly and prune stale ones (#610); charts get real axes, humanized ticks, and dark-theme tooltips (#611) and days-view timeseries show dates instead of `02:00` (#622); the AI Providers mix grid derives from the live roster (#613); forms catch up with the backend (#614); and A2A transcripts fall back to polling when the socket drops (#639).
|
||||
- **Telegram Mini App responsiveness (#647).** The missing `viewport` export meant safe-area insets were zero on notched phones; scoped to the `/tg` group along with truncation fixes, contained video, and ≥36px touch targets.
|
||||
- **Intake/Secretary chat replies survive a gated CLI (#665).** The chat driver treated stream-event text deltas as the only text channel, so when the CLI's partial-message emission is server-side gated (observed live — identical binary/flags/model, zero `stream_event` lines), the reply was generated but never rendered and the CEO saw nothing. `SdkIntakeSession` now emits an `AssistantMessage`'s complete text whenever no delta arrived during the turn; streaming mode is unaffected.
|
||||
- **Telegram cockpit reported wrong numbers three ways (#666).** GLM 5.2 now gets a grounded per-token rate instead of a silent $0.00 for a full fleet day of usage; the cockpit's spend/velocity series bucket by a configurable `ROBOCO_DISPLAY_TIMEZONE` instead of UTC; and agent activity ("active: 0, working: []") is finally real — every claim path now marks the claimant `ACTIVE` with a `current_task_id`, and every release path clears it. Task budgets also became explicit-input only: the per-`TaskType` default cap table (which blocked an unbudgeted coordination root one opus planning turn in) is gone — a null `budget_usd` is genuinely uncapped.
|
||||
- **Fleet PRs authored by a GitHub App bound as `[bot]` were reviewed as external (#668).** A bound GitHub App authors fleet PRs as `<app-slug>[bot]` with `author_association=NONE`, which the inbound classifier's author heuristics read as an outsider — the org's own dev-stream PR was ingested for adversarial external-PR review. Ownership now classifies BEFORE the author check: a same-repo PR whose head branch an active task owns is never treated as external, and the ownership scope widens to every project sharing the polled project's `git_url` (a monorepo's sibling cell-projects). A deleted-fork head now fails closed to review instead of risking a silent skip.
|
||||
- **Tool-call budget caps reached no compose file (#672).** `ROBOCO_AGENT_TOOL_CALL_HALT`/`_WARN` were read by the SDK server and defined in config but wired into no compose `environment:` stanza — the code default (halt at 300) repeatedly killed and respawned a legitimate multi-file task in 300-call slices. The build composes now default to halt=600/warn=200, matching the already-patched NAS; the registry compose passes them through unset.
|
||||
- **Board wiring, revived: research key, pitch flow, auditor playbooks (#684).** `ROBOCO_RESEARCH_API_KEY`/`_PROVIDER` and `ROBOCO_PROVISIONING_TOKEN`/`_ORG` reached no compose environment stanza, so research silently ran on the NullProvider and an approved pitch always died on `ProvisioningDisabledError`; both now pass through. A proposed pitch nudges the CEO (notification + Telegram link to the Pitches tab); `auditor_triage` now surfaces the oldest pending playbook draft once anomalies are clear, closing the loop on a curation queue nothing ever pointed the Auditor at.
|
||||
- **A haiku-pinned QA/PM looped instead of reviewing (#680).** Haiku can't reliably emit the structured envelopes the lifecycle now runs on (`pass_review`'s per-AC `criteria_verified`, `delegate`'s `covers_parent_criteria`, the findings ledger) — a haiku QA claims, gets validation-rejected, idles, and respawns without ever passing a review. `ROLE_MODEL_MAP`'s qa/documenter defaults move to sonnet, the `cost_tiered` mode's `developer:low`→haiku seed retires to empty, and a structured-verb capability floor now upgrades any below-floor Anthropic assignment to sonnet at resolution regardless of source.
|
||||
- **The sequence claim bar could phantom-hold an unrelated sibling (#681).** `stamp_wave_sequence` stamps each sibling's wave from a partial, per-task view of the graph at delegate time, so two independent same-parent dev-task streams could coincidentally land on the same raw sequence number — and the old edge-agnostic bar held a wave-N sibling behind a totally unrelated wave-(N-1) sibling from a different stream. Outside MegaTask batch root-subtasks (which keep the original strict, staged-release rule), the bar now only lets a lower-sequence sibling block when it's a real dependency-graph predecessor. A held claim also returns a dedicated `sequence_held` error naming the blocker instead of a bare rejection the verb runner misdiagnosed as a concurrent-transition error.
|
||||
- **An acceptance-criteria rewrite could silently waive the whole subtree's coverage gate (#682).** Every post-create rewrite of `acceptance_criteria` (the task PATCH route, the prompter's draft/batch update paths) overwrote the criteria text without touching `acceptance_criteria_ids`, leaving them mismatched or empty — and an empty id list disabled the parent-coverage gate entirely. `create`/`update` now reconcile ids through one pure helper (text-unchanged criteria keep their id so live child/finding references never orphan); the coverage gate also self-heals an already-broken id list in place instead of silently waiving coverage.
|
||||
- **A rebase could silently discard a developer's committed-but-unpushed work (#683).** The shared rebase primitive (`sync_branch`, the PM/CEO rebase route, submit-time freshening, the merge-conflict resolver) opened with an unconditional `reset --hard origin/<branch>` — since the `commit` do-verb never pushes, a dev routinely has commits that reset would rewind past, and the follow-on force-push then republished the truncated branch as authoritative. The primitive now classifies local vs. origin post-fetch: behind/equal resets as before, strictly-ahead skips the reset and rebases from the local tip, and a genuine two-sided divergence (checked for patch-equivalence first, so a prior rebase's failed force-push self-heals on retry) refuses outright with neither side touched.
|
||||
- **An escalate/unblock round trip could oscillate a task forever (#685).** A cell PM's `escalate_up` → BLOCKED → a main PM's `unblock` → respawn → `escalate_up` again loop had no fuel limit: the restored side dispatches through a path with no respawn counter at all. `unblock()` now counts strikes task-scoped (a cheap commit-count/`revision_count`/terminal-children progress fingerprint resets the count on real forward motion); past threshold the task is blocked for a human with both agents and the cycle count named in the CEO notification, and the dispatchers actually see the block now that `blocker_resolver_type` serializes over the wire.
|
||||
- **A rejected `delegate` call named the missing field but never showed its shape (#686).** A child draft rejected for a `covers_parent_criteria` ref that doesn't resolve now gets a copy-pasteable corrected `delegate(...)` skeleton with the parent's real criteria inlined — a PM no longer loops on a rejection it can't act on. The coverage-reject path also self-heals a legacy parent whose criteria ids are empty or out of length before rendering the hint, and the plain `pause` route now authorizes the CEO tier like its sibling lifecycle routes.
|
||||
- **A finding's `file` could be prose instead of a path (#687).** A finding with `file` set to something like a PR reference validated cleanly and then doomed the panel's code-snippet fetch. `file` is now shape-gated to look like a repo-relative path (narrative belongs in `evidence`, and the remediate says so); the Findings tab groups findings into per-round collapsible sections and only attempts a snippet fetch for a path-shaped ref.
|
||||
- **A reviewer's clone could stay frozen on pre-rebase history across every review round (#690).** `diff`/`list_changed_files`/`read_file_at_branch` (and the read-only `roboco_git_log` route) kept local-ref priority on any divergence from origin, real or rewritten — a QA/PM/reviewer clone parked before a routine force-push sync stayed there forever, and QA repeatedly bounced work that had already landed. Every caller here is a reader, never the branch's own author mid-write, so origin now wins whenever it carries anything the local ref lacks.
|
||||
- **A per-task worktree was created once and never refreshed again (#692).** A worktree from a reviewer's first `claim_review`/`claim_gate_review` stayed frozen at that commit across every later respawn even as new commits landed on origin — the mechanism behind a live multi-round QA/PR-gate bounce loop. `ensure_worktree_self_heal` now refreshes an already-present (or re-added) worktree against origin at every spawn, role-aware: an author's (developer/documenter) dirty uncommitted edits are never discarded to fast-forward, and only a pure reader's diverged copy gets hard-reset.
|
||||
|
||||
## [0.26.0] - 2026-07-20
|
||||
|
||||
|
||||
+64
-25
@@ -2527,6 +2527,7 @@ The support layer of the agent gateway: pure/cheap components the Choreographer
|
||||
| hint_for_missing_progress | function | roboco/services/gateway/remediation.py:11 | Hint: make a commit before i_am_done. |
|
||||
| hint_for_missing_reflect | function | roboco/services/gateway/remediation.py:18 | Hint: call note(scope='reflect',...). |
|
||||
| hint_for_unaddressed_acceptance_criteria | function | roboco/services/gateway/remediation.py:25 | Hint: every AC needs a referencing artifact. |
|
||||
| hint_for_missing_ac_coverage | function | roboco/services/gateway/remediation.py | #686: a `delegate` child draft rejected for a `covers_parent_criteria` ref that doesn't resolve now gets a copy-pasteable corrected skeleton with the parent's REAL criteria inlined (id when present, quoted exact text otherwise — never a bare `'<id>'` placeholder), instead of a rejection that named the field but never showed its shape. The coverage-reject path self-heals an empty/out-of-length id list on the parent before rendering so the skeleton always has real references; `cell_pm.md`/`main_pm.md` also show the field on every delegation-guidance child draft now. |
|
||||
| hint_for_missing_journal_decision | function | roboco/services/gateway/remediation.py:36 | Hint: call note(scope='decision',...) before complete. |
|
||||
| hint_for_missing_journal_learning | function | roboco/services/gateway/remediation.py:43 | Hint: call note(scope='learning',...) before pass/fail. |
|
||||
| hint_for_missing_qa_notes | function | roboco/services/gateway/remediation.py:50 | Hint: qa_notes must be >=80 chars. |
|
||||
@@ -3324,9 +3325,11 @@ The AgentOrchestrator is the runtime brain of RoboCo: it owns the per-agent Dock
|
||||
| AgentOrchestrator._notification_spawn_cooled | method | roboco/runtime/orchestrator.py:4738 | Cross-tick cooldown for notification-triggered spawns (escalation/approval/audit/a2a): one spawn per (agent, notification) per `notification_spawn_cooldown_seconds`; `0` disables (legacy every-tick respawn). |
|
||||
| AgentOrchestrator._notification_spawn_over_cap | method | roboco/runtime/orchestrator.py:4780 | Hard per-(agent, notification) attempt cap (`notification_spawn_max_attempts`, default 5) — the notification-driven analogue of the PM respawn breaker; these dispatchers carry no `task_id` so that breaker never sees them. Past the cap, stop respawning and log once; the count is id-scoped and survives map pruning. |
|
||||
| AgentOrchestrator._prune_notification_spawn_maps | method | roboco/runtime/orchestrator.py:4811 | Evict `_notification_spawn_at` entries older than a cutoff so the in-memory cooldown map doesn't grow unbounded. |
|
||||
| AgentOrchestrator._notification_has_live_work | coroutine | roboco/runtime/orchestrator.py:4848 | Live-work check before spawning a notification's recipient: 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. |
|
||||
| AgentOrchestrator._notification_has_live_work | coroutine | roboco/runtime/orchestrator.py:4848 | Live-work check before spawning a notification's recipient: skip when the notification has expired, is stale past `notification_spawn_max_age_seconds` (default 6h — wedged or reloaded from before a restart), its related task is already terminal, **or (since #685) the related task is HITL-blocked** (`_is_hitl_blocked`) — closes the admin-route escalation side of the oscillation loop the same way the dev/PM dispatchers' own HITL skip does. |
|
||||
| AgentOrchestrator._is_hitl_blocked | staticmethod | roboco/runtime/orchestrator.py:13796 | `status == "blocked" and blocker_resolver_type == "human"` over the WIRE task dict; consulted by `_dev_dispatch_one`, `_dispatch_claimed_without_agent`, and `_notification_has_live_work`. Only actually fires over the wire since #685 stamped `TaskResponse.blocker_resolver_type` — before that the field never serialized, so every wire-dict HITL check silently no-op'd regardless of the in-process row. Budget-breach (#654) and the task-scoped oscillation breaker (#685) both set this same resolver+status pair, so one skip covers both trip conditions. |
|
||||
| AgentOrchestrator._dispatch_claimed_without_agent | coroutine | roboco/runtime/orchestrator.py:14559 | Re-dispatches a `claimed` task whose owning agent isn't running; the RESTORED side of an escalate/unblock round trip (no per-(agent,task) respawn counter of its own) — gated by `_is_hitl_blocked` (#685) so an oscillation-tripped or budget-blocked task stops being re-dispatched here too. |
|
||||
| AgentOrchestrator._fetch_budget_status | coroutine | roboco/runtime/orchestrator.py:8021 | `ROBOCO_TASK_BUDGETS_ENABLED` sweep helper: live-priced spend snapshot for one active task (closed sessions' `estimated_cost_usd` + open sessions priced from token snapshots). |
|
||||
| AgentOrchestrator._task_budget_breach | coroutine | roboco/runtime/orchestrator.py:8053 | `(spend, cap)` tuple when a task's own spend exceeds its `budget_usd` (falling back to the `TaskType` default via `effective_task_budget_usd`), else `None`. |
|
||||
| AgentOrchestrator._task_budget_breach | coroutine | roboco/runtime/orchestrator.py:8053 | `(spend, cap)` tuple when a task's own spend exceeds its `budget_usd` via `effective_task_budget_usd`, else `None`. **Explicit-input only since #666**: a null `budget_usd` resolves to `None` (no cap) — the earlier per-`TaskType` default table (which blocked an unbudgeted coordination root one opus planning turn in) was removed. |
|
||||
| AgentOrchestrator._handle_task_budget_breach | coroutine | roboco/runtime/orchestrator.py:8090 | BLOCKs a breaching task (HUMAN resolver, a budget marker) BEFORE the graceful stop so the ensuing unclaim no-ops and the dispatcher never respawns onto it; notifies the CEO naming both recovery steps (raise the cap or `unblock`). |
|
||||
| AgentOrchestrator._sweep_budget_exceeded | coroutine | roboco/runtime/orchestrator.py:8152 | Iterates active agents past `_check_budget_for_agent`; gated entirely by `ROBOCO_TASK_BUDGETS_ENABLED` — off, neither cap is ever consulted. |
|
||||
| AgentOrchestrator._check_budget_for_agent | coroutine | roboco/runtime/orchestrator.py:8187 | Per-agent budget check called from the sweep; resolves the active task, checks breach, and calls `_handle_task_budget_breach`. |
|
||||
@@ -3484,6 +3487,9 @@ stateDiagram-v2
|
||||
> - `3b9fd0e0` (PR #551, Telegram V2, 2026-07-17): adds `_telegram_poll_task` + `_telegram_poll_loop`/`_run_telegram_poll_cycle` (mirrors the `_x_mentions_poll_loop` shape: gated on BOTH `telegram_enabled` AND `telegram_inbound_enabled`, started in `start()`, cancelled in `stop()`) driving the new `TelegramInboundEngine` (`roboco/services/telegram_inbound.py`) — long-polls Telegram `getUpdates` and dispatches `/status` `/queue` `/task` commands plus Approve/Reject button callbacks to the SAME CEO-gated service methods the HTTP routes call.
|
||||
> - `b91229f4` (#643, "break the notification-driven respawn loop"): adds `_notification_spawn_cooled` (line 4738), `_notification_spawn_over_cap` (line 4780), `_prune_notification_spawn_maps` (line 4811), and `_notification_has_live_work` (line 4848) — both new guards gate AFTER the existing cross-tick cooldown. `_instances.__init__`/`__new__` gain `_notification_spawn_at: dict[tuple[str, str], float]` (existing cooldown map) plus a companion hard-cap counter; every notification-triggered spawn dispatcher (escalation/approval/audit/a2a) is threaded through both new checks.
|
||||
> - `7c8453e2` (#654, "per-task and per-project cost budgets"): adds `_fetch_budget_status`/`_task_budget_breach`/`_handle_task_budget_breach`/`_sweep_budget_exceeded`/`_check_budget_for_agent` (lines 8021-8214) gated by `ROBOCO_TASK_BUDGETS_ENABLED`. The claim-time project-month-spend guard (`project_budget_exceeded_guard`) lives in `docs/map/gateway-support.md`; this is the sweep-side task-budget half. The same PR bundles `1d5a8e84` (#652's re-escalation backoff, see `docs/map/notification.md`) as a prerequisite commit in its branch history.
|
||||
> - `a036c979` (#666, "cockpit data correctness", 2026-07-23) `effective_task_budget_usd` drops its per-`TaskType` default fallback — a budget-less task is now genuinely uncapped (`None`), not silently capped at a `TaskType`-keyed dollar figure that blocked an unbudgeted coordination root one opus planning turn in. `_task_budget_breach`'s description above reflects this; the `TaskType` default table + its resolver are deleted outright.
|
||||
> - `eb0dcb6e` (#685, "task-scoped oscillation breaker", 2026-07-24) A escalate_up→BLOCKED→unblock→respawn→escalate_up round trip had no fuel limit: the RESTORED side dispatches via `_dispatch_claimed_without_agent`, which consulted no respawn counter at all. `unblock()` now counts strikes task-scoped (durable in `orchestration_markers`, no migration) via a cheap progress fingerprint (commit count + `revision_count` + `terminal_children_count`); past threshold the task is BLOCKED with `blocker_resolver_type=HUMAN` (the same posture budget-breach uses) and the CEO is notified naming both agents + the cycle count. `TaskResponse.blocker_resolver_type` now actually serializes over the wire, so `_is_hitl_blocked`'s skip (already used by `_dev_dispatch_one`) now really fires for `_dispatch_claimed_without_agent` and `_notification_has_live_work` too — previously the wire dict never carried the field, so those two HITL checks silently no-op'd.
|
||||
> - `987eb09c` (#692, "role-aware worktree refresh at every spawn", 2026-07-24) `_ensure_worktree_before_spawn` now passes `can_author` (from the spawning agent's role, `foundation.identity.WORKTREE_AUTHOR_ROLES`) into `ensure_worktree_self_heal`, which refreshes an ALREADY-PRESENT per-task worktree against origin at every respawn instead of treating it as a pure venv+chown no-op — see `docs/map/workspace.md` for the full classification.
|
||||
|
||||
## Regression Risks
|
||||
|
||||
@@ -5129,7 +5135,7 @@ The Pydantic/dataclass domain surface of RoboCo — the typed contract the API,
|
||||
| `AgentInstance` | dataclass | runtime.py:70 | Running Claude Code container record + `usage_session_id` |
|
||||
| `SpawnGitContext` | dataclass | runtime.py:28 | Git context for spawn (project_slug, branch, `task_short_id` for worktree) |
|
||||
| `MODEL_MAP` | dict | runtime.py:106 | short-name→full Claude id (opus→claude-opus-4-6, sonnet→claude-sonnet-5, haiku→…) |
|
||||
| `ROLE_MODEL_MAP` | dict | runtime.py:114 | per-role default tier: developer/cell_pm/main_pm→sonnet, qa/documenter→haiku, pr_reviewer/auditor/board/ceo→opus (qa→haiku, main_pm→sonnet, pr_reviewer→opus are the cost-tuned defaults) |
|
||||
| `ROLE_MODEL_MAP` | dict | runtime.py:120 | per-role default tier: developer/qa/documenter/cell_pm/main_pm→sonnet, pr_reviewer/auditor/board/ceo/prompter/secretary→opus. **Haiku retired from every delivery-lifecycle role (#680, 2026-07-24)**: qa/documenter moved haiku→sonnet — haiku can't reliably emit the structured envelopes the lifecycle runs on (`pass_review`'s per-AC `criteria_verified`, `delegate`'s `covers_parent_criteria`, the findings ledger), so a haiku QA/PM claims, gets validation-rejected, idles, respawns, and loops without progress. A structured-verb capability floor (`_below_capability_floor`/`_floor_below_capability`, `roboco/services/llm.py`) additionally upgrades ANY below-floor Anthropic assignment to sonnet at resolution time — from a pin, a plain ROLE row, or a future map edit — so the floor can't be silently reintroduced. Non-Anthropic providers are untouched (an Anthropic-tier floor, not a provider policy). |
|
||||
| `ROLE_EFFORT_MAP` | dict | runtime.py | per-role `CLAUDE_CODE_EFFORT_LEVEL` override injected at spawn; **empty/inert by default** (opt-in per role after verifying the level moves usage) |
|
||||
| `MODEL_CATALOG` | tuple | llm_catalog.py:67 | Settings-dropdown source of truth; Anthropic entries derived from `MODEL_MAP` |
|
||||
| `PermissionLevel` | IntEnum | permissions.py:15 | CEO=0/BOARD=1/MAIN_PM=2/CELL_PM=3/CELL_MEMBER=4/AUDITOR=99 |
|
||||
@@ -6565,7 +6571,8 @@ The Choreographer is the server-side composition layer that turns agent intent-v
|
||||
| `_briefing_for` | async helper | `_impl.py:813` | Builds `context_briefing`. Claim-scoped: `full=True` (context-acquisition verbs only — give_me_work/claims/plan/resume/triage) carries the heavy sections via `_heavy_briefing_sections` (company_goals, recent_team_activity, blockers, task_handoff, institutional_memory); every other verb gets slim signals-only (unread a2a/mentions/notifications + metadata gaps). `include_company_goals=True` is a narrower, cheap-only opt-in (`_resolve_company_goals`) that fetches just the company_goals singleton without the rest of `full`'s heavy sections — used by `board_triage`'s idle branch so the CEO's charter (brand_voice/north_star) still reaches the Product Owner's roadmap-exploration / Head of Marketing's feature-spotlight-exploration one-shot spawns, whose directly-assigned exploration task is never itself a strategic root awaiting PM review (so the `full=True` branch never fires for it). AC coverage stays independent of `full`. |
|
||||
| `_run_claim_guards` | async helper | `_impl.py:916` | already_active / paused / unmet_dependency (with re-check race narrowing) + `_lane_claim_guard`; `skip_dev_guards=False` param skips dev-only guards for pr_reviewer gate claims (claim_gate_review). |
|
||||
| `_lane_claim_guard` | async helper | `_impl.py:977` | Out-of-order-start barrier: refuse code leaf behind an earlier open same-assignee sibling. Fail-closed on lookup error. |
|
||||
| `_claim_plan_start_gate` | async helper | `_impl.py:1179` | spec gate → advisory claim lock (non-PM) → behavioral guards. |
|
||||
| `_sequencing_claim_guard` | async helper | `_impl.py:1230` | Proactive claim-time check (#681): consults `TaskService.sequence_hold_reason` and, when held, returns `Envelope.sequence_held` (naming the blocking sibling) BEFORE the claim even reaches `TaskService.claim`'s bare-`None`-on-reject path — closes the misdiagnosed `INVALID_STATE` a held PENDING/NEEDS_REVISION claim used to surface as. |
|
||||
| `_claim_plan_start_gate` | async helper | `_impl.py:1179` | spec gate → advisory claim lock (non-PM) → behavioral guards → `_sequencing_claim_guard`. |
|
||||
| `_claim_plan_start_run` | async helper | `_impl.py:1251` | `runner.run_intent(verb)` + ensure_work_session + `_touch`. |
|
||||
| `i_will_work_on` | async verb | `_impl.py:1326` | Dev claim+plan+start path; routes re-entry vs fresh claim. |
|
||||
| `open_pr` | async verb | `_impl.py:1571` | Pre-flight + `run_intent("open_pr")` (push_branch + create_pr side effects). |
|
||||
@@ -6591,6 +6598,9 @@ The Choreographer is the server-side composition layer that turns agent intent-v
|
||||
| `complete` | async verb | `_impl.py:6599` | Role-dispatch to `cell_pm_complete` / `main_pm_complete`; umbrella-in-progress bypasses spec gate. |
|
||||
| `main_pm_complete` | async verb | `_impl.py:6496` | Main PM merge + escalate to CEO (never merges master itself). |
|
||||
| `escalate_to_ceo` | async verb | `_impl.py:6844` | Escalate to `awaiting_ceo_approval`. |
|
||||
| `unblock` | async verb | `_impl.py:7041` | Human/PM restores a BLOCKED task; gated by `_oscillation_unblock_guard` (refuses while oscillation-tripped) then, on a real restore, runs `_maybe_trip_oscillation_breaker` (#685). |
|
||||
| `_oscillation_unblock_guard` | async helper | `_impl.py:7184` | Refuses `unblock` once `markers.is_oscillation_tripped(t)` — an admin override must clear the marker first. |
|
||||
| `_maybe_trip_oscillation_breaker` | async helper | `_impl.py:7209` | Task-scoped strike counter (#685) at the `unblock()` chokepoint (agent-agnostic — legitimate `needs_revision` rework never calls `unblock`, so it structurally can't trip this): `markers.bump_oscillation_strikes(t, progress_fp)` where the progress fingerprint is commit count + `revision_count` + `terminal_children_count` (a coordination root's children advancing between escalations resets the count); past threshold, sets `blocker_resolver_type=HUMAN`, marks the task oscillation-tripped, and best-effort notifies the CEO (`_notify_ceo_oscillation`) naming both agents + the cycle count. |
|
||||
| `VerbRunner.run_intent` | async method | `_verb_runner.py:37` | pre_side_effects → SAVEPOINT(composes) → side_effects; intermediate-None raises INVALID_STATE. |
|
||||
| `VerbRunner._do_pr_merge` | async handler | `_verb_runner.py:257` | `pr_merge` with `project_id` scoping (cross-repo collision fix) + `resolve_parent_branch`. |
|
||||
| `ChoreographerHelpers` | stub class | `_protocol.py:31` | TYPE_CHECKING-only typed view of `self` helpers for role mixins. |
|
||||
@@ -6734,7 +6744,9 @@ The slice is structurally sound: the SAVEPOINT boundary, intermediate-None INVAL
|
||||
| `_validate_and_set_status` | method | task.py:548 | Single chokepoint: validate transition + git requirements, set status, poke dispatcher, emit audit. |
|
||||
| `_emit_status_transition_audit` | method | task.py:652 | Write `task.<status>` audit row in caller session; bump `revision_count` on entry into `needs_revision`. |
|
||||
| `_alert_auditor_of_rework` | method | task.py:1019 | Best-effort helper that asks `NotificationDeliveryService` to send a HIGH `ALERT` to the auditor when a task enters `needs_revision`. Called from `fail_qa`, `pr_fail`, and `request_changes` immediately after `await self.session.flush()` so the transition row is visible before the alert is dispatched. |
|
||||
| `create` | method | task.py:864 | New task; depth/batch/AC validation; branchless/umbrella flags; baseline constraints attachment; (V2) vault materialize-on-create. |
|
||||
| `create` | method | task.py:864 | New task; depth/batch/AC validation; branchless/umbrella flags; baseline constraints attachment; (V2) vault materialize-on-create; stamps `acceptance_criteria_ids` through `_reconcile_ac_ids` (#682, explicitly supplied ids still win). |
|
||||
| `update` | method | task.py:2928 | Generic PATCH field loop (task route, prompter `update_live_draft`/`_patch_batch_child`/`update_live_batch`) — derives `acceptance_criteria_ids` via `_reconcile_ac_ids` whenever `acceptance_criteria` is rewritten with no explicit id list (#682; previously overwrote the criteria text without touching ids, leaving them mismatched or empty and silently disabling the parent-coverage gate). |
|
||||
| `_reconcile_ac_ids` | function | task.py | Pure helper (#682): one id per new criterion; text-unchanged criteria KEEP their existing id (children/findings reference criteria by id or exact text — a blanket re-mint would orphan every live reference); new/reworded text mints a fresh id; a dropped criterion drops its id. |
|
||||
| `_attach_baseline_constraints` | method | task.py:971 | Append conventions baseline constraints to task prompt (gated `conventions_enabled`). |
|
||||
| `_materialize_vault_note` | method | task.py:910 | V2: best-effort vault seam called from `create` — assembles + writes a deterministic task note (narrative placeholder) so a task is visible in the vault from the moment it exists, not just at Auditor curation/rebuild. Gated `obsidian_vault_enabled`; swallows + logs any failure. |
|
||||
| `list_updated_since` | method | task.py:7101 | V2: tasks touched (`COALESCE(updated_at, created_at)`) since a timestamp, ascending, paged — the vault janitor's changed-task re-projection set. |
|
||||
@@ -6749,9 +6761,12 @@ The slice is structurally sound: the SAVEPOINT boundary, intermediate-None INVAL
|
||||
| `_emit_admin_override_audit` | method | task.py:8555 | Factored out of `_apply_pre_block_restore`: writes `task.admin_override` audit row for admin-triggered blocked restores (`forced=False, restore=True`). |
|
||||
| `claim` | method | task.py:3112 | `FOR UPDATE` lock + `_validate_claim_preconditions` + `_finalize_claim`; calls `_validate_and_set_status(claimed)`. |
|
||||
| `_validate_claim_preconditions` | method | task.py:2883 | Per-claim validator chain: status, `_claim_blocked_by_sequencing` (dependency + sequence), team, pre-assignment theft, self-review. |
|
||||
| `_claim_blocked_by_sequence` | method | task.py:2805 | Strict sibling-sequence gate: a PENDING/`needs_revision` task with parent + effective `sequence` (`COALESCE(sequence, 0)`) N is held while any same-parent sibling with a strictly lower effective sequence is non-terminal — assignee-blind, independent of `dependency_ids`. Ties run parallel; cancelled siblings never block. |
|
||||
| `_claim_blocked_by_sequence` | method | task.py:2805 | Sequence gate for a PENDING/`needs_revision` task with a parent + effective `sequence` (`COALESCE(sequence, 0)`) N — **reachability-aware since #681**: it branches on `is_batch_root_subtask(task.batch_id, task.parent_task_id)`. A MegaTask root-subtask keeps the ORIGINAL strict rule unchanged (held while ANY same-parent sibling with a strictly lower sequence is non-terminal — assignee-blind, edge-agnostic; `sequence` there is a one-shot globally-computed Kahn wave, a deliberate staged-release barrier). Every other same-parent context instead routes through the pure `sequence_blocker_id` (`roboco/services/sequencing.py`), which only lets a lower-sequence sibling block when it is a real transitive predecessor via `dependency_ids` UNIONED with `completed_dependency_ids` — a task with NO dependency edge onto any same-parent sibling still falls back to the raw edge-agnostic bar (preserves the pre-#681 #452 edge-less-PM-delegation scenario). Ties run parallel; cancelled siblings never block. |
|
||||
| `sequence_blocker_id` | function | services/sequencing.py | Pure reachability check backing the non-batch branch of `_claim_blocked_by_sequence` (#681): returns the blocking sibling's id, or `None`, from `dependency_ids ∪ completed_dependency_ids` — the union matters because `_unblock_dependents` prunes a completed dependency's edge into `completed_dependency_ids` the moment it lands, almost always before the dependent is ever claimed. |
|
||||
| `sequence_hold_reason` | method | task.py | Names the blocking sibling for the caller instead of a bare `None`; feeds the gateway's `Envelope.sequence_held` (#681) so a held claim on PENDING or NEEDS_REVISION surfaces cleanly instead of `claim()`'s bare `None` return being misdiagnosed by the verb runner as a concurrent-transition `INVALID_STATE`. |
|
||||
| `_claim_blocked_by_dependencies` | method | task.py:2781 | `unmet_dependency` TIMING gate: refuses claim while any `dependency_ids` entry is non-terminal. |
|
||||
| `is_pending_claim_blocked` | method | task.py:2864 | Read-only wrapper over `_claim_blocked_by_sequencing` (dependency OR sequence) so the orchestrator dispatcher can filter a doomed claim before attempting it (`_pending_claim_blocked` in orchestrator.py). |
|
||||
| `is_pending_claim_blocked` | method | task.py:2864 | Read-only wrapper over `_claim_blocked_by_sequencing` (dependency OR sequence) so the orchestrator dispatcher can filter a doomed claim before attempting it (`_pending_claim_blocked` in orchestrator.py); extended to NEEDS_REVISION so `give_me_work`'s two offer paths (`list_pending_for_agent`, the Choreographer's `_drop_dependency_held`) never offer a task the reachability-aware bar (#681) is about to reject. |
|
||||
| `terminal_children_count` | method | task.py:9711 | Count of a task's terminal (completed/cancelled) direct children; feeds the orchestrator oscillation breaker's progress fingerprint (#685, `roboco/services/gateway/choreographer/_impl.py`) so a coordination root whose children advance between escalate/unblock round-trips resets its strike count instead of accruing toward a false trip. |
|
||||
| `stamp_wave_sequence` | method | task.py:7452 | Stamps a freshly delegated subtask's `sequence` as `1 + max(sequence of each same-parent dependency target)`, or `0` when independent — so independent siblings tie (parallel under the sequence gate) while colliding/ordered work ascends. Runs POST-wiring (after the collision DAG / cross-cell edges land); PM-authored sequences are never rewritten. |
|
||||
| `_apply_dependency_lineage` / `_merge_one_dependency` | method | task.py:2308 / 2337 | Claim-time content assist (not a gate): merges each same-repo dependency's landed work into a freshly cut branch when it lies outside the branch's own ancestor chain (`GitService.merge_dependency_lineage`); a real conflict aborts the merge and stamps a `dependency_lineage_conflict` transition note instead of failing the claim. |
|
||||
| `_finalize_claim` | method | task.py:3265 | Work-session create/inherit, branch cut, proactive-context injection; calls `_inherit_upstream_base` when `_should_inherit_base` says the reclaimed branch may need an upstream merge. |
|
||||
@@ -6860,6 +6875,8 @@ stateDiagram-v2
|
||||
- `complete()` requires PR merged (`_assert_pr_merged_for_complete`) EXCEPT branchless roots; `ceo_approve` separately checks `work_session.pr_status=="merged"` and refuses otherwise.
|
||||
- Background indexing/learning/cleanup tasks are tracked on `self._background_tasks` and are best-effort — a failure never blocks the transition.
|
||||
- The sequence gate (`_claim_blocked_by_sequence`) is enforced ONLY in `_validate_claim_preconditions`, i.e. inside `claim` itself — both the gateway claim verbs AND the orchestrator's raw dispatch claim cross it because they both funnel through `TaskService.claim`, unlike the pre-#382 dependency gate which briefly lived only on the gateway side. Any future claim path that bypasses `TaskService.claim` (a raw `admin_set_status`, for instance) does NOT get sequence enforcement.
|
||||
- Since #681, the sequence gate is NOT one rule everywhere: `stamp_wave_sequence` stamps a sibling's wave from a partial, per-task view of the graph at delegate time, which is fine within one connected chain but can coincidentally collide two INDEPENDENT same-parent dev-task streams onto the same raw sequence number. The strict edge-agnostic rule survives ONLY for `is_batch_root_subtask` rows (a MegaTask's globally-computed Kahn wave is a deliberate barrier); every other same-parent context resolves via `sequence_blocker_id`'s real-predecessor check. A task with an empty `dependency_ids`/`completed_dependency_ids` union onto its same-parent siblings still falls back to the raw bar unchanged either way.
|
||||
- An `acceptance_criteria` rewrite through `update()` with NO explicit `acceptance_criteria_ids` used to silently desync ids from text (or leave an empty id list, which the parent-coverage gate then reads as "no criteria to cover" — waiving coverage for the whole subtree). `_reconcile_ac_ids` (#682) closes this at the one chokepoint every rewrite path shares; the parent-coverage gate also self-heals an out-of-length/empty id list in place rather than returning early.
|
||||
- `_apply_dependency_lineage` is scoped to SAME-REPO dependencies only (`dep_task.project_id != ctx.project.id` short-circuits) — a cross-repo dependency edge (e.g. a MegaTask root-subtask in another project) has no shared git history to merge and is silently skipped; the dependency TIMING gate still holds the claim regardless of repo.
|
||||
- `TaskTable.orchestration_markers` is generic `JSON`, not `JSONB`. Any SQL predicate on a marker key must use `.as_string()` (or the JSON dialect's generic comparator), not `.astext`, which is JSONB-only and raises `AttributeError` at compile time. `list_open_docs_sync_tasks(version=...)` at task.py:1596 is the current example; the inline comment records the rationale.
|
||||
- Both cancel and terminal-completion now force-delete (`-D`) the task's LOCAL branch ref in the assignee's clone alongside the worktree — a completed task's PR was squash-merged (its local ref is never an ancestor of base, so a "safe" `-d` refuses unconditionally) and a cancelled task's work is discarded by decision, so the ref is spent either way. Skipped when the branch name coincides with an environment-ladder rung (`effective_environments`), which outlives any one task.
|
||||
@@ -6882,6 +6899,10 @@ stateDiagram-v2
|
||||
> (open PR #548, branch `feature/wave-2-hygiene-charts`, 2026-07-17) Local branch refs stop leaking: `_delete_task_branch_best_effort`/`_remove_task_worktree_on_terminal` now also force-delete the assignee's local branch ref (via new `WorkspaceService.delete_local_branch`) and rmtree the task's `.previews/{task8}` video-preview dir, both skipped for environment-ladder rungs. See `docs/map/worksession-git.md` for the paired `GitService.cleanup_stale_branches` sweep.
|
||||
>
|
||||
> `2e889c70` (#644, "work re-claims inherit an advanced upstream base"): a re-claim (pool release / reaper unclaim / escalation redirect / a dev's own `i_will_work_on` re-entry) reused whatever branch had been cut at the EARLIER claim, so any work that landed on the parent/base branch since then (e.g. a sibling cell merging into root, or UX/UI landing before BE/FE re-claims) never reached the reclaimed branch — silent divergence, avoidable conflicts at PR time. `_finalize_claim` now calls `_inherit_upstream_base` after resolving the work session, gated by the double check above; a conflict never fails the claim, it steers the dev to `sync_branch` via a transition note + `dev_notes`.
|
||||
>
|
||||
> `c4ba351a` (#681, "reachability-aware claim bar + sequence_held surfacing", 2026-07-24) The sequence-drift fix: `_claim_blocked_by_sequence` branches on `is_batch_root_subtask` so an independent same-parent dev-task stream can no longer phantom-hold behind an unconnected sibling that coincidentally shares a lower raw `stamp_wave_sequence` number; non-batch contexts route through the new pure `sequence_blocker_id` (real transitive predecessor via `dependency_ids ∪ completed_dependency_ids`). New `sequence_hold_reason` + a proactive gateway guard return `Envelope.sequence_held` instead of a bare `None` reaching the verb runner as a misdiagnosed `INVALID_STATE`. `is_pending_claim_blocked` extended to NEEDS_REVISION so both `give_me_work` offer paths never dangle a task the claim gate would reject.
|
||||
>
|
||||
> `3516d925` (#682, "reconcile acceptance_criteria_ids at the update chokepoint", same day) `create()` and `update()` both now stamp `acceptance_criteria_ids` through the new pure `_reconcile_ac_ids` (id-preserving for text-unchanged criteria, fresh id for new/reworded text, dropped id for a removed criterion) — every prior post-create rewrite path (task PATCH, prompter `update_live_draft`/`_patch_batch_child`/`update_live_batch`) had silently desynced or emptied the ids, and an empty id list silently waived the whole subtree's parent-coverage gate. The gate itself now self-heals an empty/out-of-length id list in place instead of returning early.
|
||||
|
||||
## Regression Risks
|
||||
|
||||
@@ -6986,7 +7007,7 @@ This slice is the git substrate every delivery agent works on. `GitService` runs
|
||||
| `GitService._merge_with_retry` | method | git.py:3555 | Single 409 retry + already-merged disambiguation → MergeConflictError |
|
||||
| `GitService._lock_parent_task_for_merge` | method | git.py:3503 | SELECT FOR UPDATE on parent task (sibling merge serialization) |
|
||||
| `GitService._resolve_merger_id` | static | git.py:3530 | merged_by attribution: actor→assigned→created→UUID(0) |
|
||||
| `GitService.rebase_onto_base` | method | git.py:3733 | Rebase primitive: rebased/superseded/conflicts classification |
|
||||
| `GitService.rebase_onto_base` | method | git.py:5158 | Rebase primitive: rebased/superseded/conflicts/**diverged** classification. Post-fetch, `_reset_head_or_diverged` classifies local vs `origin/<head_branch>` BEFORE the old unconditional `reset --hard`: behind-or-equal resets to origin (unchanged); strictly ahead skips the reset and rebases from the local tip (the committed-but-unpushed-work case `commit` leaves behind); a real divergence returns `{"status": "diverged", "local_only", "origin_only"}` untouched — no reset, no rebase, no push. A patch-equivalence probe (`rev-list --right-only --cherry-pick`) first tries to recognize the diverged shape as a prior rebase whose force-push failed (self-heals as "ahead" on retry) before declaring a genuine two-sided divergence (#683). |
|
||||
| `GitService.rebase_pr_for_task` | method | git.py:3792 | PR-keyed rebase via PR refs (project_id scoped) |
|
||||
| `GitService.sync_task_branch` | method | git.py:3847 | Task-keyed rebase through dev `sync_branch` verb (pre-PR) |
|
||||
| `GitService.is_behind_base` | method | git.py:3889 | `(behind, ahead)` counts for i_am_done submit gate |
|
||||
@@ -7007,7 +7028,8 @@ This slice is the git substrate every delivery agent works on. `GitService` runs
|
||||
| `GitService.conventions_check_for_task` | method | git.py:4368 | Run conventions validator on changed files (fail-closed) |
|
||||
| `GitService._run_conventions_validator` | method | git.py:4408 | Subprocess `python -m roboco.conventions` with 120s cap |
|
||||
| `GitService.open_conventions_pr` | method | git.py:4456 | Scaffold `.roboco/conventions.yml` on a branch + open PR |
|
||||
| `GitService.diff` / `list_changed_files` / `read_file_at_branch` | methods | git.py:4192/4225/4259 | Read-only git queries (gateway + routes) |
|
||||
| `GitService.diff` / `list_changed_files` / `read_file_at_branch` | methods | git.py:5943/5984/6021 | Read-only git queries (gateway + routes); each resolves its head ref through `_resolve_head_ref` |
|
||||
| `GitService._resolve_head_ref` | method | git.py:5893 | Shared reader-ref resolution for `diff`/`list_changed_files`/`read_file_at_branch`/`/api/git/log`. Fetches `branch_name`, then prefers `origin/<branch_name>` over the bare local ref whenever origin carries commits local lacks (`origin_only > 0` via `rev-list`) — a reviewer's clone parked on pre-rebase history no longer stays frozen there across every later round; local wins only when it strictly contains everything origin has. Unlike the write-path's `_reset_head_or_diverged`, a read never needs to tell genuine divergence apart from a rewritten history — both resolve to "serve origin" (#690). |
|
||||
| `GitService.commit` | method | git.py:4286 | Gateway content-verb commit (branch-keyed) |
|
||||
| `get_git_service` | factory | git.py:4594 | Construct GitService from AsyncSession |
|
||||
| `build_branch_name` | func | templates/git/branch.py:37 | `{type}/{team}/{root}--{sub}--...` up to MAX_TASK_DEPTH |
|
||||
@@ -7217,7 +7239,8 @@ Module-level tunables (not env): `_SLOW_GIT_OP_MS=5000`, `_CI_RUN_WINDOW=20`, `_
|
||||
- **`get_current_branch` raises on detached HEAD** instead of returning `""` — the empty string used to leak "(HEAD detached at ...)" into `checkout -b`.
|
||||
- **`MAX_TASK_DEPTH=4`** (was 3) — MegaTask's umbrella→root→cell→dev needs 4; validator rejects a child whose depth would *reach* MAX_TASK_DEPTH, so 4 permits dev at depth 3.
|
||||
- **Branch name uses 8-char UUID prefix** (`_SHORT_ID_LEN=8`), not full UUID — full UUIDs produced 140-char branch names.
|
||||
- **`rebase_onto_base` force-pushes with `--force-with-lease`** only the head branch; never touches base. `superseded` (unique==0) means close-without-merge.
|
||||
- **`rebase_onto_base` force-pushes with `--force-with-lease`** only the head branch; never touches base. `superseded` (unique==0) means close-without-merge. It no longer resets a local tip that is ahead of origin (a committed-but-unpushed dev tip used to be silently discarded before the rebase); a `diverged` result leaves BOTH sides untouched — the sync_branch verb maps it to `i_am_blocked` with a stash-preserved note (#683).
|
||||
- **Reader git queries prefer origin over a diverged local ref** (`_resolve_head_ref`, #690): a QA/PM/reviewer clone that never pushed can go stale relative to origin after a force-push sync; `diff`/`list_changed_files`/`read_file_at_branch` and `roboco_git_log` all resolve through the same fixed helper now, so a repeated review round no longer re-examines a frozen pre-rebase checkout.
|
||||
- **`is_behind_base` raises on git failure**; the i_am_done gate fail-opens on the raised error so a flaky fetch can't strand the task.
|
||||
- **Conventions validator fails closed** (`could_not_run=True` blocks submit) on resolution error / timeout / non-zero exit; branchless + no-changed-files fail open.
|
||||
- **`_assert_on_task_branch` never discards work** — it does `checkout`, not `reset --hard`, to preserve a resumed agent's unpushed commits.
|
||||
@@ -7248,6 +7271,8 @@ Baseline: `fd10cc862c2020b3f639cdb686d427b0198a2441` (master tip before the metr
|
||||
>
|
||||
> `496c24d1` (PR #548, "git hygiene", 2026-07-17) Local branch refs stop leaking alongside remote ones: `delete_task_branch` now also skips environment-ladder rungs (previously only the remote-delete's own main/master/develop guard existed) and returns `bool`; new `cleanup_stale_branches` + `_stale_branch_window` + `_cleanup_one_stale_branch` back a PM/CEO-only `POST /git/branches/cleanup` sweep of terminal tasks' remote+local branches, exposed as a confirm-dialog button on the panel Git page. See `docs/map/task-service.md` for the paired per-task reap at cancel/completion and `docs/map/workspace.md` for the new `WorkspaceService.delete_local_branch` primitive both routes share.
|
||||
>
|
||||
> `71f5426e` (PR #683, "never discard committed local work in rebase_onto_base", 2026-07-24) The stale-state family fix for the write path: `rebase_onto_base` classifies local vs `origin/<head_branch>` post-fetch instead of an unconditional `reset --hard` — behind/equal resets as before, strictly-ahead skips the reset and rebases from the local tip, and a genuine two-sided divergence (patch-equivalence-checked first, so a prior failed force-push self-heals as "ahead" on retry) returns `{"status": "diverged", ...}` with neither side touched. `sync_branch`/submit-freshen/the merge-conflict resolver all gained a `diverged` handling branch. `e97f46af` (PR #690, "reviewer reads must prefer origin over a diverged local ref", same day) is the read-path sibling: `_resolve_head_ref` (backing `diff`/`list_changed_files`/`read_file_at_branch`/`roboco_git_log`) now prefers origin whenever it carries commits the local ref lacks, closing the QA/PM-clone-frozen-on-pre-rebase-history bounce loop.
|
||||
>
|
||||
> **Forge providers — GitHub + Gitea + GitLab (2026-07-18/19, PRs #569/#571/#575/#579/#581).** A new `roboco/services/forge/` package (`base.py`/`github.py`/`gitea.py`/`gitlab.py`/`registry.py`/`router.py`/`shaping.py`) plus `roboco/foundation/policy/forge.py` route every REST call `GitService` makes (PRs/CI/reviews/labels/releases/provisioning) through a provider-agnostic transport. `388bab24` (Phase 0, #569): `projects.git_provider` column (migration 076, nullable, plain string not a pg enum — validated at the service layer, not the DB) + `validate_project_forge`/`detect_provider` (github.com auto-detects, self-hosted needs an explicit column value — the GHE/self-hosted escape hatch). `461a6e1a` (Phase 1, #571): the `GitProvider` ABC + `GitHubProvider` extracted byte-for-byte from `GitService`'s old inline `httpx` calls; `GitService._forge` (git.py:399) becomes the seam every call site routes through. `96401f4c` (Phases 2/2.1/3, #575): `GiteaProvider` + `GitLabProvider` + `ForgeRouter` (per-call transport dispatch off `RepoRef.host`) + the local-git `merge_branch` fallback for forges with no server-side merges API. `5f32d876` (Phase 4, #581): `roboco/services/github_provisioning.py` becomes provider-aware (`ROBOCO_PROVISIONING_PROVIDER`/`ROBOCO_PROVISIONING_HOST` — see `docs/map/product-strategy-research-pitch.md`) so pitch-driven repo creation works on all three forges. `d4cb5797` (#579) + the pre-existing `tests/e2e_smoke/test_gitea_live.py` are the live contract suites (self-seeding against a dockerized `gitea/gitea` / real `gitlab.com`, env-gated) that caught the slash-encoding and http-scheme gaps in the Gitea provider. Panel: the edit-project dialog's "Forge" `<Select>` (`edit-project-dialog.tsx`) offers Auto-detect/GitHub/Gitea/GitLab.
|
||||
>
|
||||
> **Protected branches + env-ladder deletion protection (#649, #651).** `GitService._protected_branches_for`/`_protected_branches_for_deletion` (git.py:1183/1219) consult the new `projects.protected_branches` column end-to-end for the first time (the column previously existed with nothing reading it): the deletion scope additionally unions in the project's env-ladder rung branches (`effective_environments`), closing the gap where the post-merge PR-source cleanup and the stale-branch sweep could still delete a branch that IS a ladder rung — `delete_task_branch`'s own local rung check is now exactly subsumed and removed. Panel: a chips editor on the edit-project dialog.
|
||||
@@ -7316,7 +7341,9 @@ WorkspaceService manages the per-agent git clone layout under {workspaces_root}/
|
||||
| WorkspaceService.ensure_worktree | method | roboco/services/workspace.py:555 | git worktree add -b <branch> <base> (or reuse existing branch); calls _park_clone_root_off_branch first; link venv; chown worktree + clone root |
|
||||
| WorkspaceService.ensure_worktree_for_resume | method | roboco/services/workspace.py:591 | Re-add a pruned worktree on resume (no -b; branch ref survives); calls _park_clone_root_off_branch first; idempotent; link venv + chown |
|
||||
| WorkspaceService._fetch_branch_ref | method | roboco/services/workspace.py:613 | Token-aware git fetch origin <branch> into clone_root; best-effort (never raises); used by ensure_worktree_self_heal (536bbb64) |
|
||||
| WorkspaceService.ensure_worktree_self_heal | method | roboco/services/workspace.py:671 | Orchestrator spawn-time chokepoint: re-attaches a per-task worktree after clone vanished (redeploy/disk loss); fetches branch ref from origin when the local ref is absent after a re-clone, then delegates to ensure_worktree (536bbb64) |
|
||||
| WorkspaceService.ensure_worktree_self_heal | method | roboco/services/workspace.py:716 | Orchestrator spawn-time chokepoint, now role-aware (`can_author`, #692): a PRESENT worktree is no longer a pure venv+chown no-op — it runs through `_refresh_present_worktree` first, so a worktree created once at first claim/`claim_review` no longer stays frozen at that commit across every later respawn. A vanished-clone re-attach still fetches the branch ref from origin when the local ref is absent (536bbb64); a surviving-ref-but-pruned-worktree re-add now also runs the same refresh classification instead of trusting the stale ref's commit. |
|
||||
| WorkspaceService._refresh_present_worktree | method | roboco/services/workspace.py:869 | Compares local `branch` HEAD against `origin/<branch>` (rev-list ahead/behind): behind-or-equal fast-forwards for every role, but never discards an author-capable role's (`can_author`) dirty uncommitted edits to do it; strictly ahead is always left alone (real unpushed work); diverged only hard-resets for a pure reader (QA/PR-gate/PM), whose local history can only ever be a stale prior round's checkout — never an author, since only developer/documenter hold the gateway `commit` tool. Every reset is additionally guarded by `_safe_to_reset` (#692). |
|
||||
| WorkspaceService._worktree_is_dirty / `_worktree_on_task_branch` / `_safe_to_reset` | methods | roboco/services/workspace.py | Dirty-guard trio backing `_refresh_present_worktree`: a failed `git status` reads as dirty (fail toward preservation, never a false-clean reset); a reset only ever fires when the worktree is actually still checked out on `branch` (a detached/drifted worktree is left alone with a warning) (#692) |
|
||||
| WorkspaceService.remove_worktree | method | roboco/services/workspace.py:733 | Best-effort git worktree remove --force + prune; no-op if gone (cancel/terminal/reaper evict) |
|
||||
| WorkspaceService.delete_local_branch | method | roboco/services/workspace.py:787 | Best-effort `git branch -d/-D <branch>` in a clone; never raises; skips main/master/develop/empty (mirrors GitService._delete_remote_branch_best_effort); callers run it AFTER remove_worktree (a still-checked-out branch refuses) |
|
||||
| WorkspaceService.resolve_workspace | method | roboco/services/workspace.py:745 | Look up agent (UUID or slug) -> team+slug -> workspace path; default team BACKEND |
|
||||
@@ -7349,7 +7376,7 @@ WorkspaceService manages the per-agent git clone layout under {workspaces_root}/
|
||||
| get_workspace_service | function | roboco/services/workspace.py:1930 | Factory: WorkspaceService(session) |
|
||||
|
||||
## Data Flow
|
||||
Inputs: an AsyncSession, a project_slug, an agent_id (UUID or slug), optionally a git_url/default_branch/force. The orchestrator, GitService, TaskService, conventions service, dep_update_engine, and gateway content_actions all obtain a WorkspaceService via get_workspace_service(session) (or WorkspaceService(db) directly in the spawn path). Control flow on ensure_workspace: _lookup_agent_or_raise -> get_workspace_path -> acquire per-(project,agent) asyncio.Lock -> if _is_workspace_healthy (.git+HEAD+objects): _ensure_agent_owned (to_thread), prune broken refs, scoped _fetch_origin_best_effort (30s TTL, force override), re-chown, install_dev_deps (digest-cache hit short-circuits), return. Else: rmtree any partial/stub dir, ProjectService.get_by_slug, resolve the clone target branch via `head_branch(project)` (the env-ladder head rung — `roboco.models.env_branches`, shimmed from `default_branch` when no ladder is declared), _resolve_git_token (decrypt PAT; raise on HTTPS-with-no-token), _clone_repo (git clone --branch --no-tags, configure identity/fileMode, scrub PAT from remote URL, _assert_no_pat_leak scanning .git/** for ghp_/github_pat_/x-access-token, chown, install_dev_deps), _maybe_scaffold_conventions (once-per-process, flag-gated). Per-task worktree path: get_clone_root_path + get_worktree_path (.worktrees/{task_short_id}); ensure_worktree runs git worktree add -b <branch> <base> (or reuses an existing branch ref), _link_shared_venv (symlink to clone-root .venv only if it exists), chowns both worktree and clone root. GitService.create_branch calls ensure_worktree; commit/rebase paths call ensure_worktree_for_resume via GitService._ensure_worktree_for_commit; the orchestrator's _ensure_worktree_before_spawn calls ensure_worktree_self_heal (post-536bbb64) which first fetches the branch ref from origin if the local ref is absent after a re-clone, then delegates to ensure_worktree; TaskService.complete/cancel call remove_worktree. ensure_read_clone is called by ConventionsService for the project-level read clone at _meta/conventions, hard-reset to origin/default. dry_upgrade_changes_lockfile (dep_update_engine) clones the read clone --local --no-hardlinks into a throwaway under the read-clone lock, runs dep_update_command, and checks git status --porcelain on lockfile paths. Outputs: workspace Path (and side effects: on-disk clone/worktree, .venv symlink, .git/.roboco-dep-install + .git/.roboco-toolchain markers, root-owned refs re-chowned to agent uid). All git/subprocess work runs via asyncio.to_thread; tokens are injected only transiently into argv (never written to .git/config) and scrubbed post-clone.
|
||||
Inputs: an AsyncSession, a project_slug, an agent_id (UUID or slug), optionally a git_url/default_branch/force. The orchestrator, GitService, TaskService, conventions service, dep_update_engine, and gateway content_actions all obtain a WorkspaceService via get_workspace_service(session) (or WorkspaceService(db) directly in the spawn path). Control flow on ensure_workspace: _lookup_agent_or_raise -> get_workspace_path -> acquire per-(project,agent) asyncio.Lock -> if _is_workspace_healthy (.git+HEAD+objects): _ensure_agent_owned (to_thread), prune broken refs, scoped _fetch_origin_best_effort (30s TTL, force override), re-chown, install_dev_deps (digest-cache hit short-circuits), return. Else: rmtree any partial/stub dir, ProjectService.get_by_slug, resolve the clone target branch via `head_branch(project)` (the env-ladder head rung — `roboco.models.env_branches`, shimmed from `default_branch` when no ladder is declared), _resolve_git_token (decrypt PAT; raise on HTTPS-with-no-token), _clone_repo (git clone --branch --no-tags, configure identity/fileMode, scrub PAT from remote URL, _assert_no_pat_leak scanning .git/** for ghp_/github_pat_/x-access-token, chown, install_dev_deps), _maybe_scaffold_conventions (once-per-process, flag-gated). Per-task worktree path: get_clone_root_path + get_worktree_path (.worktrees/{task_short_id}); ensure_worktree runs git worktree add -b <branch> <base> (or reuses an existing branch ref), _link_shared_venv (symlink to clone-root .venv only if it exists), chowns both worktree and clone root. GitService.create_branch calls ensure_worktree; commit/rebase paths call ensure_worktree_for_resume via GitService._ensure_worktree_for_commit; the orchestrator's _ensure_worktree_before_spawn calls ensure_worktree_self_heal at EVERY spawn (not just after a vanished clone, post-#692), passing `can_author` from the spawning agent's role (`WORKTREE_AUTHOR_ROLES` — developer/documenter): a present worktree now runs through `_refresh_present_worktree` (fast-forward when behind, leave alone when ahead, hard-reset only for a non-author reader when diverged) before venv-link + chown; a vanished clone still fetches the branch ref from origin if the local ref is absent, and a surviving-ref-but-pruned worktree re-adds then runs the identical refresh; TaskService.complete/cancel call remove_worktree. ensure_read_clone is called by ConventionsService for the project-level read clone at _meta/conventions, hard-reset to origin/default. dry_upgrade_changes_lockfile (dep_update_engine) clones the read clone --local --no-hardlinks into a throwaway under the read-clone lock, runs dep_update_command, and checks git status --porcelain on lockfile paths. Outputs: workspace Path (and side effects: on-disk clone/worktree, .venv symlink, .git/.roboco-dep-install + .git/.roboco-toolchain markers, root-owned refs re-chowned to agent uid). All git/subprocess work runs via asyncio.to_thread; tokens are injected only transiently into argv (never written to .git/config) and scrubbed post-clone.
|
||||
|
||||
## Mermaid
|
||||
```mermaid
|
||||
@@ -7438,7 +7465,7 @@ WorkspaceService slice
|
||||
| ensure_workspace | roboco/services/workspace.py | GitService.create_branch_for_task / push / PR ops; orchestrator spawn ensure; gateway content_actions; called transitively by many verbs |
|
||||
| ensure_worktree | roboco/services/workspace.py | GitService.create_branch_for_task on fresh claim (worktree add -b <branch> <base>) |
|
||||
| ensure_worktree_for_resume | roboco/services/workspace.py | GitService._ensure_worktree_for_commit (commit/rebase paths) |
|
||||
| ensure_worktree_self_heal | roboco/services/workspace.py | orchestrator._ensure_worktree_before_spawn before -w container launch (replaces the former ensure_worktree_for_resume call there; handles vanished clones + missing branch refs) |
|
||||
| ensure_worktree_self_heal | roboco/services/workspace.py | orchestrator._ensure_worktree_before_spawn before -w container launch (replaces the former ensure_worktree_for_resume call there; handles vanished clones + missing branch refs, and — post-#692 — refreshes an already-present worktree against origin at every respawn, role-aware via `can_author`) |
|
||||
| remove_worktree | roboco/services/workspace.py | TaskService terminal/cancel paths + claim-rollback (mid-claim failure) |
|
||||
| delete_local_branch | roboco/services/workspace.py | TaskService terminal/cancel paths (right after remove_worktree) + GitService.cleanup_stale_branches sweep |
|
||||
| ensure_read_clone | roboco/services/workspace.py | ConventionsService.scaffold/effective-map reads (project-level conventions metadata) |
|
||||
@@ -7501,21 +7528,23 @@ WorkspaceService slice
|
||||
> Further post-snapshot update (#534, env-branches ladder): `ensure_workspace`'s fresh-clone branch and `ensure_read_clone` both resolve their target branch via `roboco.models.env_branches.head_branch(project)` — the env-ladder's head rung — instead of reading `project.default_branch` directly. A project with no declared ladder resolves to the identical `default_branch` value via the read-time shim, so this is behavior-preserving until the CEO declares a real ladder in the panel.
|
||||
>
|
||||
> (open PR #548, branch `feature/wave-2-hygiene-charts`, 2026-07-17) Added `delete_local_branch` (line 787) so `TaskService`'s cancel/terminal-completion cleanup and `GitService.cleanup_stale_branches` can reap a spent local branch ref, not just the worktree — previously every task an agent ever claimed leaked a permanent `refs/heads/{branch}` in that agent's clone.
|
||||
>
|
||||
> `987eb09c` (PR #692, "role-aware worktree refresh at every spawn", 2026-07-24) Closed the frozen-worktree-across-respawns bug behind a live multi-round QA/PR-gate bounce loop: `ensure_worktree_self_heal` gained a `can_author` parameter (from `foundation.identity.WORKTREE_AUTHOR_ROLES` — developer/documenter) and now runs every PRESENT or re-added worktree through the new `_refresh_present_worktree` before venv-link + chown, not just a vanished-clone re-attach. Behind-or-equal fast-forwards for every role (an author's dirty tree is left untouched to do it); strictly ahead is always left alone; diverged only hard-resets for a pure reader. `conventions_check_for_task`'s list-vs-content gap closes as a side effect — a reviewer's worktree is current as of spawn.
|
||||
|
||||
## Regression Risks
|
||||
|
||||
| Title | File:Line | Claim | Severity |
|
||||
|---|---|---|---|
|
||||
| Worktree .venv symlink self-heal depends on a later ensure firing | roboco/services/workspace.py:534 | ensure_worktree (fresh claim) runs _link_shared_venv BEFORE install_dev_deps provisions clone_root/.venv, so the symlink is skipped on the first claim. The shared-venv optimization only self-heals if a later ensure (resume/commit via _ensure_worktree_for_commit) re-runs _link_shared_venv. If the agent commits via a path that does not re-invoke ensure and uv re-syncs a worktree-local .venv first, the lexists guard prevents replacing the real dir and the worktree is stuck with a duplicated venv. The F-fix mitigated the dangling-symlink case but did not close the already-clobbered-venv recovery (explicitly out of scope per commit msg). | medium |
|
||||
| ensure_worktree reuses an existing branch ref without validating it points at base | roboco/services/workspace.py:570 | When branch_exists is True (re-claim after rollback) ensure_worktree runs `worktree add <worktree> <branch>` with no -b and no base. If the surviving branch ref was left at an unexpected commit (e.g. a prior partial rebase, or a force-pushed-and-locally-stale ref), the worktree is created at that commit, not at the intended base. The caller (GitService.create_branch_for_task) assumes a fresh branch at base; a stale ref could spawn the agent on the wrong HEAD. | medium |
|
||||
| ensure_worktree_for_resume silently re-adds a worktree whose branch was force-updated remotely | roboco/services/workspace.py:591 | On resume via GitService._ensure_worktree_for_commit, ensure_worktree_for_resume re-adds the worktree from the surviving local branch ref (no fetch, no base). If the branch was force-pushed remotely while the agent was down and the local ref is stale, the agent resumes on the old commits with no warning. NOTE: the orchestrator spawn path (cfe725da/536bbb64) now calls ensure_worktree_self_heal instead, which fetches the branch ref from origin before re-attaching — the spawn path is resolved. The GitService commit path still uses ensure_worktree_for_resume without a fetch. | medium |
|
||||
| ensure_worktree reuses an existing branch ref without validating it points at base | roboco/services/workspace.py:570 | When branch_exists is True (re-claim after rollback) ensure_worktree runs `worktree add <worktree> <branch>` with no -b and no base. If the surviving branch ref was left at an unexpected commit (e.g. a prior partial rebase, or a force-pushed-and-locally-stale ref), the worktree is created at that commit, not at the intended base. RESOLVED for the orchestrator spawn path (#692): `ensure_worktree_self_heal` now runs every re-add through `_refresh_present_worktree`, which fetches origin and reconciles the ref before the agent starts working. The GitService commit path (`_ensure_worktree_for_commit` → `ensure_worktree_for_resume`) still calls the bare, unrefreshed re-add — residual risk there only. | low |
|
||||
| ensure_worktree_for_resume silently re-adds a worktree whose branch was force-updated remotely | roboco/services/workspace.py:591 | On resume via GitService._ensure_worktree_for_commit, ensure_worktree_for_resume re-adds the worktree from the surviving local branch ref (no fetch, no base). If the branch was force-pushed remotely while the agent was down and the local ref is stale, the agent resumes on the old commits with no warning. RESOLVED for the orchestrator spawn path: `ensure_worktree_self_heal` (#692) now runs a PRESENT worktree — and a re-added one recovered from a surviving ref — through the same `_refresh_present_worktree` classification at every respawn (fast-forward when behind, reset only for a non-author role when diverged), not just after a vanished clone. The GitService commit path still uses the bare, unrefreshed `ensure_worktree_for_resume`. | low |
|
||||
| PAT-leak scan cannot run if .git was wiped by a prior failed rmtree | roboco/services/workspace.py:1391 | _assert_no_pat_leak (line 1352) guards on `git_dir.exists()` and returns early if not. If a catastrophic clone left .git partially absent but the auth URL written elsewhere (e.g. into .git/config before .git/objects was created), the early return means the leak check is skipped. Combined with the F063 rmtree-on-failure this is low risk, but a rmtree that fails silently (ignore_errors=True at line 1382 only triggers on leak detection, not on the failure branches) could leave a tokenized .git/config. | low |
|
||||
| dry_upgrade probe lock gap could race a future interleaved sync | roboco/services/workspace.py:1673 | The dep-update probe acquires the _meta-conventions lock only around _clone_local_into (line 1676) and releases it before _probe_lockfile_on_clone (line 1679). The commit msg argues this is safe because any concurrent _sync_read_clone completes under the lock first. This holds ONLY because _sync_read_clone is the sole other holder; if a future change adds a third concurrent mutator of the read clone that interleaves between the release and re-acquire (none today), the local clone could read a half-mutated source. Fragile invariant documented only in the commit, not enforced. | low |
|
||||
| _fetch_origin_best_effort TTL cache not shared between clone root and worktree paths | roboco/services/workspace.py:1038 | _fetch_cache is keyed by str(workspace) on the instance. ensure_workspace is called with the clone-root path, but a worktree-path caller (none currently call ensure_workspace directly with a worktree path, but _resolve_clone_root exists to support worktree-aware uv env) would get a separate cache entry. Not a current bug, but a future worktree-aware ensure_workspace call could double-fetch. | low |
|
||||
| _ensure_agent_owned walk excludes .venv/node_modules but agent may need to write them | roboco/services/workspace.py:67 | _PRUNE_DIRS skips .venv, node_modules, .next etc. from the chown walk for speed. The agent normally owns these (it created them) and the symlinked worktree .venv points to the clone-root .venv which IS walked (it is not under a pruned name at clone root). But a worktree-local .venv created by uv when the symlink was missing (regression risk #1) would NOT be chowned, leaving the agent unable to write into it. Edge case, low severity. | low |
|
||||
|
||||
## Health
|
||||
WorkspaceService is a mature, heavily-instrumented slice with strong defensive hygiene: per-(project,agent) asyncio locks, partial-clone detection + rmtree, a real .git+HEAD+objects health check, scoped + TTL-cached refresh fetches, PAT injection that is never persisted to .git/config, a belt-and-suspenders leak scan that destroys the workspace on any hit, idempotent lockfile-digest-gated dev-deps install, and F123 per-task worktrees that eliminated the coordinator-PM clobber. The F063 + F116 + F123 + F-fix wave closed real deploy-blocker races (PAT leak on half-configured clone, read-clone mid-clone race, root-clobber, dangling venv symlink). Residual risk is concentrated in the worktree venv-symlink timing (first-claim skip depends on a later ensure to self-heal), the resume path reusing a possibly-stale local branch ref without a freshness check, and the process-local (not cross-process) ensure-workspace lock. The slice diverges from CLAUDE.md's "fresh claim git reset --hard" narrative — by design, post-F123 — and that doc drift should be reconciled. Overall integrity is high; the regression risks are edge-case rather than core-path.
|
||||
WorkspaceService is a mature, heavily-instrumented slice with strong defensive hygiene: per-(project,agent) asyncio locks, partial-clone detection + rmtree, a real .git+HEAD+objects health check, scoped + TTL-cached refresh fetches, PAT injection that is never persisted to .git/config, a belt-and-suspenders leak scan that destroys the workspace on any hit, idempotent lockfile-digest-gated dev-deps install, and F123 per-task worktrees that eliminated the coordinator-PM clobber. The F063 + F116 + F123 + F-fix wave closed real deploy-blocker races (PAT leak on half-configured clone, read-clone mid-clone race, root-clobber, dangling venv symlink). Residual risk is concentrated in the worktree venv-symlink timing (first-claim skip depends on a later ensure to self-heal), the GitService commit path's resume re-add still reusing a possibly-stale local branch ref without a fetch (the orchestrator spawn path's own version of this is now resolved by #692's role-aware `_refresh_present_worktree`), and the process-local (not cross-process) ensure-workspace lock. The slice diverges from CLAUDE.md's "fresh claim git reset --hard" narrative — by design, post-F123 — and that doc drift should be reconciled. Overall integrity is high; the regression risks are edge-case rather than core-path.
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -7581,7 +7610,8 @@ Cross-cutting support layer beneath the delivery services: the service-base/erro
|
||||
| `_resolve_assignment` | method | `services/llm.py:248` | Tries AGENT_SLUG first, then (when `role` AND `complexity` both given) the compound `scope_value=f"{role}:{complexity}"` ROLE row, then the plain `role` ROLE row, then GLOBAL — the compound rung reuses the existing ROLE scope + `scope_value` column, no schema change |
|
||||
| `probe_ollama_tags` | func | `services/llm.py:63` | `{base_url}/api/tags` probe; never raises, returns `([], error)` |
|
||||
| `upsert_assignment` | method | `services/llm.py:241` | Insert-or-update by `(scope, scope_value)`; routes non-catalog names to LOCAL; auto-enables LOCAL provider |
|
||||
| `apply_mode` | method | `services/llm.py:567` | Wipe role/global rows (AGENT_SLUG pins preserved) + set GLOBAL for anthropic/grok/ollama/self_hosted/codex/gemini; per-agent map for mix. `apply_mode('cost_tiered')` is the one exception — it ADDITIVELY seeds a single day-1 `developer:low` → haiku override instead of wipe-then-seed, so it composes with whatever mode is already active. |
|
||||
| `apply_mode` | method | `services/llm.py:567` | Wipe role/global rows (AGENT_SLUG pins preserved) + set GLOBAL for anthropic/grok/ollama/self_hosted/codex/gemini; per-agent map for mix. `apply_mode('cost_tiered')` is the one exception — it ADDITIVELY seeds `_COST_TIERED_SEED` instead of wipe-then-seed, so it composes with whatever mode is already active. **Retired to an empty tuple (#680, 2026-07-24)**: the one entry (`developer:low` → haiku) is gone — haiku can't reliably emit the structured lifecycle envelopes, and the structured-verb capability floor below would have silently upgraded the seeded row back to sonnet anyway. `cost_tiered` mode itself stays wired (an empty seed is inert) so an operator can re-seed a genuinely above-floor `role:complexity` tier later. |
|
||||
| `_below_capability_floor` / `_floor_below_capability` | functions | `services/llm.py:189,354` | Structured-verb capability floor (#680): any Anthropic assignment that resolves to a haiku-class model — from a pin, a plain ROLE row, or the now-empty cost-tiered seed — is upgraded to `sonnet` at resolution, in both the assignment and legacy paths. Non-Anthropic providers are untouched (an Anthropic-tier floor, not a provider policy); `pr_reviewer`/`auditor` stay on opus regardless. |
|
||||
| `derive_mode` | method | `services/llm.py:443` | Settings UI label from current assignments |
|
||||
| `set_ollama_api_key` / `set_grok_api_key` | methods | `services/llm.py:340,360` | Encrypt+enable / clear+disable on the seeded provider row |
|
||||
| `_interactive_exempt` | func | `services/llm.py:176` | True when `agent_slug` is `intake-1`/`secretary-1` AND the resolved assignment routes to OPENAI/GEMINI — neither provider supports the interactive chat driver, so these two agents stay on Anthropic regardless of a fleet-wide mode switch; an explicit AGENT_SLUG pin onto either provider for these two slugs is refused loudly at the spawn/save boundary rather than silently producing a broken interactive session |
|
||||
@@ -7903,7 +7933,7 @@ The revision-findings ledger: the structured replacement for prose-only QA/PR-ga
|
||||
| Name | Kind | File:Line | Responsibility |
|
||||
|---|---|---|---|
|
||||
| `TaskReviewFindingTable` | ORM class | `roboco/db/tables.py` | The append-only ledger row: `task_id`, `origin`, `round`, `author_slug`, `file`/`line`/`severity`/`criterion`/`expected`/`actual`/`fix`/`evidence`, `status`, `addressed_by_commit`, `resolution_note`. `origin`/`severity`/`status` are plain `String` columns, not a native Postgres enum. |
|
||||
| `Finding` | Pydantic model | `roboco/foundation/policy/content/models.py:92` | One structured finding, shared by `post_pr_review` (external PRs) and the four internal producers. Caps: `file` ≤300 (repo-relative, no `..`), `line` ≥1, `expected`/`actual` ≤300, `fix` ≤500, `evidence` ≤2000. `criterion` has **no Pydantic `max_length`** despite the DB column being `String(500)` — see Regression Risks. |
|
||||
| `Finding` | Pydantic model | `roboco/foundation/policy/content/models.py:92` | One structured finding, shared by `post_pr_review` (external PRs) and the four internal producers. Caps: `file` ≤300 (repo-relative, no `..`), `line` ≥1, `expected`/`actual` ≤300, `fix` ≤500, `evidence` ≤2000. `criterion` has **no Pydantic `max_length`** despite the DB column being `String(500)` — see Regression Risks. `file` is additionally shape-gated (`_PATH_SHAPE_RE`, #687): a value that doesn't look like a repo-relative path (prose like a PR reference, which used to validate and then doomed the panel's code-snippet fetch) is rejected with a remediate naming the file-less option for cross-cutting findings; the class admits `+`/`@` (SvelteKit route files, `@types` dirs, `@2x` assets) but excludes spaces, the prose signal. `file` remains OPTIONAL for the `issues` shim's file-less findings. |
|
||||
| `PmReviewContent` | Pydantic model | `roboco/foundation/policy/content/models.py` | New content type `"pm_review"` (`summary` + `findings`, no separate `verdict` — the transition to `needs_revision` IS the verdict); mirrors to the new `tasks.pm_notes` column via `_MIRROR_COLUMN`. |
|
||||
| `ReviewFindingsRepository` | class | `roboco/services/repositories/review_findings.py:32` | `insert_many` (append rows, one flush, no independent commit), `list_for_task` (default cap 500, newest round first), `status_counts_for_task` (SQL `GROUP BY (origin, status)`, whole ledger — independent of the 500 cap), `mark_addressed` (8-char-prefix match against OPEN rows, no-op on 0 or >1 matches, never raises), `mark_verified` (bulk, by full id), `mark_waived` (exists, unwired — no verb calls it). |
|
||||
| `findings_count_guard` / `findings_count_hint` | functions | `roboco/services/gateway/choreographer/findings.py:98,115` | Hard-reject `Envelope` above `FINDINGS_HARD_CAP=10`; non-blocking hint above `FINDINGS_NUDGE_COUNT=5`. |
|
||||
@@ -7924,7 +7954,7 @@ The revision-findings ledger: the structured replacement for prose-only QA/PR-ga
|
||||
| `hint_for_open_findings` | function | `roboco/services/gateway/remediation.py:36` | Builds the `i_am_done(resolved_findings=[...])` remediation string naming every still-open `[F-id8]`. |
|
||||
| `_open_findings_prompt_block` / `_revision_bounced_block` | functions | `roboco/runtime/orchestrator.py` | Capped-10 open-findings render for the `REVISION_REQUIRED` dev prompt and the PM triage "bounced" block; fails open to `""` on any DB error. |
|
||||
| `GET /{task_id}/findings` | route | `roboco/api/routes/tasks.py` | `list_for_task` (cap 500) + `status_counts_for_task` (SQL aggregate, whole ledger) → `TaskFindingsResponse{findings, summary, total, truncated}`. No role restriction beyond an authenticated agent context. |
|
||||
| `TabFindings` | React component | `panel/src/components/tasks/task-detail/tab-findings.tsx` | Per-round grouping, origin summary badges, severity/status badges, file:line, collapsible evidence, truncation footer. |
|
||||
| `TabFindings` | React component | `panel/src/components/tasks/task-detail/tab-findings.tsx` | Per-round **collapsible** sections (newest expanded, #687), origin summary badges, severity/status badges, file:line, collapsible evidence, truncation footer; only attempts a code-snippet fetch for a path-shaped `file` — a historical prose ref (pre-#687 data) renders as plain metadata instead of a doomed loader. |
|
||||
|
||||
## Data Flow
|
||||
|
||||
@@ -9178,8 +9208,9 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.25.0) is the s
|
||||
| `panel/src/app/(dashboard)/layout.tsx` | Dashboard shell: sidebar + header + connection status |
|
||||
| `panel/src/app/(dashboard)/overview/page.tsx` | Overview = `<CommandCenter/>` |
|
||||
| `panel/src/app/(dashboard)/metrics/page.tsx` | Metrics page: Usage + Delivery tabs, summary/donut/charts |
|
||||
| `panel/src/app/(dashboard)/tasks/page.tsx` + `tasks/[taskId]/page.tsx` | Task list + task detail (tabbed) |
|
||||
| `panel/src/app/(dashboard)/kanban/page.tsx` | Operator kanban (dev/qa/pm/pr-review views) |
|
||||
| `panel/src/app/(dashboard)/tasks/page.tsx` + `tasks/[taskId]/page.tsx` | Task list, now with top-level **List\|Kanban tabs** (#694/#688) sharing the page's URL filter state — List renders the existing `TaskFilters`+`TaskTable` unchanged, Kanban embeds the four kanban views (dev/qa/pr-review/pm) as nested sub-tabs, a controlled team filter shared bidirectionally with the List tab; + task detail (tabbed) |
|
||||
| `panel/src/app/(dashboard)/kanban/page.tsx` | **Redirects to the Tasks page's Kanban tab** (#694: `feat(panel): redirect /kanban to Tasks kanban tab, remove sidebar entry, swap bottom tab bar to Agents`) — the standalone kanban surface is gone from the sidebar; `KanbanBoard`'s dnd-kit drag-and-drop and mobile single-column nav are unchanged, just reached from `tasks?tab=kanban` now |
|
||||
| `panel/src/app/(dashboard)/projects/[projectId]/settings/page.tsx` | **Project settings full page** (Wave C, replacing the ~30-field `edit-project-dialog.tsx`, which is deleted): a card-per-concern grid — `identity-card` (name/slug/Forge/GitHub App), `git-auth-card` (PAT), `placement-card` (cell/team, protected branches), `environments-card` (env ladder), `cicd-commands-card`, `sandbox-card` (services + extensions), `budget-ops-card` (monthly budget, video engine toggle, CI-watch/dep-update/ci-watch toggles) — each with its own per-card save (`save-bar.tsx`), plus Conventions as a page-level tab at natural width instead of a cramped dialog tab |
|
||||
| `panel/src/app/(dashboard)/prompter/page.tsx` | Intake chat (single + MegaTask batch scope) |
|
||||
| `panel/src/app/(dashboard)/agents/page.tsx` | Agents hub: Fleet (roster/spawn/status, default) + Conversations + Journals tabs via `?tab=fleet\|conversations\|journals` — `AgentsFleetView` (fleet roster/spawn/activity), `A2AView` (org-wide switchboard/list + transcript + CEO reply composer + "New DM" dialog, live via `/ws/system` `a2a.message` frames — a pure lift of the old standalone `/a2a` page body), and `JournalsView` (per-agent reflection log — a pure lift of the old standalone `/journals` page body, wave 11) |
|
||||
| `panel/src/app/(dashboard)/a2a/page.tsx` | Redirect shim → `/agents?tab=conversations` (A2A folded into the Agents hub's Conversations tab, wave 9) — kept so old bookmarks/links still resolve |
|
||||
@@ -9200,8 +9231,12 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.25.0) is the s
|
||||
| `panel/src/lib/telegram/webapp.ts` | Typed wrapper over the global `window.Telegram.WebApp` (`ready`/`expand`/`initData`, `TelegramThemeParams`/`MainButton`/`BackButton`/`HapticFeedback` interfaces); `waitForTelegramWebApp` polls (100ms, 1.5s timeout) for the CDN script since it loads `afterInteractive`; `createDevMockWebApp`/`isDevMockWebApp` back the `/tg?demo=1` dev-browser fallback; null-safe `haptics` const (no-ops outside Telegram) |
|
||||
| `panel/src/lib/telegram/{hooks.tsx,theme.ts,demo.ts,demo-data.ts}` | `hooks.tsx`: `TgWebAppProvider`/`useTgWebApp` context + `useMainButton`/`useBackButton` (declarative wrappers over Telegram's native buttons — no consumer touches `window.Telegram` directly). `theme.ts`: `applyTelegramTheme` maps `themeParams` → shadcn CSS vars, hex-only trust boundary, scoped to `#tg-shell` only (the desktop dashboard is untouched); `startTelegramThemeSync` re-applies on the bridge's `themeChanged` event. `demo.ts`/`demo-data.ts`: `isTgDemoMode()` (dev-only, `?demo=1`, dead-code-eliminated in prod) gates dynamically-imported `DEMO_TASKS`/`DEMO_NOTIFICATIONS`/`DEMO_RELEASE`/`DEMO_X_POSTS`/`DEMO_VIDEO_POSTS`/`DEMO_ROADMAP`/`DEMO_TODAY` fixtures |
|
||||
| `panel/src/components/settings/` | feature-flags-card, ai-routing-card, transcript-retention-card, self-hosted-section, `x-credentials-card.tsx` (write-only OAuth 1.0a secrets, mounted in `settings/page.tsx`) |
|
||||
| `panel/src/components/conventions/conventions-tab.tsx` | Per-project architecture map + health (in edit-project dialog) |
|
||||
| `panel/src/components/projects/`, `products/`, `agents/`, `business/`, `auditor/`, `knowledge-base/`, `git/`, `journals/`, `work-sessions/`, `notifications/`, `rate-limit/`, `layout/`, `ui/` | Per-domain component groups (`projects/` and `products/` each export a `*-view.tsx` consumed by `workstation/page.tsx`, plus a `*-card-grid.tsx` reusing the sibling table's exported badge renderers; `agents/` similarly exports `agents-fleet-view.tsx` (`AgentsFleetView`) consumed by `agents/page.tsx`'s Fleet tab, plus `agent-card.tsx`'s DM quick-action button; `journals/` similarly exports `journals-view.tsx` (`JournalsView`) consumed by `agents/page.tsx`'s Journals tab); `ui/` = Radix-based primitives (dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown) |
|
||||
| `panel/src/components/conventions/conventions-tab.tsx` | Per-project architecture map + health — now a page-level tab on `/projects/[id]/settings` (Wave C; was a cramped tab inside the deleted edit-project dialog) |
|
||||
| `panel/src/components/projects/settings/*.tsx` | Wave C card-per-concern settings surfaces (`identity-card`, `git-auth-card`, `placement-card`, `environments-card`, `cicd-commands-card`, `sandbox-card`, `budget-ops-card`, `save-bar.tsx`) backing `/projects/[id]/settings`, replacing the single ~1061-line `edit-project-dialog.tsx` (deleted) |
|
||||
| `panel/src/components/projects/quick-edit-project-dialog.tsx` | Wave C: slim replacement for the old kitchen-sink edit dialog — name/cell/active only; the project list's Edit action now routes to the full settings page instead |
|
||||
| `panel/src/components/layout/command-palette.tsx` + `hooks/use-command-palette.ts` | Wave C (#694/#670): global Cmd+K palette — Radix Dialog + combobox searching tasks/agents/projects/pages, `localStorage` recents (`lib/command-palette-recents.ts`, key `roboco-cmd-recents`), fuzzy match (`lib/fuzzy-match.ts`), keyboard nav; mounted globally in the dashboard layout, wired to the header search trigger via `useUIStore.setCommandPaletteOpen` (`components/layout/header.tsx`) |
|
||||
| `panel/src/components/ui/collapsible-section.tsx` | Wave C dialog-standardization: the single sectioned-disclosure primitive, moved from `tasks/task-detail/` to `ui/` — task dialogs' raw `Collapsible` and create-project's ad-hoc `showAdvanced` both converge onto it. Every hand-rolled dialog footer across the panel became `DialogFooter`; dialog widths collapsed from ten ad-hoc classes to three named sizes (deliberate outliers annotated). No behavioral change. |
|
||||
| `panel/src/components/projects/`, `products/`, `agents/`, `business/`, `auditor/`, `knowledge-base/`, `git/`, `journals/`, `work-sessions/`, `notifications/`, `rate-limit/`, `layout/`, `ui/` | Per-domain component groups (`projects/` and `products/` each export a `*-view.tsx` consumed by `workstation/page.tsx`, plus a `*-card-grid.tsx` reusing the sibling table's exported badge renderers; `agents/` similarly exports `agents-fleet-view.tsx` (`AgentsFleetView`) consumed by `agents/page.tsx`'s Fleet tab, plus `agent-card.tsx`'s DM quick-action button; `journals/` similarly exports `journals-view.tsx` (`JournalsView`) consumed by `agents/page.tsx`'s Journals tab); `ui/` = Radix-based primitives (dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown, `collapsible-section`) |
|
||||
| `panel/src/hooks/use-websocket.ts` | Shared `useWebSocket<T>(path, handlers?, isSystem?)` hook (auto-reconnect, heartbeat) |
|
||||
| `panel/src/hooks/use-{tasks,agents,projects,products,usage,prompter,secretary,dashboard,git,journals,notifications,knowledge-base,observability,work-sessions,providers,rate-limit-{sync,websocket}}.ts` | TanStack Query + zustand data hooks |
|
||||
| `panel/src/hooks/use-a2a-live.ts` | `useA2AConversations` / `useA2AAdminPairs` / `useA2AMessages` (TanStack Query over `a2aApi`) + `useReplyAsCeo` / `useCreateCeoConversation` / `useSendCeoMessage` mutations; `a2aLiveKeys` query-key namespace |
|
||||
@@ -9232,10 +9267,11 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.25.0) is the s
|
||||
| Agents — Conversations (switchboard + reply + New DM) | `app/(dashboard)/agents/page.tsx` (`?tab=conversations`) + `components/a2a/a2a-view.tsx` + `components/a2a/*` | CEO watches every agent-to-agent conversation live: default org-chart switchboard (pair cards grouped by cell/PM-chain/board, pulsing on fresh `a2a.message` frames) or the classic conversation list; drill-in shows the transcript + a reply composer that lets the CEO chime into a watched thread as itself (task-linked conversations only). "New DM" opens a fresh CEO-owned 1:1 with any DM-capable agent (no task link needed) — either from the dialog's own picker or preselected from a validated `?dm=` deep link off the Fleet tab; the recipient is woken via the a2a_request dispatch path if offline, and the CEO's own threads render with `A2ADirectComposer` instead of the reply composer. `/a2a` redirects here |
|
||||
| Agents — Journals | `app/(dashboard)/agents/page.tsx` (`?tab=journals`) + `components/journals/journals-view.tsx` + `components/journals/*` | Per-agent reflection log lifted into the Agents hub's third tab: agent picker (left, local search state seeded from the last saved query) + a type/task-filterable entry list (right, `JournalView`); each task-linked entry carries a Task badge + copy-UUID button. Entry detail still lives at `/journals/{entryId}`, its back-links now pointing at `/agents?tab=journals`. `/journals` redirects here |
|
||||
| Workstation | `app/(dashboard)/workstation/page.tsx` + `components/{products,projects}/*-view.tsx` | Products + Projects as one sidebar entry, tab-switched via `?tab=`; each surface has a Cards\|Table view toggle (default Cards, persisted per-surface via `ui-store`'s `productsView`/`projectsView`) with client-side name/cell(-count) sorting in card view; Projects' q/cell/inactive filters are local `useState`, not URL params (scroll-bounce prevention) |
|
||||
| Project Settings / Conventions | `components/projects/edit-project-dialog.tsx` + `components/conventions/conventions-tab.tsx` | Per-project `.roboco/conventions.yml` map + health; Save / Restore via PR. Also carries the "Forge" `<Select>` (Auto-detect/GitHub/Gitea/GitLab, `project.git_provider`) — see `docs/map/worksession-git.md` |
|
||||
| Project Settings / Conventions | `app/(dashboard)/projects/[projectId]/settings/page.tsx` + `components/projects/settings/*.tsx` + `components/conventions/conventions-tab.tsx` | Wave C full-page project settings (card-per-concern, per-card save) — was the ~30-field `edit-project-dialog.tsx` (deleted). The list's Edit action routes here; a slim `quick-edit-project-dialog.tsx` (name/cell/active) covers the fast path. Conventions is a page-level tab: per-project `.roboco/conventions.yml` map + health, Save/Restore via PR. The "Forge" `<Select>` (Auto-detect/GitHub/Gitea/GitLab, `project.git_provider`) now lives on `identity-card.tsx`; protected branches on `placement-card.tsx`; env ladder on `environments-card.tsx`; video engine + budget on `budget-ops-card.tsx` — see `docs/map/worksession-git.md` |
|
||||
| Command Palette | `components/layout/command-palette.tsx` + `hooks/use-command-palette.ts` | Global Cmd+K search over tasks/agents/projects/pages with recents + fuzzy match; triggered from the header search box |
|
||||
| Usage Dashboard | `components/dashboard/usage-overview-panel.tsx` + `hooks/use-usage.ts` | Token/cost totals; live WS snapshot with HTTP-polling fallback |
|
||||
| Git | `app/(dashboard)/git/page.tsx` | Repository / Work Sessions tabs (business-page tab idiom, `?tab=`); `GitBrowser` (status/branches/log/diff + actions incl. confirm-gated "Clean Up Stale Branches") and `WorkSessionsView` (active sessions, search/status filter kept LOCAL not in URL params); old `/work-sessions` route now redirects to `/git?tab=sessions` |
|
||||
| Kanban | `components/kanban/{core,views}/*` | dnd-kit drag board; dev/qa/pm/pr-review views; drag routes through admin status-override with bypass-precondition prompt |
|
||||
| Kanban | `app/(dashboard)/tasks/page.tsx` (Kanban tab) + `components/kanban/{core,views}/*` | dnd-kit drag board; dev/qa/pm/pr-review views; drag routes through admin status-override with bypass-precondition prompt. Reached via the Tasks page's **List\|Kanban tabs** (#694); the standalone `/kanban` route now redirects here and its sidebar entry is gone (bottom tab bar swapped to Agents) |
|
||||
| Task Detail | `components/tasks/task-detail/*` | Tabbed: overview, plan, progress, commits, sessions, notes, dependencies, **findings**, AC, action dialogs |
|
||||
| AI Providers | `app/(dashboard)/settings/ai-providers/page.tsx` + `components/settings/ai-routing-card.tsx` | Per-slug/role/global model routing |
|
||||
| Telegram Mini App | `app/(tg)/tg/page.tsx` + `components/tg/*` | The CEO's phone cockpit, outside the `(dashboard)` shell: 5 tabs — Today (default: `GET /telegram/today` spend/fleet/velocity/needs-you brief + a 4-action ops ring), Approvals (native card stack over the 4 held-artifact queues, Telegram `MainButton`/`BackButton`-driven), Inbox (notifications + ack), Board (`mobile-task-board.tsx` read-only grouped-by-status + a `tg-task-sheet.tsx` detail sheet), Chat (A2A conversation list/compose/thread, now WS-live via `/ws/system`). Bootstraps via Telegram `initData` → `/telegram/webapp-auth`, requires both `telegram_miniapp_enabled` and `cloud_auth_enabled` armed server-side; `themeParams` scoped to `#tg-shell` only; a dev-only `?demo=1` mock bridge + fixtures make the whole surface workable in a plain browser |
|
||||
@@ -9487,6 +9523,9 @@ Deliberately **not** on this card (compose/env-coupled, unsafe for a runtime tog
|
||||
> - **Telegram Mini App V4** (2026-07-19, PR #576 + #582 + `a072b980` fix, "Today brief, native approvals, live data, bot tier, chat bridges" + "premium Mini App cockpit") — the cockpit gets a 5th default-opening tab and a native-app visual overhaul. `TgTabBar` gains `"today"` as the first tab. New `TgTodayTab` renders off ONE `GET /telegram/today` round trip (TanStack Query, 45s `refetchInterval` + WS-invalidate on any `USAGE_SNAPSHOT` frame): `SpendHero` (cost figure, signed delta-vs-yesterday chip, a 7-day `Sparkline`), a `TgCircleAction` quick-action ring, a `NeedsYouBanner`, a live `TgAvatar` "Fleet" block, and a "Shipped this week" `DayBars` block. New shared primitives in `ui.tsx` (`TgCircleAction`/`TgAvatar`/`TgSection`/`TgRow`/`TgRowIcon`/`TgStat`, all new). `TgApprovalsTab` becomes a genuine native card stack over the new `approvals/` subdirectory, where `PrimaryAction` drives Telegram's `MainButton` via a new `useMainButton` hook and the tab itself wires `useBackButton` for focused-card back nav — both null-safely fall back to a visible button outside Telegram. `useApprovalQueue` normalizes all 4 held-draft sources into one list and surfaces `anyFailed` so a dead queue source is never silently rendered as "queue is clear". `TgChatTab` drops polling for the desktop A2A idiom: `useA2ALiveStream()` invalidates on every `a2a.message` frame with a 10s poll fallback only while `/ws/system` is down. New Telegram-native theme adoption: `webapp.ts` gains theme/button/haptics types + `createDevMockWebApp`/`isDevMockWebApp` (the `/tg?demo=1` dev-browser fallback, `NODE_ENV==="development"`-gated); new `hooks.tsx` + `theme.ts` apply `themeParams` → shadcn CSS vars **scoped to `#tg-shell`** (the desktop dashboard is untouched); `(tg)/layout.tsx` gains the `id="tg-shell"` scoping hook + Share Tech Mono font loading; `globals.css` ships a constant dark-amber `#tg-shell` skin independent of dashboard light/dark, that Telegram's `themeParams` inline-override on top of. `a072b980` (same-day fix) closes a prod-only bug: a plain-browser `/tg` visit gets the WebApp bridge with empty `initData` (the CDN script still loads outside Telegram), which was POSTed and 422'd into "Couldn't sign in" — `tg/page.tsx` now shows the "Open from Telegram" wall for ANY non-dev-mock bridge with empty `initData`. Follow-up polish (`c7605b0d` #582): `charts.tsx` (`Sparkline`/`DayBars`, theme-driven via `currentColor`); `tg-inbox-tab.tsx`/`tg-chat-tab.tsx` adopt `TgAvatar` tokens; `TgRowIcon` gains a `tone` prop so `tg-approvals-tab.tsx`'s `KIND_META` color-codes each queue kind; backend `/telegram/today` gains `spend.series`+`delta_pct`+`velocity` — see `docs/map/notification.md` for the backend side.
|
||||
> - **"feature/tg-miniapp-v5"** (2026-07-19) — a further visual/interaction polish pass on the V4 cockpit, additive-only (the one relocation: Today's standalone "Approve" button becomes a 4-item ops ring — Approvals is still reachable via the tab bar). New `tg-icons.tsx` (175 lines) — 9 SVG duotone components (`IconToday`/`IconSeal`/`IconInbox`/`IconBoard`/`IconChat`/`IconShip`/`IconAckAll`/`IconSweep`/`IconFleet`), scoped to hero surfaces only (utility chrome stays lucide-react); wired into `tg-tab-bar.tsx` (the 5 tab icons) and `tg-today-tab.tsx` (the new ops ring). New `motion.tsx` (105 lines) — `useCountUp(target, durationMs=650)` (rAF ease-out-cubic, instant under `prefers-reduced-motion`) and `TgSheet` (bottom-sheet dialog wired to Telegram's native `BackButton`, backdrop-tap-to-close, haptic on open); used by the new `tg-task-sheet.tsx` and by `tg-today-tab.tsx`'s Fleet/Sweep sheets. New `tg-task-sheet.tsx` (151 lines) — `TgTaskSheet`: read-only task-detail bottom sheet (status badge, "bounced ×N" chip, ACs, up to 5 open revision findings via `useTaskFindings` with an overflow line, "Open PR #N" link; skips the findings fetch in demo mode); rendered from `tg-board-tab.tsx` alongside `MobileTaskBoard` via a new `onTaskPress` prop. New vendored `ShareTechMono-Regular.woff2` under `app/(tg)/fonts/`, loaded via `next/font/local` as `--font-share-tech` on `#tg-shell`, applied only to the display voice (labels/numerals/wordmark) — never body text. `tg-today-tab.tsx` (largest diff) replaces the old single "Approve" action with a 4-item ops ring (Ship/Ack-all/Sweep/Fleet): `SpendHero`'s numeral gains count-up, a new `FleetSheet` shows the full roster (opened by tapping the truncated 3-agent preview), `runAckAll` bulk-acks via the existing notifications API, `runSweep` calls the existing `gitApi.cleanupBranches` per has-token project (no new backend surface), plus a blinking-cursor "ROBOCO_" wordmark header. `demo-data.ts` gains `DEMO_TASKS`/`DEMO_NOTIFICATIONS` fixtures.
|
||||
> - **"panel-perf-p3-p4"** (2026-07-19) — kanban + task-table + scorecards perf pass, paired with the backend N+1 fix in `docs/map/metrics-observability.md`. `kanban-board.tsx`: `tasksByStatus` grouping wrapped in `useMemo`, `handleAction` stabilized via `useCallback` + a `tasksRef` so child memoization actually holds. `kanban-card.tsx`: wrapped in `React.memo`. `kanban-column.tsx`: genuine windowing via the new `@tanstack/react-virtual` dependency — `useVirtualizer` (132px estimated card height, overscan 6) replaces a plain `.map()`, only visible virtual rows mount, absolute-positioned + `measureElement`'d for real-height correction; the column itself stays the dnd-kit droppable target so windowing doesn't break drag targeting. `task-table.tsx` (673 changed lines, no virtualization): row/card JSX extracted into top-level `memo`-wrapped `TaskTableRow`/`TaskTableCard`, fed stable props (`toggleExpand` via `useCallback`, lookup maps passed through) — pagination (pre-existing) is the actual bound on rendered rows, not windowing. `scorecards-tab.tsx`: `MemberRow` no longer self-fetches — `ScorecardsTabContent` calls the new `useAllMemberScorecards()` once and passes each row its slice via a `useMemo`'d Map; a table-level "Failed to load member scorecards" banner replaces the old per-row failure cell.
|
||||
> - **"Panel consistency & UX wave"** (#694/#688, 2026-07-24) — five streams. Stream1: project/task dialog field-sync audit (`docs/frontend/forms/forms-audit.md`, `project-fields-audit.md`) plus settings-page save-feedback toasts. Stream2: the Cmd+K **command palette** (`command-palette.tsx` + `use-command-palette.ts`, fuzzy-match + localStorage recents) wired into the header search box. Stream3: the Tasks page gains **List\|Kanban tabs** sharing URL filter state, and a follow-up (#679) redirects the standalone `/kanban` route here, drops its sidebar entry, and swaps the bottom tab bar's kanban slot to Agents. Stream4: a responsiveness/touch-target sweep — `Button size="sm"` 32px→36px, `AlertDialog` gains the same scroll-at-short-viewport pattern `Dialog` already had, the Scorecards Members table wrapped in `ResponsiveTable`, and (after `sync_branch`'s rebase reset silently dropped the fix twice — see `docs/map/worksession-git.md` #683) every remaining sub-36px icon button across 8 files bumped to 36px. A closing pass (`73275ff0`) corrects the task-budget help text for the removed `TaskType`-default fallback (#666) and moves the forms-audit docs into the governed `docs/frontend/forms/` tree.
|
||||
> - **Project settings full page** (`0e97fe6a`, Wave C-2, 2026-07-24) — the ~30-field, 7-tab `edit-project-dialog.tsx` (1061 lines) is deleted outright. Settings now live at `/projects/[id]/settings`: a card-per-concern grid (`components/projects/settings/*`, one card per concern — identity, git auth, placement, environments, CI/CD commands, sandbox, budget/ops) with per-card save, Conventions promoted to a page-level tab at natural width, and a new slim `quick-edit-project-dialog.tsx` (name/cell/active) for the fast path. The project list's Edit action now routes to the settings page instead of opening a dialog.
|
||||
> - **Dialog standardization** (`581f0320`, Wave C-2, 2026-07-24) — no behavioral change: `collapsible-section.tsx` moves from `tasks/task-detail/` to `ui/` as the single sectioned-disclosure primitive (task dialogs' raw `Collapsible` and create-project's ad-hoc `showAdvanced` both converge onto it); every hand-rolled dialog footer across the panel becomes `DialogFooter`; dialog widths collapse from ten ad-hoc classes to three named sizes, deliberate outliers annotated inline.
|
||||
|
||||
## Regression Risks
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ The Choreographer is the server-side composition layer that turns agent intent-v
|
||||
| `_briefing_for` | async helper | `_impl.py:813` | Builds `context_briefing`. Claim-scoped: `full=True` (context-acquisition verbs only — give_me_work/claims/plan/resume/triage) carries the heavy sections via `_heavy_briefing_sections` (company_goals, recent_team_activity, blockers, task_handoff, institutional_memory); every other verb gets slim signals-only (unread a2a/mentions/notifications + metadata gaps). `include_company_goals=True` is a narrower, cheap-only opt-in (`_resolve_company_goals`) that fetches just the company_goals singleton without the rest of `full`'s heavy sections — used by `board_triage`'s idle branch so the CEO's charter (brand_voice/north_star) still reaches the Product Owner's roadmap-exploration / Head of Marketing's feature-spotlight-exploration one-shot spawns, whose directly-assigned exploration task is never itself a strategic root awaiting PM review (so the `full=True` branch never fires for it). AC coverage stays independent of `full`. |
|
||||
| `_run_claim_guards` | async helper | `_impl.py:916` | already_active / paused / unmet_dependency (with re-check race narrowing) + `_lane_claim_guard`; `skip_dev_guards=False` param skips dev-only guards for pr_reviewer gate claims (claim_gate_review). |
|
||||
| `_lane_claim_guard` | async helper | `_impl.py:977` | Out-of-order-start barrier: refuse code leaf behind an earlier open same-assignee sibling. Fail-closed on lookup error. |
|
||||
| `_claim_plan_start_gate` | async helper | `_impl.py:1179` | spec gate → advisory claim lock (non-PM) → behavioral guards. |
|
||||
| `_sequencing_claim_guard` | async helper | `_impl.py:1230` | Proactive claim-time check (#681): consults `TaskService.sequence_hold_reason` and, when held, returns `Envelope.sequence_held` (naming the blocking sibling) BEFORE the claim even reaches `TaskService.claim`'s bare-`None`-on-reject path — closes the misdiagnosed `INVALID_STATE` a held PENDING/NEEDS_REVISION claim used to surface as. |
|
||||
| `_claim_plan_start_gate` | async helper | `_impl.py:1179` | spec gate → advisory claim lock (non-PM) → behavioral guards → `_sequencing_claim_guard`. |
|
||||
| `_claim_plan_start_run` | async helper | `_impl.py:1251` | `runner.run_intent(verb)` + ensure_work_session + `_touch`. |
|
||||
| `i_will_work_on` | async verb | `_impl.py:1326` | Dev claim+plan+start path; routes re-entry vs fresh claim. |
|
||||
| `open_pr` | async verb | `_impl.py:1571` | Pre-flight + `run_intent("open_pr")` (push_branch + create_pr side effects). |
|
||||
@@ -48,6 +49,9 @@ The Choreographer is the server-side composition layer that turns agent intent-v
|
||||
| `complete` | async verb | `_impl.py:6599` | Role-dispatch to `cell_pm_complete` / `main_pm_complete`; umbrella-in-progress bypasses spec gate. |
|
||||
| `main_pm_complete` | async verb | `_impl.py:6496` | Main PM merge + escalate to CEO (never merges master itself). |
|
||||
| `escalate_to_ceo` | async verb | `_impl.py:6844` | Escalate to `awaiting_ceo_approval`. |
|
||||
| `unblock` | async verb | `_impl.py:7041` | Human/PM restores a BLOCKED task; gated by `_oscillation_unblock_guard` (refuses while oscillation-tripped) then, on a real restore, runs `_maybe_trip_oscillation_breaker` (#685). |
|
||||
| `_oscillation_unblock_guard` | async helper | `_impl.py:7184` | Refuses `unblock` once `markers.is_oscillation_tripped(t)` — an admin override must clear the marker first. |
|
||||
| `_maybe_trip_oscillation_breaker` | async helper | `_impl.py:7209` | Task-scoped strike counter (#685) at the `unblock()` chokepoint (agent-agnostic — legitimate `needs_revision` rework never calls `unblock`, so it structurally can't trip this): `markers.bump_oscillation_strikes(t, progress_fp)` where the progress fingerprint is commit count + `revision_count` + `terminal_children_count` (a coordination root's children advancing between escalations resets the count); past threshold, sets `blocker_resolver_type=HUMAN`, marks the task oscillation-tripped, and best-effort notifies the CEO (`_notify_ceo_oscillation`) naming both agents + the cycle count. |
|
||||
| `VerbRunner.run_intent` | async method | `_verb_runner.py:37` | pre_side_effects → SAVEPOINT(composes) → side_effects; intermediate-None raises INVALID_STATE. |
|
||||
| `VerbRunner._do_pr_merge` | async handler | `_verb_runner.py:257` | `pr_merge` with `project_id` scoping (cross-repo collision fix) + `resolve_parent_branch`. |
|
||||
| `ChoreographerHelpers` | stub class | `_protocol.py:31` | TYPE_CHECKING-only typed view of `self` helpers for role mixins. |
|
||||
|
||||
@@ -125,6 +125,7 @@ The support layer of the agent gateway: pure/cheap components the Choreographer
|
||||
| hint_for_missing_progress | function | roboco/services/gateway/remediation.py:11 | Hint: make a commit before i_am_done. |
|
||||
| hint_for_missing_reflect | function | roboco/services/gateway/remediation.py:18 | Hint: call note(scope='reflect',...). |
|
||||
| hint_for_unaddressed_acceptance_criteria | function | roboco/services/gateway/remediation.py:25 | Hint: every AC needs a referencing artifact. |
|
||||
| hint_for_missing_ac_coverage | function | roboco/services/gateway/remediation.py | #686: a `delegate` child draft rejected for a `covers_parent_criteria` ref that doesn't resolve now gets a copy-pasteable corrected skeleton with the parent's REAL criteria inlined (id when present, quoted exact text otherwise — never a bare `'<id>'` placeholder), instead of a rejection that named the field but never showed its shape. The coverage-reject path self-heals an empty/out-of-length id list on the parent before rendering so the skeleton always has real references; `cell_pm.md`/`main_pm.md` also show the field on every delegation-guidance child draft now. |
|
||||
| hint_for_missing_journal_decision | function | roboco/services/gateway/remediation.py:36 | Hint: call note(scope='decision',...) before complete. |
|
||||
| hint_for_missing_journal_learning | function | roboco/services/gateway/remediation.py:43 | Hint: call note(scope='learning',...) before pass/fail. |
|
||||
| hint_for_missing_qa_notes | function | roboco/services/gateway/remediation.py:50 | Hint: qa_notes must be >=80 chars. |
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ The Pydantic/dataclass domain surface of RoboCo — the typed contract the API,
|
||||
| `AgentInstance` | dataclass | runtime.py:70 | Running Claude Code container record + `usage_session_id` |
|
||||
| `SpawnGitContext` | dataclass | runtime.py:28 | Git context for spawn (project_slug, branch, `task_short_id` for worktree) |
|
||||
| `MODEL_MAP` | dict | runtime.py:106 | short-name→full Claude id (opus→claude-opus-4-6, sonnet→claude-sonnet-5, haiku→…) |
|
||||
| `ROLE_MODEL_MAP` | dict | runtime.py:114 | per-role default tier: developer/cell_pm/main_pm→sonnet, qa/documenter→haiku, pr_reviewer/auditor/board/ceo→opus (qa→haiku, main_pm→sonnet, pr_reviewer→opus are the cost-tuned defaults) |
|
||||
| `ROLE_MODEL_MAP` | dict | runtime.py:120 | per-role default tier: developer/qa/documenter/cell_pm/main_pm→sonnet, pr_reviewer/auditor/board/ceo/prompter/secretary→opus. **Haiku retired from every delivery-lifecycle role (#680, 2026-07-24)**: qa/documenter moved haiku→sonnet — haiku can't reliably emit the structured envelopes the lifecycle runs on (`pass_review`'s per-AC `criteria_verified`, `delegate`'s `covers_parent_criteria`, the findings ledger), so a haiku QA/PM claims, gets validation-rejected, idles, respawns, and loops without progress. A structured-verb capability floor (`_below_capability_floor`/`_floor_below_capability`, `roboco/services/llm.py`) additionally upgrades ANY below-floor Anthropic assignment to sonnet at resolution time — from a pin, a plain ROLE row, or a future map edit — so the floor can't be silently reintroduced. Non-Anthropic providers are untouched (an Anthropic-tier floor, not a provider policy). |
|
||||
| `ROLE_EFFORT_MAP` | dict | runtime.py | per-role `CLAUDE_CODE_EFFORT_LEVEL` override injected at spawn; **empty/inert by default** (opt-in per role after verifying the level moves usage) |
|
||||
| `MODEL_CATALOG` | tuple | llm_catalog.py:67 | Settings-dropdown source of truth; Anthropic entries derived from `MODEL_MAP` |
|
||||
| `PermissionLevel` | IntEnum | permissions.py:15 | CEO=0/BOARD=1/MAIN_PM=2/CELL_PM=3/CELL_MEMBER=4/AUDITOR=99 |
|
||||
|
||||
@@ -109,9 +109,11 @@ The AgentOrchestrator is the runtime brain of RoboCo: it owns the per-agent Dock
|
||||
| AgentOrchestrator._notification_spawn_cooled | method | roboco/runtime/orchestrator.py:4738 | Cross-tick cooldown for notification-triggered spawns (escalation/approval/audit/a2a): one spawn per (agent, notification) per `notification_spawn_cooldown_seconds`; `0` disables (legacy every-tick respawn). |
|
||||
| AgentOrchestrator._notification_spawn_over_cap | method | roboco/runtime/orchestrator.py:4780 | Hard per-(agent, notification) attempt cap (`notification_spawn_max_attempts`, default 5) — the notification-driven analogue of the PM respawn breaker; these dispatchers carry no `task_id` so that breaker never sees them. Past the cap, stop respawning and log once; the count is id-scoped and survives map pruning. |
|
||||
| AgentOrchestrator._prune_notification_spawn_maps | method | roboco/runtime/orchestrator.py:4811 | Evict `_notification_spawn_at` entries older than a cutoff so the in-memory cooldown map doesn't grow unbounded. |
|
||||
| AgentOrchestrator._notification_has_live_work | coroutine | roboco/runtime/orchestrator.py:4848 | Live-work check before spawning a notification's recipient: 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. |
|
||||
| AgentOrchestrator._notification_has_live_work | coroutine | roboco/runtime/orchestrator.py:4848 | Live-work check before spawning a notification's recipient: skip when the notification has expired, is stale past `notification_spawn_max_age_seconds` (default 6h — wedged or reloaded from before a restart), its related task is already terminal, **or (since #685) the related task is HITL-blocked** (`_is_hitl_blocked`) — closes the admin-route escalation side of the oscillation loop the same way the dev/PM dispatchers' own HITL skip does. |
|
||||
| AgentOrchestrator._is_hitl_blocked | staticmethod | roboco/runtime/orchestrator.py:13796 | `status == "blocked" and blocker_resolver_type == "human"` over the WIRE task dict; consulted by `_dev_dispatch_one`, `_dispatch_claimed_without_agent`, and `_notification_has_live_work`. Only actually fires over the wire since #685 stamped `TaskResponse.blocker_resolver_type` — before that the field never serialized, so every wire-dict HITL check silently no-op'd regardless of the in-process row. Budget-breach (#654) and the task-scoped oscillation breaker (#685) both set this same resolver+status pair, so one skip covers both trip conditions. |
|
||||
| AgentOrchestrator._dispatch_claimed_without_agent | coroutine | roboco/runtime/orchestrator.py:14559 | Re-dispatches a `claimed` task whose owning agent isn't running; the RESTORED side of an escalate/unblock round trip (no per-(agent,task) respawn counter of its own) — gated by `_is_hitl_blocked` (#685) so an oscillation-tripped or budget-blocked task stops being re-dispatched here too. |
|
||||
| AgentOrchestrator._fetch_budget_status | coroutine | roboco/runtime/orchestrator.py:8021 | `ROBOCO_TASK_BUDGETS_ENABLED` sweep helper: live-priced spend snapshot for one active task (closed sessions' `estimated_cost_usd` + open sessions priced from token snapshots). |
|
||||
| AgentOrchestrator._task_budget_breach | coroutine | roboco/runtime/orchestrator.py:8053 | `(spend, cap)` tuple when a task's own spend exceeds its `budget_usd` (falling back to the `TaskType` default via `effective_task_budget_usd`), else `None`. |
|
||||
| AgentOrchestrator._task_budget_breach | coroutine | roboco/runtime/orchestrator.py:8053 | `(spend, cap)` tuple when a task's own spend exceeds its `budget_usd` via `effective_task_budget_usd`, else `None`. **Explicit-input only since #666**: a null `budget_usd` resolves to `None` (no cap) — the earlier per-`TaskType` default table (which blocked an unbudgeted coordination root one opus planning turn in) was removed. |
|
||||
| AgentOrchestrator._handle_task_budget_breach | coroutine | roboco/runtime/orchestrator.py:8090 | BLOCKs a breaching task (HUMAN resolver, a budget marker) BEFORE the graceful stop so the ensuing unclaim no-ops and the dispatcher never respawns onto it; notifies the CEO naming both recovery steps (raise the cap or `unblock`). |
|
||||
| AgentOrchestrator._sweep_budget_exceeded | coroutine | roboco/runtime/orchestrator.py:8152 | Iterates active agents past `_check_budget_for_agent`; gated entirely by `ROBOCO_TASK_BUDGETS_ENABLED` — off, neither cap is ever consulted. |
|
||||
| AgentOrchestrator._check_budget_for_agent | coroutine | roboco/runtime/orchestrator.py:8187 | Per-agent budget check called from the sweep; resolves the active task, checks breach, and calls `_handle_task_budget_breach`. |
|
||||
@@ -269,6 +271,9 @@ stateDiagram-v2
|
||||
> - `3b9fd0e0` (PR #551, Telegram V2, 2026-07-17): adds `_telegram_poll_task` + `_telegram_poll_loop`/`_run_telegram_poll_cycle` (mirrors the `_x_mentions_poll_loop` shape: gated on BOTH `telegram_enabled` AND `telegram_inbound_enabled`, started in `start()`, cancelled in `stop()`) driving the new `TelegramInboundEngine` (`roboco/services/telegram_inbound.py`) — long-polls Telegram `getUpdates` and dispatches `/status` `/queue` `/task` commands plus Approve/Reject button callbacks to the SAME CEO-gated service methods the HTTP routes call.
|
||||
> - `b91229f4` (#643, "break the notification-driven respawn loop"): adds `_notification_spawn_cooled` (line 4738), `_notification_spawn_over_cap` (line 4780), `_prune_notification_spawn_maps` (line 4811), and `_notification_has_live_work` (line 4848) — both new guards gate AFTER the existing cross-tick cooldown. `_instances.__init__`/`__new__` gain `_notification_spawn_at: dict[tuple[str, str], float]` (existing cooldown map) plus a companion hard-cap counter; every notification-triggered spawn dispatcher (escalation/approval/audit/a2a) is threaded through both new checks.
|
||||
> - `7c8453e2` (#654, "per-task and per-project cost budgets"): adds `_fetch_budget_status`/`_task_budget_breach`/`_handle_task_budget_breach`/`_sweep_budget_exceeded`/`_check_budget_for_agent` (lines 8021-8214) gated by `ROBOCO_TASK_BUDGETS_ENABLED`. The claim-time project-month-spend guard (`project_budget_exceeded_guard`) lives in `docs/map/gateway-support.md`; this is the sweep-side task-budget half. The same PR bundles `1d5a8e84` (#652's re-escalation backoff, see `docs/map/notification.md`) as a prerequisite commit in its branch history.
|
||||
> - `a036c979` (#666, "cockpit data correctness", 2026-07-23) `effective_task_budget_usd` drops its per-`TaskType` default fallback — a budget-less task is now genuinely uncapped (`None`), not silently capped at a `TaskType`-keyed dollar figure that blocked an unbudgeted coordination root one opus planning turn in. `_task_budget_breach`'s description above reflects this; the `TaskType` default table + its resolver are deleted outright.
|
||||
> - `eb0dcb6e` (#685, "task-scoped oscillation breaker", 2026-07-24) A escalate_up→BLOCKED→unblock→respawn→escalate_up round trip had no fuel limit: the RESTORED side dispatches via `_dispatch_claimed_without_agent`, which consulted no respawn counter at all. `unblock()` now counts strikes task-scoped (durable in `orchestration_markers`, no migration) via a cheap progress fingerprint (commit count + `revision_count` + `terminal_children_count`); past threshold the task is BLOCKED with `blocker_resolver_type=HUMAN` (the same posture budget-breach uses) and the CEO is notified naming both agents + the cycle count. `TaskResponse.blocker_resolver_type` now actually serializes over the wire, so `_is_hitl_blocked`'s skip (already used by `_dev_dispatch_one`) now really fires for `_dispatch_claimed_without_agent` and `_notification_has_live_work` too — previously the wire dict never carried the field, so those two HITL checks silently no-op'd.
|
||||
> - `987eb09c` (#692, "role-aware worktree refresh at every spawn", 2026-07-24) `_ensure_worktree_before_spawn` now passes `can_author` (from the spawning agent's role, `foundation.identity.WORKTREE_AUTHOR_ROLES`) into `ensure_worktree_self_heal`, which refreshes an ALREADY-PRESENT per-task worktree against origin at every respawn instead of treating it as a pure venv+chown no-op — see `docs/map/workspace.md` for the full classification.
|
||||
|
||||
## Regression Risks
|
||||
|
||||
|
||||
+15
-6
@@ -12,8 +12,9 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.25.0) is the s
|
||||
| `panel/src/app/(dashboard)/layout.tsx` | Dashboard shell: sidebar + header + connection status |
|
||||
| `panel/src/app/(dashboard)/overview/page.tsx` | Overview = `<CommandCenter/>` |
|
||||
| `panel/src/app/(dashboard)/metrics/page.tsx` | Metrics page: Usage + Delivery tabs, summary/donut/charts |
|
||||
| `panel/src/app/(dashboard)/tasks/page.tsx` + `tasks/[taskId]/page.tsx` | Task list + task detail (tabbed) |
|
||||
| `panel/src/app/(dashboard)/kanban/page.tsx` | Operator kanban (dev/qa/pm/pr-review views) |
|
||||
| `panel/src/app/(dashboard)/tasks/page.tsx` + `tasks/[taskId]/page.tsx` | Task list, now with top-level **List\|Kanban tabs** (#694/#688) sharing the page's URL filter state — List renders the existing `TaskFilters`+`TaskTable` unchanged, Kanban embeds the four kanban views (dev/qa/pr-review/pm) as nested sub-tabs, a controlled team filter shared bidirectionally with the List tab; + task detail (tabbed) |
|
||||
| `panel/src/app/(dashboard)/kanban/page.tsx` | **Redirects to the Tasks page's Kanban tab** (#694: `feat(panel): redirect /kanban to Tasks kanban tab, remove sidebar entry, swap bottom tab bar to Agents`) — the standalone kanban surface is gone from the sidebar; `KanbanBoard`'s dnd-kit drag-and-drop and mobile single-column nav are unchanged, just reached from `tasks?tab=kanban` now |
|
||||
| `panel/src/app/(dashboard)/projects/[projectId]/settings/page.tsx` | **Project settings full page** (Wave C, replacing the ~30-field `edit-project-dialog.tsx`, which is deleted): a card-per-concern grid — `identity-card` (name/slug/Forge/GitHub App), `git-auth-card` (PAT), `placement-card` (cell/team, protected branches), `environments-card` (env ladder), `cicd-commands-card`, `sandbox-card` (services + extensions), `budget-ops-card` (monthly budget, video engine toggle, CI-watch/dep-update/ci-watch toggles) — each with its own per-card save (`save-bar.tsx`), plus Conventions as a page-level tab at natural width instead of a cramped dialog tab |
|
||||
| `panel/src/app/(dashboard)/prompter/page.tsx` | Intake chat (single + MegaTask batch scope) |
|
||||
| `panel/src/app/(dashboard)/agents/page.tsx` | Agents hub: Fleet (roster/spawn/status, default) + Conversations + Journals tabs via `?tab=fleet\|conversations\|journals` — `AgentsFleetView` (fleet roster/spawn/activity), `A2AView` (org-wide switchboard/list + transcript + CEO reply composer + "New DM" dialog, live via `/ws/system` `a2a.message` frames — a pure lift of the old standalone `/a2a` page body), and `JournalsView` (per-agent reflection log — a pure lift of the old standalone `/journals` page body, wave 11) |
|
||||
| `panel/src/app/(dashboard)/a2a/page.tsx` | Redirect shim → `/agents?tab=conversations` (A2A folded into the Agents hub's Conversations tab, wave 9) — kept so old bookmarks/links still resolve |
|
||||
@@ -34,8 +35,12 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.25.0) is the s
|
||||
| `panel/src/lib/telegram/webapp.ts` | Typed wrapper over the global `window.Telegram.WebApp` (`ready`/`expand`/`initData`, `TelegramThemeParams`/`MainButton`/`BackButton`/`HapticFeedback` interfaces); `waitForTelegramWebApp` polls (100ms, 1.5s timeout) for the CDN script since it loads `afterInteractive`; `createDevMockWebApp`/`isDevMockWebApp` back the `/tg?demo=1` dev-browser fallback; null-safe `haptics` const (no-ops outside Telegram) |
|
||||
| `panel/src/lib/telegram/{hooks.tsx,theme.ts,demo.ts,demo-data.ts}` | `hooks.tsx`: `TgWebAppProvider`/`useTgWebApp` context + `useMainButton`/`useBackButton` (declarative wrappers over Telegram's native buttons — no consumer touches `window.Telegram` directly). `theme.ts`: `applyTelegramTheme` maps `themeParams` → shadcn CSS vars, hex-only trust boundary, scoped to `#tg-shell` only (the desktop dashboard is untouched); `startTelegramThemeSync` re-applies on the bridge's `themeChanged` event. `demo.ts`/`demo-data.ts`: `isTgDemoMode()` (dev-only, `?demo=1`, dead-code-eliminated in prod) gates dynamically-imported `DEMO_TASKS`/`DEMO_NOTIFICATIONS`/`DEMO_RELEASE`/`DEMO_X_POSTS`/`DEMO_VIDEO_POSTS`/`DEMO_ROADMAP`/`DEMO_TODAY` fixtures |
|
||||
| `panel/src/components/settings/` | feature-flags-card, ai-routing-card, transcript-retention-card, self-hosted-section, `x-credentials-card.tsx` (write-only OAuth 1.0a secrets, mounted in `settings/page.tsx`) |
|
||||
| `panel/src/components/conventions/conventions-tab.tsx` | Per-project architecture map + health (in edit-project dialog) |
|
||||
| `panel/src/components/projects/`, `products/`, `agents/`, `business/`, `auditor/`, `knowledge-base/`, `git/`, `journals/`, `work-sessions/`, `notifications/`, `rate-limit/`, `layout/`, `ui/` | Per-domain component groups (`projects/` and `products/` each export a `*-view.tsx` consumed by `workstation/page.tsx`, plus a `*-card-grid.tsx` reusing the sibling table's exported badge renderers; `agents/` similarly exports `agents-fleet-view.tsx` (`AgentsFleetView`) consumed by `agents/page.tsx`'s Fleet tab, plus `agent-card.tsx`'s DM quick-action button; `journals/` similarly exports `journals-view.tsx` (`JournalsView`) consumed by `agents/page.tsx`'s Journals tab); `ui/` = Radix-based primitives (dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown) |
|
||||
| `panel/src/components/conventions/conventions-tab.tsx` | Per-project architecture map + health — now a page-level tab on `/projects/[id]/settings` (Wave C; was a cramped tab inside the deleted edit-project dialog) |
|
||||
| `panel/src/components/projects/settings/*.tsx` | Wave C card-per-concern settings surfaces (`identity-card`, `git-auth-card`, `placement-card`, `environments-card`, `cicd-commands-card`, `sandbox-card`, `budget-ops-card`, `save-bar.tsx`) backing `/projects/[id]/settings`, replacing the single ~1061-line `edit-project-dialog.tsx` (deleted) |
|
||||
| `panel/src/components/projects/quick-edit-project-dialog.tsx` | Wave C: slim replacement for the old kitchen-sink edit dialog — name/cell/active only; the project list's Edit action now routes to the full settings page instead |
|
||||
| `panel/src/components/layout/command-palette.tsx` + `hooks/use-command-palette.ts` | Wave C (#694/#670): global Cmd+K palette — Radix Dialog + combobox searching tasks/agents/projects/pages, `localStorage` recents (`lib/command-palette-recents.ts`, key `roboco-cmd-recents`), fuzzy match (`lib/fuzzy-match.ts`), keyboard nav; mounted globally in the dashboard layout, wired to the header search trigger via `useUIStore.setCommandPaletteOpen` (`components/layout/header.tsx`) |
|
||||
| `panel/src/components/ui/collapsible-section.tsx` | Wave C dialog-standardization: the single sectioned-disclosure primitive, moved from `tasks/task-detail/` to `ui/` — task dialogs' raw `Collapsible` and create-project's ad-hoc `showAdvanced` both converge onto it. Every hand-rolled dialog footer across the panel became `DialogFooter`; dialog widths collapsed from ten ad-hoc classes to three named sizes (deliberate outliers annotated). No behavioral change. |
|
||||
| `panel/src/components/projects/`, `products/`, `agents/`, `business/`, `auditor/`, `knowledge-base/`, `git/`, `journals/`, `work-sessions/`, `notifications/`, `rate-limit/`, `layout/`, `ui/` | Per-domain component groups (`projects/` and `products/` each export a `*-view.tsx` consumed by `workstation/page.tsx`, plus a `*-card-grid.tsx` reusing the sibling table's exported badge renderers; `agents/` similarly exports `agents-fleet-view.tsx` (`AgentsFleetView`) consumed by `agents/page.tsx`'s Fleet tab, plus `agent-card.tsx`'s DM quick-action button; `journals/` similarly exports `journals-view.tsx` (`JournalsView`) consumed by `agents/page.tsx`'s Journals tab); `ui/` = Radix-based primitives (dialog, table, tabs, select, switch, required-notes-dialog, sonner toaster, markdown, `collapsible-section`) |
|
||||
| `panel/src/hooks/use-websocket.ts` | Shared `useWebSocket<T>(path, handlers?, isSystem?)` hook (auto-reconnect, heartbeat) |
|
||||
| `panel/src/hooks/use-{tasks,agents,projects,products,usage,prompter,secretary,dashboard,git,journals,notifications,knowledge-base,observability,work-sessions,providers,rate-limit-{sync,websocket}}.ts` | TanStack Query + zustand data hooks |
|
||||
| `panel/src/hooks/use-a2a-live.ts` | `useA2AConversations` / `useA2AAdminPairs` / `useA2AMessages` (TanStack Query over `a2aApi`) + `useReplyAsCeo` / `useCreateCeoConversation` / `useSendCeoMessage` mutations; `a2aLiveKeys` query-key namespace |
|
||||
@@ -66,10 +71,11 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.25.0) is the s
|
||||
| Agents — Conversations (switchboard + reply + New DM) | `app/(dashboard)/agents/page.tsx` (`?tab=conversations`) + `components/a2a/a2a-view.tsx` + `components/a2a/*` | CEO watches every agent-to-agent conversation live: default org-chart switchboard (pair cards grouped by cell/PM-chain/board, pulsing on fresh `a2a.message` frames) or the classic conversation list; drill-in shows the transcript + a reply composer that lets the CEO chime into a watched thread as itself (task-linked conversations only). "New DM" opens a fresh CEO-owned 1:1 with any DM-capable agent (no task link needed) — either from the dialog's own picker or preselected from a validated `?dm=` deep link off the Fleet tab; the recipient is woken via the a2a_request dispatch path if offline, and the CEO's own threads render with `A2ADirectComposer` instead of the reply composer. `/a2a` redirects here |
|
||||
| Agents — Journals | `app/(dashboard)/agents/page.tsx` (`?tab=journals`) + `components/journals/journals-view.tsx` + `components/journals/*` | Per-agent reflection log lifted into the Agents hub's third tab: agent picker (left, local search state seeded from the last saved query) + a type/task-filterable entry list (right, `JournalView`); each task-linked entry carries a Task badge + copy-UUID button. Entry detail still lives at `/journals/{entryId}`, its back-links now pointing at `/agents?tab=journals`. `/journals` redirects here |
|
||||
| Workstation | `app/(dashboard)/workstation/page.tsx` + `components/{products,projects}/*-view.tsx` | Products + Projects as one sidebar entry, tab-switched via `?tab=`; each surface has a Cards\|Table view toggle (default Cards, persisted per-surface via `ui-store`'s `productsView`/`projectsView`) with client-side name/cell(-count) sorting in card view; Projects' q/cell/inactive filters are local `useState`, not URL params (scroll-bounce prevention) |
|
||||
| Project Settings / Conventions | `components/projects/edit-project-dialog.tsx` + `components/conventions/conventions-tab.tsx` | Per-project `.roboco/conventions.yml` map + health; Save / Restore via PR. Also carries the "Forge" `<Select>` (Auto-detect/GitHub/Gitea/GitLab, `project.git_provider`) — see `docs/map/worksession-git.md` |
|
||||
| Project Settings / Conventions | `app/(dashboard)/projects/[projectId]/settings/page.tsx` + `components/projects/settings/*.tsx` + `components/conventions/conventions-tab.tsx` | Wave C full-page project settings (card-per-concern, per-card save) — was the ~30-field `edit-project-dialog.tsx` (deleted). The list's Edit action routes here; a slim `quick-edit-project-dialog.tsx` (name/cell/active) covers the fast path. Conventions is a page-level tab: per-project `.roboco/conventions.yml` map + health, Save/Restore via PR. The "Forge" `<Select>` (Auto-detect/GitHub/Gitea/GitLab, `project.git_provider`) now lives on `identity-card.tsx`; protected branches on `placement-card.tsx`; env ladder on `environments-card.tsx`; video engine + budget on `budget-ops-card.tsx` — see `docs/map/worksession-git.md` |
|
||||
| Command Palette | `components/layout/command-palette.tsx` + `hooks/use-command-palette.ts` | Global Cmd+K search over tasks/agents/projects/pages with recents + fuzzy match; triggered from the header search box |
|
||||
| Usage Dashboard | `components/dashboard/usage-overview-panel.tsx` + `hooks/use-usage.ts` | Token/cost totals; live WS snapshot with HTTP-polling fallback |
|
||||
| Git | `app/(dashboard)/git/page.tsx` | Repository / Work Sessions tabs (business-page tab idiom, `?tab=`); `GitBrowser` (status/branches/log/diff + actions incl. confirm-gated "Clean Up Stale Branches") and `WorkSessionsView` (active sessions, search/status filter kept LOCAL not in URL params); old `/work-sessions` route now redirects to `/git?tab=sessions` |
|
||||
| Kanban | `components/kanban/{core,views}/*` | dnd-kit drag board; dev/qa/pm/pr-review views; drag routes through admin status-override with bypass-precondition prompt |
|
||||
| Kanban | `app/(dashboard)/tasks/page.tsx` (Kanban tab) + `components/kanban/{core,views}/*` | dnd-kit drag board; dev/qa/pm/pr-review views; drag routes through admin status-override with bypass-precondition prompt. Reached via the Tasks page's **List\|Kanban tabs** (#694); the standalone `/kanban` route now redirects here and its sidebar entry is gone (bottom tab bar swapped to Agents) |
|
||||
| Task Detail | `components/tasks/task-detail/*` | Tabbed: overview, plan, progress, commits, sessions, notes, dependencies, **findings**, AC, action dialogs |
|
||||
| AI Providers | `app/(dashboard)/settings/ai-providers/page.tsx` + `components/settings/ai-routing-card.tsx` | Per-slug/role/global model routing |
|
||||
| Telegram Mini App | `app/(tg)/tg/page.tsx` + `components/tg/*` | The CEO's phone cockpit, outside the `(dashboard)` shell: 5 tabs — Today (default: `GET /telegram/today` spend/fleet/velocity/needs-you brief + a 4-action ops ring), Approvals (native card stack over the 4 held-artifact queues, Telegram `MainButton`/`BackButton`-driven), Inbox (notifications + ack), Board (`mobile-task-board.tsx` read-only grouped-by-status + a `tg-task-sheet.tsx` detail sheet), Chat (A2A conversation list/compose/thread, now WS-live via `/ws/system`). Bootstraps via Telegram `initData` → `/telegram/webapp-auth`, requires both `telegram_miniapp_enabled` and `cloud_auth_enabled` armed server-side; `themeParams` scoped to `#tg-shell` only; a dev-only `?demo=1` mock bridge + fixtures make the whole surface workable in a plain browser |
|
||||
@@ -321,6 +327,9 @@ Deliberately **not** on this card (compose/env-coupled, unsafe for a runtime tog
|
||||
> - **Telegram Mini App V4** (2026-07-19, PR #576 + #582 + `a072b980` fix, "Today brief, native approvals, live data, bot tier, chat bridges" + "premium Mini App cockpit") — the cockpit gets a 5th default-opening tab and a native-app visual overhaul. `TgTabBar` gains `"today"` as the first tab. New `TgTodayTab` renders off ONE `GET /telegram/today` round trip (TanStack Query, 45s `refetchInterval` + WS-invalidate on any `USAGE_SNAPSHOT` frame): `SpendHero` (cost figure, signed delta-vs-yesterday chip, a 7-day `Sparkline`), a `TgCircleAction` quick-action ring, a `NeedsYouBanner`, a live `TgAvatar` "Fleet" block, and a "Shipped this week" `DayBars` block. New shared primitives in `ui.tsx` (`TgCircleAction`/`TgAvatar`/`TgSection`/`TgRow`/`TgRowIcon`/`TgStat`, all new). `TgApprovalsTab` becomes a genuine native card stack over the new `approvals/` subdirectory, where `PrimaryAction` drives Telegram's `MainButton` via a new `useMainButton` hook and the tab itself wires `useBackButton` for focused-card back nav — both null-safely fall back to a visible button outside Telegram. `useApprovalQueue` normalizes all 4 held-draft sources into one list and surfaces `anyFailed` so a dead queue source is never silently rendered as "queue is clear". `TgChatTab` drops polling for the desktop A2A idiom: `useA2ALiveStream()` invalidates on every `a2a.message` frame with a 10s poll fallback only while `/ws/system` is down. New Telegram-native theme adoption: `webapp.ts` gains theme/button/haptics types + `createDevMockWebApp`/`isDevMockWebApp` (the `/tg?demo=1` dev-browser fallback, `NODE_ENV==="development"`-gated); new `hooks.tsx` + `theme.ts` apply `themeParams` → shadcn CSS vars **scoped to `#tg-shell`** (the desktop dashboard is untouched); `(tg)/layout.tsx` gains the `id="tg-shell"` scoping hook + Share Tech Mono font loading; `globals.css` ships a constant dark-amber `#tg-shell` skin independent of dashboard light/dark, that Telegram's `themeParams` inline-override on top of. `a072b980` (same-day fix) closes a prod-only bug: a plain-browser `/tg` visit gets the WebApp bridge with empty `initData` (the CDN script still loads outside Telegram), which was POSTed and 422'd into "Couldn't sign in" — `tg/page.tsx` now shows the "Open from Telegram" wall for ANY non-dev-mock bridge with empty `initData`. Follow-up polish (`c7605b0d` #582): `charts.tsx` (`Sparkline`/`DayBars`, theme-driven via `currentColor`); `tg-inbox-tab.tsx`/`tg-chat-tab.tsx` adopt `TgAvatar` tokens; `TgRowIcon` gains a `tone` prop so `tg-approvals-tab.tsx`'s `KIND_META` color-codes each queue kind; backend `/telegram/today` gains `spend.series`+`delta_pct`+`velocity` — see `docs/map/notification.md` for the backend side.
|
||||
> - **"feature/tg-miniapp-v5"** (2026-07-19) — a further visual/interaction polish pass on the V4 cockpit, additive-only (the one relocation: Today's standalone "Approve" button becomes a 4-item ops ring — Approvals is still reachable via the tab bar). New `tg-icons.tsx` (175 lines) — 9 SVG duotone components (`IconToday`/`IconSeal`/`IconInbox`/`IconBoard`/`IconChat`/`IconShip`/`IconAckAll`/`IconSweep`/`IconFleet`), scoped to hero surfaces only (utility chrome stays lucide-react); wired into `tg-tab-bar.tsx` (the 5 tab icons) and `tg-today-tab.tsx` (the new ops ring). New `motion.tsx` (105 lines) — `useCountUp(target, durationMs=650)` (rAF ease-out-cubic, instant under `prefers-reduced-motion`) and `TgSheet` (bottom-sheet dialog wired to Telegram's native `BackButton`, backdrop-tap-to-close, haptic on open); used by the new `tg-task-sheet.tsx` and by `tg-today-tab.tsx`'s Fleet/Sweep sheets. New `tg-task-sheet.tsx` (151 lines) — `TgTaskSheet`: read-only task-detail bottom sheet (status badge, "bounced ×N" chip, ACs, up to 5 open revision findings via `useTaskFindings` with an overflow line, "Open PR #N" link; skips the findings fetch in demo mode); rendered from `tg-board-tab.tsx` alongside `MobileTaskBoard` via a new `onTaskPress` prop. New vendored `ShareTechMono-Regular.woff2` under `app/(tg)/fonts/`, loaded via `next/font/local` as `--font-share-tech` on `#tg-shell`, applied only to the display voice (labels/numerals/wordmark) — never body text. `tg-today-tab.tsx` (largest diff) replaces the old single "Approve" action with a 4-item ops ring (Ship/Ack-all/Sweep/Fleet): `SpendHero`'s numeral gains count-up, a new `FleetSheet` shows the full roster (opened by tapping the truncated 3-agent preview), `runAckAll` bulk-acks via the existing notifications API, `runSweep` calls the existing `gitApi.cleanupBranches` per has-token project (no new backend surface), plus a blinking-cursor "ROBOCO_" wordmark header. `demo-data.ts` gains `DEMO_TASKS`/`DEMO_NOTIFICATIONS` fixtures.
|
||||
> - **"panel-perf-p3-p4"** (2026-07-19) — kanban + task-table + scorecards perf pass, paired with the backend N+1 fix in `docs/map/metrics-observability.md`. `kanban-board.tsx`: `tasksByStatus` grouping wrapped in `useMemo`, `handleAction` stabilized via `useCallback` + a `tasksRef` so child memoization actually holds. `kanban-card.tsx`: wrapped in `React.memo`. `kanban-column.tsx`: genuine windowing via the new `@tanstack/react-virtual` dependency — `useVirtualizer` (132px estimated card height, overscan 6) replaces a plain `.map()`, only visible virtual rows mount, absolute-positioned + `measureElement`'d for real-height correction; the column itself stays the dnd-kit droppable target so windowing doesn't break drag targeting. `task-table.tsx` (673 changed lines, no virtualization): row/card JSX extracted into top-level `memo`-wrapped `TaskTableRow`/`TaskTableCard`, fed stable props (`toggleExpand` via `useCallback`, lookup maps passed through) — pagination (pre-existing) is the actual bound on rendered rows, not windowing. `scorecards-tab.tsx`: `MemberRow` no longer self-fetches — `ScorecardsTabContent` calls the new `useAllMemberScorecards()` once and passes each row its slice via a `useMemo`'d Map; a table-level "Failed to load member scorecards" banner replaces the old per-row failure cell.
|
||||
> - **"Panel consistency & UX wave"** (#694/#688, 2026-07-24) — five streams. Stream1: project/task dialog field-sync audit (`docs/frontend/forms/forms-audit.md`, `project-fields-audit.md`) plus settings-page save-feedback toasts. Stream2: the Cmd+K **command palette** (`command-palette.tsx` + `use-command-palette.ts`, fuzzy-match + localStorage recents) wired into the header search box. Stream3: the Tasks page gains **List\|Kanban tabs** sharing URL filter state, and a follow-up (#679) redirects the standalone `/kanban` route here, drops its sidebar entry, and swaps the bottom tab bar's kanban slot to Agents. Stream4: a responsiveness/touch-target sweep — `Button size="sm"` 32px→36px, `AlertDialog` gains the same scroll-at-short-viewport pattern `Dialog` already had, the Scorecards Members table wrapped in `ResponsiveTable`, and (after `sync_branch`'s rebase reset silently dropped the fix twice — see `docs/map/worksession-git.md` #683) every remaining sub-36px icon button across 8 files bumped to 36px. A closing pass (`73275ff0`) corrects the task-budget help text for the removed `TaskType`-default fallback (#666) and moves the forms-audit docs into the governed `docs/frontend/forms/` tree.
|
||||
> - **Project settings full page** (`0e97fe6a`, Wave C-2, 2026-07-24) — the ~30-field, 7-tab `edit-project-dialog.tsx` (1061 lines) is deleted outright. Settings now live at `/projects/[id]/settings`: a card-per-concern grid (`components/projects/settings/*`, one card per concern — identity, git auth, placement, environments, CI/CD commands, sandbox, budget/ops) with per-card save, Conventions promoted to a page-level tab at natural width, and a new slim `quick-edit-project-dialog.tsx` (name/cell/active) for the fast path. The project list's Edit action now routes to the settings page instead of opening a dialog.
|
||||
> - **Dialog standardization** (`581f0320`, Wave C-2, 2026-07-24) — no behavioral change: `collapsible-section.tsx` moves from `tasks/task-detail/` to `ui/` as the single sectioned-disclosure primitive (task dialogs' raw `Collapsible` and create-project's ad-hoc `showAdvanced` both converge onto it); every hand-rolled dialog footer across the panel becomes `DialogFooter`; dialog widths collapse from ten ad-hoc classes to three named sizes, deliberate outliers annotated inline.
|
||||
|
||||
## Regression Risks
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ The revision-findings ledger: the structured replacement for prose-only QA/PR-ga
|
||||
| Name | Kind | File:Line | Responsibility |
|
||||
|---|---|---|---|
|
||||
| `TaskReviewFindingTable` | ORM class | `roboco/db/tables.py` | The append-only ledger row: `task_id`, `origin`, `round`, `author_slug`, `file`/`line`/`severity`/`criterion`/`expected`/`actual`/`fix`/`evidence`, `status`, `addressed_by_commit`, `resolution_note`. `origin`/`severity`/`status` are plain `String` columns, not a native Postgres enum. |
|
||||
| `Finding` | Pydantic model | `roboco/foundation/policy/content/models.py:92` | One structured finding, shared by `post_pr_review` (external PRs) and the four internal producers. Caps: `file` ≤300 (repo-relative, no `..`), `line` ≥1, `expected`/`actual` ≤300, `fix` ≤500, `evidence` ≤2000. `criterion` has **no Pydantic `max_length`** despite the DB column being `String(500)` — see Regression Risks. |
|
||||
| `Finding` | Pydantic model | `roboco/foundation/policy/content/models.py:92` | One structured finding, shared by `post_pr_review` (external PRs) and the four internal producers. Caps: `file` ≤300 (repo-relative, no `..`), `line` ≥1, `expected`/`actual` ≤300, `fix` ≤500, `evidence` ≤2000. `criterion` has **no Pydantic `max_length`** despite the DB column being `String(500)` — see Regression Risks. `file` is additionally shape-gated (`_PATH_SHAPE_RE`, #687): a value that doesn't look like a repo-relative path (prose like a PR reference, which used to validate and then doomed the panel's code-snippet fetch) is rejected with a remediate naming the file-less option for cross-cutting findings; the class admits `+`/`@` (SvelteKit route files, `@types` dirs, `@2x` assets) but excludes spaces, the prose signal. `file` remains OPTIONAL for the `issues` shim's file-less findings. |
|
||||
| `PmReviewContent` | Pydantic model | `roboco/foundation/policy/content/models.py` | New content type `"pm_review"` (`summary` + `findings`, no separate `verdict` — the transition to `needs_revision` IS the verdict); mirrors to the new `tasks.pm_notes` column via `_MIRROR_COLUMN`. |
|
||||
| `ReviewFindingsRepository` | class | `roboco/services/repositories/review_findings.py:32` | `insert_many` (append rows, one flush, no independent commit), `list_for_task` (default cap 500, newest round first), `status_counts_for_task` (SQL `GROUP BY (origin, status)`, whole ledger — independent of the 500 cap), `mark_addressed` (8-char-prefix match against OPEN rows, no-op on 0 or >1 matches, never raises), `mark_verified` (bulk, by full id), `mark_waived` (exists, unwired — no verb calls it). |
|
||||
| `findings_count_guard` / `findings_count_hint` | functions | `roboco/services/gateway/choreographer/findings.py:98,115` | Hard-reject `Envelope` above `FINDINGS_HARD_CAP=10`; non-blocking hint above `FINDINGS_NUDGE_COUNT=5`. |
|
||||
@@ -63,7 +63,7 @@ The revision-findings ledger: the structured replacement for prose-only QA/PR-ga
|
||||
| `hint_for_open_findings` | function | `roboco/services/gateway/remediation.py:36` | Builds the `i_am_done(resolved_findings=[...])` remediation string naming every still-open `[F-id8]`. |
|
||||
| `_open_findings_prompt_block` / `_revision_bounced_block` | functions | `roboco/runtime/orchestrator.py` | Capped-10 open-findings render for the `REVISION_REQUIRED` dev prompt and the PM triage "bounced" block; fails open to `""` on any DB error. |
|
||||
| `GET /{task_id}/findings` | route | `roboco/api/routes/tasks.py` | `list_for_task` (cap 500) + `status_counts_for_task` (SQL aggregate, whole ledger) → `TaskFindingsResponse{findings, summary, total, truncated}`. No role restriction beyond an authenticated agent context. |
|
||||
| `TabFindings` | React component | `panel/src/components/tasks/task-detail/tab-findings.tsx` | Per-round grouping, origin summary badges, severity/status badges, file:line, collapsible evidence, truncation footer. |
|
||||
| `TabFindings` | React component | `panel/src/components/tasks/task-detail/tab-findings.tsx` | Per-round **collapsible** sections (newest expanded, #687), origin summary badges, severity/status badges, file:line, collapsible evidence, truncation footer; only attempts a code-snippet fetch for a path-shaped `file` — a historical prose ref (pre-#687 data) renders as plain metadata instead of a doomed loader. |
|
||||
|
||||
## Data Flow
|
||||
|
||||
|
||||
@@ -66,7 +66,8 @@ Cross-cutting support layer beneath the delivery services: the service-base/erro
|
||||
| `_resolve_assignment` | method | `services/llm.py:248` | Tries AGENT_SLUG first, then (when `role` AND `complexity` both given) the compound `scope_value=f"{role}:{complexity}"` ROLE row, then the plain `role` ROLE row, then GLOBAL — the compound rung reuses the existing ROLE scope + `scope_value` column, no schema change |
|
||||
| `probe_ollama_tags` | func | `services/llm.py:63` | `{base_url}/api/tags` probe; never raises, returns `([], error)` |
|
||||
| `upsert_assignment` | method | `services/llm.py:241` | Insert-or-update by `(scope, scope_value)`; routes non-catalog names to LOCAL; auto-enables LOCAL provider |
|
||||
| `apply_mode` | method | `services/llm.py:567` | Wipe role/global rows (AGENT_SLUG pins preserved) + set GLOBAL for anthropic/grok/ollama/self_hosted/codex/gemini; per-agent map for mix. `apply_mode('cost_tiered')` is the one exception — it ADDITIVELY seeds a single day-1 `developer:low` → haiku override instead of wipe-then-seed, so it composes with whatever mode is already active. |
|
||||
| `apply_mode` | method | `services/llm.py:567` | Wipe role/global rows (AGENT_SLUG pins preserved) + set GLOBAL for anthropic/grok/ollama/self_hosted/codex/gemini; per-agent map for mix. `apply_mode('cost_tiered')` is the one exception — it ADDITIVELY seeds `_COST_TIERED_SEED` instead of wipe-then-seed, so it composes with whatever mode is already active. **Retired to an empty tuple (#680, 2026-07-24)**: the one entry (`developer:low` → haiku) is gone — haiku can't reliably emit the structured lifecycle envelopes, and the structured-verb capability floor below would have silently upgraded the seeded row back to sonnet anyway. `cost_tiered` mode itself stays wired (an empty seed is inert) so an operator can re-seed a genuinely above-floor `role:complexity` tier later. |
|
||||
| `_below_capability_floor` / `_floor_below_capability` | functions | `services/llm.py:189,354` | Structured-verb capability floor (#680): any Anthropic assignment that resolves to a haiku-class model — from a pin, a plain ROLE row, or the now-empty cost-tiered seed — is upgraded to `sonnet` at resolution, in both the assignment and legacy paths. Non-Anthropic providers are untouched (an Anthropic-tier floor, not a provider policy); `pr_reviewer`/`auditor` stay on opus regardless. |
|
||||
| `derive_mode` | method | `services/llm.py:443` | Settings UI label from current assignments |
|
||||
| `set_ollama_api_key` / `set_grok_api_key` | methods | `services/llm.py:340,360` | Encrypt+enable / clear+disable on the seeded provider row |
|
||||
| `_interactive_exempt` | func | `services/llm.py:176` | True when `agent_slug` is `intake-1`/`secretary-1` AND the resolved assignment routes to OPENAI/GEMINI — neither provider supports the interactive chat driver, so these two agents stay on Anthropic regardless of a fleet-wide mode switch; an explicit AGENT_SLUG pin onto either provider for these two slugs is refused loudly at the spawn/save boundary rather than silently producing a broken interactive session |
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
| `_validate_and_set_status` | method | task.py:548 | Single chokepoint: validate transition + git requirements, set status, poke dispatcher, emit audit. |
|
||||
| `_emit_status_transition_audit` | method | task.py:652 | Write `task.<status>` audit row in caller session; bump `revision_count` on entry into `needs_revision`. |
|
||||
| `_alert_auditor_of_rework` | method | task.py:1019 | Best-effort helper that asks `NotificationDeliveryService` to send a HIGH `ALERT` to the auditor when a task enters `needs_revision`. Called from `fail_qa`, `pr_fail`, and `request_changes` immediately after `await self.session.flush()` so the transition row is visible before the alert is dispatched. |
|
||||
| `create` | method | task.py:864 | New task; depth/batch/AC validation; branchless/umbrella flags; baseline constraints attachment; (V2) vault materialize-on-create. |
|
||||
| `create` | method | task.py:864 | New task; depth/batch/AC validation; branchless/umbrella flags; baseline constraints attachment; (V2) vault materialize-on-create; stamps `acceptance_criteria_ids` through `_reconcile_ac_ids` (#682, explicitly supplied ids still win). |
|
||||
| `update` | method | task.py:2928 | Generic PATCH field loop (task route, prompter `update_live_draft`/`_patch_batch_child`/`update_live_batch`) — derives `acceptance_criteria_ids` via `_reconcile_ac_ids` whenever `acceptance_criteria` is rewritten with no explicit id list (#682; previously overwrote the criteria text without touching ids, leaving them mismatched or empty and silently disabling the parent-coverage gate). |
|
||||
| `_reconcile_ac_ids` | function | task.py | Pure helper (#682): one id per new criterion; text-unchanged criteria KEEP their existing id (children/findings reference criteria by id or exact text — a blanket re-mint would orphan every live reference); new/reworded text mints a fresh id; a dropped criterion drops its id. |
|
||||
| `_attach_baseline_constraints` | method | task.py:971 | Append conventions baseline constraints to task prompt (gated `conventions_enabled`). |
|
||||
| `_materialize_vault_note` | method | task.py:910 | V2: best-effort vault seam called from `create` — assembles + writes a deterministic task note (narrative placeholder) so a task is visible in the vault from the moment it exists, not just at Auditor curation/rebuild. Gated `obsidian_vault_enabled`; swallows + logs any failure. |
|
||||
| `list_updated_since` | method | task.py:7101 | V2: tasks touched (`COALESCE(updated_at, created_at)`) since a timestamp, ascending, paged — the vault janitor's changed-task re-projection set. |
|
||||
@@ -31,9 +33,12 @@
|
||||
| `_emit_admin_override_audit` | method | task.py:8555 | Factored out of `_apply_pre_block_restore`: writes `task.admin_override` audit row for admin-triggered blocked restores (`forced=False, restore=True`). |
|
||||
| `claim` | method | task.py:3112 | `FOR UPDATE` lock + `_validate_claim_preconditions` + `_finalize_claim`; calls `_validate_and_set_status(claimed)`. |
|
||||
| `_validate_claim_preconditions` | method | task.py:2883 | Per-claim validator chain: status, `_claim_blocked_by_sequencing` (dependency + sequence), team, pre-assignment theft, self-review. |
|
||||
| `_claim_blocked_by_sequence` | method | task.py:2805 | Strict sibling-sequence gate: a PENDING/`needs_revision` task with parent + effective `sequence` (`COALESCE(sequence, 0)`) N is held while any same-parent sibling with a strictly lower effective sequence is non-terminal — assignee-blind, independent of `dependency_ids`. Ties run parallel; cancelled siblings never block. |
|
||||
| `_claim_blocked_by_sequence` | method | task.py:2805 | Sequence gate for a PENDING/`needs_revision` task with a parent + effective `sequence` (`COALESCE(sequence, 0)`) N — **reachability-aware since #681**: it branches on `is_batch_root_subtask(task.batch_id, task.parent_task_id)`. A MegaTask root-subtask keeps the ORIGINAL strict rule unchanged (held while ANY same-parent sibling with a strictly lower sequence is non-terminal — assignee-blind, edge-agnostic; `sequence` there is a one-shot globally-computed Kahn wave, a deliberate staged-release barrier). Every other same-parent context instead routes through the pure `sequence_blocker_id` (`roboco/services/sequencing.py`), which only lets a lower-sequence sibling block when it is a real transitive predecessor via `dependency_ids` UNIONED with `completed_dependency_ids` — a task with NO dependency edge onto any same-parent sibling still falls back to the raw edge-agnostic bar (preserves the pre-#681 #452 edge-less-PM-delegation scenario). Ties run parallel; cancelled siblings never block. |
|
||||
| `sequence_blocker_id` | function | services/sequencing.py | Pure reachability check backing the non-batch branch of `_claim_blocked_by_sequence` (#681): returns the blocking sibling's id, or `None`, from `dependency_ids ∪ completed_dependency_ids` — the union matters because `_unblock_dependents` prunes a completed dependency's edge into `completed_dependency_ids` the moment it lands, almost always before the dependent is ever claimed. |
|
||||
| `sequence_hold_reason` | method | task.py | Names the blocking sibling for the caller instead of a bare `None`; feeds the gateway's `Envelope.sequence_held` (#681) so a held claim on PENDING or NEEDS_REVISION surfaces cleanly instead of `claim()`'s bare `None` return being misdiagnosed by the verb runner as a concurrent-transition `INVALID_STATE`. |
|
||||
| `_claim_blocked_by_dependencies` | method | task.py:2781 | `unmet_dependency` TIMING gate: refuses claim while any `dependency_ids` entry is non-terminal. |
|
||||
| `is_pending_claim_blocked` | method | task.py:2864 | Read-only wrapper over `_claim_blocked_by_sequencing` (dependency OR sequence) so the orchestrator dispatcher can filter a doomed claim before attempting it (`_pending_claim_blocked` in orchestrator.py). |
|
||||
| `is_pending_claim_blocked` | method | task.py:2864 | Read-only wrapper over `_claim_blocked_by_sequencing` (dependency OR sequence) so the orchestrator dispatcher can filter a doomed claim before attempting it (`_pending_claim_blocked` in orchestrator.py); extended to NEEDS_REVISION so `give_me_work`'s two offer paths (`list_pending_for_agent`, the Choreographer's `_drop_dependency_held`) never offer a task the reachability-aware bar (#681) is about to reject. |
|
||||
| `terminal_children_count` | method | task.py:9711 | Count of a task's terminal (completed/cancelled) direct children; feeds the orchestrator oscillation breaker's progress fingerprint (#685, `roboco/services/gateway/choreographer/_impl.py`) so a coordination root whose children advance between escalate/unblock round-trips resets its strike count instead of accruing toward a false trip. |
|
||||
| `stamp_wave_sequence` | method | task.py:7452 | Stamps a freshly delegated subtask's `sequence` as `1 + max(sequence of each same-parent dependency target)`, or `0` when independent — so independent siblings tie (parallel under the sequence gate) while colliding/ordered work ascends. Runs POST-wiring (after the collision DAG / cross-cell edges land); PM-authored sequences are never rewritten. |
|
||||
| `_apply_dependency_lineage` / `_merge_one_dependency` | method | task.py:2308 / 2337 | Claim-time content assist (not a gate): merges each same-repo dependency's landed work into a freshly cut branch when it lies outside the branch's own ancestor chain (`GitService.merge_dependency_lineage`); a real conflict aborts the merge and stamps a `dependency_lineage_conflict` transition note instead of failing the claim. |
|
||||
| `_finalize_claim` | method | task.py:3265 | Work-session create/inherit, branch cut, proactive-context injection; calls `_inherit_upstream_base` when `_should_inherit_base` says the reclaimed branch may need an upstream merge. |
|
||||
@@ -142,6 +147,8 @@ stateDiagram-v2
|
||||
- `complete()` requires PR merged (`_assert_pr_merged_for_complete`) EXCEPT branchless roots; `ceo_approve` separately checks `work_session.pr_status=="merged"` and refuses otherwise.
|
||||
- Background indexing/learning/cleanup tasks are tracked on `self._background_tasks` and are best-effort — a failure never blocks the transition.
|
||||
- The sequence gate (`_claim_blocked_by_sequence`) is enforced ONLY in `_validate_claim_preconditions`, i.e. inside `claim` itself — both the gateway claim verbs AND the orchestrator's raw dispatch claim cross it because they both funnel through `TaskService.claim`, unlike the pre-#382 dependency gate which briefly lived only on the gateway side. Any future claim path that bypasses `TaskService.claim` (a raw `admin_set_status`, for instance) does NOT get sequence enforcement.
|
||||
- Since #681, the sequence gate is NOT one rule everywhere: `stamp_wave_sequence` stamps a sibling's wave from a partial, per-task view of the graph at delegate time, which is fine within one connected chain but can coincidentally collide two INDEPENDENT same-parent dev-task streams onto the same raw sequence number. The strict edge-agnostic rule survives ONLY for `is_batch_root_subtask` rows (a MegaTask's globally-computed Kahn wave is a deliberate barrier); every other same-parent context resolves via `sequence_blocker_id`'s real-predecessor check. A task with an empty `dependency_ids`/`completed_dependency_ids` union onto its same-parent siblings still falls back to the raw bar unchanged either way.
|
||||
- An `acceptance_criteria` rewrite through `update()` with NO explicit `acceptance_criteria_ids` used to silently desync ids from text (or leave an empty id list, which the parent-coverage gate then reads as "no criteria to cover" — waiving coverage for the whole subtree). `_reconcile_ac_ids` (#682) closes this at the one chokepoint every rewrite path shares; the parent-coverage gate also self-heals an out-of-length/empty id list in place rather than returning early.
|
||||
- `_apply_dependency_lineage` is scoped to SAME-REPO dependencies only (`dep_task.project_id != ctx.project.id` short-circuits) — a cross-repo dependency edge (e.g. a MegaTask root-subtask in another project) has no shared git history to merge and is silently skipped; the dependency TIMING gate still holds the claim regardless of repo.
|
||||
- `TaskTable.orchestration_markers` is generic `JSON`, not `JSONB`. Any SQL predicate on a marker key must use `.as_string()` (or the JSON dialect's generic comparator), not `.astext`, which is JSONB-only and raises `AttributeError` at compile time. `list_open_docs_sync_tasks(version=...)` at task.py:1596 is the current example; the inline comment records the rationale.
|
||||
- Both cancel and terminal-completion now force-delete (`-D`) the task's LOCAL branch ref in the assignee's clone alongside the worktree — a completed task's PR was squash-merged (its local ref is never an ancestor of base, so a "safe" `-d` refuses unconditionally) and a cancelled task's work is discarded by decision, so the ref is spent either way. Skipped when the branch name coincides with an environment-ladder rung (`effective_environments`), which outlives any one task.
|
||||
@@ -164,6 +171,10 @@ stateDiagram-v2
|
||||
> (open PR #548, branch `feature/wave-2-hygiene-charts`, 2026-07-17) Local branch refs stop leaking: `_delete_task_branch_best_effort`/`_remove_task_worktree_on_terminal` now also force-delete the assignee's local branch ref (via new `WorkspaceService.delete_local_branch`) and rmtree the task's `.previews/{task8}` video-preview dir, both skipped for environment-ladder rungs. See `docs/map/worksession-git.md` for the paired `GitService.cleanup_stale_branches` sweep.
|
||||
>
|
||||
> `2e889c70` (#644, "work re-claims inherit an advanced upstream base"): a re-claim (pool release / reaper unclaim / escalation redirect / a dev's own `i_will_work_on` re-entry) reused whatever branch had been cut at the EARLIER claim, so any work that landed on the parent/base branch since then (e.g. a sibling cell merging into root, or UX/UI landing before BE/FE re-claims) never reached the reclaimed branch — silent divergence, avoidable conflicts at PR time. `_finalize_claim` now calls `_inherit_upstream_base` after resolving the work session, gated by the double check above; a conflict never fails the claim, it steers the dev to `sync_branch` via a transition note + `dev_notes`.
|
||||
>
|
||||
> `c4ba351a` (#681, "reachability-aware claim bar + sequence_held surfacing", 2026-07-24) The sequence-drift fix: `_claim_blocked_by_sequence` branches on `is_batch_root_subtask` so an independent same-parent dev-task stream can no longer phantom-hold behind an unconnected sibling that coincidentally shares a lower raw `stamp_wave_sequence` number; non-batch contexts route through the new pure `sequence_blocker_id` (real transitive predecessor via `dependency_ids ∪ completed_dependency_ids`). New `sequence_hold_reason` + a proactive gateway guard return `Envelope.sequence_held` instead of a bare `None` reaching the verb runner as a misdiagnosed `INVALID_STATE`. `is_pending_claim_blocked` extended to NEEDS_REVISION so both `give_me_work` offer paths never dangle a task the claim gate would reject.
|
||||
>
|
||||
> `3516d925` (#682, "reconcile acceptance_criteria_ids at the update chokepoint", same day) `create()` and `update()` both now stamp `acceptance_criteria_ids` through the new pure `_reconcile_ac_ids` (id-preserving for text-unchanged criteria, fresh id for new/reworded text, dropped id for a removed criterion) — every prior post-create rewrite path (task PATCH, prompter `update_live_draft`/`_patch_batch_child`/`update_live_batch`) had silently desynced or emptied the ids, and an empty id list silently waived the whole subtree's parent-coverage gate. The gate itself now self-heals an empty/out-of-length id list in place instead of returning early.
|
||||
|
||||
## Regression Risks
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ This slice is the git substrate every delivery agent works on. `GitService` runs
|
||||
| `GitService._merge_with_retry` | method | git.py:3555 | Single 409 retry + already-merged disambiguation → MergeConflictError |
|
||||
| `GitService._lock_parent_task_for_merge` | method | git.py:3503 | SELECT FOR UPDATE on parent task (sibling merge serialization) |
|
||||
| `GitService._resolve_merger_id` | static | git.py:3530 | merged_by attribution: actor→assigned→created→UUID(0) |
|
||||
| `GitService.rebase_onto_base` | method | git.py:3733 | Rebase primitive: rebased/superseded/conflicts classification |
|
||||
| `GitService.rebase_onto_base` | method | git.py:5158 | Rebase primitive: rebased/superseded/conflicts/**diverged** classification. Post-fetch, `_reset_head_or_diverged` classifies local vs `origin/<head_branch>` BEFORE the old unconditional `reset --hard`: behind-or-equal resets to origin (unchanged); strictly ahead skips the reset and rebases from the local tip (the committed-but-unpushed-work case `commit` leaves behind); a real divergence returns `{"status": "diverged", "local_only", "origin_only"}` untouched — no reset, no rebase, no push. A patch-equivalence probe (`rev-list --right-only --cherry-pick`) first tries to recognize the diverged shape as a prior rebase whose force-push failed (self-heals as "ahead" on retry) before declaring a genuine two-sided divergence (#683). |
|
||||
| `GitService.rebase_pr_for_task` | method | git.py:3792 | PR-keyed rebase via PR refs (project_id scoped) |
|
||||
| `GitService.sync_task_branch` | method | git.py:3847 | Task-keyed rebase through dev `sync_branch` verb (pre-PR) |
|
||||
| `GitService.is_behind_base` | method | git.py:3889 | `(behind, ahead)` counts for i_am_done submit gate |
|
||||
@@ -114,7 +114,8 @@ This slice is the git substrate every delivery agent works on. `GitService` runs
|
||||
| `GitService.conventions_check_for_task` | method | git.py:4368 | Run conventions validator on changed files (fail-closed) |
|
||||
| `GitService._run_conventions_validator` | method | git.py:4408 | Subprocess `python -m roboco.conventions` with 120s cap |
|
||||
| `GitService.open_conventions_pr` | method | git.py:4456 | Scaffold `.roboco/conventions.yml` on a branch + open PR |
|
||||
| `GitService.diff` / `list_changed_files` / `read_file_at_branch` | methods | git.py:4192/4225/4259 | Read-only git queries (gateway + routes) |
|
||||
| `GitService.diff` / `list_changed_files` / `read_file_at_branch` | methods | git.py:5943/5984/6021 | Read-only git queries (gateway + routes); each resolves its head ref through `_resolve_head_ref` |
|
||||
| `GitService._resolve_head_ref` | method | git.py:5893 | Shared reader-ref resolution for `diff`/`list_changed_files`/`read_file_at_branch`/`/api/git/log`. Fetches `branch_name`, then prefers `origin/<branch_name>` over the bare local ref whenever origin carries commits local lacks (`origin_only > 0` via `rev-list`) — a reviewer's clone parked on pre-rebase history no longer stays frozen there across every later round; local wins only when it strictly contains everything origin has. Unlike the write-path's `_reset_head_or_diverged`, a read never needs to tell genuine divergence apart from a rewritten history — both resolve to "serve origin" (#690). |
|
||||
| `GitService.commit` | method | git.py:4286 | Gateway content-verb commit (branch-keyed) |
|
||||
| `get_git_service` | factory | git.py:4594 | Construct GitService from AsyncSession |
|
||||
| `build_branch_name` | func | templates/git/branch.py:37 | `{type}/{team}/{root}--{sub}--...` up to MAX_TASK_DEPTH |
|
||||
@@ -324,7 +325,8 @@ Module-level tunables (not env): `_SLOW_GIT_OP_MS=5000`, `_CI_RUN_WINDOW=20`, `_
|
||||
- **`get_current_branch` raises on detached HEAD** instead of returning `""` — the empty string used to leak "(HEAD detached at ...)" into `checkout -b`.
|
||||
- **`MAX_TASK_DEPTH=4`** (was 3) — MegaTask's umbrella→root→cell→dev needs 4; validator rejects a child whose depth would *reach* MAX_TASK_DEPTH, so 4 permits dev at depth 3.
|
||||
- **Branch name uses 8-char UUID prefix** (`_SHORT_ID_LEN=8`), not full UUID — full UUIDs produced 140-char branch names.
|
||||
- **`rebase_onto_base` force-pushes with `--force-with-lease`** only the head branch; never touches base. `superseded` (unique==0) means close-without-merge.
|
||||
- **`rebase_onto_base` force-pushes with `--force-with-lease`** only the head branch; never touches base. `superseded` (unique==0) means close-without-merge. It no longer resets a local tip that is ahead of origin (a committed-but-unpushed dev tip used to be silently discarded before the rebase); a `diverged` result leaves BOTH sides untouched — the sync_branch verb maps it to `i_am_blocked` with a stash-preserved note (#683).
|
||||
- **Reader git queries prefer origin over a diverged local ref** (`_resolve_head_ref`, #690): a QA/PM/reviewer clone that never pushed can go stale relative to origin after a force-push sync; `diff`/`list_changed_files`/`read_file_at_branch` and `roboco_git_log` all resolve through the same fixed helper now, so a repeated review round no longer re-examines a frozen pre-rebase checkout.
|
||||
- **`is_behind_base` raises on git failure**; the i_am_done gate fail-opens on the raised error so a flaky fetch can't strand the task.
|
||||
- **Conventions validator fails closed** (`could_not_run=True` blocks submit) on resolution error / timeout / non-zero exit; branchless + no-changed-files fail open.
|
||||
- **`_assert_on_task_branch` never discards work** — it does `checkout`, not `reset --hard`, to preserve a resumed agent's unpushed commits.
|
||||
@@ -355,6 +357,8 @@ Baseline: `fd10cc862c2020b3f639cdb686d427b0198a2441` (master tip before the metr
|
||||
>
|
||||
> `496c24d1` (PR #548, "git hygiene", 2026-07-17) Local branch refs stop leaking alongside remote ones: `delete_task_branch` now also skips environment-ladder rungs (previously only the remote-delete's own main/master/develop guard existed) and returns `bool`; new `cleanup_stale_branches` + `_stale_branch_window` + `_cleanup_one_stale_branch` back a PM/CEO-only `POST /git/branches/cleanup` sweep of terminal tasks' remote+local branches, exposed as a confirm-dialog button on the panel Git page. See `docs/map/task-service.md` for the paired per-task reap at cancel/completion and `docs/map/workspace.md` for the new `WorkspaceService.delete_local_branch` primitive both routes share.
|
||||
>
|
||||
> `71f5426e` (PR #683, "never discard committed local work in rebase_onto_base", 2026-07-24) The stale-state family fix for the write path: `rebase_onto_base` classifies local vs `origin/<head_branch>` post-fetch instead of an unconditional `reset --hard` — behind/equal resets as before, strictly-ahead skips the reset and rebases from the local tip, and a genuine two-sided divergence (patch-equivalence-checked first, so a prior failed force-push self-heals as "ahead" on retry) returns `{"status": "diverged", ...}` with neither side touched. `sync_branch`/submit-freshen/the merge-conflict resolver all gained a `diverged` handling branch. `e97f46af` (PR #690, "reviewer reads must prefer origin over a diverged local ref", same day) is the read-path sibling: `_resolve_head_ref` (backing `diff`/`list_changed_files`/`read_file_at_branch`/`roboco_git_log`) now prefers origin whenever it carries commits the local ref lacks, closing the QA/PM-clone-frozen-on-pre-rebase-history bounce loop.
|
||||
>
|
||||
> **Forge providers — GitHub + Gitea + GitLab (2026-07-18/19, PRs #569/#571/#575/#579/#581).** A new `roboco/services/forge/` package (`base.py`/`github.py`/`gitea.py`/`gitlab.py`/`registry.py`/`router.py`/`shaping.py`) plus `roboco/foundation/policy/forge.py` route every REST call `GitService` makes (PRs/CI/reviews/labels/releases/provisioning) through a provider-agnostic transport. `388bab24` (Phase 0, #569): `projects.git_provider` column (migration 076, nullable, plain string not a pg enum — validated at the service layer, not the DB) + `validate_project_forge`/`detect_provider` (github.com auto-detects, self-hosted needs an explicit column value — the GHE/self-hosted escape hatch). `461a6e1a` (Phase 1, #571): the `GitProvider` ABC + `GitHubProvider` extracted byte-for-byte from `GitService`'s old inline `httpx` calls; `GitService._forge` (git.py:399) becomes the seam every call site routes through. `96401f4c` (Phases 2/2.1/3, #575): `GiteaProvider` + `GitLabProvider` + `ForgeRouter` (per-call transport dispatch off `RepoRef.host`) + the local-git `merge_branch` fallback for forges with no server-side merges API. `5f32d876` (Phase 4, #581): `roboco/services/github_provisioning.py` becomes provider-aware (`ROBOCO_PROVISIONING_PROVIDER`/`ROBOCO_PROVISIONING_HOST` — see `docs/map/product-strategy-research-pitch.md`) so pitch-driven repo creation works on all three forges. `d4cb5797` (#579) + the pre-existing `tests/e2e_smoke/test_gitea_live.py` are the live contract suites (self-seeding against a dockerized `gitea/gitea` / real `gitlab.com`, env-gated) that caught the slash-encoding and http-scheme gaps in the Gitea provider. Panel: the edit-project dialog's "Forge" `<Select>` (`edit-project-dialog.tsx`) offers Auto-detect/GitHub/Gitea/GitLab.
|
||||
>
|
||||
> **Protected branches + env-ladder deletion protection (#649, #651).** `GitService._protected_branches_for`/`_protected_branches_for_deletion` (git.py:1183/1219) consult the new `projects.protected_branches` column end-to-end for the first time (the column previously existed with nothing reading it): the deletion scope additionally unions in the project's env-ladder rung branches (`effective_environments`), closing the gap where the post-merge PR-source cleanup and the stale-branch sweep could still delete a branch that IS a ladder rung — `delete_task_branch`'s own local rung check is now exactly subsumed and removed. Panel: a chips editor on the edit-project dialog.
|
||||
|
||||
+10
-6
@@ -37,7 +37,9 @@ WorkspaceService manages the per-agent git clone layout under {workspaces_root}/
|
||||
| WorkspaceService.ensure_worktree | method | roboco/services/workspace.py:555 | git worktree add -b <branch> <base> (or reuse existing branch); calls _park_clone_root_off_branch first; link venv; chown worktree + clone root |
|
||||
| WorkspaceService.ensure_worktree_for_resume | method | roboco/services/workspace.py:591 | Re-add a pruned worktree on resume (no -b; branch ref survives); calls _park_clone_root_off_branch first; idempotent; link venv + chown |
|
||||
| WorkspaceService._fetch_branch_ref | method | roboco/services/workspace.py:613 | Token-aware git fetch origin <branch> into clone_root; best-effort (never raises); used by ensure_worktree_self_heal (536bbb64) |
|
||||
| WorkspaceService.ensure_worktree_self_heal | method | roboco/services/workspace.py:671 | Orchestrator spawn-time chokepoint: re-attaches a per-task worktree after clone vanished (redeploy/disk loss); fetches branch ref from origin when the local ref is absent after a re-clone, then delegates to ensure_worktree (536bbb64) |
|
||||
| WorkspaceService.ensure_worktree_self_heal | method | roboco/services/workspace.py:716 | Orchestrator spawn-time chokepoint, now role-aware (`can_author`, #692): a PRESENT worktree is no longer a pure venv+chown no-op — it runs through `_refresh_present_worktree` first, so a worktree created once at first claim/`claim_review` no longer stays frozen at that commit across every later respawn. A vanished-clone re-attach still fetches the branch ref from origin when the local ref is absent (536bbb64); a surviving-ref-but-pruned-worktree re-add now also runs the same refresh classification instead of trusting the stale ref's commit. |
|
||||
| WorkspaceService._refresh_present_worktree | method | roboco/services/workspace.py:869 | Compares local `branch` HEAD against `origin/<branch>` (rev-list ahead/behind): behind-or-equal fast-forwards for every role, but never discards an author-capable role's (`can_author`) dirty uncommitted edits to do it; strictly ahead is always left alone (real unpushed work); diverged only hard-resets for a pure reader (QA/PR-gate/PM), whose local history can only ever be a stale prior round's checkout — never an author, since only developer/documenter hold the gateway `commit` tool. Every reset is additionally guarded by `_safe_to_reset` (#692). |
|
||||
| WorkspaceService._worktree_is_dirty / `_worktree_on_task_branch` / `_safe_to_reset` | methods | roboco/services/workspace.py | Dirty-guard trio backing `_refresh_present_worktree`: a failed `git status` reads as dirty (fail toward preservation, never a false-clean reset); a reset only ever fires when the worktree is actually still checked out on `branch` (a detached/drifted worktree is left alone with a warning) (#692) |
|
||||
| WorkspaceService.remove_worktree | method | roboco/services/workspace.py:733 | Best-effort git worktree remove --force + prune; no-op if gone (cancel/terminal/reaper evict) |
|
||||
| WorkspaceService.delete_local_branch | method | roboco/services/workspace.py:787 | Best-effort `git branch -d/-D <branch>` in a clone; never raises; skips main/master/develop/empty (mirrors GitService._delete_remote_branch_best_effort); callers run it AFTER remove_worktree (a still-checked-out branch refuses) |
|
||||
| WorkspaceService.resolve_workspace | method | roboco/services/workspace.py:745 | Look up agent (UUID or slug) -> team+slug -> workspace path; default team BACKEND |
|
||||
@@ -70,7 +72,7 @@ WorkspaceService manages the per-agent git clone layout under {workspaces_root}/
|
||||
| get_workspace_service | function | roboco/services/workspace.py:1930 | Factory: WorkspaceService(session) |
|
||||
|
||||
## Data Flow
|
||||
Inputs: an AsyncSession, a project_slug, an agent_id (UUID or slug), optionally a git_url/default_branch/force. The orchestrator, GitService, TaskService, conventions service, dep_update_engine, and gateway content_actions all obtain a WorkspaceService via get_workspace_service(session) (or WorkspaceService(db) directly in the spawn path). Control flow on ensure_workspace: _lookup_agent_or_raise -> get_workspace_path -> acquire per-(project,agent) asyncio.Lock -> if _is_workspace_healthy (.git+HEAD+objects): _ensure_agent_owned (to_thread), prune broken refs, scoped _fetch_origin_best_effort (30s TTL, force override), re-chown, install_dev_deps (digest-cache hit short-circuits), return. Else: rmtree any partial/stub dir, ProjectService.get_by_slug, resolve the clone target branch via `head_branch(project)` (the env-ladder head rung — `roboco.models.env_branches`, shimmed from `default_branch` when no ladder is declared), _resolve_git_token (decrypt PAT; raise on HTTPS-with-no-token), _clone_repo (git clone --branch --no-tags, configure identity/fileMode, scrub PAT from remote URL, _assert_no_pat_leak scanning .git/** for ghp_/github_pat_/x-access-token, chown, install_dev_deps), _maybe_scaffold_conventions (once-per-process, flag-gated). Per-task worktree path: get_clone_root_path + get_worktree_path (.worktrees/{task_short_id}); ensure_worktree runs git worktree add -b <branch> <base> (or reuses an existing branch ref), _link_shared_venv (symlink to clone-root .venv only if it exists), chowns both worktree and clone root. GitService.create_branch calls ensure_worktree; commit/rebase paths call ensure_worktree_for_resume via GitService._ensure_worktree_for_commit; the orchestrator's _ensure_worktree_before_spawn calls ensure_worktree_self_heal (post-536bbb64) which first fetches the branch ref from origin if the local ref is absent after a re-clone, then delegates to ensure_worktree; TaskService.complete/cancel call remove_worktree. ensure_read_clone is called by ConventionsService for the project-level read clone at _meta/conventions, hard-reset to origin/default. dry_upgrade_changes_lockfile (dep_update_engine) clones the read clone --local --no-hardlinks into a throwaway under the read-clone lock, runs dep_update_command, and checks git status --porcelain on lockfile paths. Outputs: workspace Path (and side effects: on-disk clone/worktree, .venv symlink, .git/.roboco-dep-install + .git/.roboco-toolchain markers, root-owned refs re-chowned to agent uid). All git/subprocess work runs via asyncio.to_thread; tokens are injected only transiently into argv (never written to .git/config) and scrubbed post-clone.
|
||||
Inputs: an AsyncSession, a project_slug, an agent_id (UUID or slug), optionally a git_url/default_branch/force. The orchestrator, GitService, TaskService, conventions service, dep_update_engine, and gateway content_actions all obtain a WorkspaceService via get_workspace_service(session) (or WorkspaceService(db) directly in the spawn path). Control flow on ensure_workspace: _lookup_agent_or_raise -> get_workspace_path -> acquire per-(project,agent) asyncio.Lock -> if _is_workspace_healthy (.git+HEAD+objects): _ensure_agent_owned (to_thread), prune broken refs, scoped _fetch_origin_best_effort (30s TTL, force override), re-chown, install_dev_deps (digest-cache hit short-circuits), return. Else: rmtree any partial/stub dir, ProjectService.get_by_slug, resolve the clone target branch via `head_branch(project)` (the env-ladder head rung — `roboco.models.env_branches`, shimmed from `default_branch` when no ladder is declared), _resolve_git_token (decrypt PAT; raise on HTTPS-with-no-token), _clone_repo (git clone --branch --no-tags, configure identity/fileMode, scrub PAT from remote URL, _assert_no_pat_leak scanning .git/** for ghp_/github_pat_/x-access-token, chown, install_dev_deps), _maybe_scaffold_conventions (once-per-process, flag-gated). Per-task worktree path: get_clone_root_path + get_worktree_path (.worktrees/{task_short_id}); ensure_worktree runs git worktree add -b <branch> <base> (or reuses an existing branch ref), _link_shared_venv (symlink to clone-root .venv only if it exists), chowns both worktree and clone root. GitService.create_branch calls ensure_worktree; commit/rebase paths call ensure_worktree_for_resume via GitService._ensure_worktree_for_commit; the orchestrator's _ensure_worktree_before_spawn calls ensure_worktree_self_heal at EVERY spawn (not just after a vanished clone, post-#692), passing `can_author` from the spawning agent's role (`WORKTREE_AUTHOR_ROLES` — developer/documenter): a present worktree now runs through `_refresh_present_worktree` (fast-forward when behind, leave alone when ahead, hard-reset only for a non-author reader when diverged) before venv-link + chown; a vanished clone still fetches the branch ref from origin if the local ref is absent, and a surviving-ref-but-pruned worktree re-adds then runs the identical refresh; TaskService.complete/cancel call remove_worktree. ensure_read_clone is called by ConventionsService for the project-level read clone at _meta/conventions, hard-reset to origin/default. dry_upgrade_changes_lockfile (dep_update_engine) clones the read clone --local --no-hardlinks into a throwaway under the read-clone lock, runs dep_update_command, and checks git status --porcelain on lockfile paths. Outputs: workspace Path (and side effects: on-disk clone/worktree, .venv symlink, .git/.roboco-dep-install + .git/.roboco-toolchain markers, root-owned refs re-chowned to agent uid). All git/subprocess work runs via asyncio.to_thread; tokens are injected only transiently into argv (never written to .git/config) and scrubbed post-clone.
|
||||
|
||||
## Mermaid
|
||||
```mermaid
|
||||
@@ -159,7 +161,7 @@ WorkspaceService slice
|
||||
| ensure_workspace | roboco/services/workspace.py | GitService.create_branch_for_task / push / PR ops; orchestrator spawn ensure; gateway content_actions; called transitively by many verbs |
|
||||
| ensure_worktree | roboco/services/workspace.py | GitService.create_branch_for_task on fresh claim (worktree add -b <branch> <base>) |
|
||||
| ensure_worktree_for_resume | roboco/services/workspace.py | GitService._ensure_worktree_for_commit (commit/rebase paths) |
|
||||
| ensure_worktree_self_heal | roboco/services/workspace.py | orchestrator._ensure_worktree_before_spawn before -w container launch (replaces the former ensure_worktree_for_resume call there; handles vanished clones + missing branch refs) |
|
||||
| ensure_worktree_self_heal | roboco/services/workspace.py | orchestrator._ensure_worktree_before_spawn before -w container launch (replaces the former ensure_worktree_for_resume call there; handles vanished clones + missing branch refs, and — post-#692 — refreshes an already-present worktree against origin at every respawn, role-aware via `can_author`) |
|
||||
| remove_worktree | roboco/services/workspace.py | TaskService terminal/cancel paths + claim-rollback (mid-claim failure) |
|
||||
| delete_local_branch | roboco/services/workspace.py | TaskService terminal/cancel paths (right after remove_worktree) + GitService.cleanup_stale_branches sweep |
|
||||
| ensure_read_clone | roboco/services/workspace.py | ConventionsService.scaffold/effective-map reads (project-level conventions metadata) |
|
||||
@@ -222,18 +224,20 @@ WorkspaceService slice
|
||||
> Further post-snapshot update (#534, env-branches ladder): `ensure_workspace`'s fresh-clone branch and `ensure_read_clone` both resolve their target branch via `roboco.models.env_branches.head_branch(project)` — the env-ladder's head rung — instead of reading `project.default_branch` directly. A project with no declared ladder resolves to the identical `default_branch` value via the read-time shim, so this is behavior-preserving until the CEO declares a real ladder in the panel.
|
||||
>
|
||||
> (open PR #548, branch `feature/wave-2-hygiene-charts`, 2026-07-17) Added `delete_local_branch` (line 787) so `TaskService`'s cancel/terminal-completion cleanup and `GitService.cleanup_stale_branches` can reap a spent local branch ref, not just the worktree — previously every task an agent ever claimed leaked a permanent `refs/heads/{branch}` in that agent's clone.
|
||||
>
|
||||
> `987eb09c` (PR #692, "role-aware worktree refresh at every spawn", 2026-07-24) Closed the frozen-worktree-across-respawns bug behind a live multi-round QA/PR-gate bounce loop: `ensure_worktree_self_heal` gained a `can_author` parameter (from `foundation.identity.WORKTREE_AUTHOR_ROLES` — developer/documenter) and now runs every PRESENT or re-added worktree through the new `_refresh_present_worktree` before venv-link + chown, not just a vanished-clone re-attach. Behind-or-equal fast-forwards for every role (an author's dirty tree is left untouched to do it); strictly ahead is always left alone; diverged only hard-resets for a pure reader. `conventions_check_for_task`'s list-vs-content gap closes as a side effect — a reviewer's worktree is current as of spawn.
|
||||
|
||||
## Regression Risks
|
||||
|
||||
| Title | File:Line | Claim | Severity |
|
||||
|---|---|---|---|
|
||||
| Worktree .venv symlink self-heal depends on a later ensure firing | roboco/services/workspace.py:534 | ensure_worktree (fresh claim) runs _link_shared_venv BEFORE install_dev_deps provisions clone_root/.venv, so the symlink is skipped on the first claim. The shared-venv optimization only self-heals if a later ensure (resume/commit via _ensure_worktree_for_commit) re-runs _link_shared_venv. If the agent commits via a path that does not re-invoke ensure and uv re-syncs a worktree-local .venv first, the lexists guard prevents replacing the real dir and the worktree is stuck with a duplicated venv. The F-fix mitigated the dangling-symlink case but did not close the already-clobbered-venv recovery (explicitly out of scope per commit msg). | medium |
|
||||
| ensure_worktree reuses an existing branch ref without validating it points at base | roboco/services/workspace.py:570 | When branch_exists is True (re-claim after rollback) ensure_worktree runs `worktree add <worktree> <branch>` with no -b and no base. If the surviving branch ref was left at an unexpected commit (e.g. a prior partial rebase, or a force-pushed-and-locally-stale ref), the worktree is created at that commit, not at the intended base. The caller (GitService.create_branch_for_task) assumes a fresh branch at base; a stale ref could spawn the agent on the wrong HEAD. | medium |
|
||||
| ensure_worktree_for_resume silently re-adds a worktree whose branch was force-updated remotely | roboco/services/workspace.py:591 | On resume via GitService._ensure_worktree_for_commit, ensure_worktree_for_resume re-adds the worktree from the surviving local branch ref (no fetch, no base). If the branch was force-pushed remotely while the agent was down and the local ref is stale, the agent resumes on the old commits with no warning. NOTE: the orchestrator spawn path (cfe725da/536bbb64) now calls ensure_worktree_self_heal instead, which fetches the branch ref from origin before re-attaching — the spawn path is resolved. The GitService commit path still uses ensure_worktree_for_resume without a fetch. | medium |
|
||||
| ensure_worktree reuses an existing branch ref without validating it points at base | roboco/services/workspace.py:570 | When branch_exists is True (re-claim after rollback) ensure_worktree runs `worktree add <worktree> <branch>` with no -b and no base. If the surviving branch ref was left at an unexpected commit (e.g. a prior partial rebase, or a force-pushed-and-locally-stale ref), the worktree is created at that commit, not at the intended base. RESOLVED for the orchestrator spawn path (#692): `ensure_worktree_self_heal` now runs every re-add through `_refresh_present_worktree`, which fetches origin and reconciles the ref before the agent starts working. The GitService commit path (`_ensure_worktree_for_commit` → `ensure_worktree_for_resume`) still calls the bare, unrefreshed re-add — residual risk there only. | low |
|
||||
| ensure_worktree_for_resume silently re-adds a worktree whose branch was force-updated remotely | roboco/services/workspace.py:591 | On resume via GitService._ensure_worktree_for_commit, ensure_worktree_for_resume re-adds the worktree from the surviving local branch ref (no fetch, no base). If the branch was force-pushed remotely while the agent was down and the local ref is stale, the agent resumes on the old commits with no warning. RESOLVED for the orchestrator spawn path: `ensure_worktree_self_heal` (#692) now runs a PRESENT worktree — and a re-added one recovered from a surviving ref — through the same `_refresh_present_worktree` classification at every respawn (fast-forward when behind, reset only for a non-author role when diverged), not just after a vanished clone. The GitService commit path still uses the bare, unrefreshed `ensure_worktree_for_resume`. | low |
|
||||
| PAT-leak scan cannot run if .git was wiped by a prior failed rmtree | roboco/services/workspace.py:1391 | _assert_no_pat_leak (line 1352) guards on `git_dir.exists()` and returns early if not. If a catastrophic clone left .git partially absent but the auth URL written elsewhere (e.g. into .git/config before .git/objects was created), the early return means the leak check is skipped. Combined with the F063 rmtree-on-failure this is low risk, but a rmtree that fails silently (ignore_errors=True at line 1382 only triggers on leak detection, not on the failure branches) could leave a tokenized .git/config. | low |
|
||||
| dry_upgrade probe lock gap could race a future interleaved sync | roboco/services/workspace.py:1673 | The dep-update probe acquires the _meta-conventions lock only around _clone_local_into (line 1676) and releases it before _probe_lockfile_on_clone (line 1679). The commit msg argues this is safe because any concurrent _sync_read_clone completes under the lock first. This holds ONLY because _sync_read_clone is the sole other holder; if a future change adds a third concurrent mutator of the read clone that interleaves between the release and re-acquire (none today), the local clone could read a half-mutated source. Fragile invariant documented only in the commit, not enforced. | low |
|
||||
| _fetch_origin_best_effort TTL cache not shared between clone root and worktree paths | roboco/services/workspace.py:1038 | _fetch_cache is keyed by str(workspace) on the instance. ensure_workspace is called with the clone-root path, but a worktree-path caller (none currently call ensure_workspace directly with a worktree path, but _resolve_clone_root exists to support worktree-aware uv env) would get a separate cache entry. Not a current bug, but a future worktree-aware ensure_workspace call could double-fetch. | low |
|
||||
| _ensure_agent_owned walk excludes .venv/node_modules but agent may need to write them | roboco/services/workspace.py:67 | _PRUNE_DIRS skips .venv, node_modules, .next etc. from the chown walk for speed. The agent normally owns these (it created them) and the symlinked worktree .venv points to the clone-root .venv which IS walked (it is not under a pruned name at clone root). But a worktree-local .venv created by uv when the symlink was missing (regression risk #1) would NOT be chowned, leaving the agent unable to write into it. Edge case, low severity. | low |
|
||||
|
||||
## Health
|
||||
WorkspaceService is a mature, heavily-instrumented slice with strong defensive hygiene: per-(project,agent) asyncio locks, partial-clone detection + rmtree, a real .git+HEAD+objects health check, scoped + TTL-cached refresh fetches, PAT injection that is never persisted to .git/config, a belt-and-suspenders leak scan that destroys the workspace on any hit, idempotent lockfile-digest-gated dev-deps install, and F123 per-task worktrees that eliminated the coordinator-PM clobber. The F063 + F116 + F123 + F-fix wave closed real deploy-blocker races (PAT leak on half-configured clone, read-clone mid-clone race, root-clobber, dangling venv symlink). Residual risk is concentrated in the worktree venv-symlink timing (first-claim skip depends on a later ensure to self-heal), the resume path reusing a possibly-stale local branch ref without a freshness check, and the process-local (not cross-process) ensure-workspace lock. The slice diverges from CLAUDE.md's "fresh claim git reset --hard" narrative — by design, post-F123 — and that doc drift should be reconciled. Overall integrity is high; the regression risks are edge-case rather than core-path.
|
||||
WorkspaceService is a mature, heavily-instrumented slice with strong defensive hygiene: per-(project,agent) asyncio locks, partial-clone detection + rmtree, a real .git+HEAD+objects health check, scoped + TTL-cached refresh fetches, PAT injection that is never persisted to .git/config, a belt-and-suspenders leak scan that destroys the workspace on any hit, idempotent lockfile-digest-gated dev-deps install, and F123 per-task worktrees that eliminated the coordinator-PM clobber. The F063 + F116 + F123 + F-fix wave closed real deploy-blocker races (PAT leak on half-configured clone, read-clone mid-clone race, root-clobber, dangling venv symlink). Residual risk is concentrated in the worktree venv-symlink timing (first-claim skip depends on a later ensure to self-heal), the GitService commit path's resume re-add still reusing a possibly-stale local branch ref without a fetch (the orchestrator spawn path's own version of this is now resolved by #692's role-aware `_refresh_present_worktree`), and the process-local (not cross-process) ensure-workspace lock. The slice diverges from CLAUDE.md's "fresh claim git reset --hard" narrative — by design, post-F123 — and that doc drift should be reconciled. Overall integrity is high; the regression risks are edge-case rather than core-path.
|
||||
|
||||
@@ -30,6 +30,7 @@ Each takes `findings: list[dict]` — a list of structured findings. The legacy
|
||||
|
||||
- `severity`: `blocker` (must fix before merge/pass) → `major` (significant defect) → `minor` (small defect, fix advised) → `nit` (cosmetic).
|
||||
- `criterion`, if supplied, must match one of the task's acceptance-criterion ids or exact text — a criterion that matches neither is rejected outright, so a typo doesn't silently detach the finding from what it's actually about.
|
||||
- `file`, if supplied, must actually LOOK like a repo-relative path (letters/digits/`._-/()[]+@`, no spaces) — narrative like "PR #676 description" is rejected outright instead of silently accepted and then dooming the panel's code-snippet fetch. Cross-cutting or narrative feedback that has no single file:line home should omit `file` entirely and put the detail in `evidence` instead — that's the legal file-less option, not a workaround.
|
||||
- **Count guard**: a soft nudge appears (non-blocking) above 5 findings in one call; more than 10 is a hard reject — split across calls or prioritize the blocking ones first. An oversized findings list is as unreviewable as an oversized task.
|
||||
|
||||
## What happens when you file one
|
||||
|
||||
@@ -71,7 +71,7 @@ Your agent clone is **shared across all your tasks**, but each **claimed task**
|
||||
```
|
||||
|
||||
- **Claim** (`i_will_work_on` / `claim_review` / `claim_doc_task`) adds a worktree at `.worktrees/{task-id-first-8}/` and checks out the task's branch there. The clone root's HEAD is **never moved** by a claim.
|
||||
- **Your container is started with `-w` pointing at the worktree** for your current task, so `commit`, edits, and `uv run` all resolve there automatically. Spawn resolves the worktree from your `current_task_id` on every spawn (never cached), so a resume/respawn re-attaches a pruned worktree before launch.
|
||||
- **Your container is started with `-w` pointing at the worktree** for your current task, so `commit`, edits, and `uv run` all resolve there automatically. Spawn resolves the worktree from your `current_task_id` on every spawn (never cached), so a resume/respawn re-attaches a pruned worktree before launch. Every spawn also RE-SYNCS an already-present worktree against origin (role-aware): if you're a developer/documenter, your own dirty uncommitted edits are never discarded to do it — a behind-or-equal worktree fast-forwards, a strictly-ahead one (your own unpushed commits) is left exactly alone; if you're QA/PR-gate/PM reviewing someone else's branch, a diverged worktree hard-resets to origin, since your local history there can only ever be a stale prior-round checkout. This closes the "reviewer keeps re-examining its own frozen round-1 checkout across a multi-round bounce" class — you're never respawned onto commits older than what's actually on the branch.
|
||||
- **The clone-root `.venv` is shared** — each worktree's `.venv` is a symlink to `../../.venv`, so `uv run` from a worktree resolves the clone-root venv. No per-worktree re-sync.
|
||||
- **Git ops split by kind**: checkout/HEAD-moving ops (`create_branch`, `commit`, `rebase`, `checkout`) target the worktree; branch-by-name ops (`push`, `pull`, `fetch`, `pr_merge`, `diff`) run from the clone root. You never do either by hand — the verbs resolve the worktree for you.
|
||||
- **One active WorkSession per task** is enforced both in the service layer and by a DB unique index — a re-claim (pool release, reaper unclaim, escalation redirect) supersedes any prior agent's stale session for that task.
|
||||
|
||||
@@ -89,7 +89,7 @@ request_changes(task_id, findings=[...])
|
||||
escalate_up(task_id, reason) # escalate to your escalation target
|
||||
```
|
||||
|
||||
After `i_will_plan` and each `delegate`, the envelope includes a coverage view of the parent — `parent_ac_coverage` (per-criterion `id` / `text` / `claimed` / `verified`) and `unclaimed_parent_acs` (criteria no subtask covers yet). A parent cannot idle with unclaimed criteria, nor `complete` / `submit_up` / `escalate_to_ceo` until every criterion traces to a child that passed QA. `delegate` refusing a child with no `covers_parent_criteria` (above) is what puts every parent with acceptance criteria under this coverage discipline from its first subtask on — a decomposition can no longer opt out by never declaring. `i_will_plan`'s planning briefing also carries `collision_context` (in `context_briefing`, not `evidence`) surfacing any same-parent siblings that already collide on file globs or migrations, so you can sequence your delegation before you commit to it. See `docs/rag/workflows/task-planning.md`.
|
||||
After `i_will_plan` and each `delegate`, the envelope includes a coverage view of the parent — `parent_ac_coverage` (per-criterion `id` / `text` / `claimed` / `verified`) and `unclaimed_parent_acs` (criteria no subtask covers yet). A parent cannot idle with unclaimed criteria, nor `complete` / `submit_up` / `escalate_to_ceo` until every criterion traces to a child that passed QA. `delegate` refusing a child with no `covers_parent_criteria` (above) is what puts every parent with acceptance criteria under this coverage discipline from its first subtask on — a decomposition can no longer opt out by never declaring. A rejection now includes a copy-pasteable corrected `delegate(...)` skeleton with the parent's real criteria inlined (an id when the parent has one, its exact quoted text otherwise) — retry with that shape verbatim rather than re-deriving the field's syntax. `i_will_plan`'s planning briefing also carries `collision_context` (in `context_briefing`, not `evidence`) surfacing any same-parent siblings that already collide on file globs or migrations, so you can sequence your delegation before you commit to it. See `docs/rag/workflows/task-planning.md`.
|
||||
|
||||
**Delegation rules** (enforced): `main_pm -> cell_pm`; `cell_pm -> its team's devs`. Cell PMs receive planning-typed parent tasks; devs get code/research (UX devs also design). Always create subtasks via `delegate` with `parent_task_id` set — there is no standalone task-create verb for agents.
|
||||
|
||||
|
||||
@@ -69,6 +69,14 @@ Don't checkout by hand — there is no `roboco_git_checkout` tool.
|
||||
|
||||
**Fix:** Either `commit(message=...)` (omit `files` to stage everything) then `sync_branch(task_id)` again, OR call `sync_branch(task_id, stash=True)` to auto-stash (tracked + untracked), rebase, and restore your changes in one call. If the stash pop conflicts, the envelope's `next` tells you so — your stash is preserved (never dropped); resolve by hand and `commit(...)`. Still stuck after that? `unclaim(task_id)` releases the claim back to the pool rather than looping.
|
||||
|
||||
## sync_branch refused: branch DIVERGED from its origin copy
|
||||
|
||||
**Error:** `sync_branch` returns `next` telling you your branch and its origin copy have DIVERGED (both sides carry commits the other lacks).
|
||||
|
||||
**Cause:** Your workspace and `origin/<your-branch>` each hold real work the other doesn't — not the routine "behind base" case. `sync_branch` first checks whether this is actually the harmless residue of an EARLIER `sync_branch` call whose rebase succeeded locally but whose force-push then failed (a network blip, a container reap) — that self-heals silently as an ordinary rebase on this retry. What's left after that check is a genuine divergence, most often your task bounced to a different agent's clone that pushed meanwhile.
|
||||
|
||||
**Fix:** Neither side is touched or lost — nothing was reset, nothing was rebased, nothing was pushed. `i_am_blocked(reason='...')` naming the divergence so a human can reconcile the two histories by hand (fetch, inspect both tips, merge or rebase deliberately). If `stash=True` was passed and the stash pop ALSO conflicted, your stash is preserved (never dropped) — resolve it by hand alongside the divergence.
|
||||
|
||||
## src refspec does not match any (during open_pr)
|
||||
|
||||
**Error:** `src refspec '<branch>' does not match any`
|
||||
|
||||
@@ -115,7 +115,7 @@ escalate_to_ceo(task_id=parent_id, reason="...")
|
||||
|
||||
**Symptom**: Your in-progress task flips to `blocked` with a budget marker, and you weren't the one who blocked it.
|
||||
|
||||
**Cause** (only when task budgets are armed): a periodic sweep prices the task's own spend against its `budget_usd` (or the `TaskType` default when unset) and blocks it before you get a chance to finish gracefully, so the dispatcher won't respawn onto an already-over-budget task.
|
||||
**Cause** (only when task budgets are armed): a periodic sweep prices the task's own spend against its explicit `budget_usd` and blocks it before you get a chance to finish gracefully, so the dispatcher won't respawn onto an already-over-budget task. A task with no `budget_usd` set is uncapped at this check (explicit-input only — there is no `TaskType`-keyed default anymore); the project's `monthly_budget_usd` is the separate fleet-wide backstop.
|
||||
|
||||
**Fix**: This is a PM/CEO decision, not yours to route around — `unblock` re-checks live spend and refuses again while still over the cap. Escalate (`i_am_blocked`/`escalate_up`) naming the budget breach; the CEO either raises the task's/project's cap or the PM redirects the remaining work.
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ If your task has `dependency_ids` in the same repo, the fresh branch cut also ba
|
||||
- **Self-review prevention**: QA cannot `claim_review` tasks they developed
|
||||
- **Self-documentation prevention**: Documenter cannot claim tasks they developed
|
||||
- **Branch requirement**: Branch auto-created on `i_will_work_on`
|
||||
- **Sequence order (strict, assignee-blind)**: if a task has a parent and a `sequence` number, it cannot be claimed while any sibling with a strictly lower sequence is still non-terminal — regardless of who owns which task. Siblings on the SAME sequence run in parallel (independent work ties at 0, or at the wave a delegating PM stamped from the collision graph). This is independent of, and stricter than, `dependency_ids`: a claim attempt on a sequence-held task fails even with no unmet dependency. The error names the blocking sibling by title — `unclaim`/wait is the only remedy, there is no override verb. The dispatcher pre-filters sequence-held (and dependency-held) tasks before attempting a claim, so you should rarely see this in practice — but a claim you make directly (rather than via `give_me_work`) can still hit it.
|
||||
- **Sequence order (assignee-blind, reachability-aware outside MegaTask batches)**: if a task has a parent and a `sequence` number, a lower-sequence sibling can hold it — but whether ANY lower-sequence sibling blocks, or only a REAL one, depends on context. Inside a MegaTask batch (a root-subtask under the umbrella), the rule is strict and edge-agnostic exactly as before: held while ANY same-parent sibling with a strictly lower sequence is non-terminal, regardless of whether the two tasks are actually connected (the batch's `sequence` is a one-shot, globally-computed staged-release wave). Everywhere else, a lower-sequence sibling only blocks when it's a real (transitive) predecessor via `dependency_ids` — a task with no dependency edge onto any same-parent sibling still falls back to the strict raw bar. Siblings on the SAME sequence always run in parallel; cancelled siblings never block. This is independent of, and stricter than (in the batch case), `dependency_ids`: a claim attempt on a sequence-held task fails even with no unmet dependency. A held claim now returns a dedicated `sequence_held` error naming the blocking sibling — `unclaim`/wait is the only remedy, there is no override verb. The dispatcher pre-filters sequence-held (and dependency-held) tasks before attempting a claim (including on `needs_revision` reclaims), so you should rarely see this in practice — but a claim you make directly (rather than via `give_me_work`) can still hit it.
|
||||
- **Project budget cap** (when task budgets are armed): `i_will_work_on` / `i_will_plan` are refused once the project's `monthly_budget_usd` has been reached this calendar month — a WORK-STARTING claim only, so a QA/doc/PR-review/PM-merge claim on already-in-flight work is never blocked by this. There is no override; wait for the next month or ask the CEO to raise the cap.
|
||||
|
||||
## Releasing a Claimed Task
|
||||
|
||||
Reference in New Issue
Block a user