Skip to content

Nubosloop

The Nubosloop is the per-task runtime inside np:execute-phase. Every task runs through it: Executor builds, mechanical checks run, the Critic reviews, the routing engine decides the next step, and the loop iterates until zero findings (commit) or loop.maxRounds (stuck, escalate).

ADR-0010 ratifies the design (last amended 2026-05-05, Single-Critic Revision plus Cost Layer L5/L6). The runtime lives in lib/nubosloop.cjs.

The 6 steps

        ┌──────────────────────────────────────────────────┐
        │  Pre-flight: matchExistingLearning (cache hit?)  │
        └──────────────────────────────────────────────────┘

                ┌─────────┴──────────┐
                │ hit                │ miss
                ▼                    ▼
       Render [CACHED]      Researcher-Schwarm (k=3)
       to RESEARCH.md       merge: Mehrheit/Union/Schnittmenge
                │                    │
                └─────────┬──────────┘

                    Executor (round 1)
                    or Build-Fixer (round 2+)


              Mechanical Checks
              (verify, phpstan, pint, tsc,
              tool-use audit)

                  ┌───────┴────────┐
                  │ red            │ green
                  │                ▼
                  │       np-critic (sonnet)
                  │       — three-axis audit
                  │       — writes full JSON to <report_path>
                  │       — emits {verdict, blockers_count, …} envelope
                  │                │
                  │      mergeCriticOutputs (file → array)
                  │                │
                  │       routeFindings → next destination
                  │                │
                  │     ┌──────────┼─────────────────┐
                  │     │          │                 │
                  │     ▼          ▼                 ▼
                  └─→ Executor  Researcher-Schwarm  askuser/plan-checker/stuck

                                round < maxRounds: loop
                                round = maxRounds:  stuck → escalate
                                no findings:        commit + auto-log-learning

Step 1 — Pre-flight

lib/knowledge-adapter.cjs::match against .nubos-pilot/knowledge/learnings.json. Threshold default 0.9 Jaccard similarity, minOccurrence default 3.

When memory.enabled = true (ADR-0014), the same adapter additionally queries .nubos-pilot/memory/ and merges the BM25 hits with vector hits via α·BM25 + (1−α)·vector (default α = 0.6). The combined score replaces BM25-only; threshold semantics are unchanged. See Vector-Memory.

A hit short-circuits the swarm: the cached pattern is rendered as RESEARCH.md with provenance [CACHED], and Step 2 is skipped. Token cost is roughly zero.

Step 2 — Researcher-Schwarm (on demand)

Spawn swarm.research.k=3 (default) np-researcher agents in parallel. Each receives the same Ticket / CONTEXT / Stack input plus a seed_delta field that nudges its prompt without disclosing the swarm.

lib/researcher-swarm.cjs::mergeConsensus merges the outputs:

  • Decisions: Mehrheit (⌈k/2⌉ agreements) → consensus, else FLAGGED.
  • Risks: Union, deduplicated by semantic fingerprint, severity = max.
  • Patterns: Schnittmenge (≥ 2 spawns) → accepted; solo → demoted to [ASSUMED].
  • Open Questions / Sources: Union, credibility = max.

The merged output enters the Executor's prompt with a <consensus_meta> block. Layer-C requires exactly swarm.research.k np-researcher audits per round (k-of-k gate).

Step 3 — Executor or Build-Fixer

Round 1: np-executor (sonnet) writes code in scope (files_modified from the task plan).

Round 2+: np-build-fixer (sonnet) takes the prior Critic findings + verify output and patches in scope.

Step 4 — Mechanical Checks

The orchestrator runs the checks; lib/nubosloop.cjs only RECEIVES the results via CLI input:

  1. Task verify — orchestrator runs the task's verify command from frontmatter, then signals via loop-run-round --phase post-executor --verify-exit-code <int>.
  2. Stack-specific lintersphpstan, pint (PHP); tsc, eslint, prettier (TS); node:test (JS); etc.
  3. Tool-use audit (Rule 9) — for each spawn, the orchestrator forwards its tool-use log via loop-audit-tool-use --agent <name> --tool-use-log <json>. auditToolUse checks for at least one of SEARCH_TOOLS (search-knowledge, match-existing-learning, …) and persists a violation entry stamped with the current round.

The audit log is converted to findings at the next post-critics phase: auditFindingsForRound(taskId, round, cwd) reads the round's audit entries (with carry-forward for unrouted prior-round violations) and emits one rule-9-violation finding per violating spawn (severity=fail, route=executor). The result joins the merged Critic output before routing.

Red verify or any rule-9-violation routes the next round to the executor with the failure output appended.

Step 5 — Critic (Single-Critic Revision, ADR-0010 §2026-05-05)

One np-critic (sonnet) spawns and audits all three orthogonal axes — style, tests, acceptance — in a single structured findings JSON. The agent loads three audit-surface modules during its read step; module files are not spawnable agents:

Module fileTier metadataAxis
np-critic-stylehaikunaming, conventions, dead code, dangling imports
np-critic-testssonnetcoverage, edge cases, assertion quality
np-critic-acceptancesonnetsuccess_criteria satisfied with evidence

Verdict-Only Contract (Cost Layer L5). The critic writes its full findings JSON to a <report_path> the orchestrator hands it (typically ${TMPDIR}/nubos-pilot/critic-reports/critic-<task>-r<round>.json) and emits a small envelope as its final message:

json
{ "critic": "critic", "task_id": "M001-S001-T0001", "round": 1,
  "verdict": "passed | issues_found", "blockers_count": 0,
  "report_path": "...", "run_id": "..." }

The full findings/criteria payload never crosses into parent context. loop-run-round --phase post-critics --critic-outputs-path <file> reads the on-disk JSON directly and feeds it through mergeCriticOutputs. This cuts per-round token cost on the critic axis by roughly 95% without losing information. Routing semantics (five-field contract, fingerprint dedup, auto-promotion of criteria) are unchanged.

mergeCriticOutputs then folds in auditFindingsForRound for the round (Rule 9 audit injection — see Findings Routing § Rule 9 audit injection) and routeFindings dispatches per the routing table.

Step 6 — Route or Commit

lib/nubosloop.cjs::routeFindings maps findings to next destinations. See Findings Routing.

  • Zero findings → atomic commit + auto-log-learning + Messaging sweep.
  • loop.maxRounds reached → stuck, four-option askuser dialog (+5 Runden / replan / stuck / manual fix), STATE.md marker.
  • next_action=plan-checker → three-option askuser dialog (replan / stuck / manual-fix); see ADR-0010 Failure Mode.
  • Otherwise → spawn the routed destination, increment round (when route ∈ {executor, researcher, askuser}), return to Step 3.

Layer-B precondition extension (ADR-0015). _runCommit refuses with loop-commit-precondition-missing (details.missing = "pending-replies-cleared") while any expects_reply: true message in the current task is unarchived. The Critic and Executor / Build-Fixer use the addressed-messaging surface (Named-Agent-Messaging) for per-finding dialogue. An unanswered question keeps the loop alive instead of letting the commit through. --force-commit-phase bypasses the gate, same shape as the existing Layer-A/B overrides.

Phase-completion hook (commit-phase, always-on). After Layer-A/B/C pass and autoLogLearning runs, _runCommit performs a Messaging sweep (ADR-0015): messaging.sweepTaskOnCommit(taskId) moves every message with the matching phase from inbox/ and archive/ into archive/by-task/<taskId>/ and emits a task-swept event in manifest.jsonl. Future tasks see clean inboxes; the per-task audit-trail stays accessible.

The Vector-Memory index is not written at commit — it is a derived cache rebuilt lazily at Pre-flight from the learnings store (ADR-0014; see Vector-Memory).

The _runCommit response:

json
{
  "phase": "commit",
  "next_action": "commit-task",
  "learning_logged": <result | null>,
  "learning_skip_reason": <reason | null>,
  "messages_swept": <count>,
  "forced": <bool>
}

Configuration

.nubos-pilot/config.json:

json
{
  "loop": { "maxRounds": 3 },
  "swarm": {
    "research": { "k": 3, "threshold": 0.9, "minOccurrence": 3 },
    "critic":   { "style_tier": "haiku", "tests_tier": "sonnet", "acceptance_tier": "sonnet" },
    "knowledge_adapter": "local"
  },
  "spawn": {
    "headless": {
      "enabled": false,
      "agents": ["np-critic", "np-researcher"],
      "timeout_ms": 600000,
      "fallback_on_error": true
    }
  },
  "auto_log_learning": true
}

loop.maxRounds defaults to 3 and is clamped to the range [1, 100] by resolveLoopOpts in lib/nubosloop.cjs.

swarm.critic ships as a per-axis tier triple — style_tier (default haiku), tests_tier (default sonnet), acceptance_tier (default sonnet), verified in lib/config-defaults.cjs. The np-critic agent itself resolves from its frontmatter tier (sonnet); resolve-model accepts an optional swarm.critic.tier override for the agent and reads the three *_tier keys as overrides for the corresponding audit-surface modules.

spawn.headless (Cost Layer L6, opt-in) routes critic and researcher spawns through a claude -p subprocess for true parent-context detach. See ADR-0010 §L6 and Configuration → Spawn.

knowledge_adapter selects how cache lookups are routed; "local" (BM25 over .nubos-pilot/knowledge/learnings.json) is the only adapter shipped.

Stuck escalation

Hitting loop.maxRounds is a first-class state, not a silent downgrade. The doctrine treats it as "this task may be mis-planned, discuss with the user" rather than "give up". The orchestrator's stuck handler calls askuser with four options:

  1. Weitermachen (+5 Runden) — Loop-Cap +5, persisted as nubosloop.max_rounds_override so /np:resume-work survives a crash with the operator's decision.
  2. Task neu planen — flag plan-bug, run plan-checker, restart task.
  3. Task als stuck markieren — STATE.md marker, abort wave.
  4. Manuell fixen, dann resumen — pause, operator edits, re-invoke.

np:dashboard surfaces the marker prominently with a red badge.

Auto-log-learning

On commit, lib/nubosloop.cjs::autoLogLearning persists the Researcher-Schwarm consensus + the Executor's final diff to .nubos-pilot/knowledge/learnings.json. Future similar tasks bypass the swarm at Step 1.

_runCommit skips the auto-log when (a) cache_hit: true (the cached pattern is already in the store), (b) --learning-pattern is a sentinel placeholder (<...>), or (c) --learning-pattern is empty.

Set auto_log_learning: false in config to disable.

Worked trace

A task with one round-1 style violation that fixes in round 2:

RoundActionOutcome
1Executor writes draftVerify green
1np-critic writes report file, emits envelope {verdict: issues_found, blockers_count: 1}1 finding: style: todo-marker @ src/foo.php:42
1Routingexecutor (style → executor)
2Build-Fixer removes TODOVerify green
2np-critic envelope {verdict: passed, blockers_count: 0}0 findings
2Routingcommit
Atomic committask(M001-S001-T0001): …
auto-log-learning"remove TODO marker before commit" → learnings.json

A task that gets stuck:

RoundActionOutcome
1, 2, 3Executor → Critic loopSame finding persists across all three rounds
3 (cap)evaluateLoopstuckSTATE.md marker, dashboard badge, four-option askuser

Driving the loop from the CLI

loop-run-round is the agent-native state-machine driver. Every non-LLM transition lives in this verb; LLM spawns (researcher, executor, critic) stay external. A non-LLM runtime drives one round with a handful of shell-outs:

bash
# 1. Pre-flight cache lookup (advances round counter on first call)
node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" --phase preflight --query "$TASK_QUERY"

# 2. Spawn researcher-swarm (if no cache hit) — extern.
#    Stamp k audit entries:
node .nubos-pilot/bin/np-tools.cjs loop-audit-tool-use "$TASK_ID" --agent np-researcher --tool-use-log '[…]'  # × k
node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" --phase post-researcher

# 3. Spawn executor — extern.
#    Signal verify result + executor audit:
node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" --phase post-executor \
  --verify-exit-code "$VERIFY_EXIT" --verify-output-path "$VERIFY_LOG"
node .nubos-pilot/bin/np-tools.cjs loop-audit-tool-use "$TASK_ID" \
  --agent np-executor --tool-use-log "$TOOL_USE_JSON"

# 4. Spawn critic — extern.
#    Critic writes full JSON to $CRITIC_REPORT_PATH; final-message envelope is small.
#    Stamp critic audit:
node .nubos-pilot/bin/np-tools.cjs loop-audit-tool-use "$TASK_ID" --agent np-critic --tool-use-log '[]'

# 5. Feed critic outputs through routing — Verdict-Only path:
node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" --phase post-critics \
  --critic-outputs-path "$CRITIC_REPORT_PATH"

# Legacy inline fallback (still accepted):
#   --critic-outputs "$CRITIC_JSON"
# Both at once → loop-run-round-post-critics-conflicting-outputs.

# 6. Commit OR stuck OR back to step 3 (depending on next_action)
node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" --phase commit \
  --learning-pattern "$CONSENSUS_PATTERN" --learning-outcome verified

For low-level state inspection / ad-hoc updates, the granular primitives are still available: loop-state-read, loop-state-record, loop-evaluate, loop-preflight, loop-stuck, loop-metrics. See CLI Commands.