Skip to content

Phase 2 — State-machine & gateway integrity (rides 0.19.0)#316

Merged
rennf93 merged 114 commits into
fix/scan-auth-securityfrom
fix/scan-state-machine-gateway
Jul 7, 2026
Merged

Phase 2 — State-machine & gateway integrity (rides 0.19.0)#316
rennf93 merged 114 commits into
fix/scan-auth-securityfrom
fix/scan-state-machine-gateway

Conversation

@rennf93

@rennf93 rennf93 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Phase 2 — State-machine & gateway integrity (rides 0.19.0)

Second phase of the 2026-07-06 full-codebase-scan fix program. Eleven verified-real state-machine / gateway concurrency and integrity fixes plus an e2e smoke module, all under the existing ## [0.19.0] CHANGELOG section. Stacks on top of Phase 1 / Phase 1b (fix/scan-auth-security, PR #315) — base is fix/scan-auth-security; merge #315 first.

Fixes

  • M18 (7172ab61) — add_dependency rejects self-reference + detects cycles. Prevents a task from depending on itself or forming a dependency cycle that would deadlock dispatch.
  • H10 (aec12879) — WorkSessionService.create translates the DB IntegrityError on a duplicate active session into a clean ConflictError. Closes the claim/plan crash-into-respawn loop when a stale active session row collides with a fresh claim (migration 047's partial-unique index fires).
  • M19 (fb31534e) — _qa_or_doc_claim locks the task row FOR UPDATE before the claim-guard check, with a competing-claimant guard. Prevents two QA/doc agents claiming the same awaiting_qa/awaiting_documentation task concurrently and both succeeding. PM-owner is intentionally overridable by the first reviewer (enforce_competing_claimant=False on pr_gate_claim).
  • H4 (aad9c307) — docs_complete + mark_pr_created lock the task row FOR UPDATE. Prevents the lost-update where the documenter and developer's parallel completion writes race on the same task row.
  • H3 (79961b06) — complete() gated to leaf / branchless tasks only when IN_PROGRESS. Closes the PM-bypass of the PR-review gate (a PM could complete() its own in-progress branchless root, skipping awaiting_pr_review).
  • H5 (da546de6) — _unclaim_from_blocked clears the stale pre-block snapshot. Prevents a re-claim after a block reusing a stale snapshot and resuming from the wrong state.
  • M20 (0002c14d) — admin_set_status terminal guard + skips the revision_count increment under force=True. A forced admin status change is not a QA/PR bounce and must not inflate rework metrics.
  • H7 (4bcbc9a2) — cell_pm_complete runs an idempotent "already merged to target" pre-check before merging. Prevents the merge-after-already-merged error when the integration branch was already merged to target.
  • H6 (6af30978) — gateway post-transition side-effects (pass_review reassign+a2a, doc handoff, documents stamp) wrapped in the runner savepoint / try-except. A side-effect failure no longer corrupts the transition's committed state.
  • L29 (16cf2d5e) — pass_qa/fail_qa accept only tasks in awaiting_qa. Operational QA routes can no longer act on a task that already left the QA state.
  • L30 (0c840258) — mark_pr_created passes audit_agent_id. The PR-created audit event now attributes to the acting agent instead of a blank.
  • e2e smoke (fb066a0f) — tests/e2e_smoke/test_state_machine.py, one scenario per finding (M18/H4/H3/H5/H6/L29), scripted-agent over the real API + ephemeral Postgres + fake GitHub.
  • quality gate (b0b646b9) — ruff/mypy green; bundles three follow-on fixes surfaced during the gate: submit_for_qa clears active_claimant_id (root-cause for M19's competing-claimant guard letting the legitimate QA claim through), _StubGit.is_pr_merged_for_task (H7 test rig), and M19 unit-test mock-session fixes.
  • M19 follow-on root-cause (ab5dac8f) — opus whole-branch review found the active_claimant_id clear lived in the gateway wrappers (qa_pass/qa_fail) not the transition methods (pass_qa/fail_qa) themselves. The direct REST routes POST /pass-qa + POST /fail-qa call the transitions directly, bypassing the wrappers → stale claimant rejected the next legitimate doc/QA claim; admin_set_status had the same gap for a non-blocked override into a review/queue state. Fixed by moving the clear INTO pass_qa/fail_qa + admin_set_status (mirroring submit_for_qa), dropping the now-redundant wrapper clears. Covering tests: test_pass_qa_clears_active_claimant_for_doc_claim, test_fail_qa_clears_active_claimant, test_admin_set_status_into_review_queue_clears_active_claimant, test_admin_set_status_non_review_queue_keeps_active_claimant.

Plan discrepancies (acknowledged)

  • H4 — the plan's failing-test-first proof doesn't reproduce in single-session (the status gate catches the double-transition first). The FOR UPDATE lock is still the correct fix for the real concurrent race (two agents claiming the same row); the test is a guard, not a TDD proof. Noted in the commit.
  • M19 — the plan claimed pr_gate_claim's role-scoped guard is a superset of the competing-claimant guard. It is not (PM-owner is intentionally overridable by the first reviewer). Fixed with an explicit enforce_competing_claimant param.

Verification

  • uv run ruff format + ruff check — clean.
  • uv run mypy roboco/ — clean (357 files).
  • uv run pytest tests/unit/ tests/integration/ — 6905 passed; 10 pre-existing DB-contamination failures + 1 error (test_metrics_service.py, test_tasks_routes.py, test_board_review_gate_timeline_real_db.py) — all 206 pass when those three files run in isolation. Pre-existing test-isolation gap, not a Phase-2 regression.
  • make e2e-smoke — every e2e scenario passes in isolation (all 19: 4 auth-gate from Phase 1b, 6 state-machine from Phase 2, 9 pre-existing). The full-suite run is order-dependent (9–15 pass / 4–10 fail) because the session-scoped e2e_stack shares the be-dev-1 workspace across scenarios — test_state_machine.py's 6 scenarios contaminate each other within the file (full file = 4 failed / 2 passed; each scenario alone = 1 passed), and the bleed varies with pytest's run order. Pre-existing harness limitation (per-scenario workspace reset would fix it), documented out of scope for this phase; not a product regression.
  • Whole-branch opus review: Ready to push (no Critical; the one Important finding — M19 follow-on active_claimant_id clear in wrappers not transitions — fixed in ab5dac8f; two Minor findings (M-2 H3 CEO force path, M-3 L30 sibling callers) are arguably-correct/theoretical-only, not push-blocking).

Known pre-existing issues (not introduced here)

  • Pytest full-suite DB-state contamination across the three files named above (each passes in isolation).
  • e2e smoke harness shares the be-dev-1 workspace across scenarios in one file — a per-scenario fixture reset would fix it. Harness change, not a service-layer fix.

Model note

Implementer ran on sonnet; whole-branch review on opus. kimi-k2.7-code:cloud is not routable in this harness.

🤖 Generated with Claude Code

rennf93 added 14 commits July 6, 2026 17:36
ruff format + check: green
mypy roboco/: green (357 files)
pytest unit+integration: 6905 passed, 10 pre-existing DB-contamination
  failures (pass in isolation)
e2e smoke: 11 passed, 4 cross-scenario workspace-contamination failures
  (all 6 state-machine scenarios pass individually)

Quality-gate fixes:
- move function-local imports to module top (PLC0415)
- fix M19 regression: submit_for_qa clears active_claimant_id so the
  competing-claimant guard lets the QA claim through
- fix H7 regression: _StubGit gains is_pr_merged_for_task
- fix M19 unit tests: mock session.execute for the FOR UPDATE lock
- e2e H3: notes >= 20 chars; e2e H5: rich i_will_work_on inputs +
  PM unclaims (block reassigns to PM)
…_status (M19 follow-on)

Phase 2 opus whole-branch review found the M19 follow-on clear lived in
the gateway wrappers (qa_pass/qa_fail) not the transition methods
(pass_qa/fail_qa) themselves. The direct REST routes POST /pass-qa and
POST /fail-qa call the transitions directly, bypassing the wrappers and
leaving the QA's stale active_claimant_id set in AWAITING_DOCUMENTATION
/ NEEDS_REVISION — the competing-claimant guard then rejects the next
legitimate documenter/QA claim. admin_set_status had the same gap for a
non-blocked override into a review/queue state (IN_PROGRESS->AWAITING_QA
left the dev's id, blocking qa_claim).

Root-cause fix: move the clear INTO pass_qa and fail_qa (mirroring
submit_for_qa), add a clear in admin_set_status when
new_status in _REVIEW_QUEUE_STATES and from_status != BLOCKED, and drop
the now-redundant clears + flushes from the qa_pass/qa_fail wrappers.
Every caller is covered; the wrappers keep their actor-mismatch warnings.

Covering tests: test_pass_qa_clears_active_claimant_for_doc_claim
(asserts a subsequent doc_claim succeeds), test_fail_qa_clears_active_claimant,
test_admin_set_status_into_review_queue_clears_active_claimant,
test_admin_set_status_non_review_queue_keeps_active_claimant. Updated
the two wrapper unit tests that asserted the wrapper clears (now the
transition's job).
@github-project-automation github-project-automation Bot moved this to Backlog in RoboCo Kanban Jul 6, 2026
@github-actions github-actions Bot added area: services Touches roboco/services/ (business logic, side effects) tests Test suite changes area: gateway Touches roboco/services/gateway/ (Choreographer, verb surface) labels Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Thanks for opening your first pull request on RoboCo!

Quick checklist before review (most of these are enforced by CI, but worth a glance):

  • make quality — ruff format check, ruff check, mypy, pytest (≥80% coverage), and the rest of the gate
  • Panel changes pass pnpm lint and pnpm exec tsc --noEmit (run from panel/)
  • No # noqa / # type: ignore shortcuts; pre-existing violations in touched files are fixed
  • Added an entry under ## [Unreleased] in CHANGELOG.md
  • Signed the CLA (the bot will prompt you on this PR)
  • Signed your commits — master requires verified signatures (SSH signing setup)
  • Updated any affected docs under docs/

See CONTRIBUTING.md for the full workflow and the Code of Conduct for the community standards we follow.

Welcome aboard — a maintainer will review shortly.

@rennf93 rennf93 force-pushed the fix/scan-state-machine-gateway branch from ab5dac8 to f7097c1 Compare July 6, 2026 15:52
rennf93 added 9 commits July 6, 2026 18:43
JournalService.delete_entry deleted the DB row but never de-indexed the
RAG chunks, so deleted/private journal content bled forever into RAG
answers and claim-time briefings. Add OptimalService.unindex_journal_entry
mirroring unindex_playbook (vector-store delete_by_source + tracking-row
delete via get_db_context, both idempotent + best-effort), and call it
from delete_entry after the row commit inside a try/except so a de-index
failure never errors the delete.
The memory distiller emits lessons with a fixed 'Problem: …' opening
shape, so two distinct lessons whose first 100 chars match collided on
learning_id = f"lrn-{md5(content[:100])[:12]}". replace_on_reingest then
routed both to the same source URI and the second ingest's replace_chunks
DELETE wiped the first lesson's chunks — silent data loss.

Hash the full content (widening the hex slice 12→16) so distinct bodies
get distinct ids and each retains its chunks.
… time

mark_read and mark_all_read used to zero the unread counter FIRST, then run
a bulk UPDATE … WHERE read_at IS NULL that stamped every inbound unread row.
A send_chat_message committing between the counter-zero and the UPDATE
inserted a new read_at NULL row that the UPDATE then stamped as read — the
new message was silently consumed while the counter stayed 0.

Mirrors get_unread_messages (same file): SELECT the unread message IDs at
call time, UPDATE exactly those IDs, then recompute the unread counter from
the DB via the existing _reset_unread_counter helper. A message arriving
mid-call is not in the selected ID set, so the UPDATE skips it and the
recomputed counter keeps it unread.
rennf93 and others added 20 commits July 7, 2026 06:38
[phase9] Auth hardening: bound agent tokens (iat/exp) + JWT jti revocation
[scan-fix] Phase 8: LLM providers / usage / billing
[Phase 7] schemas/conventions/MegaTask/API fixes (15 findings, scan-fix program)
Phase 6 — Panel frontend hardening (rides 0.19.0)
Phase 5 — Background-engine hardening (rides 0.19.0)
Phase 4 — Git / workflow hardening (rides 0.19.0)
Phase 3 — Data-integrity fixes (rides 0.19.0)
@github-actions github-actions Bot added documentation Docs, README, CHANGELOG, governance files dependencies pyproject.toml / uv.lock or panel package manifests area: panel Touches panel/ (Next.js control panel) area: api Touches roboco/api/ (FastAPI routes, schemas, app) area: db Touches roboco/db/ or roboco/models/ area: alembic Touches alembic/ (database migrations) area: orchestrator Touches roboco/runtime/ (agent spawner, dispatch loops) labels Jul 7, 2026
@rennf93 rennf93 merged commit 92bbf72 into fix/scan-auth-security Jul 7, 2026
4 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in RoboCo Kanban Jul 7, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area: alembic Touches alembic/ (database migrations) area: api Touches roboco/api/ (FastAPI routes, schemas, app) area: db Touches roboco/db/ or roboco/models/ area: gateway Touches roboco/services/gateway/ (Choreographer, verb surface) area: orchestrator Touches roboco/runtime/ (agent spawner, dispatch loops) area: panel Touches panel/ (Next.js control panel) area: services Touches roboco/services/ (business logic, side effects) dependencies pyproject.toml / uv.lock or panel package manifests documentation Docs, README, CHANGELOG, governance files tests Test suite changes

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant