nubos-pilot 0.8.2 → 0.9.0

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.
Files changed (102) hide show
  1. package/agents/np-architect.md +12 -0
  2. package/agents/np-build-fixer.md +11 -0
  3. package/agents/np-codebase-documenter.md +11 -0
  4. package/agents/np-critic-acceptance.md +102 -0
  5. package/agents/np-critic-style.md +87 -0
  6. package/agents/np-critic-tests.md +88 -0
  7. package/agents/np-executor.md +13 -0
  8. package/agents/np-nyquist-auditor.md +10 -0
  9. package/agents/np-plan-checker.md +33 -2
  10. package/agents/np-planner.md +92 -8
  11. package/agents/np-researcher.md +12 -0
  12. package/agents/np-sc-extractor.md +10 -0
  13. package/agents/np-security-reviewer.md +11 -0
  14. package/agents/np-verifier.md +11 -0
  15. package/bin/check-completeness.cjs +112 -0
  16. package/bin/check-completeness.test.cjs +168 -0
  17. package/bin/np-tools/_args.cjs +63 -0
  18. package/bin/np-tools/_commands.cjs +12 -0
  19. package/bin/np-tools/checkpoint.cjs +1 -1
  20. package/bin/np-tools/dashboard.test.cjs +1 -1
  21. package/bin/np-tools/doctor.cjs +156 -0
  22. package/bin/np-tools/doctor.test.cjs +66 -0
  23. package/bin/np-tools/learning-list.cjs +37 -0
  24. package/bin/np-tools/learning-log.cjs +82 -0
  25. package/bin/np-tools/learning-match.cjs +41 -0
  26. package/bin/np-tools/loop-audit-tool-use.cjs +53 -0
  27. package/bin/np-tools/loop-commands.test.cjs +552 -0
  28. package/bin/np-tools/loop-evaluate.cjs +77 -0
  29. package/bin/np-tools/loop-metrics.cjs +14 -0
  30. package/bin/np-tools/loop-preflight.cjs +39 -0
  31. package/bin/np-tools/loop-run-round.cjs +266 -0
  32. package/bin/np-tools/loop-state-read.cjs +38 -0
  33. package/bin/np-tools/loop-state-record.cjs +50 -0
  34. package/bin/np-tools/loop-stuck.cjs +57 -0
  35. package/bin/np-tools/plan-milestone.cjs +60 -2
  36. package/bin/np-tools/plan-milestone.test.cjs +23 -0
  37. package/bin/np-tools/research-phase.cjs +51 -0
  38. package/bin/np-tools/research-phase.test.cjs +54 -0
  39. package/bin/np-tools/resolve-model.cjs +16 -0
  40. package/bin/np-tools/resolve-model.test.cjs +42 -0
  41. package/bin/np-tools/resume-work.cjs +17 -1
  42. package/docs/adr/0010-nubosloop.md +87 -0
  43. package/docs/adr/0011-researcher-swarm-consensus.md +84 -0
  44. package/docs/adr/0012-completeness-doctrine.md +85 -0
  45. package/docs/adr/0013-learnings-store-schema-evolution.md +128 -0
  46. package/docs/adr/README.md +6 -0
  47. package/docs/agent-frontmatter-schema.md +1 -0
  48. package/lib/agents.test.cjs +3 -0
  49. package/lib/checkpoint.cjs +126 -10
  50. package/lib/checkpoint.test.cjs +193 -0
  51. package/lib/config-defaults.cjs +36 -0
  52. package/lib/config.cjs +47 -0
  53. package/lib/core.cjs +68 -2
  54. package/lib/core.test.cjs +124 -2
  55. package/lib/dashboard.cjs +67 -8
  56. package/lib/dashboard.test.cjs +37 -2
  57. package/lib/knowledge-adapter.cjs +57 -0
  58. package/lib/knowledge-adapter.test.cjs +103 -0
  59. package/lib/learnings.cjs +520 -0
  60. package/lib/learnings.test.cjs +667 -0
  61. package/lib/nubosloop.cjs +646 -0
  62. package/lib/nubosloop.test.cjs +672 -0
  63. package/lib/plan-checker-contract.test.cjs +2 -1
  64. package/lib/researcher-swarm.cjs +369 -0
  65. package/lib/researcher-swarm.test.cjs +273 -0
  66. package/np-tools.cjs +29 -0
  67. package/package.json +1 -1
  68. package/templates/COMPLETENESS.md +191 -0
  69. package/templates/RULES.md +8 -2
  70. package/workflows/add-tests.md +23 -0
  71. package/workflows/add-todo.md +8 -0
  72. package/workflows/architect-phase.md +25 -0
  73. package/workflows/context-stats.md +8 -0
  74. package/workflows/dashboard.md +9 -1
  75. package/workflows/discuss-phase.md +9 -0
  76. package/workflows/discuss-project.md +9 -0
  77. package/workflows/doctor.md +9 -0
  78. package/workflows/execute-phase.md +95 -3
  79. package/workflows/help.md +8 -0
  80. package/workflows/knowledge.md +9 -0
  81. package/workflows/new-milestone.md +8 -0
  82. package/workflows/new-project.md +9 -0
  83. package/workflows/note.md +8 -0
  84. package/workflows/park.md +8 -0
  85. package/workflows/pause-work.md +8 -0
  86. package/workflows/plan-phase.md +13 -0
  87. package/workflows/propose-milestones.md +9 -0
  88. package/workflows/research-phase.md +42 -1
  89. package/workflows/reset-slice.md +8 -0
  90. package/workflows/resume-work.md +8 -0
  91. package/workflows/scan-codebase.md +9 -0
  92. package/workflows/session-report.md +8 -0
  93. package/workflows/skip.md +8 -0
  94. package/workflows/state.md +8 -0
  95. package/workflows/stats.md +8 -0
  96. package/workflows/thread.md +8 -0
  97. package/workflows/undo-task.md +8 -0
  98. package/workflows/undo.md +8 -0
  99. package/workflows/unpark.md +8 -0
  100. package/workflows/update-docs.md +9 -0
  101. package/workflows/validate-phase.md +9 -0
  102. package/workflows/verify-work.md +9 -0
@@ -15,6 +15,17 @@ You DO NOT propose patches. You DO NOT edit source. You report.
15
15
  If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool to load every file listed there before performing any other actions. This is your primary context.
16
16
  </role>
17
17
 
18
+ ## Completeness Mandate
19
+
20
+ This agent operates under [`templates/COMPLETENESS.md`](../templates/COMPLETENESS.md). The rules that bind this role:
21
+
22
+ - **Rule 1 — Do the whole thing.** Every `files_modified` path across the milestone gets scanned against every applicable OWASP category. No "skipped because it looks fine".
23
+ - **Rule 5 — Aim to genuinely impress.** Each Risk finding cites the file, the line, the OWASP category, the concrete attack vector, and the remediation. Vague findings are findings against you.
24
+ - **Rule 8 — Never present a workaround when the real fix exists.** Risk-level findings recommend the real fix; only when the real fix is structurally blocked do you escalate to a `Defer` with an ADR reference.
25
+ - **Rule 12 — Boil the ocean.** No silent skips. If a category is not applicable, declare so explicitly with one-line justification — that is part of the audit, not its absence.
26
+
27
+ Refusal of any rule is a hard-stop. Surface the violation to the orchestrator verbatim and abort the spawn.
28
+
18
29
  ## Inputs
19
30
 
20
31
  | Input | Purpose | Typical path |
@@ -18,6 +18,17 @@ You do NOT propose fixes. You do NOT edit source files. You classify each criter
18
18
  If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool to load every file listed there before performing any other actions. This is your primary context.
19
19
  </role>
20
20
 
21
+ ## Completeness Mandate
22
+
23
+ This agent operates under [`templates/COMPLETENESS.md`](../templates/COMPLETENESS.md). You are the final gate that decides whether the milestone's work is genuinely "done" — uphold the standard. The rules that bind this role:
24
+
25
+ - **Rule 5 — Aim to genuinely impress.** Honest verdicts only. "Mostly Pass" is not a category. If you would mark Pass with a footnote, the footnote means Fail.
26
+ - **Rule 10 — Test before shipping.** Pass requires deterministic evidence (commit SHA + test name + grep hit). Manual "I tried it once" evidence is Fail.
27
+ - **Rule 11 — Ship the complete thing.** Every milestone success_criterion gets a verdict. No "skipped because trivial".
28
+ - **Rule 12 — Boil the ocean.** If evidence is missing, the verdict is Fail with the missing-evidence pattern documented — not a polite Defer.
29
+
30
+ Refusal of any rule is a hard-stop. Surface the violation to the orchestrator verbatim and abort the spawn.
31
+
21
32
  ## Inputs
22
33
 
23
34
  The orchestrator provides these in your prompt context. Read every path it hands you via `Read` — do not guess.
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const REPO_ROOT = path.resolve(__dirname, '..');
8
+ const AGENTS_DIR = path.join(REPO_ROOT, 'agents');
9
+ const WORKFLOWS_DIR = path.join(REPO_ROOT, 'workflows');
10
+ const COMPLETENESS_PATH = path.join(REPO_ROOT, 'templates', 'COMPLETENESS.md');
11
+
12
+ const AGENT_HEADING_RE = /^##\s+Completeness Mandate\b/m;
13
+ const WORKFLOW_HEADING_RE = /^##\s+Definition of Done\b/m;
14
+ const COMPLETENESS_LINK_RE = /COMPLETENESS\.md/;
15
+
16
+ function _listMd(dir) {
17
+ if (!fs.existsSync(dir)) return [];
18
+ return fs
19
+ .readdirSync(dir)
20
+ .filter((f) => f.endsWith('.md'))
21
+ .map((f) => path.join(dir, f))
22
+ .sort();
23
+ }
24
+
25
+ function checkAgents(rootDir) {
26
+ const dir = rootDir ? path.join(rootDir, 'agents') : AGENTS_DIR;
27
+ const out = [];
28
+ for (const file of _listMd(dir)) {
29
+ const body = fs.readFileSync(file, 'utf-8');
30
+ if (!AGENT_HEADING_RE.test(body)) {
31
+ out.push({ file, kind: 'agent', code: 'missing-completeness-mandate', message: 'Agent file lacks "## Completeness Mandate" heading.' });
32
+ continue;
33
+ }
34
+ if (!COMPLETENESS_LINK_RE.test(body)) {
35
+ out.push({ file, kind: 'agent', code: 'missing-completeness-link', message: 'Agent file mentions Mandate but does not link to templates/COMPLETENESS.md.' });
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function checkWorkflows(rootDir) {
42
+ const dir = rootDir ? path.join(rootDir, 'workflows') : WORKFLOWS_DIR;
43
+ const out = [];
44
+ for (const file of _listMd(dir)) {
45
+ const body = fs.readFileSync(file, 'utf-8');
46
+ if (!WORKFLOW_HEADING_RE.test(body)) {
47
+ out.push({ file, kind: 'workflow', code: 'missing-definition-of-done', message: 'Workflow file lacks "## Definition of Done" heading.' });
48
+ continue;
49
+ }
50
+ if (!COMPLETENESS_LINK_RE.test(body)) {
51
+ out.push({ file, kind: 'workflow', code: 'missing-completeness-link', message: 'Workflow file mentions Definition of Done but does not link to templates/COMPLETENESS.md.' });
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+
57
+ function checkCompletenessFile(rootDir) {
58
+ const file = rootDir ? path.join(rootDir, 'templates', 'COMPLETENESS.md') : COMPLETENESS_PATH;
59
+ const out = [];
60
+ if (!fs.existsSync(file)) {
61
+ out.push({ file, kind: 'doctrine', code: 'missing-completeness-file', message: 'templates/COMPLETENESS.md is missing.' });
62
+ return out;
63
+ }
64
+ const body = fs.readFileSync(file, 'utf-8');
65
+ // R5/nit from fifth review: capture IDs in one matchAll pass instead of
66
+ // matching twice (once with /g to find headings, once per heading to pull
67
+ // out the digit). The /g + matchAll pattern surfaces the capture group
68
+ // directly.
69
+ const ids = [];
70
+ for (const m of body.matchAll(/^###\s+(\d+)\.\s+/gm)) {
71
+ ids.push(Number(m[1]));
72
+ }
73
+ const expected = Array.from({ length: 12 }, (_, i) => i + 1);
74
+ if (ids.length !== 12 || ids.some((id, i) => id !== expected[i])) {
75
+ out.push({ file, kind: 'doctrine', code: 'doctrine-drift', message: 'templates/COMPLETENESS.md must contain exactly 12 sequentially numbered rule headings ("### 1." through "### 12.").', ids });
76
+ }
77
+ return out;
78
+ }
79
+
80
+ function checkAll(rootDir) {
81
+ const root = rootDir || REPO_ROOT;
82
+ const violations = [
83
+ ...checkCompletenessFile(root),
84
+ ...checkAgents(root),
85
+ ...checkWorkflows(root),
86
+ ];
87
+ return { violations, exitCode: violations.length ? 1 : 0 };
88
+ }
89
+
90
+ function main() {
91
+ const { violations, exitCode } = checkAll(process.argv[2] || REPO_ROOT);
92
+ if (violations.length) {
93
+ process.stderr.write('check-completeness: ' + violations.length + ' violation(s)\n');
94
+ for (const v of violations) {
95
+ process.stderr.write(' ' + v.file + ' [' + v.kind + ':' + v.code + '] ' + v.message + '\n');
96
+ }
97
+ }
98
+ process.exit(exitCode);
99
+ }
100
+
101
+ if (require.main === module) main();
102
+
103
+ module.exports = {
104
+ checkAgents,
105
+ checkWorkflows,
106
+ checkCompletenessFile,
107
+ checkAll,
108
+ REPO_ROOT,
109
+ AGENT_HEADING_RE,
110
+ WORKFLOW_HEADING_RE,
111
+ COMPLETENESS_LINK_RE,
112
+ };
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const test = require('node:test');
7
+ const assert = require('node:assert/strict');
8
+
9
+ const cc = require('./check-completeness.cjs');
10
+
11
+ function _mkRoot() {
12
+ const r = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-test-'));
13
+ fs.mkdirSync(path.join(r, 'agents'), { recursive: true });
14
+ fs.mkdirSync(path.join(r, 'workflows'), { recursive: true });
15
+ fs.mkdirSync(path.join(r, 'templates'), { recursive: true });
16
+ return r;
17
+ }
18
+
19
+ function _seedDoctrine(root, opts) {
20
+ const { count } = Object.assign({ count: 12 }, opts || {});
21
+ const headings = [];
22
+ for (let i = 1; i <= count; i += 1) headings.push('### ' + i + '. Rule ' + i + '\n\nText.');
23
+ fs.writeFileSync(
24
+ path.join(root, 'templates', 'COMPLETENESS.md'),
25
+ '# Doctrine\n\n' + headings.join('\n\n') + '\n',
26
+ 'utf-8',
27
+ );
28
+ }
29
+
30
+ function _seedAgent(root, name, body) {
31
+ const fm = '---\nname: ' + name + '\ndescription: x\ntier: sonnet\ntools: Read\n---\n';
32
+ fs.writeFileSync(path.join(root, 'agents', name + '.md'), fm + (body || ''), 'utf-8');
33
+ }
34
+
35
+ function _seedWorkflow(root, name, body) {
36
+ fs.writeFileSync(path.join(root, 'workflows', name + '.md'), body || '# ' + name + '\n', 'utf-8');
37
+ }
38
+
39
+ test('CC-1: complete root → no violations', () => {
40
+ const r = _mkRoot();
41
+ try {
42
+ _seedDoctrine(r);
43
+ _seedAgent(r, 'np-foo', '## Completeness Mandate\n\nSee templates/COMPLETENESS.md.\n');
44
+ _seedWorkflow(r, 'foo', '# foo\n\n## Definition of Done\n\nSee templates/COMPLETENESS.md.\n');
45
+ const res = cc.checkAll(r);
46
+ assert.deepEqual(res.violations, []);
47
+ assert.equal(res.exitCode, 0);
48
+ } finally {
49
+ fs.rmSync(r, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ test('CC-2: agent missing Completeness Mandate heading → violation', () => {
54
+ const r = _mkRoot();
55
+ try {
56
+ _seedDoctrine(r);
57
+ _seedAgent(r, 'np-foo', 'No mandate here.\n');
58
+ const v = cc.checkAgents(r);
59
+ assert.equal(v.length, 1);
60
+ assert.equal(v[0].code, 'missing-completeness-mandate');
61
+ assert.ok(v[0].file.endsWith('np-foo.md'));
62
+ } finally {
63
+ fs.rmSync(r, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ test('CC-3: agent has heading but no COMPLETENESS.md link → violation', () => {
68
+ const r = _mkRoot();
69
+ try {
70
+ _seedDoctrine(r);
71
+ _seedAgent(r, 'np-foo', '## Completeness Mandate\n\nThis lacks the link.\n');
72
+ const v = cc.checkAgents(r);
73
+ assert.equal(v.length, 1);
74
+ assert.equal(v[0].code, 'missing-completeness-link');
75
+ } finally {
76
+ fs.rmSync(r, { recursive: true, force: true });
77
+ }
78
+ });
79
+
80
+ test('CC-4: workflow missing Definition of Done heading → violation', () => {
81
+ const r = _mkRoot();
82
+ try {
83
+ _seedDoctrine(r);
84
+ _seedWorkflow(r, 'foo', '# foo\n\nNo DoD.\n');
85
+ const v = cc.checkWorkflows(r);
86
+ assert.equal(v.length, 1);
87
+ assert.equal(v[0].code, 'missing-definition-of-done');
88
+ } finally {
89
+ fs.rmSync(r, { recursive: true, force: true });
90
+ }
91
+ });
92
+
93
+ test('CC-5: workflow has heading but no COMPLETENESS.md link → violation', () => {
94
+ const r = _mkRoot();
95
+ try {
96
+ _seedDoctrine(r);
97
+ _seedWorkflow(r, 'foo', '# foo\n\n## Definition of Done\n\nText only.\n');
98
+ const v = cc.checkWorkflows(r);
99
+ assert.equal(v.length, 1);
100
+ assert.equal(v[0].code, 'missing-completeness-link');
101
+ } finally {
102
+ fs.rmSync(r, { recursive: true, force: true });
103
+ }
104
+ });
105
+
106
+ test('CC-6: doctrine file missing → violation', () => {
107
+ const r = _mkRoot();
108
+ try {
109
+ const v = cc.checkCompletenessFile(r);
110
+ assert.equal(v.length, 1);
111
+ assert.equal(v[0].code, 'missing-completeness-file');
112
+ } finally {
113
+ fs.rmSync(r, { recursive: true, force: true });
114
+ }
115
+ });
116
+
117
+ test('CC-7: doctrine has only 11 rules → drift violation', () => {
118
+ const r = _mkRoot();
119
+ try {
120
+ _seedDoctrine(r, { count: 11 });
121
+ const v = cc.checkCompletenessFile(r);
122
+ assert.equal(v.length, 1);
123
+ assert.equal(v[0].code, 'doctrine-drift');
124
+ } finally {
125
+ fs.rmSync(r, { recursive: true, force: true });
126
+ }
127
+ });
128
+
129
+ test('CC-8: doctrine with skipped numbering (1,2,4,…) → drift', () => {
130
+ const r = _mkRoot();
131
+ try {
132
+ fs.writeFileSync(
133
+ path.join(r, 'templates', 'COMPLETENESS.md'),
134
+ '### 1. A\n\n### 2. B\n\n### 4. C\n\n### 5. D\n\n### 6. E\n\n### 7. F\n\n### 8. G\n\n### 9. H\n\n### 10. I\n\n### 11. J\n\n### 12. K\n\n### 13. L\n',
135
+ 'utf-8',
136
+ );
137
+ const v = cc.checkCompletenessFile(r);
138
+ assert.equal(v.length, 1);
139
+ assert.equal(v[0].code, 'doctrine-drift');
140
+ } finally {
141
+ fs.rmSync(r, { recursive: true, force: true });
142
+ }
143
+ });
144
+
145
+ test('CC-9: real nubos-pilot repo passes — every agent + workflow + doctrine compliant', () => {
146
+ const res = cc.checkAll();
147
+ if (res.violations.length) {
148
+ const lines = res.violations
149
+ .map((v) => ' ' + v.file + ' [' + v.code + '] ' + v.message)
150
+ .join('\n');
151
+ assert.fail('Real-tree completeness violations:\n' + lines);
152
+ }
153
+ assert.equal(res.exitCode, 0);
154
+ });
155
+
156
+ test('CC-10: CLI exits 1 on a sandbox missing the doctrine', () => {
157
+ const { spawnSync } = require('node:child_process');
158
+ const r = _mkRoot();
159
+ try {
160
+ const result = spawnSync(process.execPath, [path.join(__dirname, 'check-completeness.cjs'), r], {
161
+ encoding: 'utf-8',
162
+ });
163
+ assert.equal(result.status, 1);
164
+ assert.match(result.stderr, /violation/i);
165
+ } finally {
166
+ fs.rmSync(r, { recursive: true, force: true });
167
+ }
168
+ });
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const { NubosPilotError } = require('../../lib/core.cjs');
4
+
5
+ function getFlag(rest, name) {
6
+ const idx = rest.indexOf(name);
7
+ return idx !== -1 ? rest[idx + 1] : undefined;
8
+ }
9
+
10
+ function getJsonFlag(rest, name, missingCode, hint) {
11
+ const raw = getFlag(rest, name);
12
+ if (raw === undefined) {
13
+ throw new NubosPilotError(
14
+ missingCode,
15
+ name + ' is required',
16
+ hint ? { hint } : {},
17
+ );
18
+ }
19
+ try { return JSON.parse(raw); }
20
+ catch (err) {
21
+ throw new NubosPilotError(
22
+ missingCode + '-invalid-json',
23
+ name + ' must be valid JSON',
24
+ { cause: err && err.message },
25
+ );
26
+ }
27
+ }
28
+
29
+ function optionalJsonFlag(rest, name) {
30
+ const raw = getFlag(rest, name);
31
+ if (raw === undefined) return undefined;
32
+ try { return JSON.parse(raw); }
33
+ catch (err) {
34
+ throw new NubosPilotError(
35
+ 'arg-invalid-json',
36
+ name + ' must be valid JSON when provided',
37
+ { cause: err && err.message, flag: name },
38
+ );
39
+ }
40
+ }
41
+
42
+ function assertMatch(value, re, code, label) {
43
+ if (typeof value !== 'string' || !re.test(value)) {
44
+ throw new NubosPilotError(
45
+ code,
46
+ label + ' must match ' + re.toString() + ' (got ' + JSON.stringify(value) + ')',
47
+ { value },
48
+ );
49
+ }
50
+ }
51
+
52
+ function assertOptionalMatch(value, re, code, label) {
53
+ if (value == null) return;
54
+ assertMatch(value, re, code, label);
55
+ }
56
+
57
+ module.exports = {
58
+ getFlag,
59
+ getJsonFlag,
60
+ optionalJsonFlag,
61
+ assertMatch,
62
+ assertOptionalMatch,
63
+ };
@@ -77,6 +77,18 @@ const COMMANDS = [
77
77
  { name: 'context-stats', category: 'Utility', description: 'Aggregated context-budget stats (file counts + bytes per group, knowledge-index size)', description_de: 'Aggregierte Context-Budget-Stats (Dateien/Bytes pro Gruppe, Knowledge-Index-Größe)' },
78
78
  { name: 'session-snapshot-write', category: 'Utility', description: 'Capture session snapshot (current_task + recent commits + open handoffs) for resume', description_de: 'Erfasst Session-Snapshot (current_task + letzte Commits + offene Handoffs) für Resume' },
79
79
  { name: 'session-snapshot-read', category: 'Utility', description: 'Print last session snapshot as JSON', description_de: 'Gibt letzten Session-Snapshot als JSON aus' },
80
+
81
+ { name: 'loop-state-read', category: 'Execution', description: 'Read the per-task Nubosloop state from the checkpoint (round, last_action, findings)', description_de: 'Liest Nubosloop-State pro Task aus dem Checkpoint (round, last_action, findings)' },
82
+ { name: 'loop-state-record', category: 'Execution', description: 'Atomically merge a partial Nubosloop state update into the task checkpoint', description_de: 'Mergt einen partiellen Nubosloop-State-Update atomar in den Task-Checkpoint' },
83
+ { name: 'loop-evaluate', category: 'Execution', description: 'Run evaluateLoop over critic outputs JSON; emit next_action + findings + routing', description_de: 'Führt evaluateLoop auf Critic-Outputs (JSON) aus; gibt next_action + Findings + Routing aus' },
84
+ { name: 'loop-preflight', category: 'Execution', description: 'Per-task pre-flight cache lookup (ADR-0010 Step 1) — short-circuits the Researcher-Schwarm on hit', description_de: 'Per-Task Pre-Flight-Cache-Lookup (ADR-0010 Step 1) — short-circuited den Researcher-Schwarm bei Treffer' },
85
+ { name: 'loop-run-round', category: 'Execution', description: 'Drive the per-task Nubosloop state machine — phases: preflight | post-executor | post-critics | commit | stuck', description_de: 'Treibt die Per-Task Nubosloop-State-Machine — Phasen: preflight | post-executor | post-critics | commit | stuck' },
86
+ { name: 'loop-audit-tool-use', category: 'Execution', description: 'Record/read the tool-use audit per spawn (Completeness Rule 9 mechanical check)', description_de: 'Tool-use Audit pro Spawn schreiben/lesen (Completeness Rule 9 mechanische Prüfung)' },
87
+ { name: 'loop-stuck', category: 'Execution', description: 'Mark a task as stuck (writes loop-state + flips checkpoint status to stuck)', description_de: 'Markiert Task als stuck (schreibt Loop-State + setzt Checkpoint-Status auf stuck)' },
88
+ { name: 'loop-metrics', category: 'Utility', description: 'Aggregate Nubosloop telemetry across all checkpoints (commits, stuck, route distribution)', description_de: 'Aggregiert Nubosloop-Telemetrie über alle Checkpoints (Commits, Stuck, Routing)' },
89
+ { name: 'learning-log', category: 'Execution', description: 'Persist a learning to the local store (or MCP adapter when configured)', description_de: 'Persistiert ein Learning im lokalen Store (oder MCP-Adapter falls konfiguriert)' },
90
+ { name: 'learning-match', category: 'Utility', description: 'Query the learnings store for cached patterns matching a free-text query', description_de: 'Fragt den Learnings-Store nach Cached-Patterns ab' },
91
+ { name: 'learning-list', category: 'Utility', description: 'List learnings sorted by occurrence (most-used first)', description_de: 'Listet Learnings sortiert nach Occurrence (meistgenutzt zuerst)' },
80
92
  ];
81
93
 
82
94
  const CATEGORY_LABELS = Object.freeze({
@@ -6,7 +6,7 @@ const {
6
6
  readCheckpoint,
7
7
  } = require('../../lib/checkpoint.cjs');
8
8
 
9
- const _VALID_STATUSES = new Set(['in-progress', 'verifying', 'pre-commit', 'done']);
9
+ const _VALID_STATUSES = new Set(['in-progress', 'verifying', 'pre-commit', 'done', 'stuck']);
10
10
 
11
11
  function _parseFlags(rest) {
12
12
  const flags = {};
@@ -76,5 +76,5 @@ test('CLI-DB-6: --json snapshot stays language-neutral', () => {
76
76
  const code = subcmd.run(['--json'], { cwd: root, stdout: cap.stub });
77
77
  assert.equal(code, 0);
78
78
  const parsed = JSON.parse(cap.get());
79
- assert.deepEqual(Object.keys(parsed), ['milestones']);
79
+ assert.deepEqual(Object.keys(parsed).sort(), ['milestones', 'nubosloop']);
80
80
  });
@@ -336,6 +336,158 @@ function _checkMilestoneLayout(projectRoot) {
336
336
  return issues;
337
337
  }
338
338
 
339
+ const NUBOSLOOP_CRITICS = ['np-critic-style', 'np-critic-tests', 'np-critic-acceptance'];
340
+
341
+ function _checkNubosloopCritics(projectRoot) {
342
+ const issues = [];
343
+ const scope = _readScope(projectRoot);
344
+ const payloadDir = _payloadDirFor(projectRoot, scope);
345
+ const agentsDir = path.join(payloadDir, 'agents');
346
+ if (!fs.existsSync(agentsDir)) {
347
+ issues.push({
348
+ id: 'nubosloop-agents-dir-missing',
349
+ severity: 'warn',
350
+ fixable: 'reinstall',
351
+ details: {
352
+ expected: agentsDir,
353
+ hint: 'run `npx nubos-pilot update` to refresh the payload (Critic-Schwarm agents ship as part of the payload).',
354
+ },
355
+ });
356
+ return issues;
357
+ }
358
+ for (const agent of NUBOSLOOP_CRITICS) {
359
+ const agentPath = path.join(agentsDir, agent + '.md');
360
+ if (!fs.existsSync(agentPath)) {
361
+ issues.push({
362
+ id: 'nubosloop-critic-missing',
363
+ severity: 'warn',
364
+ fixable: 'reinstall',
365
+ details: {
366
+ agent,
367
+ expected: agentPath,
368
+ hint: 'run `npx nubos-pilot update` to refresh the payload.',
369
+ },
370
+ });
371
+ }
372
+ }
373
+ return issues;
374
+ }
375
+
376
+ function _checkNubosloopKnowledgeStore(projectRoot) {
377
+ const issues = [];
378
+ const stateDir = path.join(projectRoot, '.nubos-pilot');
379
+ if (!fs.existsSync(stateDir)) return issues;
380
+ const learningsPath = path.join(stateDir, 'knowledge', 'learnings.json');
381
+ if (!fs.existsSync(learningsPath)) {
382
+ issues.push({
383
+ id: 'nubosloop-knowledge-store-missing',
384
+ severity: 'info',
385
+ fixable: 'auto',
386
+ details: {
387
+ expected: learningsPath,
388
+ hint: 'auto-created on first Nubosloop commit; safe to ignore on a fresh project.',
389
+ },
390
+ });
391
+ return issues;
392
+ }
393
+ try {
394
+ const parsed = JSON.parse(fs.readFileSync(learningsPath, 'utf-8'));
395
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.learnings)) {
396
+ issues.push({
397
+ id: 'nubosloop-knowledge-store-corrupt',
398
+ severity: 'warn',
399
+ fixable: 'manual',
400
+ details: {
401
+ path: learningsPath,
402
+ hint: 'expected JSON with `version` and `learnings[]`; remove or restore from a backup.',
403
+ },
404
+ });
405
+ }
406
+ } catch (err) {
407
+ issues.push({
408
+ id: 'nubosloop-knowledge-store-corrupt',
409
+ severity: 'warn',
410
+ fixable: 'manual',
411
+ details: { path: learningsPath, cause: err && err.message },
412
+ });
413
+ }
414
+ return issues;
415
+ }
416
+
417
+ function _checkNubosloopConfig(projectRoot) {
418
+ const issues = [];
419
+ const cfgPath = path.join(projectRoot, '.nubos-pilot', 'config.json');
420
+ if (!fs.existsSync(cfgPath)) return issues;
421
+ let cfg;
422
+ try { cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')); } catch { return issues; }
423
+ const swarm = cfg && cfg.swarm;
424
+ const adapter = swarm && swarm.knowledge_adapter;
425
+ if (adapter && adapter !== 'local' && adapter !== 'mcp') {
426
+ issues.push({
427
+ id: 'nubosloop-knowledge-adapter-invalid',
428
+ severity: 'warn',
429
+ fixable: 'manual',
430
+ details: {
431
+ value: adapter,
432
+ supported: ['local', 'mcp'],
433
+ hint: 'set swarm.knowledge_adapter to "local" or "mcp" — falls back to "local" silently otherwise.',
434
+ },
435
+ });
436
+ }
437
+ if (adapter === 'mcp') {
438
+ issues.push({
439
+ id: 'nubosloop-knowledge-adapter-mcp-pending',
440
+ severity: 'info',
441
+ fixable: 'manual',
442
+ details: {
443
+ hint: 'mcp adapter is configured but the transport ships in Phase 7/8; matchExistingLearning will throw mcp-adapter-not-implemented until then.',
444
+ },
445
+ });
446
+ }
447
+ const loop = cfg && cfg.loop;
448
+ if (loop && loop.maxRounds != null) {
449
+ const n = Number(loop.maxRounds);
450
+ if (!Number.isFinite(n) || n < 1 || n > 10) {
451
+ issues.push({
452
+ id: 'nubosloop-maxRounds-out-of-range',
453
+ severity: 'warn',
454
+ fixable: 'manual',
455
+ details: { value: loop.maxRounds, expected_range: '[1, 10]' },
456
+ });
457
+ }
458
+ }
459
+ return issues;
460
+ }
461
+
462
+ function _checkOrphanTmpFiles(projectRoot) {
463
+ const issues = [];
464
+ const stateDir = path.join(projectRoot, '.nubos-pilot');
465
+ const dirs = [
466
+ stateDir,
467
+ path.join(stateDir, 'checkpoints'),
468
+ path.join(stateDir, 'knowledge'),
469
+ path.join(stateDir, 'state'),
470
+ ];
471
+ const { sweepStaleTmpFiles } = require('../../lib/core.cjs');
472
+ for (const d of dirs) {
473
+ let result;
474
+ try { result = sweepStaleTmpFiles(d, { olderThanMs: 60 * 60 * 1000 }); }
475
+ catch { continue; }
476
+ if (!result || !Array.isArray(result.swept) || result.swept.length === 0) continue;
477
+ issues.push({
478
+ id: 'orphan-tmp-files-cleaned',
479
+ severity: 'info',
480
+ fixable: 'auto',
481
+ details: {
482
+ dir: d,
483
+ cleaned: result.swept.length,
484
+ hint: 'Orphaned tmp files (>1h old) from a hard-killed process were swept.',
485
+ },
486
+ });
487
+ }
488
+ return issues;
489
+ }
490
+
339
491
  function _audit(projectRoot) {
340
492
  const scope = _readScope(projectRoot);
341
493
  const payloadDir = _payloadDirFor(projectRoot, scope);
@@ -349,6 +501,10 @@ function _audit(projectRoot) {
349
501
  issues.push(..._checkAskUserBroken());
350
502
  issues.push(..._checkCodebaseDocs(projectRoot));
351
503
  issues.push(..._checkMilestoneLayout(projectRoot));
504
+ issues.push(..._checkNubosloopCritics(projectRoot));
505
+ issues.push(..._checkNubosloopKnowledgeStore(projectRoot));
506
+ issues.push(..._checkNubosloopConfig(projectRoot));
507
+ issues.push(..._checkOrphanTmpFiles(projectRoot));
352
508
  return { issues, _codexContent: codex.content };
353
509
  }
354
510
 
@@ -139,3 +139,69 @@ test('DOC-5: no tbd flag after prose applied', async () => {
139
139
  const tbd = out.issues.find((i) => i.id === 'codebase-tbd-docs');
140
140
  assert.ok(!tbd, 'expected no codebase-tbd-docs');
141
141
  });
142
+
143
+ test('DOC-7: flags nubosloop-knowledge-store-corrupt when JSON is malformed', async () => {
144
+ const root = makeSandbox();
145
+ fs.writeFileSync(path.join(root, 'src.js'), 'export function a(){}');
146
+ scanCodebase.run([], { cwd: root, stdout: captureStdout().stub });
147
+ fs.mkdirSync(path.join(root, '.nubos-pilot', 'knowledge'), { recursive: true });
148
+ fs.writeFileSync(path.join(root, '.nubos-pilot', 'knowledge', 'learnings.json'), 'NOT JSON');
149
+
150
+ const cap = captureStdout();
151
+ await doctor.run([], { cwd: root, stdout: cap.stub, stderr: cap.stub, askUser: async () => ({ value: false }) });
152
+ const out = cap.json();
153
+ const ids = out.issues.map((i) => i.id);
154
+ assert.ok(ids.includes('nubosloop-knowledge-store-corrupt'));
155
+ });
156
+
157
+ test('DOC-8: flags nubosloop-knowledge-adapter-invalid for unsupported adapter', async () => {
158
+ const root = makeSandbox();
159
+ fs.writeFileSync(path.join(root, 'src.js'), 'export function a(){}');
160
+ scanCodebase.run([], { cwd: root, stdout: captureStdout().stub });
161
+ fs.writeFileSync(
162
+ path.join(root, '.nubos-pilot', 'config.json'),
163
+ JSON.stringify({ swarm: { knowledge_adapter: 'pinecone' } }),
164
+ );
165
+ const cap = captureStdout();
166
+ await doctor.run([], { cwd: root, stdout: cap.stub, stderr: cap.stub, askUser: async () => ({ value: false }) });
167
+ const out = cap.json();
168
+ const ids = out.issues.map((i) => i.id);
169
+ assert.ok(ids.includes('nubosloop-knowledge-adapter-invalid'));
170
+ });
171
+
172
+ test('DOC-9: flags nubosloop-maxRounds-out-of-range when value > 10', async () => {
173
+ const root = makeSandbox();
174
+ fs.writeFileSync(path.join(root, 'src.js'), 'export function a(){}');
175
+ scanCodebase.run([], { cwd: root, stdout: captureStdout().stub });
176
+ fs.writeFileSync(
177
+ path.join(root, '.nubos-pilot', 'config.json'),
178
+ JSON.stringify({ loop: { maxRounds: 99 } }),
179
+ );
180
+ const cap = captureStdout();
181
+ await doctor.run([], { cwd: root, stdout: cap.stub, stderr: cap.stub, askUser: async () => ({ value: false }) });
182
+ const out = cap.json();
183
+ const ids = out.issues.map((i) => i.id);
184
+ assert.ok(ids.includes('nubosloop-maxRounds-out-of-range'));
185
+ });
186
+
187
+ test('DOC-10: clean config produces no nubosloop issues', async () => {
188
+ const root = makeSandbox();
189
+ fs.writeFileSync(path.join(root, 'src.js'), 'export function a(){}');
190
+ scanCodebase.run([], { cwd: root, stdout: captureStdout().stub });
191
+ fs.mkdirSync(path.join(root, '.nubos-pilot', 'knowledge'), { recursive: true });
192
+ fs.writeFileSync(
193
+ path.join(root, '.nubos-pilot', 'knowledge', 'learnings.json'),
194
+ JSON.stringify({ version: 1, learnings: [] }),
195
+ );
196
+ fs.writeFileSync(
197
+ path.join(root, '.nubos-pilot', 'config.json'),
198
+ JSON.stringify({ loop: { maxRounds: 3 }, swarm: { knowledge_adapter: 'local' } }),
199
+ );
200
+ const cap = captureStdout();
201
+ await doctor.run([], { cwd: root, stdout: cap.stub, stderr: cap.stub, askUser: async () => ({ value: false }) });
202
+ const out = cap.json();
203
+ const ids = out.issues.map((i) => i.id);
204
+ assert.ok(!ids.some((id) => id.startsWith('nubosloop-knowledge-store-corrupt')));
205
+ assert.ok(!ids.some((id) => id.startsWith('nubosloop-knowledge-adapter-invalid')));
206
+ assert.ok(!ids.some((id) => id.startsWith('nubosloop-maxRounds')));
207
+ });