mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fd10cc862c2020b3f639cdb686d427b0198a2441
342
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e2f7097aab |
Persist the PM-respawn counter across orchestrator restarts (#275)
* feat(orchestrator): add respawn_tracker table + migration 051 Durable backing for AgentOrchestrator._pm_respawn_tracker (the PM-respawn loop breaker). Kept only in memory it reset to count=1 on every restart, re-burning the strike threshold against a still-wedged task. RespawnTrackerTable mirrors WaitingRecordTable: composite PK (agent_slug, task_id) matching the in-memory key; task_id is intentionally NOT a FK (the startup loader validates against live tasks so a stale counter can't resurrect). Migration 051 verified with a real alembic upgrade head + downgrade -1 + re-upgrade on Postgres. * feat(orchestrator): persist the PM-respawn counter across restarts The PM-respawn loop breaker (_pm_respawn_tracker) lived only in memory, so an orchestrator restart reset a wedged task's strike count to 1 and re-burned the whole threshold (4 spawns x container cost) before the gate fired again. Write-through each gate mutation to the respawn_tracker table via a fire-and-forget _schedule_respawn_persist (on the existing _bg_tasks strong-ref set; a DB hiccup degrades to in-memory-only, never gates/un-gates a spawn), and restore_respawn_tracker() repopulates the counter at startup, validating each row against live tasks (drops terminal/missing) so a stale counter can't resurrect against a fixed task. Best-effort + inert when the table is empty. Cannot manufacture a spawn — the counter only ever suppresses one. (_instances reconcile, the spec's other goal, already shipped as _readopt_running_agents.) * fix(types): cast Mapped[UUID] columns in project routes + self_heal A clean `mypy roboco/ tests/` run surfaces 7 pre-existing errors in files this branch doesn't touch: project-route handlers and self_heal_engine pass a ProjectTable.id (declared Mapped[UUID] against SQLAlchemy's dialect UUID, so mypy infers sqlalchemy.sql.sqltypes.UUID[Any]) where a uuid.UUID is expected. An incremental .mypy_cache had hidden them. Apply the same targeted cast unblock used for the prior batch; the deeper fix (migrating the ~88 Mapped[UUID] columns to Mapped[uuid.UUID]) remains a separate dedicated task. * docs(orchestrator): document respawn_tracker durability Add the orchestrator runtime-state durability note to CLAUDE.md (respawn_tracker write-through + restore; _instances reconciled-from-Docker) + the migration-051 narrative, and a CHANGELOG [Unreleased] Fixed entry. Also type-clean the new respawn_tracker table test (cast __table__ to Table under TYPE_CHECKING). --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
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> |
||
|
|
153723406e |
Feat/autonomous maintenance (#264)
* feat(ci-watch): config flags Default-off CI-watch config (mirrors self_heal_*): ci_watch_enabled, ci_watch_default_workflow (ci.yml), ci_watch_interval_seconds (1800), ci_watch_max_open_tasks (3), ci_watch_max_per_cycle (1). Registers ci_watch_enabled in the panel FEATURE_FLAGS. 4 tests. * feat(ci-watch): per-project ci_watch_enabled/workflow (migration 048) Adds projects.ci_watch_enabled (bool NOT NULL default false) + projects.ci_watch_workflow (varchar null) — the per-project opt-in for multi-repo CI-watch. ProjectTable + Pydantic Project fields + migration 048 (off 047_ws_single_active). Real upgrade->downgrade->upgrade chain verified against a throwaway Postgres; 2 ORM round-trip tests. * feat(runtime): prune dangling agent images in the background sweeper Every agent-image rebuild orphans the prior build's layers as an untagged <none> image; across deploys these pile up (the operator hit ~80). The sweeper now runs 'docker image prune -f --filter dangling=true' (dangling only — a tagged image or one backing a running container is never dangling), throttled to settings.image_prune_interval_seconds (default 6h) and gated by image_prune_enabled (default on). Best-effort: any failure is logged, never raised into the sweeper. Mirrors the transcript-retention prune. 4 tests. * feat(ci-watch): source tag + open-task dedupe query CI_WATCH_SOURCE='ci_watch' + TaskService.list_open_ci_watch_tasks(git_url=None): non-terminal ci_watch tasks (the dedupe + open-cap basis), optionally scoped to one repo by git_url — a monorepo registers several cell-projects on one git_url, so dedupe keys on the repo, not the slug. 2 real-PG tests. * feat(ci-watch): multi-project CI telemetry fan-out MultiProjectCITelemetrySource.fetch(projects) reuses the hardened per-project get_latest_ci_conclusion for each opted-in project (passing its ci_watch_workflow or the configured default). Per-project isolation: a GitHub error or absent signal yields NO sample (unknown, never read as green) and never aborts the sweep; only a real conclusion yields a sample (fail→breach, pass→non-breach). self-heal source untouched. 3 tests + self-heal regression green. * feat(ci-watch): engine — fan-out, originate, dedupe, cap CiWatchEngine.run_cycle(projects) mirrors SelfHealEngine: assess via MultiProjectCITelemetrySource, open one PENDING ci_watch fix task per red repo (team=main_pm, assigned_to=main-pm, confirmed_by_human=True so it dispatches without an Approve-&-Start — the |
||
|
|
2c403c77a2 |
Fix/run hardening prep (#263)
* fix(git): don't delete a branch that still has open dependent PRs
Root cause of the run-zombifying "integration branch gone from origin" wedge.
_delete_remote_branch_best_effort deleted a merged PR's head branch
unconditionally, so:
- merging a cell->root PR deleted the cell branch while a sibling leaf PR was
still targeting it as base, and
- the CEO's root->master merge deleted the feature/main_pm/{root} integration
branch.
The dependent PRs lost their base, every later git op against the vanished
branch failed, and the task zombified (
|
||
|
|
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. |
||
|
|
a51c3d312a |
fix(run-hardening): don't respawn-loop when a merge target branch is gone from origin
When an integration (cell/root) branch is deleted from origin — e.g. after a sibling cell->root merge, stranding a late straggler leaf — pr_merge's post-merge _sync_target_branch ran 'git fetch origin <branch>' (check=True) and raised 'couldn't find remote ref'. That surfaced as a retryable SERVICE_ERROR, so complete() re-blocked the task and respawn-looped the PM on an already-landed merge (observed live on cell branches feature/backend/31ae12fc--0e49e04e and 7aeee245--dcfe9fc2, blocking be-pm's complete() 5+ cycles). pr_merge reaches the post-merge sync only after the authoritative GitHub merge has already succeeded (_merge_with_retry raises otherwise), so refreshing the local workspace copy of the target branch is cosmetic. Route it through a new _sync_target_branch_best_effort that logs and returns None instead of raising. merge_pull_request (CEO path) keeps the strict sync — its target is the default branch, which always exists on origin. |
||
|
|
e1651edeb3 |
test(claim): type-clean the agent stand-in so mypy passes
make quality runs 'mypy roboco/ tests/'; the new PM-needs_revision claim test passed a SimpleNamespace where _get_valid_claim_statuses expects AgentTable | None, failing the type gate. The helper only reads agent.role, so cast the lightweight stand-in to AgentTable (no DB row, no type: ignore). |
||
|
|
bdb5e1a93a |
fix(run-hardening): let PMs re-claim needs_revision coordination roots
The lifecycle spec (CLAIM_RULES) grants CELL_PM/MAIN_PM claim of NEEDS_REVISION so a rejected coordination root (pr_fail / qa_fail / ceo_reject) can be re-claimed via i_will_plan and re-delegated. The runtime mapping _ROLE_CLAIM_STATUSES omitted it for PMs, so the spec gate allowed i_will_plan on a needs_revision root while the composed claim() rejected it -> returned None -> INVALID_STATE: the PM could neither plan nor idle its own rejected root and respawn-looped (observed live on cell root 0e49e04e, ~143 INVALID_STATE rejections across 11 PM sessions; the tail of the 2026-06-24/25 run). Add NEEDS_REVISION to the cell_pm/main_pm runtime claim statuses, and a parity test locking _ROLE_CLAIM_STATUSES to lifecycle.CLAIM_RULES so the two can't drift again. |
||
|
|
dfbb8649d0 |
fix(coordination): stop phantom re-delegation from text-vs-id AC-ref mismatch (#259)
A parent's acceptance-criteria coverage is matched by stable criterion id, but a PM may declare covers_parent_criteria on a child by EITHER the criterion's id OR its full text (both happen in practice). _parent_ac_ref_sets unioned the raw refs and matched by id only, so a COMPLETED child that declared coverage by text was invisible to the matcher: the criterion read "uncovered", the roll-up gate refused, and the PM re-delegated the already-finished work as a brand-new empty subtask (0 commits, no PR) that can never close — looping for hours and burning tokens (observed live: a parent's xenon work completed + merged via one child, then re-delegated 2h later as an empty phantom). Normalize every child ref to the criterion id (text -> id via the parent's own criteria) in a small _normalize_ac_refs helper, so coverage counts regardless of how it was declared. Fixes existing mismatched data and future declarations; all three consumers (uncovered/unclaimed/parent_ac_coverage) share the builder. An unknown ref (neither id nor a current criterion text) passes through and matches nothing, exactly as before. Adds two regression tests. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
cfef0f3019 |
[831988ba] Fix PRECONDITION_OWNERSHIP rejection kind in lifecycle spec + update affected test (#256) (#257) (#258)
* [831988ba] fix(lifecycle): add rejection_kind to Precondition, PRECONDITION_OWNERSHIP uses not_authorized Add rejection_kind: RejectionKind = 'tracing_gap' field to the Precondition frozen dataclass. PRECONDITION_OWNERSHIP now carries rejection_kind='not_authorized' so ownership failures surface as authorization issues rather than tracing gaps. Update _check_intent_preconditions to dispatch Decision.reject(kind='not_authorized') when the first failing precondition has rejection_kind='not_authorized' — for all other rejection_kinds the existing Decision.tracing_gap path applies. Update test_can_invoke_intent_open_pr_rejects_non_owner to assert not_authorized instead of tracing_gap (90 parity tests in test_lifecycle_consumer_parity.py now agree: choreographer and spec both return not_authorized for owned=False). All 4871 foundation tests pass, 3264 unit tests pass, ruff/mypy green. * [831988ba] docs(architecture): document preconditions and rejection kinds in lifecycle spec Add comprehensive guide explaining how Precondition rejection_kind field works in the lifecycle spec. Documents the distinction between tracing_gap (missing artifact) and not_authorized (identity/role boundary) rejections, includes the dispatch logic in _check_intent_preconditions, and explains agent-visible impact of the change. This context is essential for agents to understand why PRECONDITION_OWNERSHIP failures now return not_authorized instead of tracing_gap, and when to use each rejection_kind for new preconditions. --------- Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev> Co-authored-by: Backend Documenter <be-doc@agents.roboco.dev> |
||
|
|
88ad03c8cb |
[067ce5d1] fix(runtime): gate all spawns while provider is parked
Generalize the GROK-only _grok_spawn_parked guard to _provider_spawn_parked. spawn_agent now consults the RateLimitStateTracker for every provider, so Anthropic session/overload parking suppresses container launches instead of letting the dispatcher re-spawn every tick. Fail-open on tracker errors. - Rename _grok_spawn_parked -> _provider_spawn_parked (any provider) - Update spawn_agent log + gate - Update grok-rate-limit tests to cover general provider behavior |
||
|
|
75788f519c |
[067ce5d1] fix(runtime): detect session-limit 429 in Claude transcript for provider parking
The SDK server writes runtime output to /tmp/sdk-server.log inside the agent container, so the session-limit markers never appeared in docker logs. Read the newest durable Claude transcript from ~/.claude/projects as a fallback so the provider gets parked and auto-revived instead of crash-retrying. - Add _transcript_tail_text to read the agent's transcript tail - Use it in _provider_rate_limit_park_target alongside docker logs - Add regression test for transcript-only detection |
||
|
|
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> |
||
|
|
4a18dbe4c8 |
fix(git): fetch and track missing parent branch during leaf PR merge
_sync_target_branch previously did a bare 'git checkout <target>' with no fallback. In the shared-agent-clone model the leaf developer's workspace often has only the task branch locally; the parent/cell branch exists only on origin. That produced a SERVICE_ERROR which cycled the task back to blocked every time the PM retried complete(). Now, when checkout fails, we fetch the target branch from origin and create a tracking branch before the pull. Includes regression tests for the local, fallback, and origin-missing paths. |
||
|
|
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>
|
||
|
|
7b0c8291bd |
fix(git): push + open PR on the task branch by name, not the current checkout
A developer's single clone is shared across all of their tasks, so by the QA-submission / open_pr boundary the workspace is usually parked on a LATER task's branch. push_task_branch / push_for_task asserted the current branch and pushed it, and create_pull_request used get_current_branch as the PR head — so for an earlier task the push was rejected (BRANCH_MISMATCH) and the locally-committed work never reached origin, leaving the task branch empty and open_pr failing with GitHub's "No commits between" 422. The work was committed correctly on the local task branch, just never pushed. Operate on the task's recorded branch by name, independent of the checkout: - push() takes an explicit branch and pushes that named ref - push_task_branch / push_for_task push the task's branch by name (drop the assert-on-current-branch gate that rejected the shared-clone case) - create_pull_request uses the task's branch_name as the PR head via the new _pr_head_branch helper Regression tests assert push and PR-head target the task branch from any checkout. 45 git + 735 gateway + 59 git-integration tests green; ruff/mypy/ xenon clean. |
||
|
|
dba4a378ad |
fix(gate): make the work-session invariant fix pass the full gate
Two failures the full make-quality flagged after
|
||
|
|
06adf9782d |
fix(run-hardening): enforce one active work session per task
A task re-claimed by a different agent (pool release, reaper unclaim, escalation redirect) left the prior holder's active work session open. WorkSessionService.get_active_for_task then ran a one-row query over the duplicates and raised MultipleResultsFound; the caught failure surfaced as the cryptic "'NoneType' object has no attribute 'id'" that crashed the claim/plan/start flow — so the task could never advance, the orchestrator re-spawned its PM every ~30s forever, and its dependents stayed blocked. Fixed at three layers: - active-session lookups return the most-recent session instead of raising - claiming a task supersedes any other agent's stale active session (the single-active-per-task invariant), in both WorkSessionService.create and TaskService._create_work_session_if_needed - a partial unique index (migration 047, which de-duplicates existing rows keeping the most recent) enforces it at the DB level; mirrored on the model Verified: 1614 tests green (work_session + gateway + services), ruff/mypy clean, migration chain applies + reverses, dedup proven on the real schema. |
||
|
|
acaf3486e8 |
fix: note-tool timeout (background RAG indexing) + feature-flag raw-key display (#252)
- note timeout: JournalService.add_entry awaited RAG indexing inline (despite its "non-blocking" comment); indexing embeds via Ollama, which is CPU-bound, so under concurrent load it slowed enough to time the `note` gateway tool out. The entry is already committed before indexing, so it's best-effort — schedule it fire-and-forget (_schedule_rag_index) so the write returns immediately. A new drain_rag_index_tasks() helper lets tests await the pending index. - feature flags: the "Gateway-health recovery" toggle rendered its raw key `gateway_health_enabled` (the only flag with no human description). Added the blurb and changed the fallback to render nothing rather than leak a raw key. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
7094c3c171 |
fix(run-hardening): workspace branch-collision + verb-runner None guard (#251)
* 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. * fix(git): reset the dev workspace before a fresh-claim branch checkout A developer's persistent per-dev clone is shared across tasks, so a finished or abandoned prior task can leave it dirty and on a sibling branch. create_branch's checkouts then fail on the dirty tree — and because this git work runs as a side-effect AFTER the claim's DB transition commits, the task is left marked assigned while the workspace stays on the wrong branch, so the dev's next commit is rejected BRANCH_MISMATCH (stalling then blocking the task). reset --hard the tree before the base/feature checkouts. This runs only on a fresh claim (resume short-circuits in _dev_reentry), so discarded changes are abandoned cruft from a finished task — never commits (reset --hard keeps HEAD), never the gitignored .venv. The branch-preservation test invariant is refined to its real intent: a work-carrying branch must never be RE-POINTED (reset --hard <base>); a bare tree-clean reset is allowed. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
d8254300cd |
fix(learning): never broadcast agent learnings to human roles (#250)
Every recorded learning was sent as a knowledge-share notification to all agents, and the recipient query included the human / human-driven roles (CEO, prompter, secretary) — so the CEO's inbox filled with agent learnings. Exclude those roles from the recipient query. The human-role set is resolved from the foundation enum at import (a module constant) so a test that patches the models.base AgentRole alias can't break it. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
40d685bd9f |
fix(run-hardening): park the workforce on a session-limit + PR-review verdict colour (#249)
* fix(orchestrator): park the provider on a Claude session-limit 429, not crash-loop
When the org Claude usage ("5-hour") session limit is hit, an agent container
exits non-zero with a 0-token 429 rejection. The provider-unavailable break only
recognized 5xx overload signatures (529/500/503), so a session-limit crash fell
through to the normal crash-retry path — the orchestrator respawned the agent
straight back into the limit, fleet-wide, until the window reset.
Add a sibling detector _provider_rate_limit_park_target that matches the
session-limit markers ("hit your session limit", "five_hour") in the dead
container's output and parks the provider with kind="rate_limited" (a longer
probe cadence), checked before the overload path in _handle_stopped_container.
Reuses the existing park-and-probe machinery, so the background probe loop
revives the parked tasks when the quota resets — no churn. Gated by the same
overload_break_enabled flag.
Also backfills the CHANGELOG Fixed entry for the orchestrator self-call auth fix
(merged in #248 without one).
* fix(panel): PR Reviewer Notes card colour reflects the verdict
The card was hardcoded teal/green regardless of the review verdict, so a Failed
review sat inside a green card and read as passing at a glance. Derive the card
background from the verdict (red on failed, green on approved/passed, amber on
changes-requested, neutral teal before a verdict) — mirroring the QA Notes card.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
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>
|
||
|
|
07008baaf5 |
fix(api): serialize pr_reviewer_notes/doc_notes/notes_structured in the task response
task_to_response (the builder behind the task list + detail endpoints the panel reads) set dev_notes/qa_notes/quick_context but dropped pr_reviewer_notes, doc_notes, and notes_structured, and TaskResponse didn't declare notes_structured at all — so the PR-reviewer notes, documenter notes, and the structured PR-review verdict were always blank in the panel regardless of what agents persisted. The structured-content write-path + obligation gates were fine; the data just wasn't serialized. Now returns every note section + the structured source of truth. Regression test asserts all three round-trip. |
||
|
|
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. |
||
|
|
a8e5fa6467 |
[ed9c5b59] fix(tests): resolve 3 mypy arg-type errors in test_conventions_resolve using typing.cast (#244) (#245) (#246)
Use cast("ProjectTable", project) at the 3 _resolve() call sites where
SimpleNamespace was passed as a ProjectTable argument, satisfying mypy
without any # type: ignore suppressions. ProjectTable import is kept
under TYPE_CHECKING since the quoted cast form requires no runtime
symbol; cast is imported from typing for the runtime call.
Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
|
||
|
|
fe029fe34b |
fix(self-heal): dispatch fix tasks autonomously instead of stranding them
A self-heal fix task was opened confirmed_by_human=false and held out of dispatch until "Approve & Start" — but that button only renders for a board-reviewed Intake task (pending + board_review_complete + team != main_pm), never for a self-heal task (team=main_pm, no board review). So there was no way to start it: it sat in pending forever and the Main PM never picked it up. Self-heal is RoboCo healing itself, not an Intake draft — it shouldn't need a manual Approve & Start. Origination now opens the fix task confirmed + assigned to the Main PM agent, the PM dispatcher's self-heal hold is dropped, and the now-dead approve_and_start special-case is removed. The fix still ships through the normal gates (dev -> QA -> PR review -> the CEO's merge); the loop never starts, merges, or deploys. |
||
|
|
90946c3abc |
fix(conventions): keep the read clone current on a private repo
The read-clone refresh reused the orchestrator's token-less best-effort fetch, but _clone_repo scrubs the token from the remote URL — so on a PRIVATE repo the refresh fetch failed silently and the clone stayed frozen at clone-time, never seeing commits merged afterwards. The panel then showed "auto-derived defaults" for a project whose .roboco/conventions.yml was already on the default branch. Replace the refresh with a token-authenticated fetch + hard-reset to the default branch (the token is injected transiently into the fetch argv, mirroring the clone; the read clone is orchestrator-side and never mounted into an agent container). A public repo with no token still refreshes unauthenticated. |
||
|
|
17ec52d1b7 |
feat(conventions): generalize defaults, backfill old projects, adopt the standard in-repo
Harden the architectural-conventions standard so it works out-of-the-box on any project and resolves for projects that predate it, and make RoboCo pass its own gate. General defaults (apply to every project, not just one with a tuned file): - The auto-scan excludes test and documentation trees (tests/, docs/) — those legitimately define fixtures and aren't enforced code. - Helper placement seeds at warn, not block: `helper` matches any top-level function, too blunt a signal to hard-block a route file's small private glue. Misplaced model/route/component stay block; the body-level thin_routes check remains the real fat-handler guard. - thin_routes no longer counts transaction-lifecycle calls (commit/flush/ refresh) as data access — an explicit `db.commit()` after delegating to a service is a valid pattern. - no_lint_suppressions exempts a small allowlist of structurally-unavoidable framework codes (ruff TC001-TC003, pydantic prop-decorator); bare or other suppressions still flag. - CLAUDE.md rule-lifting skips bare common-word tokens that would match everywhere (e.g. "commit"), keeping only specific identifiers. - The ambient prompt block lists only constrained modules and truncates at a line boundary with a "+N more" pointer instead of cutting mid-line. Backfill: the standard previously read the committed file + repo scan from project.workspace_path, a field only a manual API call set — so an older project (or one whose workspace was cleared) showed an empty "missing" map no matter what was pushed. The service now ensures a dedicated, default-branch read clone on demand (WorkspaceService.ensure_read_clone) and resolves from it, persisting the resolved path + real HEAD. The panel tab, the spawn-time ambient block, and the per-task constraints all resolve the committed standard with no manual setup. Adopt in-repo: relocate the inline request/response models from the system and *_live route modules into roboco/api/schemas/ so the codebase passes its own placement gate, and ship a canonical .roboco/conventions.yml. no_models_in_routes and modular_cohesion are now clean and enforced at block. Docs updated across the user guide, the agent-facing RAG standard, the developer and pr_reviewer role prompts, CLAUDE.md, and the changelog. New unit tests cover the scan exclusions, helper-warn, the suppression allowlist, the commit exemption, and the resolve/backfill path; the conventions + project integration suites pass against Postgres. |
||
|
|
3a3cd69e1e |
feat(docs): documenter output is committed to the project repo, not just the knowledge store
Documenter docs only ever reached DOCS_BASE_PATH=/app/docs (a host-mounted, RAG-indexed knowledge store) and were never committed to the project's git repository — so the documenter's deliverable never landed in the repo. write_doc now also writes the doc into the agent's workspace clone under docs/<type>/<file> and commits it onto the task branch via GitService.commit, so it rides the existing PR into the repository on merge. Best-effort: a documenter without a cloned workspace or task branch still succeeds (logged, never fatal); the /app/docs knowledge store + RAG indexing are unchanged. Adds tests that the doc is written into the workspace and committed onto the task branch, and that it no-ops cleanly without a branch. |
||
|
|
2bbd1c2e70 |
test(conventions): baseline asserts the universal hygiene block rule, not a placement rule
The baseline-constraint and conventions-map integration tests asserted 'no models in routers' — the old UNIVERSAL block default. Now that placement rules are scan-derived (only seeded where the target module exists), an empty-scan test project carries no_models_in_routers no longer; the universal block rule is no_lint_suppressions. Updated the five assertions accordingly; behaviour (baseline attaches, isn't suppressed, is idempotent) is unchanged. |
||
|
|
4a8c508f91 |
feat(conventions): modularity checks + scan-derived, language-aware rules
The standard was architectural LINTING (placement + hygiene) — things ruff/eslint already do — and it forced backend rules onto frontend projects. This makes it enforce MODULARIZATION, the separation-of-concerns a senior demands that linters are blind to: - modular_cohesion: a file that mixes architectural concerns (a model defined in a router, a schema in a component) is a monolith — split it. One concern per file. - thin_routes (Python): a route handler that runs its own DB access instead of delegating to a service. - thin_components (TypeScript/React): a component that fetches data in its body instead of using a hook. - god_class: a class past a method-count threshold (single responsibility). The checks inspect a definition's BODY and a file's COMPOSITION via tree-sitter, precision-over-recall (fire only on a confident structural signal). Rules are now scan-derived and language-aware: hygiene seeds universally, placement only for modules that exist, and modularity per stack — so a frontend project carries no_models_in_components + thin_components, never a backend no_models_in_routers. BUILTIN_RULES is reduced to language-agnostic hygiene. |
||
|
|
fdfdc23b72 |
fix(orchestrator): give agent gate containers the test-DB env so the suite runs DB-backed
An agent's make quality runs against no Postgres, so the conftest skips every integration test and coverage collapses far below the 80% threshold — a self-hosted PM read 71% on a suite that is ~96% with a DB and chased it as a code regression. _append_gate_env now injects ROBOCO_TEST_DB_* (host/port/user/password/admin-db) from the orchestrator's own DB settings into each spawn; agents share the Docker network so the host resolves, and the conftest creates throwaway test databases isolated from the live one. The app runtime reads ROBOCO_DATABASE_*, never ROBOCO_TEST_DB_*, so this only feeds the test harness. Gated on toolchain_match_enabled, the faithful-gate flag. |
||
|
|
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. |
||
|
|
02f43f5dfe |
fix(tests): make crypto tests hermetic with a self-supplied Fernet key
test_crypto's round-trip tests called the real encrypt/decrypt path, which needs settings.encryption_key configured — so they silently depended on ROBOCO_ENCRYPTION_KEY being present in the environment. In agent gate containers it is not, so four tests failed there and the agent mis-read it as a code regression. An autouse fixture now monkeypatches a valid generated Fernet key, so the tests pass without any ambient secret and agents never need the production key injected to gate. |
||
|
|
c49dcebae4 |
fix(toolchain): raise requires-python floor to 3.13 so the resolver matches the runtime
RoboCo's code imports tomllib (3.11+) and the stack runs on 3.13, but requires-python declared >=3.10. The toolchain resolver picks the lowest satisfying version, so it provisioned agent workspaces of the self-hosted roboco-api project at Python 3.10 — an interpreter the suite cannot even be collected under, leaving the workspace .venv unusable and the gate running in an ad-hoc fallback env. Raising the floor to >=3.13 makes resolve_target_python return 3.13, matching the agent image. Re-locks to drop the now-unreachable 3.10-3.12 backports; a guard test pins the repo's own resolution to 3.13. |
||
|
|
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> |
||
|
|
01e10ad693 |
Feat/provider overload break (#242)
* feat(conventions): standard schema models + effective-map merge * feat(orchestrator): park provider on persistent server overload (529/500) A 429 rate limit already parks a provider — queue its spawns, probe until it recovers — but a persistent 529/500/503 overload had no such break: the run died and the orchestrator crash-retried straight back into the overload, burning tokens in a respawn loop. Generalize the park to provider-unavailability. On a non-graceful Anthropic agent exit, match the API's overload markers (overloaded_error / internal_server_error / "API Error: 5xx") against the dead container's own output and park the provider with kind="overloaded"; the existing spawn gate already queues any parked provider, and the probe-resume loop revives the task when it recovers. Grok keeps its exit-75 path; both now route through one _park_provider_unavailable helper. Markers are kept specific so an agent that merely writes about HTTP 500/529 can't trip the break. Fix the recovery probe to require a 2xx: it treated any non-429 as recovered, so a probe that itself got a 529 would have resumed agents straight back into the overload — wrong for the new path and for a 429 that lifts into a 5xx. Gated by ROBOCO_OVERLOAD_BREAK_ENABLED (default on; off => crash-retry). --------- 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. |
||
|
|
78c6981089 |
test(toolchain): end-to-end interpreter-match + loud-fail guard
Ties the pieces at the logic level: a target whose .python-version (3.13) conflicts with requires-python (>=3.14) provisions against 3.14, a collection error records 'broken', and the gate guard then refuses the pass. Flag-off provisions exactly as today and never blocks. |
||
|
|
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. |
||
|
|
036416878d |
feat(toolchain): provision against the target interpreter + runnability smoke
When toolchain matching is enabled, install_dev_deps resolves the target's
Python and provisions the venv against it (uv sync --extra dev --python <v>,
uv auto-downloads), then runs a runnability smoke (uv run pytest --collect-only)
and records {python, status} in a .git/.roboco-toolchain marker the gates read.
Precision over recall: only a pytest collection error (the interpreter-mismatch
signature) reports 'broken'; pytest-absent / timeout report 'unknown' so a gate
never blocks on an inconclusive smoke. Flag off => provisioning is unchanged.
|
||
|
|
6df083c594 |
feat(toolchain): flag + WorkSession toolchain columns + migration
ROBOCO_TOOLCHAIN_MATCH_ENABLED (default-off) gates the whole subsystem. Adds work_sessions.toolchain_python / toolchain_status (nullable VARCHAR(20)) recording the interpreter the workspace was provisioned with and whether the target's suite can be executed (ok | broken | unknown). Migration 042 verified with a real upgrade head + downgrade -1 + re-upgrade on a throwaway Postgres. |
||
|
|
9274cd4584 |
feat(toolchain): resolve target interpreter from requires-python
Pure resolver (services/toolchain.py) that derives the Python version an agent should provision a target workspace with. Defends against uv's resolution order — a .python-version file overrides requires-python during interpreter selection — by honoring the pin only when it satisfies requires-python, else resolving a concrete version from requires-python for the caller to pass via --python. This is the root cause behind the live guard-core-app failure (pin 3.13 vs packages needing 3.14). Promotes packaging to a direct dependency. |
||
|
|
8cf697816f |
feat(content): obligate role note sections like journals
Completes the note(scope='handoff') write-path (WIP
|
||
|
|
2aa00ca124 |
test(content): assert transition notes via markers, not quick_context
Three integration tests still asserted the old quick_context soup format for completion_notes / escalation_notes — update them to read the new orchestration_markers transition_note (the source moved in the prior commit). |
||
|
|
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). |