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:
+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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user