Appearance
Findings Routing
After the Critic runs, the orchestrator merges the critic's findings (across all three audit axes) and routes each to the right next-spawn destination. Routing is deterministic: finding category → destination is a lookup in lib/nubosloop.cjs::ROUTE_TABLE, not a heuristic.
ADR-0010 Step 6 ratifies the design. The implementation is lib/nubosloop.cjs::routeFindings.
Since the Single-Critic Revision (2026-05-05) one np-critic agent emits findings across all three axes in a single JSON. The Source critic column below points at the audit-surface module that defines the category (agents/np-critic-{style,tests,acceptance}.md, module: true); these are not spawnable agents, the parent np-critic spawn loads them via Read.
The full routing table
| Finding category | Source critic | Routed to | Effect on loop |
|---|---|---|---|
style | style module | Executor / Build-Fixer | Round++ |
dead-code | style module | Executor / Build-Fixer | Round++ |
dangling-thread | style module | Executor / Build-Fixer | Round++ |
todo-marker | style module | Executor / Build-Fixer | Round++ |
import-hygiene | style module | Executor / Build-Fixer | Round++ |
comment-hygiene | style module | Executor / Build-Fixer | Round++ |
lint-violation | style module | Executor / Build-Fixer | Round++ |
critic-error | critic (hard-stop) | stuck | Loop terminates |
rule-9-violation | loop-audit-tool-use (Rule 9 enforcement) | Executor / Build-Fixer | Round++ — re-run with explicit instruction to invoke search-knowledge |
missing-test | tests module | Executor / Build-Fixer | Round++ |
edge-case-gap | tests module | Executor / Build-Fixer | Round++ |
weak-assertion | tests module | Executor / Build-Fixer | Round++ |
silenced-failure | tests module | Executor / Build-Fixer | Round++ |
test-naming | tests module | Executor / Build-Fixer | Round++ |
non-deterministic | tests module | Executor / Build-Fixer | Round++ |
verify-mismatch | tests module | Executor / Build-Fixer | Round++ |
unmet-criterion | acceptance module | Executor / Build-Fixer | Round++ |
scope-creep | acceptance module | Executor / Build-Fixer | Round++ |
information-missing | acceptance module | Researcher-Schwarm | New Researcher round |
question-to-user | acceptance module | askuser (Temporal-style signal-wait) | Loop pauses |
locked-decision-violation | acceptance module | np-plan-checker | Plan-checker re-run; potential plan revision |
infrastructure-mismatch | acceptance module | np-plan-checker | Container/runtime skew — milestone-level infra config bumps |
stuck-detected | critic | stuck | Loop terminates |
Rule 9 audit injection (rule-9-violation)
rule-9-violation findings are not emitted by any Critic. They originate from the mechanical tool-use audit (Step 4 of the Nubosloop). The orchestrator persists each spawn's audit verdict via loop-audit-tool-use with the current round stamp; at loop-run-round --phase post-critics, auditFindingsForRound(taskId, round, cwd) converts the round's violations into findings with category = rule-9-violation and critic = audit.
These synthetic findings join the merged Critic-Schwarm output before routing, so they:
- Participate in the same fingerprint dedup (a Critic that already flagged the same gap collapses with the audit finding instead of double-counting).
- Sort by the same
(confirmation, severity, category)order —confirmed_by: ['audit'], severityfail. - Route via
ROUTE_TABLE['rule-9-violation'] = 'executor'so the next round re-runs the spawn with explicit instruction to invokesearch-knowledge.
The round stamp on each audit entry keeps the chain idempotent: re-running --phase post-critics for the same round produces the same findings; advancing the round filters out the prior round's audits.
Merge before routing
lib/nubosloop.cjs::mergeCriticOutputs deduplicates findings by fingerprint:
fingerprint = category | file (lowercased) | line | first-80-chars-of-remediation (lowercased)The single np-critic spawn emits one merged JSON across all three axes. Fingerprint dedup still matters because the same gap is sometimes surfaced by both an explicit findings[] entry AND auto-promoted from a criteria[] Unsatisfied verdict. After dedup, the merged list sorts by:
- Confirmation count (descending).
confirmed_byis a single-element list (['critic']or['audit']) under the post-Single-Critic regime; legacy three-Critic markers (['style','tests','acceptance']) are still accepted bymergeCriticOutputsfor backward-compat with cached outputs. - Severity (
fail<risk<nit). - Category (lexicographic) for stable tie-breaking.
The next-round prompt therefore highlights the most critical findings first, and the Executor / Build-Fixer addresses them in priority order.
Verdict-Only Contract (Cost Layer L5). The critic writes the full findings JSON to <report_path> (provided in the spawn prompt). loop-run-round --phase post-critics --critic-outputs-path <file> reads it directly, so only a small envelope traverses parent context. Inline --critic-outputs <json> is the legacy fallback; both at once is loop-run-round-post-critics-conflicting-outputs. See ADR-0010 §L5.
Decision rules
routeFindings(findings) runs after the merge. The next destination is decided by:
| Condition | Next destination |
|---|---|
any finding has category = stuck-detected OR loop.maxRounds reached | stuck |
any finding routes to askuser | askuser |
any finding routes to plan-checker | plan-checker |
any finding routes to researcher | researcher |
any finding routes to executor | executor |
| no findings | commit |
askuser and plan-checker outrank researcher and executor because they require human or planner intervention, which the executor cannot resolve.
Stuck escalation
Each routed round increments the per-task loop counter persisted in lib/checkpoint.cjs (under the nubosloop key). When the round reaches loop.maxRounds (default 3):
evaluateLoopreturns{ next_action: 'stuck', stuck: true }.- The orchestrator writes a
stuckmarker toSTATE.mdwith the last Critic findings appended. np:dashboardshows the marker with a red badge.askuserprompts the operator: continue with manual override, escalate to a human developer, ornp:reset-sliceand re-plan.
The stuck state is first-class; it never silently downgrades to "good enough". This makes Rule 12 (Boil the ocean) of the Completeness Doctrine mechanical.
Worked routing trace
Round 1 of a task that touches an unfamiliar API:
np-critic envelope (final message, ~150 bytes):
{ critic: "critic", task_id: "M001-S001-T0001", round: 1,
verdict: "issues_found", blockers_count: 3,
report_path: "/tmp/nubos-pilot/critic-reports/critic-M001-S001-T0001-r1.json",
run_id: "…" }
loop-run-round --phase post-critics --critic-outputs-path <report_path>
reads the file:
findings[]: 3 entries
[ todo-marker @ src/api.php:42 (severity=fail) ]
[ missing-test @ tests/Feature/ApiTest.php (severity=fail) ]
[ information-missing @ — (severity=fail, "Need GetAG webhook spec") ]
criteria[]: (auto-promoted to findings as needed)
mergeCriticOutputs (no duplicates by fingerprint):
3 findings retained, sorted by severity (all `fail`), then category.
routeFindings:
todo-marker → executor (bucket: executor)
missing-test → executor (bucket: executor)
information-missing → researcher (bucket: researcher)
next_destination decision:
- askuser: empty
- plan-checker: empty
- researcher: 1 finding → next = researcher
→ Spawn Researcher-Schwarm for "GetAG webhook spec".
Round counter advances to 2.
After researcher returns, Executor consumes consensus + addresses style/test findings.Configuration
The route table is fixed in lib/nubosloop.cjs::ROUTE_TABLE. Adding a new finding category requires a code change plus an ADR amendment. This rigidity is intentional: it keeps routing deterministic and audit-friendly.
Related
- Nubosloop — Step 6 lives here.
- Researcher-Schwarm — destination for
information-missing. - Completeness Doctrine — Rule 12 makes "stuck" first-class.
- ADR-0010 — full architectural decision record.
