Skip to content

Findings Routing

After the Critic runs, the orchestrator merges the critic's findings (across every active audit axis) 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 every active axis 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,economy}.md, module: true); these are not spawnable agents, the parent np-critic spawn loads them via Read. The economy module loads only when agents.economy is full or ultra, so its four categories appear only then.

The full routing table

Finding categorySource criticRouted toEffect on loop
stylestyle moduleExecutor / Build-FixerRound++
dead-codestyle moduleExecutor / Build-FixerRound++
dangling-threadstyle moduleExecutor / Build-FixerRound++
todo-markerstyle moduleExecutor / Build-FixerRound++
import-hygienestyle moduleExecutor / Build-FixerRound++
comment-hygienestyle moduleExecutor / Build-FixerRound++
lint-violationstyle moduleExecutor / Build-FixerRound++
critic-errorcritic (hard-stop)stuckLoop terminates
rule-9-violationloop-audit-tool-use (Rule 9 enforcement)Executor / Build-FixerRound++ — re-run with explicit instruction to invoke search-knowledge
missing-testtests moduleExecutor / Build-FixerRound++
edge-case-gaptests moduleExecutor / Build-FixerRound++
weak-assertiontests moduleExecutor / Build-FixerRound++
silenced-failuretests moduleExecutor / Build-FixerRound++
test-namingtests moduleExecutor / Build-FixerRound++
non-deterministictests moduleExecutor / Build-FixerRound++
verify-mismatchtests moduleExecutor / Build-FixerRound++
unmet-criterionacceptance moduleExecutor / Build-FixerRound++
scope-creepacceptance moduleExecutor / Build-FixerRound++
over-engineeringeconomy module (full/ultra)Executor / Build-FixerRound++
stdlib-reinventioneconomy module (full/ultra)Executor / Build-FixerRound++
native-duplicationeconomy module (full/ultra)Executor / Build-FixerRound++
shrinkableeconomy module (full/ultra)Executor / Build-FixerRound++
information-missingacceptance moduleResearcher-SchwarmNew Researcher round
question-to-useracceptance moduleaskuser (Temporal-style signal-wait)Loop pauses
locked-decision-violationacceptance modulenp-plan-checkerPlan-checker re-run; potential plan revision
infrastructure-mismatchacceptance modulenp-plan-checkerContainer/runtime skew — milestone-level infra config bumps
stuck-detectedcriticstuckLoop 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'], severity fail.
  • Route via ROUTE_TABLE['rule-9-violation'] = 'executor' so the next round re-runs the spawn with explicit instruction to invoke search-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:

  1. Confirmation count (descending). confirmed_by is a single-element list (['critic'] or ['audit']) under the post-Single-Critic regime; legacy three-Critic markers (['style','tests','acceptance']) are still accepted by mergeCriticOutputs for backward-compat with cached outputs.
  2. Severity (fail < risk < nit).
  3. 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:

ConditionNext destination
any finding has category = stuck-detected OR loop.maxRounds reachedstuck
any finding routes to askuseraskuser
any finding routes to plan-checkerplan-checker
any finding routes to researcherresearcher
any finding routes to executorexecutor
no findingscommit

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):

  1. evaluateLoop returns { next_action: 'stuck', stuck: true }.
  2. The orchestrator writes a stuck marker to STATE.md with the last Critic findings appended.
  3. np:dashboard shows the marker with a red badge.
  4. askuser prompts the operator: continue with manual override, escalate to a human developer, or np:reset-slice and 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.