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,242 @@
1
+ const { test, afterEach } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { makeSandbox, seedPhaseDir, cleanupAll } =
7
+ require('../../tests/helpers/fixture.cjs');
8
+
9
+ const subcmd = require('./discuss-phase-power.cjs');
10
+
11
+ const SAMPLE_TEMPLATE =
12
+ '# Phase {{phase}} Context (padded={{padded}})\n\n' +
13
+ '## Domain\n{{domain_text}}\n\n' +
14
+ '## Decisions\n{{decisions_text}}\n\n' +
15
+ '## Canonical References\n{{canonical_refs_text}}\n\n' +
16
+ '## Code Context\n{{code_context_text}}\n\n' +
17
+ '## Specifics\n{{specifics_text}}\n\n' +
18
+ '## Deferred\n{{deferred_text}}\n';
19
+
20
+ function _seedTemplate(sandbox) {
21
+ const dir = path.join(sandbox, '.nubos-pilot', 'templates');
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ fs.writeFileSync(path.join(dir, 'CONTEXT.md'), SAMPLE_TEMPLATE, 'utf-8');
24
+ }
25
+
26
+ function _capture() {
27
+ let buf = '';
28
+ const stub = { write: (s) => { buf += s; return true; } };
29
+ return { stub, get: () => buf };
30
+ }
31
+
32
+ afterEach(cleanupAll);
33
+
34
+ test('P1: init without prior QUESTIONS.json creates file with schema', () => {
35
+ const sandbox = makeSandbox();
36
+ seedPhaseDir(sandbox, 5, 'planning-workflows-agents');
37
+ _seedTemplate(sandbox);
38
+ const cap = _capture();
39
+ subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap.stub });
40
+ const qpath = path.join(
41
+ sandbox, '.nubos-pilot', 'phases', '05-planning-workflows-agents',
42
+ '05-QUESTIONS.json'
43
+ );
44
+ assert.ok(fs.existsSync(qpath), 'QUESTIONS.json created');
45
+ const doc = JSON.parse(fs.readFileSync(qpath, 'utf-8'));
46
+ assert.equal(doc.phase, 5);
47
+ assert.equal(doc.padded, '05');
48
+ assert.equal(doc.mode, 'power');
49
+ assert.equal(typeof doc.created, 'string');
50
+ assert.ok(Array.isArray(doc.questions));
51
+ assert.equal(doc.answers_status, 'pending');
52
+
53
+ const areas = new Set(doc.questions.map((q) => q.area));
54
+ for (const a of ['domain', 'decisions', 'canonical_refs', 'code_context', 'specifics', 'deferred']) {
55
+ assert.ok(areas.has(a), 'area covered: ' + a);
56
+ }
57
+ });
58
+
59
+ test('P2: init when QUESTIONS.json already exists throws power-questions-exist', () => {
60
+ const sandbox = makeSandbox();
61
+ const phaseDir = seedPhaseDir(sandbox, 5, 'planning-workflows-agents', {
62
+ '05-QUESTIONS.json': '{}',
63
+ });
64
+ const cap = _capture();
65
+ assert.throws(
66
+ () => subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap.stub }),
67
+ (err) => err.name === 'NubosPilotError' && err.code === 'power-questions-exist'
68
+ && err.details && err.details.path && err.details.path.includes(phaseDir),
69
+ );
70
+ });
71
+
72
+ test('P3: refresh emits stats JSON with totals per area', () => {
73
+ const sandbox = makeSandbox();
74
+ seedPhaseDir(sandbox, 5, 'planning-workflows-agents');
75
+ _seedTemplate(sandbox);
76
+ const cap0 = _capture();
77
+ subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap0.stub });
78
+ const qpath = path.join(
79
+ sandbox, '.nubos-pilot', 'phases', '05-planning-workflows-agents',
80
+ '05-QUESTIONS.json'
81
+ );
82
+
83
+ const doc = JSON.parse(fs.readFileSync(qpath, 'utf-8'));
84
+ const dq = doc.questions.find((q) => q.area === 'domain');
85
+ dq.answer = 'scoped';
86
+ fs.writeFileSync(qpath, JSON.stringify(doc, null, 2), 'utf-8');
87
+
88
+ const cap = _capture();
89
+ subcmd.run(['refresh', '5'], { cwd: sandbox, stdout: cap.stub });
90
+ const stats = JSON.parse(cap.get().trim());
91
+ assert.equal(typeof stats.total_questions, 'number');
92
+ assert.ok(stats.answered >= 1);
93
+ assert.ok(stats.pending >= 0);
94
+ assert.ok(stats.areas && typeof stats.areas === 'object');
95
+ assert.ok(stats.areas.domain && typeof stats.areas.domain.total === 'number');
96
+ assert.ok(typeof stats.areas.domain.answered === 'number');
97
+
98
+ const ctxPath = path.join(
99
+ sandbox, '.nubos-pilot', 'phases', '05-planning-workflows-agents',
100
+ '05-CONTEXT.md'
101
+ );
102
+ assert.ok(!fs.existsSync(ctxPath), 'refresh does not touch CONTEXT.md');
103
+ });
104
+
105
+ test('P4: finalize with all answers renders CONTEXT.md and marks finalized', () => {
106
+ const sandbox = makeSandbox();
107
+ seedPhaseDir(sandbox, 5, 'planning-workflows-agents');
108
+ _seedTemplate(sandbox);
109
+
110
+ const qpath = path.join(
111
+ sandbox, '.nubos-pilot', 'phases', '05-planning-workflows-agents',
112
+ '05-QUESTIONS.json'
113
+ );
114
+ const fixture = fs.readFileSync(
115
+ path.join(__dirname, '..', '..', 'tests', 'fixtures', 'discuss', 'power-questions-sample.json'),
116
+ 'utf-8'
117
+ );
118
+ fs.writeFileSync(qpath, fixture, 'utf-8');
119
+
120
+ const cap = _capture();
121
+ subcmd.run(['finalize', '5'], { cwd: sandbox, stdout: cap.stub });
122
+
123
+ const ctxPath = path.join(
124
+ sandbox, '.nubos-pilot', 'phases', '05-planning-workflows-agents',
125
+ '05-CONTEXT.md'
126
+ );
127
+ assert.ok(fs.existsSync(ctxPath), 'CONTEXT.md rendered');
128
+ const ctx = fs.readFileSync(ctxPath, 'utf-8');
129
+ assert.ok(ctx.includes('Phase 5'), 'phase header present');
130
+ assert.ok(ctx.includes('power-mode file-UI'), 'domain answer substring');
131
+ assert.ok(ctx.includes('No HTML companion'), 'decisions answer substring');
132
+
133
+ const doc = JSON.parse(fs.readFileSync(qpath, 'utf-8'));
134
+ assert.equal(doc.answers_status, 'finalized');
135
+ });
136
+
137
+ test('P5: finalize with pending answers throws power-finalize-incomplete', () => {
138
+ const sandbox = makeSandbox();
139
+ seedPhaseDir(sandbox, 5, 'planning-workflows-agents');
140
+ _seedTemplate(sandbox);
141
+ const cap0 = _capture();
142
+ subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap0.stub });
143
+ assert.throws(
144
+ () => subcmd.run(['finalize', '5'], { cwd: sandbox, stdout: _capture().stub }),
145
+ (err) => err.name === 'NubosPilotError'
146
+ && err.code === 'power-finalize-incomplete'
147
+ && err.details && Array.isArray(err.details.pending_ids)
148
+ && err.details.pending_ids.length > 0,
149
+ );
150
+ });
151
+
152
+ test('P6: explain returns question object with explain field populated', () => {
153
+ const sandbox = makeSandbox();
154
+ seedPhaseDir(sandbox, 5, 'planning-workflows-agents');
155
+ _seedTemplate(sandbox);
156
+ const cap0 = _capture();
157
+ subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap0.stub });
158
+
159
+ const cap = _capture();
160
+ subcmd.run(['explain', '5', 'Q-01'], { cwd: sandbox, stdout: cap.stub });
161
+ const body = JSON.parse(cap.get().trim());
162
+ assert.equal(body.id, 'Q-01');
163
+ assert.ok(typeof body.explain === 'string' && body.explain.length > 0);
164
+ });
165
+
166
+ test('P7: exit emits status JSON; does NOT delete QUESTIONS.json', () => {
167
+ const sandbox = makeSandbox();
168
+ seedPhaseDir(sandbox, 5, 'planning-workflows-agents');
169
+ _seedTemplate(sandbox);
170
+ const cap0 = _capture();
171
+ subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap0.stub });
172
+ const qpath = path.join(
173
+ sandbox, '.nubos-pilot', 'phases', '05-planning-workflows-agents',
174
+ '05-QUESTIONS.json'
175
+ );
176
+
177
+ const cap = _capture();
178
+ subcmd.run(['exit', '5'], { cwd: sandbox, stdout: cap.stub });
179
+ const body = JSON.parse(cap.get().trim());
180
+ assert.equal(body.status, 'exited');
181
+ assert.ok(fs.existsSync(qpath), 'QUESTIONS.json preserved after exit');
182
+ });
183
+
184
+ test('P8: HTML-ban regression — no HTML markers in source file', () => {
185
+ const src = fs.readFileSync(path.join(__dirname, 'discuss-phase-power.cjs'), 'utf-8');
186
+ const banned = [/<html/, /<!DOCTYPE/i, /showOpenFilePicker/, /FileSystemFileHandle/, /document\./];
187
+ for (const re of banned) {
188
+ assert.ok(!re.test(src), 'banned HTML marker found: ' + re);
189
+ }
190
+ });
191
+
192
+ test('P9: writes use atomicWriteFileSync', () => {
193
+ const src = fs.readFileSync(path.join(__dirname, 'discuss-phase-power.cjs'), 'utf-8');
194
+ const occurrences = (src.match(/atomicWriteFileSync/g) || []).length;
195
+ assert.ok(occurrences >= 2, 'atomicWriteFileSync used in ≥2 places, got ' + occurrences);
196
+ });
197
+
198
+ test('P10: concurrent refresh+finalize does not corrupt QUESTIONS.json', async () => {
199
+ const sandbox = makeSandbox();
200
+ seedPhaseDir(sandbox, 5, 'planning-workflows-agents');
201
+ _seedTemplate(sandbox);
202
+
203
+ const qpath = path.join(
204
+ sandbox, '.nubos-pilot', 'phases', '05-planning-workflows-agents',
205
+ '05-QUESTIONS.json'
206
+ );
207
+ const fixture = fs.readFileSync(
208
+ path.join(__dirname, '..', '..', 'tests', 'fixtures', 'discuss', 'power-questions-sample.json'),
209
+ 'utf-8'
210
+ );
211
+ fs.writeFileSync(qpath, fixture, 'utf-8');
212
+
213
+ const results = await Promise.allSettled([
214
+ Promise.resolve().then(() => subcmd.run(['refresh', '5'], { cwd: sandbox, stdout: _capture().stub })),
215
+ Promise.resolve().then(() => subcmd.run(['finalize', '5'], { cwd: sandbox, stdout: _capture().stub })),
216
+ ]);
217
+
218
+ const fulfilled = results.filter((r) => r.status === 'fulfilled').length;
219
+ assert.ok(fulfilled >= 1);
220
+ const doc = JSON.parse(fs.readFileSync(qpath, 'utf-8'));
221
+ assert.equal(doc.phase, 5);
222
+ assert.ok(Array.isArray(doc.questions));
223
+ });
224
+
225
+ test('P11: init + explain full round-trip preserves question set', () => {
226
+ const sandbox = makeSandbox();
227
+ seedPhaseDir(sandbox, 5, 'planning-workflows-agents');
228
+ _seedTemplate(sandbox);
229
+ const cap0 = _capture();
230
+ subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap0.stub });
231
+ const qpath = path.join(
232
+ sandbox, '.nubos-pilot', 'phases', '05-planning-workflows-agents',
233
+ '05-QUESTIONS.json'
234
+ );
235
+ const before = JSON.parse(fs.readFileSync(qpath, 'utf-8')).questions.length;
236
+
237
+ const cap = _capture();
238
+ subcmd.run(['explain', '5', 'Q-01'], { cwd: sandbox, stdout: cap.stub });
239
+
240
+ const after = JSON.parse(fs.readFileSync(qpath, 'utf-8')).questions.length;
241
+ assert.equal(before, after, 'explain does not add/remove questions');
242
+ });
@@ -0,0 +1,132 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const os = require('node:os');
4
+ const crypto = require('node:crypto');
5
+
6
+ const { NubosPilotError, projectStateDir } = require('../../lib/core.cjs');
7
+ const { getPhase } = require('../../lib/roadmap.cjs');
8
+ const { paddedPhase, phaseSlug, findPhaseDir } = require('../../lib/phase.cjs');
9
+
10
+ const INLINE_THRESHOLD_BYTES = 16 * 1024;
11
+
12
+ function _parseArgs(args) {
13
+ const rest = [];
14
+ const flags = { assumptions: false };
15
+ for (const a of args || []) {
16
+ if (a === '--assumptions') flags.assumptions = true;
17
+ else rest.push(a);
18
+ }
19
+ return { positional: rest, flags };
20
+ }
21
+
22
+ function _validatePhaseArg(raw) {
23
+ if (raw == null || raw === '') {
24
+ throw new NubosPilotError(
25
+ 'discuss-invalid-phase-arg',
26
+ 'discuss-phase requires a phase number (integer or decimal like 7.1)',
27
+ { value: raw == null ? '' : String(raw) },
28
+ );
29
+ }
30
+ const s = String(raw);
31
+ const integerOk = /^\d+$/.test(s);
32
+ const decimalOk = /^\d+\.\d+$/.test(s);
33
+ if (!integerOk && !decimalOk) {
34
+ throw new NubosPilotError(
35
+ 'discuss-invalid-phase-arg',
36
+ 'Invalid phase number: ' + s,
37
+ { value: s },
38
+ );
39
+ }
40
+ return s;
41
+ }
42
+
43
+ function _agentSkills() {
44
+ try {
45
+ const agents = require('../../lib/agents.cjs');
46
+ if (typeof agents.getAgentSkills === 'function') {
47
+ return { planner: agents.getAgentSkills('np-planner') };
48
+ }
49
+ } catch (_err) { }
50
+ return { planner: null };
51
+ }
52
+
53
+ function _resolvePhaseDir(n, cwd, slug) {
54
+ const hit = findPhaseDir(n, cwd);
55
+ if (hit) return hit;
56
+
57
+
58
+
59
+ const padded = paddedPhase(n);
60
+ let stateDir;
61
+ try { stateDir = projectStateDir(cwd); } catch (_err) {
62
+ stateDir = path.join(path.resolve(cwd), '.nubos-pilot');
63
+ }
64
+ return path.join(stateDir, 'phases', padded + '-' + slug);
65
+ }
66
+
67
+ function _emit(payload, stdout, cwd) {
68
+ const json = JSON.stringify(payload, null, 2);
69
+ if (Buffer.byteLength(json, 'utf-8') <= INLINE_THRESHOLD_BYTES) {
70
+ stdout.write(json);
71
+ return;
72
+ }
73
+ let tmpDir;
74
+ try {
75
+ tmpDir = path.join(projectStateDir(cwd), '.tmp');
76
+ fs.mkdirSync(tmpDir, { recursive: true });
77
+ } catch (_err) {
78
+ tmpDir = os.tmpdir();
79
+ }
80
+ const suffix = process.pid + '-' + crypto.randomBytes(4).toString('hex');
81
+ const tmpPath = path.join(tmpDir, 'init-discuss-phase-' + suffix + '.json');
82
+ fs.writeFileSync(tmpPath, json, 'utf-8');
83
+ stdout.write('@file:' + tmpPath);
84
+ }
85
+
86
+ function run(args, ctx) {
87
+ const context = ctx || {};
88
+ const cwd = context.cwd || process.cwd();
89
+ const stdout = context.stdout || process.stdout;
90
+ const { positional, flags } = _parseArgs(args);
91
+ const phaseArg = _validatePhaseArg(positional[0]);
92
+
93
+ let phase;
94
+ try {
95
+ phase = getPhase(phaseArg, cwd);
96
+ } catch (err) {
97
+ if (err && err.code === 'phase-not-found') {
98
+ throw new NubosPilotError(
99
+ 'discuss-phase-not-found',
100
+ 'Phase ' + phaseArg + ' not found in roadmap.yaml',
101
+ { number: phaseArg },
102
+ );
103
+ }
104
+ throw err;
105
+ }
106
+
107
+ const padded = paddedPhase(phaseArg);
108
+ const slug = phase.slug || phaseSlug(phase.name);
109
+ const phase_dir = _resolvePhaseDir(phaseArg, cwd, slug);
110
+ const contextPath = path.join(phase_dir, padded + '-CONTEXT.md');
111
+ const has_context = fs.existsSync(contextPath);
112
+
113
+ const payload = {
114
+ _workflow: 'discuss-phase',
115
+ phase_number: phaseArg,
116
+ padded,
117
+ phase_dir,
118
+ phase_name: phase.name,
119
+ phase_slug: slug,
120
+ has_context,
121
+ goal: phase.goal || '',
122
+ requirements: Array.isArray(phase.requirements) ? phase.requirements : [],
123
+ success_criteria: Array.isArray(phase.success_criteria) ? phase.success_criteria : [],
124
+ mode: flags.assumptions ? 'assumptions' : 'adaptive',
125
+ agent_skills: _agentSkills(),
126
+ };
127
+
128
+ _emit(payload, stdout, cwd);
129
+ return payload;
130
+ }
131
+
132
+ module.exports = { run, _parseArgs, _validatePhaseArg, INLINE_THRESHOLD_BYTES };
@@ -0,0 +1,148 @@
1
+ const { test, afterEach } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { makeSandbox, seedRoadmapYaml, seedPhaseDir, cleanupAll } =
7
+ require('../../tests/helpers/fixture.cjs');
8
+ const subcmd = require('./discuss-phase.cjs');
9
+
10
+ function _baseRoadmap() {
11
+ return {
12
+ schema_version: 1,
13
+ milestones: [
14
+ {
15
+ id: 'v1.0',
16
+ name: 'first',
17
+ phases: [
18
+ {
19
+ number: 3,
20
+ name: 'Observability',
21
+ slug: 'observability',
22
+ goal: 'Ship structured logging + metrics',
23
+ depends_on: [],
24
+ requirements: ['OBS-01'],
25
+ success_criteria: ['Logs emit JSON'],
26
+ status: 'planned',
27
+ plans: [],
28
+ },
29
+ {
30
+ number: 7,
31
+ name: 'Seven',
32
+ slug: 'seven',
33
+ goal: 'Phase seven goal',
34
+ depends_on: [],
35
+ requirements: [],
36
+ success_criteria: [],
37
+ status: 'planned',
38
+ plans: [],
39
+ },
40
+ ],
41
+ },
42
+ ],
43
+ };
44
+ }
45
+
46
+ function _captureStdout() {
47
+ let buf = '';
48
+ const stub = { write: (s) => { buf += s; return true; } };
49
+ return { stub, get: () => buf };
50
+ }
51
+
52
+ afterEach(cleanupAll);
53
+
54
+ test('DP-1: run(["3"]) on valid phase returns JSON payload with expected shape', () => {
55
+ const sandbox = makeSandbox();
56
+ seedRoadmapYaml(sandbox, _baseRoadmap());
57
+ seedPhaseDir(sandbox, 3, 'observability', {});
58
+ const cap = _captureStdout();
59
+ subcmd.run(['3'], { cwd: sandbox, stdout: cap.stub });
60
+ const raw = cap.get().trim();
61
+ assert.ok(!raw.startsWith('@file:'));
62
+ const payload = JSON.parse(raw);
63
+ assert.equal(payload.phase_number, '3');
64
+ assert.equal(payload.padded, '03');
65
+ assert.ok(payload.phase_dir.endsWith('03-observability'));
66
+ assert.equal(payload.phase_name, 'Observability');
67
+ assert.equal(payload.has_context, false);
68
+ assert.equal(payload.goal, 'Ship structured logging + metrics');
69
+ assert.deepEqual(payload.requirements, ['OBS-01']);
70
+ assert.ok('agent_skills' in payload);
71
+ assert.equal(payload.mode, 'adaptive');
72
+ });
73
+
74
+ test('DP-2: run(["nonexistent"]) throws discuss-invalid-phase-arg', () => {
75
+ const sandbox = makeSandbox();
76
+ seedRoadmapYaml(sandbox, _baseRoadmap());
77
+ const cap = _captureStdout();
78
+ assert.throws(
79
+ () => subcmd.run(['nonexistent'], { cwd: sandbox, stdout: cap.stub }),
80
+ (err) => err.name === 'NubosPilotError' && err.code === 'discuss-invalid-phase-arg',
81
+ );
82
+ });
83
+
84
+ test('DP-3: run(["99"]) where phase not in roadmap throws discuss-phase-not-found', () => {
85
+ const sandbox = makeSandbox();
86
+ seedRoadmapYaml(sandbox, _baseRoadmap());
87
+ const cap = _captureStdout();
88
+ assert.throws(
89
+ () => subcmd.run(['99'], { cwd: sandbox, stdout: cap.stub }),
90
+ (err) => err.name === 'NubosPilotError' && err.code === 'discuss-phase-not-found',
91
+ );
92
+ });
93
+
94
+ test('DP-4: existing CONTEXT.md flips has_context=true', () => {
95
+ const sandbox = makeSandbox();
96
+ seedRoadmapYaml(sandbox, _baseRoadmap());
97
+ seedPhaseDir(sandbox, 3, 'observability', {
98
+ '03-CONTEXT.md': '# existing context\n',
99
+ });
100
+ const cap = _captureStdout();
101
+ subcmd.run(['3'], { cwd: sandbox, stdout: cap.stub });
102
+ const payload = JSON.parse(cap.get().trim());
103
+ assert.equal(payload.has_context, true);
104
+ });
105
+
106
+ test('DP-5: --assumptions flag sets mode=assumptions', () => {
107
+ const sandbox = makeSandbox();
108
+ seedRoadmapYaml(sandbox, _baseRoadmap());
109
+ seedPhaseDir(sandbox, 3, 'observability', {});
110
+ const cap = _captureStdout();
111
+ subcmd.run(['3', '--assumptions'], { cwd: sandbox, stdout: cap.stub });
112
+ const payload = JSON.parse(cap.get().trim());
113
+ assert.equal(payload.mode, 'assumptions');
114
+ });
115
+
116
+ test('DP-6: decimal phase number 7.1 accepted; 7.1 not in roadmap falls through to phase-not-found', () => {
117
+ const sandbox = makeSandbox();
118
+ seedRoadmapYaml(sandbox, _baseRoadmap());
119
+ const cap = _captureStdout();
120
+
121
+
122
+ assert.throws(
123
+ () => subcmd.run(['7.1'], { cwd: sandbox, stdout: cap.stub }),
124
+ (err) => err.name === 'NubosPilotError' && err.code === 'discuss-phase-not-found',
125
+ );
126
+ });
127
+
128
+ test('DP-7: oversized payload emits @file:<tmp> pointer', () => {
129
+ const sandbox = makeSandbox();
130
+ const big = _baseRoadmap();
131
+
132
+ const filler = [];
133
+ for (let i = 0; i < 1200; i++) {
134
+ filler.push('REQ-' + i + '-with-additional-padding-to-grow-bytes-effectively');
135
+ }
136
+ big.milestones[0].phases[0].requirements = filler;
137
+ seedRoadmapYaml(sandbox, big);
138
+ seedPhaseDir(sandbox, 3, 'observability', {});
139
+ const cap = _captureStdout();
140
+ subcmd.run(['3'], { cwd: sandbox, stdout: cap.stub });
141
+ const out = cap.get().trim();
142
+ assert.ok(out.startsWith('@file:'), 'large payload produced @file: pointer');
143
+ const tmpPath = out.slice('@file:'.length);
144
+ const body = fs.readFileSync(tmpPath, 'utf-8');
145
+ const payload = JSON.parse(body);
146
+ assert.equal(payload.phase_number, '3');
147
+ fs.unlinkSync(tmpPath);
148
+ });
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const { computeNextStep: realComputeNextStep } = require('../../lib/next.cjs');
5
+ const { askUser: defaultAskUser } = require('../../lib/askuser.cjs');
6
+
7
+ const VALID_ACTIONS = new Set(['discuss', 'plan', 'execute', 'verify']);
8
+
9
+ function _parseArgs(args) {
10
+ const list = Array.isArray(args) ? args : [];
11
+ let phase = null;
12
+ let force = false;
13
+ let actionOverride = null;
14
+ for (const a of list) {
15
+ if (a === '--force') { force = true; continue; }
16
+ if (typeof a === 'string' && a.startsWith('--action=')) {
17
+ actionOverride = a.slice('--action='.length);
18
+ continue;
19
+ }
20
+ if (phase == null && typeof a === 'string' && !a.startsWith('--')) {
21
+ phase = a;
22
+ }
23
+ }
24
+ return { phase, force, actionOverride };
25
+ }
26
+
27
+ function _normalize(result) {
28
+ if (!result || typeof result !== 'object') {
29
+ return { nextAction: null, ambiguous: false, alternatives: [] };
30
+ }
31
+ if (typeof result.nextAction === 'string') {
32
+ return {
33
+ nextAction: result.nextAction,
34
+ ambiguous: Boolean(result.ambiguous),
35
+ alternatives: Array.isArray(result.alternatives) ? result.alternatives : [],
36
+ reasoning: result.reasoning,
37
+ };
38
+ }
39
+
40
+ const cmd = result.next_step && result.next_step.command;
41
+ let action = null;
42
+ if (typeof cmd === 'string') {
43
+ const m = cmd.match(/\/np:([a-z-]+)/);
44
+ if (m) {
45
+ const verb = m[1];
46
+ if (verb.startsWith('discuss')) action = 'discuss';
47
+ else if (verb.startsWith('plan')) action = 'plan';
48
+ else if (verb.startsWith('execute')) action = 'execute';
49
+ else if (verb.startsWith('verify')) action = 'verify';
50
+ }
51
+ }
52
+ return {
53
+ nextAction: action,
54
+ ambiguous: false,
55
+ alternatives: [],
56
+ reasoning: result.next_step && result.next_step.reason,
57
+ };
58
+ }
59
+
60
+ async function run(args, ctx) {
61
+ const context = ctx || {};
62
+ const cwd = context.cwd || process.cwd();
63
+ const stdout = context.stdout || process.stdout;
64
+ const askUser = typeof context.askUser === 'function' ? context.askUser : defaultAskUser;
65
+ const computeNextStep = typeof context.computeNextStep === 'function'
66
+ ? context.computeNextStep
67
+ : realComputeNextStep;
68
+
69
+ const { phase, force, actionOverride } = _parseArgs(args);
70
+
71
+ if (actionOverride != null) {
72
+ if (!VALID_ACTIONS.has(actionOverride)) {
73
+ throw new NubosPilotError(
74
+ 'dispatch-unknown-action',
75
+ 'Unknown --action override: ' + actionOverride,
76
+ { action: actionOverride, valid: [...VALID_ACTIONS] },
77
+ );
78
+ }
79
+ }
80
+
81
+ const raw = await computeNextStep(cwd, { phase });
82
+ const resolved = _normalize(raw);
83
+
84
+ let action = actionOverride || resolved.nextAction;
85
+
86
+ if (!actionOverride && !force && resolved.ambiguous) {
87
+ const options = [resolved.nextAction, ...(resolved.alternatives || [])]
88
+ .filter((x) => x && VALID_ACTIONS.has(x));
89
+ if (options.length > 1) {
90
+ const answer = await askUser({
91
+ type: 'select',
92
+ question: `State ambiguous. Recommended: ${resolved.nextAction}. Override?`,
93
+ options,
94
+ default: resolved.nextAction,
95
+ });
96
+ if (answer && typeof answer.value === 'string') action = answer.value;
97
+ }
98
+ }
99
+
100
+ if (!action || !VALID_ACTIONS.has(action)) {
101
+ throw new NubosPilotError(
102
+ 'dispatch-no-action',
103
+ 'computeNextStep did not yield a routable action',
104
+ { resolved, phase },
105
+ );
106
+ }
107
+
108
+ const payload = {
109
+ skill: 'np-' + action,
110
+ args: { phase },
111
+ };
112
+ stdout.write(JSON.stringify(payload));
113
+ return payload;
114
+ }
115
+
116
+ module.exports = { run };