nubos-pilot 0.1.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 (273) hide show
  1. package/agents/np-ai-researcher.md +140 -0
  2. package/agents/np-code-fixer.md +363 -0
  3. package/agents/np-code-reviewer.md +351 -0
  4. package/agents/np-domain-researcher.md +136 -0
  5. package/agents/np-eval-auditor.md +167 -0
  6. package/agents/np-eval-planner.md +153 -0
  7. package/agents/np-executor.md +72 -0
  8. package/agents/np-framework-selector.md +171 -0
  9. package/agents/np-nyquist-auditor.md +185 -0
  10. package/agents/np-plan-checker.md +165 -0
  11. package/agents/np-planner.md +199 -0
  12. package/agents/np-researcher.md +150 -0
  13. package/agents/np-security-auditor.md +206 -0
  14. package/agents/np-ui-auditor.md +369 -0
  15. package/agents/np-ui-checker.md +192 -0
  16. package/agents/np-ui-researcher.md +324 -0
  17. package/agents/np-verifier.md +79 -0
  18. package/bin/check-coverage.cjs +40 -0
  19. package/bin/check-workflows.cjs +171 -0
  20. package/bin/check-workflows.test.cjs +208 -0
  21. package/bin/install.js +500 -0
  22. package/bin/np-tools/_commands.cjs +70 -0
  23. package/bin/np-tools/add-tests.cjs +171 -0
  24. package/bin/np-tools/add-tests.test.cjs +122 -0
  25. package/bin/np-tools/add-todo.cjs +108 -0
  26. package/bin/np-tools/add-todo.test.cjs +112 -0
  27. package/bin/np-tools/agent-skills.cjs +14 -0
  28. package/bin/np-tools/agent-skills.test.cjs +42 -0
  29. package/bin/np-tools/ai-integration-phase.cjs +109 -0
  30. package/bin/np-tools/ai-integration-phase.test.cjs +123 -0
  31. package/bin/np-tools/askuser.cjs +53 -0
  32. package/bin/np-tools/askuser.test.cjs +49 -0
  33. package/bin/np-tools/autonomous.cjs +69 -0
  34. package/bin/np-tools/autonomous.test.cjs +74 -0
  35. package/bin/np-tools/checkpoint.cjs +101 -0
  36. package/bin/np-tools/checkpoint.test.cjs +119 -0
  37. package/bin/np-tools/code-review.cjs +133 -0
  38. package/bin/np-tools/code-review.test.cjs +96 -0
  39. package/bin/np-tools/commit-task.cjs +120 -0
  40. package/bin/np-tools/commit-task.test.cjs +160 -0
  41. package/bin/np-tools/commit.cjs +103 -0
  42. package/bin/np-tools/commit.test.cjs +93 -0
  43. package/bin/np-tools/config.cjs +101 -0
  44. package/bin/np-tools/config.test.cjs +71 -0
  45. package/bin/np-tools/discuss-phase-power.cjs +265 -0
  46. package/bin/np-tools/discuss-phase-power.test.cjs +242 -0
  47. package/bin/np-tools/discuss-phase.cjs +132 -0
  48. package/bin/np-tools/discuss-phase.test.cjs +148 -0
  49. package/bin/np-tools/dispatch.cjs +116 -0
  50. package/bin/np-tools/doctor.cjs +242 -0
  51. package/bin/np-tools/eval-review.cjs +116 -0
  52. package/bin/np-tools/eval-review.test.cjs +123 -0
  53. package/bin/np-tools/execute-phase.cjs +182 -0
  54. package/bin/np-tools/execute-phase.test.cjs +116 -0
  55. package/bin/np-tools/execute-plan.cjs +124 -0
  56. package/bin/np-tools/execute-plan.test.cjs +82 -0
  57. package/bin/np-tools/help.cjs +28 -0
  58. package/bin/np-tools/help.test.cjs +29 -0
  59. package/bin/np-tools/init-dispatch.test.cjs +91 -0
  60. package/bin/np-tools/metrics.cjs +97 -0
  61. package/bin/np-tools/metrics.test.cjs +188 -0
  62. package/bin/np-tools/new-milestone.cjs +288 -0
  63. package/bin/np-tools/new-milestone.test.cjs +166 -0
  64. package/bin/np-tools/new-project.cjs +284 -0
  65. package/bin/np-tools/new-project.test.cjs +165 -0
  66. package/bin/np-tools/next.cjs +7 -0
  67. package/bin/np-tools/next.test.cjs +30 -0
  68. package/bin/np-tools/park.cjs +48 -0
  69. package/bin/np-tools/park.test.cjs +50 -0
  70. package/bin/np-tools/pause-work.cjs +24 -0
  71. package/bin/np-tools/pause-work.test.cjs +74 -0
  72. package/bin/np-tools/phase.cjs +71 -0
  73. package/bin/np-tools/phase.test.cjs +81 -0
  74. package/bin/np-tools/plan-diff.cjs +57 -0
  75. package/bin/np-tools/plan-diff.test.cjs +134 -0
  76. package/bin/np-tools/plan-milestone-gaps.cjs +115 -0
  77. package/bin/np-tools/plan-milestone-gaps.test.cjs +122 -0
  78. package/bin/np-tools/plan-phase.cjs +350 -0
  79. package/bin/np-tools/plan-phase.test.cjs +263 -0
  80. package/bin/np-tools/progress.cjs +7 -0
  81. package/bin/np-tools/progress.test.cjs +44 -0
  82. package/bin/np-tools/queue.cjs +213 -0
  83. package/bin/np-tools/research-phase.cjs +144 -0
  84. package/bin/np-tools/research-phase.test.cjs +154 -0
  85. package/bin/np-tools/reset-slice.cjs +17 -0
  86. package/bin/np-tools/reset-slice.test.cjs +96 -0
  87. package/bin/np-tools/resolve-model.cjs +110 -0
  88. package/bin/np-tools/resolve-model.test.cjs +200 -0
  89. package/bin/np-tools/resume-work.cjs +76 -0
  90. package/bin/np-tools/resume-work.test.cjs +91 -0
  91. package/bin/np-tools/skip.cjs +48 -0
  92. package/bin/np-tools/skip.test.cjs +66 -0
  93. package/bin/np-tools/slug.cjs +34 -0
  94. package/bin/np-tools/slug.test.cjs +46 -0
  95. package/bin/np-tools/state.cjs +16 -0
  96. package/bin/np-tools/state.test.cjs +40 -0
  97. package/bin/np-tools/stats.cjs +151 -0
  98. package/bin/np-tools/stats.test.cjs +118 -0
  99. package/bin/np-tools/triage.cjs +128 -0
  100. package/bin/np-tools/ui-phase.cjs +108 -0
  101. package/bin/np-tools/ui-phase.test.cjs +121 -0
  102. package/bin/np-tools/ui-review.cjs +108 -0
  103. package/bin/np-tools/ui-review.test.cjs +120 -0
  104. package/bin/np-tools/undo-task.cjs +31 -0
  105. package/bin/np-tools/undo-task.test.cjs +117 -0
  106. package/bin/np-tools/undo.cjs +43 -0
  107. package/bin/np-tools/undo.test.cjs +120 -0
  108. package/bin/np-tools/unpark.cjs +48 -0
  109. package/bin/np-tools/unpark.test.cjs +50 -0
  110. package/bin/np-tools/verify-work.cjs +186 -0
  111. package/bin/np-tools/verify-work.test.cjs +97 -0
  112. package/docs/adr/0001-no-daemon-invariant.md +82 -0
  113. package/docs/adr/0002-zero-runtime-dependencies.md +90 -0
  114. package/docs/adr/0003-max-six-unit-types.md +85 -0
  115. package/docs/adr/0004-atomic-commit-per-unit.md +102 -0
  116. package/docs/adr/0005-three-orthogonal-file-trees.md +98 -0
  117. package/docs/adr/0006-yaml-dependency-amendment.md +60 -0
  118. package/docs/adr/README.md +27 -0
  119. package/docs/agent-frontmatter-schema.md +84 -0
  120. package/docs/phase-artifact-schemas.md +292 -0
  121. package/docs/phase-directory-layout.md +82 -0
  122. package/lib/__tests__/README.md +1 -0
  123. package/lib/agents.cjs +98 -0
  124. package/lib/agents.test.cjs +286 -0
  125. package/lib/askuser.cjs +36 -0
  126. package/lib/askuser.test.cjs +310 -0
  127. package/lib/checkpoint.cjs +135 -0
  128. package/lib/checkpoint.test.cjs +184 -0
  129. package/lib/core.cjs +165 -0
  130. package/lib/core.test.cjs +405 -0
  131. package/lib/fixtures/README.md +1 -0
  132. package/lib/fixtures/phase-tree/README.md +1 -0
  133. package/lib/fixtures/plans/cycle/PLAN.md +16 -0
  134. package/lib/fixtures/plans/cycle/tasks/T-01.md +20 -0
  135. package/lib/fixtures/plans/cycle/tasks/T-02.md +20 -0
  136. package/lib/fixtures/plans/cycle/tasks/T-03.md +20 -0
  137. package/lib/fixtures/plans/linear/PLAN.md +16 -0
  138. package/lib/fixtures/plans/linear/tasks/T-01.md +20 -0
  139. package/lib/fixtures/plans/linear/tasks/T-02.md +20 -0
  140. package/lib/fixtures/plans/linear/tasks/T-03.md +20 -0
  141. package/lib/fixtures/plans/parallel/PLAN.md +16 -0
  142. package/lib/fixtures/plans/parallel/tasks/T-01.md +20 -0
  143. package/lib/fixtures/plans/parallel/tasks/T-02.md +20 -0
  144. package/lib/fixtures/plans/parallel/tasks/T-03.md +20 -0
  145. package/lib/fixtures/plans/wave-conflict/PLAN.md +16 -0
  146. package/lib/fixtures/plans/wave-conflict/tasks/T-01.md +20 -0
  147. package/lib/fixtures/plans/wave-conflict/tasks/T-02.md +20 -0
  148. package/lib/fixtures/roadmap/ROADMAP-malformed.md +3 -0
  149. package/lib/fixtures/roadmap/ROADMAP-minimal.md +51 -0
  150. package/lib/fixtures/roadmap/roadmap-malformed.yaml +7 -0
  151. package/lib/fixtures/roadmap/roadmap-minimal.yaml +40 -0
  152. package/lib/fixtures/roadmap/roadmap-ten-phases.yaml +101 -0
  153. package/lib/fixtures/templates/phase-context.md +6 -0
  154. package/lib/fixtures/templates/plan-skeleton.md +6 -0
  155. package/lib/frontmatter.cjs +251 -0
  156. package/lib/frontmatter.test.cjs +177 -0
  157. package/lib/gaps.cjs +197 -0
  158. package/lib/gaps.test.cjs +200 -0
  159. package/lib/git.cjs +207 -0
  160. package/lib/git.test.cjs +305 -0
  161. package/lib/install/agents-md.cjs +77 -0
  162. package/lib/install/backup.cjs +70 -0
  163. package/lib/install/codex-toml.cjs +440 -0
  164. package/lib/install/managed-block.cjs +30 -0
  165. package/lib/install/manifest.cjs +148 -0
  166. package/lib/install/mcp-writer.cjs +127 -0
  167. package/lib/install/runtime-detect.cjs +44 -0
  168. package/lib/install/staging.cjs +149 -0
  169. package/lib/metrics-aggregate.cjs +229 -0
  170. package/lib/metrics-aggregate.test.cjs +192 -0
  171. package/lib/metrics.cjs +120 -0
  172. package/lib/metrics.test.cjs +182 -0
  173. package/lib/model-aliases.regression.test.cjs +16 -0
  174. package/lib/model-profiles.cjs +42 -0
  175. package/lib/model-profiles.test.cjs +61 -0
  176. package/lib/next.cjs +236 -0
  177. package/lib/next.test.cjs +194 -0
  178. package/lib/phase.cjs +95 -0
  179. package/lib/phase.test.cjs +189 -0
  180. package/lib/plan-checker-contract.test.cjs +72 -0
  181. package/lib/plan-diff.cjs +173 -0
  182. package/lib/plan-diff.test.cjs +217 -0
  183. package/lib/plan.cjs +85 -0
  184. package/lib/plan.test.cjs +263 -0
  185. package/lib/progress.cjs +95 -0
  186. package/lib/progress.test.cjs +116 -0
  187. package/lib/researcher-contract.test.cjs +61 -0
  188. package/lib/roadmap-render.cjs +206 -0
  189. package/lib/roadmap-render.test.cjs +121 -0
  190. package/lib/roadmap.cjs +416 -0
  191. package/lib/roadmap.test.cjs +371 -0
  192. package/lib/runtime/_contract.test.cjs +61 -0
  193. package/lib/runtime/_readline.cjs +119 -0
  194. package/lib/runtime/_readline.test.cjs +126 -0
  195. package/lib/runtime/claude.cjs +48 -0
  196. package/lib/runtime/claude.test.cjs +101 -0
  197. package/lib/runtime/codex.cjs +35 -0
  198. package/lib/runtime/codex.test.cjs +114 -0
  199. package/lib/runtime/gemini.cjs +35 -0
  200. package/lib/runtime/gemini.test.cjs +109 -0
  201. package/lib/runtime/index.cjs +49 -0
  202. package/lib/runtime/index.test.cjs +181 -0
  203. package/lib/runtime/opencode.cjs +35 -0
  204. package/lib/runtime/opencode.test.cjs +124 -0
  205. package/lib/state.cjs +205 -0
  206. package/lib/state.test.cjs +264 -0
  207. package/lib/surface-audit.test.cjs +46 -0
  208. package/lib/tasks.cjs +327 -0
  209. package/lib/tasks.test.cjs +389 -0
  210. package/lib/template.cjs +66 -0
  211. package/lib/template.test.cjs +159 -0
  212. package/lib/undo.cjs +179 -0
  213. package/lib/undo.test.cjs +261 -0
  214. package/lib/verify.cjs +116 -0
  215. package/lib/verify.test.cjs +187 -0
  216. package/np-tools.cjs +303 -0
  217. package/package.json +39 -0
  218. package/templates/AI-SPEC.md +90 -0
  219. package/templates/CONTEXT.md +32 -0
  220. package/templates/PLAN.md +69 -0
  221. package/templates/PROJECT.md +60 -0
  222. package/templates/REQUIREMENTS.md +38 -0
  223. package/templates/SECURITY.md +61 -0
  224. package/templates/UI-SPEC.md +64 -0
  225. package/templates/VALIDATION.md +76 -0
  226. package/templates/claude/payload/README.md +11 -0
  227. package/templates/opencode/opencode.json +6 -0
  228. package/templates/opencode/payload/AGENTS.md +9 -0
  229. package/workflows/add-backlog.md +212 -0
  230. package/workflows/add-tests.md +69 -0
  231. package/workflows/add-todo.md +222 -0
  232. package/workflows/ai-integration-phase.md +230 -0
  233. package/workflows/autonomous.md +94 -0
  234. package/workflows/cleanup.md +325 -0
  235. package/workflows/code-review-fix.md +435 -0
  236. package/workflows/code-review.md +447 -0
  237. package/workflows/discuss-phase-assumptions.md +269 -0
  238. package/workflows/discuss-phase-power.md +139 -0
  239. package/workflows/discuss-phase.md +386 -0
  240. package/workflows/dispatch.md +9 -0
  241. package/workflows/doctor.md +10 -0
  242. package/workflows/eval-review.md +243 -0
  243. package/workflows/execute-phase.md +142 -0
  244. package/workflows/execute-plan.md +82 -0
  245. package/workflows/help.md +8 -0
  246. package/workflows/new-milestone.md +166 -0
  247. package/workflows/new-project.md +213 -0
  248. package/workflows/next.md +8 -0
  249. package/workflows/note.md +244 -0
  250. package/workflows/park.md +29 -0
  251. package/workflows/pause-work.md +34 -0
  252. package/workflows/plan-milestone-gaps.md +233 -0
  253. package/workflows/plan-phase.md +351 -0
  254. package/workflows/progress.md +8 -0
  255. package/workflows/queue.md +9 -0
  256. package/workflows/research-phase.md +327 -0
  257. package/workflows/reset-slice.md +39 -0
  258. package/workflows/resume-work.md +79 -0
  259. package/workflows/review.md +489 -0
  260. package/workflows/secure-phase.md +209 -0
  261. package/workflows/session-report.md +243 -0
  262. package/workflows/skip.md +29 -0
  263. package/workflows/state.md +7 -0
  264. package/workflows/stats.md +170 -0
  265. package/workflows/thread.md +214 -0
  266. package/workflows/triage.md +9 -0
  267. package/workflows/ui-phase.md +246 -0
  268. package/workflows/ui-review.md +222 -0
  269. package/workflows/undo-task.md +42 -0
  270. package/workflows/undo.md +55 -0
  271. package/workflows/unpark.md +29 -0
  272. package/workflows/validate-phase.md +231 -0
  273. package/workflows/verify-work.md +83 -0
@@ -0,0 +1,188 @@
1
+ const fs = require('node:fs');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+ const { test, afterEach } = require('node:test');
5
+ const assert = require('node:assert/strict');
6
+
7
+ const subcmd = require('./metrics.cjs');
8
+
9
+ const _sandboxes = [];
10
+
11
+ function makeSandbox() {
12
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-metrics-cli-'));
13
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
14
+ _sandboxes.push(root);
15
+ return root;
16
+ }
17
+
18
+ function captureStdio(fn) {
19
+ const outChunks = [];
20
+ const errChunks = [];
21
+ const origOut = process.stdout.write.bind(process.stdout);
22
+ const origErr = process.stderr.write.bind(process.stderr);
23
+ process.stdout.write = (c) => { outChunks.push(String(c)); return true; };
24
+ process.stderr.write = (c) => { errChunks.push(String(c)); return true; };
25
+ let rc;
26
+ try { rc = fn(); } finally {
27
+ process.stdout.write = origOut;
28
+ process.stderr.write = origErr;
29
+ }
30
+ return { stdout: outChunks.join(''), stderr: errChunks.join(''), rc };
31
+ }
32
+
33
+ function withCwd(cwd, fn) {
34
+ const orig = process.cwd();
35
+ process.chdir(cwd);
36
+ try { return fn(); } finally { process.chdir(orig); }
37
+ }
38
+
39
+ afterEach(() => {
40
+ while (_sandboxes.length) {
41
+ try { fs.rmSync(_sandboxes.pop(), { recursive: true, force: true }); } catch { }
42
+ }
43
+ });
44
+
45
+ const ISO_MS_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
46
+
47
+ test('MCLI-1: run(["now"]) / start-timestamp / end-timestamp all print ISO-ms timestamp and exit 0', () => {
48
+ for (const sub of ['now', 'start-timestamp', 'end-timestamp']) {
49
+ const cap = captureStdio(() => subcmd.run([sub]));
50
+ assert.equal(cap.rc, 0, sub + ' must return 0');
51
+ const line = cap.stdout.trim();
52
+ assert.match(line, ISO_MS_RE, sub + ' must print ISO-8601 with ms: got ' + JSON.stringify(line));
53
+ }
54
+ });
55
+
56
+ test('MCLI-2: run(record ...claude-full-argv) writes phase-09.jsonl and exits 0', () => {
57
+ const cwd = makeSandbox();
58
+ withCwd(cwd, () => {
59
+ const cap = captureStdio(() => subcmd.run([
60
+ 'record',
61
+ '--agent', 'np-executor',
62
+ '--tier', 'sonnet',
63
+ '--resolved-model', 'claude-sonnet-4-6',
64
+ '--phase', '09',
65
+ '--plan', '09-01',
66
+ '--task', '09-01-T02',
67
+ '--started', '2026-04-16T14:30:12.123Z',
68
+ '--ended', '2026-04-16T14:31:08.987Z',
69
+ '--tokens-in', '3421',
70
+ '--tokens-out', '812',
71
+ '--status', 'ok',
72
+ '--runtime', 'claude',
73
+ ]));
74
+ assert.equal(cap.rc, 0, 'stderr was: ' + cap.stderr);
75
+ const filePath = path.join(cwd, '.nubos-pilot', 'metrics', 'phase-09.jsonl');
76
+ assert.ok(fs.existsSync(filePath));
77
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8').split(os.EOL).filter(Boolean)[0]);
78
+ assert.equal(parsed.phase, '09');
79
+ assert.equal(parsed.tokens_in, 3421);
80
+ assert.equal(parsed.runtime, 'claude');
81
+ });
82
+ });
83
+
84
+ test('MCLI-3: run(record ...phase="" runtime=codex) routes to meta.jsonl with null tokens (D-09)', () => {
85
+ const cwd = makeSandbox();
86
+ withCwd(cwd, () => {
87
+ const cap = captureStdio(() => subcmd.run([
88
+ 'record',
89
+ '--agent', 'np-executor',
90
+ '--tier', 'sonnet',
91
+ '--resolved-model', 'claude-sonnet-4-6',
92
+ '--phase', '',
93
+ '--plan', '',
94
+ '--task', '',
95
+ '--started', '2026-04-16T14:30:12.123Z',
96
+ '--ended', '2026-04-16T14:31:08.987Z',
97
+ '--tokens-in', '9999',
98
+ '--tokens-out', '9999',
99
+ '--status', 'ok',
100
+ '--runtime', 'codex',
101
+ ]));
102
+ assert.equal(cap.rc, 0, 'stderr: ' + cap.stderr);
103
+ const metaPath = path.join(cwd, '.nubos-pilot', 'metrics', 'meta.jsonl');
104
+ assert.ok(fs.existsSync(metaPath));
105
+ const parsed = JSON.parse(fs.readFileSync(metaPath, 'utf-8').split(os.EOL).filter(Boolean)[0]);
106
+ assert.equal(parsed.phase, '');
107
+ assert.equal(parsed.runtime, 'codex');
108
+ assert.equal(parsed.tokens_in, null);
109
+ assert.equal(parsed.tokens_out, null);
110
+ });
111
+ });
112
+
113
+ test('MCLI-4: run(record --json @file:<path>) reads JSON blob and emits schema-identical record', () => {
114
+ const cwd = makeSandbox();
115
+ const jsonPayload = {
116
+ agent: 'np-eval-planner',
117
+ tier: 'opus',
118
+ resolved_model: 'claude-opus-4-7',
119
+ phase: '09',
120
+ plan: '09-02',
121
+ task: '09-02-T03',
122
+ started_at: '2026-04-16T14:30:12.123Z',
123
+ ended_at: '2026-04-16T14:31:08.987Z',
124
+ tokens_in: 100,
125
+ tokens_out: 50,
126
+ retry_count: 0,
127
+ status: 'error',
128
+ runtime: 'claude',
129
+ error: { code: 'eval-timeout', message: 'the agent timed out waiting for a long response' },
130
+ };
131
+ const jsonPath = path.join(cwd, 'payload.json');
132
+ fs.writeFileSync(jsonPath, JSON.stringify(jsonPayload));
133
+ withCwd(cwd, () => {
134
+ const cap = captureStdio(() => subcmd.run(['record', '--json', '@file:' + jsonPath]));
135
+ assert.equal(cap.rc, 0, 'stderr: ' + cap.stderr);
136
+ const filePath = path.join(cwd, '.nubos-pilot', 'metrics', 'phase-09.jsonl');
137
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8').split(os.EOL).filter(Boolean)[0]);
138
+ assert.equal(parsed.agent, 'np-eval-planner');
139
+ assert.equal(parsed.task, '09-02-T03');
140
+ assert.deepEqual(parsed.error, { code: 'eval-timeout', message: jsonPayload.error.message });
141
+ });
142
+ });
143
+
144
+ test('MCLI-5: run(record ...missing fields) exits 1 and writes JSON error envelope to stderr', () => {
145
+ const cwd = makeSandbox();
146
+ withCwd(cwd, () => {
147
+ const cap = captureStdio(() => subcmd.run([
148
+ 'record',
149
+ '--agent', 'np-executor',
150
+ '--tier', 'sonnet',
151
+ ]));
152
+ assert.equal(cap.rc, 1);
153
+ const parsed = JSON.parse(cap.stderr.trim());
154
+ assert.equal(parsed.code, 'metrics-invalid-record');
155
+ assert.ok(Array.isArray(parsed.details.missing));
156
+ });
157
+ });
158
+
159
+ test('MCLI-6: run(record --error-code E1 --error-message boom --status error) writes error={code,message}', () => {
160
+ const cwd = makeSandbox();
161
+ withCwd(cwd, () => {
162
+ const cap = captureStdio(() => subcmd.run([
163
+ 'record',
164
+ '--agent', 'np-executor',
165
+ '--tier', 'opus',
166
+ '--resolved-model', 'claude-opus-4-7',
167
+ '--phase', '09',
168
+ '--plan', '09-02',
169
+ '--task', '09-02-T01',
170
+ '--started', '2026-04-16T14:30:12.123Z',
171
+ '--ended', '2026-04-16T14:31:08.987Z',
172
+ '--status', 'error',
173
+ '--runtime', 'claude',
174
+ '--error-code', 'E1',
175
+ '--error-message', 'boom',
176
+ ]));
177
+ assert.equal(cap.rc, 0, 'stderr: ' + cap.stderr);
178
+ const filePath = path.join(cwd, '.nubos-pilot', 'metrics', 'phase-09.jsonl');
179
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8').split(os.EOL).filter(Boolean)[0]);
180
+ assert.deepEqual(parsed.error, { code: 'E1', message: 'boom' });
181
+ });
182
+ });
183
+
184
+ test('MCLI-7: run(["unknown-subcommand"]) exits 1 with usage on stderr', () => {
185
+ const cap = captureStdio(() => subcmd.run(['unknown-subcommand']));
186
+ assert.equal(cap.rc, 1);
187
+ assert.match(cap.stderr, /Usage/i);
188
+ });
@@ -0,0 +1,288 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const {
5
+ NubosPilotError,
6
+ atomicWriteFileSync,
7
+ projectStateDir,
8
+ } = require('../../lib/core.cjs');
9
+ const { addMilestone, addPhase, parseRoadmap } = require('../../lib/roadmap.cjs');
10
+ const { createPhaseDir, phaseSlug } = require('../../lib/phase.cjs');
11
+ const { mutateState } = require('../../lib/state.cjs');
12
+
13
+ const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates');
14
+ const PLACEHOLDER_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
15
+
16
+ function _render(raw, vars, templateName) {
17
+ return raw.replace(PLACEHOLDER_RE, (_match, key) => {
18
+ if (!(key in vars)) {
19
+ throw new NubosPilotError(
20
+ 'template-unresolved-var',
21
+ `Undefined placeholder {{${key}}} in template "${templateName}"`,
22
+ { template: templateName, variable: key, available: Object.keys(vars) },
23
+ );
24
+ }
25
+ return String(vars[key]);
26
+ });
27
+ }
28
+
29
+ function _loadTemplate(name) {
30
+ return fs.readFileSync(path.join(TEMPLATES_DIR, name + '.md'), 'utf-8');
31
+ }
32
+
33
+ function _slugify(raw) {
34
+ return String(raw || '')
35
+ .toLowerCase()
36
+ .replace(/[^a-z0-9]+/g, '-')
37
+ .replace(/^-+|-+$/g, '');
38
+ }
39
+
40
+ function _writeFile(target, content) {
41
+
42
+
43
+
44
+ if (path.basename(target) === 'PROJECT.md') {
45
+ throw new NubosPilotError(
46
+ 'new-milestone-forbidden-write',
47
+ 'new-milestone.cjs is never allowed to write PROJECT.md (D-29)',
48
+ { path: target },
49
+ );
50
+ }
51
+ atomicWriteFileSync(target, content);
52
+ }
53
+
54
+ function _emit(stdout, payload) {
55
+ stdout.write(JSON.stringify(payload, null, 2));
56
+ }
57
+
58
+ function _interviewPayload() {
59
+ return {
60
+ mode: 'interview',
61
+ questions: [
62
+ { key: 'milestone_name', type: 'input',
63
+ question: 'Milestone name (e.g. "v2.0")?' },
64
+ { key: 'milestone_goal', type: 'input',
65
+ question: 'Milestone goal — one sentence describing what ships in this milestone?' },
66
+ { key: 'first_phase_name', type: 'input',
67
+ question: 'First phase name for this milestone?' },
68
+ { key: 'create_req_prefix', type: 'confirm',
69
+ question: 'Create a new "## <milestone> Requirements" section in REQUIREMENTS.md?' },
70
+ ],
71
+ };
72
+ }
73
+
74
+ function _validateAnswers(a) {
75
+ for (const key of ['milestone_name', 'milestone_goal', 'first_phase_name']) {
76
+ if (typeof a[key] !== 'string' || a[key].trim() === '') {
77
+ throw new NubosPilotError(
78
+ 'answers-missing-field',
79
+ 'answers JSON missing field: ' + key,
80
+ { field: key },
81
+ );
82
+ }
83
+ }
84
+ if ('create_req_prefix' in a && typeof a.create_req_prefix !== 'boolean') {
85
+ throw new NubosPilotError(
86
+ 'answers-invalid-field',
87
+ 'create_req_prefix must be a boolean',
88
+ { field: 'create_req_prefix', value: a.create_req_prefix },
89
+ );
90
+ }
91
+ }
92
+
93
+ function _guardInitialized(root) {
94
+ const projectMd = path.join(root, '.nubos-pilot', 'PROJECT.md');
95
+ if (!fs.existsSync(projectMd)) {
96
+ throw new NubosPilotError(
97
+ 'project-not-initialized',
98
+ 'PROJECT.md not found — run np:new-project first',
99
+ { hint: 'Run np:new-project first', path: projectMd },
100
+ );
101
+ }
102
+ }
103
+
104
+ function _appendReqPrefix(root, milestoneName) {
105
+ const reqPath = path.join(root, '.nubos-pilot', 'REQUIREMENTS.md');
106
+ const current = fs.readFileSync(reqPath, 'utf-8');
107
+
108
+ const header = `\n## ${milestoneName} Requirements\n\n<!-- TBD: first requirement -->\n- [ ] **REQ-TBD**: TBD\n`;
109
+ let next;
110
+ const marker = '\n## Out of Scope';
111
+ const idx = current.indexOf(marker);
112
+ if (idx >= 0) {
113
+ next = current.slice(0, idx) + header + current.slice(idx);
114
+ } else {
115
+ next = current.endsWith('\n') ? current + header : current + '\n' + header;
116
+ }
117
+ _writeFile(reqPath, next);
118
+ }
119
+
120
+ function _apply(answersPath, cwd, stdout) {
121
+ let raw;
122
+ try {
123
+ raw = fs.readFileSync(answersPath, 'utf-8');
124
+ } catch (err) {
125
+ throw new NubosPilotError(
126
+ 'answers-not-readable',
127
+ 'answers file not readable: ' + answersPath,
128
+ { path: answersPath, cause: err && err.code },
129
+ );
130
+ }
131
+ let answers;
132
+ try {
133
+ answers = JSON.parse(raw);
134
+ } catch (err) {
135
+ throw new NubosPilotError(
136
+ 'answers-parse-error',
137
+ 'answers file is not valid JSON',
138
+ { path: answersPath, cause: err && err.message },
139
+ );
140
+ }
141
+ _validateAnswers(answers);
142
+
143
+ const root = path.resolve(cwd);
144
+ _guardInitialized(root);
145
+
146
+ const milestoneId = _slugify(answers.milestone_name);
147
+ if (milestoneId === '') {
148
+ throw new NubosPilotError(
149
+ 'invalid-slug',
150
+ 'milestone_name slugifies to empty string',
151
+ { value: answers.milestone_name, field: 'milestone_name' },
152
+ );
153
+ }
154
+ const firstPhaseSlug = phaseSlug(answers.first_phase_name);
155
+ if (firstPhaseSlug === '') {
156
+ throw new NubosPilotError(
157
+ 'invalid-slug',
158
+ 'first_phase_name slugifies to empty string',
159
+ { value: answers.first_phase_name, field: 'first_phase_name' },
160
+ );
161
+ }
162
+
163
+
164
+
165
+
166
+
167
+
168
+
169
+ const { phases: existingPhases } = parseRoadmap(root);
170
+ let globalMax = 0;
171
+ for (const p of existingPhases) {
172
+ const n = Number(p.number);
173
+ if (Number.isInteger(n) && n > globalMax) globalMax = n;
174
+ }
175
+ const nextPhaseNumber = globalMax + 1;
176
+
177
+
178
+
179
+
180
+
181
+
182
+
183
+ addMilestone(
184
+ {
185
+ id: milestoneId,
186
+ name: answers.milestone_name,
187
+ phases: [
188
+ {
189
+ number: nextPhaseNumber,
190
+ slug: firstPhaseSlug,
191
+ name: answers.first_phase_name,
192
+ goal: answers.milestone_goal,
193
+ depends_on: [],
194
+ requirements: [],
195
+ success_criteria: [],
196
+ status: 'pending',
197
+ plans: [],
198
+ },
199
+ ],
200
+ },
201
+ root,
202
+ );
203
+ const phaseResult = {
204
+ milestoneId,
205
+ number: nextPhaseNumber,
206
+ slug: firstPhaseSlug,
207
+ };
208
+
209
+
210
+
211
+ void addPhase;
212
+
213
+
214
+
215
+ const phaseDir = createPhaseDir(phaseResult.number, firstPhaseSlug, root);
216
+ const padded = String(phaseResult.number).padStart(2, '0');
217
+ const ctxVars = {
218
+ phase_number: String(phaseResult.number),
219
+ phase_name: answers.first_phase_name,
220
+ phase_padded: padded,
221
+ phase_slug: firstPhaseSlug,
222
+ created_date: new Date().toISOString().slice(0, 10),
223
+ domain_text: '<!-- TBD: phase boundary -->',
224
+ decisions_text: '<!-- TBD: decisions -->',
225
+ canonical_refs_text: '<!-- TBD: canonical references -->',
226
+ code_context_text: '<!-- TBD: existing code insights -->',
227
+ specifics_text: '<!-- TBD: specific ideas / references -->',
228
+ deferred_text: '<!-- TBD: deferred ideas -->',
229
+ };
230
+ const contextMdPath = path.join(phaseDir, padded + '-CONTEXT.md');
231
+ _writeFile(contextMdPath, _render(_loadTemplate('CONTEXT'), ctxVars, 'CONTEXT'));
232
+
233
+
234
+
235
+ if (answers.create_req_prefix === true) {
236
+ _appendReqPrefix(root, answers.milestone_name);
237
+ }
238
+
239
+
240
+
241
+ mutateState((state) => {
242
+ const fm = Object.assign({}, state.frontmatter, {
243
+ milestone: milestoneId,
244
+ milestone_name: answers.milestone_name,
245
+ current_phase: Number(phaseResult.number),
246
+ current_plan: null,
247
+ current_task: null,
248
+ last_updated: new Date().toISOString(),
249
+ });
250
+ return { frontmatter: fm, body: state.body };
251
+ }, root);
252
+
253
+
254
+ projectStateDir(root);
255
+
256
+ _emit(stdout, {
257
+ mode: 'apply',
258
+ milestoneId,
259
+ phaseNumber: phaseResult.number,
260
+ phaseSlug: firstPhaseSlug,
261
+ created_req_prefix: answers.create_req_prefix === true,
262
+ });
263
+ }
264
+
265
+ function run(args, ctx) {
266
+ const context = ctx || {};
267
+ const cwd = context.cwd || process.cwd();
268
+ const stdout = context.stdout || process.stdout;
269
+ const argv = args || [];
270
+
271
+ const applyIdx = argv.indexOf('--apply');
272
+ if (applyIdx >= 0) {
273
+ const answersPath = argv[applyIdx + 1];
274
+ if (!answersPath) {
275
+ throw new NubosPilotError(
276
+ 'missing-apply-path',
277
+ '--apply requires a path to the answers JSON file',
278
+ { args: argv.slice() },
279
+ );
280
+ }
281
+ _apply(answersPath, cwd, stdout);
282
+ return;
283
+ }
284
+
285
+ _emit(stdout, _interviewPayload());
286
+ }
287
+
288
+ module.exports = { run, _interviewPayload };
@@ -0,0 +1,166 @@
1
+ const { test, afterEach } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const crypto = require('node:crypto');
7
+
8
+ const newProject = require('./new-project.cjs');
9
+ const subcmd = require('./new-milestone.cjs');
10
+
11
+ const _sandboxes = [];
12
+
13
+ function makeEmptySandbox() {
14
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-newms-'));
15
+ _sandboxes.push(root);
16
+ return root;
17
+ }
18
+
19
+ afterEach(() => {
20
+ while (_sandboxes.length) {
21
+ const p = _sandboxes.pop();
22
+ try { fs.rmSync(p, { recursive: true, force: true }); } catch { }
23
+ }
24
+ });
25
+
26
+ function _captureStdout() {
27
+ let buf = '';
28
+ const stub = { write: (s) => { buf += s; return true; } };
29
+ return { stub, get: () => buf };
30
+ }
31
+
32
+ function _seedInitializedProject(root) {
33
+ const answers = {
34
+ project_name: 'Demo',
35
+ core_value: 'ship',
36
+ primary_constraints: 'nodejs',
37
+ first_milestone_name: 'v1.0',
38
+ first_phase_name: 'First Phase',
39
+ };
40
+ const p = path.join(root, 'init-answers.json');
41
+ fs.writeFileSync(p, JSON.stringify(answers), 'utf-8');
42
+ newProject.run(['--apply', p], { cwd: root, stdout: _captureStdout().stub });
43
+ fs.unlinkSync(p);
44
+ }
45
+
46
+ function _writeAnswers(root, answers, name) {
47
+ const p = path.join(root, (name || 'ms-answers') + '.json');
48
+ fs.writeFileSync(p, JSON.stringify(answers), 'utf-8');
49
+ return p;
50
+ }
51
+
52
+ function _baseMsAnswers() {
53
+ return {
54
+ milestone_name: 'v2.0',
55
+ milestone_goal: 'second milestone',
56
+ first_phase_name: 'Second Phase',
57
+ create_req_prefix: false,
58
+ };
59
+ }
60
+
61
+ test('NM-1: run([]) emits interview JSON with 4 questions', () => {
62
+ const sandbox = makeEmptySandbox();
63
+ const cap = _captureStdout();
64
+ subcmd.run([], { cwd: sandbox, stdout: cap.stub });
65
+ const payload = JSON.parse(cap.get().trim());
66
+ assert.equal(payload.mode, 'interview');
67
+ const keys = payload.questions.map((q) => q.key);
68
+ for (const expected of [
69
+ 'milestone_name',
70
+ 'milestone_goal',
71
+ 'first_phase_name',
72
+ 'create_req_prefix',
73
+ ]) {
74
+ assert.ok(keys.includes(expected), 'interview missing ' + expected);
75
+ }
76
+ });
77
+
78
+ test('NM-2: --apply without PROJECT.md throws project-not-initialized', () => {
79
+ const sandbox = makeEmptySandbox();
80
+ const answersPath = _writeAnswers(sandbox, _baseMsAnswers());
81
+ assert.throws(
82
+ () => subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub }),
83
+ (err) => err.name === 'NubosPilotError' && err.code === 'project-not-initialized',
84
+ );
85
+ });
86
+
87
+ test('NM-3: --apply on initialized project appends milestone + phase', () => {
88
+ const sandbox = makeEmptySandbox();
89
+ _seedInitializedProject(sandbox);
90
+ const answersPath = _writeAnswers(sandbox, _baseMsAnswers());
91
+ subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub });
92
+ const YAML = require('yaml');
93
+ const rm = YAML.parse(fs.readFileSync(path.join(sandbox, '.nubos-pilot', 'roadmap.yaml'), 'utf-8'));
94
+ assert.equal(rm.milestones.length, 2, 'roadmap.yaml should have 2 milestones');
95
+ assert.equal(rm.milestones[1].id, 'v2-0');
96
+ assert.equal(rm.milestones[1].phases.length, 1);
97
+ });
98
+
99
+ test('NM-4: --apply does NOT touch PROJECT.md (byte-equal before/after) — D-29', () => {
100
+ const sandbox = makeEmptySandbox();
101
+ _seedInitializedProject(sandbox);
102
+ const projectMdPath = path.join(sandbox, '.nubos-pilot', 'PROJECT.md');
103
+ const beforeBytes = fs.readFileSync(projectMdPath);
104
+ const beforeHash = crypto.createHash('sha256').update(beforeBytes).digest('hex');
105
+
106
+ const answersPath = _writeAnswers(sandbox, _baseMsAnswers());
107
+ subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub });
108
+
109
+ const afterBytes = fs.readFileSync(projectMdPath);
110
+ const afterHash = crypto.createHash('sha256').update(afterBytes).digest('hex');
111
+ assert.equal(afterHash, beforeHash, 'PROJECT.md bytes changed — D-29 violation');
112
+ });
113
+
114
+ test('NM-5: duplicate milestone_name throws roadmap-duplicate-milestone', () => {
115
+ const sandbox = makeEmptySandbox();
116
+ _seedInitializedProject(sandbox);
117
+
118
+
119
+ const answers = Object.assign({}, _baseMsAnswers(), { milestone_name: 'v1.0' });
120
+ const answersPath = _writeAnswers(sandbox, answers);
121
+ assert.throws(
122
+ () => subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub }),
123
+ (err) => err.name === 'NubosPilotError' && err.code === 'roadmap-duplicate-milestone',
124
+ );
125
+ });
126
+
127
+ test('NM-6: create_req_prefix=true appends H2 section to REQUIREMENTS.md', () => {
128
+ const sandbox = makeEmptySandbox();
129
+ _seedInitializedProject(sandbox);
130
+ const reqPath = path.join(sandbox, '.nubos-pilot', 'REQUIREMENTS.md');
131
+ const before = fs.readFileSync(reqPath, 'utf-8');
132
+ assert.doesNotMatch(before, /## v2\.0 Requirements/);
133
+
134
+ const answers = Object.assign({}, _baseMsAnswers(), { create_req_prefix: true });
135
+ const answersPath = _writeAnswers(sandbox, answers);
136
+ subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub });
137
+
138
+ const after = fs.readFileSync(reqPath, 'utf-8');
139
+ assert.match(after, /## v2\.0 Requirements/);
140
+ });
141
+
142
+ test('NM-7: create_req_prefix=false leaves REQUIREMENTS.md byte-equal', () => {
143
+ const sandbox = makeEmptySandbox();
144
+ _seedInitializedProject(sandbox);
145
+ const reqPath = path.join(sandbox, '.nubos-pilot', 'REQUIREMENTS.md');
146
+ const beforeHash = crypto.createHash('sha256').update(fs.readFileSync(reqPath)).digest('hex');
147
+
148
+ const answersPath = _writeAnswers(sandbox, _baseMsAnswers());
149
+ subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub });
150
+
151
+ const afterHash = crypto.createHash('sha256').update(fs.readFileSync(reqPath)).digest('hex');
152
+ assert.equal(afterHash, beforeHash);
153
+ });
154
+
155
+ test('NM-8: STATE.md milestone pointer advances to new milestone id', () => {
156
+ const sandbox = makeEmptySandbox();
157
+ _seedInitializedProject(sandbox);
158
+ const answersPath = _writeAnswers(sandbox, _baseMsAnswers());
159
+ subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub });
160
+ const { readState } = require('../../lib/state.cjs');
161
+ const st = readState(sandbox);
162
+ assert.equal(st.frontmatter.milestone, 'v2-0');
163
+
164
+ assert.equal(typeof st.frontmatter.current_phase, 'number');
165
+ assert.ok(st.frontmatter.current_phase >= 1);
166
+ });