nubos-pilot 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { NubosPilotError } = require('../../lib/core.cjs');
7
+ const swarm = require('../../lib/researcher-swarm.cjs');
8
+
9
+ function _parseArgs(args) {
10
+ const out = { inputs: null, output: null, heading: null };
11
+ for (let i = 0; i < args.length; i++) {
12
+ const a = args[i];
13
+ if (a === '--inputs') { out.inputs = args[++i] || null; continue; }
14
+ if (a === '--output') { out.output = args[++i] || null; continue; }
15
+ if (a === '--heading') { out.heading = args[++i] || null; continue; }
16
+ }
17
+ return out;
18
+ }
19
+
20
+ function _readSpawnOutput(filePath) {
21
+ let raw;
22
+ try {
23
+ raw = fs.readFileSync(filePath, 'utf-8');
24
+ } catch (err) {
25
+ throw new NubosPilotError(
26
+ 'research-merge-input-missing',
27
+ 'Cannot read spawn output file: ' + filePath + ' — ' + err.message,
28
+ { path: filePath },
29
+ );
30
+ }
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(raw);
34
+ } catch (err) {
35
+ throw new NubosPilotError(
36
+ 'research-merge-input-invalid-json',
37
+ 'Spawn output is not valid JSON: ' + filePath + ' — ' + err.message,
38
+ { path: filePath },
39
+ );
40
+ }
41
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
42
+ throw new NubosPilotError(
43
+ 'research-merge-input-shape',
44
+ 'Spawn output must be a JSON object: ' + filePath,
45
+ { path: filePath },
46
+ );
47
+ }
48
+ return parsed;
49
+ }
50
+
51
+ function run(args, opts) {
52
+ const o = opts || {};
53
+ const cwd = o.cwd || process.cwd();
54
+ const stdout = o.stdout || process.stdout;
55
+ const parsed = _parseArgs(Array.isArray(args) ? args : []);
56
+
57
+ if (!parsed.inputs) {
58
+ throw new NubosPilotError(
59
+ 'research-merge-missing-inputs',
60
+ 'research-merge requires --inputs <comma-separated JSON paths>',
61
+ { args },
62
+ );
63
+ }
64
+ if (!parsed.output) {
65
+ throw new NubosPilotError(
66
+ 'research-merge-missing-output',
67
+ 'research-merge requires --output <RESEARCH.md path>',
68
+ { args },
69
+ );
70
+ }
71
+
72
+ const inputPaths = parsed.inputs.split(',')
73
+ .map((s) => s.trim())
74
+ .filter(Boolean)
75
+ .map((p) => (path.isAbsolute(p) ? p : path.resolve(cwd, p)));
76
+ if (!inputPaths.length) {
77
+ throw new NubosPilotError(
78
+ 'research-merge-empty-inputs',
79
+ 'research-merge --inputs resolved to zero paths',
80
+ { raw: parsed.inputs },
81
+ );
82
+ }
83
+
84
+ const spawnOutputs = inputPaths.map(_readSpawnOutput);
85
+ const consensus = swarm.mergeConsensus(spawnOutputs);
86
+ const md = swarm.renderConsensusToMarkdown(
87
+ consensus,
88
+ parsed.heading ? { heading: parsed.heading } : undefined,
89
+ );
90
+
91
+ const outputPath = path.isAbsolute(parsed.output)
92
+ ? parsed.output
93
+ : path.resolve(cwd, parsed.output);
94
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
95
+ fs.writeFileSync(outputPath, md, 'utf-8');
96
+
97
+ stdout.write(JSON.stringify({
98
+ output_path: outputPath,
99
+ inputs: inputPaths,
100
+ meta: consensus.meta,
101
+ }) + '\n');
102
+ return 0;
103
+ }
104
+
105
+ module.exports = { run, _parseArgs };
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ const { test, afterEach } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+
8
+ const { makeSandbox, cleanupAll } = require('../../tests/helpers/fixture.cjs');
9
+ const subcmd = require('./research-merge.cjs');
10
+
11
+ afterEach(cleanupAll);
12
+
13
+ function _capture() {
14
+ let buf = '';
15
+ const stub = { write: (s) => { buf += s; return true; } };
16
+ return { stub, get: () => buf };
17
+ }
18
+
19
+ function _writeSpawn(sandbox, name, payload) {
20
+ const dir = path.join(sandbox, '.nubos-pilot', '.tmp-swarm');
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ const target = path.join(dir, name);
23
+ fs.writeFileSync(target, JSON.stringify(payload), 'utf-8');
24
+ return target;
25
+ }
26
+
27
+ test('RM-1: merges 3 spawn JSONs into RESEARCH.md and emits meta', () => {
28
+ const sandbox = makeSandbox();
29
+ const a = _writeSpawn(sandbox, 'spawn-0.json', {
30
+ decisions: [{ claim: 'Use jose@6.0.10', confidence: 'HIGH', provenance: '[VERIFIED]' }],
31
+ risks: [{ description: 'Token expiry not validated', severity: 'HIGH' }],
32
+ patterns: [{ name: 'JWT verify wrapper' }],
33
+ open_questions: ['Refresh token rotation policy?'],
34
+ sources: [{ url: 'https://example.com/jose', credibility: 'HIGH' }],
35
+ });
36
+ const b = _writeSpawn(sandbox, 'spawn-1.json', {
37
+ decisions: [{ claim: 'Use jose@6.0.10', confidence: 'HIGH', provenance: '[VERIFIED]' }],
38
+ patterns: [{ name: 'JWT verify wrapper' }],
39
+ open_questions: ['Refresh token rotation policy?'],
40
+ });
41
+ const c = _writeSpawn(sandbox, 'spawn-2.json', {
42
+ decisions: [{ claim: 'Use jose@6.0.10', confidence: 'HIGH', provenance: '[VERIFIED]' }],
43
+ risks: [{ description: 'Token expiry not validated', severity: 'HIGH' }],
44
+ });
45
+
46
+ const out = path.join(sandbox, '.nubos-pilot', 'milestones', 'M001', 'RESEARCH.md');
47
+ const cap = _capture();
48
+ const rc = subcmd.run([
49
+ '--inputs', [a, b, c].join(','),
50
+ '--output', out,
51
+ ], { cwd: sandbox, stdout: cap.stub });
52
+
53
+ assert.equal(rc, 0);
54
+ const payload = JSON.parse(cap.get().trim());
55
+ assert.equal(payload.output_path, out);
56
+ assert.equal(payload.meta.k, 3);
57
+ assert.equal(payload.meta.flagged_count, 0);
58
+ assert.ok(fs.existsSync(out));
59
+ const md = fs.readFileSync(out, 'utf-8');
60
+ assert.match(md, /# Researcher-Schwarm Consensus/);
61
+ assert.match(md, /<consensus_meta>/);
62
+ assert.match(md, /Use jose@6\.0\.10/);
63
+ });
64
+
65
+ test('RM-2: --heading overrides default consensus heading', () => {
66
+ const sandbox = makeSandbox();
67
+ const a = _writeSpawn(sandbox, 's.json', { decisions: [{ claim: 'X' }] });
68
+ const out = path.join(sandbox, 'R.md');
69
+ const cap = _capture();
70
+ subcmd.run([
71
+ '--inputs', a,
72
+ '--output', out,
73
+ '--heading', 'M001 Phase 5 Research',
74
+ ], { cwd: sandbox, stdout: cap.stub });
75
+ const md = fs.readFileSync(out, 'utf-8');
76
+ assert.match(md, /^# M001 Phase 5 Research/);
77
+ });
78
+
79
+ test('RM-3: missing --inputs throws structured error', () => {
80
+ const sandbox = makeSandbox();
81
+ const cap = _capture();
82
+ assert.throws(
83
+ () => subcmd.run(['--output', 'R.md'], { cwd: sandbox, stdout: cap.stub }),
84
+ (err) => err && err.code === 'research-merge-missing-inputs',
85
+ );
86
+ });
87
+
88
+ test('RM-4: missing --output throws structured error', () => {
89
+ const sandbox = makeSandbox();
90
+ const cap = _capture();
91
+ assert.throws(
92
+ () => subcmd.run(['--inputs', 'a.json'], { cwd: sandbox, stdout: cap.stub }),
93
+ (err) => err && err.code === 'research-merge-missing-output',
94
+ );
95
+ });
96
+
97
+ test('RM-5: missing input file throws structured error', () => {
98
+ const sandbox = makeSandbox();
99
+ const cap = _capture();
100
+ assert.throws(
101
+ () => subcmd.run([
102
+ '--inputs', path.join(sandbox, 'nope.json'),
103
+ '--output', path.join(sandbox, 'R.md'),
104
+ ], { cwd: sandbox, stdout: cap.stub }),
105
+ (err) => err && err.code === 'research-merge-input-missing',
106
+ );
107
+ });
108
+
109
+ test('RM-6: invalid JSON input throws structured error', () => {
110
+ const sandbox = makeSandbox();
111
+ const bad = path.join(sandbox, 'bad.json');
112
+ fs.writeFileSync(bad, '{not-json', 'utf-8');
113
+ const cap = _capture();
114
+ assert.throws(
115
+ () => subcmd.run([
116
+ '--inputs', bad,
117
+ '--output', path.join(sandbox, 'R.md'),
118
+ ], { cwd: sandbox, stdout: cap.stub }),
119
+ (err) => err && err.code === 'research-merge-input-invalid-json',
120
+ );
121
+ });
122
+
123
+ test('RM-7: array-shaped JSON input throws structured error', () => {
124
+ const sandbox = makeSandbox();
125
+ const bad = path.join(sandbox, 'arr.json');
126
+ fs.writeFileSync(bad, '[]', 'utf-8');
127
+ const cap = _capture();
128
+ assert.throws(
129
+ () => subcmd.run([
130
+ '--inputs', bad,
131
+ '--output', path.join(sandbox, 'R.md'),
132
+ ], { cwd: sandbox, stdout: cap.stub }),
133
+ (err) => err && err.code === 'research-merge-input-shape',
134
+ );
135
+ });
136
+
137
+ test('RM-8: relative paths resolve against cwd', () => {
138
+ const sandbox = makeSandbox();
139
+ const a = _writeSpawn(sandbox, 'rel.json', { decisions: [{ claim: 'Y' }] });
140
+ const rel = path.relative(sandbox, a);
141
+ const cap = _capture();
142
+ subcmd.run([
143
+ '--inputs', rel,
144
+ '--output', 'out.md',
145
+ ], { cwd: sandbox, stdout: cap.stub });
146
+ const payload = JSON.parse(cap.get().trim());
147
+ assert.equal(payload.output_path, path.join(sandbox, 'out.md'));
148
+ assert.ok(fs.existsSync(path.join(sandbox, 'out.md')));
149
+ });
150
+
151
+ test('RM-9: flagged decision (only 1 of 3 spawns) is marked, not accepted', () => {
152
+ const sandbox = makeSandbox();
153
+ const a = _writeSpawn(sandbox, '0.json', { decisions: [{ claim: 'Solo claim' }] });
154
+ const b = _writeSpawn(sandbox, '1.json', { decisions: [{ claim: 'Other' }] });
155
+ const c = _writeSpawn(sandbox, '2.json', { decisions: [{ claim: 'Other' }] });
156
+ const out = path.join(sandbox, 'R.md');
157
+ const cap = _capture();
158
+ subcmd.run(['--inputs', [a, b, c].join(','), '--output', out],
159
+ { cwd: sandbox, stdout: cap.stub });
160
+ const payload = JSON.parse(cap.get().trim());
161
+ assert.equal(payload.meta.k, 3);
162
+ assert.equal(payload.meta.flagged_count, 1);
163
+ const md = fs.readFileSync(out, 'utf-8');
164
+ assert.match(md, /## Flagged Decisions \(no majority\)/);
165
+ assert.match(md, /Solo claim/);
166
+ });
@@ -6,10 +6,10 @@ const os = require('node:os');
6
6
 
7
7
  const { atomicWriteFileSync, NubosPilotError } = require('../core.cjs');
8
8
 
9
- const STATUSLINE_REL = '.claude/nubos-pilot/hooks/np-statusline.js';
10
- const CTX_MONITOR_REL = '.claude/nubos-pilot/hooks/np-ctx-monitor.js';
11
- const NP_STATUSLINE_MARKER = 'np-statusline.js';
12
- const NP_CTX_MONITOR_MARKER = 'np-ctx-monitor.js';
9
+ const STATUSLINE_REL = '.claude/nubos-pilot/hooks/np-statusline.cjs';
10
+ const CTX_MONITOR_REL = '.claude/nubos-pilot/hooks/np-ctx-monitor.cjs';
11
+ const NP_STATUSLINE_MARKER = 'np-statusline.';
12
+ const NP_CTX_MONITOR_MARKER = 'np-ctx-monitor.';
13
13
 
14
14
  function _settingsPath(scope, projectRoot) {
15
15
  if (scope === 'global') return path.join(os.homedir(), '.claude', 'settings.json');
@@ -12,8 +12,8 @@ function _mkSandbox() {
12
12
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-claude-hooks-'));
13
13
  fs.mkdirSync(path.join(dir, '.claude'), { recursive: true });
14
14
  fs.mkdirSync(path.join(dir, '.claude', 'nubos-pilot', 'hooks'), { recursive: true });
15
- fs.writeFileSync(path.join(dir, '.claude', 'nubos-pilot', 'hooks', 'np-statusline.js'), '// stub\n');
16
- fs.writeFileSync(path.join(dir, '.claude', 'nubos-pilot', 'hooks', 'np-ctx-monitor.js'), '// stub\n');
15
+ fs.writeFileSync(path.join(dir, '.claude', 'nubos-pilot', 'hooks', 'np-statusline.cjs'), '// stub\n');
16
+ fs.writeFileSync(path.join(dir, '.claude', 'nubos-pilot', 'hooks', 'np-ctx-monitor.cjs'), '// stub\n');
17
17
  return dir;
18
18
  }
19
19
 
@@ -26,10 +26,10 @@ test('claude-hooks: fresh install writes both hooks to local settings', () => {
26
26
  assert.equal(res.results.ctxMonitor.action, 'installed');
27
27
  const settings = JSON.parse(fs.readFileSync(res.path, 'utf-8'));
28
28
  assert.equal(settings.statusLine.type, 'command');
29
- assert.ok(settings.statusLine.command.includes('np-statusline.js'));
29
+ assert.ok(settings.statusLine.command.includes('np-statusline.cjs'));
30
30
  assert.ok(Array.isArray(settings.hooks.PostToolUse));
31
31
  assert.equal(settings.hooks.PostToolUse[0].matcher, '.*');
32
- assert.ok(settings.hooks.PostToolUse[0].hooks[0].command.includes('np-ctx-monitor.js'));
32
+ assert.ok(settings.hooks.PostToolUse[0].hooks[0].command.includes('np-ctx-monitor.cjs'));
33
33
  } finally {
34
34
  fs.rmSync(dir, { recursive: true, force: true });
35
35
  }
@@ -62,7 +62,7 @@ test('claude-hooks: --force overwrites foreign statusLine', () => {
62
62
  const res = mod.installClaudeHooks({ projectRoot: dir, scope: 'local', force: true });
63
63
  assert.equal(res.results.statusline.action, 'overwrote');
64
64
  const settings = JSON.parse(fs.readFileSync(res.path, 'utf-8'));
65
- assert.ok(settings.statusLine.command.includes('np-statusline.js'));
65
+ assert.ok(settings.statusLine.command.includes('np-statusline.cjs'));
66
66
  } finally {
67
67
  fs.rmSync(dir, { recursive: true, force: true });
68
68
  }
@@ -98,7 +98,7 @@ test('claude-hooks: preserves unrelated PostToolUse hooks', () => {
98
98
  const settings = JSON.parse(fs.readFileSync(res.path, 'utf-8'));
99
99
  assert.equal(settings.hooks.PostToolUse.length, 2);
100
100
  assert.equal(settings.hooks.PostToolUse[0].matcher, 'Bash');
101
- assert.ok(settings.hooks.PostToolUse[1].hooks[0].command.includes('np-ctx-monitor.js'));
101
+ assert.ok(settings.hooks.PostToolUse[1].hooks[0].command.includes('np-ctx-monitor.cjs'));
102
102
  } finally {
103
103
  fs.rmSync(dir, { recursive: true, force: true });
104
104
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "AI-driven planning and execution tool for code projects",
5
5
  "homepage": "https://github.com/Nubos-AI/nubos-pilot",
6
6
  "repository": {
@@ -102,99 +102,52 @@ if [ "$TOTAL_TASKS" = "0" ]; then
102
102
  fi
103
103
  ```
104
104
 
105
- ## Nubosloop (per task)
105
+ ## Execution per-task Nubosloop, slices serial
106
106
 
107
- Every task runs through the Nubosloop ([ADR-0010](../docs/adr/0010-nubosloop.md), `lib/nubosloop.cjs`). The loop terminates only on (a) zero Critic-Schwarm findings followed by an atomic commit, or (b) the orchestrator-enforced `loop.maxRounds` cap (default `3`), in which case the task transitions to `stuck` and the orchestrator escalates via `askuser`.
107
+ Every task runs through the **Nubosloop** ([ADR-0010](../docs/adr/0010-nubosloop.md), `lib/nubosloop.cjs`) — pre-flight cache lookup → researcher-schwarm (on miss) → executor or build-fixer → mechanical checks + tool-use audit → critic-schwarm → route. The loop terminates only on (a) `loop-evaluate.next_action == "commit"` (zero blocking findings) followed by `commit-task` (atomic commit per ADR-0004), or (b) `loop.maxRounds` cap (default `3`) reached `loop-run-round --phase stuck` writes the marker, dashboard surfaces it, orchestrator escalates via `askuser`. Single-pass `executor → commit-task` is forbidden — the loop is the only sanctioned path.
108
108
 
109
- **Agent-native driver:** the per-task state machine is exposed through `node .nubos-pilot/bin/np-tools.cjs loop-run-round <task-id> --phase <phase>`. Every non-LLM transition lives in this verb; LLM spawns (researcher, executor, critics) remain extern and feed their results back as `--query` / `--verify-exit-code` / `--critic-outputs` arguments. A non-LLM runtime can drive the loop with five shell-outs per round.
109
+ **Wave shape (slices serial, tasks parallel within a slice):**
110
110
 
111
- **Per task, per round:**
112
-
113
- 1. **Pre-flight** agent-native CLI: `node .nubos-pilot/bin/np-tools.cjs loop-preflight --query "$TASK_QUERY" --threshold $THRESHOLD --min-occurrence $MIN_OCC`. Output is `{hit, bypass_swarm, cache_miss_reason}`. A hit at similarity ≥ `swarm.research.threshold` and `occurrence ≥ swarm.research.minOccurrence` short-circuits the Researcher-Schwarm; the cached pattern enters the Executor's prompt with provenance `[CACHED]`. Soft cache failures (mcp-not-implemented, adapter-unknown) downgrade to a miss with `cache_miss_reason` populated; hard failures (corrupt store, version mismatch) propagate.
114
- 2. **Researcher-Schwarm (on demand)** — when no cached pattern exists, the orchestrator spawns `swarm.research.k=3` independent `np-researcher` agents in parallel and merges their outputs through `lib/researcher-swarm.cjs::mergeConsensus` (Mehrheit / Union / Schnittmenge). The merged consensus enters the Executor's prompt.
115
- 3. **Executor (or Build-Fixer on Round ≥ 2)** — single `np-executor` spawn writes code in scope. Round 2+ uses `np-build-fixer` with the prior Critic findings + verify output appended to its prompt.
116
- 4. **Mechanical Checks** — the orchestrator (NOT the agent) runs the task's `verify` command, plus stack-specific linters (`phpstan`, `pint`, `tsc`, `eslint`), plus a tool-use audit confirming the agent invoked `search-knowledge` or `match-existing-learning` at least once. Red ⇒ findings route back to Step 3.
117
- 5. **Critic-Schwarm** — three Critic agents spawn in parallel (`agents/np-critic-style.md` haiku, `agents/np-critic-tests.md` sonnet, `agents/np-critic-acceptance.md` sonnet). Each emits structured findings JSON.
118
- 6. **Route + Loop or Commit** — the orchestrator merges + decides next-action via the agent-native CLI:
119
-
120
- ```bash
121
- ROUND=$(node .nubos-pilot/bin/np-tools.cjs loop-state-read "$TASK_ID" | node -e 'process.stdin.on("data",d=>{const s=JSON.parse(d);console.log((s&&s.round)||1)})')
122
- EVAL=$(node .nubos-pilot/bin/np-tools.cjs loop-evaluate \
123
- --round "$ROUND" --max-rounds "$LOOP_MAX_ROUNDS" \
124
- --json "$CRITIC_OUTPUTS_JSON")
125
- NEXT=$(echo "$EVAL" | node -e 'process.stdin.on("data",d=>console.log(JSON.parse(d).next_action))')
126
- ```
127
-
128
- `next_action` ∈ `{commit, executor, researcher, askuser, plan-checker, stuck}`. Routing rules:
129
- - `executor` — Style / Bug / Test / Acceptance findings → spawn `np-build-fixer` on Round ≥ 2.
130
- - `researcher` — `information-missing` findings → re-run Researcher-Schwarm with the gap as input.
131
- - `askuser` — `question-to-user` findings → block on user reply.
132
- - `plan-checker` — `locked-decision-violation` → orchestrator escalation.
133
- - `commit` — zero findings → atomic commit per ADR-0004 + auto-`learning-log`.
134
- - `stuck` — `loop.maxRounds` reached → `loop-stuck $TASK_ID --reason ... --findings ...`.
135
-
136
- **End-to-end round (single CLI surface):**
137
-
138
- ```bash
139
- # Step 1 — preflight cache lookup (advances round counter, stamps cache_hit)
140
- node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" --phase preflight \
141
- --query "$TASK_QUERY"
142
-
143
- # Step 2 — LLM spawns (researcher swarm if no cache hit, then executor) run extern.
144
-
145
- # Step 3 — after executor commits draft, signal verify result
146
- node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" --phase post-executor \
147
- --verify-exit-code "$VERIFY_EXIT" --verify-output-path "$VERIFY_LOG"
111
+ 1. Dispatch **all tasks in the slice in parallel** — each task is one independent Nubosloop instance.
112
+ 2. Wait until every task in the slice committed OR is `stuck` OR hit `plan-checker`.
113
+ 3. If any task is `stuck` or hit `plan-checker` stop the wave and exit non-zero. Previously committed tasks remain committed.
114
+ 4. Move to the next slice.
148
115
 
149
- # Step 4 LLM spawns (critic schwarm) run extern.
116
+ **Per-task driver (single agent-native CLI surface):** `node .nubos-pilot/bin/np-tools.cjs loop-run-round <task-id> --phase <preflight|post-executor|post-critics|commit|stuck>`. Every non-LLM transition lives in this verb; LLM spawns (researcher, executor / build-fixer, critics) remain extern and feed their results back via `--query` / `--verify-exit-code` / `--critic-outputs`. A non-LLM runtime can drive the loop with five shell-outs per round.
150
117
 
151
- # Step 5 — feed critic outputs into the routing engine
152
- node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" --phase post-critics \
153
- --critic-outputs "$CRITIC_JSON"
118
+ **Per-task, per-round protocol:**
154
119
 
155
- # Step 6depending on next_action: commit OR stuck OR back to step 2/4
156
- node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" --phase commit \
157
- --learning-pattern "$CONSENSUS_PATTERN" --learning-outcome verified
158
- ```
120
+ 1. **Pre-flight cache lookup** (Round 1 only) `loop-run-round --phase preflight --query "$TASK_QUERY"`. A hit at similarity ≥ `swarm.research.threshold` and `occurrence ≥ swarm.research.minOccurrence` short-circuits the Researcher-Schwarm; the cached pattern enters the Executor prompt with provenance `[CACHED]`. Soft cache failures (mcp-not-implemented, adapter-unknown) downgrade to a miss with `cache_miss_reason` populated; hard failures (corrupt store, version mismatch) propagate.
121
+ 2. **Researcher-Schwarm (on cache miss, or on `next_action=researcher` re-route)** — orchestrator spawns `swarm.research.k=3` independent `np-researcher` agents IN PARALLEL (single message, three Agent blocks) and merges their outputs through `lib/researcher-swarm.cjs::mergeConsensus` (Mehrheit / Union / Schnittmenge). The merged consensus enters the Executor prompt with provenance.
122
+ 3. **Executor (R1) or Build-Fixer (R≥2)** — single LLM spawn. Round 1 spawns `agents/np-executor.md`. Round ≥ 2 spawns `agents/np-build-fixer.md` with prior critic findings + verify output appended. Edits ONLY paths in `files_modified` (D-04 — no scope expansion). Does NOT call `commit-task`.
123
+ 4. **Mechanical Checks (orchestrator, NOT the agent)** — run task's `<verify>` command + stack linters (`phpstan`, `pint`, `tsc`, `eslint`); capture exit code + output to `$VERIFY_LOG`. Then `loop-audit-tool-use --task-id ... --round ...` confirms the spawn invoked `search-knowledge` or `match-existing-learning` ≥ 1× (Rule 9). Audit findings get round-stamped and feed `loop-evaluate` alongside critic findings. Then call `loop-run-round --phase post-executor --verify-exit-code "$VERIFY_EXIT" --verify-output-path "$VERIFY_LOG"`. On verify-red the verb returns `next_action: spawn-build-fixer` — skip critics, advance to next round directly.
124
+ 5. **Critic-Schwarm (verify-green only)** — three Critic agents spawn IN PARALLEL (single message, three Agent blocks): `agents/np-critic-style.md` (haiku), `agents/np-critic-tests.md` (sonnet), `agents/np-critic-acceptance.md` (sonnet). Each emits structured findings JSON.
125
+ 6. **Route** — `loop-run-round --phase post-critics --critic-outputs "$CRITIC_JSON"` returns `next_action ∈ {commit, executor, researcher, askuser, plan-checker, stuck}`:
159
126
 
160
- **Per-round state persistence** (lower-level primitives, available for ad-hoc updates):
127
+ | `next_action` | Trigger | Action |
128
+ |------------------|------------------------------------|-----------------------------------------------------------------|
129
+ | `commit` | Zero blocking findings | `loop-run-round --phase commit` + `commit-task` (atomic) |
130
+ | `executor` | Style/Bug/Test/Acceptance findings | R≥2: spawn `np-build-fixer` with prior findings (next round) |
131
+ | `researcher` | `information-missing` finding | Re-run Researcher-Schwarm with the gap as input (next round) |
132
+ | `askuser` | `question-to-user` finding | Block on user reply via `askuser`; resume same round |
133
+ | `plan-checker` | `locked-decision-violation` | Abort wave; orchestrator escalates |
134
+ | `stuck` | `loop.maxRounds` reached | `loop-run-round --phase stuck` + dashboard + askuser escalation |
161
135
 
162
- ```bash
163
- node .nubos-pilot/bin/np-tools.cjs loop-state-record "$TASK_ID" \
164
- --json "$(printf '{"round":%s,"last_action":"%s","findings":%s}' "$ROUND" "$NEXT" "$FINDINGS_JSON")"
165
- ```
136
+ 7. **Commit** — `loop-run-round --phase commit --learning-pattern "$CONSENSUS_PATTERN" --learning-outcome verified` stamps the checkpoint to `pre-commit` and auto-logs the learning (when `auto_log_learning=true`, default — feeds future Round-1 cache hits). Then `node .nubos-pilot/bin/np-tools.cjs commit-task "$TASK_ID"` performs the atomic commit per ADR-0004.
166
137
 
167
- **Auto-`log-learning`** on commit (when `auto_log_learning=true`, default):
138
+ **Per-task loop control values (read once at wave start):**
168
139
 
169
140
  ```bash
170
- node .nubos-pilot/bin/np-tools.cjs learning-log \
171
- --pattern "$CONSENSUS_PATTERN" --outcome verified \
172
- --task-id "$TASK_ID" --milestone-id "$MILESTONE_ID"
173
- ```
174
-
175
- Future similar tasks hit the cache and bypass the Researcher-Schwarm at Step 1.
176
-
177
- ```bash
178
- # Per-task loop control values (read once from config)
179
141
  LOOP_MAX_ROUNDS=$(node .nubos-pilot/bin/np-tools.cjs config-get loop.maxRounds 2>/dev/null || echo 3)
180
142
  SWARM_K=$(node .nubos-pilot/bin/np-tools.cjs config-get swarm.research.k 2>/dev/null || echo 3)
143
+ SWARM_THRESHOLD=$(node .nubos-pilot/bin/np-tools.cjs config-get swarm.research.threshold 2>/dev/null || echo 0.9)
144
+ SWARM_MIN_OCC=$(node .nubos-pilot/bin/np-tools.cjs config-get swarm.research.minOccurrence 2>/dev/null || echo 3)
181
145
  AUTO_LOG_LEARNING=$(node .nubos-pilot/bin/np-tools.cjs config-get auto_log_learning 2>/dev/null || echo true)
182
146
  ```
183
147
 
184
- On `next_action == "stuck"`, `loop-stuck` writes the `stuck` marker into both the per-task `nubosloop` block and the checkpoint envelope, surfaces it on `np:dashboard`, and the orchestrator prompts the operator via `askuser`: continue with manual override, escalate to a human developer, or `np:reset-slice` and re-plan.
185
-
186
- ## Execution — slices serial, tasks parallel within a slice
187
-
188
- For each wave (slice) in `waves[]`, in order:
189
-
190
- 1. Dispatch **all tasks in the slice in parallel** (one Nubosloop per task — see above).
191
- 2. Wait until every task in the slice is committed OR one failed OR `stuck`.
192
- 3. If any task failed or is `stuck` → stop the wave and exit non-zero. Previous committed tasks remain committed.
193
- 4. Move to the next slice.
148
+ **Wave + per-task pseudocode (this is the executable shape the orchestrator drives this verbatim, not just „shape but not concrete syntax"):**
194
149
 
195
150
  ```bash
196
- # Pseudocode for the per-wave loop. The orchestrator uses its parallel-spawn
197
- # primitive; this pseudocode shows the shape but not the concrete agent syntax.
198
151
  for WAVE_INDEX in 0 1 2 ...; do
199
152
  WAVE=$(echo "$INIT" | node -e "process.stdin.on('data', d => console.log(JSON.stringify(JSON.parse(d).waves[$WAVE_INDEX])))")
200
153
  [ -z "$WAVE" ] || [ "$WAVE" = "undefined" ] && break
@@ -205,10 +158,9 @@ for WAVE_INDEX in 0 1 2 ...; do
205
158
  echo "=== Wave $((WAVE_INDEX+1)): $SLICE_FULL_ID — tasks: $TASK_IDS ===" >&2
206
159
 
207
160
  # Worktree-Isolation (ADR-0008): when workflow.worktree_isolation=true,
208
- # create an isolated git worktree for this slice before spawning executors.
209
- # Executors run inside the worktree (cwd = worktree path), commits land on
210
- # the slice branch np/<slice-full-id>, and the slice is fast-forward merged
211
- # back on success. On failure: worktree stays in place for inspection.
161
+ # create an isolated git worktree for this slice. Nubosloop instances
162
+ # run inside the worktree (cwd = worktree path); commits land on the
163
+ # slice branch np/<slice-full-id>; FF-merged back on success.
212
164
  SLICE_CWD="$PWD"
213
165
  if [ "$WORKTREE_ISOLATION" = "true" ]; then
214
166
  WT_CREATE=$(node .nubos-pilot/bin/np-tools.cjs worktree-create "$SLICE_FULL_ID")
@@ -216,27 +168,120 @@ for WAVE_INDEX in 0 1 2 ...; do
216
168
  echo "[np:execute-phase] worktree created at $SLICE_CWD (branch np/$SLICE_FULL_ID)" >&2
217
169
  fi
218
170
 
219
- # For each task id in TASK_IDS, spawn an executor IN PARALLEL.
220
- # The orchestrator's parallel primitive dispatches all of them in a single
221
- # message (multiple Agent tool use blocks in one send).
171
+ # PARALLEL DISPATCH per task one Nubosloop instance per task.
172
+ # The orchestrator's parallel primitive dispatches each task's loop
173
+ # body in a single message (one Agent block per task per LLM step).
222
174
  for TASK_ID in $TASK_IDS; do
223
- # IN PARALLEL:
224
- node .nubos-pilot/bin/np-tools.cjs checkpoint start "$TASK_ID" --phase "$PHASE" --plan "$SLICE_FULL_ID" --wave "$((WAVE_INDEX+1))"
175
+ # IN PARALLEL across tasks in the slice:
176
+
177
+ node .nubos-pilot/bin/np-tools.cjs checkpoint start "$TASK_ID" \
178
+ --phase "$PHASE" --plan "$SLICE_FULL_ID" --wave "$((WAVE_INDEX+1))"
225
179
 
226
180
  TASK_JSON=$(node .nubos-pilot/bin/np-tools.cjs init execute-milestone execute-task "$PHASE" "$TASK_ID")
227
181
  if [[ "$TASK_JSON" == @file:* ]]; then TASK_JSON=$(cat "${TASK_JSON#@file:}"); fi
182
+ TASK_QUERY=$(echo "$TASK_JSON" | node -e "process.stdin.on('data', d => { const j=JSON.parse(d); console.log(j.query || j.name || ''); })")
228
183
 
229
184
  EXECUTOR_START=$(node .nubos-pilot/bin/np-tools.cjs metrics start-timestamp)
230
- EXECUTOR_MODEL=$(node .nubos-pilot/bin/np-tools.cjs resolve-model np-executor --profile frontier)
185
+ CONSENSUS_PATTERN=""
186
+ NEXT_ACTION=""
187
+ CACHE_HIT="false"
188
+ ROUND=1
189
+
190
+ while [ "$ROUND" -le "$LOOP_MAX_ROUNDS" ]; do
191
+
192
+ # === Step 1: pre-flight cache lookup (Round 1 only) ===
193
+ if [ "$ROUND" -eq 1 ]; then
194
+ PREFLIGHT=$(node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" \
195
+ --phase preflight --query "$TASK_QUERY")
196
+ CACHE_HIT=$(echo "$PREFLIGHT" | node -e 'process.stdin.on("data",d=>console.log(JSON.parse(d).hit||false))')
197
+ fi
231
198
 
232
- # Spawn agents/np-executor.md (tier: sonnet, model resolved as $EXECUTOR_MODEL)
233
- # with a <files_to_read> block containing: the task plan file, the slice
234
- # plan file, prior slice SUMMARY files, milestone CONTEXT.md.
235
- # Executor edits EXACTLY the paths in files_modified (D-04 — no scope
236
- # expansion), runs <verify> commands, then invokes commit-task:
199
+ # === Step 2: Researcher-Schwarm (cache miss on R1, or re-route on R≥2) ===
200
+ # PARALLEL spawn of $SWARM_K agents/np-researcher.md (single message,
201
+ # $SWARM_K Agent blocks). Merge via lib/researcher-swarm.cjs::mergeConsensus.
202
+ # Result is injected into the next executor prompt as $CONSENSUS_PATTERN
203
+ # with provenance ([VERIFIED] on majority + spawn-citation, else [PROVISIONAL]).
204
+ if { [ "$ROUND" -eq 1 ] && [ "$CACHE_HIT" != "true" ]; } || [ "$NEXT_ACTION" = "researcher" ]; then
205
+ CONSENSUS_PATTERN="<merged consensus from $SWARM_K researchers>"
206
+ elif [ "$CACHE_HIT" = "true" ] && [ -z "$CONSENSUS_PATTERN" ]; then
207
+ CONSENSUS_PATTERN="<cached pattern from preflight ([CACHED] provenance)>"
208
+ fi
209
+
210
+ # === Step 3: Executor (R1) or Build-Fixer (R≥2) — LLM spawn extern ===
211
+ if [ "$ROUND" -eq 1 ]; then
212
+ EXECUTOR_AGENT="np-executor"
213
+ else
214
+ EXECUTOR_AGENT="np-build-fixer"
215
+ fi
216
+ EXECUTOR_MODEL=$(node .nubos-pilot/bin/np-tools.cjs resolve-model "$EXECUTOR_AGENT" --profile frontier)
217
+ # Spawn agents/${EXECUTOR_AGENT}.md (model resolved as $EXECUTOR_MODEL) with:
218
+ # - <files_to_read>: task plan, slice plan, prior slice SUMMARYs, CONTEXT.md
219
+ # - $CONSENSUS_PATTERN with provenance
220
+ # - On Round ≥ 2: prior critic findings + verify output excerpt
221
+ # - $LANG_DIRECTIVE + $AGENT_SKILLS_EXECUTOR (skill triggers from §Skills mapping)
222
+ # Agent edits ONLY paths in files_modified (D-04). Does NOT call commit-task.
223
+
224
+ node .nubos-pilot/bin/np-tools.cjs checkpoint transition "$TASK_ID" verifying
225
+
226
+ # === Step 4: Mechanical Checks + tool-use audit (orchestrator-side) ===
227
+ VERIFY_LOG="${TMPDIR:-/tmp}/np-verify-${TASK_ID}-r${ROUND}.log"
228
+ # Orchestrator (NOT the agent) runs the task's <verify> command + stack
229
+ # linters; redirect stdout+stderr to $VERIFY_LOG.
230
+ VERIFY_EXIT=$?
231
+ node .nubos-pilot/bin/np-tools.cjs loop-audit-tool-use "$TASK_ID" \
232
+ --round "$ROUND" --agent "$EXECUTOR_AGENT"
233
+
234
+ POST_EXEC=$(node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" \
235
+ --phase post-executor \
236
+ --verify-exit-code "$VERIFY_EXIT" --verify-output-path "$VERIFY_LOG")
237
+ POST_EXEC_NEXT=$(echo "$POST_EXEC" | node -e 'process.stdin.on("data",d=>console.log(JSON.parse(d).next_action))')
238
+
239
+ # Verify-red short-circuits to build-fixer next round (skip critics).
240
+ if [ "$POST_EXEC_NEXT" = "spawn-build-fixer" ]; then
241
+ ROUND=$((ROUND+1))
242
+ continue
243
+ fi
237
244
 
238
- node .nubos-pilot/bin/np-tools.cjs checkpoint transition "$TASK_ID" verifying
239
- node .nubos-pilot/bin/np-tools.cjs checkpoint transition "$TASK_ID" pre-commit
245
+ # === Step 5: Critic-Schwarm three agents in PARALLEL ===
246
+ # Spawn IN PARALLEL (single message, three Agent blocks):
247
+ # - agents/np-critic-style.md (haiku) → CRITIC_STYLE_JSON
248
+ # - agents/np-critic-tests.md (sonnet) → CRITIC_TESTS_JSON
249
+ # - agents/np-critic-acceptance.md (sonnet) → CRITIC_ACCEPTANCE_JSON
250
+ CRITIC_OUTPUTS_JSON=$(printf '[%s,%s,%s]' "$CRITIC_STYLE_JSON" "$CRITIC_TESTS_JSON" "$CRITIC_ACCEPTANCE_JSON")
251
+
252
+ # === Step 6: Route via loop-evaluate (post-critics) ===
253
+ POST_CRIT=$(node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" \
254
+ --phase post-critics --critic-outputs "$CRITIC_OUTPUTS_JSON")
255
+ NEXT_ACTION=$(echo "$POST_CRIT" | node -e 'process.stdin.on("data",d=>console.log(JSON.parse(d).next_action))')
256
+
257
+ case "$NEXT_ACTION" in
258
+ commit) break ;;
259
+ executor) ROUND=$((ROUND+1)); continue ;;
260
+ researcher) ROUND=$((ROUND+1)); continue ;;
261
+ askuser) # spec from POST_CRIT.routing — block on user reply,
262
+ # then resume the same round (no ROUND increment).
263
+ node .nubos-pilot/bin/np-tools.cjs askuser --json "$ASKUSER_SPEC"
264
+ continue ;;
265
+ plan-checker) echo "[np:execute-phase] $TASK_ID hit locked-decision-violation — see loop-state for details." >&2
266
+ exit 2 ;;
267
+ stuck) node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" \
268
+ --phase stuck --reason "max-rounds" --findings "$CRITIC_OUTPUTS_JSON"
269
+ echo "[np:execute-phase] $TASK_ID stuck after $LOOP_MAX_ROUNDS rounds." >&2
270
+ exit 3 ;;
271
+ esac
272
+ done
273
+
274
+ # Defensive: if the while loop exited without NEXT_ACTION=commit (shouldn't
275
+ # happen — loop-evaluate emits stuck at maxRounds), stamp stuck and bail.
276
+ if [ "$NEXT_ACTION" != "commit" ]; then
277
+ node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" \
278
+ --phase stuck --reason "loop-exited-without-commit"
279
+ exit 3
280
+ fi
281
+
282
+ # === Step 7: atomic commit ===
283
+ node .nubos-pilot/bin/np-tools.cjs loop-run-round "$TASK_ID" --phase commit \
284
+ --learning-pattern "$CONSENSUS_PATTERN" --learning-outcome verified
240
285
  node .nubos-pilot/bin/np-tools.cjs commit-task "$TASK_ID"
241
286
  COMMIT_STATUS=$?
242
287
 
@@ -244,11 +289,11 @@ for WAVE_INDEX in 0 1 2 ...; do
244
289
  EXECUTOR_STATUS=ok
245
290
  [ "$COMMIT_STATUS" -ne 0 ] && EXECUTOR_STATUS=error
246
291
  node .nubos-pilot/bin/np-tools.cjs metrics record \
247
- --agent np-executor --tier sonnet --resolved-model "$EXECUTOR_MODEL" \
292
+ --agent "$EXECUTOR_AGENT" --tier sonnet --resolved-model "$EXECUTOR_MODEL" \
248
293
  --phase "$PHASE" --plan "$SLICE_FULL_ID" --task "$TASK_ID" \
249
294
  --started "$EXECUTOR_START" --ended "$EXECUTOR_END" \
250
295
  --tokens-in "${TOKENS_IN:-0}" --tokens-out "${TOKENS_OUT:-0}" \
251
- --retry-count "${RETRY_COUNT:-0}" --status "$EXECUTOR_STATUS" --runtime "$RUNTIME"
296
+ --retry-count "$((ROUND-1))" --status "$EXECUTOR_STATUS" --runtime "$RUNTIME"
252
297
 
253
298
  if [ "$COMMIT_STATUS" -ne 0 ]; then
254
299
  echo "[np:execute-phase] commit-task failed for $TASK_ID — aborting wave $SLICE_FULL_ID." >&2
@@ -258,7 +303,7 @@ for WAVE_INDEX in 0 1 2 ...; do
258
303
  exit "$COMMIT_STATUS"
259
304
  fi
260
305
  done
261
- # wait for all parallel executors in this wave to finish before next wave
306
+ # Wait for all parallel Nubosloop instances in this wave to finish before next wave.
262
307
 
263
308
  # After every task in the slice committed: aggregate per-task summaries into
264
309
  # the slice-level S<NNN>-SUMMARY.md so /np:validate-phase can audit it.
@@ -294,16 +339,26 @@ After every slice completes, point the operator at `/np:validate-phase $PHASE` t
294
339
 
295
340
  <!-- scope_guardrail -->
296
341
  **Do:**
297
- - Dispatch all tasks in a slice **in parallel** (one executor per task).
298
- - Move to next slice **only after** every task in the current slice is committed.
299
- - Start one checkpoint per task before spawning the executor agent.
300
- - Spawn `agents/np-executor.md` once per task with only that task's `files_modified` in scope.
342
+ - Dispatch all tasks in a slice **in parallel** one Nubosloop instance per task.
343
+ - Move to next slice **only after** every task in the current slice committed (or `stuck`/`plan-checker` aborted the wave).
344
+ - Start one checkpoint per task before kicking off the loop.
345
+ - Run `loop-run-round --phase preflight` BEFORE every Round-1 executor spawn never skip the cache lookup.
346
+ - Spawn `agents/np-executor.md` on Round 1, `agents/np-build-fixer.md` on Round ≥ 2 — once per round, with only that task's `files_modified` in scope (D-04, no scope expansion).
347
+ - Spawn the three Critic agents (`np-critic-style`, `np-critic-tests`, `np-critic-acceptance`) IN PARALLEL — single message, three Agent blocks per task per round.
348
+ - Run `loop-run-round --phase post-executor` AFTER mechanical checks; honor `next_action: spawn-build-fixer` (verify-red short-circuit, skip critics this round).
349
+ - Run `loop-run-round --phase post-critics` AFTER critics return, to obtain the routing `next_action`.
350
+ - Run `loop-audit-tool-use` per round per spawn — Rule 9 (search-knowledge / match-existing-learning) is mechanically enforced.
301
351
  - Route every commit through `node .nubos-pilot/bin/np-tools.cjs commit-task` so `assertCommittablePaths` (D-25) runs.
302
- - Hard-stop the wave when `commit-task` returns a non-zero exit.
352
+ - Hard-stop the wave when `commit-task` returns non-zero, OR a task hits `stuck`/`plan-checker`.
303
353
 
304
354
  **Don't:**
305
355
  - Run tasks across slices in parallel — slices are serial.
306
356
  - Run intra-slice tasks serially — they're parallel by planner contract.
357
+ - Skip the Nubosloop and call `commit-task` directly after the executor (single-pass executor → commit is forbidden — ADR-0010).
358
+ - Spawn the Critic agents serially — they MUST run in parallel (single message, three Agent blocks).
359
+ - Use `np-executor` on Round ≥ 2 — use `np-build-fixer` (it gets prior critic findings + verify output excerpt).
360
+ - Skip `loop-audit-tool-use` — Rule 9 violations must surface as `rule-9-violation` findings, not be silenced.
361
+ - Extend a task's scope beyond `files_modified` — D-04 violations route to `plan-checker`, not post-hoc PLAN.md mutations.
307
362
  - Invoke `git commit`, `git add`, or any bare git command from this workflow or the spawned agent (CLAUDE.md §Git operations).
308
363
  - Bundle two tasks into one commit (ADR-0004 atomicity).
309
364
  - Skip the checkpoint start step — it's the crash-safety primitive `resume-work` depends on.
@@ -313,18 +368,22 @@ After every slice completes, point the operator at `/np:validate-phase $PHASE` t
313
368
  ## Output
314
369
 
315
370
  - One git commit per completed task (`task(<milestone-id>-<slice-id>-T<NNNN>): <name>`).
316
- - Per-task checkpoint lifetime: `start` → (`transition verifying|pre-commit`)+ → `deleteCheckpoint` (inside commit-task on success).
371
+ - Per-task checkpoint lifetime: `start` → (`transition verifying`)+ → `pre-commit` (set by `loop-run-round --phase commit`) → `deleteCheckpoint` (inside commit-task on success).
372
+ - Per-task `nubosloop` state block on the checkpoint envelope: `last_phase`, `last_action`, `round`, `findings`, `committed_at` / `stuck_at` — surfaced on `np:dashboard`.
373
+ - Auto-`learning-log` entry per committed task (when `auto_log_learning=true`, default) — feeds future Round-1 cache hits.
317
374
  - STATE.md updated via `startTask`'s coordinated lock-cycle (D-08).
318
- - Per slice: updated `S<NNN>-SUMMARY.md` aggregated from task summaries (triggered by the executor agent after the last task in a wave).
375
+ - Per slice: updated `S<NNN>-SUMMARY.md` aggregated from task summaries (triggered after the last task in the wave).
319
376
  - Verified work surface for `/np:validate-phase $PHASE`.
377
+
320
378
  ## Definition of Done
321
379
 
322
380
  This workflow exits successfully only when, per [`templates/COMPLETENESS.md`](../templates/COMPLETENESS.md):
323
381
 
324
- - Rule 1 (Do the whole thing) — every task in every slice committed; no partial slices left.
325
- - Rule 3 (Do it with tests) — every commit ships verify-green; commits without verify transitions are refused by `commit-task`.
326
- - Rule 4 (Do it with documentation) — `update-docs` ran for every committed task; stale module docs fail the workflow.
327
- - Rule 10 (Test before shipping) — verify-green is a hard gate, not advice.
328
- - Rule 12 (Boil the ocean) — no task left in `stuck` state; the orchestrator escalates rather than silently downgrading.
382
+ - Rule 1 (Do the whole thing) — every task in every slice ran its Nubosloop to `next_action=commit` and committed; no partial slices, no `stuck` left silent.
383
+ - Rule 3 (Do it with tests) — every commit ships verify-green; mechanical checks per round are a hard gate; `commit-task` refuses commits without a `verifying` → `pre-commit` transition.
384
+ - Rule 4 (Do it with documentation) — `update-docs` ran for every committed task; stale module docs surface as a `np-critic-acceptance` finding and route the loop back, not forward.
385
+ - Rule 9 (Tool-use audit) — `loop-audit-tool-use` confirms every spawn invoked `search-knowledge` or `match-existing-learning` ≥ 1×; violations route as `rule-9-violation` findings into `loop-evaluate`.
386
+ - Rule 10 (Test before shipping) — verify-green is a hard gate per round, not advice.
387
+ - Rule 12 (Boil the ocean) — no task left in `stuck` state; the orchestrator escalates via askuser rather than silently downgrading or retrying past `loop.maxRounds`.
329
388
 
330
389
  Any violation = workflow exits non-zero. The orchestrator does not relax these.