mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
9028d3b641dca44d20cb55f471173b14378ef572
135
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
67b8893071 |
[F048] notify: reject human-only recipients (prompter/secretary) — no agent ack path
notify() only checked the SENDER role. The recipient was resolved by NotificationService._resolve_recipients, which drops only unresolvable slugs — it does not exclude human-only roles. The prompter (intake-1) and secretary (secretary-1) are seeded agent rows, so they resolved, and an ack-required ALERT addressed to them sat permanently unacked (no agent auto-acks it), polluted the panel's pending-ack view, and — via the dedup query's ~acked_by.contains — permanently suppressed any later same-purpose notification from the same sender to that human role. The knowledge-share path already excludes all three human-only roles; the general notify path did not. Fix: a recipient-role guard in notify() via _reject_disallowed_recipient (folds the new check into the existing CEO-dependency-block return slot so notify stays under the PLR0911 return limit). Rejects prompter/secretary with not_authorized; the CEO is human too but acks via the panel, so it stays an allowed recipient (its only disallowed case, a dependency-block page, is preserved). TDD test_notify.py (+3: reject prompter, reject secretary, allow CEO). |
||
|
|
ee5321fde1 |
[F047] conventions: reviewer-aware block-finding remediation on pr_pass gate
The pr_pass (reviewer) conventions guard reused the dev-path block-finding remediation: 'add a waiver to .roboco/conventions.yml in your branch'. A pr_reviewer does not own the assembled cell->root / root->master branch and has no commit verb on it, so the waiver remediation is unreachable — a false positive stranded the gate with no self-recovery (the reviewer could neither commit a waiver nor pr_pass). The fail-open content path is documented precision-over-recall and stays as-is; the actionable gap is the remediation. Fix: _conventions_rejection now branches the block-finding remediation on reviewer=True (mirroring the could_not_run branch from F044). The reviewer path points at pr_fail carrying the findings as issues so the PR returns to needs_revision and the DEV fixes the violation or commits the waiver (the dev CAN commit to the branch); waiver authorship is framed as the dev's action, not the reviewer's. Dev i_am_done path wording unchanged. TDD test_conventions_gate_pr_pass.py (+1). |
||
|
|
86ab732f7a |
[F046] pr_gate: guard None runner result on concurrent transition (pr_pass/pr_fail)
_gate_decision dereferenced the verb-runner result without a None guard. run_intent returns None when a concurrent transition (cancel or a racing reviewer) moves the task out of awaiting_pr_review between the precondition gate and the runner's final composed action (the verb runner's documented last-action source-status contract). The subsequent t.assigned_to / t.status / _post_gate_review_to_pr(t, ...) dereferences then crashed the gate with a 500 AttributeError. Add a None guard that surfaces a clean invalid_state rejection (re-fetch + re-issue) before any dereference; no PR post or a2a runs against a None task. TDD test_pr_gate_notifies_pm.py (+2). |
||
|
|
a6de1330aa |
[F045] rate-limit: loud activate-failure log + in-memory orphan-probe fallback
The in-verb i_am_blocked(rate_limited) path wrapped RateLimitStateTracker.activate in a bare contextlib.suppress. A silent activate failure stranded the fleet: agents were parked in _waiting_records but the provider never entered the tracker, so the tracker-driven _sweep_rate_limit_probes never probed it and no _on_probe_success ever resumed them — parked agents stuck in WAITING_LONG. Fix: (1) replace the bare suppress with a try/except that logs an error event naming the provider + affected agents; (2) in _sweep_rate_limit_probes, after probing the tracker-listed set, scan _waiting_records for any rate_limit_lifted provider the loop did NOT cover and probe it via the time-expiry fallback (empty state -> probe now) so _on_probe_success resumes the parked agents. The fallback reads only local memory, so it still resumes when Redis was down at park time (list_rate_limited_providers failure now falls through to the orphan scan instead of returning early). |
||
|
|
05a5852bb5 |
[F044] pr_pass gate remediation points the reviewer at pr_fail, not i_am_blocked
The pr_pass gate runs the toolchain + conventions guards on the REVIEWER's workspace, but their remediation text said 'call i_am_blocked' — a verb the PR reviewer does not have. The reviewer would chase a verb they cannot call instead of rejecting the PR. Make the guards reviewer-aware: a reviewer=True flag (passed by _pr_pass_blocked) switches the remediation to pr_fail(issues=[...]) — the reviewer's reject lever, sending the PR back to needs_revision for the dev to fix the environment / validator. The dev (i_am_done) path keeps i_am_blocked, which a dev does have. _conventions_guard (the pr_pass path) now passes reviewer=True through to _conventions_rejection. |
||
|
|
33dce5f127 | [F018] claim_guards: treat blocked as active + broaden the guard lookup so a blocked dev can't double-claim | ||
|
|
366800299b |
[F008] evidence_builder: surface persisted pr_review verdict+issues in the PM task_handoff
The pr_fail a2a steer to the owning PM is fire-and-forget; a PM respawned into needs_revision later read none of it (build_task_handoff never looked at notes_structured), saw a generic 'needs revision' with zero concrete change-requests, and re-submitted the same PR (the 2026-06-27 infinite pr_fail loop on 9980d0a0 / PR #138). The signal-gap was only partially closed by the a2a. build_task_handoff now extracts notes_structured.pr_review (verdict/summary/issues/head_sha — the slot pr_fail authors on every fail) into a pr_review field on the handoff, so every PM briefing for the task carries the concrete change-requests. A prior pr_fail alone now counts as prior-work-worth-resuming. Type-guarded + capped; absent => no key (no misleading empty slot). TDD red->green; ruff + mypy clean; evidence_builder suite green (14 passed). |
||
|
|
2ed7f5978f |
[F007] choreographer: cell-level unchanged-PR re-submit loop-stopper for submit_up
The root loop-stopper (F016) was root-only; a weak cell PM could re-submit the unchanged cell->root PR after a pr_fail and loop awaiting_pr_review -> pr_fail forever (the cell analogue of the 2026-06-27 root loop). pr_fail stamps the assembled PR's head SHA into notes_structured.pr_review .head_sha for cell AND root gate tasks alike (the capture is gate-verb- level, not root-level), so the same structural refusal applies to submit_up: if the cell PR's current head SHA equals the SHA the last pr_fail recorded, no new dev work landed on the cell branch -> the diff is byte-identical -> refuse, do not re-open the gate. Different SHA -> branch advanced -> allow. - _submit_up_unchanged_pr_guard mirrors _submit_root_unchanged_pr_guard (cell-PM remediation: re-delegate to the dev + wait for re-assembly), wired into submit_up after _submit_up_guard passes. - Renamed shared _current_root_pr_head_sha -> _current_pr_head_sha (both guards use it; the lookup was never root-specific). - Every ambiguous case FAILS OPEN (no prior fail, no recorded sha, no pr_number, no resolvable project, git/closed-PR None) — only the exact- unchanged case is hard-blocked. TDD red->green; ruff + mypy clean; F007+F016 guard suites green (15 passed). |
||
|
|
742e0ebfaf |
[F016,F017] choreographer: surface invalid_state instead of None.status 500 on submit_root / i_am_blocked
Both verbs compose a single atomic action whose None return (the verb's own result) flowed out of run_intent and was dereferenced as t.status, HTTP 500-ing with no actionable rejection: - F016 submit_root: submit_for_review returns None when the root->master PR was already opened / the task raced out of in_progress. Post-runner None-guard extracted into _submit_root_finalize -> invalid_state (re-fetch; if awaiting_pr_review the PR is open, wait for reviewer; else re-delegate fixes and retry) instead of None.status. - F017 i_am_blocked: escalate returns None in four cases (no task, no agent, no resolvable escalation-target slug, no target agent row) e.g. a developer whose role has no PM above it. _run_i_am_blocked_intent now guards updated is None -> (t, invalid_state rejection) with remediation (re-fetch + escalate to CEO directly / retry) instead of the caller deref'ing None.status -> 500 + respawn-loop. TDD red->green; ruff + mypy clean; gateway suite green (58 passed). |
||
|
|
d579d3120a |
[F002] retype board-routed MegaTask root-subtasks code->planning on activation
_activate_batch_root_subtasks flipped a held root-subtask to team=MAIN_PM but left task_type=code (intake only coerces main_pm-team drafts, so a board-routed code root reached activation still code-typed). The main_pm+code combo re-introduces the 2026-06-27 meltdown. Mirror approve_and_start's own retype via main_pm_cannot_own_code so the activated child is a planning-typed coordination root. TDD: RED test_activate_batch_root_subtasks_retypes_code_to_planning watched fail (task_type stayed CODE), then GREEN after the retype. ruff+mypy clean; 125 batch/umbrella/approve tests green, no regressions. |
||
|
|
fbddef403b |
[feature] i_am_done behind-base submit gate (Phase B2)
A sibling's PR merging into the parent branch while a dev worked leaves the
dev's branch behind its base — the assembled PR then can't merge cleanly and
the sibling's changes go missing (the 2026-06-27 out-of-order dev-task break).
The behind-base gate refuses i_am_done in that state and steers the dev to
sync_branch (the Phase B1 gate-level rebase verb).
- GitService.is_behind_base: rev-list --left-right --count across
origin/{base}...origin/{head} → (behind, ahead); fetch-first so origin
reflects the pushed head. Raises on git failure (consistent with
rebase_onto_base); malformed stdout degrades to (0,0).
- Choreographer._behind_base_gate: wired into _i_am_done_gate after
_ensure_branch_pushed. behind>0 → invalid_state remediate→sync_branch.
Fail-open on git/base-resolution error (flaky fetch can't strand a task at
the submit gate — the merge layer has its own behind checks). Skipped for
branchless roots and protected bases (master/main/-prefixed).
Tests: gate (6: refuse+steer/up-to-date/branchless/protected/fail-open-base/
fail-open-git), is_behind_base (6: parse/up-to-date/malformed/argv-form/
requires-branch/missing-project). ruff + mypy roboco/ tests/ clean; unit green.
|
||
|
|
250be5c246 |
[feature] sync_branch dev verb — gate-level branch rebase (Phase B1)
Raw shell git is denied to agents (Bash(git:*) base deny), so a developer whose branch fell behind its base had no gate-level rebase — only the CEO/PM-only /rebase HTTP route. sync_branch is the dev verb that wraps the rebase through the gate (traced + evidenced), so the 'everything goes through the gates' invariant holds. - lifecycle: IntentSpec sync_branch (dev-only, ownership-gated, composes=(), git-only — no DB transition); _next_hint_synced helper. - GitService.sync_task_branch: rebase task.branch_name onto its resolved base via rebase_onto_base (fetch + rebase + force-with-lease push). - Choreographer.sync_branch + _sync_branch_preflight_rejection: not_found / unknown-role / spec-gate / no-branch / protected-base guards, then the git op; conflicts abort (no force-push) and steer to resolve-by-hand; git failure steers to i_am_blocked. - HTTP route /api/v1/flow/developer/sync_branch + SyncBranchRequest schema. - MCP tool sync_branch(task_id) + _TOOLS registration (manifest auto-propagates via intents_for_role(Role.DEVELOPER)). Tests: intent spec (5), choreographer handler (8: happy/conflicts/not_found/ not_authorized/no-branch/protected-base/git-failure/audit), route (1), MCP (1). ruff + mypy roboco/ tests/ clean; unit suite green (DB-fixture errors env-only). |
||
|
|
6f8d0a4e0b |
[chore] mypy tests/: clear all 15 pre-existing type errors so make quality can go green
The branch tip had 15 mypy tests/ errors in files this bundle did not author,
which blocked CI's make quality mypy step (mypy roboco/ tests/) regardless of
the bundle's own commits. Pre-existing is still existing — fix every one:
- test_schemas_v1_flow.py (8): the StrList coercion tests intentionally pass
SDK-nested list-of-strings input ([[['...']]], {'item':{'$text':'...'}}, int,
dict). Annotate those literals as list[Any] locals so mypy accepts the
coerce-able shape; the StrList BeforeValidator still flattens to list[str] at
runtime. No type:ignore.
- test_pr_gate_records_verdict.py (3): notes_structured is dict|None; narrow
with 'assert t.notes_structured is not None' before indexing (the existing
pattern at line 90).
- test_pr_review_hand_format_guard.py (1 site, 2 errors): the _verb_runner()
spy assertion — use the cc: Any = c alias idiom so assert_not_awaited
resolves; drops the now-unused type:ignore[union-attr].
- test_pr_gate_notifies_pm.py (1): drop the unused type:ignore[method-assign]
on the a2a.send reassignment.
- test_content_models.py (1): narrow coerced with isinstance(coerced,
PrReviewContent) before reading .issues (the base _Content lacks the field).
Gates: rm -rf .mypy_cache && mypy roboco/ tests/ = Success (855 files);
ruff check + format clean; 5 affected suites = 40 passed.
|
||
|
|
e52fd05d59 |
[fix] submit_root: hard unchanged-PR gate stops the pr_fail re-submit loop
The 2026-06-27 infinite pr_fail loop: a Main-PM root (PR #139) was pr_fail'd, routed to needs_revision, and re-submitted byte-identical → awaiting_pr_review → pr_fail again, forever. The prior hint/a2a steer was ignored by the weak coordinator model — hints don't stop a model that won't read them. A HARD gate refuses the re-submit when the assembled root PR's head SHA is unchanged since the last pr_fail (no new cell work → identical diff); a different SHA ⇒ the branch advanced ⇒ allow. Every ambiguous case fails open (no prior fail, no recorded SHA, no pr_number, unresolvable slug, git error, closed PR) — only the exact-unchanged case is hard-blocked. - content/models: PrReviewContent.head_sha (optional; JSON col → no migration). - git: get_pr_head_sha (GitHub pulls API; None on any failure → fail-open). - pr_gate: pr_fail captures head_sha into the verdict record; pr_pass does not. - _impl: submit_root runs _submit_root_unchanged_pr_guard after _submit_up_guard; _current_root_pr_head_sha resolves slug + current SHA (fail-open). - pr_review: extract module-level resolve_task_project_slug, shared by the mixin and the gate helper (_LegacyChoreographer reaches it via cast to the ChoreographerHelpers typed view — it doesn't inherit the helpers mixin). - tests: test_submit_root_unchanged_pr_guard (11 — refuse/allow/6 fail-open/3 capture-side, mypy-clean via cc:Any spy idiom, zero type:ignore) + test_pr_gate_notifies_pm capture-path stub. |
||
|
|
e202ce397d | Fix: Make main_pm + task_type=code impossible | ||
|
|
5b931c367f |
Fix different project same PR number collision problem
Fix (two layers): 1. Root cause — pr_merge and rebase_pr_for_task now take a required project_id and scope the lookup where(pr_number == X AND project_id == Y). Required so no caller can forget — the bug class can't recur. All 4 call sites updated (choreographer cell_pm_complete, the rebase-retry, the superseded close_pull_request now passes project_id, and _verb_runner._do_pr_merge). 2. Crash guard — _finalize_cell_complete None-checks the complete() return and returns a clean invalid_state envelope (with a remediate hint) instead of dereffing None → 500 → respawn loop. |
||
|
|
53d60da37e | Bunch of runtime fixes for MegaTask and other issues | ||
|
|
88d00aaa0a |
fix(pr-review): reject a verdict that contradicts the review's findings
post_pr_review (inbound external/fork PR review) derived both the recorded notes_structured.pr_review.verdict AND the posted GitHub review event solely from its `event` argument, which defaults to REQUEST_CHANGES — and, unlike the in-path gate's pr_fail, it never required any findings. A reviewer that concluded 'approve' in the summary but left event at the default filed (and posted to the contributor's PR) a blocking 'changes requested' with nothing cited, contradicting the approving summary the CEO saw on the PR Reviewer Notes card. Enforce a verdict<->findings invariant before any record or GitHub post: - REQUEST_CHANGES must cite >=1 finding (almost always a forgotten event='APPROVE'), mirroring pr_fail's 'at least one issue' rule; - APPROVE may not carry a blocker/major finding. The check is the pure policy fn pr_review_conflict() wired through the new choreographer _verdict_consistency_gate, rejected with a clear remediate hint. Steer the agent at the source too: the flow MCP tool + request schema now spell out the invariant and to pass event='APPROVE' explicitly for a clean PR. |
||
|
|
5612375cba |
Feat/v0.13.0 (#270)
* feat(release): add release-manager feature flag (default off) * feat(release): change classification + semver-bump derivation * feat(release): readiness audit (changelog/version-ref/docs/migration/gate) * feat(release): release-manager engine proposes a gated release * feat(release): fail-closed release executor (bump, gate, publish) * feat(release): CEO approve/reject release-proposal surface * docs(release): document the gated release manager * feat(memory): add org-memory feature flags (default off) * feat(memory): add playbooks table + status enum + migration * feat(memory): playbook service with auditor curation transitions * feat(memory): playbooks RAG index plugin * feat(memory): index a playbook into RAG on approval * feat(memory): distill a high-signal lesson at task completion * feat(memory): keep private journal reflections out of the shared RAG corpus * feat(memory): draft_playbook verb + auditor curation verbs * fix(ci): resolve mypy tests/ errors blocking the gate (UUID casts, annotations) * feat(memory): auto-inject similar lessons/playbooks into the briefing * feat(memory): auditor playbook review queue (api + panel) * docs(memory): document the org-memory loop + playbook verbs * fix(provisioning): idempotent pitch provisioning (reuse product/project by slug on re-approval) * fix(memory): add chunks_playbooks to the chunk schema + isolate release route tests - Migration 030's CHUNK_TABLES was missing chunks_playbooks, breaking the IndexType<->migration parity guard once the PLAYBOOKS index landed. The upgrade is ALTER ... IF EXISTS so adding it is safe on any DB shape. - The release-route fixture's approve/reject paths call db.commit() (real behavior), so a held proposal outlived the per-test rollback and leaked into engine tests that read the global list_open_release_proposals(). Tear down source=release_manager rows after each test. - Make the gather_snapshot real-repo smoke version-agnostic (semver match) so it stops pinning the literal repo version. * chore(release): 0.13.0 * ++ --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
99cf56dff3 | [48849b22] Identify and fix the failing make quality step on roboco-api master (#260) (#261) (#262) | ||
|
|
2cce7d6a9f |
fix(pr-gate): land gate verdict on product-scoped PRs + persist it to notes
Two in-path PR-review-gate bugs surfaced reviewing the guard-core-app recovery roots (PR #107 / root fead4372): 1. No verdict comment reached the PR. _project_slug_for returned None whenever project_id was None — but a Main-PM coordination root (the only task a root->master PR ever sits on) carries just a product_id (the cell->repo map), so _post_gate_review_to_pr resolved a None slug and silently no-op'd. It now falls through to the product's first distinct project (mirrors GitService._project_for_task), so the gate verdict actually lands on the PR. 2. The task's PR-reviewer notes contradicted the transition. pr_pass / pr_fail only threaded their notes through the tracing-gate shim and posted to GitHub; nothing wrote notes_structured.pr_review. A root passed once and later failed kept showing verdict=passed while the real transition was pr_fail. The gate now authors the canonical pr_review note on every decision (pr_pass -> passed, pr_fail -> failed), best-effort so a malformed note never rolls back the gate. Adds unit tests for the product-slug fallback and the verdict persistence. |
||
|
|
60c64c70e8 |
fix(run-hardening): break PM decision-gate, stale-agent, and empty-diff loops (#255)
Forensic triage of a 24h run reconstructed the dominant gateway.rejected loops from the audit_log. After earlier deploys fixed the i_will_plan crash and the open_pr push-gap, three real, recurring-capable burn loops remained. This fixes them at the architecture level, not by prompt-nagging. journal:decision write-then-gate (the dominant completion-path blocker): PM decision-point verbs required a separate note(scope='decision') call before the verb, which loaded/weak models forget to chain — so complete and unblock hit a tracing_gap (journal:decision missing) and respawn-looped, stranding finished tasks forever. Each verb now auto-records its OWN rationale as the journal:decision before the gate runs (the proven i_am_blocked -> write_struggle pattern), so the gate passes off real, persisted reasoning. unblock gains a required `reason` (threaded MCP tool -> request schema -> routes -> choreographer); delegate derives the decision from its title + description; complete/submit_up/submit_root/escalate_up/ escalate_to_ceo reuse their existing notes/reason. The gate still runs as defense-in-depth; the auto-record is idempotent within the decision window and best-effort. Adds JournalService.write_decision and Choreographer._ensure_pm_decision. open_pr empty-diff 422: an overlapping-decomposition leaf with zero commits vs its base makes GitHub 422 "No commits between ...". The generic invalid_state "retry" looped the dev 15x on one task. open_pr now steers to a terminal i_am_blocked hand-off so the PM completes or cancels the redundant leaf. owns_task stale-agent loop (41x): a superseded agent (task reassigned away) calling i_am_done/open_pr got a PRECONDITION_OWNERSHIP tracing_gap it read as a fixable precondition and retried forever. Both verbs now short-circuit with the clear not_authorized "no longer yours -> give_me_work" steer that resume/unclaim already use. RAG docs updated for the new unblock(reason) signature; CHANGELOG entries added under 0.11.0 (unreleased). open_pr refactored into _open_pr_preflight_rejection + _open_pr_failure_env to stay within the return-count and complexity budgets. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
2bd35e1c9e |
fix(run-hardening): stop three blocked-task respawn loops (#253)
* fix(run-hardening): stop three blocked-task respawn loops
Three independent fixes for blocked-task respawn loops observed in the live
run (the bleeders behind a wedged near-complete run):
- verb runner: re-check the working task after EACH composed atomic action,
not just at entry. A concurrent transition between a verb's precondition
gate and execution (e.g. a racing i_am_blocked moving a root from
needs_revision to blocked) made claim() return None mid-sequence; the next
composed step dereferenced None.id and crashed with the opaque
"'NoneType' object has no attribute 'id'", looping the PM. Now fails fast
with an actionable INVALID_STATE; the savepoint rolls the partial run back.
- blocker dispatch: never dispatch a Board role (product-owner / head-
marketing) as a blocker resolver. Board roles have no unblock verb, so the
dispatcher respawned one forever to "resolve" a blocker it could only
notify/triage about — one incident burned ~6400 tool calls on a single
mis-owned root. _blocker_resolver_slug now returns None for a Board
assignee so the dispatch skips it.
- git push: recover a missing local task-branch ref from origin/<branch>
before push-by-name. A re-provisioned shared clone can lack the branch
locally though its commits are on origin, so push died on
"src refspec <branch> does not match any" and the task wedged at i_am_done.
Now materializes the ref (no-op push when already on origin) or fails loud
with an unclaim+reclaim instruction when the work is on neither.
Adds regression tests for all three. Full no-DB gate green (ruff, reflow,
mypy, xenon); pytest+coverage validated by CI.
* fix(verb-runner): only raise on an INTERMEDIATE composed None, not the last
The mid-composition None-guard was too aggressive: it raised for a None
returned by the LAST composed action too (e.g. start()), preempting the
caller's existing `if task is None` handler that surfaces the verb-specific
message ("start failed for task ...", the board verb's decline envelope).
Three tests asserting those messages broke in CI.
Only an INTERMEDIATE None is fatal (the next action would deref None.id). A
None from the last action is the verb's own result and must flow out as the
runner's return value. Guard now fires only for position > 0, before the
next dispatch — still prevents the crash, preserves the last-action contract.
* fix(escalation): never hand a Main-PM coordination root to the Board
The upstream cause of the board catch-22 (which the orchestrator-side
blocker-dispatch guard only backstopped): the escalation chain points
main-pm -> product-owner, and i_am_blocked/escalate REASSIGNS the task to
that chain target. apply_escalation's board-advisory guard only refused
descendant cell tasks (both predicates require parent_task_id), so a
top-level Main-PM coordination root slipped through and the whole root was
reassigned to the Product Owner + marked blocked. The board has no unblock
verb, so it spam-notified the CEO and respawn-looped (~6400 tool calls on
one root).
Add _is_coordination_task (team == main_pm — covers a delivery root AND a
MegaTask root-subtask) and a shared _board_cannot_own predicate, applied at
all four board-refusal sites (escalation, reassign, reassign_active_claim,
dependency-revival). A main_pm coordination task escalated/reassigned onto a
board role is now diverted to the pool for a role-matched (Main-PM) reclaim.
Complements the blocker-dispatch backstop in the prior commits (defense in
depth). Tests: coordination-root predicate cases + apply_escalation divert;
existing teamless-root / board-root behavior unchanged.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
855e4aea54 |
fix(gateway): guard the verb runner against a None task/agent
The runner's atomic steps dereference task.id / agent.id with no None-check, so a verb invoked when the task or agent could not be resolved crashed with a cryptic "'NoneType' object has no attribute 'id'" (observed on i_will_plan for a task forced into an unexpected state out-of-band). Fail fast at run_intent's entry with an actionable INVALID_STATE error instead. |
||
|
|
889f3689e7 |
MegaTask (#248)
* feat(batch): batch_id + collision descriptor columns
Sequenced batch intake ("Mega task") foundation: tasks.batch_id (indexed)
groups a batch of top-level tasks created together; intends_to_touch (text[]),
adds_migration and touches_shared (bool, NOT NULL default false) are the
per-task collision surface the SequencingService will read to wire dependency
waves. Mirrored on the Task model + TaskCreateRequest and wired through
TaskService.create. Migration 046 (real upgrade->downgrade->upgrade verified
vs a throwaway pgvector PG); a non-batch task declares no surface (defaults).
Task 1 of the 0.11.0 sequenced-batch-intake plan.
* feat(batch): flag + draft collision descriptors
Default-off ROBOCO_BATCH_INTAKE_ENABLED (config + FEATURE_FLAGS + panel card);
the propose_draft tool doc + the TS DraftProposal gain the per-task collision
surface intends_to_touch / adds_migration / touches_shared. The draft is a loose
dict so the descriptors ride it through the relay intact (test asserts the
forwarded payload); the analyzer (Task 3) reads them to wire dependency waves.
Task 2 of the 0.11.0 sequenced-batch-intake plan.
* feat(batch): deterministic collision-sequencing analyzer
SequencingService.analyze turns a batch's per-task collision surfaces into a
dependency DAG + execution waves — correctness in CODE, not agent judgment.
Rules in order: file overlap serializes (more-important first), migrations form
a serial chain (no concurrent Alembic heads), touches_shared runs last, cell
contention warns (never serializes); then dedupe, existence + cycle check, and
Kahn topological layering. Pure (no DB/services); SequencingError on a cycle or
out-of-range edge.
Golden test reproduces the CEO's hand-sequenced 4 waves of the 11-item
guard-core-app batch (the effort that deadlocked the Main PM): S6 alone last,
the R1/R3/R4 migration chain, R2/R3/S8 serialized on the shared threat service,
S1/S2/S7 in one parallel wave.
Task 3 of the 0.11.0 sequenced-batch-intake plan.
* chore(batch): brand the user-facing surfaces "MegaTask"
The user-facing name is MegaTask: the feature-flag label is "MegaTask intake",
the panel flag-card and the config description lead with MegaTask. Internal
names stay technical (batch_intake_enabled, batch_id, SequencingService).
* chore(batch): drop the feature flag — MegaTask is a core intake scope
MegaTask is additive and opt-in by its own nature (the Prompter proposes a
batch only when the CEO asks for several tasks; single-task intake is
unchanged), so there is no risk surface a flag protects — 'don't create a
MegaTask' is the off switch. Remove batch_intake_enabled from config, the
FEATURE_FLAGS registry, the panel flag card, and its tests. MegaTask will be
a third scope option in the Intake modal (single-cell / multi-project /
MegaTask), not a toggle.
* feat(batch): MegaTask identity predicate + orchestrator branchless recognition
The single source of truth for the umbrella's exemptions: pure
is_batch_umbrella / is_batch_root_subtask / is_branchless_coordination
(foundation/policy/batch.py) — an umbrella has a batch_id and is top-level; a
root-subtask shares the batch_id but is parented. The orchestrator's
_is_coordination_task now consults is_branchless_coordination, so a MegaTask
umbrella is recognized as doing no git of its own (git-exempt at spawn-readiness
/ stuck-detection) exactly like a product fan-out root. Non-batch behavior is
identical (the predicate reduces to the old no-project+product check; the
orchestrator coordination suite stays green), and the umbrella branch is inert
until the create path exists.
First slice of the MegaTask umbrella enforcement (branchless guard).
* feat(batch): branchless umbrella guard across the git-exemption sites
A MegaTask umbrella does no git of its own — every git-exemption site in
TaskService now consults the shared is_branchless_coordination predicate
instead of an inline product-only check, so the umbrella's exemptions
cannot drift between sites:
- the claimed->in_progress branch gate (GitContext.is_coordination) lets
an unbranched umbrella reach in_progress and delegate;
- _ensure_branch_for_task short-circuits an umbrella to "" instead of the
misconfigured raise (the claim path ignores the return, treating it as
branchless);
- CEO-reject routing sends a rejected umbrella to the Main PM in PENDING
(needs_revision is developer-claim-only and would deadlock it).
Covers both shapes via the predicate (product fan-out root OR umbrella);
a batch root-subtask keeps its own branch/PR. Adds orchestrator
recognition tests for the umbrella plus claim/branch/reject integration
tests.
* feat(batch): umbrella assembles no PR; completes branchless
submit_root now hard-rejects a MegaTask umbrella up front (a preflight
that also folds in the unknown-role refusal to stay within the
return-count budget): the umbrella spans many projects with no single
master, so each root-subtask opens and is reviewed on its own PR — the
umbrella never enters the in-path review gate. The Main PM completes it
directly once every root-subtask is terminal.
Umbrella completion needs no new code: it is branchless (no branch_name),
so _main_pm_complete_guard already accepts it from in_progress, checks
all_subtasks_terminal, and main_pm_complete walks it to awaiting_pm_review
and escalates to the CEO with no PR creation — exactly the product
fan-out root path. Adds the submit_root-reject and umbrella-completion
gateway tests; pins batch_id=None on the normal-root submit_root test
(a MagicMock auto-attr would otherwise read as an umbrella).
* feat(batch): MegaTask create path — umbrella + sequenced root-subtasks
PrompterService.confirm_live_batch turns N confirmed drafts into a real
MegaTask: it builds each draft's collision surface, runs the pure
SequencingService to get conflict-free waves, creates the branchless
umbrella (batch_id, no project/product), then one root-subtask per draft
(own project, parent=umbrella, sequence=wave index, descriptors), and
wires the analyzer's edges through add_dependency so the existing
dependency-gate runs the waves in order. The route picks the start path
like a single confirm: 'board' holds the root-subtasks in BACKLOG for the
batch review; 'main_pm' creates them PENDING so wave 0 dispatches at once.
create_task_from_draft gains a BatchPlacement (parent/batch/sequence/
team_override) and forwards the collision descriptors; the exactly-one-
target rule (here and the TaskService.create invariant) is relaxed for an
umbrella, which legitimately targets neither. New route
POST /live/{session}/confirm-batch + BatchConfirmRequest mirror the single
confirm. Adds the structural-invariant + board-hold + empty-batch tests.
* feat(batch): release MegaTask root-subtasks on CEO approval; board awareness
The board route holds a MegaTask's root-subtasks in BACKLOG so the work
waits for the batch review. approve_and_start (CEO gate #1, board->Main PM)
now releases them via _activate_batch_root_subtasks: each held child flips
BACKLOG -> PENDING + team=main_pm so the dependency-gate dispatches wave 0.
No-op for a non-umbrella; idempotent (children past BACKLOG untouched).
The Product Owner and Head of Marketing identity prompts gain a MegaTask
section so they review the whole batch + wave plan and adjust scope before
sign-off (they review drafts; the umbrella is their unit). Also extracts
the create() target invariant into _require_target_or_umbrella to keep the
method under the complexity gate after the umbrella exemption. Adds the
umbrella-approval activation test.
* feat(batch): multi-project intake scope for MegaTask
A MegaTask spans several possibly-unrelated repos, so the intake chat can
now be scoped to an explicit project list (not just one project or one
product). StartLiveRequest gains project_ids; /live/start threads it
through start/spawn_intake_session -> _spawn_intake_container ->
_clone_intake_scope. The multi-repo clone machinery already existed for
products; _intake_scope_slugs now also resolves an explicit project_ids
set (split into _slugs_for_project_ids / _slugs_for_product), cloning each
repo with the first as the primary cwd and the siblings readable. Scope
validation is now 'exactly one of project_slug / product_id / project_ids'
via the shared _require_one_intake_scope. Adds scope-resolution, spawn,
and route tests for the MegaTask path.
* feat(batch): propose_batch intake tool (MegaTask multi-draft hand-off)
The intake agent can now hand the panel a whole MegaTask in one tool call.
Both intake paths gain propose_batch alongside propose_draft:
- Claude (intake_driver): a propose_batch tool registered on the in-SDK
MCP server + allowlisted; the driver intercepts the ToolUseBlock and
emits ONE StreamChunk(kind="batch") carrying {drafts:[...], title}.
- grok (intake_server): a propose_batch tool that POSTs a "batch" relay
event via the shared _post_event helper (post_draft/post_batch).
A batch carries N drafts, each the propose_draft shape PLUS its own
project_id (a MegaTask spans unrelated repos) and collision surface so the
analyzer sequences the waves. The prompter prompt documents the MegaTask
scope + when to call propose_batch. Adds Claude-normalize and grok-relay
tests for the batch path.
* feat(batch): MegaTask intake panel — third scope, batch review, waves
The panel now drives a MegaTask end to end. The intake modal gains a
third scope, 'MegaTask', beside Single cell and Board-led: a multi-project
checklist (a MegaTask spans several possibly-unrelated repos), validated
to at least two. start() sends project_ids; use-prompter accumulates the
agent's single propose_batch hand-off as a 'batch' SSE event into a
BatchProposal and lands in a new batch_preview state.
A new BatchReviewCard lists every proposed task with its target project +
collision-surface badges (migration / shared) and offers one start path
for the whole batch — Board review & Start or Approve & Start — wired to
confirmBatch → POST /confirm-batch. The success card shows the sequenced
result: N tasks in M waves (+ any advisory notes). prompter.ts gains the
DraftScale 'megatask' + the BatchConfirm payload/result types; the SSE
client allows the 'batch' kind. Panel typecheck + lint + 113 tests green.
* docs(batch): MegaTask across changelog, CLAUDE.md, site, and RAG
The four documentation obligations for the MegaTask feature:
- CHANGELOG: an Unreleased entry covering the umbrella model, sequencing,
multi-project intake, propose_batch, and the create/approval path.
- CLAUDE.md: a MegaTask section (identity predicate, umbrella/root-subtask
hierarchy, sequencing rules, intake + create path, board activation).
- Published site: a user-facing company/megatask.md (scopes, waves, the
umbrella, the two start buttons) + nav entry; a pointer added to the
intake chapter of the Tour.
- RAG corpus: workflows/megatask.md so the Main PM (and any agent) can
retrieve the umbrella's branchless / no-PR / completion rules at runtime.
The runtime concurrent-migration guard is intentionally NOT added: the
analyzer already chains migration-adders into dependencies and the
dependency-gate serializes them, so a separate guard would be dead code.
* feat(batch): batch_id guardrail + wave preview + batch_id on TaskResponse
Guardrail (CEO): a batch_id is denied on any task that is not a well-formed
MegaTask member. is_valid_batch_shape permits batch_id only on an umbrella
(no parent → must target neither project nor product) or a root-subtask
(has a parent → exactly one target); TaskService.create enforces it AND
verifies a root-subtask's parent is the batch umbrella (same batch_id,
top-level). This closes a latent hole: is_batch_umbrella is true for a
batch_id + no-parent task even with a project, so a stray batch_id could
have spoofed the branchless branch-gate / no-PR exemption. (The public
task API never exposed batch_id for write; this guards the service layer.)
Wave preview: PrompterService.preview_batch + POST .../preview-batch
compute a MegaTask's waves from the proposed drafts WITHOUT creating
anything, so the panel can show the sequencing before confirm. Extracted
_sequence_drafts as the single source shared by preview and confirm, so
the previewed waves are exactly the ones wired.
TaskResponse now carries batch_id so the panel can badge the umbrella.
* feat(batch): MegaTask review — project editor, wave preview, persistence, badge
Closes the panel gaps in the MegaTask review experience:
- Per-task project editor: each proposed task gets an inline project
Select (updateBatchDraftProject), so a task the agent put in the wrong
or no repo can be fixed before launch — not only by re-chatting. Launch
stays blocked until every task has a project.
- Wave preview: on a batch proposal the panel fetches POST .../preview-batch
(no task created) and shows the conflict-free wave plan, so the human
reviews the sequencing before confirming.
- Refresh durability: the MegaTask review (batch + waves + projectIds) is
persisted, so a browser reload mid-review restores it like a single draft.
- MegaTask badge: TaskResponse exposes batch_id, the panel Task type
carries it, and the task table badges the umbrella row 'MegaTask'.
Panel typecheck + lint + 113 tests green.
* test(batch): stub task carries batch_id for task_to_response
task_to_response now serializes batch_id (TaskResponse field), so the
_stub_task SimpleNamespace fixture must provide it — without it the reader
hit AttributeError, failing the 8 task-schema serialization/enrichment
tests. Test-only; the real TaskTable carries the column (migration 046).
* fix(batch): close MegaTask audit gaps — completion crash, analyzer cycle, guardrails
An adversarial multi-agent audit of the feature surfaced 20 verified gaps;
this closes the backend ones.
HIGH:
- Umbrella completion crashed. escalate_to_ceo hard-required a pr_number,
which a branchless umbrella never has, so main_pm_complete dereferenced
None. Both pr_number gates now waive a MegaTask umbrella (escalate_to_ceo
+ the awaiting_pm_review->awaiting_ceo_approval lifecycle gate via a new
GitContext.is_umbrella), and main_pm_complete guards a None return. The
completion test had mocked escalate_to_ceo, hiding it — now a real
service test covers the waiver.
- The collision analyzer could fabricate a cycle (a touches_shared +
adds_migration draft overlapping another migration draft) and raise
SequencingError — a bare ValueError that escaped as an opaque 500. The
migration chain is now shared-last-aware (never contradicts rule 3), and
_sequence_drafts translates SequencingError to a clean 400.
MEDIUM:
- Collisions are now project-scoped: two repos can't collide on a
coincidental path or serialize independent migrations (DraftSurface
carries project_id; rules 1/2/3 respect it).
- The batch_id guardrail ran only at create. update() + the PATCH
null-clear path now re-assert is_valid_batch_shape, so a mutation can't
break a member's shape and spoof the branchless exemption.
- A draft missing title/acceptance_criteria now raises ValidationError
(was a bare KeyError -> 500).
- confirm_live_batch re-asserts every draft targets a scoped project and
the batch spans >=2 distinct projects (project_ids added to the request).
- Route-level tests for confirm-batch / preview-batch.
LOW: strict multi-repo clone (fail loud on any unresolvable project);
malformed/empty propose_batch surfaces an error chunk (Claude) / refuses
to POST (grok) instead of silently acking; dropped malformed drafts are
counted and surfaced; stale grok intake docstrings updated.
* fix(batch): MegaTask panel + doc audit gaps
Frontend half of the audit fixes:
- The confirm payload now carries project_ids (the schema requires it), and
the panel re-checks every task targets one of the scoped repos before
launching, naming the offending task.
- The Review-MegaTask project picker is filtered to the scoped repos and
the per-task validity (border + launch gate) keys off scoped membership,
so a task can only be (re)pointed at an in-scope project — also fixing the
case where the agent emitted a non-UUID / unknown project.
- Dropped malformed drafts are surfaced as a chat error so the human knows
the batch shrank instead of silently confirming fewer tasks.
- Doc wording: a wave releases on the previous wave's terminal state
(normally a merge; a cancellation releases it too), not strictly 'merged'.
* test(batch): lock the CEO's EXACT 4-wave hand-sequencing as the golden bar
The golden test asserted the constraints (S6 last, the migration chain, the
shared-threats serialization, S1/S2/S7 parallel) but not the full wave
partition. The bar for MegaTask is 'reproduce my exact waves or it's not
done', so assert the exact 4-wave partition the analyzer produces for the
guard-core-app batch:
wave 1: R1 R2 S1 S2 S3 S5 S7 · wave 2: R3 · wave 3: R4 S8 · wave 4: S6
Confirmed unchanged by the audit's analyzer fixes (no migration is shared;
single project).
* fix(batch): tolerate a stub task in assert_batch_shape_intact
The batch-shape re-validation read task.batch_id directly, but update()'s
partial-caller contract is exercised with a SimpleNamespace stub that has no
batch_id column → AttributeError. Use getattr(..., None) for batch_id and the
shape fields so the guard no-ops on any task lacking the column (a stub, or a
non-batch task) while still enforcing on a real batch member.
* fix(orchestrator): authenticate internal API self-calls with the system identity
The dispatcher httpx clients were built without an agent identity, so the
orchestrator's self-PATCHes to /api/tasks/{id} (auto-block, auto-resume,
auto-recover, SLA annotation) were rejected 401 "Missing X-Agent-ID" and
silently no-op'd. The auto-resume that lifts a PM's paused parent could never
write, so paused/blocked parents stayed wedged and stranded their dependents
(the fe-pm/be-pm respawn churn seen in prod).
Header propagation was inconsistent across the separate AsyncClient call-sites:
only the main dispatch client carried the system identity; the readiness and
sweep clients did not. Hoist the identity into a shared _SYSTEM_API_HEADERS
constant and apply it to every API-facing dispatcher client. The system role
holds TaskAction.ASSIGN, so it is authorized for the audited admin_set_status
path those write routes use. The external provider-recovery probe client is
intentionally left untouched.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
c09cf80b40 |
Feature/observability gateway health (#247)
* feat(observability): revision_count + audit_log query index (migration 045)
Adds tasks.revision_count (the O(1) rework counter — forward-only, existing
rows default 0) and the composite index audit_log(target_id, event_type,
timestamp) that powers the cycle-time and rework reconstruction queries.
Verified the real upgrade/downgrade/upgrade chain on a throwaway pgvector PG.
First task of the 0.10.0 observability dashboards.
* feat(observability): count reworks + attribute qa_fail/pr_fail to the rejector
Every transition into needs_revision increments tasks.revision_count at the
single audit chokepoint (exactly once per bounce, across all paths incl. pr_fail
and ceo_reject), so the rework rate is an O(1) read. A QA or PR-review bounce
also emits a named task.qa_fail / task.pr_fail audit event carrying the
rejector's agent_id, so the per-agent rework scorecard charges the rejection to
the reviewer who made it, not the developer who owns the task.
* feat(observability): cycle-time, bottleneck, rework, and scorecard metrics
MetricsService gains four read methods on the audit_log + tasks data: per-stage
cycle time reconstructed from the transition journey (excluding the named
qa_fail/pr_fail events), bottleneck distribution (cumulative dwell + live parked
counts), rework rate (overall/by-team/by-agent with rejector attribution + cost
via spawn-session task_id), and a fused per-agent/per-cell scorecard. Dataclass
models with to_dict(). Verified against a real Postgres journey.
* feat(observability): cycle-time/bottleneck/rework/scorecard read endpoints
Thin read-only routes on the dashboard router delegating to MetricsService:
/metrics/cycle-time, /metrics/bottlenecks, /metrics/rework, and
/metrics/scorecard/{agent,team}. 404 when an agent scorecard target is absent.
5 route tests (200 + shape + the agent-404 case).
* feat(panel): Delivery observability tab (cycle-time, bottlenecks, rework, scorecards)
A third Metrics tab built on the observability endpoints: a per-stage
cycle-time bar chart, a bottleneck panel (worst stage + cumulative dwell +
live parked counts), a rework panel (rate + by-team + by-agent attribution +
cost), and per-cell scorecards. Reuses Recharts + Card/Badge/Skeleton and the
React-Query hook pattern; observabilityApi mirrors usageApi with mock-mode
fallbacks. tsc + eslint clean; 113 panel tests pass.
* docs(observability): changelog + CLAUDE.md for the delivery dashboards
* feat(gateway-health): recover a broken-but-alive agent instead of protecting it
The verb-heartbeat cannot tell a quiet-healthy agent from one whose MCP gateway
is broken (a corrupted /app/.venv firing no verb) yet whose container is up — the
reaper's live-skip would shield it forever. The reaper now probes the gateway
out-of-band (docker exec: does the gateway venv import its deps?) and, once it
has been broken past gateway_health_grace_seconds (tolerating a transient probe
miss), kills + evicts the container so it falls through to release + respawn.
Probe-inconclusive or healthy spares the container. Gated by
gateway_health_enabled (default-on reliability fix; in the panel Feature Flags).
Defers the optional agent-side self-check + full registry re-adoption — the
reaper's docker-liveness fallback already recovers a broken-after-restart agent.
* docs(gateway-health): changelog + CLAUDE.md for broken-but-alive recovery
* docs(observability): user-facing docs for the Delivery dashboards + gateway-health
Documents the new Metrics -> Delivery tab (cycle-time, bottlenecks, rework with
rejector attribution, cell scorecards) in the panel guide and the operations
health-and-metrics guide, and adds the gateway-health env vars + an agent-gateway
recovery note. Published MkDocs site only; settings.md's default-off flag table
intentionally omits the default-on gateway-health flag (same as overload-break).
* chore(release): cut 0.10.0 (changelog section + version refs)
* fix(gateway): exempt PM coordinators from single-task claim guards
A Main/Cell PM plans and delegates many root tasks in parallel; the work
then runs in the delegated cells, not in the PM's own hands. But the
claim-time concurrency guards meant for developers — already_active and
paused (the latter firing after i_am_idle auto-pauses the PM's own
umbrella) — were applied to the PM too, so once it held one root it could
never plan a second: it thrashed between its claimed roots and respawned
forever, burning tokens for zero progress.
_run_claim_guards now skips already_active/paused for the coordinator PM
roles (_COORDINATOR_ROLES = {main_pm, cell_pm}); only unmet_dependency — a
real upstream sequence constraint, which parks the root back to pending —
still gates a PM. paused_tasks_guard also excludes the target task itself,
so a PM re-entering its own paused umbrella never self-blocks.
Tests: a coordinator plans a second root with one in_progress + one paused
sibling (full path + claimed-recovery path), the paused target exclusion,
and the developer guards still fire. Repurposed the pre-fix test that
asserted the now-removed PM block.
* fix(metrics): coerce SQL avg/extract hours aggregates to float (panel toFixed crash)
EXTRACT(epoch ...) returns numeric on PostgreSQL 14+, which asyncpg surfaces
as a Decimal; a Decimal serializes to a quoted JSON string, so the panel's
avg_cycle_hours.toFixed(1) (and the other hours fields) threw 'toFixed is not
a function' and blanked the Delivery tab.
A single _as_hours helper now rounds every SQL-averaged hours field to a real
float — avg_cycle_hours on the new scorecards plus the pre-existing
avg_completion_hours / avg_blocked_hours / longest_blocked_hours. Token and
cost fields were already float()-cast and are unaffected.
Regression test asserts _as_hours coerces Decimal -> float and preserves the
None/zero behavior.
* feat(panel): edit a task's sequence from the details page
A task's sequence (order within siblings, lower runs first) was display-only
with no way to change it from the UI, and TaskUpdate didn't carry the field
so PATCH couldn't set it either. The details page's Dependencies tab now has
an inline sequence editor mirroring the parent / dependency editors, and
PATCH /tasks/{id} accepts a sequence field (owner or privileged role) through
the existing generic update path.
* fix(mypy): green the full make-quality type gate
make quality runs 'mypy roboco/ tests/', which the per-module checks on the
0.10.0 branch never exercised. Two issues surfaced:
- The coordinator-exemption change added role_str to
Choreographer._run_claim_guards but not to the ChoreographerHelpers
protocol base, so the composed Choreographer had incompatible base-class
signatures. Sync the protocol signature.
- The gateway-health / stale-reaper tests stubbed methods by direct
assignment (orch._m = AsyncMock()) and typed their duck-typed task doubles
as object, tripping method-assign / assignment / attr-defined. Switch to
monkeypatch.setattr (keeping a local mock ref for the assertions) and type
the doubles as Any — no type: ignore.
Full mypy roboco/ tests/ clean (785 files); the 21 runtime tests pass.
* fix(metrics): static cycle-time SQL — clear bandit B608 (CI gate)
The cycle-time query interpolated an optional team clause into the text() SQL
via an f-string, which bandit flags as B608 (hardcoded SQL) and turned the
merge gate red. The team value was always a bound parameter, so it was a false
positive — but the f-string is the trigger. Rebuilt as one static query with
(CAST(:team AS text) IS NULL OR a.details->>'team' = :team) and an always-bound
team param (CAST, not ::text — SQLAlchemy's :param parser collides with
PostgreSQL's :: cast operator, which broke the query as a stray param).
Full make quality green vs a real pgvector PG (all 21 gate steps).
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
ebc3b1ea18 |
fix(docs): push the documenter's doc commit so it lands in the PR
The documenter writes and commits docs onto the task branch in its own workspace clone, but i_documented had no push step — so the commit stayed local and the PM merged the already-open PR without the docs, which then vanished on merge. i_documented now pushes the task branch before handoff, mirroring the developer's _ensure_branch_pushed; a push failure holds the task in awaiting_documentation for a retry instead of silently dropping the docs. Extract _finalize_documented to keep the verb under the return-count ceiling. |
||
|
|
0a890c401e |
fix(gateway): surface an 'unknown' toolchain status at the gate instead of passing silently
The toolchain guard fails open on a recorded 'unknown' status (precision over recall — never strand a task on an inconclusive smoke). But an 'unknown' means provisioning ran yet the smoke could not confirm the suite is collectable under the interpreter, so the gate was proceeding blind with no trace — a silent hollow pass. It now emits a 'toolchain.unverified_gate_pass' warning with the agent and task ids when proceeding past 'unknown', while still not blocking; a missing marker (None) stays silent so the warning carries signal. |
||
|
|
16789c1ca7 |
Feature/architectural conventions standard (#243)
* feat(conventions): standard schema models + effective-map merge * feat(conventions): tree-sitter Python classifier + placement checks * feat(conventions): TS classifier, hygiene/custom checks, runner + CLI * feat(conventions): ROBOCO_CONVENTIONS_ENABLED flag + cache table + migration * feat(conventions): repo auto-scan + scaffold draft renderer * feat(conventions): ConventionsService (cache/baseline/ambient/scaffold/restore) * feat(conventions): auto-scaffold on project registration (flag-gated) * feat(conventions): TaskDescription.constraints + auto-baseline attach * feat(conventions): ambient architecture-map injection at spawn * test(conventions): subprocess CLI smoke for the agent-image entrypoint * feat(conventions): block i_am_done on block-level convention violations * feat(conventions): block pr_pass on unresolved convention violations * feat(conventions): surface convention findings into QA evidence * docs(prompts): convention awareness for PO/Intake/Dev/QA/PR-reviewer * feat(conventions): panel Conventions tab + flag toggle + parity * test(conventions): end-to-end block, fix, and waiver through the gate * refactor(conventions): extract pr_pass guards to keep pr_gate under the gate * style(conventions): format the baseline-constraints attach in task.create * test(conventions): type-annotate test helpers for the full mypy gate * build(conventions): ignore types-PyYAML in deptry (mypy-only type stub) * docs(conventions): document the standard in CLAUDE.md + PM prompt awareness * fix(conventions): baseline constraints are non-suppressible (dedup-append) * feat(conventions): scaffold on first workspace clone (threaded workspace) * feat(conventions): multi-project ambient map for PO/Intake (per-product) * feat(conventions): persist findings + violations-feed route (migration 044) * feat(conventions): panel violations feed in the Conventions tab * test(conventions): intake-spawn mock accepts the ambient layer kwarg * fix(docker): ollama-init best-effort pull, gate startup on cached models present A degraded/slow ollama registry made the model manifest re-check fail under set -e, so ollama-init exited 1 and blocked the orchestrator's service_completed_successfully gate — taking the whole stack down even though both models were already cached. Pulls are now best-effort; success is gated on the models being present, so a flaky registry can't down a cached deployment. * refactor(content): drop dead TaskDescription.with_baseline_constraints The structured baseline-merge helper had zero production callers. Project-task baseline constraints are attached by the wired string backstop (TaskService._attach_baseline_constraints), and a real task is free-form prose that cannot form a valid TaskDescription (requires a non-trivial objective + non-empty the_work), so the helper was unreachable from any live path — a leftover from the structured-merge -> string-append design pivot. Removing it leaves a single enforcement path. The constraints field itself stays: it is a member of the well-formed-spec schema (Objective / What This Builds / The Work / Notes / Constraints / Acceptance Criteria), rendered by render_markdown and unit-tested. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
7d50405ccb |
fix(toolchain): type-annotate test helpers for the full mypy gate
make quality runs mypy over tests/ too; the toolchain test fixtures (monkeypatch params, the fake-subprocess factories) were missing annotations. No behavior change. |
||
|
|
0ea9f48187 |
feat(toolchain): block QA/dev/PR gates when the suite cannot be executed
Adds _toolchain_broken_guard: when toolchain matching is on and the acting agent's workspace recorded a 'broken' status (the project's suite cannot be collected under the provisioned interpreter), i_am_done, pass_review, and pr_pass refuse with an i_am_blocked remediation instead of letting the role "pass" on a source read. fail_review / pr_fail stay available. git gains toolchain_status_for_task (resolves the actor's workspace, reads the marker, fail-open None). Inert when the flag is off; a missing/unknown status never strands a task. |
||
|
|
8cf697816f |
feat(content): obligate role note sections like journals
Completes the note(scope='handoff') write-path (WIP
|
||
|
|
3a67fa4a0f |
fix(content): anti-soup guard on pitch slug + restore pitch test intent
Sweep found pitch slug was the one content-tool free-text field still outside the anti-soup guard (a product could be slugged 'wip'/'asdf'). Add it to the guard (min 2, so real short slugs like 'ui' pass). Also fix two pitch tests that were false-passing on a 1-char title (the title guard rejected first): give them substantive fields so they actually exercise the role gate (not_authorized) and the non-cell-target rejection (invalid_state). |
||
|
|
8463808616 |
fix(lifecycle): let a PM recover its rejected coordination task from needs_revision
The in-path PR-review gate created a deadlock: when an assembled cell→root / root→master PR fails the gate (pr_fail) — or qa_fail / ceo_reject fires — the PM-owned coordination task lands in needs_revision, which was developer-claim- only. So the task had no actor and no exit but cancel, and the cell PM escalated in a loop (8KB of [ESCALATED] dev_notes on one task). Pre-gate, the PM simply re-delegated from in_progress; the gate routed the failure through the PM's own task instead. Add NEEDS_REVISION to the CELL_PM / MAIN_PM claim rules so the PM re-claims via i_will_plan, revises the plan, and re-delegates the fixes — pr_fail/qa_fail already reassign the failed task to its owning PM and the revision dispatcher re-spawns it; the claim rule was the only missing piece. Scope is by give_me_work routing (offers only the caller's own assigned tasks), the same mechanism that scopes a developer's leaf-revision — NOT a gateway-only ownership gate, which would violate the spec=gateway parity invariant and can't use task_type anyway (main-PM coordination roots can be code-typed). Regenerates panel/lib/lifecycle.json. |
||
|
|
4c85cc6dfa |
feat(content): anti-soup guard on the flow verbs' free-text
Extends structured-content enforcement from the content tools to every flow verb that carries agent free-text, closing the last hole where a dev/PM could pass word soup: i_am_blocked(reason), i_am_done(notes), submit_up/submit_root/complete(notes), escalate_up/escalate_to_ceo(reason), pass_review(notes), fail_review/pr_fail(issues), pr_pass(notes), i_documented(notes), delegate(title/description). Plans (i_will_plan / i_will_work_on) keep their existing >=150-char approach + sub_task gates and are skipped here so recovery re-entry with thin values still works. Shared helpers on the choreographer: _free_text_soup (bare envelope, list aware) and _soup_or_decision_env (folds the soup check into a verb's existing spec-gate return so no verb gains a return or tips the xenon bound). reject_trivial now also catches all-filler multi-token strings. base.md documents the broadened rule for agents. |
||
|
|
96ca53eff2 |
feat(content): extend anti-soup guard to pitch title, open_session topic, pr_update, note narratives
The universal guard covered say/dm/note-text/progress/notify/pitch problem+solution. Close the remaining content-tool fields the agent authors: pitch title, open_session topic, pr_update title/body (when supplied), and the decision/reflect narrative sub-fields of note (rationale/context/what_done/...). Narrative fields are only checked when the agent fills them — an omitted field keeps its tolerant '(not provided)' placeholder so a thin note still records and never trips the do-server circuit breaker. Fold pr_update's no-fields + soup checks into one helper to stay under the return-count bound. |
||
|
|
db74f546a3 | feat(content): universal anti-soup guard on every agent free-text field | ||
|
|
11c1d9eee7 | feat(content): structured PR-review findings + generated GitHub comment | ||
|
|
5fe1e6df58 |
feat: in-path PR-review gate — per-cell + main reviewers (#229)
* feat(lifecycle): add the in-path PR-review gate status + reviewer verbs
Insert awaiting_pr_review between the assembled-PR submit and the PM merge,
giving the merge level the rejection capability it structurally lacks — today
only qa_fail and ceo_reject ever reach needs_revision, so a PM review is a
merge button with no teeth.
- New Status awaiting_pr_review + submit_for_review / pr_pass / pr_fail actions
(pr_pass -> awaiting_pm_review, pr_fail -> needs_revision, mirroring the QA gate).
- Reviewer verbs claim_gate_review / pr_pass / pr_fail, and a main-PM submit_root
verb (the root analogue of the cell PM's submit_up; opens the root->master PR).
- Extend the self-review-symmetry validator to the new sign-off actions.
- Mirror the value into the ORM TaskStatus enum + the A2A state map, and add the
postgres taskstatus enum value (migration 040, forward-only like 037).
- Regenerate the per-role verb tables; add gate spec tests.
Spec surface only; the gateway methods + dispatch are wired in follow-ups, so the
verbs are advertised but dormant (flow_server tolerates unregistered verbs).
* feat(identity): add the three cell PR-review-gate reviewers
The in-path gate needs a reviewer per cell so each cell's assembled cell->root
PR is reviewed by a stack-specialized agent, while pr-reviewer-1 serves the
root->master gate (and keeps doing inbound external PRs).
- be/fe/ux-pr-reviewer: PR_REVIEWER role, team-scoped (so dispatch routes each
cell's gate to its own reviewer); seeded identities + ROLE_TEAM_RULES + names.
AI agent count 22 -> 25.
- They reuse the existing roboco-agent-pr-reviewer image (AGENT_IMAGES maps the
three slugs to it, as be-dev-1/-2 share one image) — no new image.
- Tracing table: pr_pass/pr_fail require a learning entry (parity with
post_pr_review), submit_root mirrors submit_up, claim_gate_review is waived
(its tracing applies on pr_pass/pr_fail) — completes the verb surface added
in the prior commit.
- Update the roster-pinning identity tests.
* feat(gateway): wire the in-path PR-review gate end to end
Make the assembled-PR review gate operational across the choreographer, the
TaskService transitions, and the v1 flow surface.
- TaskService: submit_for_review (in_progress→awaiting_pr_review), pr_gate_claim
(no-transition reviewer claim), pr_pass (→awaiting_pm_review), pr_fail
(→needs_revision); mirror qa_pass/qa_fail (clear claim, actor-mismatch warn,
issues appended for the PM's revision). VerbRunner gains the matching atomic
handlers + a create_root_pr side effect.
- Repoint submit_up to compose submit_for_review (cell→root PR enters the gate),
and add a main-PM submit_root verb (opens the root→master PR, enters the gate).
- Split main_pm_complete: a code root must pass the gate first (requires
awaiting_pm_review; rejects an in_progress code root toward submit_root and no
longer reopens the PR), while a branchless coordination root still walks
straight through, ungated.
- PRGateMixin (claim_gate_review / pr_pass / pr_fail) composed onto the
Choreographer; flow_server forwarders + v1 routes (pr_reviewer + main_pm) +
request schemas.
- Tests: gate spec + the updated submit_up / main_pm_complete expectations + new
real-DB integration tests driving submit_for_review→pr_gate_claim→pr_pass and
pr_fail through the real enforcement layer.
* feat(orchestrator): dispatch the in-path PR-review gate
Make the gate live in the dispatch loop.
- _dispatch_pr_gate_work: route awaiting_pr_review tasks to reviewers by level —
a cell→root task to its cell reviewer (be/fe/ux-pr-reviewer), the root→master
task to pr-reviewer-1. The reviewer self-claims via claim_gate_review (no
pre-claim, mirroring the external-PR dispatcher); registered in
_dispatch_all_work. _select_agent_for_cell learns the pr_reviewer role.
- _build_pr_gate_prompt: anchors the reviewer to the parent objective + full
acceptance criteria + the FE<->BE contract, then pr_pass / pr_fail.
- _readiness_check_role_for_status: awaiting_pr_review -> pr_reviewer.
- Fail routing: pr_fail reassigns the failed assembled task to its PM
(_revision_pm_for_task: cell PM for a cell team, Main PM for the root), and the
revision dispatcher is generalized from coordination-roots-only to any
PM-owned needs_revision task so the gate-failed task is re-coordinated instead
of deadlocking.
* docs: document the in-path PR-review gate + the cell reviewers (22→25)
Reflect the shipped gate across the canonical + RAG docs.
- CLAUDE.md: agent count 22→25, the cell reviewers in the org chart, an
awaiting_pr_review state + the gate transitions + a gate note in the lifecycle
section, and submit_root / claim_gate_review / pr_pass / pr_fail in the verb
surface table.
- docs/rag/architecture: org-structure (count, cell-reviewer roster, cells
table), agent-uuids (be/fe/ux-pr-reviewer rows), agent-model (role + team
rows).
- docs/rag/roles/pr-reviewer: the in-path gate section + the gate verbs.
- Wrap reviewer.id with UUID(str(...)) in the gate DB tests for mypy.
* docs: finish the gate doc sweep across README + RAG + generated artifacts
Catch the remaining surfaces beyond the canonical docs.
- README + how-to: agent count 22→25, the 6-agent cells (+ PR Reviewer), the
main reviewer's root→master gate role.
- RAG: permissions + tool-permissions + task-tools list the gate verbs
(claim_gate_review / pr_pass / pr_fail) for pr_reviewer; regenerate the
lifecycle artifacts (intent-verbs, status-transitions, the per-role
lifecycle-*.md prompts, panel lifecycle.json) from the spec via
build_lifecycle_artifacts.py so they carry the new status + verbs.
* fix(migration): shorten the 040 revision id to fit alembic_version VARCHAR(32)
The revision id '040_taskstatus_awaiting_pr_review' is 33 chars; alembic's
alembic_version.version_num column is VARCHAR(32), so recording the migration on
a real 'alembic upgrade head' failed with 'value too long for type character
varying(32)' (surfaced on the NAS deploy). The test suite missed it: the test DB
is built via Base.metadata.create_all and the parity test only renders SQL
offline, so nothing actually applied the migration chain.
- Rename to '040_awaiting_pr_review' (22 chars).
- Add a guard test asserting every revision id fits the VARCHAR(32) column.
- Verified by applying the full chain 001->040 against real Postgres: it now
reaches head and records '040_awaiting_pr_review' without truncation.
* fix(migration): land the actual 040 revision-id shortening + guard test
The prior commit captured only the file rename (git add aborted on the deleted
old path), leaving the long revision id and missing the guard test. This commit
carries the real content: revision id '040_awaiting_pr_review' (22 chars) and the
revision-id length guard. Re-verified against real Postgres — the full chain
reaches head and records the short id without truncation.
* fix(product): flush cell deletes before inserts when re-mapping projects
Editing a product's cell->project map (PATCH /api/products/{id}) 409'd with
'duplicate key value violates unique constraint uq_product_projects_product_team'
whenever a team already had a mapping. _replace_cells clears the old rows and
appends the new ones, but within a single flush SQLAlchemy orders INSERTs before
DELETEs for the same table, so the new (product_id, team) rows collided with the
not-yet-deleted old ones. Flush the deletes first.
Pre-existing bug (unrelated to the PR-review gate); surfaced on the NAS. New
real-Postgres regression test re-maps all three cells to different projects —
it fails with the unique violation without the fix and passes with it. The
existing update test only changed WHICH team was mapped, so it never collided.
* fix(gateway): let main_pm submit_root past the shared submit-up guard
submit_root reused the cell PM's _submit_up_ownership_guard, which
hardcoded agent.role != cell_pm and rejected the Main PM with
"submit_up is reserved for cell_pm". A branch-bearing code root could
then never close: submit_root bounced to complete, while complete
required awaiting_pm_review (reachable only via submit_root) and bounced
back — a circular rejection.
Both callers already run the spec gate (can_invoke_intent), which
enforces submit_up→cell_pm and submit_root→main_pm, so the guard's role
re-check was redundant for submit_up and wrong for submit_root. Broaden
it to accept either PM role as a defense-in-depth non-PM reject.
Adds the first choreographer-level submit_root test (the gap that let
this ship).
* fix(gateway): proactively steer both PMs to their bubble-up verb
The submit_root deadlock had a sibling steering gap: nothing told a PM
which verb opens the gate. The delegate next-hint said only 'i_am_idle
when done', and complete's in_progress rejection named submit_root for
the Main PM but left the Cell PM with a bare 'not ready for completion'
— no submit_up pointer, the same guess-the-verb trap.
- delegate hint now names the role-correct verb (root → submit_root,
cell parent → submit_up) proactively, before any rejection.
- cell_pm_complete's in_progress rejection now steers to submit_up,
mirroring the Main PM's submit_root gate hint.
Tests cover both the cell-PM steer and the role-aware delegate hint.
* docs: correct who-merges-which-PR across the gate docs + complete description
Audit of the gate docs found the merge actors mis-stated in several
places — the exact ambiguity that risks 'the reviewer/PM merges the root
PR' confusion:
- complete IntentSpec description said 'Main PM merges root PR' — false;
main_pm_complete escalates and the CEO merges root→master. Corrected
(propagated to intent-verbs.md, lifecycle.json, generated role prompts
via build_lifecycle_artifacts.py).
- task-tools.md: submit_up target was awaiting_pm_review (should be
awaiting_pr_review); Main PM flow had no submit_root — added it.
- README.md: lifecycle diagram now shows the awaiting_pr_review gate.
- cell-pm.md / main-pm.md: dropped the stale 'submit_up hands work to the
Main PM who merges your cell branch' model — the cell PM merges its own
gated cell→root PR; the Main PM owns the root + submit_root; the CEO
merges master. Added submit_root to the main-pm manifest.
- git-commits.md, pr-creation.md, tool-permissions.md, git-tools.md:
stopped attributing root→master PR opening to complete (it's submit_root).
No behavior change; verb wiring + state machine verified gap-free this
session (the pr_fail→needs_revision→PM respawn loop closes correctly).
* fix(orchestrator): stop closure respawn waiting the reaper window
A PM that finished its subtasks and idled left its parent 'paused' with a
fresh last_heartbeat_at. _is_recently_paused gated closure respawn on
_claim_heartbeat_ttl — the REAPER window (stale_claim_reap_seconds: 600s
default, 1800s on the NAS) — so the parent sat untouched for up to 10-30
minutes before its PM was respawned to close it. The whole chain stalled
behind it.
The race that guard actually protects against (i_am_idle auto-pauses, then
the agent is marked IDLE + its container tears down) is seconds, and the
live-session case is already covered by _is_agent_active. Introduce a
dedicated short debounce (pm_closure_recently_paused_seconds, default 45s)
and gate closure on that instead.
The existing test fixture masked this by setting _claim_heartbeat_ttl to
claim_stale_seconds (180s), not the production reaper value. Fixture now
mirrors production; adds a regression test that a parent paused past the
debounce but within the reaper window respawns immediately.
* feat(gate): post the in-path review verdict on the assembled PR
The in-path gate previously left no trace on the PR it gated — pr_pass /
pr_fail were pure status transitions. Now each verdict is posted as a
GitHub review on the assembled PR itself (server-side, bot account), so
the decision is visible on the very PR the PM merges.
- pr_pass → APPROVE, pr_fail → REQUEST_CHANGES on a cell→root PR.
- The root→master PR ALWAYS gets a plain COMMENT, never APPROVE/REQUEST_
CHANGES: only the CEO acts on master, so the gate must never leave an
approval that could satisfy branch protection (letting someone else
merge) nor a blocking review that could impede the CEO's merge.
- Best-effort and AFTER the DB transition — a GitHub failure is logged,
never rolls back the gate decision. Reuses git.post_pr_review's existing
self-review→COMMENT downgrade for the org's own PRs.
Adds _project_slug_for to the ChoreographerHelpers protocol (mypy) and a
unit suite covering event selection, the master-bound COMMENT rule, the
no-PR skip, and failure-swallowing. Docs updated (pr-reviewer, task-tools).
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
de1336c74d |
fix(gateway): let a dev idle past lane-held code-queue siblings
Per-dev sequenced queues (the prior commit) have a PM delegate a dev's whole code queue up front, so a dev owns several pending, assigned-but-unclaimed code leaves at once (seq0 + seq2). The orchestrator's lane barrier holds the seq2 SPAWN while seq0 is non-terminal — but _pending_assignment_guard rejected i_am_idle for ANY pending assigned task, with no lane awareness. So a dev whose current leaf just moved to QA (awaiting_qa) could neither idle (guard rejects) nor proceed cleanly: it was steered to claim seq2 early (the claim path has no lane/sequence check, since delegate sets `sequence` not `dependency_ids`), jumping its own queue order, or it looped on the rejection. An adversarial review of the queue work surfaced this; it is latent until PMs actually delegate multi-item per-dev queues, so the green suite hid it. Fix: TaskService.has_earlier_incomplete_code_sibling mirrors the orchestrator's lane barrier in the service layer; _pending_assignment_guard now drops a dev's lane-held pending code leaves (via _pending_blocking_idle / _pending_not_lane_held) so the dev idles cleanly and the orchestrator spawns the next queue item when the lane clears — preserving one-leaf-at-a-time, in order. `is not True` keeps it inert under partial test mocks. Tests cover the service primitive (live / terminal / higher-seq / non-code / missing-field) and the guard (dev idles when lane-held; still blocks a non-lane-held pending leaf). Full mypy + xenon green. |
||
|
|
e209e285b8 |
feat(dispatch): per-dev sequenced queues for code subtasks (guardrails spec 3)
True two-dev parallelism: a cell PM delegates the FULL set of code units up front — each dev gets its own queue, both build at the same time, each works its queue one task at a time in order. Replaces the old ceiling (≤2 code subtasks per parent, one per dev) which structurally forced under-decomposition. - Cap: `code` removed from `_SPINE_TYPE_CAPS` — no per-parent code cap (total fan-out still bounded by `_SUBTASK_HARD_CAP=12`); `planning`/`documentation` stay sequential at 1. `_same_assignee_rejection` exempts `code` so a dev may own a queue, but still rejects an exact same-title duplicate (the accidental re-delegation bug). `_spine_type_dup_envelope` simplified to the sequential spine it now only serves. - Dispatch barrier: `_blocked_by_earlier_lane_sibling` holds a dev's higher-sequence pending code leaf while it still has an earlier non-terminal code sibling under the same parent (keyed on assignee, gates only code) — the dev works its queue in order. Wired into `_spawn_pending_dev`. Loop-free (skip the tick, no reject/respawn) and best-effort (lookup failure → dispatch), mirroring the existing merge barrier. The merge barrier is unchanged: leaf PRs still merge serially in sequence order into the shared cell branch, so the independent build lanes never wedge it. - Prompt: cell_pm role guidance rewritten from the two-subtask-cap model to the per-dev-queue model (delegate all units now; dependent units go in one dev's queue, upstream first). Independent per-dev queues (each lane advances at its own pace) rather than strict cross-dev wave-sync, by design — more parallel and leaves the wedge-prone merge barrier untouched. Pairs with the spec-2 idle coverage gate: removing the code cap lets a PM claim every criterion up front, so that gate is always satisfiable. |
||
|
|
1fb723174a |
feat(gateway): decomposition coverage gate + AC visibility (guardrails spec 2)
The decomposition floor that pairs with the roll-up gate (spec 4): a PM
cannot finish decomposing a parent while one of its acceptance criteria has
no subtask responsible for it — the "two leaves, half the ACs silently
dropped" pattern. Three parts:
- Gate: i_am_idle is rejected for a cell_pm/main_pm whose owned parent still
has criteria in unclaimed_parent_acceptance_criteria (claimed = referenced
by any live, non-cancelled child). Distinct from the roll-up gate, which
fires at submit_up/complete and demands a *completed* child; this fires
earlier and asks only that every criterion be *claimed*. Safe-by-
construction: inert until a PM declares coverage, so legacy / not-yet-
adopted decompositions are never blocked.
- Visibility: PM-facing briefings (give_me_work, i_will_plan, submit_up) and
every delegate response now carry parent_ac_coverage ({id,text,claimed,
verified} per criterion) + unclaimed_parent_acs, so a PM can map subtasks
to criterion ids via covers_parent_criteria and see what is still
uncovered after each delegate. Off for leaf roles, so a developer's own
criteria never surface as bogus "unclaimed" noise.
- Prompts: cell_pm / main_pm role prompts document covers_parent_criteria and
the new idle enforcement in the existing Coverage discipline.
TaskService.{parent_ac_coverage,unclaimed_parent_acceptance_criteria} added
beside uncovered_parent_acceptance_criteria; all three refactored onto a
shared _parent_ac_ref_sets helper (keeps each under the xenon B ceiling,
preserves the committed roll-up behavior). Verb tables regenerated for the
new delegate param — the regen also syncs pre-existing table drift that was
never regenerated after earlier merges (read_messages, pass_review
ac_verdicts, board pitch). Two brand-new generated tables (prompter,
secretary) are left untracked pending a separate decision.
|
||
|
|
46d89b58fe |
feat: company-in-a-box — goal-aware company layer (0.4.0) (#171)
* feat(goals): company charter singleton — data layer (Business Goals slice 1)
First slice of the company-in-a-box "Business Goals" phase: a single CEO-owned
charter row (north star + objectives + constraints + operating policy) that
will be injected into every agent's context_briefing so all work is goal-aware.
- CompanyGoalsTable: singleton table (all-zeros id), JSON objectives /
constraints / operating_policy, updated_at / updated_by.
- migration 032: create + seed the singleton row (offline-renderable; column
server-defaults fill an INSERT of just the id).
- CompanyGoalsService: get() (empty defaults when unset) + upsert() (singleton,
partial update, caller commits).
- tests: empty defaults, roundtrip, singleton + partial-update preservation.
Next slices (mapped, not yet built): briefing injection (BriefingInputs +
build_context_briefing + EvidenceRepo), API route (GET any / PUT CEO-only),
panel /goals page, and base/Board/PM prompt mentions.
* feat(goals): inject the company charter into every agent briefing (slice 2)
The charter is now goal-aware context for every agent:
- BriefingInputs gains company_goals; build_context_briefing surfaces it.
- EvidenceRepo.company_goals(): single-row lookup returning a COMPACT charter
(north star + objectives + constraints + operating policy; audit columns
dropped, lists capped) or None when unset, so an empty charter never bloats
the per-verb briefing.
- _briefing_for wires it into every context_briefing.
Tests: briefing surfaces company_goals (defaults None); repo returns None for an
absent/empty charter and the compact dict when set.
* feat(goals): company charter API — GET any agent, PUT CEO-only (slice 3)
- routes/company_goals.py: GET returns the charter (any authenticated agent —
it drives every briefing); PUT is CEO-only (403 otherwise), partial update via
model_dump(exclude_unset=True), explicit commit.
- schemas/company_goals.py: response + partial-update models.
- registered at /api/company-goals.
- tests: GET open to any role, CEO update persists + is readable, non-CEO 403.
* feat(goals): make the company charter actionable in agent prompts (slice 5)
Agents already receive company_goals in the briefing (slice 2); now tell them to
act on it:
- base.md: universal "Align with the company charter" section — favour work and
trade-offs that advance the objectives, honour the constraints, flag conflicts;
never a license to leave your role.
- board / main_pm / cell_pm: role-specific lines tying triage / cell-routing /
subtask decomposition to the charter.
Prompts are composed at spawn from base.md + roles/*.md directly (compose_prompt),
so no _generated regeneration is needed.
* feat(goals): company charter panel page (slice 4)
CEO-facing editor for the charter at /company-goals:
- lib/api/company-goals.ts: get / update (PUT) client.
- company-goals-card.tsx: edit north star + constraints (one per line) +
objectives / operating_policy (JSON, parsed + validated with toast errors);
display derives from server state (no set-state-in-effect).
- (dashboard)/company-goals/page.tsx + a "Company Goals" sidebar nav link.
tsc --noEmit + eslint clean. Completes Phase 1 (Business Goals): data, briefing
injection, API, prompts, panel.
* fix(test): make test_app route assertions robust to FastAPI 0.137 _IncludedRouter
FastAPI 0.137 stopped flattening include_router into app.routes — each include is
now an _IncludedRouter (a BaseRoute with no .path), so `{r.path for r in
app.routes}` raised AttributeError and the two router-registration tests failed
(the bump arrived via the claude-agent-sdk update in uv.lock). Add
_registered_paths(): OpenAPI schema paths (the stable public contract) plus each
included router's prefix, which also covers the websocket /ws mount (never in the
schema). Drops the now-incorrect type: ignore[attr-defined].
* feat(research): pluggable web search/fetch for Board + PM agents
Add a provider-agnostic web-research capability so the Board and PMs can
ground decisions in current external evidence the knowledge base can't
answer.
- ResearchService selects a provider adapter from config: Tavily, Brave,
and Exa adapters plus a NullProvider that degrades gracefully when no
key is set. Result count and fetched-content size are clamped to caps.
- /api/research/search and /api/research/fetch: role-gated to Board + PMs
(and the CEO), with a per-agent/day Redis quota that fails open.
- roboco-search MCP server (web_search / web_fetch) calls those routes;
the provider key stays server-side and agent containers never egress.
Mounted per role by the orchestrator, behind a master switch.
- Charter-aware prompt guidance for Board, Main PM, and Cell PM.
Additive: with no key configured it is a no-op and the existing delivery
lifecycle is unchanged.
* feat(pitch): Board pitch -> CEO approve -> auto-provision repos
Add an additive origination path so a product can be proposed, approved,
and stood up without manual repo/Project setup.
- Pitch entity + migration (pitches table); PitchService create/list/
reject/approve.
- GitHubProvisioningService: the one place that creates repos (POST
/orgs/{org}/repos). Server-side token/org; when unconfigured the whole
approve path is inert and nothing is created.
- On approval: provision one repo per target cell, register a Project per
repo, create a Product when multi-cell, and seed one Main-PM delivery
task — all reusing the existing Product / coordination-task machinery.
- /api/pitches: Board authors (PO/HoM), CEO approves/rejects, Board+PM+CEO
view. Errors mapped via a single translator.
Additive: the delivery lifecycle is untouched; with no provisioning token
the capability is a no-op. Agent-facing pitch tool + panel are follow-ups.
* feat(strategy): dormant autonomous strategy engine (engine 2)
Add a second, optional engine that watches the company against its
standing goals and surfaces what needs the CEO — without touching the
delivery lifecycle (engine 1).
- StrategyEngine.assess() reports observations: the company is idle while
goals stand, and tasks stranded in 'blocked' past a threshold.
- run_cycle() notifies the CEO (notify-only; it never spends, builds, or
auto-approves — originating work stays a CEO decision).
- Orchestrator runs it on its own interval, started/stopped with the other
background loops; the loop returns immediately unless enabled.
DORMANT by default (strategy_engine_enabled=False): the loop never runs and
a standard deployment is unchanged. Auto-origination is a further opt-in.
* docs(changelog): record Business Goals, Web Research, Pitch->Provision, and the dormant strategy engine under Unreleased
* feat(secretary): wire the Secretary role end-to-end (foundation)
Add SECRETARY as a distinct role — the CEO's conversational chief-of-staff,
governed separately from the Prompter (which stays read-only/human-only).
This is the role foundation only; authority, the live agent, and the panel
land in following commits.
- foundation/identity: Role.SECRETARY (board level), seeded secretary-1 agent,
role-level mapping.
- journaling read tier (ALL — it advises the CEO), role_config entry,
per-role model (opus), prompt-layer mapping + roles/secretary.md.
- i_am_idle gains SECRETARY so the role has a verb surface.
- migration 034: add 'secretary' to the agentrole enum (mirrors 025).
- Role-registry tests updated for the new role.
Inert by itself (nothing spawns it yet); additive — existing roles unchanged.
* feat(secretary): directives + gate-list authority (backend)
The Secretary acts only under CEO command. Low-risk directives (relay a
dictated message) execute immediately; high-impact ones — charter edits,
task start/cancel/override, pitch approval, announcements — are recorded
pending and run only after the CEO confirms (the gate list).
- secretary_directives table (migration 035) as the command audit + queue.
- SecretaryService: read company state; submit (direct->run, gated->queue +
notify CEO); confirm/reject; execution runs with the CEO as actor through
the existing services (the Secretary never holds CEO authority itself).
- /api/secretary: submit + state/task reads (Secretary or CEO); list/confirm/
reject (CEO only). Writes commit explicitly.
* feat(secretary): live conversational agent (container + bridge)
Stand up the Secretary as a persistent Claude-SDK container the CEO chats
with, mirroring the Intake agent and reusing its driver/session machinery.
- secretary_driver: build_secretary_options exposes read_company_state /
read_task / submit_directive as SDK tools that call /api/secretary/* with
the agent's HMAC token; backend-call logic is module-level + tested.
- secretary_main: container entrypoint (receiver + relay) reusing IntakeDriver.
- orchestrator: start/spawn/reap secretary session + run-cmd builder; no
workspace clone (reads state via API), mints a role=secretary token.
- secretary_live routes: panel <-> container bridge over the live registry.
- agent-secretary image (Dockerfile + compose build service).
Inert until a session is started; additive — intake and all agents unchanged.
* feat(secretary): panel chat + directive confirmation queue
The CEO's Secretary surface: a live chat (SSE) to talk to the Secretary, and
a 'Needs your confirmation' queue listing gated directives the Secretary
proposed — each with Confirm / Reject. Adds the sidebar nav entry.
- lib/api/secretary.ts: live (start/stream/status/send/stop) + directive
(list/confirm/reject) + state clients (all as the CEO).
- hooks/use-secretary.ts: drives one chat, accumulating SSE token deltas.
- secretary page: chat pane + pending-directive cards.
Completes the Secretary end-to-end (role + authority + live agent + panel).
* feat(pitch): agent-facing pitch tool + pitches panel
Complete the pitch path: the Board can now author pitches through the gateway,
and the CEO reviews/approves them in the panel.
- content_actions.pitch (Board-only) -> PitchService.create, returning an
Envelope; wired as a do-tool (do_server + /api/v1/do/pitch + schema) and
added to the Board's do-tools.
- Panel /pitches page: lists pitches with CEO Approve & provision / Reject;
sidebar nav entry.
Pitch (Phase 4) is now end-to-end: author -> CEO approve -> auto-provision.
* feat(cockpit): read-only 'is the business winning?' summary
A pure aggregation for the CEO over existing data — no new state, no writes.
- CockpitService.summary(): charter north-star/objectives, delivery counts
(in-flight/blocked/awaiting-CEO), 30-day spend vs the charter's budget cap,
pending pitches, and the strategy engine's signals (what needs you). Stamped
basis='proxy' — performance is a proxy until real launches.
- GET /api/cockpit/summary (CEO / Board / Main PM / Secretary).
- Panel /cockpit page + sidebar nav.
Reuses goals + usage + StrategyEngine.assess(); reads only.
* docs(changelog): add the Secretary and Cockpit to Unreleased
* fix(test): isolate the company-goals empty-defaults test from committed state
The shared test DB persists committed writes across tests; a route test
commits a charter, so the unit test's 'unset' assertion must establish its
own clean precondition rather than assume global emptiness.
* fix(gateway): lower evidence_repo complexity to rank A (xenon gate)
company_goals()'s 4-way `or` emptiness check tipped the module average to
rank B; `any(...)` is equivalent and keeps the module under the gate's A bar.
* chore(compose): mirror agent-secretary-image build into docker-compose.yaml
Both compose files are byte-identical and tracked; .yaml carries the same
agent-secretary-image build service already present in docker-compose.yml.
* chore(lifecycle): regenerate artifacts for secretary i_am_idle
The secretary role gained i_am_idle in the lifecycle spec; regenerate the
generated prompt/doc/json artifacts so foundation-check stays green.
* docs(changelog): cut the company-in-a-box phases to 0.4.0
Label the six additive phases (business goals, web research, pitch-provision,
strategy engine, secretary, cockpit) as 0.4.0; tag v0.4.0 is held until the
branch merges to master so it points at the release commit.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
2817ca1ceb |
Fix the PR-divergence respawn loop: loop gate, CEO god-mode, PR conflict resolver, sequence-ordered merge (#164)
* fix(orchestrator,panel): bound the respawn loop gate and give the CEO a status override
The PM respawn loop gate could never fire on a recurring tracing_gap: every
same-status respawn that emitted a tracing_gap reset the strike counter, so a
task whose unblock can never satisfy its decision gate respawned forever. Cap
the number of tracing_gap resets (pm_respawn_max_tracing_resets) so strikes
accrue once a gap is clearly recurring rather than progressing, and route the
pm-review and blocker dispatch respawn paths through the gate so it actually
applies to those loops.
Panel: the task status dropdown was driven solely by the lifecycle graph, so a
task wedged in a terminal/blocked state offered no actionable transitions. Add
an audited admin status override (PATCH status -> admin_set_status) for every
non-in-band target, letting the human operator force any state.
* feat(git): add rebase_onto_base and close_pull_request PR-divergence primitives
Agents had no way to resolve a PR that could not merge because a sibling merged
overlapping work first: their only moves were complete (which 405s) or block
(which loops). Add the two missing operations:
- rebase_onto_base rebases a head branch onto the latest base and classifies
the outcome: superseded (no unique commits -> safe to close), rebased (unique
work -> force-pushed, ready to merge), or conflicts (aborted, needs a human).
- close_pull_request retires a superseded PR with an explanatory comment.
These back both the sequence-ordered merge and the conflict resolver.
* feat(gateway): auto-resolve a leaf PR that can't merge instead of looping
When a sibling lands overlapping work first, the cell PM's complete() merge
hits a GitHub 405 and the task re-blocks, respawning the PM forever (the
production wedge: one task burned 6000+ tool calls over 3 hours). The merge
now raises MergeConflictError, and cell_pm_complete resolves it:
- rebase the branch onto the current base;
- superseded (no unique commits) -> close the dead PR + complete the task
without a redundant merge (the manual action operators kept requesting);
- rebased (unique work) -> retry the merge, then complete;
- genuine conflicts -> admin-override the task to awaiting_ceo_approval and
alert the CEO, so it leaves agent dispatch instead of looping.
MergeConflictError subclasses GitError, so existing handlers are unaffected.
* test(git): silence unused-arg lint in close_pull_request stub
* feat(orchestrator): sequence-ordered merge for leaf siblings
Leaf siblings share one cell branch, but within-cell siblings were all left at
the default sequence 0, so two leaf PRs raced into the same branch and the
second wedged. Now:
- decomposition assigns each new sibling the next ordinal within its parent, so
the merge order is well-defined;
- the pm-review dispatcher holds a higher-sequence leaf until its earlier
same-team siblings are terminal, so they merge into the shared branch in order
instead of racing.
Loop-free by construction: a gated task is simply not dispatched this tick (no
reject, no respawn). Terminal siblings never block, so a cancelled sibling can't
deadlock the rest; any sibling lookup failure degrades to dispatch.
* test: use monkeypatch.setattr instead of type:ignore in new tests
CI type-checks tests/ (the type-gated suite) which my local 'mypy roboco/' skipped.
The method-mock assignments tripped mypy method-assign/assignment; replace the
silencing comments with monkeypatch.setattr and local mock refs for assertions,
matching the project's no-type:ignore rule.
* fix(git): stop get_status misreporting an unstaged deletion as staged
git_status used stdout.strip().split() before parsing porcelain. strip() eats
the leading space on the first line, so an unstaged deletion (' D file') became
'D file' and parsed as a STAGED deletion — the false 'staged' that caused 6
wasted QA cycles when a dev deleted a file without staging it. Use splitlines(),
which preserves the index/worktree status columns.
* feat(panel): mobile sidebar hamburger + Sheet drawer (AC1)
The umbrella's AC1 was never built: on mobile the sidebar had no entry point.
Extract the nav/footer into shared SidebarNav/SidebarFooter, hide the static
sidebar below md, and add a hamburger in the header that opens the same nav in a
left Sheet drawer (closing on navigation). Desktop is unchanged.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
6cf99a1b0a |
[beb8cae1] Type-gate tests/ under mypy — fix all errors and flip quality gate (#156) (#157)
* [420e5e68] Fix mypy errors in tests/unit/ and create tests/__init__.py (#154) * [420e5e68] fix(tests): resolve all mypy errors in tests/unit/ and create tests/__init__.py - Create tests/__init__.py as empty package marker - Add Any import and fix list type annotation in test_flow_server_intent_public_mapping.py - Move AsyncIterator to TYPE_CHECKING block and fix m.cls.__name__ attr error in test_app.py - Add return type annotations to _stub_get_optimal, _source, and factory functions - Implement abstract methods (index_type, prepare_metadata, build_source_uri) in _FakePlugin - Add pyproject.toml per-file-ignore for ARG002 on test_optimal_grounding.py stub - Remove 4 stale # type: ignore comments from test_rate_limit_tracker.py - Fix method-assignment patterns in test_rate_limit_sweep.py via patch.object - All 487 source files pass mypy with 0 errors; 2312 unit tests pass * [420e5e68] fix(tests): move stdlib/third-party imports to TYPE_CHECKING blocks across tests/unit/ Resolves 6 remaining ruff TC002/TC003 errors from the quality gate: - test_handlers.py: Iterator → TYPE_CHECKING - test_quality_gate.py: pathlib → TYPE_CHECKING - test_board_dispatch.py: AsyncIterator + httpx → TYPE_CHECKING - test_streaming.py: Iterator → TYPE_CHECKING - test_notification.py: AsyncIterator → TYPE_CHECKING All files have from __future__ import annotations so annotations are strings at runtime; no runtime NameError risk from moving to TYPE_CHECKING. * [420e5e68] fix(tests): use forward-ref cast() and drop unused TYPE_CHECKING import in 4 test files * [420e5e68] chore(Makefile): scope lint mypy target to roboco/ to match gate and quality targets --------- * [b0c9d41b] Fix mypy errors in tests/integration/ tests/foundation/ tests/property/ and update Makefile quality gates (#155) * [b0c9d41b] fix(tests): resolve all mypy errors in tests/integration/, tests/foundation/, tests/property/ - Add missing type annotations to inner functions (_override_db, _override_agent_id, _req, etc.) - Use cast("UUID", ...) to fix SQLAlchemy UUID vs uuid.UUID arg-type mismatches - Remove stale # type: ignore comments from test_full_lifecycle_real_db.py and test_task_service_lifecycle_misc.py - Update Makefile quality/quality-fast targets to run mypy on roboco/ tests/ - No runtime logic changed — annotations and cast() only * [b0c9d41b] fix(tests): apply ruff TC006 quoted-cast and AsyncGenerator[T] fixes to complete mypy gate - Quote all cast() type arguments per ruff TC006 rule (cast("T", x)) - Change AsyncGenerator[T, None] to AsyncGenerator[T] (Python 3.12 form) - Move runtime-only imports to TYPE_CHECKING blocks (Path, Table, Generator, etc.) - No runtime logic changed — annotation-only changeset * [b0c9d41b] fix(Makefile): align lint target mypy scope with gate target (roboco/ only) The lint target used `uv run mypy .` (all files) while gate uses `uv run mypy roboco/`. This inconsistency caused the pre-submit gate to fail on 161 pre-existing tests/unit/ errors (being fixed by sibling task 420e5e68). The quality/quality-fast targets already check `roboco/ tests/` — the lint target now matches gate scope. --------- --------- Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev> Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev> |
||
|
|
a8b387b5bb |
feat(gateway): run the full fast gate (incl. complexity) at the dev's desk
Add a per-project `quality_command` (model + ORM + API + panel + migration 029) that the pre-submit gate prefers over the lint/typecheck pair. Pointed at the new `make gate` target (ruff format --check + ruff check + mypy + xenon, no tests), i_am_done now catches lint, type AND complexity failures in the developer's workspace before QA — closing the gap where over-complex code only failed in CI. Falls back to lint+typecheck when unset; a no-op when no commands are configured. |
||
|
|
481c5fe17b |
feat(gateway): run a fast quality gate at i_am_done, before QA
The developer's i_am_done submit now runs the project's fast quality gate (lint + typecheck) in the developer's workspace and blocks the transition to awaiting_qa if it's red, returning the failing output as the remediate hint — so a red gate is caught at the dev's desk instead of in QA review or CI. The slow test suite intentionally stays on CI. The gate is fail-open on infrastructure errors (missing workspace/toolchain never blocks a submit) and a no-op for projects that configure no lint/typecheck commands. Developer prompt updated. |
||
|
|
2db11c7833 |
feat(qa): require a per-acceptance-criterion verdict before pass_review
QA may no longer pass a task with a single gestalt approval — pass_review now takes ac_verdicts (one verification entry per acceptance criterion) and the gateway rejects a pass that does not cover every criterion. If a criterion does not hold, QA fails the review instead. The verdicts are folded into the persisted qa_notes for the audit trail. Threaded through the flow MCP tool, the HTTP request schema, and the route; QA prompt updated. |
||
|
|
8affb283f5 |
feat(gateway): enable 2-devs-per-cell parallelism + split-before-claim sizing
- Intake / Main-PM / Cell-PM prompts: enumerate independently-shippable work units, inherit the breakdown down the chain, and dispatch independents in parallel (dependency order, never one-at-a-time). - Raise the code-spine concurrency cap from 1 to 2 per parent (one per cell developer) so both devs build in parallel; keep the same-assignee guard and the planning/documentation cap at 1, plus the cross-team planning exemption. - Split-before-claim: hard-block an egregiously-bundled code leaf at delegate time so the PM splits it before any dev claims it; nudge the moderate band in the delegate success envelope. |
||
|
|
303c2db289 |
Fix: rate limit real probe (#110)
* fix(rate-limit): real provider liveness probe instead of time-based stub
The rate-limit recovery sweeper cleared a provider and resumed parked agents
purely on elapsed time — _do_probe was a stub that always returned True once
the retry_after window passed, so it never confirmed the provider had actually
stopped rate-limiting us. Under a sustained limit that resumes agents straight
into another 429, re-parking them: avoidable churn.
Make the probe real. _do_probe now issues a free, unmetered liveness call —
Anthropic GET /v1/models or Ollama GET /api/tags — and treats any non-429
response as the limit having lifted. A 429 keeps the provider parked; a
network error keeps it parked too (retry next sweep). When the provider can't
be probed (no API key, or an unrecognized provider), it falls back to the
prior time-expiry optimism rather than stranding agents. _probe_target keeps
URL/header resolution separate and testable, and _do_probe stays a
monkeypatchable boundary so the existing sweep tests are unaffected.
Also drop two acceptance-criteria-number labels from comments in this file.
* chore(rate-limit): clear merged gate debt in rate-limit tests + deps lint
The rate-limit PR landed with ruff violations the full gate flags but the
authors' runs missed: test_rate_limit_sweep.py was unformatted, and
test_rate_limit_tracker.py had unsorted/unused imports and magic-value
comparisons. Format the sweep test, drop the dead imports, and bind the
magic comparison values to locals. Also strip acceptance-criteria-number
labels from comments/docstrings across the three rate-limit test files
(leaving genuine acceptance_criteria=[...] test data untouched), and add
api/deps.py to the PLC0415 per-file-ignore — it is the DI wiring hub and
defers a couple of service imports to call time to avoid import cycles,
the same rationale already applied to api/routes, runtime, and services.
* fix(rate-limit): resolve redis type errors in RateLimitStateTracker
A cold mypy run (the gate's true state — prior passes were warm-cache only)
flagged four redis-typing errors in rate_limit_tracker.py that the merge
missed: three unused type:ignore[type-arg] on redis.Redis, and an
aclose() the bundled redis type stub doesn't expose.
Drop the now-unused ignores, and close the scan client via
'async with redis.from_url(...) as r:' instead of a finally-block
aclose(). The context manager closes the client on exit using the modern
redis.asyncio API — no deprecated close(), no stub-missing aclose(), no
suppression. Extend the test's redis mock to model the async
context-manager protocol so it returns itself on enter.
* test(prompter): pass route='main_pm' in the product main-PM routing test
Pre-existing master failure, unrelated to the rate-limit work. The test is
named ...product_routes_to_main_pm and asserts team=MAIN_PM, but called
confirm_live_draft without a route, so it got the 'board' default — which
assigns the Product Owner and yields team=BOARD by design (the board-review
path keeps the root at team=board until the CEO approves). The Main-PM path
is selected with route='main_pm', exactly as the sibling
...main_pm_route_assigns_main_pm test does. Add the missing kwarg so the test
verifies the path it names; behaviour under test is unchanged.
* Updated uv.lock
* refactor(complexity): bring all rank-C blocks under the xenon B ceiling
The full quality gate's xenon step (--max-absolute B --max-modules A
--max-average A) failed on eight rank-C blocks plus the extraction module
average — debt the rate-limit and token-analytics merges deferred. Reduce
each by extracting cohesive helpers, behaviour unchanged:
- orchestrator._probe_one_provider: split into _too_early_to_probe,
_on_probe_success, _on_probe_failure, _parked_agents_for.
- rate_limit_tracker.list_rate_limited_providers: extract _read_rate_limited_entry
and a _decode helper.
- trigger_filter.decide_spawn: extract _stale_trigger_decision (drops the
PLR0911 suppression too).
- ollama_embedder (embed_query, _embed_batch_sync, aembed_query,
_embed_batch_async): share _rl_backoff / _map_embed_error / _log_429 /
_sleep_connect_retry / _asleep_connect_retry; remove a dead post-loop guard
in aembed_query.
- mentor._synthesize_answer: extract _select_system_prompt and
_answer_from_response.
- indexes/base.ask: extract the 429-retried LLM call into _ask_llm.
- extraction.__init__: extract _compile_patterns so the module average
lands at rank A.
xenon now exits 0; rate-limit, optimal_brain, extraction, and events suites
all green.
* chore(deps): drop obsolete types-redis stub; honor redis 8.0 inline types
types-redis 4.6 (typed for redis 4.x) shadowed redis 8.0's own inline types,
which both masked real annotation mismatches in stream_bus.py and forced
awkward workarounds elsewhere. The stale stub is why the mypy gate only ever
passed warm-cached: a cold run under the wrong stub disagreed with the code.
Remove types-redis (and its orphaned transitive stubs) so mypy uses redis's
shipped types. That surfaces that xreadgroup/xclaim return bytes-keyed records
while _handle_message is annotated str — the code already decodes bytes
defensively, so this is an annotation gap, not a runtime bug. Make the types
honest: cast each result to its concrete shape and decode the stream name and
message id to str at the dispatch boundary via a _to_str helper.
mypy roboco/ is now clean cold (247 files) against redis's real types; events
suite green.
* Updated uv.lock
* fix(workspace): install the dev extra so agents can run make quality
Agent workspaces were set up with plain `uv sync`, which installs only the
project's default dependency group (pytest) — not the `dev` *extra* where the
gate tools live (ruff, mypy, xenon, radon, vulture, bandit, deptry). So an
agent's .venv had pytest but no linters, and `make quality` died immediately
on `ruff: command not found`. Agents literally could not lint, type-check, or
complexity-check their own work, which is how format/mypy/xenon debt merged
unseen. Sync the `dev` extra (`uv sync --extra dev`) so the workspace gets the
full toolchain the setup's own docstring already promised.
* fix(panel): rate-limit endpoint shape + websocket path
Two panel-facing breakages from the rate-limit rework:
- GET /api/system/rate-limits returned a raw list, but the panel store reads
response.entries — so `r.entries is not iterable` crashed the banner sync on
page load. Return the panel's contract: a { entries: [...] } envelope whose
items are camelCase {provider, affectedAgents, hitAt, resumeAt,
retryAfterSeconds}, derived from the raw Redis state (resumeAt = hitAt +
retryAfter).
- The rate-limit websocket hook passed "/ws/system" while getWebSocketUrl()
already supplies the "/ws" base, producing the doubled "/ws/ws/system" URL.
Pass "/system" to match the agents/channels/notifications hooks.
Note: the backend /ws/system endpoint itself does not yet exist (the rework
shipped the panel hook only); the REST fix keeps the banner correct on load
and reconnect until that endpoint is built.
* test(workspace): assert uv sync installs the dev extra
Follow the workspace setup change: the dependency-install command is now
`uv sync --extra dev` so the agent workspace gets the lint/type/complexity
toolchain. Update the three assertions that pinned the old `uv sync`.
* feat(ws): add /ws/system stream and bridge rate-limit events to the panel
The rate-limit rework shipped the panel's websocket hook but no backend: there
was no /ws/system endpoint and nothing forwarded RATE_LIMIT_HIT/LIFTED to a
socket, so the banner got no live updates.
Build the missing half:
- ConnectionManager grows a system-wide connection set with connect_system /
broadcast_system, and disconnect() now clears it.
- A /ws/system websocket endpoint (operator stream, no per-agent keying) with
the same connected + ping/pong lifecycle as the other streams.
- websocket_bridge subscribes RATE_LIMIT_HIT/LIFTED and forwards each to
broadcast_system tagged with the type the panel switches on. Both events
ride the same StreamEventBus singleton, and the subscriptions register
before start_listening(), so the consumer reads their streams.
Pairs with the panel hook now passing '/system' (getWebSocketUrl supplies the
'/ws' base). Covered by handler, manager, and endpoint-lifecycle tests.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|