docs(rag): close the corpus drift — findings/collision/fast-path/forge/env-ladder coverage (#596)

Seventeen-page sweep of the agent-facing KB against shipped behavior:
required covers_parent_criteria and per-AC criteria_verified reach the
QA/PM/task-tools pages (the QA docs also named non-callable pass_review/
fail_review — the MCP tools are pass/fail); collision_context lands in the
QA/gate/planning evidence docs; the possibilities matrix gets its own
architecture page + config entry; the auditor page gains its missing
waive_finding and playbook-curation verbs; git-pr-types.md is rewritten
off the long-dead is_root_pr model; PR/workspace/git-error pages stop
assuming GitHub (forge-agnostic + env-ladder semantics).

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-19 19:00:48 +02:00
committed by GitHub
co-authored by Renn F
parent 700dbcd285
commit 862c0b27cd
17 changed files with 217 additions and 47 deletions
+18 -3
View File
@@ -42,17 +42,32 @@ You still cannot claim tasks, message agents, or write code — the scheduled sw
- Record private observations via `note(text="...", scope="reflect")`
- Attach evidence via `evidence(task_id)`
- Search the knowledge base via `roboco_ask_mentor` / `roboco_kb_search`
- Waive one open **minor/nit** revision-findings-ledger finding via `waive_finding(finding_id, note)` — see below
- Curate the KB's playbook queue via `approve_playbook` / `reject_playbook` / `archive_playbook` — a deliberate, bounded expansion of your read-only surface (KB curation, not agent comms, so the no-`dm` restriction still holds)
- Curate the Obsidian vault's narrative for a just-completed root task-tree via `curate_vault(task_id, narrative)` — see below (only when `ROBOCO_OBSIDIAN_VAULT_ENABLED`)
## What You CANNOT Do
- Claim, create, assign, complete, or cancel tasks
- Pass or fail QA
- Escalate (`triage` is your only flow verb besides `i_am_idle`)
- Escalate (`triage` is your only flow verb besides `i_am_idle`/`waive_finding`)
- DM agents (`dm`) or send `notify`
- Acknowledge notifications (silent observer — `notify_ack` is not yours)
- Write to project docs, write code, or run git write operations
## Waiving a review finding
The Auditor is the **only** role that can close a revision-findings-ledger finding without a dev actually fixing it — and only for non-blocking severity:
```python
waive_finding(
finding_id="a1b2c3d4",
note="Cosmetic — the naming nit doesn't affect behavior; not worth a rework cycle.",
)
```
`severity=blocker` and `severity=major` findings are refused outright — they must be fixed, never waived. Only `minor`/`nit` findings, still `open`, are eligible, and `note` is required (an empty note is rejected). No task status change: the ledger row moves `open -> waived` and a `task.finding_waived` audit event records the decision. See `docs/rag/architecture/review-findings.md`.
## Silent Observer Mode
The Auditor has **silent read access** across the org:
@@ -89,8 +104,8 @@ When the Obsidian vault is armed, the orchestrator spawns you once per completed
| MCP server | Verbs you can call |
|-----------------------|--------------------|
| `roboco-flow` | `triage`, `i_am_idle` |
| `roboco-do` | `note` (scope=`reflect`), `evidence`, `notify_list`, `notify_get`, `curate_vault` |
| `roboco-flow` | `triage`, `waive_finding`, `i_am_idle` |
| `roboco-do` | `note` (scope=`reflect`), `evidence`, `notify_list`, `notify_get`, `approve_playbook`, `reject_playbook`, `archive_playbook`, `curate_vault` |
| `roboco-git-readonly` | `roboco_git_status`, `roboco_git_log`, `roboco_git_diff`, `roboco_git_branch_list` |
| `roboco-optimal` | `roboco_ask_mentor`, `roboco_kb_search` |
+2 -2
View File
@@ -18,7 +18,7 @@
## What You CAN Do
- Pull pending parent tasks via `give_me_work()`
- Plan and start a parent task via `i_will_plan(task_id, plan)` (this also auto-creates the parent branch)
- Plan and start a parent task via `i_will_plan(task_id, plan)` (this also auto-creates the parent branch); its planning briefing carries `collision_context` when same-parent siblings already declare overlapping file globs or migrations, so you can sequence subtasks before you delegate them
- Create subtasks via `delegate(parent_task_id, title, description, body)`
- Triage your cell's queue via `triage()`
- Unblock blocked tasks via `unblock(task_id, reason, restore=True)``reason` (why the block is cleared) is recorded as your `journal:decision`, so no separate `note(scope='decision')` call is needed
@@ -105,7 +105,7 @@ delegate(
)
```
The args are **flat keywords** (not a nested `body=` dict). `assigned_to` must be a slug your role can delegate to (cell PMs only delegate to their own team's dev / QA / doc — see `_validate_delegation_chain` in `roboco/services/gateway/choreographer/_impl.py`). `covers_parent_criteria` lists the parent acceptance-criterion ids this subtask is responsible for — split the parent's criteria across subtasks so their union covers ALL of them, or the parent won't roll up. The subtask inherits the parent's `project_id` automatically; you don't pass it.
The args are **flat keywords** (not a nested `body=` dict). `assigned_to` must be a slug your role can delegate to (cell PMs only delegate to their own team's dev / QA / doc — see `_validate_delegation_chain` in `roboco/services/gateway/choreographer/_impl.py`). `covers_parent_criteria` lists the parent acceptance-criterion ids (or their exact text) this subtask is responsible for — split the parent's criteria across subtasks so their union covers ALL of them, or the parent won't roll up. This is **required, not advisory**, whenever the parent has any acceptance criteria: `delegate` refuses a child that declares none, and a ref that matches neither an AC id nor exact text is rejected naming the valid criteria — you can still delegate across multiple waves and leave some criteria for a later `delegate` call, but every subtask you create must name what it covers. The success envelope carries `parent_ac_coverage` (`covered`/`uncovered`) so you see the remaining gap in the same turn. The subtask inherits the parent's `project_id` automatically; you don't pass it.
## Completing Tasks
+12 -1
View File
@@ -48,7 +48,10 @@ open_pr(task_id) → opens the PR, transitions to awaiting_qa
└── QA fails → returns to needs_revision; fix + commit + open_pr again
i_am_blocked(task_id, reason) → external dependency; cell PM unblocks
i_am_done(task_id, notes, resolved_findings?) → batched verify + open_pr shortcut
i_am_done(task_id, notes, resolved_findings?) → batched verify + open_pr shortcut;
silently fast-paths straight to QA when the
possibilities matrix is armed and your work already
looks done (see "The possibilities-matrix fast path")
unclaim(task_id) → release a task back to the queue
resume(task_id) → recover after compact / restart
i_am_idle() → no work in your queue right now
@@ -97,6 +100,14 @@ When toolchain matching is enabled, `i_am_done` is refused if the project's test
When the architectural-conventions standard is enabled, `i_am_done` is refused on any block-level convention finding (e.g. a model defined in a router), reported with the offending `file:line` and a fix hint. A genuine false positive is cleared by committing a waiver in `.roboco/conventions.yml`.
## The possibilities-matrix fast path
When `ROBOCO_POSSIBILITIES_MATRIX_ENABLED` is armed, `i_am_done` checks whether your work already looks done — commits exist, the PR is open, every acceptance criterion is addressed, and no revision finding is still open. If so, it takes a fast path straight to `awaiting_qa` in one call instead of the standard multi-turn verify/journal derivation. You don't call anything different or opt in — you always just call `i_am_done(task_id, notes, resolved_findings?)`, and the fast path silently applies when it applies. The non-negotiable guards still run either way: ownership, branch pushed and not behind base, conventions, and every open finding named via `resolved_findings`. The fast path trusts the PR's own CI-green signal as the quality gate; if there's no CI signal it falls back to the local `make quality` gate, and a known-red CI refuses the fast path outright (fix CI, don't route around it) rather than shipping a broken build to QA.
## Sandbox DB and video-render preview
If your project opted into sandbox services (`projects.sandbox_services`), call the `request_sandbox(services=None, extensions=None)` content tool for a throwaway Postgres/Redis/Mongo instead of assuming your gate tooling has a real database — see `docs/rag/architecture/sandbox-db.md`. On a `source=video` authoring task, `i_am_done` refuses until you've called `request_render(...)` and Read every returned frame to verify the rendered clip (not just its HyperFrames source) — see `docs/rag/architecture/video-engine.md`.
## Recovering from a bounce (`needs_revision`)
QA (`fail`), the in-path PR reviewer (`pr_fail`), your PM (`request_changes`), or the CEO (`ceo_reject`) can bounce your task back to `needs_revision` — and now the feedback is structured, not just a prose note. `evidence(task_id)` carries `revision_findings`: the OPEN entries from the revision-findings ledger, each with `file`/`line`/`severity`/`expected`/`actual`/`fix`. Read every one before you touch code — this is the actual code-level feedback, not a summary of it.
+5 -3
View File
@@ -62,7 +62,7 @@ delegate(
notify(target="be-pm", text="New initiative assigned — see task", task_id=subtask_id)
```
`delegate` validates the delegation chain (main_pm → cell_pm) and the assignee-vs-task_type rule. Documentation is NOT delegatable — the lifecycle auto-creates the doc phase after the code subtask passes QA.
`delegate` validates the delegation chain (main_pm → cell_pm) and the assignee-vs-task_type rule. `covers_parent_criteria` is **required** whenever the initiative has acceptance criteria — `delegate` refuses a cell-PM subtask that declares none, and a ref matching neither an AC id nor exact text is rejected naming the valid criteria. Documentation is NOT delegatable — the lifecycle auto-creates the doc phase after the code subtask passes QA.
## Cross-Cell Coordination
@@ -106,10 +106,12 @@ This is for *help while work is in flight*. Finished cell-scoped work arrives by
You own the **root** task and the root→master PR. Each Cell PM assembles, gates, and merges its own cell→root PR into your integration branch (its `submit_up` enters the cell-level PR-review gate, not your queue) — so cell work lands on the root branch without you acting per-cell.
```
master ← feature/main_pm/{root} ← feature/{cell}/{root}/{cell-pm} ← dev branches
(CEO) (you, via gate) (cell PM, via gate) (devs)
head rung ← feature/main_pm/{root} ← feature/{cell}/{root}/{cell-pm} ← dev branches
(CEO) (you, via gate) (cell PM, via gate) (devs)
```
"head rung" is the project's env-ladder head (`roboco.models.env_branches.head_branch`) — typically `master`, but never assume the literal string: a project with no declared environment ladder resolves this from `projects.default_branch`, so this is unchanged for most projects. See `CLAUDE.md` "Env-branches ladder".
- A cell PM's `complete` merges a leaf PR into its cell branch; after the cell gate, its `complete` merges the cell→root PR into your root branch. You do not merge cell branches.
- Once every cell's parent is terminal, **`submit_root(root_task_id, notes)`** opens the root→master PR and enters the in-path gate (`awaiting_pr_review`). The **main PR reviewer** checks the assembled root diff: `pr_pass``awaiting_pm_review`; `pr_fail``needs_revision` (owned by you, fix + re-`submit_root`). The reviewer's verdict + structured findings are carried in your task handoff (`revision_findings`), and re-`submit_root` is refused if the root PR is **unchanged** since the last `pr_fail` — fix and commit before re-submitting. If a still-open finding remains unresolved, `submit_root` itself refuses (name it via `resolved_findings=[...]` first — see `docs/rag/architecture/review-findings.md`).
+6 -4
View File
@@ -10,22 +10,24 @@
## Core Responsibilities
1. Review **inbound** pull requests the org did not open — external / fork PRs (gated by an author allowlist), and, behind a second flag, internal org-repo PRs opened outside the agent task-flow.
2. Read the PR diff adversarially against the project's standards and post **one** complete change-request as a real GitHub review **on the PR itself** — no agent-to-agent chatter.
2. Read the PR diff adversarially against the project's standards and post **one** complete change-request as a real review **on the PR itself** (whichever forge the project uses — GitHub, Gitea, or GitLab) — no agent-to-agent chatter.
3. Journal evidence of what was checked.
The org's own in-flight integration PRs are skipped by the **inbound** poll above — a live task already owns their branch. Re-review of inbound PRs is driven by the PR's head commit: an unchanged PR is skipped, new commits open a fresh review.
## In-path PR-review gate
The `pr_reviewer` role also runs the **in-path gate** on the org's OWN assembled delivery PRs — the merge-level review QA does not do. When a cell PM bubbles up its cell→root PR (`submit_up`) or the Main PM opens the root→master PR (`submit_root`), the task enters `awaiting_pr_review`. The cell reviewer (be/fe/ux-pr-reviewer) reviews its cell's assembled PR; pr-reviewer-1 reviews the root→master PR for the cross-cell integration seam (the bug class that lives where the FE and BE meet). Workflow: `claim_gate_review(task_id)` → review the assembled diff against the parent objective + every acceptance criterion + the FE↔BE contract → `note(scope="learning", ...)``pr_pass(task_id, notes)` (moves it on to the PM merge) or `pr_fail(task_id, findings=[{file?, line?, severity, criterion?, expected, actual, fix?, evidence?}])` (sends it back to `needs_revision`, like a QA fail — the old `issues=[...]` string form still works this release but is deprecated). Either verdict is also posted on the assembled PR itself as a GitHub review (server-side, via the bot account) so the decision is visible on the PR the PM merges: `pr_pass` posts an APPROVE and `pr_fail` a REQUEST_CHANGES — except on the root→master PR, which only ever gets a plain COMMENT because only the CEO acts on `master`. This gate gives the merge level the reject teeth the PM otherwise lacks. Leaf dev tasks and branchless coordination roots skip the gate.
The `pr_reviewer` role also runs the **in-path gate** on the org's OWN assembled delivery PRs — the merge-level review QA does not do. When a cell PM bubbles up its cell→root PR (`submit_up`) or the Main PM opens the root→master PR (`submit_root`), the task enters `awaiting_pr_review`. The cell reviewer (be/fe/ux-pr-reviewer) reviews its cell's assembled PR; pr-reviewer-1 reviews the root→master PR for the cross-cell integration seam (the bug class that lives where the FE and BE meet). Workflow: `claim_gate_review(task_id)` → review the assembled diff against the parent objective + every acceptance criterion + the FE↔BE contract → `note(scope="learning", ...)``pr_pass(task_id, notes)` (moves it on to the PM merge) or `pr_fail(task_id, findings=[{file?, line?, severity, criterion?, expected, actual, fix?, evidence?}])` (sends it back to `needs_revision`, like a QA fail — the old `issues=[...]` string form still works this release but is deprecated). Either verdict is also posted on the assembled PR itself as a review (server-side, via the bot account) so the decision is visible on the PR the PM merges: `pr_pass` posts an APPROVE and `pr_fail` a REQUEST_CHANGES — except on the root→master PR, which only ever gets a plain COMMENT because only the CEO acts on `master`. This is forge-agnostic on GitHub and Gitea, which both support a real "request changes" review; GitLab has no such primitive, so on a GitLab-backed project `pr_fail` posts as a plain MR note instead of a blocking review — the task still transitions to `needs_revision` normally regardless of forge, only the PR-visible signal differs. This gate gives the merge level the reject teeth the PM otherwise lacks. Leaf dev tasks and branchless coordination roots skip the gate.
On a round ≥2 review, `claim_gate_review` also returns `prior_findings` — the FULL revision-findings ledger for this task, newest first. Your own prior verdict and every finding filed on it arrive in the briefing; check each one against the current diff before deciding, rather than re-deriving what you already found. `pr_fail`'s findings are capped the same way QA's are: a soft nudge above 5 in one call, a hard reject above 10. See `docs/rag/architecture/review-findings.md`.
`claim_gate_review` evidence also carries `collision_context` when the task under review has same-parent siblings that collide with it (overlapping declared file globs, or both adding a migration) — each entry names the sibling, the overlapping globs, and a drift flag when the diff's actual touched files stray from what was declared. `None` when there's no parent or no colliding sibling. Same collision map QA and the delegating PM see.
### Gate enforcement
When the architectural-conventions standard is enabled, `pr_pass` is refused on any block-level convention finding, the same way the developer's `i_am_done` is — the remediation hint points you at the offending `file:line` + the `pr_fail` verb (not `i_am_blocked`). When toolchain matching is enabled, `pr_pass` is likewise refused on a "broken" toolchain status. Your verdict note is a mandatory structured field (`pr_reviewer_notes`) written at `pr_pass` / `pr_fail`; it is persisted structured with a derived text mirror.
`pr_pass` also refuses while CI on the assembled PR's head commit is not resolvably green. Failing CI names the check(s) and points the remediation at `pr_fail` with a finding naming the failing check; pending / not-yet-scheduled / a transient GitHub API error are framed as retryable — wait and call `pr_pass` again once CI resolves, not a defect to route back to the dev via `pr_fail` unless the diff itself is also bad. A project with no CI configured at all passes through cleanly (the verdict note is stamped `ci_status: "no CI configured on this project"` so the PM can see the guard ran and deliberately did not block). Do not chase `i_am_blocked` for any of these — the reject lever is always `pr_fail`.
`pr_pass` also refuses while CI on the assembled PR's head commit is not resolvably green. Failing CI names the check(s) and points the remediation at `pr_fail` with a finding naming the failing check; pending / not-yet-scheduled / a transient forge-API error are framed as retryable — wait and call `pr_pass` again once CI resolves, not a defect to route back to the dev via `pr_fail` unless the diff itself is also bad. CI vocabulary differs per forge (GitHub check runs, GitLab pipelines, Gitea commit statuses) but is shaped into the same envelope before it reaches you — the green/red/pending read is identical regardless of which forge the project uses. A project with no CI configured at all passes through cleanly (the verdict note is stamped `ci_status: "no CI configured on this project"` so the PM can see the guard ran and deliberately did not block). Do not chase `i_am_blocked` for any of these — the reject lever is always `pr_fail`.
**The per-AC evidence-walk is non-negotiable.** Do not assert "criteria met" from a skim. Walk every acceptance criterion on the parent task ONE AT A TIME and pin it to a concrete `file:line` in the assembled diff that satisfies it. A criterion you cannot pin to a line is not satisfied — treat it exactly like a missing deliverable, not a maybe: a silently dropped AC is an automatic `pr_fail`.
@@ -36,7 +38,7 @@ You cannot `pr_pass` / `pr_fail` an assembled PR you authored (self-review guard
## What You CAN Do
- Pull an inbound-PR review task via `give_me_work()` and claim it via `claim_pr_review(task_id)`.
- Post your verdict via `post_pr_review(task_id, ...)` — the change-request lands on the PR as a GitHub review (server-side; you never push to the contributor's fork).
- Post your verdict via `post_pr_review(task_id, ...)` — the change-request lands on the PR as a review, server-side, on whichever forge the project uses (you never push to the contributor's fork).
- Run the in-path gate on the org's assembled delivery PRs: `claim_gate_review(task_id)``pr_pass(task_id, notes)` or `pr_fail(task_id, findings=[...])`.
- Read-only inspect git via `roboco_git_status / _log / _diff / _branch_list`.
- Search the knowledge base via `roboco_ask_mentor` / `roboco_kb_search`.
+16 -3
View File
@@ -17,7 +17,7 @@
## What You CAN Do
- Pull awaiting-QA tasks via `give_me_work()` / `claim_review(task_id)`
- Pass via `pass(task_id, notes)` (transitions to `awaiting_documentation`)
- Pass via `pass(task_id, notes, criteria_verified=[{criterion, evidence}, ...])` (transitions to `awaiting_documentation`) — one entry per task acceptance criterion, see "Passing QA" below
- Fail via `fail(task_id, findings=[{file?, line?, severity, criterion?, expected, actual, fix?, evidence?}])` (returns to `needs_revision`) — see "Failing QA" below. The old `issues=[...]` (plain strings) form still works this release but is deprecated.
- Read-only inspect git via `roboco_git_status / _log / _diff / _branch_list`
- Search the knowledge base via `roboco_ask_mentor` / `roboco_kb_search`
@@ -39,7 +39,9 @@
give_me_work() → returns an awaiting_qa task
claim_review(task_id) → claim for review
(auto-checks-out the dev's branch)
pass(task_id, notes) → moves to awaiting_documentation
pass(task_id, notes, criteria_verified=[{criterion, evidence}])
→ moves to awaiting_documentation; one
criteria_verified entry per task AC
fail(task_id, findings=[...]) → moves to needs_revision; the dev's
original assignee gets it back
i_am_blocked(task_id, reason=...) → external blocker (broken env, can't
@@ -81,10 +83,17 @@ pass(
"Redis TTL matches, tests cover the boundary. ruff + mypy "
"clean. Journal logged."
),
criteria_verified=[
{"criterion": "429 fires at the 101st request", "evidence": "test_rate_limit_boundary passes; manually traced the >= vs > fix at rate_limit.py:88"},
{"criterion": "Redis key TTL matches the configured window", "evidence": "verified TTL=60 in test_ttl_matches_window"},
{"criterion": "AC #3 — Redis-down failover path", "evidence": "test_redis_down_failover covers the fallback branch"},
],
)
```
`notes` must be substantive — the enforcement layer rejects empty or near-empty notes. The transition takes the task to `awaiting_documentation`; the documenter and the dev work in parallel from there.
`notes` must be substantive — the enforcement layer rejects empty or near-empty notes. `criteria_verified` is **required whenever the task has acceptance criteria**: one `{criterion, evidence}` entry per criterion, `criterion` matched against the task's AC ids/exact text (the same fuzzy matcher the findings ledger uses) and `evidence` capped at 500 chars and soup-checked (no filler). Missing an entry, or naming a criterion the task doesn't have, is rejected — the error lists exactly which criteria are still unverified, so a gestalt "looks good" pass without a per-AC trace is structurally impossible. Each entry renders deterministically into `qa_notes` as its own line: `[AC] <criterion> — verified: <evidence>`, appended after your `notes`. A zero-AC task imposes no `criteria_verified` requirement.
The transition takes the task to `awaiting_documentation`; the documenter and the dev work in parallel from there.
Your pass/fail note is a mandatory structured note (a QaNote) carrying substantive findings, not an empty string. It is persisted structured, and the legacy `qa_notes` text column is derived from it.
@@ -94,6 +103,10 @@ When the architectural-conventions standard is enabled, the evidence returned on
On a round ≥2 review (a task that has bounced before), `claim_review` also carries `prior_findings` — the FULL revision-findings ledger for this task, every round, newest first. Check each prior finding against the current diff before you pass; one still unaddressed is a fail, not a pass with a note. See `docs/rag/architecture/review-findings.md`.
## Collision Context in Review Evidence
`claim_review` evidence also carries `collision_context` when this task has same-parent siblings that would collide with it — overlapping declared `intends_to_touch` globs, or both siblings adding a migration. Each entry names the sibling, the overlapping globs, and (when the diff's actual touched files are known) an `undeclared` list flagging files touched but never declared — a drift signal worth a second look, not an automatic fail. `collision_context` is `None` when the task has no parent or no colliding siblings. This is the same collision map the PR-gate reviewer and the delegating PM see (`docs/rag/architecture/review-findings.md` covers findings; the collision builder itself is `roboco/services/gateway/choreographer/collision.py`).
## Failing QA
```python