mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
ec6558e168d8778524a1caee0bf1b765c9a074f7
17
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e9ca7d4036 |
Delegation detail-fidelity + PM-loop hardening (#541)
* feat(gateway): delegation detail-fidelity — details survive hand-off, both directions
Details thinned out at every delegation hop: a PM child task mapped to no
parent criterion was legal (coverage only surfaced at submit_up, after the
whole wave ran — a 12-subtask docs tree grew through 8 review rounds that
way, one child titled 'docs page and route wrapper' shipping only the
page), and QA could pass work on a gestalt read (a 4-scene video brief
shipped 3 scenes past every gate because the features existed only in
prose). Three chokepoint gates:
- delegate (down): every child must declare covers_parent_criteria
resolving against the parent's real acceptance criteria — no mapping or
an unresolvable ref rejects naming every offending child and the valid
criteria; the success envelope carries parent_ac_coverage
{covered, uncovered} so a wave-planning PM sees remaining gaps in the
same turn. Full coverage stays enforced at submit_up (waves stay legal).
- pass_review (up): mandatory criteria_verified — one {criterion,
evidence} entry per task AC, matched by the findings ledger's
id-or-exact-text matcher, evidence soup-checked and capped; rejects
naming the unverified criteria; entries render deterministically into
qa_notes as '[AC] <criterion> — verified: <evidence>' lines. The old
count-only ac_verdicts gate is superseded (arg kept for back-compat).
- video briefs (structured detail at origination): an enumerable feature
list (release highlights, or input_props.highlights carried onto a
reject re-author) becomes its own scene acceptance criterion, bounded to
the AC caps; a re-author without highlights carries the
feedback-addressed criterion instead.
Extracted findings.py's criterion matcher into shared unmatched_criteria /
uncovered_acceptance_criteria instead of duplicating it; criteria_verified
joins the WAF free-text exclusion set like findings/issues.
* fix(gateway): break the block/unblock wedge — four hardening fixes from the live PM loop
A cell task looped fe-pm/main-pm block/unblock for hours (10 cycles, 43
spawns): a transient GitHub API error resolving CI became an unwaivable
blocker finding whose own fix text said no code change was required, the
submit freshness guard then demanded a commit no finding called for,
escalate_up auto-blocked, and main-pm's correct recovery plan 422'd on
the approach length cap, degrading it to a bare unblock. Four fixes:
- pr_pass CI-unresolvable refusal is now explicitly transient-worded:
retry pr_pass shortly, do NOT pr_fail over a CI-status lookup error —
a platform blip is not a code finding
- submit freshness guard grants ONE unchanged-head resubmission per
head sha when the findings ledger has zero open rows (all addressed
without code changes) — stamped via the resubmit_unchanged_head
marker so the same head can never loop a second time
- unblock carries a flip breaker: block_flip_count marker, and at the
third flip a one-shot CEO notification flags the task as structurally
wedged (unblock itself still succeeds — the breaker signals, it does
not wedge recovery)
- i_will_plan's approach cap truncates at 800 chars instead of
rejecting — an over-detailed plan must never cost the PM its turn
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
3849c1737e |
feat(video): switch 0.19.0 renderer Remotion → HyperFrames (HTML-native, Apache-2.0) (#314)
* feat(video): rewrite sidecar render core to HyperFrames (in place)
* feat(video): convert motion compositions from Remotion TSX to HyperFrames HTML
* refactor(video): rename render client to video_renderer_client (renderer-agnostic)
* chore(video): rename remotion-renderer prose in test_video_pipeline docstrings
* chore(video): rename sidecar to video-renderer + add system ffmpeg for HyperFrames
* chore(video): rename stray remotion-renderer refs in sidecar + py docstrings (controller cleanup)
* chore(video): fix stale Remotion API names in Dockerfile comment (controller cleanup)
* docs(video): rewrite video-engine prose for HyperFrames + add map entry + folded prose fixes
* docs(video): add trailing newline to docs/map/video-engine.md (controller cleanup)
* chore(video): drop internal spec refs + minio/test suppressions (folded hygiene)
* fix(video): reclaim outDir on createRenderJob throw + hide empty 4th highlight
Final whole-branch review (Opus) triaged two FIX items from the SDD nits
ledger; the rest ship as-is.
- render.js: a synchronous throw from createRenderJob (post-mkdtemp, not
awaited) left an empty outDir on disk — the outer catch only reclaimed
extractDir. Reclaim outDir too when it exists, and correct the stale
comment that claimed the out dir was never created.
- {vertical,square}.html: the 4th highlights <li> lived in the DOM hidden
only by JS, so a no-JS / failed-script render would show an empty bullet.
Start it style="display:none" and reveal on populate, so an unscripted
render shows nothing instead.
Vitest smoke (release-announcement.test.js) 4/4 green; render.js syntax
checked. Python suite untouched by this fix (JS/HTML only).
* fix(video): type _override_db yield as AsyncSession | None
T7 widened _build_app's db_session param to AsyncSession | None (to drop the
4x # type: ignore[arg-type] on the DB-independent _build_app(None, ...) calls)
but left the inner _override_db fixture typed AsyncIterator[AsyncSession] —
so 'yield db_session' yielded AsyncSession | None into a declared AsyncSession,
and mypy failed at test_video_routes.py:177 ('Incompatible types in yield').
The DB-independent media tests pass db_session=None deliberately: their route
uses a monkeypatched task service and never awaits the session, so yielding
None is safe at runtime. Type the override's yield as AsyncSession | None to
match — no cast, no # type: ignore, no assert, runtime behavior unchanged.
The 3 media tests (3 passed) and the 19 db-gated tests (skipped locally) hold.
* chore(gate): skip .superpowers scratch in markdown prose gate
reflow_md.py walks the filesystem via rglob('*.md') and skips tooling dirs
(.venv, .mypy_cache, .pytest_cache, ...) but not .superpowers/ — the
superpowers SDD workflow's scratch dir (briefs, reports, progress ledger,
all gitignored). A dev running SDD locally would hit a false markdown-prose
gate failure on those transient files. Add .superpowers to SKIP_DIRS,
consistent with the existing tooling-scratch exclusions.
* fix(video): validate composition_id to close path traversal (CodeQL)
compositionId flowed unvalidated from the POST body into path.join
under extractDir/motion/compositions/, so a '../..'-style value could
escape the composition dir (CodeQL: Uncontrolled data used in path
expression). Validate at the trust boundary in server.js
(/^[A-Za-z0-9_-]+$/) and add a path.resolve + startsWith containment
check in render.js so it stays safe regardless of caller.
* fix(mcp): send X-Agent-Token + X-Agent-Team from flow/do servers
flow_server._build_headers and do_server._build_headers constructed
only X-Agent-ID/Role/Correlation-ID, omitting X-Agent-Token and
X-Agent-Team (unlike ApiClient._get_agent_headers used by the other
MCP servers). Latent since the gateway refactor — surfaced when
ROBOCO_AGENT_AUTH_REQUIRED=true was armed on the NAS, 401-ing every
flow/do verb with 'Missing X-Agent-Token header'. Add both headers
(mirroring ApiClient) so the HMAC gate passes. Tests assert the
headers are now injected.
* [video-engine] Per-project video_engine_enabled opt-in toggle
Mirrors ci_watch_enabled (migration 048): the global
ROBOCO_VIDEO_ENGINE_ENABLED flag arms the subsystem; the new
projects.video_engine_enabled column (migration 063) opts a repo into
authoring against its motion/ dir. VideoEngine._opted_in_project no-ops
open_video_task at the single chokepoint covering all three trigger
paths (on-release, on-spotlight, CEO on-demand) until the operator
flips it in the panel edit-project dialog. Existing projects stay
opted out (server_default=false).
* fix(auth): send X-Agent-Token + X-Agent-Team from all agent->API call sites
The prior fix (
|
||
|
|
60c64c70e8 |
fix(run-hardening): break PM decision-gate, stale-agent, and empty-diff loops (#255)
Forensic triage of a 24h run reconstructed the dominant gateway.rejected loops from the audit_log. After earlier deploys fixed the i_will_plan crash and the open_pr push-gap, three real, recurring-capable burn loops remained. This fixes them at the architecture level, not by prompt-nagging. journal:decision write-then-gate (the dominant completion-path blocker): PM decision-point verbs required a separate note(scope='decision') call before the verb, which loaded/weak models forget to chain — so complete and unblock hit a tracing_gap (journal:decision missing) and respawn-looped, stranding finished tasks forever. Each verb now auto-records its OWN rationale as the journal:decision before the gate runs (the proven i_am_blocked -> write_struggle pattern), so the gate passes off real, persisted reasoning. unblock gains a required `reason` (threaded MCP tool -> request schema -> routes -> choreographer); delegate derives the decision from its title + description; complete/submit_up/submit_root/escalate_up/ escalate_to_ceo reuse their existing notes/reason. The gate still runs as defense-in-depth; the auto-record is idempotent within the decision window and best-effort. Adds JournalService.write_decision and Choreographer._ensure_pm_decision. open_pr empty-diff 422: an overlapping-decomposition leaf with zero commits vs its base makes GitHub 422 "No commits between ...". The generic invalid_state "retry" looped the dev 15x on one task. open_pr now steers to a terminal i_am_blocked hand-off so the PM completes or cancels the redundant leaf. owns_task stale-agent loop (41x): a superseded agent (task reassigned away) calling i_am_done/open_pr got a PRECONDITION_OWNERSHIP tracing_gap it read as a fixable precondition and retried forever. Both verbs now short-circuit with the clear not_authorized "no longer yours -> give_me_work" steer that resume/unclaim already use. RAG docs updated for the new unblock(reason) signature; CHANGELOG entries added under 0.11.0 (unreleased). open_pr refactored into _open_pr_preflight_rejection + _open_pr_failure_env to stay within the return-count and complexity budgets. Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
8cf697816f |
feat(content): obligate role note sections like journals
Completes the note(scope='handoff') write-path (WIP
|
||
|
|
b97465a631 | test(content): assert pr_fail issues land in pr_reviewer_notes (gate path) | ||
|
|
0fba4eed01 | refactor(content): move orchestration markers off quick_context to typed jsonb | ||
|
|
5fe1e6df58 |
feat: in-path PR-review gate — per-cell + main reviewers (#229)
* feat(lifecycle): add the in-path PR-review gate status + reviewer verbs
Insert awaiting_pr_review between the assembled-PR submit and the PM merge,
giving the merge level the rejection capability it structurally lacks — today
only qa_fail and ceo_reject ever reach needs_revision, so a PM review is a
merge button with no teeth.
- New Status awaiting_pr_review + submit_for_review / pr_pass / pr_fail actions
(pr_pass -> awaiting_pm_review, pr_fail -> needs_revision, mirroring the QA gate).
- Reviewer verbs claim_gate_review / pr_pass / pr_fail, and a main-PM submit_root
verb (the root analogue of the cell PM's submit_up; opens the root->master PR).
- Extend the self-review-symmetry validator to the new sign-off actions.
- Mirror the value into the ORM TaskStatus enum + the A2A state map, and add the
postgres taskstatus enum value (migration 040, forward-only like 037).
- Regenerate the per-role verb tables; add gate spec tests.
Spec surface only; the gateway methods + dispatch are wired in follow-ups, so the
verbs are advertised but dormant (flow_server tolerates unregistered verbs).
* feat(identity): add the three cell PR-review-gate reviewers
The in-path gate needs a reviewer per cell so each cell's assembled cell->root
PR is reviewed by a stack-specialized agent, while pr-reviewer-1 serves the
root->master gate (and keeps doing inbound external PRs).
- be/fe/ux-pr-reviewer: PR_REVIEWER role, team-scoped (so dispatch routes each
cell's gate to its own reviewer); seeded identities + ROLE_TEAM_RULES + names.
AI agent count 22 -> 25.
- They reuse the existing roboco-agent-pr-reviewer image (AGENT_IMAGES maps the
three slugs to it, as be-dev-1/-2 share one image) — no new image.
- Tracing table: pr_pass/pr_fail require a learning entry (parity with
post_pr_review), submit_root mirrors submit_up, claim_gate_review is waived
(its tracing applies on pr_pass/pr_fail) — completes the verb surface added
in the prior commit.
- Update the roster-pinning identity tests.
* feat(gateway): wire the in-path PR-review gate end to end
Make the assembled-PR review gate operational across the choreographer, the
TaskService transitions, and the v1 flow surface.
- TaskService: submit_for_review (in_progress→awaiting_pr_review), pr_gate_claim
(no-transition reviewer claim), pr_pass (→awaiting_pm_review), pr_fail
(→needs_revision); mirror qa_pass/qa_fail (clear claim, actor-mismatch warn,
issues appended for the PM's revision). VerbRunner gains the matching atomic
handlers + a create_root_pr side effect.
- Repoint submit_up to compose submit_for_review (cell→root PR enters the gate),
and add a main-PM submit_root verb (opens the root→master PR, enters the gate).
- Split main_pm_complete: a code root must pass the gate first (requires
awaiting_pm_review; rejects an in_progress code root toward submit_root and no
longer reopens the PR), while a branchless coordination root still walks
straight through, ungated.
- PRGateMixin (claim_gate_review / pr_pass / pr_fail) composed onto the
Choreographer; flow_server forwarders + v1 routes (pr_reviewer + main_pm) +
request schemas.
- Tests: gate spec + the updated submit_up / main_pm_complete expectations + new
real-DB integration tests driving submit_for_review→pr_gate_claim→pr_pass and
pr_fail through the real enforcement layer.
* feat(orchestrator): dispatch the in-path PR-review gate
Make the gate live in the dispatch loop.
- _dispatch_pr_gate_work: route awaiting_pr_review tasks to reviewers by level —
a cell→root task to its cell reviewer (be/fe/ux-pr-reviewer), the root→master
task to pr-reviewer-1. The reviewer self-claims via claim_gate_review (no
pre-claim, mirroring the external-PR dispatcher); registered in
_dispatch_all_work. _select_agent_for_cell learns the pr_reviewer role.
- _build_pr_gate_prompt: anchors the reviewer to the parent objective + full
acceptance criteria + the FE<->BE contract, then pr_pass / pr_fail.
- _readiness_check_role_for_status: awaiting_pr_review -> pr_reviewer.
- Fail routing: pr_fail reassigns the failed assembled task to its PM
(_revision_pm_for_task: cell PM for a cell team, Main PM for the root), and the
revision dispatcher is generalized from coordination-roots-only to any
PM-owned needs_revision task so the gate-failed task is re-coordinated instead
of deadlocking.
* docs: document the in-path PR-review gate + the cell reviewers (22→25)
Reflect the shipped gate across the canonical + RAG docs.
- CLAUDE.md: agent count 22→25, the cell reviewers in the org chart, an
awaiting_pr_review state + the gate transitions + a gate note in the lifecycle
section, and submit_root / claim_gate_review / pr_pass / pr_fail in the verb
surface table.
- docs/rag/architecture: org-structure (count, cell-reviewer roster, cells
table), agent-uuids (be/fe/ux-pr-reviewer rows), agent-model (role + team
rows).
- docs/rag/roles/pr-reviewer: the in-path gate section + the gate verbs.
- Wrap reviewer.id with UUID(str(...)) in the gate DB tests for mypy.
* docs: finish the gate doc sweep across README + RAG + generated artifacts
Catch the remaining surfaces beyond the canonical docs.
- README + how-to: agent count 22→25, the 6-agent cells (+ PR Reviewer), the
main reviewer's root→master gate role.
- RAG: permissions + tool-permissions + task-tools list the gate verbs
(claim_gate_review / pr_pass / pr_fail) for pr_reviewer; regenerate the
lifecycle artifacts (intent-verbs, status-transitions, the per-role
lifecycle-*.md prompts, panel lifecycle.json) from the spec via
build_lifecycle_artifacts.py so they carry the new status + verbs.
* fix(migration): shorten the 040 revision id to fit alembic_version VARCHAR(32)
The revision id '040_taskstatus_awaiting_pr_review' is 33 chars; alembic's
alembic_version.version_num column is VARCHAR(32), so recording the migration on
a real 'alembic upgrade head' failed with 'value too long for type character
varying(32)' (surfaced on the NAS deploy). The test suite missed it: the test DB
is built via Base.metadata.create_all and the parity test only renders SQL
offline, so nothing actually applied the migration chain.
- Rename to '040_awaiting_pr_review' (22 chars).
- Add a guard test asserting every revision id fits the VARCHAR(32) column.
- Verified by applying the full chain 001->040 against real Postgres: it now
reaches head and records '040_awaiting_pr_review' without truncation.
* fix(migration): land the actual 040 revision-id shortening + guard test
The prior commit captured only the file rename (git add aborted on the deleted
old path), leaving the long revision id and missing the guard test. This commit
carries the real content: revision id '040_awaiting_pr_review' (22 chars) and the
revision-id length guard. Re-verified against real Postgres — the full chain
reaches head and records the short id without truncation.
* fix(product): flush cell deletes before inserts when re-mapping projects
Editing a product's cell->project map (PATCH /api/products/{id}) 409'd with
'duplicate key value violates unique constraint uq_product_projects_product_team'
whenever a team already had a mapping. _replace_cells clears the old rows and
appends the new ones, but within a single flush SQLAlchemy orders INSERTs before
DELETEs for the same table, so the new (product_id, team) rows collided with the
not-yet-deleted old ones. Flush the deletes first.
Pre-existing bug (unrelated to the PR-review gate); surfaced on the NAS. New
real-Postgres regression test re-maps all three cells to different projects —
it fails with the unique violation without the fix and passes with it. The
existing update test only changed WHICH team was mapped, so it never collided.
* fix(gateway): let main_pm submit_root past the shared submit-up guard
submit_root reused the cell PM's _submit_up_ownership_guard, which
hardcoded agent.role != cell_pm and rejected the Main PM with
"submit_up is reserved for cell_pm". A branch-bearing code root could
then never close: submit_root bounced to complete, while complete
required awaiting_pm_review (reachable only via submit_root) and bounced
back — a circular rejection.
Both callers already run the spec gate (can_invoke_intent), which
enforces submit_up→cell_pm and submit_root→main_pm, so the guard's role
re-check was redundant for submit_up and wrong for submit_root. Broaden
it to accept either PM role as a defense-in-depth non-PM reject.
Adds the first choreographer-level submit_root test (the gap that let
this ship).
* fix(gateway): proactively steer both PMs to their bubble-up verb
The submit_root deadlock had a sibling steering gap: nothing told a PM
which verb opens the gate. The delegate next-hint said only 'i_am_idle
when done', and complete's in_progress rejection named submit_root for
the Main PM but left the Cell PM with a bare 'not ready for completion'
— no submit_up pointer, the same guess-the-verb trap.
- delegate hint now names the role-correct verb (root → submit_root,
cell parent → submit_up) proactively, before any rejection.
- cell_pm_complete's in_progress rejection now steers to submit_up,
mirroring the Main PM's submit_root gate hint.
Tests cover both the cell-PM steer and the role-aware delegate hint.
* docs: correct who-merges-which-PR across the gate docs + complete description
Audit of the gate docs found the merge actors mis-stated in several
places — the exact ambiguity that risks 'the reviewer/PM merges the root
PR' confusion:
- complete IntentSpec description said 'Main PM merges root PR' — false;
main_pm_complete escalates and the CEO merges root→master. Corrected
(propagated to intent-verbs.md, lifecycle.json, generated role prompts
via build_lifecycle_artifacts.py).
- task-tools.md: submit_up target was awaiting_pm_review (should be
awaiting_pr_review); Main PM flow had no submit_root — added it.
- README.md: lifecycle diagram now shows the awaiting_pr_review gate.
- cell-pm.md / main-pm.md: dropped the stale 'submit_up hands work to the
Main PM who merges your cell branch' model — the cell PM merges its own
gated cell→root PR; the Main PM owns the root + submit_root; the CEO
merges master. Added submit_root to the main-pm manifest.
- git-commits.md, pr-creation.md, tool-permissions.md, git-tools.md:
stopped attributing root→master PR opening to complete (it's submit_root).
No behavior change; verb wiring + state machine verified gap-free this
session (the pr_fail→needs_revision→PM respawn loop closes correctly).
* fix(orchestrator): stop closure respawn waiting the reaper window
A PM that finished its subtasks and idled left its parent 'paused' with a
fresh last_heartbeat_at. _is_recently_paused gated closure respawn on
_claim_heartbeat_ttl — the REAPER window (stale_claim_reap_seconds: 600s
default, 1800s on the NAS) — so the parent sat untouched for up to 10-30
minutes before its PM was respawned to close it. The whole chain stalled
behind it.
The race that guard actually protects against (i_am_idle auto-pauses, then
the agent is marked IDLE + its container tears down) is seconds, and the
live-session case is already covered by _is_agent_active. Introduce a
dedicated short debounce (pm_closure_recently_paused_seconds, default 45s)
and gate closure on that instead.
The existing test fixture masked this by setting _claim_heartbeat_ttl to
claim_stale_seconds (180s), not the production reaper value. Fixture now
mirrors production; adds a regression test that a parent paused past the
debounce but within the reaper window respawns immediately.
* feat(gate): post the in-path review verdict on the assembled PR
The in-path gate previously left no trace on the PR it gated — pr_pass /
pr_fail were pure status transitions. Now each verdict is posted as a
GitHub review on the assembled PR itself (server-side, bot account), so
the decision is visible on the very PR the PM merges.
- pr_pass → APPROVE, pr_fail → REQUEST_CHANGES on a cell→root PR.
- The root→master PR ALWAYS gets a plain COMMENT, never APPROVE/REQUEST_
CHANGES: only the CEO acts on master, so the gate must never leave an
approval that could satisfy branch protection (letting someone else
merge) nor a blocking review that could impede the CEO's merge.
- Best-effort and AFTER the DB transition — a GitHub failure is logged,
never rolls back the gate decision. Reuses git.post_pr_review's existing
self-review→COMMENT downgrade for the org's own PRs.
Adds _project_slug_for to the ChoreographerHelpers protocol (mypy) and a
unit suite covering event selection, the master-bound COMMENT rule, the
no-PR skip, and failure-swallowing. Docs updated (pr-reviewer, task-tools).
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
2db11c7833 |
feat(qa): require a per-acceptance-criterion verdict before pass_review
QA may no longer pass a task with a single gestalt approval — pass_review now takes ac_verdicts (one verification entry per acceptance criterion) and the gateway rejects a pass that does not cover every criterion. If a criterion does not hold, QA fails the review instead. The verdicts are folded into the persisted qa_notes for the audit trail. Threaded through the flow MCP tool, the HTTP request schema, and the route; QA prompt updated. |
||
|
|
ff35a646fa |
Chore: reduce analytics complexity (#100)
* refactor(analytics): reduce cyclomatic complexity in usage/pricing/rollup
Collapse the three near-identical get_by_* aggregation methods in
UsageService into a shared _aggregate_by helper parameterized by group
column and key name, and centralize token null-coalescing in a
_row_tokens helper. Extract the per-row upsert in _sweep_daily_rollup
into _upsert_rollup_row, and the pricing-table lookup into
_lookup_prices. All blocks now rank <= B and both modules rank A, so the
xenon gate passes; behavior is unchanged and existing tests stay green.
* feat(billing): make token pricing provider-aware
Distinguish three cases when a model has no per-token rate: a non-Anthropic
model (local Ollama, or an Ollama Cloud ":cloud" model billed by flat
subscription / GPU-time) legitimately has no per-token cost and returns 0.0
silently; an unpriced Anthropic ("claude"-named) model also returns 0.0 but
logs a warning, since that is real spend being undercounted and catches new or
renamed Claude models missing from the table. Folds the old ollama/ prefix
special-case into the general non-Anthropic path so there is one code path,
and replaces the blanket 'no pricing data' warning that fired even for
self-hosted models.
* fix(tasks): preserve ownership when force-unclaiming to pending
The stale-claim reaper and the dependency-blocked release both routed through
_force_unclaim_to_pending, which nulled assigned_to and left the task in a
pending state owned by nobody — no dispatcher re-spawns an ownerless pending
task, so it went dormant. The dispatcher-side claimed_by fallback only masked
half the cases.
Capture the owner before releasing the claim and keep both assigned_to and
claimed_by pointed at it (mirroring the unblock restore), releasing only the
live claim (active_claimant_id + heartbeat) and the WorkSession. The same agent
now resumes the task once it re-dispatches. Updates the reaper test that
asserted the old orphaning behavior and adds owner-preservation coverage for
both the reaper and dependency-release paths.
* fix(tasks): unblock restores the owner into both ownership fields
Audit follow-up to the force-unclaim ownership fix. unblock() only restored
assigned_to from blocker_raised_by, which block() stashes solely from
assigned_to. A task claimed via give_me_work (claimed_by set, assigned_to null)
therefore unblocked into a split-owner state — assigned_to null but claimed_by
set — that both the dev dispatcher and the PM pool-router race to pick up. It
also left claimed_by pointing at the resolver PM after an escalation.
Resolve the owner as blocker_raised_by or assigned_to or claimed_by and write
it to both fields, matching the force-unclaim and reassign convention so the
original worker resumes cleanly. Adds coverage for the give_me_work-claim case
and asserts owner restoration on the existing in_progress-resume test.
* test(orchestrator): cover dev owner resolution and the claimed_by fallback
_resolve_dev_owner_uuid had no coverage. Add the status-dependent precedence
(claimed/blocked prefer the live claimant; other statuses prefer the
PM-assigned owner) and the half-reap fallback where a pending task with
assigned_to nulled still resolves its owner from claimed_by instead of going
dormant.
* fix(tasks): wire the pre-block snapshot so unblock(restore=True) works
The restore=True path on a PM unblock was a no-op: pre_block_state /
pre_block_assignee (migration 006) were read by unblock_with_restore but never
written, so it always fell through to legacy unblock() and the restore flag did
nothing.
Snapshot the resting status + owner at every block entry (dependency block,
soft block, escalation) before mutating, capturing only the first block in a
chain so a re-block doesn't overwrite the original state. Escalation snapshots
the outgoing owner, not the escalation target, so restore returns the original
worker. The restore path applies the same branchless guard legacy unblock()
relies on — a snapshotted in_progress with no branch diverts to pending instead
of looping the dispatcher — and is extracted into _apply_pre_block_restore to
keep complexity under the gate. Adds coverage for snapshot capture, restore,
the branchless divert, and escalation owner restoration.
* test(tasks): update orphan-reconciler and dependency-release tests for owner preservation
Both the startup orphan reconciler and the dependency-blocked claim release
route through unclaim_for_reaper / _force_unclaim_to_pending, which now preserve
the owner instead of nulling assigned_to. Update the two tests that asserted the
old orphaning behavior to assert the owner is kept (so the same agent resumes)
while the live claim is released.
* chore(tests): scrub internal work-item labels from test names, docstrings, comments
Rename four test files that carried audit work-item IDs in their filenames
(test_p0_7_branch_atomicity, test_p2_8_orphan_reconciler,
test_p2_9_autogen_prompt_layer, test_p2_7_attempt_id) to describe what they
test, and strip the matching P-/D-/S- cluster labels from docstrings, comments,
and assertion messages across the test suite and two orchestrator comments.
These are internal references with no meaning in the codebase; behavior is
unchanged.
* style: reformat assertion line shortened by the internal-ref scrub
* build: waive unreachable torch CVE-2025-3000 in pip-audit gate
torch is a transitive CPU-pinned dep (piragi / sentence-transformers) never
loaded at runtime — the stack uses Ollama over HTTP for all embeddings/LLM, so
the vulnerable torch.jit.script path is unreachable. CVE-2025-3000 is MEDIUM,
local-only, with no published fix. Documented --ignore-vuln waiver; revisit when
a fixed torch ships.
* fix(orchestrator): route unplaceable pending tasks to main-pm instead of dropping them
_get_routing_target returned None when a 'dev'-classified task had no cell
agent (no team, or a non-cell team like fullstack/system) or when the routing
classification was unrecognized. _route_unassigned_pm_task logged 'no routing
target found' and returned, leaving the task ownerless and pending — and no
dispatcher re-spawns an unrouted pending task, so it went dormant for 10+ min
until the stuck-task detector caught it.
Fall back to main-pm (the same default cell_pm routing and escalation already
use) so the task is always owned and triaged, never stranded. Logs the fallback
so unplaceable tasks stay visible. Adds a test asserting no (routing, team)
combination ever resolves to None.
* fix(panel): make intake chat markdown inherit the bubble's text color
MarkdownBody is shared by the assistant (text-foreground) and user
(text-primary-foreground) bubbles. [&_*]:!text-inherit only colored the prose
div's descendants, so the prose div itself kept the prose typography body color
(gray) and children inherited that — unreadable on the muted assistant bubble.
Add !text-inherit on the prose div itself so it inherits the bubble's color
too; descendants then inherit the correct foreground. Fixes both bubbles without
hardcoding a color.
* fix(prompter): keep a board-reviewed product on the board team so Approve & Start shows
A product coordination root confirmed via 'Board review & Start' is assigned to
a board reviewer (product-owner) for review, but create_task_from_draft set
team=main_pm for every product unconditionally. The CEO's Approve & Start gate
keys on team=board, so the button never appeared — and because the owner stayed
a board agent while the team said main_pm, the dispatcher routed it to the board
path (nothing left to do after review) and the task stranded at pending, with
the board agent fruitlessly trying to escalate it up.
Route a product by its assignee: a board reviewer keeps it team=board (so the
gate appears and approve_and_start later hands it to Main PM), while a main-pm
assignee — the 'Approve & Start' straight-through path — is team=main_pm. Adds
_assignee_is_board mirroring TaskService's board-role check, and a test.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
06682f33c6 |
Fix: agent workflow hardening (#70)
* fix(gateway): push the branch before QA handoff so reviewers see the latest commits The commit content tool commits locally without pushing; only open_pr pushed the branch. On the first submission that was fine, but a fix committed while addressing needs_revision never reached origin (open_pr is skipped once the PR exists), so QA — which reviews the remote PR branch — re-reviewed the stale remote and re-failed the task on every cycle, a loop that never converged. i_am_done now pushes the task branch (idempotent; a no-op when nothing is unpushed) as part of the shared submit gate, covering both the normal and resume-from-verifying paths. A push failure blocks the handoff with a clear remediation rather than parking the task in awaiting_qa with commits that exist only in the developer's local workspace. * fix(orchestrator): don't reap a stale claim while the agent's container is alive The stale-claim reaper released any claimed/in_progress task whose last_heartbeat_at exceeded the TTL. The heartbeat only updates on certain gateway calls, so a developer deep in a long edit/test cycle outran the TTL and had its claim reaped mid-work — churning the task and risking a double spawn against the still-running container. The reaper now skips a task whose assignee still holds a live (ACTIVE) agent instance, trusting container liveness — the ground truth — over the heartbeat proxy. The check is defensive on missing fields so a heartbeat-only caller (and the reaper's existing unit tests) behave exactly as before. * fix(gateway): refuse to unblock a task while a dependency is unfinished A PM unblock on a dependency-gated task moved it straight to in_progress, overriding the dependency — letting a dependent proceed without its upstream's work (e.g. a frontend task built before its UX design lands). A dependency block is meant to clear on its own via _unblock_dependents the moment the upstream reaches a terminal state. unblock now refuses while any dependency is still non-terminal, returning a clear remediation that the block resolves automatically. Manual unblock remains available for genuine, non-dependency blockers. * fix(gateway): release a dependency-blocked claim to pending instead of looping A task that reached claimed/in_progress with an unfinished dependency was left in that state when the claim guard rejected, so the orchestrator's respawn loop kept reviving its assignee — which could make no progress — burning work for nothing. The claim guard now releases such a task back to pending. claimed -> blocked is not a legal transition, so pending — held by the dispatch dependency filter — is the lifecycle-correct resting state: the respawn loop ignores pending tasks, and _unblock_dependents re-dispatches it once the upstream reaches a terminal state. release_dependency_blocked_claim shares a _force_unclaim_to_pending core with unclaim_for_reaper so both record a truthful work-session abandon reason. * feat(security): warn at startup in header-trust mode + document the auth posture When ROBOCO_AGENT_AUTH_REQUIRED is not enabled the API accepts the X-Agent-Id / X-Agent-Role headers without a signed token, so any client that can reach it may act as any role (including 'ceo'). The API now logs a clear warning at startup in this mode, and the README gains a Security section documenting the auth posture and how to harden it. Acceptable only on a trusted private network — do not expose the API to untrusted networks. * fix(workspace): scope the refresh fetch to current + default branch ensure_workspace's healthy short-circuit ran an all-refs 'git fetch origin' to keep every origin/<branch> ref current. On a monorepo with many accumulated feature/* branches that exceeds the refresh timeout, the fetch silently fails, and the workspace keeps a stale base — so an agent builds on an out-of-date branch. The refresh now fetches only the workspace's current branch and the repo's default branch (resolved via origin/HEAD), with --no-tags --prune: it transfers near-nothing and can't time out. Readers need their own branch and the default; the integration branch is refreshed at branch-creation time. * fix(git): refresh a dependency-blocked task's branch off the current integration tip A cross-cell dependent (e.g. a frontend task waiting on the UX design) was branched off a base captured before its upstream merged into the integration branch, and the branch was never re-synced — so the agent built on a stale snapshot with none of the upstream's work. Two changes close the gap: - release_dependency_blocked_claim now clears branch_name, so the re-claim (after the dependency clears) re-runs branch creation. - create_branch, when the branch is already on disk with no commits of its own, resets it onto the freshly-pulled base — the dependent now builds on the current integration tip. A branch carrying real commits is left untouched, so no work is discarded; the cell->leaf cascade carries the upstream down to the dev branch automatically. * refactor(gateway): drop the sibling-sequence claim guard Sibling sequence no longer gates a claim. Cross-cell ordering is enforced by task dependencies — a cell task that depends on another is held until its upstream reaches a terminal state, a stronger, status-aware gate than the sequence-number check. That check was dormant in practice anyway: every fan-out child carries sequence 0, on which the guard short-circuited. `sequence` stays a sibling-ordering / dispatch-priority field (list_pending ordering and the panel). Removes sibling_sequence_guard and its _earlier_blocking_sibling helper, the now-unused skip_sequence parameter threaded through the claim verbs, and the sibling fetch that fed it. * feat(gateway): sort a cross-cell dependent after its upstream When the frontend cell task is wired to depend on its UX/UI sibling, set its sequence to the upstream's sequence + 1 so it sorts after the design it waits on — list_pending ordering and the panel now show UX ahead of the implementation it gates, in either delegation order. Adds TaskService.set_sequence (the sibling-ordering field is a service write; it carries no claim-gating semantics — dependencies gate claims). * feat(gateway): make the backend cell depend on UX too UX/UI design defines the screens and API contracts both implementation cells build against, so the backend cell — not just the frontend — waits on the UX/UI cell task in a product fan-out and sorts after it. Wires in either delegation order: a backend task delegated after UX gets the dependency directly; a UX task delegated after a still-pending backend sibling retro-wires it. Mirrors the existing frontend wiring (_depend_backend_on_ux and _depend_pending_backends_on_ux). Backend is held by the same dependency gate, so it costs no extra dispatch churn. * fix(websocket): forward notification acks instead of logging them incomplete The bridge handler serves both notification.sent and notification.acked, but acked events carry `agent_id` (the acking agent) rather than `recipient_id`, so every acknowledgement tripped the missing-field guard and logged "Incomplete notification event" instead of reaching the panel. Accept either field as the recipient. * feat(api): hint the full UUID when a truncated task id fails validation Agents copy the 8-character task prefix the system shows them (the commit prefix, task summaries) and send it as task_id, which fails UUID validation with an opaque "invalid length" 422 and wastes a call. The request-validation handler now detects a task_id UUID error and attaches a `remediate` hint telling the agent to retry with the full 36-character UUID from its task envelope. * fix(audit): record the blocked transition when a task is escalated Escalation sets a task to blocked by writing task.status directly, which bypassed the validated transition helper and so never emitted a task.blocked audit row — the lifecycle moved but the Auditor saw nothing. Extract the audit emit from the central transition helper into _emit_status_transition_audit and call it from the escalate path, capturing the prior status and outgoing owner before reassignment so the row is attributed correctly. * fix(docs): stop doubling the docs path so design specs index into RAG The documenter sometimes hands a doc path already rooted at docs/, and joining it onto DOCS_BASE_PATH (/app/docs) produced /app/docs/docs/..., so the file was never found and the spec never indexed — the frontend cell could not retrieve the UX design over RAG. Normalize the path before joining: trust an absolute path, otherwise strip a single redundant leading docs/ segment. * feat(security): let the control panel authenticate in secure mode With ROBOCO_AGENT_AUTH_REQUIRED=true every request must carry a valid HMAC token, which locked the human control panel out — it sends role headers but no token. nginx, the only trusted hop between the browser and the API, now injects the CEO token on /api and /ws, so the browser never holds the signing secret. The injected value is just the existing per-agent token issued for the CEO identity (issue_panel_token), so the token-verification path is unchanged. An empty value (dev/header-trust mode) renders to no header. `make panel-token` prints the value; set it as ROBOCO_PANEL_AGENT_TOKEN in .env before enabling secure mode. .env.example and the README Security section document the flow. * chore(compose): consolidate the two compose files into one docker-compose.yml and docker-compose.yaml had diverged: .yml — the file Docker actually uses — carried ROBOCO_PUBLIC_BASE_URL but was missing the /app/manifests bind-mount, while .yaml had the manifests mount but not the base URL. Merge the union into docker-compose.yml and delete the duplicate so there is one source of truth and no "multiple config files" warning. This activates the manifests mount in the deployed file: without it the orchestrator writes per-agent tool manifests to its ephemeral container fs, they never reach the host for the daemon to bind-mount, and agents fall back to all-verbs registration. Drop the stale .yaml reference from the config.py docstring, the labeler, and the CI path filters. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com> |
||
|
|
d88a8df9f4 | test: drop dead MAIN_PM delete that broke lifecycle isolation across test files | ||
|
|
110aaa7a77 |
Chore: v1 removal gateway canonical (#46)
* chore(agent_sdk): remove dead /traceability/remind endpoint and reminder map
The TRACEABILITY_REMINDERS dict and its /traceability/remind endpoint were
keyed entirely on pre-gateway tool names (roboco_task_*, roboco_journal_*,
roboco_message_send, roboco_session_create_for_tasks) deleted in the gateway
cutover. The endpoint had zero callers; v2 enforces traceability server-side
in the Choreographer.
* fix(bootstrap,seeds): onboarding prompts call give_me_work(), not deleted roboco_task_scan()
The startup prompt and the seeded cell/all-hands channel onboarding messages
instructed agents to call roboco_task_scan() — a tool removed in the gateway
cutover. Point them at the live give_me_work() flow verb.
* fix: replace remaining deleted v1 tool names with gateway verbs
Spawn prompts, onboarding strings, remediation messages, and comments still
referenced pre-gateway tools deleted in the cutover (roboco_task_*,
roboco_agent_idle, roboco_notify_*, roboco_message_send,
roboco_session_create_for_tasks, roboco_journal_*, roboco_escalate). Rewrote
each to the correct role-scoped gateway verb (give_me_work/i_will_work_on for
workers, triage for PMs, i_am_done vs complete, notify/notify_ack, escalate_up,
unclaim, i_documented, open_session, note). Updated one enforcement-message
test that matched the old tool name by coincidence.
* test: guard against deleted v1 tool names reappearing in roboco/
Scans roboco/ for the deleted pre-gateway tool names; excludes the orphaned
roboco/agents/ subtree (removed in a later phase).
* chore(exceptions): drop 8 unused pre-gateway exception classes + their tests
LLMError, RAGError, AlreadyExistsError, TaskBlockedError, TaskClaimError,
AgentNotAvailableError, AgentBusyError, NotificationPermissionError were never
raised in production. SessionClosedError/DatabaseError are kept (live + tested).
* chore(models): drop unused pre-gateway notification/channel/handoff factories
Removes create_task_assignment/_blocker_escalation/_review_request/
_documentation_request/_priority_change/_alert/_broadcast, create_cell_channel/
_cross_cell_channel/_announcements_channel, create_handoff (+ HandoffParams),
ProactiveContext, and A2APartType. The gateway choreographer builds these
server-side now. Drops the matching dead-code tests.
* chore(services): drop unused pre-gateway permission/messaging/audit/optimal/remediation methods
These pre-gateway helpers (channel-permission checks, channel-membership ops,
permission-denial audit hooks, doc ingestion, two remediation hints) have no
production caller — the gateway role_config + enforcement layer replaced them.
Drops the matching dead-code tests; live methods (send_message, the SESSION_*
flow, log_task_action_denial, etc.) are untouched.
* chore(orchestrator,ws,events,config): drop unused pre-gateway lifecycle/broadcast/roster symbols
orchestrator: get_running_agents, is_agent_busy, queue_priority_work,
get_all_instances (+ their OrchestratorAccessProtocol declarations in events.py).
websocket: broadcast_new_message, broadcast_session_closed (no event type emits
them). agents_config: ALL_PMS/ALL_DEVS/ALL_QA/CELL_PMS roster constants (ALL_DOCS
stays — it gates docs-write workspace perms).
* refactor(agents): delete orphaned pre-gateway agent subtree + dead organization model
The Gateway/full cutover replaced the Python agent-class implementations with
the server-side Choreographer; the classes survived only as a self-referential
island. Removes roboco/agents/{base,mixins,factory,board,developer,documenter,
pm,qa,orchestrator}.py and roboco/agents/factories/{board,cells,developers,
documenters,pms,qa}.py, plus roboco/models/organization.py (Cell/Board/
Organization — used only by those factories). Keeps factories/_base.py
(compose_prompt — the live prompt-layer composer the orchestrator calls at
spawn) behind minimal package __init__ files.
* chore(db): drop dead tasks.execution_log + outputs columns (migration 015)
Both JSON columns had zero readers/writers in code, tests, and migrations —
execution progress is tracked via progress_updates and artifacts via
commits/documents. Removes the ORM columns, the Pydantic Task.execution_log/
outputs fields, the ExecutionLog/FileRef models (+ their __init__ exports), and
the now-invalid kwargs from test fixtures. Migration 015 (down_revision
014_drop_pm_approvals) verified live: upgrade drops, downgrade re-adds.
Apply on the NAS with 'alembic upgrade head' at next deploy.
* chore(config): drop 16 unread Settings fields
Verified unused (no settings.X, no self.X property use, no getattr-by-name):
app_name, reload, workers, openai_api_key, secret_key, access_token_expire_minutes,
algorithm, log_level, log_format, the four session_* limits, message_max_length,
commit_subject_min_chars, commit_banned_words, agent_budget_sweep_interval_seconds.
Removes the empty Logging + Sessions&Messages sections and orphaned .env.example
vars. Kept: redis_db/redis_password (redis_url property), agent_sla_* (read via
getattr in task_lifecycle), encryption_key, and all live thresholds.
NOTE: commit_banned_words/commit_subject_min_chars and
agent_budget_sweep_interval_seconds were feature-config never wired to their
consumer (commit validator / budget sweep) — removed as dead, but flagged in
case the intent was to wire them.
* test(lifecycle): give i_will_work_on calls a substantive plan (#171 contract)
The real-DB lifecycle tests called i_will_work_on with a 13-char plan and no
risks/technical_considerations, so the substantive-plan gate (#171) rejected
them with incomplete_input — failing on master. Supply a >=150-char plan plus
technical_considerations and risks (mirroring tests/unit/gateway/
test_choreographer_dev.py). All 6 now pass; gate runs with no deselect.
* feat(gateway): wire commit-validator thresholds to settings
commit_subject_min_chars and commit_banned_words were config defined but never
read — the gateway commit() gate used the validator's hardcoded module defaults.
Re-add the two Settings fields and pass them through validate_commit_message in
content_actions.commit(), so config is the source of truth (validator defaults
remain the standalone/CI fallback). Adds wiring tests that monkeypatch settings
and assert the gate honors them.
* refactor(orchestrator): retire gateway_enabled flag; trigger_filter is unconditional
The gateway_enabled Settings field gated only the trigger_filter spawn-cooldown
(never the agent tool surface). Prod ran it on; the Phase-0 'legacy dispatch
path' it guarded no longer exists. Remove the field + the early-return branch in
gateway_pre_spawn_check so the cooldown runs for every spawn, drop the now-dead
ROBOCO_GATEWAY_ENABLED from docker-compose.yml, and update the stale Phase-0
comments + cooldown test. The per-container ROBOCO_GATEWAY_ENABLED env (set by
_append_manifest_args, read by agent_sdk to load the manifest) is unaffected.
* refactor(api): relabel /api/v2 -> /api/v1 as the canonical gateway surface
The gateway is the only agent API now, so the 'v2' label (with no v1) was
misleading. Renames roboco/api/routes/v2 -> routes/v1, schemas/v2 -> schemas/v1
(+ the matching test dirs and test_v2_role_dep/test_schemas_v2_flow files),
rewrites every /api/v2 path, routes.v2/schemas.v2 import, and v2-* router tag to
v1, and refreshes the stale 'v2' comments/docstrings. The panel is untouched (it
uses the unversioned /api/* REST routes). flow_server/do_server now POST to
/api/v1/*.
* docs(scripts): reset_runtime_state header matches actual SQL behavior
The header claimed it preserves groups + journals, but the .sql wipes both
(verified live: groups 6->0, journals 5->0; only agents/projects/channels
survive). Correct the wiped/preserved lists to match.
* refactor(gateway): extract _build_rich_plan to drop i_will_work_on under the complexity gate
i_will_work_on was cyclomatic rank C (11) — one over the xenon --max-absolute B
threshold — because of the five `x or default` fallbacks in the rich_plan dict.
Move that dict into a small _build_rich_plan helper (behaviour identical); both
methods are now rank B. make quality is fully green (xenon was its last failure;
bandit already passed — its 34 findings are all LOW severity, filtered by -ll).
* feat(foundation): add canonical CELL_TEAMS set; dedupe cell-subset literals
* feat(db): add ProductTable + ProductProjectTable ORM (per-cell project map)
* feat(task): add additive nullable product_id (ORM + model + DTO + create threading)
* feat(task): thread product_id through create_subtask/route/response
* feat(db): migration 016 — products, product_projects, tasks.product_id
* fix(db): document migration 016 plan deviations (revision len, FK name)
Two values in migration 016 intentionally diverge from the Task 2.4 plan
literals; this strengthens the in-file justification so the deviations are
self-documenting and verifiable.
- revision id (plan line 623): the plan's 36-char
"016_add_products_and_task_product_id" overflows alembic_version.version_num
(VARCHAR(32)) — alembic upgrade head raises asyncpg
StringDataRightTruncationError. Kept at 27 chars
("016_add_products_product_id") so Step 4's live round-trip stays green.
- downgrade FK name (plan line 683): roboco/db/base.py sets a metadata
naming_convention, so the FK upgrade() creates is
"fk_tasks_product_id_products", not the Postgres default
"tasks_product_id_fkey". The plan literal does not exist in the DB and
would fail the downgrade with "constraint does not exist".
Both verified via the live upgrade/downgrade round-trip on a throwaway DB.
Issue 3 note: the prior commit (b896cac) also touched
tests/unit/api/test_schemas_tasks.py (added product_id=None to the
task_to_response stub). That line is load-bearing — task_to_response reads
task.product_id (added in Task 2.3, commit 67afa6b) — and belongs to Task 2.3's
scope; it is left in place because removing it breaks 4 tests and history is
not rewritten.
* refactor(db): trim migration 016 deviation notes to plan-faithful form
Reverts the out-of-scope documentation expansion (commit 1a4f296), which
was a second undocumented commit beyond Task 2.4's single plan-specified
commit and only bloated the migration docstring/comments.
The migration file now matches the plan-specified commit (b896cac) byte for
byte: the two necessary deviations from the plan literals stay (revision id
shortened to fit alembic_version.version_num VARCHAR(32); downgrade FK name
follows db/base.py's metadata naming_convention), each kept to a concise
inline note in the plan's header style.
The Task 2.3-scoped test stub line (tests/unit/api/test_schemas_tasks.py
product_id=None) is load-bearing — task_to_response reads task.product_id —
and is left in place; history is not rewritten.
Verified: live alembic upgrade head + downgrade to 015 round-trip on a
throwaway DB drops products/product_projects/tasks.product_id cleanly, and
make quality is green.
* refactor(test): annotate db_session and drop type: ignore in migration 016 test
Annotate the test_products_tables_and_task_fk_exist param as
db_session: AsyncSession (imported under TYPE_CHECKING) and remove the
# type: ignore[no-untyped-def] suppression, matching the typed db_session
pattern used across tests/integration/.
* feat(models): Product + ProductCreate/Update + ProductCellMapping (cell-validated)
* refactor(models): minimize ProductCellMapping config override to use_enum_values
The previous override re-declared validate_assignment, populate_by_name,
and extra=forbid, which RobocoBase already supplies. Pydantic merges
model_config across inheritance, so overriding only use_enum_values=False
is sufficient to keep team as a real Team enum (required so team in
CELL_TEAMS and enum identity hold for callers) while inheriting the rest
of the base config.
* fix(models): document ProductCellMapping use_enum_values override as plan-mandated
Resolves SPEC-COMPLIANCE review notes for Task 3.1 (Product domain models).
1. The ProductCellMapping use_enum_values=False override is a deviation from a
bare project.py mirror, but it is mandated by the plan's own Task 3.1 code:
RobocoBase sets use_enum_values=True, which coerces team to the plain string
"backend". The plan's Step 1 test asserts m.team is Team.BACKEND (enum
identity) and the Step 3 validator formats its error with v.value, both of
which require team to remain a real Team enum. The override is therefore
necessary; this commit relabels the comment to cite the specific spec lines
that force it instead of leaving it as an unexplained departure. Downstream
Task 3.2 (_replace_cells / project_for) already tolerates either form and the
ORM stores the same value regardless, so the override has no behavioral reach
beyond the in-memory enum identity the plan's test checks.
2. test_product_model.py hoists 'from uuid import uuid4' to module level rather
than inline (as the plan's verbatim Step 1 code shows) because the global
Pylint PLC0415 rule (import-outside-top-level) forbids inline imports and
there is no per-file-ignore for tests/unit/models/. The hoisted form is the
only ruff-clean rendering of the plan's test; left unchanged here.
3. Task 3.1 landed across two commits (c616d95 create, 6ebad255 refactor) rather
than the plan's single Step 5 commit. Earlier history is intentionally not
rewritten; this single follow-up commit brings the model to its final
spec-faithful, fully-documented state.
* feat(service): ProductService CRUD + project_for per-cell resolver
* feat(api): Product CRUD routes + schemas, wired into the app
* fix(api): roll back and map cell-replacement IntegrityError on product update
update_product replaced cells via ProductService._replace_cells without
any try/except, so a duplicate-team cell (uq_product_projects_product_team)
or a non-existent project_id (product_projects.project_id FK) raised an
IntegrityError at flush, poisoning the AsyncSession and surfacing an
unhandled 500 with no rollback. Wrap the update + commit in a try/except
that rolls back and maps the UNIQUE violation to 409 and the FK violation
to 422, mirroring create_product's rollback discipline. Add integration
tests covering both client-error paths.
* fix(api): map create_product cell-mapping IntegrityError to 409/422
create_product only caught the slug conflict ('already exists' in str(e))
and bare-raised everything else, so a cells entry whose project_id does not
reference any project let the product_projects.project_id FK IntegrityError
propagate out of the route as an unhandled 500. The matching update_product
path was already hardened (uq_product_projects_product_team -> 409, FK
violation -> 422); apply the same mapping in create_product so a bad
project_id (or a duplicate-team cell) is a client error, not a server error.
The slug conflict is now caught as ConflictError directly instead of via a
broad except + string match.
* feat(gateway): add optional project_id to delegate inputs/request/routes
* feat(gateway): per-cell project routing (override -> product map -> parent) + product_id inheritance
* feat(task): approve_and_start — reassign board task to Main PM (CEO gate #1)
* feat(api): POST /tasks/{id}/approve-and-start (CEO gate #1, notes-required)
* test(api): cover approve-and-start 404-before-notes-gate for missing task
* feat(panel): Product types + Task.product_id
* feat(panel): productsApi + hooks + tasksApi.approveAndStart
* feat(panel): Products management screen + sidebar nav
* feat(panel): Approve & Start button (CEO gate #1)
* fix(api): narrow delete_product to IntegrityError + cover 204/409 delete paths
* test(task): assert approve_and_start persists + appends the audit note
* refactor(db): migration 016 names the tasks.product_id FK explicitly (house style)
* fix(db): make migrations authoritative + self-heal orphan product tables
init_db() no longer silently falls back to create_all when alembic upgrade
fails. That fallback masked migration failures and, since create_all cannot
ALTER an existing table, left the schema inconsistent — turning an unapplied
migration 016 into a crash loop: 016's CREATE TABLE products failed, the
upgrade rolled back, create_all re-created an empty orphan products table, and
every later boot failed again on the now-existing table while tasks.product_id
never got added. Now a migration failure is raised so the real error surfaces.
Migration 016 additionally drops EMPTY orphan products/product_projects tables
left by the old fallback before creating them, so an already-polluted DB
self-heals on the next deploy with no manual SQL. Skipped in offline (--sql)
mode; refuses to drop a table that holds rows.
* fix(db): create_all is the schema source of truth; alembic for increments
The Alembic chain is incomplete relative to the ORM — columns/tables like
notifications.delivered_at and the RAG indexed_documents table have NO migration
and have only ever been materialized by create_all. Tests don't catch this
because the test DB is also built via create_all, so migrations are never
exercised. The prior 'migrations are authoritative' init_db (and before it, the
create_all-only-on-failure fallback) therefore left a migrate-only boot with
missing columns/tables.
init_db now reflects reality:
- Fresh DB -> create_all builds the full current ORM schema, then stamp
Alembic at head so later incremental migrations apply.
- Existing -> run pending migrations (a real failure is raised, not masked),
then create_all(checkfirst) to gap-fill any missing ORM tables.
create_all cannot add a column to an existing table, so an ORM column added
without a migration needs a fresh rebuild of that table to appear.
* fix(db): migration 017 reconciles the Alembic chain with the full ORM schema
For years the live schema was built by create_all, not migrations, so the chain
drifted — tables/columns/indexes in the ORM had no migration (the
indexed_documents table, notifications.delivered_at, ~15 indexes, plus
timestamptz/server-default metadata). With init_db no longer masking that via a
create_all fallback, a migrate-only boot was missing those objects.
017 was produced by 'alembic revision --autogenerate' against Base.metadata,
reviewed, and verified: on a fresh DB, 'alembic upgrade head' (001..017) now
reproduces the create_all schema EXACTLY — a re-run of autogenerate detects zero
changes — and the 017 upgrade/downgrade round-trips cleanly. The migration chain
is now complete: migrate-only and create_all converge.
Also updates the init_db tests to assert the new behaviour (raise on an existing
DB's migration failure; create_all + stamp head on a fresh DB) instead of the
removed silent fallback.
* feat(panel): Product picker in the New Task form (drives per-cell routing)
The Products screen and Approve & Start button shipped, but the task-creation
form had no way to attach a Product — so a human couldn't set product_id from
the UI, which is exactly what drives per-cell project routing of delegated
subtasks. Adds an optional Product dropdown (Advanced -> Git config) populated
from useProducts(); 'None' falls back to the single project.
* fix(db): seed data is preserved on a fresh DB (run migrations, not bare create_all)
The previous fresh-DB path (create_all + stamp head) built the tables but never
ran the migration chain, so migration-embedded SEED DATA was skipped — most
visibly the AI providers seeded in 004. After a DB reset that left
provider_configs empty, so PUT /api/providers/ollama-key 404'd (the handler
raises NotFoundError when the Ollama provider row is missing).
Since migration 017 made the chain reproduce the full ORM schema, init_db now
runs 'alembic upgrade head' from base on a fresh DB — building every
table/column/index AND running the seeds. Verified: a fresh upgrade head seeds
both provider rows. Existing DBs still get migrations + create_all gap-fill.
Updates the init_db fresh-DB test accordingly.
* feat(task): project_id optional when a product_id is set (board fan-out tasks)
A board task that fans out across cells via a Product has no single repo of its
own — backend/frontend/ux_ui are each wrong, because the root coordinates and
delegates. Forcing one arbitrary Project was broken design (flagged at design
time). project_id is now nullable; a task must have project_id OR product_id:
- TaskCreate model validator + a TaskService.create() invariant (covers every
create path).
- ORM/DTO/schema: project_id nullable; task_to_response uses to_python_uuid.
- Gateway: a parent with only a product can delegate (guard now needs BOTH
project and product to be None to reject); _resolve_subtask_project resolves
each subtask from the product map and raises a clear error if a cell has no
mapping and no parent project.
- Migration 018 (tasks.project_id nullable), round-trip verified; fresh
upgrade head still seeds providers.
- Panel: Project no longer required once a Product is selected.
- Removed the dead, never-called a2a create_task_from_message (it could only
ever create a repo-less task) + its two coverage-only tests.
make quality green; panel tsc/lint/build green.
* Upgrade to Minimax M3
* fix(db): seed providers on existing DBs + correct enum casing
Migration 004 created the modelprovider/assignmentscope enums and seeded
provider rows in UPPERCASE, but the ORM (_str_enum) reads/writes the
lowercase StrEnum .value — so a fresh migrate-from-base DB built an enum
the ORM cannot read. Lowercase the enum labels and seed values in 004.
Add idempotent migration 019 to (re)seed the Anthropic + Ollama Cloud
providers with ON CONFLICT (name) DO NOTHING, so an existing DB whose
provider_configs table was created by create_all (and never ran 004's
seed) gets the rows on the next `alembic upgrade head` — fixing the
/api/providers/ollama-key 404 without a volume wipe.
* fix(tasks): let board/fan-out coordination tasks flow without a repo
A coordination task (project_id NULL, product_id set) targets no repo of
its own — it fans out to cell subtasks that each resolve a real project
from the product's cell->project map. Several paths still assumed every
task does git work and blocked it:
- orchestrator: add _is_coordination_task() and exempt these tasks from
the project/branch/git-token gates in _readiness_check_task,
_readiness_gate, _check_stuck_conditions, _validate_task_for_spawn.
- services/task.py: _ensure_branch_for_task returns "" (no branch) for a
coordination task instead of raising; activate requires project OR
product. This unblocks Main PM's i_will_plan claim, which otherwise
raised before it could delegate the fan-out.
- gateway: _pending_assignment_guard exempts advisory roles
(product_owner/head_marketing/auditor) from the "assigned but never
claimed" idle gate — they review without claiming, so they could not
satisfy a claim-or-unclaim remediation.
Adds focused unit tests for each.
* fix(tasks): coordination tasks reach in_progress + team reflects Main PM
The board->cells fan-out deadlocked: a coordination/fan-out task (product set,
no project of its own) could be created and claimed, but start()'s
claimed->in_progress transition hit validate_git_requirements, which still
demanded a branch_name and raised GitRequirementError. So Main PM's i_will_plan
never completed — it looped and never delegated. c961282 exempted
_ensure_branch_for_task (branch creation) but missed this parallel git gate in
the enforcement layer.
- task_lifecycle.py: add GitContext.is_coordination; skip the
claimed->in_progress branch_name gate when it is set.
- task.py: populate is_coordination=(project_id is None and product_id is not
None) in _validate_and_set_status; a branchless code task is still gated.
- approve_and_start: set team=Team.MAIN_PM on hand-off so the task isn't left
labelled team=board after it leaves the board (now assigned to main-pm).
Adds a lifecycle-gate unit test and an end-to-end integration test that
claims, plans, and starts a project-less coordination task.
* fix(hooks): remove dead traceability hook + stale deleted-verb references
The v1-removal cleanup (2cfbf39) deleted the /traceability/remind SDK endpoint
but left the PostToolUse hook that curls it, so every gateway tool call 404'd
and agents silently lost their traceability reminders. Remove the dangling hook
(registration + TRACEABILITY_TRIGGER_TOOLS + Dockerfile COPY + the script); v2
carries per-verb guidance on the Envelope. Also correct two stale pre-gateway
tool names in hook text: the budget loop-detector nudged agents toward the
deleted roboco_task_escalate() (now unclaim()/i_am_idle(), which every looping
role has), and an sdk-startup comment referenced roboco_task_scan/get.
Extends the deleted-tool-name guard to scan docker/scripts/*.sh and to assert
every $SDK_URL/<path> a hook curls is a route still served by the SDK — the
check that would have caught this class (it lives in shell, invisible to mypy
and the Python import graph).
* fix(db): backfill ORM enum values the migration chain never added
Several StrEnum values were added to the ORM over time without a matching
`ALTER TYPE ... ADD VALUE` migration; 017 was autogenerate-derived and
autogenerate does not detect added enum labels, so the drift survived. On a DB
whose enum type predates the value, binding it raises at runtime — e.g.
`invalid input value for enum notificationtype: "a2a_request"` on
GET /api/notifications (list_system_notifications), and the same class for
blockerresolvertype/handoffstatus/team.
Migration 020 adds every drifted value idempotently (ADD VALUE IF NOT EXISTS —
no-op when 009 already reconciled it). Runs on the next `alembic upgrade head`.
Detected by comparing each ORM enum's values to the labels the migration chain
produces; adds tests/unit/test_enum_migration_parity.py which renders the chain
offline and fails on any future drift — the check that would have caught both
this and the provider-enum bug.
* fix(orchestrator): stop branch auto-block, board reassign, unblock livelock, agentless claims
Cluster C1 — four coupled orchestrator/task-invariant defects:
#18: a branch is created only at claim, so a pending, never-claimed code task
legitimately has no branch_name. The stuck-detection sweep (pending-only) and
readiness gate flagged that as "Task missing branch_name" and auto-blocked the
task every tick, so it never dispatched. Centralize the gate in
_branch_is_expected (status in claimed/in_progress/verifying, never a
coordination task) and apply it in both _check_stuck_conditions and
_readiness_check_task.
#14: the main_pm -> product_owner escalation rung handed an in_progress
descendant code task to the Product Owner (a board role) and marked it BLOCKED;
the board has no verb to own code work, so the dev's finished work deadlocked.
TaskService.apply_escalation (the single write primitive — covers both the
gateway escalate verb and the HTTP escalate route) now diverts a descendant code
task targeting a board/advisory role: it releases the task to PENDING for a
role-matched cell claim instead of stranding it.
#17: a blocked task reassigned to Main PM kept respawning the ex-assignee cell
PM to unblock it, but the assignee-only pre-unblock note returned not_authorized
— a livelock. _dispatch_blocker_work now dispatches the task's CURRENT PM/board
assignee (the unblock authority), falling back to the cell PM only when no
PM/board holds it. Also: a branchless coordination parent yields no valid merge
target — resolve_parent_branch now falls back to the child's own project default
branch (e.g. master) via TaskService.project_default_branch_for_task, and
_check_parent_branch_ready no longer blocks a child on a coordination parent's
non-existent branch.
#19: a task left claimed/in_progress with an assignee but no running container
was invisibly stuck (only PENDING tasks get fresh dispatch; the heartbeat reaper
can't see a freshly-seeded claim). New _dispatch_claimed_without_agent net:
after a short grace window it respawns the assignee, or releases the claim to
pending (lifecycle-safe via unclaim_for_reaper) when the assignee is unknown.
New config ROBOCO_CLAIMED_NO_AGENT_GRACE_SECONDS (default 120).
* fix(gateway): tolerant note verb + lock evidence do-tool invariant
#15: the note verb no longer hard-rejects thin decision/reflect payloads.
List-typed fields (options, consequences, next_steps) coerce a lone scalar
into a one-element list at both the NoteRequest schema (mode=before
validator) and the service layer; missing narrative fields default to a
visible placeholder instead of returning incomplete_input. The note is
always recorded, preserving audit value, and a well-intentioned note can no
longer trip the do-server 3-strikes circuit breaker. Widen the agent-facing
do_server.note hints to accept list-or-scalar and refresh the docstrings.
#8: add regression coverage locking the invariant that every role's do_tools
carries evidence (role_config + developer spawn manifest). The current source
already registers mcp__roboco-do__evidence for developers end-to-end; the
report stemmed from a stale deployed build, and the tests prevent silent
regression.
* fix(gateway): allow UX devs to receive design tasks; surface delegation rules to cell PM
The UX/UI cell's developers (ux-dev-1/ux-dev-2, Role.DEVELOPER on
Team.UX_UI) ARE its designers, but _validate_assignee_task_type rejected
task_type='design' for every DEVELOPER, blocking the UX cell's normal
design delegation. Allow 'design' for UX-team devs only; backend/frontend
devs stay rejected (design routing belongs to the UX cell). The
orchestrator already dispatches a developer for a design task
(_dev_dispatch_role_matches returns True), so this creates no orphan like
the documentation case.
Replace the static Cell-PM 'pass planning' remediate with a per-assignee
hint so a dev/design mis-type gets a developer-class next-step instead of
an off-topic planning hint.
Surface the three delegation guardrails in the cell-PM prompt so PMs stop
probing them by trial and error: valid task_type per assignee (incl.
design for UX devs), documentation auto-creation (non-delegatable), and
the sequential single-active code-spine. Fix the delegate-row task_type
list (documentation is NOT delegatable) and update the lifecycle spec
description; regenerate the lifecycle artifacts.
* fix(orchestrator): improve agent briefings for handoff consumption, product/project model, and workspace/secret hygiene
Main PM (roles/main_pm.md):
- Require reading the upstream Product Owner / Head of Marketing handoff
(their decision/reflect journal entries + task description) BEFORE doing
any own research or calling i_will_plan, so the Main PM builds on the
Board's analysis instead of duplicating it. Added a dedicated section,
hardened workflow step 1, and added an anti-pattern.
- Add a 'Products vs Projects' section: a Product fans out to one Project
per cell; those Projects may be the SAME repo (monorepo subtrees) or
DIFFERENT repos (multi-repo). The Main PM coordinates across them and
must not assume one repo or call a monorepo subtree 'a separate repo'.
Names the Prompter monorepo case (github.com/rennf93/roboco).
Developer (roles/developer.md):
- State the exact workspace path convention
/data/workspaces/<project-slug>/<team>/<agent-slug>/, that the cwd is
already set there, to stay inside the own cell workspace, and to not
probe/guess the path (ls /, find /).
- Sanctioned secret handling: env/printenv is bash-guard denied and
reveals nothing; needed secrets arrive via the task description, else
i_am_blocked so the PM supplies them. Added matching anti-patterns.
Tests: add tests/unit/agents/test_briefing_cluster_c4.py asserting the
composed system prompt (the text mounted into agent containers) carries
each of the above.
* fix(orchestrator): board review involves PO+HoM and notifies CEO
Cluster C5 (#2, #4): a board/coordination task was reviewed by the Product
Owner alone, and the CEO got no formal signal when the review finished —
only buried channel chatter — so the Approve & Start handoff was invisible.
#4 — Board review is now a two-reviewer gate. _handle_board_assigned_task
dispatches BOTH the Product Owner and the Head of Marketing (one-shot each),
regardless of which one holds assigned_to, and the unassigned board-routing
path delegates here instead of claiming + spawning the PO alone. Board tasks
stay pending/unassigned for the CEO's Approve & Start. The board prompt now
makes the PO+HoM pair-review model explicit (HoM owns the UX/positioning
dimension).
#2 — Once BOTH reviewers have finished (dispatched and no longer active),
the orchestrator emits exactly one formal CEO notification via
NotificationService.send_board_review_complete_notification (APPROVAL type,
ack-required, carrying related_task_id) so the handoff is an actionable
signal. One-shot per task; a notification failure clears the guard so a
later tick can retry.
To let the non-assignee board member record its review note on a task held
by the other board member, content-action ownership now exempts a board role
posting to a board/coordination task (project_id is None, product_id set).
The exemption is narrow: it does not widen ownership for any other role or
any project-backed task.
Unit tests cover both reviewers dispatched, one-shot dispatch, the CEO
notification fired exactly once when both are done (and not before), the
retry-on-failure path, the notification builder, and the board co-review
ownership exemption (allowed for board+coordination, blocked otherwise).
* fix(workspace): install dev deps post-clone + raise git commit timeout for large changesets
Cluster C6 (#10, #13, #12-investigate).
#10: per-agent workspace clones never had the project's dev dependencies
installed, so the make-quality gate (ruff/mypy/pytest for Python, the TS
toolchain for the panel) was missing and devs re-downloaded tooling per
task. WorkspaceService now runs the project's install after cloning
(`uv sync` for Python, `pnpm install`/`npm ci`/`npm install` for Node/TS,
detected by manifest/lockfile). Idempotent via a lockfile-digest marker
under .git/ so a re-entry with unchanged lockfiles is a no-op; also runs on
the healthy short-circuit so pre-existing clones get backfilled. Gated by
workspace_install_dev_deps (default on) with workspace_dep_install_timeout_seconds.
#13: the gateway commit verb timed out on the large panel changeset because
every git op used the hardcoded 30s _GIT_TIMEOUT and each call also re-walks
the tree to chown. _run_git now takes a per-call timeout override sourced
from settings (git_command_timeout_seconds default); the staging + commit
ops in commit() and create_commit() use the longer git_commit_timeout_seconds
(default 180s). httpx REST timeouts unchanged in value.
#12 (investigate only — no push, no history change): the clone base ref is
NOT hardcoded; it already comes from project.default_branch threaded through
git.get_workspace -> ensure_workspace -> _clone_repo (git clone --branch).
The stale-base problem is a deploy/process issue (GitHub master is behind the
deployed migration chain), resolvable only by pushing the chain to master.
The default_branch column is the existing configurable lever.
* fix(panel): gate Approve & Start to board coordination tasks; stop 404 storm on closed sessions
CEO gate #1 button only renders for a PENDING board coordination/fan-out
task (no project_id, has product_id) — the board-reviewed handoff that
approve_and_start accepts — instead of every PENDING board-team task.
approve_and_start requires PENDING (it re-targets to Main PM without a
status change), so the gate stays on PENDING rather than the unrelated
end-of-work awaiting_ceo_approval state.
Session/message reads now treat a 404 as terminal and never retry it: a
reaped session is gone for good, and retrying every dead session-id is
what produced the growing 404 storm on GET /api/messages. The transcript
loads once (staleTime Infinity, no focus/reconnect refetch) so closed
sessions stay viewable without re-polling.
* fix(orchestrator): role-correct respawn prompt, throttle agentless dispatch, broaden #14 guard
#19 wrong-role prompt on respawn: _get_prompt_for_agent fell through to the
developer prompt for every non-dev/doc/qa role, so a respawned PM or board
agent was told to write code and call verbs it does not own. Route by the
agent's actual role through the existing per-role prompt builders
(developer/qa/documenter/cell_pm/main_pm/product_owner/head_marketing/auditor).
Both callers benefit; _spawn_pending_dev only ever passes developer/documenter/
unknown, so its behavior is unchanged.
#19 spawn-burst: _dispatch_claimed_without_agent looped over every agentless
claimed/in_progress task and could spawn many containers in one tick. Break
after the first respawn so a restart can't trigger a burst, matching every
sibling dispatcher. The release-to-pending path spawns nothing and keeps
draining stale unknown claims.
#14 guard scope: _is_descendant_code_task only matched CODE, so a descendant
DOCUMENTATION or DESIGN task escalated to a board/advisory role was still
stranded on a role with no verb to own it. Rename to
_is_descendant_executable_task and broaden to CODE/DOCUMENTATION/DESIGN — the
cell-executed types a board role cannot own. PLANNING/RESEARCH/ADMINISTRATIVE
route to a PM, not a cell agent, and are left unchanged; root tasks are still
reviewed up the chain.
* fix(docker): add node+pnpm to orchestrator so it pre-installs frontend cell deps
* Added .github workflows
* refactor(services): extract helpers to keep install_dev_deps + developer task-type check under the xenon complexity gate
* chore(github): add launch kit — CI, GHCR release, labels, templates, funding, dependabot npm, community docs
* chore(github): bump_version — drop unused noqa, fix datetime UTC import
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
|
||
|
|
9e9dd55b5d |
fix(task): drop cell_pm→main_pm auto-escalation in complete (#178)
`service.complete()` ran a two-tier approval chain
(`_apply_complete_approval_chain`): when a Cell PM completed an
`awaiting_pm_review` task, `_handle_cell_pm_escalation` silently
reassigned `task.assigned_to = main_pm.id` (+ `claimed_by`) and kept
the task in `awaiting_pm_review` for a second-tier review by Main PM.
That model is incompatible with the gateway's `main_pm_complete`,
which explicitly rejects any non-root task
(`if t.parent_task_id is not None: return invalid_state("main_pm
complete only operates on root tasks")` —
choreographer/_impl.py:3860). Result: the leaf got handed to main-pm
with no verb that could advance it → permanent wedge.
Observed end-to-end this session (smoke run 02:25–02:35):
- 02:25:56 leaf → awaiting_pm_review (correctly assigned to be-pm via
notify_pm_of_docs_complete).
- 02:26:42 be-pm cell_pm_complete REJECTED tracing_gap journal:reflect
(proves leaf IS assigned to be-pm).
- ~02:27 (silent — success-path is INFO, filtered): be-pm wrote the
reflect note + retried → choreographer cell_pm_complete → git.pr_merge
→ service.cell_pm_complete → service.complete(agent=be-pm) →
_apply_complete_approval_chain → _handle_cell_pm_escalation →
task.assigned_to = main_pm.id (no `task.reassigned` audit because the
event goes to `_emit_task_event(EventType.TASK_ESCALATED_TO_MAIN_PM)`,
not the gateway audit log).
- 02:27:30 _dispatch_pm_review_work (orchestrator) saw leaf with
assigned_to=main-pm → spawned main-pm against the leaf (target_id
in audit_log confirms it: `target_id=f3bdd585 agent_slug=main-pm`).
- 02:28:21+ be-pm cell_pm_complete → not_authorized "not assigned to
you". main-pm main_pm_complete → invalid_state "only operates on
root tasks". Closure dispatcher cycled both PMs to budget-reap.
Fix: remove the cell_pm branch from `_apply_complete_approval_chain`.
Cell PM completing a non-root awaiting_pm_review task now transitions
it to COMPLETED (the gateway model). Cell→main escalation, when
intended, uses the dedicated `submit_up` verb on the cell-level parent,
not `complete`. The main_pm → CEO branch for root parents stays.
Removed the now-dead `_handle_cell_pm_escalation` helper and the now-
unused `agent_id` parameter on `_apply_complete_approval_chain`.
Tests inverted: `test_complete_cell_pm_escalates_to_main_pm` →
`test_complete_cell_pm_does_not_escalate_to_main_pm` (asserts
status=COMPLETED, assigned_to != main_pm.id). The no-Main-PM-fallback
test trivially still passes (the path is now the only path).
Lifecycle test comment updated. make quality green.
|
||
|
|
4c397e1768 |
feat(gateway): developer i_will_work_on takes a substantive step checklist (#172)
The dev plan was a free string with only a presence gate, so the executing dev had no checklist for plan-driven progress (#173). - IWillWorkOnRequest gains `steps` (same SubTask shape as a PM's sub_tasks); flow_dev route threads it through. - i_will_work_on layers steps onto the narrative plan via the same panel-shaped path PMs use, so task.plan.sub_tasks is populated (panel render + #173 progress). - New _dev_steps_gate (mirrors _pm_sub_tasks_gate, runs after the spec gate): a developer FRESH claim must supply a non-empty steps list with every description >= _PM_SUBTASK_DESC_MIN_LEN. Re-entry/recovery short-circuit before the gate (extracted _dev_reentry + _fresh_dev_claim keep i_will_work_on within the return-count + cyclomatic gates). - developer role prompt: steps template + "thin steps rejected" + the progress(plan_step=...) handoff. - Updated every dev-fresh-claim test fixture across the suite to pass substantive steps; added dedicated _dev_steps_gate coverage. Commit 2 of 3 for the plan/progress quality work (#171/#172/#173). |
||
|
|
4fdde2b082 |
fix(gateway): evidence/QA/doc paths populate files_changed from git
Bug:
ContentActions.evidence() hard-coded files_changed=[] and diffed
against HEAD~1 instead of the branch's parent. QA's _build_qa_claim_
evidence (and doc/_impl mirrors) sourced files_changed from
work_session.files_modified, which the gateway commit() never
populates (no add_files_modified plumbing). Result: QA / docs / PM
reviewers saw an empty change list on real PRs and only the latest
commit's delta — flagged in smoke-9 when PR #20 showed the README
change on GitHub but evidence() reported empty.
Fix:
Added GitService.list_changed_files (git diff --name-only against
parent branch). evidence(), _build_qa_claim_evidence,
_claim_doc_evidence, and _build_i_am_done_ok all source files_changed
from this — git is the authoritative source. evidence() also drops
the HEAD~1 base so the diff is the full PR.
Wired EvidenceRepo into ContentActionsDeps so evidence() returns
journal_highlights too, matching the QA/doc shape.
|
||
|
|
41ef7f6b4e |
feat(gateway): C8 PM-decision gate windowed satisfaction
_check_pm_decision_required now requires the latest journal:decision within pm_decision_window_seconds (default 300). Older decisions no longer satisfy the gate. Adds JournalService.latest_decision_at. Future-tighten (out of scope): per-verb-group consumption tracking would need persistent state — Choreographer is per-request today. |
||
|
|
207aaecd72 |
Feature: lifecycle canonical spec (#14)
* chore: clean make quality baseline on feature/lifecycle-canonical-spec
Three classes of pre-existing issues blocking `make quality`:
1. Alembic migrations 002/009/011 used runtime introspection
(op.get_bind() + inspect / bind.execute) without guarding for
offline (--sql) mode. `alembic upgrade head --sql` is part of
`make quality`; in offline mode `op.get_bind()` returns a
MockConnection with no inspection system, so the migrations
crashed before emitting their SQL stubs. Each migration now
short-circuits or simplifies in `context.is_offline_mode()` —
live-DB behavior is unchanged.
2. ruff format drift on three files left over from prior in-flight
edits (choreographer/_impl.py, content_actions.py, and one test
file). `ruff format` applied.
3. vulture flagged two unused `tb` parameters in async __aexit__
stubs in test_task_service_lifecycle_misc.py. The parameter is
protocol-required but unused by the body — renamed to `_tb`
(vulture treats underscore-prefixed names as intentionally unused).
`make quality` is now green from this branch's HEAD; subsequent
lifecycle-spec work can use it as the per-task gate.
* feat(lifecycle): canonical spec package + Role/Status/TaskType enums
Foundation for the canonical lifecycle/permissions module. Enums
mirror docs/internal/old/workflows/STATUS_TRANSITIONS.md +
PERMISSIONS.md. Tests pin enum membership against both the
predecessor canon and roboco.models.base.TaskType.
* feat(lifecycle): Decision dataclass with allow/reject/tracing_gap constructors
Single rejection shape every consumer maps to its native format
(Envelope, HTTP code, prompt hint). __post_init__ enforces the
allowed/rejection_kind invariants so a malformed Decision can't reach
a consumer.
* fix(lifecycle): tighten Decision invariants per Task 2 review
Two reviewer findings on the Task 2 Decision dataclass, addressed
in one commit:
1. The docstring promised `allowed=True ⇒ rejection_kind is None
AND missing == [] AND remediate is None`, but __post_init__ only
checked the rejection_kind half. A caller could construct an
allow-shaped Decision with stale missing/remediate fields and
sneak it past validation. Tighten __post_init__ to enforce the
full invariant. Add a regression test.
2. tracing_gap defensively copies the missing list (`list(missing)`)
to isolate the stored list from later caller-side mutation, but
no test pinned this. Add a regression test that mutates the source
list after construction and asserts the stored list is unchanged.
Issue 2 from the same review (mutable list vs tuple for `missing`)
is a broader design call deferred until consumers exist; the
defensive copy is sufficient until then.
* feat(lifecycle): Precondition/ActionSpec/IntentSpec/StatusTransition dataclasses
The four dataclasses that hold the canonical tables. ActionSpec and
StatusTransition are direct ports of pre-gateway PERMISSIONS.md +
STATUS_TRANSITIONS.md rows. IntentSpec is the gateway-only addition:
each gateway intent verb declares which atomic actions it composes.
* feat(lifecycle): _STATUS_TRANSITIONS table + STATUS_GRAPH view
Direct port of STATUS_TRANSITIONS.md. Every transition records its
trigger action and (optionally) a role constraint. STATUS_GRAPH is
the precomputed source→{targets} view callers use for reachability
checks.
* fix(lifecycle): pin role_constraint values + clarify Task-5 handoff
Two reviewer findings on Task 4 _STATUS_TRANSITIONS, addressed in
one commit:
1. The original Task-4 tests verified (source, target) pairs but
not role_constraint contents. A typo in a single role name (e.g.
forgetting MAIN_PM from escalate_to_ceo) would have slipped past
them silently. Add test_status_transitions_role_constraints_match_canon
pinning every non-None constraint and the cancel-block invariant.
2. role_constraint=None on the `claim` rows from PENDING and
NEEDS_REVISION was load-bearing — it is the explicit handoff
point between the StatusTransition table (state machine layer)
and CLAIM_RULES (per-role claim authority, lands in Task 5).
The original inline comment said this in passing; expand it so
the design choice is unmissable for a stranger reading just
spec.py.
* feat(lifecycle): _ATOMIC_ACTIONS + CLAIM_RULES + ROLE_TEAM_RULES tables
Direct port of PERMISSIONS.md. Every task management tool gets an
ActionSpec with allowed_roles, source_statuses, target_status,
self_review_block, and needs_team_match flags. CLAIM_RULES maps each
Role to the statuses they can claim from. ROLE_TEAM_RULES is the
per-slug team restriction.
* fix(lifecycle): tighten ActionSpec contracts per Task 5 review
Three reviewer findings on Task 5's _ATOMIC_ACTIONS table, addressed
in one commit:
1. set_plan.source_statuses widened to {CLAIMED, IN_PROGRESS} but
every existing caller (i_will_work_on / i_will_plan compositions)
runs set_plan while CLAIMED, between claim and start. Narrow to
{CLAIMED} only. If a future "edit plan mid-flight" feature lands,
widen explicitly with test coverage at that time.
2. needs_team_match was set True only on claim/qa_pass/qa_fail/
docs_complete. Defense-in-depth says every role-scoped task
action should re-assert team match (don't rely on the inheritance
chain through assigned_to alone). Flip to True on: start,
set_plan, block, pause, submit_verification, submit_qa,
submit_pm_review, complete, create_subtask. Leave False on
board/CEO actions and PM cross-cell interventions (unblock,
resume, cancel) where the cross-cell semantics are intentional.
3. claim.source_statuses is intentionally a SUPERSET of any single
role's CLAIM_RULES allowance (the table holds the union; CLAIM_RULES
holds the per-role authority). Add an inline comment above the
claim ActionSpec so a future reader doesn't conclude the two
tables disagree — they don't, they encode overlapping facts at
different grains.
* feat(lifecycle): _INTENT_VERBS table — every gateway verb declared
Each gateway intent verb is now a named composition of atomic actions
plus optional side effects. i_will_work_on = (claim, set_plan, start);
i_am_done = (submit_verification, submit_qa); open_pr is pure side
effects (push_branch, create_pr); etc.
* fix(lifecycle): widen block.allowed_roles to include QA + Documenter
Task 6 review caught a role-set inconsistency: i_am_blocked.allowed_roles
admits dev/QA/doc, but the underlying block.allowed_roles only allowed
dev+PM. Result: a QA or documenter calling i_am_blocked would pass the
IntentSpec gate and then be rejected by the composed ActionSpec gate
when Task 7 wires can_invoke_intent.
Widen block to include QA + Documenter. The semantic case is sound: a
QA reviewing a task can discover an external blocker; a documenter
writing docs may need PM intervention. Predecessor PERMISSIONS.md
restricted block to dev+PM, but with the gateway exposing i_am_blocked
to all worker roles, the underlying atomic must agree.
The deeper unclaim/escalate_up "imperative verb" concern from the same
review (composes=() but mutates state) is deferred to Task 8 where the
validator design lands.
* feat(lifecycle): public lookup functions + Context + preconditions
can_claim, can_invoke_action, can_invoke_intent, valid_next_verbs,
composed_actions_for, intents_for_role, status_after — the entire
public surface every consumer will use. Context carries the
caller-supplied state preconditions need (plan, journal-decision
flag, etc.). Preconditions for plan/commits/no_pr/ownership are
declared once and wired into the relevant IntentSpecs.
* fix(lifecycle): wire PRECONDITION_OWNERSHIP through Context.actor_id
Task 7 review found _p_owns_task reads agent.id but every call site
passes None for the agent arg. Result: getattr(None, "id", object())
returns a fresh sentinel, task.assigned_to == <sentinel> is always
False, and open_pr / i_am_done would reject every owner the moment
Task 9 wires consumers.
Fix: thread identity through Context.actor_id (new UUID field) and
rewrite _p_owns_task to read from the context. Both call sites already
pass the Context — no signature changes elsewhere. Add green-path
test exercising the owner-can-open-pr case the existing tests
missed (the Task 7 plan only tested precondition-failure paths,
which masked the bug).
Plus surface hygiene: STATUS_GRAPH, CLAIM_RULES, ROLE_TEAM_RULES,
and the four PRECONDITION_* constants are now in
roboco.lifecycle.__init__.__all__ so consumers in Tasks 8/9 don't
depend on the implicit `from roboco.lifecycle.spec import ...`
backdoor.
* feat(lifecycle): import-time self-consistency validators
10 validators run at module import; first failure raises
LifecycleSpecError and prevents the package from loading. Covers
status enum coverage, reachability, terminal exits, intent
compositions, status chain consistency, claim-rule role/status
coverage, self-review symmetry, team-rule slug existence, and
StatusTransition action references.
* fix(lifecycle): close validator gaps; resolve BACKLOG-claim and submit_qa IN_PROGRESS-shortcut ambiguity
Three reviewer follow-ups on Task 8's _validate.py, plus two real
data corrections the new action-target-reachability validator
surfaced.
1. Design spec §9 calls for "every ActionSpec.target_status, when
set, is reachable from each source_status via STATUS_GRAPH" —
missing from Task 8's 10 validators. Add
_check_action_target_reachable_from_source.
2. _check_role_team_rules_slugs verified slug existence in
AGENT_UUIDS but NOT that the cell team in ROLE_TEAM_RULES
matches the seed. Add _check_role_team_rules_team_match,
scoped to non-None entries only — None means "exempt from
team-match enforcement" (cross-cell roles), not "no team in
org chart".
3. test_validators_pass_on_real_spec was ceremonial. Add
test_run_all_validators_raises_on_unknown_intent_action,
a deliberate-break regression that monkeypatches _INTENT_VERBS
to inject a fake action and asserts LifecycleSpecError raises.
The new action-target-reachability validator caught two real
data inconsistencies between the predecessor canon docs and the
spec tables:
A. claim.source_statuses listed BACKLOG and CLAIM_RULES[*PM]
listed BACKLOG, but STATUS_GRAPH[BACKLOG] = {PENDING, CANCELLED}
only. Resolution: PMs use the explicit \`activate\` action to
move BACKLOG → PENDING, then claim from PENDING. Drop BACKLOG
from claim.source_statuses and CLAIM_RULES.
B. submit_qa.source_statuses listed IN_PROGRESS, but
STATUS_GRAPH[IN_PROGRESS] does NOT include AWAITING_QA. The
intent verb i_am_done composes (submit_verification, submit_qa)
which forces IN_PROGRESS → VERIFYING → AWAITING_QA — no
shortcut. Drop the stale IN_PROGRESS entry from
submit_qa.source_statuses.
Both corrections tighten the canonical state machine to a strict
no-skip transition graph. Pre-gateway PERMISSIONS.md/STATUS_TRANSITIONS.md
disagreements are resolved here; spec.py is the canon now.
* feat(gateway): Envelope.from_decision maps lifecycle Decisions to envelopes
Single shape adapter so verb bodies stop hand-composing rejection
envelopes. Each rejection_kind maps to a specific envelope flavor;
'self_review' folds into 'not_authorized' with a parenthetical hint;
constructing from an allow Decision raises (programmer error).
* feat(gateway): VerbRunner for atomic composed-action dispatch
Wraps spec.composed_actions_for(intent) in session.begin_nested()
so mid-sequence failures roll the DB back. Side effects run AFTER
the savepoint commits. Each atomic action name dispatches to a
TaskService method via a single, exhaustive _dispatch_atomic
mapping. New verbs slot in by adding an IntentSpec entry + a
_dispatch_atomic case if a new atomic is needed.
* refactor(gateway): i_will_work_on uses spec.can_invoke_intent + VerbRunner
Replace the bespoke status-branch dispatcher in i_will_work_on with the
spec-driven flow: load task -> load agent -> build spec.Context ->
spec.can_invoke_intent (and spec.can_claim for per-role status authority)
-> Envelope.from_decision on rejection -> VerbRunner.run_intent on success.
The _i_will_work_on_pending, _i_will_work_on_claimed,
_i_will_work_on_needs_revision, and _start_failed_envelope helpers are
removed; the runner replaces them. Two narrow verb-body re-entry blocks
remain for behaviors the spec does not yet model:
1. in_progress + same agent -> idempotent heartbeat-only return
2. claimed + same agent -> _resume_from_claimed (set_plan + start)
to recover from a stuck mid-claim crash without re-running claim
against a state the spec excludes.
The behavioral claim guards (already_active / paused / sibling_sequence)
also stay imperative for now -- they're not in the spec yet and migrate
into spec.extra_preconditions in a later task. Per-role claim authority
is enforced via spec.can_claim because the atomic claim action's
source_statuses are the union across roles; CLAIM_RULES narrows.
Parity test in tests/lifecycle/test_consumer_parity.py runs the verb
against every (role x status x task_type='code') combo (112 rows) and
asserts the envelope error matches the spec's Decision (or can_claim's
Decision when the intent gate passes but per-role claim authority does
not). This is the contract that makes spec/verb drift impossible.
Existing tests updated where rejection-message text changed (the spec
now produces the messages, e.g. "role 'cell_pm' may not call
'i_will_work_on'" instead of "PM cannot execute code") or where the
spec's stricter view ("invalid_state" -> "not_authorized" for a dev
trying to claim awaiting_qa) is more accurate. Test fixtures were
updated to wire task.session.begin_nested as a proper async context
manager (required by VerbRunner) and to set agent_for().id so runner-
driven calls line up with assert_awaited_with(task_id, agent_id).
* refactor(lifecycle): push CLAIM_RULES enforcement into can_invoke_action
Task 11's i_will_work_on migration had to call spec.can_claim()
separately after spec.can_invoke_intent() because the claim action's
source_statuses is the union across all claim-eligible roles —
can_invoke_intent alone would let a developer pass for claiming
awaiting_qa (a QA-only state).
The retrofit pattern would repeat in every claim-composing verb
(i_will_plan, claim_review, claim_doc_task). Push the per-role
narrowing inside can_invoke_action when the action is "claim",
using the same not_authorized vs invalid_state disambiguation
can_claim already implemented (status-reserved-for-another-role
returns not_authorized; status-no-role-can-claim returns
invalid_state). Extracted the body to _check_claim_rules_narrow
to keep can_invoke_action under xenon's complexity threshold.
Update _i_will_work_on_gate to drop the redundant spec.can_claim
call. Update test_consumer_parity.py to assert only against
can_invoke_intent's Decision.
Tasks 12-22 will inherit the cleaner pattern: spec.can_invoke_intent
is the single gate; verb bodies don't need per-action retrofits.
* refactor(gateway): i_will_plan uses spec.can_invoke_intent + VerbRunner
Migrates i_will_plan to the spec-driven pattern Task 11 set up for
i_will_work_on. The verb body now: (1) loads task + agent, (2) builds
Context, (3) checks idempotent/recovery re-entry, (4) calls
spec.can_invoke_intent, (5) returns Envelope.from_decision on
rejection, (6) delegates composition to VerbRunner. The
_i_will_plan_* helpers are removed — the runner replaces them.
Parity test in tests/lifecycle/test_consumer_parity.py runs the verb
against every (role × status × task_type) combo and asserts the
envelope matches spec.Decision.
* refactor(gateway): delegate uses spec.can_invoke_intent for role/state gate
Migrates delegate to the spec-driven role/state gate. The chain
validation (main_pm->cell_pm, cell_pm->its team's devs), the
assignee-vs-task_type rule (Cell PMs receive planning-typed only),
the enum coercion, and the parent-lifecycle/cap guards STAY in the
verb body — they encode delegate-specific semantics the spec
doesn't model.
Parity test in tests/lifecycle/test_consumer_parity.py asserts the
spec's role+state rejection is correctly surfaced. Chain/assignee
rejections continue to be tested in test_choreographer_pm_extras.
* refactor(gateway): open_pr uses spec.can_invoke_intent + VerbRunner
Migrates open_pr to spec-driven gating. The spec's
extra_preconditions (PRECONDITION_OWNERSHIP, PRECONDITION_COMMITS,
PRECONDITION_NO_PR) handle all three precondition checks; the verb
body delegates side-effect dispatch (push_branch, create_pr) to
VerbRunner.
Idempotent re-entry retained: an open_pr call against a task that
already has a PR (and the caller owns it) returns OK without
re-opening, rather than the tracing_gap the spec would otherwise
produce. This preserves agent ergonomics — two calls in a row
shouldn't surface a misleading "no_prior_pr" hint.
Parity test in tests/lifecycle/test_consumer_parity.py runs the verb
against representative (status x commits x pr_number) combos and
asserts the envelope matches spec.Decision.
* refactor(gateway): i_am_done uses spec.can_invoke_intent + VerbRunner
Migrates i_am_done to spec-driven gating. The spec's
extra_preconditions (PRECONDITION_OWNERSHIP, PRECONDITION_COMMITS)
handle ownership and commit-count checks; VerbRunner dispatches
the (submit_verification, submit_qa) atomic chain.
The tracing-gate preconditions (progress entry, journal:reflect,
acceptance criteria) and the field-level submit-qa gates stay in
the verb body — they model gates the spec doesn't yet cover.
Defense-in-depth: those gates run after the spec accepts the
ownership/commits checks.
Parity test in tests/lifecycle/test_consumer_parity.py runs the
verb against (role × status × ownership × commits) and asserts
the envelope matches spec.Decision.
* refactor(gateway): i_am_blocked uses spec.can_invoke_intent + VerbRunner
Migrates i_am_blocked to spec-driven gating. The journal:struggle
write stays in the verb body (it's a side effect outside the
lifecycle action). VerbRunner dispatches the `block` atomic action
via task_service.escalate.
Parity test in tests/lifecycle/test_consumer_parity.py.
* refactor(gateway): unclaim and resume use spec.can_invoke_intent
Migrates both verbs to the spec-driven gate. unclaim's verb body
keeps its dispatch (task.unclaim_for_agent) because composes=();
resume goes through VerbRunner with composes=("resume",).
The reassignment-rejection branch (introduced in
|