mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* release-manager: fencing-token mutex + executor/readiness hardening
Closes the release-mutex TTL race (#17, HIGH) and the remaining
release-manager gaps (#88, #89, #201, #202):
- #17: the release mutex is now acquired with a uuid4 fencing token and
released via Lua compare-and-del; a background asyncio heartbeat
compare-and-expires the TTL ~every 60s while the execute owns the lock,
so a live execute no longer expires and a crashed one auto-releases
<=3000s. A second approve after TTL expiry cannot usurp and rm -rf the
in-flight clone — the fenced first-finally keeps its lock.
- #89: a Redis outage during acquire stays fail-closed (the execute never
runs) but now returns a distinct redis_unavailable result + log so the
CEO sees the cause instead of a false already_in_progress.
- #88: commit_and_push RuntimeError is wrapped into a structured
ReleaseResult(commit_failed) instead of a 500.
- #201: first-release fallback still emits untracked version-ref files as
gaps (no longer silenced by the first-release branch).
- #202: _await_proc awaits proc.wait() after kill() so a timeout cannot
leak a zombie.
TDD: tests/unit/services/test_release_proposal_concurrency.py extends
_FakeRedis with eval/get/expire and pins the fencing/heartbeat/usurper
invariants + the redis_unavailable result.
* PM/code-task creation guard + main_pm coverage + issue carve-out
Closes the creation-time role x task_type gap (the user's explicit example)
and the main_pm delegate hole:
- New pure helper `pm_cannot_own_code(role, task_type, is_issue_resolution)`
in foundation/policy/batch.py — single source of truth. Both PM roles
(cell_pm + main_pm) coordinate; a `code` task assigned/claimed by a PM is
a structural mismatch, EXCEPT a PM taking a code task in needs_revision to
resolve review/QA issues directly (the carve-out).
- Creation-time guard: TaskService.create calls the helper (closes the
create-with-cell-PM-assignee hole the team-based check misses).
- Delegate path + spec claim gate consult the same helper.
`_validate_assignee_task_type` / `_task_type_hint_for` now key on the
Role (CELL_PM OR MAIN_PM), not the cell-PM slug set — closes the
delegate-to-main-pm-as-code hole.
- identity.role_for_uuid_or_none is None-tolerant (treats None as "not a
PM" and proceeds) so a malformed/missing assignee cannot crash the guard.
- prompter.create_task_from_draft reuses the guard at draft-create.
TDD: test_batch.py (helper matrix + carve-out), test_main_pm_code_guard.py
(main_pm coverage), test_delegate_assignee_task_type.py (delegate parity),
test_lifecycle_spec.py (claim gate: rejects PM claiming code from pending,
allows from needs_revision + PM claiming planning + dev claiming code).
* task-service: completion hooks + escalation/cancel/audit hardening
Closes the task-service cluster (#21/#98, #99, #100, #101, #103, #216; #102
verified already-covered, #217 verified already-guarded):
- #21/#98: ceo_approve now closes the work session + triggers completion
hooks before worktree removal (no-op when work_session_id is None), so a
CEO-approved task lands the same close-path as PM-completed.
- #99: apply_escalation routes through the transition validator with an
enumerated escalation exemption (_ESCALATABLE_TO_BLOCKED) instead of an
arbitrary source->BLOCKED write; BACKLOG is refused.
- #100: branchless ceo_reject awaiting_ceo_approval->pending gets a real
spec edge (ceo_reject_to_pool ActionSpec + _STATUS_TRANSITIONS entry) so
future admin-override tightening can't wedge the path.
- #101: revision_count bump is documented as the single chokepoint, with
the pre-block RESTORE path undoing it when restoring a snapshotted
needs_revision (same cycle resuming, not a new rejection).
- #103: cancel cascade surfaces non-terminal orphans instead of swallowing
the role violation.
- #216: _remove_task_worktree_on_terminal escalates recurring FS/permission
failure (audit/notify after N) instead of silent-failing forever.
- #102: pinned in test_verb_runner_midverb_invalid_state.py (committed with
the choreographer cluster) — verb-runner savepoints already surface a
concurrent mid-verb state change as INVALID_STATE.
- #217: submit_for_qa claimed_by guard verified intact.
TDD: test_task.py, test_worktree_cleanup_on_complete.py,
test_escalation_board_guard.py (#99), test_task_service_* integration,
test_lifecycle_spec.py.
* choreographer: gate-claim guards + pr-gate hardening + fail-open logging
Closes the choreographer cluster (#5/#222, #29, #30, #82, #188, #189,
#192; #157/#187 verified already-fixed/pinned; #102 pin lives here):
- #5/#222: the unchanged-PR guard's fail-open head_sha lookup now logs
(warning) on a slug-resolver/git-helper error so a regression cannot
silently turn the pr_fail re-submit loop-stopper into a no-op. Stays
fail-open (never wedges the PM).
- #29: pinned (REFUTED-with-pin) — _lane_claim_guard already returns the
error envelope without releasing the claim on a transient lookup error.
- #30: pinned (REFUTED-with-negative-pin) — a non-batch branchless main_pm
root cannot bypass the complete spec gate (is_batch_umbrella requires
batch_id set).
- #82: _post_gate_review_to_pr wraps the slug-resolution call in try/except
(mirrors _capture_pr_head_sha) so a malformed cell_map AttributeError no
longer 500s the reviewer after a committed gate transition.
- #188: _is_hand_formatted_verdict anchors the header regex to line-start,
so a quoted (> ## Summary) or inline (mid-prose) header mention no longer
false-refuses a hand-formatted verdict.
- #189: pr_fail re-captures the PR head SHA after the transition commits and
re-stamps the verdict note only when it advanced (closes the stale-SHA
false-allow loop-hole); no-advance stays a single note write.
- #192: claim_gate_review skips the dev claim guards (already_active/paused/
lane) via a new skip_dev_guards param — a pr_reviewer inspecting an
assembled PR does not start work, so the single-active-task / code-lane
invariants do not apply; the dependency guard is kept, and QA's
claim_review parity is preserved.
- #157/#187: verified in tree — pr_review-only handoff is intentionally
prior-work-worth-resuming; self_review_block wiring (reviewer != dev)
holds on assembled tasks with 4 existing pin tests.
TDD: test_choreographer_*, test_pr_gate_posts_review (#82),
test_pr_review_hand_format_guard (#188), test_submit_root_unchanged_pr_guard
(#189), test_claim_gate_review_guards (#192), test_verb_runner_midverb
_invalid_state (#102 pin).
* playbook curate: guard the gating commit against a poisoned session (#55)
The explicit `session.commit()` that gates the RAG index (commit-before-index
so an uncommitted playbook cannot land in the corpus) raised PendingRollbackError
when a prior mid-verb failure had rolled the caller's session back — 500ing the
whole curation verb instead of returning a clean envelope, and (worse) risking a
fall-through to index an uncommitted playbook. Wrap the commit: on
PendingRollbackError, log + return invalid_state with a re-fetch/retry remediate
and skip the index. The happy path still commits exactly once then indexes.
TDD: test_playbook_verbs.py — poisoned-session returns a clean invalid_state and
does NOT index; clean-session still commits once + indexes (pins no fail-closed
inversion / no double-commit).
* [chore] gateway: atomic activate merge — preserve probe_failures across re-park (#156)
activate() was a blind SET that reset probe_failures to 0, so a probe-failure
increment that just landed (or was in flight) could be wiped by a concurrent
re-park — resetting the give-up / CEO-notify count mid-episode. Route activate
through a server-side Lua merge (roboco:activate_rate_limit) that refreshes the
episode metadata (kind / activated_at / retry_after / affected_agents) while
carrying over the previous probe_failures count. Indivisible w.r.t. the
increment/reset scripts (Redis single-threads an EVAL).
#56 (notify to prompter/secretary refused) verified SAFE — the pin tests
(test_notify_rejects_prompter_recipient / _secretary_recipient /
_allows_ceo_recipient) already cover the only human notify target invariant;
no legitimate send is dropped, no code change.
* [chore] foundation/policy: spec gates + QA retry-key pin (Cluster F)
#50 sync_branch composes=() so the spec gate accepted a terminal/paused/
blocked task and the handler rebased a dead/parked branch — add a
PRECONDITION_SYNC_BRANCH_STATE (claimed/in_progress/verifying/needs_revision
only), rejection_kind=invalid_state. TDD: 28 spec tests.
#148 submit_root's prose asserts 'a Main-PM root is planning-typed, never
code' but only the creation path (main_pm_cannot_own_code) backed it — add
PRECONDITION_ROOT_NOT_CODE on the submit_root IntentSpec (defense in depth),
scoped to submit_root only so the shared submit_for_review action keeps
cell_pm+code submit_up parity. Graceful on Mock/None task_type so the
choreographer Mock-task tests don't crash. TDD: 2 spec tests.
#150 VERB_RETRY_LIMITS is keyed by the MCP-exposed names (pass/fail), not
the IntentSpec-internal pass_review/fail_review — already correct; add a
pin test so a one-sided rename can't silently drop the QA-handoff cap.
#142 main_pm_cannot_own_code/pm_cannot_own_code already normalize casing
(.lower()) — no-op, pin test test_main_pm_cannot_own_code_is_case_insensitive
already in tree.
* [chore] worksession-git: 405 merge-method fallback + non-destructive close (Cluster W)
#108 _merge_with_retry hardcoded 'squash' and raised MergeConflictError on a
405 with no method fallback — wedging the PM on an open, mergeable PR whose
repo merely had the squash button off. Add a 405 fallback to a permitted
method (via _first_allowed_merge_method, exclude='squash'), mirroring the CEO
merge_pull_request path. A 405 with no permitted fallback (or a second 405)
still falls through to the already-merged disambiguation / MergeConflictError.
TDD: 2 new tests (fallback-success, no-permitted-method-raises).
#109 close_pull_request defaulted delete_branch=True, so the choreographer
supersede path deleted a superseded PR's branch while the orchestrator
supersede path explicitly preserved it — the two disagreed, and the
destructive default ran on the 'close the dead PR' path where the branch may
still be referenced / useful for audit. Flip the default to False (opt-in
deletion) and make the choreographer caller explicit (parity with the
orchestrator). TDD: 1 new test (default preserves branch); existing
deletion-when-requested test now passes delete_branch=True explicitly.
Dispositions verified against current code (no silent drops):
- #27 REFUTED/FIXED-UNDEPLOYED: work_session.merge_pr resolves by session_id
(no global pr_number lookup); the real cross-repo collision fix
(project_id scoping on pr_merge/close_pull_request/rebase_pr_for_task/
pr_target) is already in tree + tested (test_pr_merge_scopes_task_lookup_
by_project_id, test_close_pull_request_scopes_task_lookup_by_project_id,
test_git_pr_target_scoping). Verify-only.
- #106 REFUTED: a guard exists (rev-list --count {base_ref}..{branch} == 0)
before reset --hard + base_ref falls back to default_branch; tests lock
the safety (test_create_branch_never_repoints_branch_with_real_work,
test_create_branch_does_not_reset_or_checkout_shared_clone).
- #218 BY-DESIGN: the merge_pr idempotent guard intentionally preserves the
audit trail (docstring + test_merge_pr_idempotent_on_already_completed_
preserves_audit_trail); a COMPLETED session always carries attribution
(COMPLETED only via merge_pr), so the NULL-COMPLETED case is unreachable.
- #104 BY-DESIGN: agents never merge to the repo default branch in RoboCo's
model (root→master is CEO-only); the guard is a correct CEO-only rail,
locked by test_pr_merge_into_default_branch_is_ceo_only.
* [chore] llm: surface disabled-provider downgrade + scrub probe log (#20/#3/#211)
#20/#3 resolve_for_agent silently fell through to the legacy Anthropic path
when a configured provider was disabled — indistinguishable from 'no
assignment', so the operator got no signal that spawns bypassed the
provider. Surface the bypass with a warning (graceful degradation stays the
default — a stalled spawn is worse than a routing miss) and add an opt-in
ROBOCO_ROUTING_STRICT (default-off) that fail-closes instead. Wired into the
panel Feature Flags card. TDD: 3 unit tests (warn-on-disabled, strict-raises,
no-assignment-stays-silent).
#211 probe_ollama_tags logged str(exc) raw on the generic-exception branch —
structured log could carry connection internals / stack traces. Log the
exception class name only. TDD: existing generic-branch test strengthened to
assert the log kwargs don't leak the raw text.
* [chore] support/stream/optimal/playbook/comms hardening (Cluster S)
Logical-gaps sweep, Cluster S (TDD, red→green per item):
#64 notification_delivery.acknowledge published the NOTIFICATION_ACKED bus
event directly (bypassing the outbox) — a rollback left a phantom ACK. Route
it through defer_bus_publish (after_commit), mirroring deliver.
#76 playbook.archive()/reject() stamped the archiver into approved_by/
approved_at, overwriting approval provenance (and fabricating approval for a
rejected draft). Add archived_by/archived_at (migration 053 + table + model)
and write those on archive/reject, leaving approval attribution intact.
#181 vector_store.replace_chunks wiped existing index rows even when every
chunk lacked an embedding (embedder failure). Skip the wipe when chunks is
non-empty but records is empty — preserve good rows for nothing.
#182/#183 optimal.record_learning recomputed a learn-{md5(full_content)}
tracking source that never matched the URI the plugin embedded chunks under
(roboco://learnings/{doc_id}, doc_id=lrn-{hash100}). Use the plugin's
returned doc_id so de-index/lookup-by-source finds the chunk rows.
#96/#97 transcription periodic flush only peeked ready buffers (unbounded
map growth) and ran sync callbacks on the event loop (a slow callback
blocked the flush task). Flush (remove) each ready buffer after notifying,
and offload each callback to a thread.
#212 _TEAM_SCOPED_ROLES was duplicated across communications/agents_config/
seeds. Single-source it in foundation.policy.communications; consumers
reference that object (identity-tested).
#19 stream_bus._dispatch_event re-ran already-succeeded handlers on a
recover_pending replay (duplicate side effects). Add a per-(event.id,
handler) SET-NX idempotency guard: skip on a hit, clear the key on handler
failure so a replay re-runs it, fail-open when redis is unavailable.
Dispositions (no code change): #77 approve() index-write pair asserted
BY-DESIGN; #62/#63 notification DB-dedup verified pinned; #184/#185 REFUTED;
#214 REFUTED; #215 BY-DESIGN.
* [chore] db/migrations: graph-integrity guard + conftest unreachable-DB warning (Cluster D)
Logical-gaps sweep, Cluster D (TDD + real alembic upgrade head verification):
#16/#37 add tests/unit/test_migration_graph_integrity.py — a static guard that
the alembic migration graph has exactly one head, every down_revision resolves,
every revision is reachable from a root, and no revision id is duplicated. The
suite builds its DB via Base.metadata.create_all (not alembic upgrade head), so
a forked head / dangling down_revision / duplicate id would otherwise ship
silently and break a real deploy mid-stream.
Caught a real bug in the process: migration 053's revision id
"053_playbook_archived_attribution" (33 chars) exceeded alembic's
alembic_version.version_num VARCHAR(32) — a fresh `alembic upgrade head` raised
"value too long for type character varying(32)" at the 053 stamp. Renamed to
"053_playbook_archived_attr" (26 chars). Verified end-to-end on a scratch PG:
upgrade head stamps 053, downgrade -1 returns to 052. (The pre-existing
test_every_migration_revision_id_fits_the_alembic_version_column guard is now
green too; it had been red on the 33-char id.)
#90 conftest silently pytest.skip'd every DB test when Postgres was unreachable
— a non-Docker box reported a green run of all-skips. Extract the warning into
_warn_if_pg_unavailable and fire it at import so the operator sees the DB is
down (the per-test skip path is unchanged). Test: warns when unavailable, silent
when reachable (verified under -W error::UserWarning).
Dispositions (verified against real code + a fresh alembic upgrade head, no code
change): #6 REFUTED — sa.Enum(create_type=False) at 001:119/304 does NOT break a
fresh upgrade head (001→052 applied cleanly on a scratch DB); #8 REFUTED — the
upgrade passed 030/031 (RAG chunk tables) without pgvector installed; pgvector is
a runtime concern handled by roboco/db/base.py, not a migration prerequisite;
#40 REFUTED — the `|| echo` mask was already removed and partial-schema drift
reports exit 1 (only by-design unreachable/unmigrated skips remain); #204 REFUTED
— the property walk seed IS pinned (random.Random(20260504), line 97); #205/#206
REFUTED — the smoke-trace fixture IS wired via
test_lifecycle_smoke_replay.py (8 passed); no shell smoke scripts exist in the
tree to wire; #137 BY-DESIGN — pyproject version 0.14.0 is an operational note,
no code gate.
* [chore] panel: admin-override force flag + kanban subtask_count + ws cleanup + ui-store dedupe (Cluster P)
#13: kanban admin-override into a hatch state (completed / awaiting_qa /
awaiting_pm_review) now requires an explicit force=true from the panel and
emits a dedicated task.admin_override audit row server-side; non-hatch
overrides need no force. Backend gate in tasks route + admin_set_status;
panel kanban-board sends force for hatch targets; TaskUpdate carries force.
#198: kanban service threads the real subtask_count (one grouped query) into
dev + priority-swimlane + main-pm-flat boards instead of a hardcoded 0.
#79: useWebSocket cleanup clears messages/lastMessage/state on unmount or
endpoint change so a dep-change (navigating to another stream) can't leak the
prior subscription's stale snapshot as live.
#186: disambiguate the duplicate ui-store modules -- the session/scroll store
in lib/stores renamed to useScrollRestorationStore / scroll-restoration-store
(barrel + 2 consumers updated); the sidebar/theme useUIStore in @/store is now
the sole useUIStore.
#12: verified already in-tree (release-proposal-card surfaces non-404 errors
with retry; getProposal maps only 404->null). #80 by-design (handleTransportError
already resets isSending on a no-payload SSE drop). #81 docs (streamUrl docstring
records that live-intake SSE auth is session-id-based bearer-style).
Backend: ruff+mypy clean, 278 tests green. Panel: lint+typecheck clean, 159 tests.
* orchestrator: park/reaper/readopt/a2a hardening + self-heal/ci-watch dedupe (Cluster O)
Closes the orchestrator-side logical gaps from the sweep:
- #75 a2a human-only drop surfaced: _dispatch_a2a_work logs the skip
("a2a request targets a human-only role; left as a notification for the
human (not spawned)") instead of silently dropping the target — the
CEO/secretary/prompter still see the notification; only the spawn is
suppressed. (orchestrator.py)
- #72 readopt liveness: _readopt_running_agents requires a non-stale live
claim (via _agent_holds_live_claim) and skips a zombie container so a
reaped-but-restart-readopted agent isn't double-counted as active.
- #74 shutdown drain: stop() calls _flush_respawn_tracker so the durable
respawn counter write-throughs aren't lost on a clean stop.
- #71 resolve_wait active-guard + deferred liveness: a rate_limit_lifted
WaitingRecord is only confirmed-live after a _confirm_resume_liveness
probe (deferred deletion _resume_confirm_delay=30.0), and an
already-active agent short-circuits the repark. Scoped to
rate_limit_lifted records (the only ones at risk of a false lift).
- #73 stuck-Claude kill: _maybe_kill_stuck_claude + _claude_stuck_kill_ttl
(config.claude_stuck_kill_seconds) — a live container whose heartbeat is
stale past the grace AND whose gateway probe is broken is killed+evicted,
not protected forever by the reaper's live-skip.
- #230 verified FIXED-UNDEPLOYED: _gateway_broken_past_grace already
requires N consecutive false-broken probes (not one flaky streak); no
change, test added to pin the N-consecutive invariant.
- #43 self-heal per-observation dedupe: a fingerprint collapses repeat
CEO notifications for the same CI regression.
- #44 ci_watch dedupe by (git_url, workflow): a monorepo's multiple
workflows each get their own fix task (was collapsed by git_url alone).
- #49 identity.role_for_slug_or_none None-hardening: a stale/malformed
slug resolves to None and the human-only skip falls through to the safe
"not spawnable" path instead of crashing.
- #193 strategy engine: notify the CEO on a persistent assess failure
instead of failing silently in the background loop.
TDD: test_no_spawn_human_roles (a2a skip surfaced), test_orchestrator_
shutdown_drain (#74), test_provider_overload_break (#71), test_readopt_
running_agents (#72), test_resolve_wait_repark (#71), test_stale_claim_
reaper (#73/#230), test_strategy_engine_loop (#193, new),
test_self_heal_engine (#43), test_ci_watch_engine (#44), test_identity
(#49). All red->green.
* chore: make-quality green — xenon complexity refactors + mypy test fixes + lifecycle regen
No behavior changes. Brings the tree to a fully green `make quality` (the
base branch never passed the xenon B-rank gate on several blocks; the
lifecycle artifacts had drifted from the committed ceo_reject_to_pool edge).
Xenon B-rank refactors (extract a helper; preserve semantics exactly):
- api/routes/tasks.py: _apply_forced_status_override + _StatusOverride
dataclass bundle (update_task override block).
- services/task.py: _enforce_no_pm_code_on_create (create guards) +
_escalation_diverts_to_pool (collapses the two board/advisory +
main_pm+code divert branches into one predicate).
- services/prompter.py: _coerce_pm_code_to_planning (create_task_from_draft).
- services/notification.py: _duplicate_unacked_exists (_create_notification
purpose-based dedup query + ACK_REQUIRED_BY_TYPE gate).
- services/sequencing.py: _same_assignee_lane_edges (the undeclared-surface
same-assignee lane fallback at the tail of dev_task_collision_edges).
- gateway/choreographer/_impl.py: _pm_task_type_error static helper
(_validate_assignee_task_type compound PM guard).
- gateway/choreographer/pr_gate.py: _gate_review_event_verdict +
_gate_review_body static helpers (_post_gate_review_to_pr).
mypy test fixes (no type:ignore — banned; use typing.cast with quoted
strings per TC006):
- test_task_update_completeness: TaskUpdate(acceptance_criteria=None).
- test_bus: cast("Redis", _FakeRedis()); Redis import under TYPE_CHECKING.
- test_pr_merge_concurrency: capture AsyncMocks into locals before asserting.
- test_notification_delivery_phantom: cast("UUID", to_agents[0]).
Lifecycle artifact regen (owed from Cluster T #100 — the
awaiting_ceo_approval -> pending `ceo_reject_to_pool` edge was added to the
spec in 3d633084 without regenerating the derived artifacts the
foundation-check gate diffs against): docs/rag/lifecycle/intent-verbs.md,
docs/rag/lifecycle/status-transitions.md, panel/lib/lifecycle.json.
services/kanban.py: ruff format only (collapses the _load_subtask_counts
signature that drifted unformatted from Cluster P).
* [chore] logical-gaps sweep — Cluster I (intake/product/pitch)
#57/#58 prompter: preserve a top-level product_id with a 1-cell map
(prompter.py create_task_from_draft — top-level target wins over a
redundant 1-cell map instead of dropping product_id); reject — not
silently skip — a malformed project_id in the_work cell entries
(prompter.py _draft_cell_map raises ValidationError).
#59/#159 prompter: create_task_from_draft now operates on a copy
(_copy_draft) so _validate_and_coerce_draft / _clean_list never mutate
the caller's draft dict.
#160 prompter: _resolve_owning_team consults product/board routing
before forcing MAIN_PM on a multi-cell map (product root stays Board,
product+assignee-is-board stays Board).
#83/#84 github_provisioning: create_repo is idempotent by GitHub name
— a 422 "name already exists" (orphaned repo from a rolled-back prior
approval) is fetched and reused instead of erroring; pitch re-approval
now reuses the orphaned repo end-to-end.
#196 kanban: flat main-PM board has a "coordination" column for
non-cell teams (MAIN_PM/Board) instead of dropping their cards.
#197 project update: an explicit null in the PATCH body now clears the
stored field, distinct from an absent field (leave unchanged).
ProjectService.update drops exclude_none so explicit-None applies; the
PATCH route uses ProjectUpdate.model_validate(data.model_dump(
exclude_unset=True)) to preserve the request's unset-tracking (the old
field-by-field construction marked every field set and defeated the
distinction — nulling NOT-NULL git_url).
TDD: prompter 47, github_provisioning+pitch 16, kanban+project 67,
project routes 37 — all green; ruff + mypy clean.
* [chore] logical-gaps sweep — Cluster M (mcp-servers)
#60 flow_server/do_server: the circuit-breaker substitution no longer
erases the fixable rejection — the original envelope (kind/message/
remediate) is nested as inner on a copy of the SDK's circuit_open
envelope (the SDK dict is not mutated in place). The agent still sees
WHY the verb failed, not just that the breaker tripped.
#61 flow_server/do_server: a 404 carrying a *descriptive* detail
(not FastAPI's bare default {"detail":"Not Found"}) is now a
real resource not_found, surfaced as not_found so the agent
re-fetches state — instead of a misleading "server-side wiring gap"
invalid_state. The bare default and unparseable 404s still synthesize
the wiring-gap envelope; a 404 with a real Envelope (error field)
is still surfaced as-is.
#161 flow_server/do_server: dict error.code classification now uses
an exact-code map (authoritative for the codes the handlers emit) with
a substring fallback for unknown codes. Fixes the real regression:
AUTHENTICATION_REQUIRED carries no AUTHORIZED/DENIED/PERMISSION
substring, so the old substring-only rule dropped it to invalid_state
instead of not_authorized — an auth storm attributed as a state storm.
The fallback also adds AUTH so future AUTH-prefixed codes classify.
#162 flow_server/do_server: _register_tools gains a
ROBOCO_ALLOW_FULL_TOOLSET env override (default-off) so a missing
manifest falls back to the full tool set instead of raising — a
dev/test escape hatch. Production fail-loud behaviour is unchanged.
#163 intake_server: propose_batch accepts name as well as
title (intake drafts in the wild have used both), normalizing a
name-only draft onto a copy as title (caller's dict never mutated),
and reports the dropped count + reason in the return instead of
silently vanishing malformed drafts. The empty-batch hint now names
name as an alternative.
TDD: 123 mcp_servers tests green (14 new + 2 updated); ruff + mypy clean.
* [chore] Cluster N — conventions/docs logical-gaps sweep
#33: _create_new_doc/_update_existing_doc now resolve via
_resolve_contained_path (the RAG-returned update path was not containment-
checked — an escaping source could write/overwrite outside the docs dir).
#34: _commit_doc_to_repo returns committed/skipped/failed instead of
swallowing all exceptions; surfaced on DocRef.commit_status, the write
response, and the docs MCP guidance so a failed repo commit is fail-loud.
#35: write_doc only updates the similar doc when its filename matches — a
different filename creates a new file instead of collapsing onto the
similar doc's path (the dedup-overwrite defect codified by the old tests).
#129: a custom rule scoped to a language the validator never reports (a
typo) is surfaced as a warn finding on .roboco/conventions.yml via the
runner's once-per-run validation; #32 (tsx->typescript dialect) stays
BY-DESIGN.
#130: _cache_put only swallows a UNIQUE violation (23505) as a concurrent
duplicate; a non-unique IntegrityError (FK/NOT NULL/check) is log-errored
and re-raised instead of being silently misattributed.
#132: health re-reads the live file status (a cached degraded row hid an
in-place repair at a stale head key); get_map skips cached degraded rows
and stops caching degraded so a repaired file re-derives. #134 BY-DESIGN.
#133: _DB_METHODS gains stream/stream_scalars (SQLAlchemy 2.0 streaming
constructs are data access too — a route calling them is not thin).
#199: regenerate_verb_tables._annot_str strips Annotated[...] metadata
(BeforeValidator) before rendering; regenerated verbs.md + per-role
prompts so the BeforeValidator(func=...) repr (with a memory address) no
longer leaks into agent-facing prompt text.
TDD: 206 conventions/docs tests green (incl. 5 new files / appended
cases); ruff + mypy clean.
* [chore] Cluster 16 — cross-cutting hygiene logical-gaps sweep
Disposition + fix the 10 cross-cutting-hygiene gaps, TDD. make quality green
(ruff, mypy 944 files, pytest, xenon, vulture, foundation-check, enum-parity).
FIX:
- #24 /ws/system now gated by _require_panel_token (matches every sibling
/ws/* stream); rejects a missing token in strict mode and a forged token
even in dev. (roboco/api/websocket.py)
- #25 two drifted _require_ceo implementations (orchestrator router vs release
handler) unified on a single require_ceo_role helper in deps — same 403,
same role set, accepts Role/AgentRole/"ceo". (roboco/api/deps.py,
routes/orchestrator.py, routes/release.py)
- #11 a spawn session for a delivery role (developer/qa/documenter) with no
task_id now logs an unattributed-usage warning via is_unattributed_delivery_spawn.
(roboco/runtime/orchestrator.py)
- #65 pricing returns a structured CostResult(cost_usd, unpriced, is_anthropic)
so an unpriced Anthropic model (real spend we'd undercount) is flagged
instead of silently $0; calculate_cost stays a thin float wrapper.
(roboco/billing/pricing.py, billing/__init__.py)
- #67 blocker-metrics "blocked since" reads the task.blocked audit transition
(indexed on target_id/event_type/timestamp), not updated_at — which
over-counted when a blocked task was touched for a non-blocking reason.
Falls back to updated_at/created_at only with no audit row.
(roboco/services/metrics.py)
- #94 grok refresh_if_stale uses double-checked locking (_refresh_lock +
_recheck_or_refresh) so two concurrent callers don't both POST the
single-use refresh grant and burn the credential. (grok_auth.py)
DOCS (fix the doc, behavior already correct/pinned by tests):
- #66 get_summary docstring corrected — it sums raw agent_spawn_sessions rows
(sub-day precise); daily_usage_rollups/get_today_summary can diverge for
"today" until the sweeper catches up. (roboco/services/usage.py)
- #68 DashboardStorage is a documented in-memory stub; added a test pinning
that auditor flags are lost on storage reset (persisting = a migration +
service refactor, out of scope as a half-implementation).
(tests/integration/test_dashboard_service.py)
BY-DESIGN (no code change, with file:line evidence):
- #28 dashboard reads are open to the authenticated operator (dashboard.py:36
documents this); mutating auditor routes already gate via
_require_auditor_or_ceo. Role-gating reads would break the panel (no
X-Agent-ID on dashboard reads) and CEO-token-gating the router would block
the Auditor (auditor token != CEO token). nginx is the prod boundary.
REFUTED (narrowing would reintroduce a documented hang):
- #93 the ~/.grok directory mount (vs a single auth.json file) is load-bearing
— a single-file bind mount pins the inode so the atomic tmp.replace refresh
doesn't propagate to running containers (they hang at grok's login prompt).
Already documented in grok.py:161 and locked by
test_intake_grok_mounts_subscription_auth_when_present.
Incidental gate-greening (mypy errors a stale .mypy_cache had hidden in
earlier-cluster test files; xenon refactors for the new B-threshold):
- tests/unit/test_regenerate_verb_tables.py: type the dynamic-module loader.
- tests/unit/services/test_prompter.py: annotate the draft dict as dict[str,Any].
- tests/unit/services/test_conventions_cache_put.py: _FakeOrig is a real Exception
(IntegrityError's orig arg requires BaseException).
- metrics._blocked_since_map extracted from get_blocker_metrics (complexity).
- intake_server._normalize_batch_drafts extracted from propose_batch (complexity).
* Docs update
* [bug] spawn: self-heal vanished clone + branch ref before worktree ensure (be-dev-1 fatal loop)
A vanished clone_root (disk loss / /data/workspaces wipe / manual cleanup)
fatal-looped the resume path: _ensure_worktree_before_spawn ran
`git -C <missing>` and released the claim, but the reaper-style release
preserves assigned_to + branch_name so the next dispatch is a RESUME
(create_branch never re-runs to re-clone) and the same missing clone failed
every ~30s.
- workspace.py: ensure_worktree_self_heal re-attaches a present worktree +
symlinks the shared .venv; on a missing local branch ref it fetches from
origin (create_branch pushes at claim time, so pushed work survives) and
re-creates the ref, falling back to -b origin/HEAD only when the branch
was never pushed. _fetch_branch_ref is the token-aware fetch helper.
- orchestrator.py: _ensure_worktree_before_spawn health-checks the clone
and re-clones via ensure_workspace BEFORE the worktree self-heal. Fatal
git-state (WorkspaceError) still releases the claim + aborts; transient
failures abort without releasing (a fresh claim wouldn't help and
re-cloning is destructive).
TDD: 21 new + 61 related worktree/git/cancel/cleanup tests green; ruff +
mypy clean.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
356 lines
93 KiB
Markdown
356 lines
93 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to RoboCo are documented in this file.
|
||
|
||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||
|
||
## [Unreleased]
|
||
|
||
## [0.14.0] - 2026-06-29
|
||
|
||
### Added
|
||
|
||
- **Multi-level MegaTask sequencing — a batch now runs in the right order, structurally, not by luck.** A MegaTask that spans several cells (and may mix per-cell projects from different products or OSS libraries) can now be routed per-cell without standing up a Product for it: each root-subtask carries an ad-hoc per-cell project map (`task_cell_projects`, migration 052, with a panel per-cell project picker), then cuts `feature/main_pm/{root}` and opens a root→master PR per repo exactly like a Product fan-out. On top of that map the dependency graph now carries the sequencing edges that collision and migration ordering need, enforced in the DAG rather than hoped for in the prompt: a dev task declares its collision surface (`intends_to_touch` globs, `adds_migration`, `touches_shared`, migration 046) on `delegate`; file-overlap serializes (more-important first), migration-adders chain serially, and a shared-surface edit runs after each non-shared task it overlaps — independent tasks still run in parallel — with cell-task wave chains and a by-osmosis edge completing the multi-level chain. Two new gate-level verbs close the "agent started out of order / drifted behind base" hole that no amount of prompting fixed: `sync_branch` (a dev gate verb that rebases the task branch onto its base and force-pushes, through the gate — raw git stays denied), and an `i_am_done` behind-base submit gate that structurally refuses to submit a task whose branch has fallen behind its base. Single-task intake is byte-for-byte unchanged.
|
||
|
||
### Fixed
|
||
|
||
- **Per-task git worktrees — a coordinator PM's multiple in-progress roots no longer clobber each other on one shared checkout (F123).** A coordinator PM (Main / Cell) legitimately holds several in-progress roots at once, but its clone is a single checkout — so every fresh claim ran `git reset --hard` + `checkout -b` to the *new* branch and destroyed uncommitted tracked changes on the still-active *first* root (a live run showed `main-pm` ping-ponging two roots on one clone for ~13h). The reset's own comment assumed it was discarding "abandoned cruft from a finished task," but neither root was finished, and the git mutation was non-transactional with the DB claim (rollback restored DB fields, not the working tree). Each task now gets its own working tree via `git worktree add` under `{clone_root}/.worktrees/{task-short}/` on the same underlying clone, so a PM's roots each have an independent checkout and the F123 `reset --hard` dissolves entirely (a fresh worktree is clean by construction). The shared clone keeps the real `.git` object store, the per-project `.venv`, and `.uv-python`; each worktree gets a `.venv → ../../.venv` symlink so `uv` resolves the shared clone-root venv (no per-worktree re-sync), and `.uv-python` is now gitignored so every worktree inherits it. Branch-by-name git ops (`push`, `pull`, `fetch`, `pr_merge`, `diff`) run from the clone root as before; checkout/HEAD-moving ops (`create_branch`/`commit`/`rebase`/`checkout`) target the worktree. Spawn resolves the worktree from `current_task_id` on every spawn (never cached) and `-w`'s the container there; a resume/respawn re-attaches a pruned worktree before launch; claim-rollback `worktree remove --force`s on a mid-claim failure so a retry doesn't collide with a stale worktree; terminal cancel removes the worktree (the stale-claim reaper does not — it routes to `pending` for a re-claim that reuses it). The destructive `reset --hard origin/<head>` in rebase recovery is pre-existing semantics, preserved. Invariants untouched: only the CEO merges master (no merge/release path touched), `/app/.venv` (the image-baked MCP-gateway venv) stays sacred, and the coordinator-PM concurrency exemption is unchanged — only the workspace resolution underneath became per-task. A real-`git`+`uv` integration test proves the clone root stays on `main` while two task worktrees each hold their own branch, and that `uv run` from a worktree resolves the clone-root venv through the symlink.
|
||
|
||
- **The worktree switch's two missed cwd-dependent git ops now route to the worktree (F123 followup, both deploy-blockers).** The worktree switch wired `create_branch` + `commit` to the worktree but left two checkout-dependent ops resolving the clone root, both of which would have broken live. (1) `rebase_onto_base` does `git checkout <head>` + `git reset --hard origin/<head>` in the resolved workspace — but post-worktree the branch is checked out in the linked worktree, so a `checkout` in the clone root is refused ("already checked out at '<worktree>'"), wedging the `sync_branch` behind-base recovery and the PM's `rebase_pr_for_task` wedged-PR recovery with a fatal `GitCommandError`. `sync_task_branch` and `rebase_pr_for_task` now resolve the worktree via `_worktree_for_task` and rebase there (the `checkout` becomes a no-op on the already-checked-out branch). (2) `conventions_check_for_task` ran the validator with `--root <clone root>`, and the validator reads `(root/rel).read_bytes()` — so it analyzed default-branch content, not the dev's worktree changes: newly-added files were absent from the clone root (false pass, the conventions block gate silently disabled) and modified files were validated at stale content. It now resolves the worktree and runs the validator there, so `i_am_done` / `pr_pass` gate against the real diff.
|
||
|
||
- **Completed/merged tasks now clean up their per-task worktree (F123 followup).** Only `cancel()` and the `create_branch` rollback removed per-task worktrees, so every completed/merged task leaked its `{clone_root}/.worktrees/{task-short}/` on disk until the whole agent or project was deleted — accumulating clutter live (a PM doing many roots left N stale working trees). The two terminal→completed paths now remove the assignee's worktree best-effort: cell-PM `complete` (after the leaf PR merges) and CEO `ceo_approve` (after root→master merges). Removal is terminal-only — a dev task bounces `needs_revision` off the earlier review states and needs its worktree back, so cleanup fires only at `completed` (post-merge, branch truly done), never at `awaiting_qa`/`awaiting_documentation`/`awaiting_pm_review`/PR-merge. No-op for branchless/umbrella tasks (no worktree was ever cut). Best-effort (`check=False`, wrapped in try/except), so a removal failure never blocks completion. The stale-claim reaper's "don't remove, reuse on re-claim" rule is unchanged — only the terminal path is new. No merge/release path touched.
|
||
- **The give_me_work → claim path now enforces the per-dev lane barrier.** The lane order check (`has_earlier_incomplete_code_sibling`: a code leaf may not start while an earlier same-assignee sibling is still open) lived only on the orchestrator's spawn path and `i_am_idle`, so a developer who asked for work through `give_me_work` — or claimed a task directly via `i_will_work_on` — bypassed it and could start a later code leaf before the earlier one's PR merged, cutting a branch from a base that predates the sibling's unmerged changes. `give_me_work`'s pre-assigned path now filters through `_pending_not_lane_held` (a lane-held leaf is dropped, not offered), and `_run_claim_guards` refuses a direct claim of a lane-held code task (`invalid_state`, parked back to `pending` via `release_dependency_blocked_claim`). The predicate is CODE-only so coordinator PMs are naturally inert; the claim guard is fail-closed on a lookup error so a DB hiccup never lets an out-of-order start through. `is not True` keeps both paths inert under partial test mocks.
|
||
|
||
- **Dev-task sequencing now chains undeclared-surface siblings on the same assignee.** The collision DAG only wired edges for dev tasks that declared a surface (`intends_to_touch` / `adds_migration` / `touches_shared`); a PM that delegated two dev tasks to the same developer without declaring surfaces wired no edge, so the later task could start while the earlier one's PR was still unmerged — the out-of-order start that wedged the merge. `wire_sibling_collision_dag` now falls back (only when no declared-surface collision edges exist) to chaining each same-`(project, assignee)` lane by `(priority, sequence)`: same-assignee siblings share a working tree, so the later one waits for the earlier. The lane is same-assignee scoped so cross-dev parallel work is untouched, and the edge lives in `dependency_ids` so it survives reassignment. Idempotent + incremental by construction (stable sort, `add_dependency` dedupes).
|
||
|
||
- **Loop-prone notifications now have a bounded re-fire guard.** `TASK_ASSIGNMENT` / `REVIEW_REQUEST` / `DOCUMENTATION_REQUEST` / `BROADCAST` can be re-fired by a coordinator PM every tick while a task sits in a state, flooding inboxes. The existing DB purpose-dedup never fires for these four (`ACK_REQUIRED_BY_TYPE` marks them `requires_ack=False`, so the dedup is gated off), and the delivery path (`_persist_and_deliver`) had no dedup at all — so a wedged task re-sent the same signal every cycle, inflating each recipient's unacked set and driving respawn churn. A 60s Redis `SET NX` window per `(type, sender, recipient, task)` now coalesces the re-fire on both creation chokepoints (`NotificationService._create_notification` and `NotificationDeliveryService._persist_and_deliver`): the first fire acquires (marks) keys for fresh recipients, subsequent fires within the window are suppressed when no recipient was fresh, and the storm converges. Fail-open: Redis unavailable → never suppress (a notification is never dropped over dedup infra). One-shot types (`KNOWLEDGE_SHARE` / `MENTION` / `A2A_REQUEST`) bypass entirely (distinct content per send, no dedup key).
|
||
|
||
- **A whole-codebase logic-gap sweep — 230 deduped regression risks plus the PM/code-task creation guard, every one dispositioned against the live tree.** The dominant body of this release. Each item was read against the real code first (four of the prior batch's Highs had been false alarms, so the inventory was never trusted blindly), then TDD-fixed; the dispositions ran 86 FIX, 78 BY-DESIGN (intentional/documented tradeoffs, with the silent-swallow-only cases reclassified to FIX with logging added), 18 REFUTED (the cited code already guards it), and 8 DOCS. The categories: **cross-repo PR scoping** — `pr_number` and `branch_name` are per-repo but were stored and looked up unscoped, so two tasks on different repos sharing a PR number could merge the wrong repo's PR or skip the org's own in-flight integration PR; every PR-merge and branch-ownership lookup is now `project_id`-scoped, and `close_pull_request` / `pr_target` make `project_id` mandatory. **Advisory locks closing TOCTOU races** — per-agent on claim, per-parent on `delegate`, per-task on `open_pr` (preventing a milestone double-emit), plus an atomic server-side Redis probe-failure counter and a single-transaction `replace_chunks` (delete+insert) closing a reindex race. **Audit-row transactionality** — status-transition audit rows and the rework counter are written in-session in the caller's transaction (the old fire-and-forget path is gone), so the audit trail can't diverge from the state change. **Signal gaps** — `pr_fail` now pushes the reviewer's issues to the owning cell PM (the re-submit loop where a PM respawned into `needs_revision` blind and re-submitted the same PR is closed), and `fail_qa` routes a `needs_revision` dev task back to the dev, never the pool. **Asyncio cleanup** — `OptimalService.close()` cancels its startup indexing task before the periodic task and the plugin clear, so it can't write against closed plugins. **Conventions standard** — the validator now times out and reaps on hang, and the gate fails closed on resolution errors (a broken standard can no longer silently disable the gate). **WebSocket** — fan-out is non-blocking with finally-disconnect, idle-timeout, and dead-socket reaping on send error. **Orchestrator runtime** — it drains its fire-and-forget background set on shutdown and stops in lifespan shutdown before closing the DB; the probe-resume loop actually revives parked agents; the grok auth token is refreshed before expiry and parked (not crash-retried) when missing. **Release executor** — every subprocess (git/make/gh/clone) is deadline-bounded and it fails closed on a git add/commit before push. Dozens more across org-memory (private-leak closures, playbook index/unindex as a post-commit step so the RAG corpus never leads the status transaction), the reaper, the provider-park/overload break, and the live-chat bridges. The single HIGH was the release-mutex TTL race (its own bullet below). The full readjusted mapping — every gap → disposition → `file:line` → how it works now — lives in `docs/internal/how-it-works-now-2026-06-30.md` (gitignored).
|
||
|
||
- **The release mutex is now fenced + heartbeated (the sweep's single HIGH).** `ReleaseProposalService.approve` guarded the fail-closed `ReleaseExecutor` with a Redis `SET NX EX` lock, but the lock held a static value (no fencing token), had no heartbeat, and its TTL (~50 min) was shorter than the worst-case clone+gate+CI+publish run (~90 min). On TTL expiry a second CEO approve re-acquired and `_prepare_release_clone` `rm -rf`'d the in-flight shared clone. The lock now carries a uuid4 fencing token; release is a Lua compare-and-del that only fires when `GET == token` (a late first-finally cannot delete a usurper's lock); a background heartbeat refreshes the TTL every 60 s while execute owns it. A Redis outage is distinguished from a held lock and both stay fail-closed (`redis_unavailable` vs `already_in_progress`).
|
||
|
||
- **The 2026-06-27 live-run meltdown cluster — root-caused and closed.** A run hit several compounding wedges at once, each TDD-fixed and verified green: a `main_pm` assigned a `code`-typed task is a structural impossibility (a coordinator PM does no coding) and is now hard-rejected at the gate; `cell_pm_complete` resolved a merge by global `pr_number` and merged the wrong repo's PR (closed by the cross-repo `project_id` scoping above); `submit_root` re-submitted an unchanged PR into an infinite `pr_fail` loop (now hard-gated); `fail_qa` bounced a dev task to the pool instead of back to the dev; a `note(scope='handoff')` with an empty section crashed the note path and tripped a PM respawn loop; the MegaTask four-layer hierarchy (umbrella → root → cell → dev) hit a depth cap sized for three layers; and the durable respawn counter's persist raced under fire-and-forget (an atomic upsert closes it).
|
||
|
||
- **The CEO, prompter, and secretary can no longer be spawned as agent containers.** These are human-only roles (the CEO is the human; the prompter is the on-demand intake interviewer; the secretary is the on-demand chief-of-staff) with no delivery lifecycle, yet a `_dispatch_a2a_work` path that spawned any notification target — plus an `_is_agent_active('ceo')` that always returned false — could nonetheless launch them and burn a container on a role that has no work to do. A chokepoint in `spawn_agent` plus a dispatcher skip on human-only assignees closes it at both the spawn and the dispatch layer.
|
||
|
||
- **The PM-respawn loop breaker now survives an orchestrator restart.** The circuit breaker that stops RoboCo from respawning the same PM on the same wedged task forever (`_pm_respawn_tracker`) lived only in memory, so a deploy/crash/OOM reset a task's strike count to 1 and re-burned the whole threshold — four full agent spawns × container cost — against the still-broken task before the gate fired again. The counter is now write-through-persisted to a new `respawn_tracker` table (migration 051) on every mutation and restored at startup, validated against live tasks so a stale counter can't resurrect against a fixed one. Best-effort and inert when empty (a DB hiccup degrades to exactly the prior in-memory behaviour); it can only ever suppress a spawn, never manufacture one.
|
||
|
||
- **The `mypy` / `ruff` quality gate is green again, with no `type: ignore` suppressions in `tests/`.** A round of pre-existing type errors in the test suite (ORM `<row>.id` passed where `uuid.UUID` was expected, missing annotations, `None`-attribute accesses) and every remaining `# type: ignore` in `tests/` are cleared, so `make quality` passes cleanly and the no-suppression convention holds.
|
||
|
||
### Security
|
||
|
||
- **Phase 5 — the live-chat bridges now enforce the CEO-signed panel token.** The intake (`prompter_live`) and secretary (`secretary_live`) panel-facing endpoints were the only API surface that ran unauthenticated at the route layer — their SSE stream (`GET /stream`) carried no identity at all (browser `EventSource` cannot set headers), and the start / status / messages / stop endpoints took no auth dependency. They now require the existing CEO-signed HMAC panel token (`require_panel_token`, the HTTP sibling of the WS `_require_panel_token`): nginx already injects `X-Agent-Token` on `/api/` in prod, so the browser never holds the secret and no panel/nginx change was needed; in dev a missing token is allowed but a forged one is still rejected. This closes the last ungated panel-facing surface using the existing scheme verbatim — no new auth, no client changes.
|
||
|
||
- **Agent-token gates and secret-scrubbing hardened across the API.** The HMAC agent-token gate is now enforced on the `do` content routes and the WebSocket streams (not just the a2a message routes); the orchestrator signs its own `X-Agent-Token` on self-API calls; 422 error logs are scrubbed of secrets; the a2a / dashboard / orchestrator routes are gated; and SSE runs one session per query. With the Phase 5 bridge gate above, no panel-facing or inter-agent HTTP surface is now unauthenticated when auth is required.
|
||
|
||
### Changed
|
||
|
||
- **The local LLM was bumped to `glm-5.2` and the Ollama fleet defaults swapped off minimax.** The in-house RAG / hybrid-retrieval model and the default fleet model assignment move to `glm-5.2:cloud`; a stale minimax default that no longer matched the running fleet is cleared.
|
||
|
||
- **Agent-facing RAG docs and generated prompts readjusted to the post-fix behavior.** The per-task worktree model (F123), the `/app/.venv` is-sacred rule, the PM/code-task invariant, and the `MAX_TASK_DEPTH=4` MegaTask hierarchy are now documented in the RAG corpus (`docs/rag/architecture/workspaces.md`, `workflows/task-claiming.md`, `workflows/git-commits.md`, `workflows/task-planning.md`, `roles/developer.md`) and the generated verb/status tables, so a respawned agent resumes against current guidance instead of the pre-fix model.
|
||
|
||
## [0.13.0] - 2026-06-26
|
||
|
||
### Added
|
||
|
||
- **Gated release manager — RoboCo prepares its own releases, you approve them.** Cutting a release was a manual, error-prone checklist (enumerate changes, derive the semver bump, update the CHANGELOG, bump eight version refs, gate, tag, publish). A default-off background loop now runs a fully deterministic readiness sweep — diff since the last tag, conventional-commit classification, the semver bump, version-reference completeness (the "you forgot to bump file X" guard), CHANGELOG completeness, docs drift, migration single-head, and the CI gate state — and, past a threshold with a green gate, opens ONE **release proposal** held for the CEO. The proposal is held (never dispatched to an agent); you approve or reject-with-changes in the panel, and only on approval does a **fail-closed** executor write the bumps + CHANGELOG, run `make quality` (aborting before any commit on red), commit + push, wait for green CI (aborting before publish on red), then publish the GitHub release. Correctness is code, not agent judgment; the only generative step is the CHANGELOG prose, which you review; it never publishes without you. Default-off (`ROBOCO_RELEASE_MANAGER_ENABLED`).
|
||
- **Organizational memory loop — agents stop re-learning what the company already knows.** Three parts behind one default-off flag (`ROBOCO_ORG_MEMORY_ENABLED`). ① At task completion the company distills ONE high-signal lesson (Problem → Approach → Gotcha, ≤120 words) via the local model instead of dumping noisy raw notes, and private journal reflections are kept out of the shared knowledge corpus. ② The keystone: when an agent claims a task, the briefing is auto-injected with the top relevant past lessons and approved playbooks for work like this (role-shaped query, relevance-floored so nothing low-signal is added) — the agent never has to think to ask. ③ A first-class, curated **playbook** library: delivery agents draft playbooks via a new `draft_playbook` gateway verb, the Auditor approves / rejects / archives them (a bounded, deliberate expansion of its surface — curation, not agent comms), and approved playbooks are embedded into a new `PLAYBOOKS` knowledge index and surfaced in a panel review queue. Adds the `playbooks` table (migration 050). Distillation and retrieval run on the local model only and are best-effort — a failure never blocks a completion or a claim.
|
||
|
||
### Fixed
|
||
|
||
- **Pitch auto-provisioning is now idempotent — a re-approval no longer collides.** When a pitch's approval partially failed and its DB writes rolled back while the created GitHub repos survived, re-approving it tried to re-create the repos and re-insert the product → project cell mappings, hitting a duplicate-key crash on `(product_id, team)` and leaving an orphaned product that could not be cleaned up. Provisioning now reuses an existing Project (by slug) and an existing Product (by slug, refreshing its cell map with delete-before-insert ordering) instead of re-creating them, so a re-approval converges cleanly. First-time provisioning is unchanged.
|
||
- **The `mypy roboco/ tests/` quality gate is green again.** A batch of test files carried type errors that turned the gate red (SQLAlchemy `<row>.id` passed where `uuid.UUID` was expected, a couple of missing return annotations, an invariant-`list` argument, and a `None`-attribute access). Each is now typed correctly so the full gate passes. (The deeper cause — many ORM columns annotated `Mapped[UUID]` against SQLAlchemy's `UUID` type rather than `uuid.UUID` — is noted for a separate, dedicated cleanup.)
|
||
- **An external-PR review can no longer record a verdict that contradicts its own summary.** The inbound-PR reviewer verb (`post_pr_review`) derived both the recorded 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. So a reviewer that concluded "approve" in the summary but left `event` at its default filed (and posted to the contributor's PR) a blocking "changes requested" with nothing cited. The verb now enforces a verdict↔findings invariant before any record or post: `REQUEST_CHANGES` must cite at least one finding (almost always a forgotten `event='APPROVE'`), and `APPROVE` may not carry a blocker/major finding — rejected with a clear remediation otherwise.
|
||
|
||
## [0.12.0] - 2026-06-25
|
||
|
||
### Added
|
||
|
||
- **Dependency-update bot — the company keeps its own dependencies current.** A default-off, per-project engine that periodically (weekly by default) checks whether a dependency upgrade would change a project's lockfiles and, if so, opens one "update dependencies" task into that project — which flows through the normal dev → QA → PR-review → CEO-merge pipeline and **never auto-merges**. Detection is read-only: it runs the project's configured `dep_update_command` (e.g. `uv lock --upgrade` / `pnpm update`) in a throwaway clone of a read-only copy and checks whether any lockfile path got dirty — the read clone is never mutated and nothing is committed or pushed. Fail-safe: a missing or failing command opens nothing. Bounded and deduped per repo (one open update task per git URL) with per-cycle and rolling caps. A project participates only when its `dep_update_command` is set (panel → project settings). Default-off (`ROBOCO_DEP_UPDATE_ENABLED`). Adds `projects.dep_update_command` / `dep_update_paths` (migration 049), the `WorkspaceService.dry_upgrade_changes_lockfile` probe, `DepUpdateEngine`, and a dedicated orchestrator loop.
|
||
|
||
- **Multi-repo CI-watch — the company watches every repo it owns, not just its own.** Self-heal already watched RoboCo's own CI and opened a fix task when it went red; CI-watch generalizes that to *any* project the operator opts in. Flip `ci_watch_enabled` on a project (panel → project settings) and, on each pass, RoboCo checks that project's latest CI conclusion on its default branch; if it's red it opens one fix task into that project (and notifies that project's cell PM) — which flows through the normal dev → QA → PR-review → CEO-merge pipeline and **never auto-merges**. It reuses the same hardened per-project CI lookup self-heal uses, so a missing signal is treated as "unknown" (never a false green) and one project's GitHub error never aborts the sweep. Bounded and deduped per repo (a monorepo's several cell-projects share one fix task, keyed on the git URL), with per-cycle and rolling open-task caps. Default-off (`ROBOCO_CI_WATCH_ENABLED`), and the single-repo self-heal loop is untouched. Adds `projects.ci_watch_enabled` / `ci_watch_workflow` (migration 048) and the `MultiProjectCITelemetrySource` + `CiWatchEngine` + a dedicated orchestrator loop.
|
||
|
||
- **The orchestrator now reclaims dangling Docker images on its own.** Every rebuild of an agent image orphans the prior build's layers as an untagged `<none>` image; across many deploys these pile up (the operator hit ~80). The background sweeper now runs `docker image prune` for dangling images only — throttled to roughly every six hours — so they don't accumulate. It is deliberately conservative: only *dangling* images are removed (a tagged image, or one backing a running container, is never dangling), it is best-effort (a failure is logged, never raised), and it can be turned off with `ROBOCO_IMAGE_PRUNE_ENABLED=false`.
|
||
|
||
### Fixed
|
||
|
||
- **An external PR on a monorepo is no longer reviewed twice.** Inbound external/internal PR review de-duplicated per `(project_id, pr, head_sha)`, but several cell-projects can map to one repo (a monorepo) — and the poll already collapses to a single canonical project per repo, so the dedupe and the poll disagreed once a review task was re-pointed to a sibling project: the next poll, checking the canonical project, no longer saw it and opened a second review of the same PR. The dedupe is now scoped to the **repo** (`git_url`) rather than a single project, so the same PR on any project sharing the repo is reviewed once; re-review on a new head commit still works, and genuinely different repos that happen to share a PR number are reviewed independently.
|
||
|
||
- **Completing a task whose PR is already merged no longer loops.** A merge request against an already-merged PR returns the same `405` from GitHub as a genuine "not mergeable" conflict, so the completion path treated an already-landed PR as a conflict and tried to rebase / close-superseded / escalate it — bouncing the task between blocked and unblocked forever (the case where a prior cycle, a sibling, or the CEO had already merged it). The merge now disambiguates: if the PR reports as merged, the merge is treated as idempotent success and completion proceeds; only a PR that is genuinely unmerged raises the conflict.
|
||
|
||
- **After an orchestrator restart, a still-running agent is no longer double-spawned.** The orchestrator's in-memory instance registry is lost on a restart while the agent containers keep running. The stale-claim reaper already had a Docker-liveness fallback for that, but the spawn gate (`_is_agent_active`) did not — so right after a restart it saw a live agent as inactive and could launch a second container onto the work the forgotten-but-running one was already doing. Startup now re-adopts surviving containers: it probes each known agent slug's container (the same `docker inspect` the reaper uses) and re-registers a minimal active instance for any that is running, before the dispatcher and reaper loops start. Inert when nothing is running, and best-effort (a probe error just leaves that slot for the reaper's own fallback to cover).
|
||
|
||
- **A resumed agent on a drifted shared clone no longer wedges with `BRANCH_MISMATCH`.** A dev/documenter/QA clone is shared across that agent's tasks; on a respawn/resume it can sit on a sibling task's branch, or a re-provisioned clone can lack the task branch as a local ref (its commits are only on origin). The fresh-claim path git-resets the clone clean, but resume deliberately short-circuits before it — so the agent's next `commit` hit the branch-mismatch guard, failed, and the task wedged in a blocked respawn loop (e.g. the documenter that could never land its doc commit). The guard now *recovers* instead of only rejecting: it fetches and checks out the task's branch (recreating a missing local ref from origin) and only raises when it genuinely cannot switch — i.e. uncommitted changes block it. It never discards work (checkout, not reset), so a resumed agent's unpushed commits are preserved.
|
||
|
||
- **An integration branch is no longer deleted out from under in-flight work (the "branch gone from origin" zombification).** After a PR merged, the post-merge cleanup deleted its head branch unconditionally — so merging a cell→root PR deleted the cell branch while a sibling leaf PR was still targeting it as its base, and the CEO's root→master merge deleted the `feature/main_pm/{root}` integration branch. The dependent PRs then had no base, every later git op against the vanished branch failed, and the task zombified (the symptom an earlier fix only made non-fatal). The remote-branch delete chokepoint now first checks whether any **open PR still targets the branch as its base** — an active integration target — and preserves it if so; it fails safe (on any error it keeps the branch, since cleanup is best-effort but stranding is not). True leaf branches with no open dependents are still cleaned up as before.
|
||
|
||
- **Mypy [unreachable] error in test_pr_gate_records_verdict resolved.** A test assigned `t.notes_structured = None` in the function body, causing mypy to narrow the attribute type to `None`. Since the test's helper function took the object as `Any`, mypy did not reset its narrowing after the call, treating `assert t.notes_structured is not None` as statically always-False and marking the next line as `[unreachable]`, failing the quality gate. Fixed by introducing `_TaskWithNoNotes` — a helper class that declares `notes_structured: dict[str, Any] | None = None` in `__init__` — so mypy uses the declared union type rather than a narrowed literal. All tests pass with no suppressions. This pattern is documented in the testing standards for future reference.
|
||
|
||
- **Ruff lint errors from autonomous-maintenance PR (#264) resolved.** The Feat/autonomous-maintenance merge introduced 4 ruff lint errors that broke the quality gate: (1) unused `cast` import in `roboco/api/routes/project.py` (F401), (2) unused `cast` and `UUID` imports in `roboco/services/self_heal_engine.py` (F401), and (3) `Sequence` import in `roboco/services/telemetry/source.py` placed at module level instead of in TYPE_CHECKING block (TC003). Root cause: the autonomous-maintenance refactoring orphaned these imports (cast was imported but never called; UUID was in TYPE_CHECKING but not referenced; Sequence was used only in annotations and must be in TYPE_CHECKING when `from __future__ import annotations` is present for proper runtime safety). Fixed by removing the unused imports and moving Sequence to TYPE_CHECKING. The TC003 pattern is a best practice: all imports used only in type annotations should reside in TYPE_CHECKING to avoid circular imports at runtime and reduce module startup cost. No suppressions added; all quality gates pass (10197 tests, 95.51% coverage).
|
||
|
||
## [0.11.1] - 2026-06-25
|
||
|
||
### Fixed
|
||
|
||
- **A PM no longer respawn-loops on its own coordination root after it is bounced back for revision.** The lifecycle spec lets a cell/main PM re-claim a `needs_revision` coordination root — so a root rejected by `pr_fail` / `qa_fail` / `ceo_reject` can be re-planned and re-delegated via `i_will_plan` — but the runtime's claim-status map omitted `needs_revision` for the PM roles. The spec gate allowed the verb while the composed `claim()` underneath rejected it, returned nothing, and surfaced as a cryptic `INVALID_STATE`: the PM could neither plan nor idle its own rejected root and respawn-looped (one live run logged ~143 such rejections across 11 PM sessions — the tail of the 2026-06-24/25 firefight). The runtime claim statuses now include `needs_revision` for the PM roles, and a parity test locks the runtime map to the lifecycle spec so the two can't drift apart again.
|
||
|
||
- **A finished merge no longer respawn-loops the PM when its target branch has been deleted from origin.** When an integration (cell/root) branch is removed from origin — e.g. a sibling cell→root merge that strands a late straggler leaf — the post-merge `_sync_target_branch` ran `git fetch origin <branch>` and raised "couldn't find remote ref". But `pr_merge` only reaches that sync *after* the authoritative GitHub merge has already succeeded, so refreshing the local copy of the now-gone target branch is purely cosmetic — yet the raise surfaced as a retryable `SERVICE_ERROR`, so `complete()` re-blocked the task and respawn-looped the PM on an already-landed merge (observed live blocking a cell PM's `complete()` for 5+ cycles). The post-merge sync is now best-effort (it logs and returns instead of raising); the CEO merge path keeps the strict sync, since its target is the always-present default branch.
|
||
|
||
- **A PM no longer re-delegates already-finished work as an empty phantom subtask.** 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), and the coverage matcher only counted id matches. So a completed child that had declared its coverage by text was invisible to the roll-up: the criterion read "uncovered", the gate refused to close the parent, and the PM re-delegated the already-merged work as a brand-new empty subtask (zero commits, no PR) that can never close — looping for hours and burning tokens (observed live: a parent's work completed and merged via one child, then re-delegated two hours later as an empty phantom). Every child ref is now normalized to the criterion id (text → id via the parent's own criteria) before counting, so coverage is recognized however it was declared; an unknown ref still matches nothing, exactly as before.
|
||
|
||
- **An ownership failure now reads as an authorization error instead of a fixable tracing gap.** A `PRECONDITION_OWNERSHIP` rejection (a non-owner invoking an owner-only verb) was dispatched as a generic `tracing_gap` — which looks like a *recoverable* missing-artifact precondition, so a superseded agent kept retrying the same verb instead of fetching new work. Preconditions now carry a `rejection_kind`, and `PRECONDITION_OWNERSHIP` is tagged `not_authorized`, so an ownership failure surfaces as the clear identity/role boundary it is (and the choreographer and lifecycle spec now agree on the kind across the parity suite). This generalizes, at the spec layer, the same `not_authorized` steer the reassigned-developer `i_am_done` / `open_pr` short-circuit already gives.
|
||
|
||
- **The spawn gate now suppresses respawns for every parked provider, not just Grok.** When a provider is parked — a rate-limit 429, a persistent overload, or the Claude session limit — the dispatcher must stop launching new agent containers until it recovers, or it just re-spawns agents every tick straight back into the wall. That guard was Grok-only, so an Anthropic park still let the dispatcher churn. The spawn gate now consults the rate-limit tracker for *every* provider (failing open if the tracker itself errors), so any provider's park actually quiets dispatch.
|
||
|
||
- **A Claude session-limit hit is now detected from the agent's transcript, so the park actually fires.** Parking the workforce on the Claude "5-hour" session limit (added in 0.11.0) read the session-limit 429 markers from the agent container's `docker logs` — but the Claude SDK server writes its runtime output to a log file *inside* the container, so those markers never reached docker logs and the detector silently missed them, letting the whole fleet crash-respawn back into the limit. The detector now also reads the tail of the newest durable Claude transcript as a fallback, so a session-limit exit parks the provider and the background probe loop auto-revives the agents when the window resets.
|
||
|
||
- **A failed in-path PR-review gate now actually leaves its verdict on the PR.** The gate posts its pass/fail review to the assembled PR so the decision is visible where the PM (or CEO) merges — but it could only resolve the PR's repo from the task's `project_id`, and a Main-PM coordination root (the only task a root→master PR ever sits on) usually carries just a `product_id` (the cell→repo map) and no project of its own. So the slug resolved to nothing and the post silently no-op'd: a root→master PR could be failed back to `needs_revision` with no comment on the PR explaining why. The gate — and the external-PR reviewer's read-only diff fetch — now fall through to the product's repo when the task has no direct project, so the verdict reaches the PR.
|
||
|
||
- **A task's PR-reviewer notes no longer show "passed" after the gate failed it.** `pr_pass` / `pr_fail` only threaded their notes through the tracing-gate check and posted to GitHub — neither wrote the task's structured `pr_review` slot. So a task passed once and later failed kept displaying `verdict: passed` (green card and all) while its real transition was `pr_fail` → `needs_revision`. The gate now authors the canonical `pr_review` note on every decision — `pr_pass` records *passed*, `pr_fail` records *failed* with the issues — so the panel's PR-Reviewer card always matches the actual outcome (best-effort: a malformed note is skipped, never rolling back the gate decision).
|
||
|
||
## [0.11.0] - 2026-06-24
|
||
|
||
### Added
|
||
|
||
- **MegaTask — describe several tasks in one intake chat and ship them as one sequenced batch.** When the CEO wants several pieces of work at once — even across projects that don't share a codebase (e.g. a SaaS app, its open-source core engine, and a framework adapter) — the intake modal now offers a third scope, **MegaTask**, beside Single cell and Board-led. You pick the repos it spans; the intake agent reads them all and proposes the whole batch in one hand-off (the new `propose_batch` tool), one draft per task, each carrying its own project plus a collision surface (which files it touches, whether it adds a migration, whether it edits a widely-shared component). A deterministic analyzer (`SequencingService`) turns those surfaces into conflict-free **waves** — file-overlap and migration-adding tasks are serialized, a shared-surface edit runs after what it overlaps, independent tasks run in parallel — and the Board reviews the batch once. On confirm RoboCo creates a branchless **umbrella** task (the Main PM's coordination + board-review + CEO-approve unit) over N **root-subtasks**, each a real coordination root with its own project, branch, and PR, wired with the analyzer's dependencies so the existing dependency-gate dispatches the waves in order. The umbrella assembles no PR of its own, is exempt from the branch gate, and completes only when every root-subtask is terminal (then it escalates to the CEO). On the Board route the root-subtasks are held until the umbrella is approved, then released. Surfaced as a core capability — no feature flag — branded "MegaTask" across the panel, prompts, and docs; internal names stay technical (`batch_id`, `SequencingService`). Adds `tasks.batch_id` + the three collision-surface columns (migration 046), `confirm_live_batch` + `POST /prompter/live/{session}/confirm-batch`, multi-project intake spawn (`project_ids`), the `propose_batch` tool on both intake runtimes (Claude SDK driver + grok CLI server), and the panel's MegaTask scope + Review-MegaTask card.
|
||
|
||
- **New Ollama Cloud models in the LLM catalog.** Added `kimi-k2.7-code:cloud` and `nemotron-3-ultra:cloud` to the Settings model picker. `north-mini-code-1.0` is available only as a self-hosted Ollama tag, so it is left out of the cloud catalog and will appear automatically in the self-hosted picker when pulled locally.
|
||
|
||
### Fixed
|
||
|
||
- **A PM that forgot to journal its decision no longer stalls a finished task forever.** Every PM decision-point verb (`unblock`, `complete`, `submit_up` / `submit_root`, `escalate_up` / `escalate_to_ceo`) required a *separate* `note(scope='decision')` call to be made **before** the verb — and loaded or weak models reliably forget to chain that prior side-effect, so the verb hit a `tracing_gap` (`journal:decision` missing), the agent retried, and a task whose work was actually done sat stranded in a reject → respawn loop (the dominant remaining completion-path blocker in a 24h run; one live case: a corrected PR that could not be merged because the Main PM's `unblock` kept failing the gate). The verb now auto-records its **own rationale** as the `journal:decision` the gate needs, *before* the gate runs — the same write-then-gate pattern already used for `i_am_blocked → write_struggle` and for the `qa_notes` / `pr_reviewer_notes` sections — so the gate passes off real, persisted reasoning instead of demanding a redundant bookkeeping call. `complete` / `submit_up` / `submit_root` / `escalate_*` reuse the `notes` / `reason` the PM already passes; `unblock` now takes a required `reason` (threaded MCP tool → request schema → routes → choreographer); `delegate` derives the decision from the subtask's title + description. The gate still runs as defense-in-depth, the auto-record is idempotent within the decision window and best-effort (a journal hiccup falls back to the prior reject, never a crash), and the recorded decision is the PM's real words — so accountability is preserved, not bypassed.
|
||
|
||
- **An empty-diff subtask no longer loops a developer on "open a PR".** When overlapping decomposition leaves a leaf branch with zero commits relative to its base (its work was actually delivered by the parent or a sibling), `open_pr` pushed the branch and GitHub refused the PR with a `422 "No commits between …"`. That was surfaced as a generic `invalid_state` whose remediation said "retry", so the developer re-issued `open_pr` over and over (observed 15× on a single task) and never progressed. `open_pr` now recognizes the empty-diff 422 and returns a terminal hand-off instead: it tells the developer not to retry — the branch has no diff, so the work was delivered by the parent — and to call `i_am_blocked` so the PM can complete or cancel the redundant leaf.
|
||
|
||
- **A reassigned developer no longer retries `i_am_done` / `open_pr` forever.** When a task is reassigned out from under a still-running agent (a pool release, reaper unclaim, or escalation redirect), the agent's later `i_am_done` or `open_pr` failed the spec's ownership precondition as a `tracing_gap` (`owns_task` missing) — which reads like a *fixable* precondition, so the superseded agent kept retrying the same verb (41 such rejections in one run). Both verbs now short-circuit a non-owner with the same clear `not_authorized` "this task is no longer yours — call `give_me_work()`" steer that `resume` and `unclaim` already use, so a superseded agent is told plainly to fetch new work or go idle instead of looping.
|
||
|
||
- **A Main PM blocking its own coordination root no longer hands the whole root to the Board (a respawn catch-22).** Root cause: the generic escalation chain points `main-pm → product-owner`, and `i_am_blocked` / escalate REASSIGNS the task to that chain target. The board-advisory guard that refuses such a hand-off only covered descendant cell tasks (it required `parent_task_id`), so a top-level Main-PM **coordination root** slipped through and the entire root was reassigned to the Product Owner and marked blocked. A Board role has no `unblock` verb at all (only notify / note / triage / i_am_idle) and the unblock gate is assignee-only, so it could neither resolve the blocker nor hand it off — it just spam-notified the CEO while the blocked-task dispatcher respawned it every tick (one live incident burned an estimated 6400+ tool calls on a single root). Fixed at both layers: the escalation / reassign / revival guard now also refuses a Board owner for a `main_pm` coordination task (root or MegaTask root-subtask) and diverts it to the pool for a role-matched (Main-PM) re-claim — the upstream cure — via a single shared `_board_cannot_own` predicate; and, as a defense-in-depth backstop, the orchestrator's blocker dispatcher no longer treats a Board role as a blocker resolver (it returns no resolver, so a mis-owned blocked task is skipped rather than respawned onto a role that physically cannot act).
|
||
|
||
- **A racing state change mid-verb no longer crashes a PM into a respawn loop.** The gateway's verb runner guards the *initial* task/agent against `None`, but its composed atomic actions reassign the working task from each step (`i_will_plan` runs claim → set_plan → start). When a concurrent agent transitioned the row between the verb's precondition gate and execution — e.g. a racing `i_am_blocked` moved a coordination root from `needs_revision` to `blocked` — `claim()` found no valid transition and returned `None`, then the next step dereferenced `None.id` and crashed with the opaque `'NoneType' object has no attribute 'id'`, surfaced to the agent as a cryptic "verb runner failed" so the PM respawn-looped on the wedged root. The runner now re-checks after *each* composed action and fails fast with an actionable `INVALID_STATE` that tells the agent the row changed under it and to re-fetch and re-issue its verb (the savepoint rolls the partial sequence back).
|
||
|
||
- **A completed task no longer wedges when its branch is missing from a re-provisioned clone.** Push-by-name (the fix that decoupled the push from the workspace checkout) still requires the named task branch to exist as a *local* ref — but a developer's shared clone can be freshly re-provisioned (the per-task workspace-collision recovery re-clones it), leaving the task branch absent locally even though its commits are safely on `origin` and the clone is parked on a different task's branch. `git push origin <branch>` then died with the cryptic `src refspec <branch> does not match any` and the task blocked-looped at `i_am_done`. The push now recovers a missing local ref from `origin/<branch>` first (a clean no-op when the work is already on origin); if the branch exists on neither the clone nor origin the commits are genuinely gone from this clone, so it fails loud with a recoverable "unclaim the task and re-claim it to rebuild the branch, then replay your commits" instruction instead of the raw refspec error.
|
||
|
||
- **The orchestrator's own recovery actions now actually run.** Its background dispatcher made internal HTTP calls to its own API without an agent identity, so every self-`PATCH` to a task — auto-blocking a task with missing prerequisites, auto-resuming a PM's paused parent, auto-recovering a stale-blocked parent, annotating an SLA breach — was rejected with `401 Missing X-Agent-ID` and silently dropped. The visible effect was paused/blocked parent tasks staying wedged and their dependent work stranded (with the dispatcher logging a "respawning assignee" loop). Header propagation was inconsistent across the orchestrator's separate HTTP-client call-sites — only the main dispatch loop sent the identity. The system identity is now hoisted into one shared constant and applied to every API-facing dispatcher client (the external provider-recovery probe is intentionally excluded); the `system` role holds the permission required for the audited status-override path those routes use.
|
||
|
||
- **A developer's completed work no longer silently fails to reach GitHub ("No commits between").** A developer's single git clone is shared across all of their tasks, so by the time a task's PR is opened the clone has usually moved on to a *later* task's branch. The push at the QA-submission / `open_pr` boundary, and the PR's head branch, were both taken from the clone's *current* checkout — so the push was rejected (the workspace was parked on another task's branch) 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 on disk and correct, just never pushed. Both the push and the PR head now operate on the task's recorded branch **by name**, independent of the checkout (`push(branch=…)` targets the named ref; the PR head is the task's `branch_name`). Work committed on any of a shared clone's task branches now pushes and opens its PR correctly.
|
||
|
||
- **Hitting the Claude session limit now parks the workforce instead of crash-looping it.** When the org's Claude usage ("5-hour") limit is reached, each agent container exits with a 429 rejection; the orchestrator was treating that like any crash and immediately respawning the agent straight back into the limit, over and over, across the whole fleet. It already parks the provider on a persistent server *overload* (529/500/503) and revives the parked work once it recovers — but that detection only matched the overload signatures, not the session-limit 429. The same park-and-resume break now also recognizes the session limit: the provider is parked, dispatch goes quiet, and the background probe loop brings the agents back automatically when the window resets — no churn, no wasted respawns.
|
||
|
||
- **A failed PR review no longer looks green.** On a task's detail page, the "PR Reviewer Notes" card was painted a fixed teal/green background regardless of the review verdict, so a `Failed` review — red badge and all — sat inside a green card and could read as passing at a glance. The card background now mirrors the verdict the way the QA Notes card already does: red on a failed review, green on approved/passed, amber on changes-requested, and neutral before a verdict is in.
|
||
|
||
- **The CEO and other human roles no longer get spammed with agent "learnings."** Whenever an agent recorded a learning, RoboCo broadcast it as a knowledge-share notification — and the recipient query swept in the human roles too (the CEO, plus the human-driven prompter and secretary). Agent knowledge-sharing is a signal for *agents*; in a human's inbox it is just noise. Those roles are now excluded from learning broadcasts.
|
||
|
||
- **A gateway verb on a vanished task/agent fails cleanly instead of crashing cryptically.** The verb runner's atomic steps dereference `task.id` / `agent.id` with no guard, so a verb invoked when the task or agent could not be resolved (e.g. a task forced into an unexpected state out-of-band) crashed with an opaque `'NoneType' object has no attribute 'id'`. The runner now fails fast with an actionable `INVALID_STATE` error that tells the agent to re-fetch and re-issue its claim verb.
|
||
|
||
- **An agent could be permanently wedged in a respawn loop by duplicate work sessions on one task.** A task is owned by one agent at a time, so it must have at most one *active* git work session — but nothing enforced that: when a task was re-claimed by a **different** agent (after a pool release, reaper unclaim, or escalation redirect) the prior holder's active session was left open. `WorkSessionService.get_active_for_task` then ran a one-row query across 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 the task's dependents stayed blocked. (This was the real root cause behind the verb-runner `INVALID_STATE` guard above, which only made the crash legible.) Fixed at three layers: the active-session lookups now 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); and a partial unique index — migration 047, which first de-duplicates existing rows, keeping the most recent — enforces it at the database level so it can never recur.
|
||
|
||
- **A dev claiming a new task no longer gets stuck on `BRANCH_MISMATCH`.** Each developer has one persistent clone shared across all their tasks, so a finished or abandoned prior task could leave the clone dirty and sitting on a sibling task's branch. The claim's git work (creating/checking out the new task's branch) runs as a side-effect *after* the claim's DB transition commits — so when the checkout failed on that dirty tree, the task was already marked assigned while the workspace stayed on the wrong branch, and the dev's next commit was rejected with `BRANCH_MISMATCH` (stalling, then blocking, the task). The claim now does a `git reset --hard` to clean the tree before the checkouts. It runs only on a fresh claim (resume short-circuits earlier), so the discarded changes are abandoned cruft from a finished task — never committed work, and never the gitignored `.venv`.
|
||
|
||
- **The `note` tool no longer times out under load.** Writing a journal entry / note synchronously waited on RAG indexing, which embeds via Ollama — and Ollama is CPU-bound, so under concurrent load that embed slowed enough to time the `note` gateway tool out entirely (despite a "non-blocking" comment on the code). The entry is already persisted before indexing, so indexing is pure best-effort enrichment: it now runs fire-and-forget on the event loop, and the note/journal write returns immediately.
|
||
|
||
- **A feature flag stopped showing its raw internal key.** In Settings → Feature Flags, the "Gateway-health recovery" toggle displayed its raw key `gateway_health_enabled` as its description (the only flag missing a human blurb). Added the description, and changed the fallback so a future flag without one renders nothing rather than leaking a snake_case key.
|
||
|
||
- **A missing local parent branch no longer blocks every leaf PR merge.** When a cell PM completes a leaf task, `_sync_target_branch` checked out the parent/cell branch with a bare `git checkout <branch>` and no fallback — but the agent's shared clone often only has the leaf's own task branch locally, while the parent branch exists only on `origin`. That produced a "git workspace state inconsistent" SERVICE_ERROR that cycled the task back to `blocked` each time the PM retried. The merge path now fetches the target branch from origin and creates a tracking branch when the local ref is missing, then pulls and returns the merge commit the same as before.
|
||
|
||
## [0.10.0] - 2026-06-23
|
||
|
||
### Added
|
||
|
||
- **Delivery observability dashboards — cycle-time, bottlenecks, rework rate, and per-agent/per-cell scorecards.** A new "Delivery" tab on the Metrics page surfaces how work *flows*, built on data RoboCo already captures: per-stage cycle time reconstructed from the `audit_log` transition journey, a bottleneck view (which lifecycle stage holds the most cumulative time + how many tasks are parked there now), a rework view (how often work bounces to `needs_revision`, by team and by agent, with the rejection attributed to the QA / PR-reviewer who made it, plus the rework's token cost), and fused per-agent / per-cell scorecards. Backed by new read-only `MetricsService` methods and `/dashboard/metrics/{cycle-time,bottlenecks,rework,scorecard}` endpoints. To make rework correct and O(1), each task now carries a `revision_count` incremented at the single transition chokepoint (migration 045, with a composite `audit_log(target_id, event_type, timestamp)` index for the reconstruction queries), and QA/PR-review bounces emit rejector-attributed `task.qa_fail` / `task.pr_fail` audit events. No feature flag — it reads the always-on metrics surface.
|
||
- **Gateway-health recovery — a broken-but-alive agent is recovered instead of protected forever.** The verb-driven heartbeat cannot distinguish a healthy agent quiet during a long edit/test cycle from one whose MCP gateway is broken (e.g. a corrupted `/app/.venv` so every gateway tool import raises) while its container stays up — and the reaper's live-skip would shield that broken agent indefinitely. The reaper now probes the gateway out-of-band (`docker exec`: does the gateway venv import its deps?) and, once it has been broken longer than `ROBOCO_GATEWAY_HEALTH_GRACE_SECONDS` (so a transient probe miss is tolerated), kills + evicts the container so it falls through to release + respawn; a healthy or inconclusive probe spares it. Gated by `ROBOCO_GATEWAY_HEALTH_ENABLED` (default-on reliability fix, in the panel Feature Flags). Builds on the shipped bash-guard `/app` block and reaper Docker-liveness fallback — together the third leg the live incident exposed.
|
||
- **Edit a task's sequence from the task details page.** A task's `sequence` (its order within siblings — lower runs first) was display-only with no way to change it from the UI. The details page's Dependencies tab now carries an inline sequence editor alongside the parent / dependency editors, and `PATCH /tasks/{id}` accepts a `sequence` field (owner or privileged role), so an operator can re-order sibling work directly.
|
||
|
||
### Fixed
|
||
|
||
- **Metrics "hours" fields serialized as JSON strings, crashing the panel.** `EXTRACT(epoch …)` returns `numeric` on PostgreSQL 14+, which asyncpg surfaces as a `Decimal` — and a `Decimal` serializes to a quoted JSON *string*. Every SQL-averaged hours field — `avg_cycle_hours` on the new Delivery scorecards, plus the pre-existing `avg_completion_hours` / `avg_blocked_hours` / `longest_blocked_hours` — was therefore a string, so the panel's `value.toFixed(…)` threw `toFixed is not a function` and blanked the tab. A single `_as_hours` coercion now rounds each to a real `float`, so every hours field is a JSON number. (Token/cost fields were already `float()`-cast and unaffected.)
|
||
- **The Main PM could not advance past its first coordination task — the developer single-task concurrency guards were deadlocking the coordinator.** A PM plans and delegates many root tasks in parallel; the real work then runs in the delegated cells, not in the PM's own hands. But the claim-time guards that correctly keep a *developer* to one task at a time — `already_active` (you have another claimed / in-progress task) and `paused` (you have a paused task, resume it first — which fires after `i_am_idle` auto-pauses the PM's own umbrella) — were applied to the PM as well, so once it held one root it could never plan a second: it thrashed between its claimed roots and respawned every few minutes, burning tokens for zero progress. These two guards are now skipped for the coordinator PM roles (`main_pm` / `cell_pm`): a PM may hold any number of roots in parallel, gated only by a genuine upstream **sequence dependency** (`unmet_dependency`), which still parks the task to `pending` until its dependency reaches a terminal state. As defense-in-depth the `paused` guard now also excludes the target task itself, so a PM re-entering its own paused umbrella can never self-block.
|
||
- **Task notes were invisible in the panel — the API response dropped them.** The `task_to_response` serializer (used by the task list and detail endpoints the panel reads) set `dev_notes` / `qa_notes` / `quick_context` but **omitted `pr_reviewer_notes`, `doc_notes`, and `notes_structured`**, and `TaskResponse` didn't even declare `notes_structured` — so the PR-reviewer's notes, the documenter's notes, and the structured PR-review verdict were always blank in the UI no matter what the agents wrote to the DB (the structured-content write-path and obligation gates work; the data simply wasn't being serialized). The builder now returns all note sections plus the structured source of truth. (`dev_notes`/`qa_notes` on an in-flight task are still legitimately empty until the developer submits / QA reviews.)
|
||
|
||
## [0.9.0] - 2026-06-23
|
||
|
||
### Added
|
||
|
||
- **Architectural Conventions Standard — a per-project, repo-canonical architecture map that gates where code may live.** Beyond the `make`-style checks (syntax, types, tests), each project can carry a `.roboco/conventions.yml` declaring which definition *kinds* belong in which modules, a toggleable rule set, custom regex rules, and waivers — so an agent can no longer land a Pydantic model inside a router or a lint suppression (a misplaced *helper* — any top-level function — warns rather than blocks). A tree-sitter validator CLI (Python + TypeScript) classifies every changed definition and emits findings; a `block`-level finding refuses a developer's `i_am_done` and the in-path PR gate's `pr_pass` with the offending `file:line` and a fix hint, and findings surface in QA's review evidence. The auto-derived defaults exclude test and documentation trees, count an explicit `db.commit()` in a route as legitimate (not a fat-route violation), and exempt a small allowlist of structurally-unavoidable framework suppressions (ruff `TC001`–`TC003`, pydantic `prop-decorator`). The committed file and repo scan are read from a dedicated project-level read clone the service ensures on demand — so the standard resolves even for a project created before it existed, with no manual workspace configuration. The file is auto-scaffolded on first clone, editable from a per-project Conventions tab in the panel, and a false positive is cleared by a waiver committed in the branch and reviewed in the PR. Gated by `ROBOCO_CONVENTIONS_ENABLED` (default off) and fully inert when off.
|
||
- **Agent runtime toolchain matching — agents build each target project under the Python that project actually requires.** The agent image bakes one interpreter, but the projects RoboCo builds don't all share it, so a self-gate could pass against the wrong runtime. The workspace now resolves each target's Python from its `requires-python` / `.python-version`, provisions the clone with `uv sync --extra dev --python <version>` (fetching the interpreter on demand), and records a `.git/.roboco-toolchain` marker. A guard refuses a developer's `i_am_done`, QA's `pass_review`, and the PR gate's `pr_pass` when the suite cannot be collected under the provisioned interpreter, so "verifying by reading source" can't masquerade as a passing gate. Gated by `ROBOCO_TOOLCHAIN_MATCH_ENABLED` (default off).
|
||
- **Provider overload circuit-break — a persistent model-API overload parks the provider instead of crash-retrying into it.** A sustained 529/500/503 (the SDK already retries transient ones) now trips the same park-and-probe break as a rate limit: the spawn gate queues further work for that provider and a background loop revives it when the overload lifts, instead of respawning the agent straight back into the failure and burning tokens. Gated by `ROBOCO_OVERLOAD_BREAK_ENABLED` (default on).
|
||
- **Structured content standard with obligated note sections.** Every agent-authored handoff (developer, QA, documenter, PR-reviewer, auditor, PM resumption) is now a validated structured model persisted as the source of truth, with the legacy text column derived from it through a single chokepoint. An anti-soup guard rejects filler and all-token-noise free-text across the flow and content verbs, structured PR-review findings render a generated GitHub comment, and each role's note section is obligated at its lifecycle transition the way journals already were.
|
||
- **User-facing documentation site.** A MkDocs Material site (source under `docs/`) is now built and deployed to GitHub Pages, publishing the organizational blueprint, role descriptions, task lifecycle, and how-to guides at the project's github.io site; the agent-facing RAG corpus under `docs/rag/` stays excluded from the published site. Documenter output is also committed into the project repository (not only the RAG knowledge store) so it ships through the open PR.
|
||
|
||
### Changed
|
||
|
||
- **RoboCo adopts its own architectural standard.** The repo now ships a canonical `.roboco/conventions.yml`, and the inline request/response models that lived in the `system` and `*_live` route modules were relocated to `roboco/api/schemas/` so the codebase passes its own placement gate (`no_models_in_routes` / `modular_cohesion` are now clean and enforced at `block`).
|
||
- **RoboCo's own `requires-python` floor is raised to `>=3.13`.** The codebase imports `tomllib` (3.11+) and runs on 3.13; the previous `>=3.10` floor made the toolchain resolver provision the self-hosted build at 3.10, where the suite cannot even be collected. Agent gate containers now also receive the test-database connection, so an agent's `make quality` runs the real, DB-backed suite instead of a coverage-collapsing unit-only subset.
|
||
|
||
### Fixed
|
||
|
||
- **Documentation now actually lands in the project repo.** A documenter's output reached a host-mounted, RAG-indexed knowledge store and (more recently) was committed onto the task branch — but in the documenter's own workspace clone, and nothing ever pushed that commit, so the PM merged the already-open PR without the docs and the deliverable vanished on merge. The documenter's `i_documented` now pushes the task branch before handing off (mirroring the developer's pre-QA push), so the doc commit rides the open PR into the repository; a push failure holds the task in `awaiting_documentation` for a retry instead of silently dropping the docs.
|
||
- **The conventions standard now resolves for projects created before it existed.** It previously read the committed `.roboco/conventions.yml` and the repo scan from `project.workspace_path` — a field only a manual API call ever 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 and reads from it, persisting the resolved path + HEAD (the backfill). The panel tab, the spawn-time ambient block, and the per-task constraints all resolve the committed standard with no manual setup.
|
||
- **The conventions ambient prompt block no longer truncates mid-line.** It now lists only modules that actually constrain a kind, and when the list would exceed its budget it trims at a line boundary with a `+N more` pointer instead of cutting a module in half.
|
||
- **The conventions read clone now stays current on a private repo.** Its refresh reused the orchestrator's token-less best-effort fetch, but the clone's remote URL is credential-stripped — 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 showed "auto-derived defaults" even after the standard was merged to the default branch). The refresh now performs a token-authenticated fetch + hard-reset, mirroring the clone.
|
||
- **Self-heal fix tasks dispatch autonomously instead of being stranded.** A self-heal task was opened `confirmed_by_human=false` and held out of dispatch until an "Approve & Start" — but that button only renders for board-reviewed Intake tasks, never for a self-heal task (`team=main_pm`, no board review), so there was no way to start it and it sat in `pending` forever. Self-heal now opens the fix task confirmed + assigned to the Main PM, so the dispatcher picks it up immediately. The fix still ships through the normal gates (dev → QA → PR review → the CEO's merge); the loop never starts, merges, or deploys.
|
||
- **Self-heal no longer reads the wrong branch and fails silently.** The CI-signal fetch filtered runs by `project.default_branch or "main"` — the only `"main"` fallback in the codebase (everywhere else falls back to `"master"`) — so a project whose default branch is `master` (like RoboCo) with an unset `default_branch` matched zero runs and the signal silently went dark: no fix task, no notification. The fallback now matches the rest of the codebase, and an armed self-heal that reads no CI signal (no/expired token, wrong branch, or a GitHub error) now logs a loud warning instead of an invisible no-op.
|
||
- **The toolchain gate no longer passes silently on an unverifiable workspace.** A `broken` interpreter still blocks; an `unknown` status — the smoke could not confirm the suite is collectable — now emits a warning when the gate proceeds, instead of slipping through unseen.
|
||
- **The crypto tests are hermetic.** The Fernet round-trip tests supply their own key instead of depending on `ROBOCO_ENCRYPTION_KEY` in the environment, so they pass in any gate container without the production secret being injected.
|
||
- **`ollama-init` is best-effort and gates startup on the models being present**, so a slow or unreachable model registry can no longer down a fully-cached deployment.
|
||
- **A PM can recover its own coordination task from `needs_revision`**, and lifecycle-transition notes are kept off the human-facing `quick_context` / `dev_notes` columns.
|
||
- **Panel:** a copyable task-id chip with a stable, non-shifting task header, clickable Branch / PR links with a branch-copy button, and clearer agent status badges.
|
||
- **Panel:** the per-project Conventions editor lays out in a responsive two-column grid (Module boundaries | Rules, then Waivers | Custom rules) with Recent violations full-width, inside a wider modal on large viewports — instead of one long single column. Each row's two cards share an equal height, and the Module-boundaries list scrolls internally so it matches the Rules card instead of running long. It collapses to a single column on mobile and is capped so it stays sane up to a 27" display.
|
||
|
||
## [0.8.0] - 2026-06-20
|
||
|
||
### Added
|
||
|
||
- **In-path PR-review gate — every assembled PR is reviewed before the PM merges.** A new `awaiting_pr_review` status sits between the work and the PM merge: the cell PM's `submit_up` opens the cell→root PR and the Main PM's `submit_root` opens the root→master PR, and each enters `awaiting_pr_review`, where a reviewer `pr_pass`es it on to PM review or `pr_fail`s it back for revision — the merge-level reject the PM previously lacked (motivated by a front-end/back-end seam bug that slipped straight through to master). Three new team-scoped cell PR-reviewers (backend, frontend, UX/UI) join the existing main reviewer, taking the company to 25 agents, each with its own first-class image and spawn manifest; leaf developer tasks and branchless coordination roots skip the gate. Ships migration 040 (the `awaiting_pr_review` enum value) plus the panel surfacing: a legible PR-review status badge, a dedicated "PR Review" kanban tab, and a PR-review column on the management board.
|
||
- **Panel test gate.** The Next.js panel gains a baseline vitest suite over its lib and stores, a `pnpm test` step enforced in the CI panel job, and a `make panel-gate` target, so panel changes are quality-gated the way the Python side already is.
|
||
|
||
### Changed
|
||
|
||
- **`get_team_metrics` reuses the shared `ACTIVE_STATUSES` constant** instead of re-listing the active task statuses inline, keeping the definition in one place.
|
||
|
||
### Fixed
|
||
|
||
- **The self-healing CI signal is now deterministic.** The regression watch defaulted to the latest completed run across all of the repo's workflows, so on a multi-workflow repo an unrelated green run — or a green run on an older commit — could mask a red CI run and the loop fired only intermittently. The signal is now scoped to the `ci.yml` workflow by default, pulls a window of recent completed runs and resolves the conclusion against the branch's current HEAD (a green re-run supersedes the failure; a stale green run can't hide it), and retries transient GitHub errors instead of reading one network blip as all-green.
|
||
- **Self-heal fix tasks are assigned to the Main PM agent, not just the `main_pm` team.** A team-only task fell to slow unassigned-team routing after the CEO approved it; it is now assigned to the Main PM agent up front so the orchestrator dispatches it straight away once approved. The confirmed-by-human hold that keeps the task inert until CEO approval is unchanged.
|
||
|
||
### Security
|
||
|
||
- **bash-guard denies git verbs hidden in command substitutions** — a `$(...)`- or backtick-wrapped git command could previously slip past the guard.
|
||
- **Transcript retention matches the encoded workspaces root at a path boundary**, so a sibling directory sharing a name prefix is no longer mistaken for the workspaces root during pruning.
|
||
- **The v1 role guard binds to a verified agent token before trusting a role claim**, so the role a request asserts is checked against its signed token rather than taken at face value.
|
||
- **pydantic-settings upgraded to 2.14.2** to pull in the fix for GHSA-4xgf-cpjx-pc3j.
|
||
|
||
## [0.7.0] - 2026-06-19
|
||
|
||
### Added
|
||
|
||
- **Grok agents on xAI's official `grok` CLI, on a SuperGrok subscription.** A new `roboco/llm/providers/` seam (an `AgentProvider` lifecycle ABC + a `ProviderRegistry` keyed by `ModelProvider`) lets the orchestrator drive agent backends other than Claude Code, and the first is Grok — running xAI's official `grok` CLI authenticated by a **SuperGrok subscription** rather than a metered API key, so a Grok workforce can't stall mid-task on out-of-credits. It reaches parity with the Claude path by construction: the same MCP gateway + tool-manifest wiring, per-role tool removal and git-operation deny rules, a prompt-injection guard on the task prompt, headless tool auto-approval, and per-agent token/cost capture from the grok session store. It covers both one-shot delivery roles and the interactive Intake (Prompter) and Secretary chats (per-turn `grok -p` with session resume, streamed turn-by-turn). The change is purely additive — only `GROK` routes through the registry; Anthropic / Ollama Cloud / self-hosted spawns are untouched — and ships migration 038 (the `grok` enum) + 039 (the seeded provider row), first-class `roboco-agent-grok` / `-prompter` / `-secretary` images wired into all three compose files and the release workflow, and a Settings provider card.
|
||
- **SuperGrok token auto-refresh.** The grok access token has a fixed ~6h server-set TTL and the CLI cannot refresh it headlessly — on an expired token it hangs forever at an interactive login prompt — so the orchestrator now mints a fresh token from the offline-access refresh token (xAI's OIDC `refresh_token` grant) before expiry and rewrites the shared `auth.json` in place, keeping every Grok agent's credential live with no recurring manual `grok login`. As a backstop the agent entrypoint refuses to start (exit 78) on a missing or expired token instead of hanging.
|
||
- **Self-healing CI loop (default-off).** RoboCo can now watch its own repository's CI and, on a detected regression, open a fix task that is held out of dispatch until the CEO approves it — then dispatch it through the normal delivery flow, so the company repairs its own breakages. It is dormant by default and armed from two Feature-Flags panel toggles; the CI signal is scoped to a single named workflow, and task origination is bounded by rolling and per-cycle caps so it can't flood the backlog.
|
||
- **Company Scorecard.** A company scorecard on the panel's Business Goals tab.
|
||
|
||
### Fixed
|
||
|
||
- **The PR-reviewer is no longer wedge-killed before it can post a review.** `pr_review_claim` now seeds the claim heartbeat like every other claim path; without it a Grok reviewer was treated as a silent (NULL-heartbeat) wedged container and killed before it could call `post_pr_review`, churning the task back to pending in a respawn loop.
|
||
- **Grok one-shot runs are observable, and their usage is captured.** The entrypoint streams agent activity to the container log live (`--output-format streaming-json`) instead of buffering it to a file until the run ends, and per-agent token/cost is read from the grok session store's actual cumulative-total field (it was silently reading `$0`).
|
||
- **Path-injection hardening of the Grok usage directory.** The agent id is validated and reduced to a single safe path component before it is used to build the per-agent usage path, on both the write/mount and finalize-read sides.
|
||
|
||
## [0.6.0] - 2026-06-17
|
||
|
||
### Added
|
||
|
||
- **Inbound PR review — the org reviews, and can take over, pull requests it didn't open.** A new read-only `pr_reviewer` role (a 22nd agent, its own first-class image and spawn manifest, migration 037) discovers inbound PRs, reviews the diff adversarially, and posts a single complete change-request as a real GitHub review on the PR itself — no agent-to-agent chatter. It covers external / fork PRs, gated by a configurable author allowlist, and — behind a second flag — internal org-repo PRs opened outside the agent task-flow (the org's own in-flight integration PRs are skipped, since a live task already owns their branch and they pass QA + PM review). Re-review is driven by the PR's head commit (an unchanged PR is skipped, new commits open a fresh review), and polling is repo-aware so a monorepo is no longer reviewed several times over. External-PR review is enabled by default in the shipped compose (with human-confirm on); internal-PR review is off by default. Both are flippable from the panel.
|
||
- **CEO decision queue + supersede for reviewed PRs.** Completed reviews surface in a PR-review queue in the panel — in-flight reviews are shown too, linking to the PR, so it never goes dark. From there the CEO can dismiss a review, or **supersede** the PR: the system cuts a roboco-owned branch off the contributor's commits and opens a Main-PM coordination task to finish and harden the work to our standards on that branch, open our own PR, and — once that replacement actually merges — close and link the contributor's PR. We never push to a contributor's fork.
|
||
- **Feature-flags panel.** A Settings → Feature Flags card toggles env-gated subsystems (external / internal PR review, web research, the strategy engine, pitch provisioning, RAG auto-update, transcript pruning) from the panel instead of hand-editing environment variables. A toggle persists in the existing settings store and takes effect on the next backend restart; an unset flag falls back to its environment / config default, and secrets (API keys, tokens) are never surfaced to the client.
|
||
- **Required-cells decomposition gate.** When a coordination task names the cells that must deliver it, the Main PM can no longer go idle having silently dropped one — `i_am_idle` is rejected until every named cell has a subtask, and the Main-PM prompt now insists on honoring explicitly-named cells rather than quietly dropping them. Inert until the marker is set, so existing flows are unaffected.
|
||
|
||
### Changed
|
||
|
||
- **Run from pre-built registry images.** A standalone registry compose runs the full stack — including every per-agent image, now extended to the Secretary and the PR-reviewer — from published images rather than a local build. Both deploy paths, the registry knobs, and measured idle / under-load resource usage are documented.
|
||
- **Documentation, for humans and agents.** The how-to guide is now a structured, multi-chapter walkthrough under `docs/how-to/` with a new business-workflow chapter (charter → Cockpit → Secretary, and the research / strategy / PR-review toggles); the published reference docs (README, usage, deployment, CLAUDE) were refreshed against the current code, with a CI guard that keeps documentation prose single-line. Agents also get richer in-context guidance: new RAG role docs for the Prompter, Secretary, and PR-reviewer, the 0.4.0 company layer (goals / research / strategy / provisioning) documented for them, and a refreshed guardrails surface.
|
||
|
||
### Fixed
|
||
|
||
- **The PR-reviewer no longer respawns in a loop.** Without a spawn manifest the reviewer had no flow verbs, so it could never claim its review and was respawned over and over (burning tokens); it now ships a role-scoped manifest and reliably claims its work. The supersede close-on-land path was also hardened — the contributor PR is retired only once our replacement PR has actually merged, not merely when the umbrella task completed.
|
||
- **A CEO-rejected coordination root no longer deadlocks.** A product-linked coordination task the CEO sends back to needs-revision is re-dispatched to its owning PM (and the readiness gate now accepts a PM on a coordination root in that state), instead of sitting unowned forever because the developer dispatcher skipped it.
|
||
- **Panel UI standardization + usability pass.** A panel-wide pass plus targeted fixes: the Settings grid layout, the Journals and Kanban scroll regions, the agent-list item, the Projects table, kanban cards whose text overflowed, the PR-review queue's empty state, and the Secretary chat composer buttons.
|
||
|
||
### Internal
|
||
|
||
- DB-backed and httpx-mocked test coverage for the inbound-PR read and lifecycle paths (ingest / dedup / classify / claim / complete / supersede); a cyclomatic-complexity refactor of the git PR-creation and lifecycle-validation code to clear the xenon gate; and the cell-PM / main-PM role docs corrected to the real `delegate` signature and cross-linked to each other.
|
||
|
||
## [0.5.0] - 2026-06-16
|
||
|
||
### Added
|
||
|
||
- **Acceptance-criteria & decomposition guardrails.** Every task's acceptance criteria now carry stable per-criterion ids, and each decomposed subtask records which parent criteria it is responsible for (`covers_parent_criteria`). Two gates build on that linkage: a PM can no longer go idle leaving a parent criterion with no subtask responsible for it (the decomposition floor), and a parent can no longer complete / submit up / escalate to the CEO unless every one of its criteria traces to a child that passed QA on it (the roll-up gate). PMs see live coverage in their briefings (`parent_ac_coverage`, `unclaimed_parent_acs`) after each `delegate`. Safe-by-construction: every gate stays inert until a PM starts declaring coverage, so existing decompositions are never blocked. (Migration 036.)
|
||
- **Per-dev sequenced code queues.** A cell PM now delegates each developer its full queue of code subtasks up front instead of one task at a time. Both cell developers build in parallel, and each works its own queue one task at a time, in order — enforced by a per-lane dispatch barrier, with leaf PRs still merged in sequence into the shared cell branch. The old "two code subtasks per parent" ceiling is removed; the 12-subtask hard cap and a same-title duplicate guard remain.
|
||
- **Unified Business page.** The Company Goals, Secretary, and Pitches pages are consolidated into one tabbed **Business** page (Goals / Secretary / Pitches), modeled on the Knowledge Base page with deep-linkable `?tab=` URLs. A single sidebar entry replaces four.
|
||
|
||
### Changed
|
||
|
||
- **Company Goals, Secretary, and Pitches brought to the panel's standards.** Skeleton loading and offline/error states, structured fields instead of raw JSON dumps, required-note confirmation dialogs for pitch and directive decisions, and markdown rendering in the Secretary chat.
|
||
|
||
### Removed
|
||
|
||
- **The standalone Cockpit page.** Its data duplicated the Dashboard and Metrics; its one unique element — the strategy-engine "needs your attention" signals — was relocated to the Dashboard, served by a new lightweight `GET /api/cockpit/signals` endpoint. The `/cockpit`, `/company-goals`, `/secretary`, and `/pitches` panel routes are all retired (404); the Goals, Secretary, and Pitches views now live under `/business?tab=…`.
|
||
|
||
### Fixed
|
||
|
||
- **Agent MCP/SDK servers no longer stall on spawn.** They launch with `uv run --no-sync`, so a workspace clone whose lockfile has drifted from the baked image no longer triggers a multi-minute dependency re-sync that left the gateway tools stuck "pending" and the developer respawning in a loop.
|
||
- **`open_pr` no longer fails on a missing base branch.** `create_pr` auto-creates and pushes the PR's base branch off the default branch when it is not yet on the remote, instead of returning a GitHub 422.
|
||
- **Admin status overrides restore task ownership.** Forcing a blocked task back to pending / in_progress now restores its pre-block assignee, so an escalated code task no longer re-enters the pool still owned by a PM and is dispatched to that PM as if it were a developer.
|
||
- **A developer can idle past its own queued work.** With per-dev queues, a dev whose current leaf has moved to QA now idles cleanly while its later queue items wait their turn (the orchestrator respawns it when the lane clears), instead of looping on the idle guard or claiming the next leaf out of order.
|
||
- **26 verified panel UI bugs** across the dashboard, kanban, task detail, and API layer: consistent priority labels and badge sizing, dark-mode coverage, kanban drag-and-drop that prompts for the required audit note, auto-scroll in the message and mentor-chat views, corrected WebSocket reconnect counting, `PATCH` (not `PUT`) for partial task updates, working "Activate Task" and "Start Revision" actions for backlog and needs-revision tasks (no more dead-end menus), the previously-dead "New / Generate Report" buttons, a duplicate agent id, a "0h ago" timestamp, and more.
|
||
|
||
### Internal
|
||
|
||
- Verb-table generation no longer emits tables for the driver-based roles (prompter, secretary), whose real tools live in their SDK drivers rather than the gateway verb surface; and the `_briefing_for` typed stub was aligned with its implementation so the composed choreographer type-checks under full mypy.
|
||
|
||
## [0.4.0] - 2026-06-15
|
||
|
||
### Added
|
||
|
||
- **Business Goals — the company charter.** A single CEO-owned charter (north star, prioritized objectives, constraints, operating policy) injected compactly into every agent's briefing so all work is goal-aware. `GET /api/company-goals` (any agent) / `PUT` (CEO-only), with a panel editor.
|
||
- **Web research for the Board and PMs.** Pluggable `web_search` / `web_fetch` exposed through a `roboco-search` MCP server backed by `/api/research/*`, with Tavily / Brave / Exa adapters and a graceful no-op when no provider is configured. The provider key stays server-side — agent containers never make the external request themselves — and a per-agent daily quota (Redis, fail-open) bounds cost.
|
||
- **Pitch → approve → provision.** The Board proposes a product (a "pitch"); on CEO approval the system provisions a GitHub repo per target cell, registers a Project for each (and a Product when multi-cell), and seeds one Main-PM delivery task — reusing the existing Product / coordination-task machinery. Default-off: with no provisioning token configured, approval is refused and nothing is created.
|
||
- **Autonomous strategy engine (dormant).** An optional second engine that watches the company against its standing goals and surfaces drift, idle, and long-stranded blocked work to the CEO (notify-only — it never spends, builds, or auto-approves). Off by default; the delivery lifecycle is unchanged.
|
||
- **The Secretary — the CEO's chief-of-staff.** A live conversational agent (its own role, distinct from the Prompter) the CEO chats with in the panel. It acts only under the CEO's command: it reads company state and relays dictated messages directly, but high-impact actions — editing the charter, starting / cancelling / overriding tasks, approving a pitch, announcements — are queued and run only after the CEO's explicit confirmation (the gate list). Its authority is HMAC-scoped to the secretary role and routed through the existing enforcement, never a parallel permission model.
|
||
- **The Cockpit.** A read-only `/cockpit` view answering "is the business winning, what's happening, what needs me" — the charter, delivery counts, 30-day spend vs the budget cap, pending pitches, and the strategy engine's signals. Honestly stamped `basis: proxy` (a proxy until real launches).
|
||
|
||
All of these are additive and opt-in or default-off — an unconfigured deployment behaves exactly as before.
|
||
|
||
## [0.3.0] - 2026-06-15
|
||
|
||
### Added
|
||
|
||
- **In-house RAG engine.** Replaced the piragi/torch retrieval stack with an in-house pgvector engine (asyncpg), then added **hybrid retrieval** — pgvector cosine fused with Postgres full-text ranking — retiring HyDE, plus an embed-once / concurrent-search pass that cut multi-index query latency.
|
||
- **Self-hosted LLM provider** with dynamic model discovery, so agents can run against a local or self-hosted model endpoint.
|
||
- **Quality gates at the source.** Developers run a fast quality gate at `i_am_done` and the full fast gate (including complexity) at their desk; QA requires a per-acceptance-criterion verdict before passing; cells run two developers in parallel with split-before-claim sizing.
|
||
- **Board redraft loop** — the Board can send a drafted task back to intake for an in-context re-draft before it starts.
|
||
- **Transcript retention** — a background sweep prunes old agent transcripts, with a panel-tunable retention window.
|
||
- **`tests/` type-gated under mypy** — the whole test suite now type-checks in CI.
|
||
|
||
### Fixed
|
||
|
||
- **PR-divergence respawn-loop meltdown.** Capped the PM respawn loop-gate, added CEO god-mode status override, a PR-conflict auto-resolver (rebase → close-superseded / re-merge / escalate), and sequence-ordered sibling merge; the dispatcher can now claim an ownerless `awaiting_pm_review` task without transitioning it.
|
||
- **Git robustness.** Fall back to a permitted merge method when the repo refuses the requested one, and retarget a PR's base to the default branch when the resolved base is missing on the remote.
|
||
- **RAG outage.** Migrated the live `chunks_*` tables to the in-house schema (offline-renderable migration), closed engine audit gaps, decoded jsonb metadata returned as a string by asyncpg, and kept the embedding model resident to stop ingest timeouts.
|
||
- **Panel.** Fixed task lifecycle (updates, merge, reassignment, copy), responsive grids + mobile overflow, the status dropdown duplicating the current status, the orchestrator-status reachability signal, and surfaced the CEO "Approve & Start" gate so it can't be missed.
|
||
- **Usage attribution.** Agent transcripts are attributed by an orchestrator-assigned session id, fixing zeroed token/cost capture for review-role agents.
|
||
- Composed the prompter role layer for the intake agent; aligned auditor channel permissions; made the app route-registration test robust to FastAPI 0.137; cleared an xenon complexity failure and fixable test warnings.
|
||
|
||
### Security
|
||
|
||
- Documented that WebSocket authentication is REST-only and `/ws/system` is unauthenticated.
|
||
|
||
## [0.2.0] - 2026-06-11
|
||
|
||
### Added
|
||
|
||
- **Provider rate-limit handling.** End-to-end backpressure for LLM-provider 429s: a Redis-backed `RateLimitStateTracker`, a spawn gate that **queues** (never drops) work while a provider is rate-limited, agent parking via `i_am_blocked(reason="rate_limited")`, and a background probe-and-resume loop that auto-revives parked agents when the limit lifts — escalating to the CEO after repeated failed probes. Surfaced live in the panel via a rate-limit banner.
|
||
- **Token usage & cost analytics.** Per-agent-session token capture read from the Claude Code transcript (`/usage/sync`), persisted to spawn-session rows and daily rollups, with provider-aware pricing (Anthropic models priced; local/Ollama models intentionally $0). Visible on the usage dashboard.
|
||
- **`/ws/system` operator WebSocket stream** with a `websocket_bridge` that forwards system events from the event bus to panel clients in real time — the rate-limit lifecycle and live token/cost usage (`USAGE_UPDATE` / `USAGE_SNAPSHOT`), so the dashboard's "Token Usage & Cost" panel updates over the socket and falls back to HTTP polling when it drops.
|
||
|
||
### Fixed
|
||
|
||
- Agent workspaces now install the project's `dev` extra (`uv sync --extra dev`) so spawned agents have the full `make quality` toolchain (ruff/mypy/xenon) and can gate their own work — closing the gap that let lint/type/complexity debt merge unchecked.
|
||
- Token-usage capture: the dashboard previously recorded zeros because nothing populated the per-session counters.
|
||
- Panel rate-limit endpoint shape (`/api/system/rate-limits` returns the `{ entries: [...] }` envelope the dashboard expects) and the doubled `/ws/ws/system` WebSocket path.
|
||
- Control-panel logo and all `/public` assets returning 500 — the panel image copied them without chowning to the non-root runtime user.
|
||
- Provider-aware pricing (Opus corrected to $5/$25 per 1M; non-Anthropic models no longer warn or mis-price).
|
||
|
||
## [0.1.0] - 2026-06-09
|
||
|
||
### Added
|
||
|
||
- Initial public release of **RoboCo** — an open-source AI agent "company": a virtual organization of 20 AI agents and 1 human CEO that plans, builds, reviews, documents, and ships software.
|
||
- Organizational hierarchy: on-demand Intake, Board (Product Owner, Head of Marketing, Auditor), Main PM, and Backend / Frontend / UX-UI cells.
|
||
- **Task Assistant** (the intake Prompter): a live, codebase-aware chat that interviews the CEO and drafts a well-formed, board-ready task — objective, per-cell breakdown, and acceptance criteria — then launches it into the lifecycle (Board review, or straight to the Main PM).
|
||
- Agent gateway (`roboco-flow`, `roboco-do`) backed by the server-side Choreographer; intent-verb tool surface per role.
|
||
- Task lifecycle state machine with role-based transitions and git workflow (PR-before-QA, CEO approval for major work).
|
||
- A2A protocol, journals, channels/notifications, kanban, and RAG (piragi + pgvector) knowledge base.
|
||
- Next.js control panel (`panel/`) behind a single nginx entry point.
|
||
- Multi-agent workspace management with per-project encrypted git tokens.
|
||
|
||
[0.5.0]: https://github.com/rennf93/roboco/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/rennf93/roboco/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/rennf93/roboco/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/rennf93/roboco/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/rennf93/roboco/releases/tag/v0.1.0
|