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,71 @@
1
+ const { NubosPilotError } = require('../../lib/core.cjs');
2
+ const { parseRoadmap } = require('../../lib/roadmap.cjs');
3
+
4
+ function _usage() {
5
+ return 'Usage:\n np-tools.cjs phase next-decimal <base> [--raw]';
6
+ }
7
+
8
+ function _emitError(err, stderr) {
9
+ const code = err && err.name === 'NubosPilotError' ? err.code : 'phase-internal-error';
10
+ const message = (err && err.message) || String(err);
11
+ const details = (err && err.details) || null;
12
+ stderr.write(JSON.stringify({ code, message, details }) + '\n');
13
+ }
14
+
15
+ function nextDecimal(base, cwd) {
16
+ if (!/^[0-9]+$/.test(String(base))) {
17
+ throw new NubosPilotError('phase-invalid-base', 'phase next-decimal base must be a non-negative integer: ' + base, { base });
18
+ }
19
+ const baseStr = String(base);
20
+ let phases;
21
+ try {
22
+ phases = parseRoadmap(cwd || process.cwd()).phases;
23
+ } catch (err) {
24
+ if (err && err.code === 'roadmap-parse-error') return baseStr + '.1';
25
+ throw err;
26
+ }
27
+ const prefix = baseStr + '.';
28
+ let max = 0;
29
+ for (const ph of phases) {
30
+ const n = String(ph.number);
31
+ if (n.startsWith(prefix)) {
32
+ const suf = Number(n.slice(prefix.length));
33
+ if (Number.isInteger(suf) && suf > max) max = suf;
34
+ }
35
+ }
36
+ return baseStr + '.' + (max + 1);
37
+ }
38
+
39
+ function run(argv, ctx) {
40
+ const context = ctx || {};
41
+ const cwd = context.cwd || process.cwd();
42
+ const stdout = context.stdout || process.stdout;
43
+ const stderr = context.stderr || process.stderr;
44
+ const args = Array.isArray(argv) ? argv.slice() : [];
45
+ const sub = args.shift();
46
+ if (sub !== 'next-decimal') {
47
+ stderr.write(_usage() + '\n');
48
+ return 1;
49
+ }
50
+ const raw = args.includes('--raw');
51
+ const base = args.find((a) => !String(a).startsWith('--'));
52
+ if (base == null) {
53
+ stderr.write(_usage() + '\n');
54
+ return 1;
55
+ }
56
+ try {
57
+ const result = nextDecimal(base, cwd);
58
+ if (raw) stdout.write(result);
59
+ else stdout.write(result + '\n');
60
+ return 0;
61
+ } catch (err) {
62
+ _emitError(err, stderr);
63
+ return 1;
64
+ }
65
+ }
66
+
67
+ module.exports = { run, nextDecimal };
68
+
69
+ if (require.main === module) {
70
+ process.exit(run(process.argv.slice(2)));
71
+ }
@@ -0,0 +1,81 @@
1
+ const fs = require('node:fs');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+ const { test } = require('node:test');
5
+ const assert = require('node:assert/strict');
6
+ const { Writable } = require('node:stream');
7
+
8
+ const phaseCli = require('./phase.cjs');
9
+
10
+ const _sandboxes = [];
11
+
12
+ function makeSink() {
13
+ const chunks = [];
14
+ const w = new Writable({
15
+ write(chunk, _enc, cb) { chunks.push(chunk); cb(); },
16
+ });
17
+ w.toString = () => Buffer.concat(chunks.map((c) => Buffer.isBuffer(c) ? c : Buffer.from(String(c)))).toString('utf-8');
18
+ return w;
19
+ }
20
+
21
+ function makeSandboxWithRoadmap(yaml) {
22
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-phase-'));
23
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
24
+ fs.writeFileSync(path.join(root, '.nubos-pilot', 'roadmap.yaml'), yaml);
25
+ _sandboxes.push(root);
26
+ return root;
27
+ }
28
+
29
+ test.afterEach(() => {
30
+ while (_sandboxes.length) {
31
+ try { fs.rmSync(_sandboxes.pop(), { recursive: true, force: true }); } catch { }
32
+ }
33
+ });
34
+
35
+ test('PHASE-1: next-decimal 999 on empty roadmap → 999.1', () => {
36
+ const sb = makeSandboxWithRoadmap('milestones:\n - id: v1.0\n name: v1\n phases: []\n');
37
+ const stdout = makeSink();
38
+ const stderr = makeSink();
39
+ const code = phaseCli.run(['next-decimal', '999', '--raw'], { cwd: sb, stdout, stderr });
40
+ assert.equal(code, 0);
41
+ assert.equal(stdout.toString(), '999.1');
42
+ });
43
+
44
+ test('PHASE-2: next-decimal scans existing 999.1 and 999.3 → 999.4', () => {
45
+ const yaml = [
46
+ 'milestones:',
47
+ ' - id: backlog',
48
+ ' name: Backlog',
49
+ ' phases:',
50
+ ' - number: "999.1"',
51
+ ' name: First',
52
+ ' slug: first',
53
+ ' - number: "999.3"',
54
+ ' name: Third',
55
+ ' slug: third',
56
+ ].join('\n') + '\n';
57
+ const sb = makeSandboxWithRoadmap(yaml);
58
+ const stdout = makeSink();
59
+ const stderr = makeSink();
60
+ const code = phaseCli.run(['next-decimal', '999', '--raw'], { cwd: sb, stdout, stderr });
61
+ assert.equal(code, 0);
62
+ assert.equal(stdout.toString(), '999.4');
63
+ });
64
+
65
+ test('PHASE-3: invalid base rejected with phase-invalid-base', () => {
66
+ const sb = makeSandboxWithRoadmap('milestones: []\n');
67
+ const stdout = makeSink();
68
+ const stderr = makeSink();
69
+ const code = phaseCli.run(['next-decimal', 'abc'], { cwd: sb, stdout, stderr });
70
+ assert.equal(code, 1);
71
+ assert.match(stderr.toString(), /"code":\s*"phase-invalid-base"/);
72
+ });
73
+
74
+ test('PHASE-4: unknown subcommand prints usage', () => {
75
+ const sb = makeSandboxWithRoadmap('milestones: []\n');
76
+ const stdout = makeSink();
77
+ const stderr = makeSink();
78
+ const code = phaseCli.run(['unknown', '1'], { cwd: sb, stdout, stderr });
79
+ assert.equal(code, 1);
80
+ assert.match(stderr.toString(), /Usage:/);
81
+ });
@@ -0,0 +1,57 @@
1
+ const { renderTwoPartDiff, archiveRejected } = require('../../lib/plan-diff.cjs');
2
+
3
+ function _usage() {
4
+ return [
5
+ 'Usage:',
6
+ ' np-tools.cjs plan-diff <phase> <plan-id>',
7
+ ' np-tools.cjs plan-diff --archive-rejected <phase> <plan-id> --reason "<text>"',
8
+ ].join('\n');
9
+ }
10
+
11
+ function run(argv) {
12
+ const args = Array.isArray(argv) ? argv.slice() : process.argv.slice(3);
13
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
14
+ process.stderr.write(_usage() + '\n');
15
+ return 1;
16
+ }
17
+ try {
18
+ if (args[0] === '--archive-rejected') {
19
+ const phase = args[1];
20
+ const planId = args[2];
21
+ let reason = '';
22
+ for (let i = 3; i < args.length; i++) {
23
+ if (args[i] === '--reason') {
24
+ reason = args[i + 1] == null ? '' : String(args[i + 1]);
25
+ }
26
+ }
27
+ if (!phase || !planId) {
28
+ process.stderr.write(_usage() + '\n');
29
+ return 1;
30
+ }
31
+ const archivePath = archiveRejected({ phase, planId, reason, cwd: process.cwd() });
32
+ process.stdout.write(archivePath + '\n');
33
+ return 0;
34
+ }
35
+ const phase = args[0];
36
+ const planId = args[1];
37
+ if (!phase || !planId) {
38
+ process.stderr.write(_usage() + '\n');
39
+ return 1;
40
+ }
41
+ const result = renderTwoPartDiff({ phase, planId, cwd: process.cwd() });
42
+ if (!result.hasPrior) return 0;
43
+ process.stdout.write(result.combined + '\n');
44
+ return 0;
45
+ } catch (err) {
46
+ if (err && err.name === 'NubosPilotError') {
47
+ process.stderr.write(
48
+ JSON.stringify({ code: err.code, message: err.message, details: err.details }) + '\n',
49
+ );
50
+ } else {
51
+ process.stderr.write(String(err && err.stack || err) + '\n');
52
+ }
53
+ return 1;
54
+ }
55
+ }
56
+
57
+ module.exports = { run };
@@ -0,0 +1,134 @@
1
+ const { test, after } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const os = require('node:os');
6
+ const { execFileSync } = require('node:child_process');
7
+
8
+ const subcmd = require('./plan-diff.cjs');
9
+
10
+ const _repos = [];
11
+
12
+ function makeRepo() {
13
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-plan-diff-cli-'));
14
+ execFileSync('git', ['init', '-q', '-b', 'main', root], { stdio: 'pipe' });
15
+ execFileSync('git', ['-C', root, 'config', 'user.email', 'test@nubos-pilot.local']);
16
+ execFileSync('git', ['-C', root, 'config', 'user.name', 'nubos-test']);
17
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
18
+ execFileSync('git', ['-C', root, 'commit', '--allow-empty', '-q', '-m', 'chore: init'], { stdio: 'pipe' });
19
+ _repos.push(root);
20
+ return root;
21
+ }
22
+
23
+ function seedPlanCommit(root, phase, phaseSlug, planId, body) {
24
+ const padded = String(phase).padStart(2, '0');
25
+ const phaseDir = path.join(root, '.nubos-pilot', 'phases', padded + '-' + phaseSlug);
26
+ fs.mkdirSync(phaseDir, { recursive: true });
27
+ const rel = path.join('.nubos-pilot', 'phases', padded + '-' + phaseSlug, planId + '-PLAN.md');
28
+ const abs = path.join(root, rel);
29
+ fs.writeFileSync(abs, body, 'utf-8');
30
+ execFileSync('git', ['-C', root, 'add', '--', rel], { stdio: 'pipe' });
31
+ execFileSync('git', ['-C', root, 'commit', '-q', '-m', 'seed'], { stdio: 'pipe' });
32
+ return { abs, rel, phaseDir };
33
+ }
34
+
35
+ function planBody(tasks) {
36
+ const lines = ['---', 'plan: 1', 'phase: 9', '---', ''];
37
+ for (const t of tasks) {
38
+ lines.push('<task tier="' + t.tier + '" id="' + t.id + '">body</task>');
39
+ }
40
+ return lines.join('\n');
41
+ }
42
+
43
+ function captureIO() {
44
+ let sout = '';
45
+ let serr = '';
46
+ return {
47
+ stdoutWrite: (s) => { sout += s; return true; },
48
+ stderrWrite: (s) => { serr += s; return true; },
49
+ stdout: () => sout,
50
+ stderr: () => serr,
51
+ };
52
+ }
53
+
54
+ function runInRepo(root, argv) {
55
+ const prev = process.cwd();
56
+ const po = process.stdout.write;
57
+ const pe = process.stderr.write;
58
+ const cap = captureIO();
59
+ process.stdout.write = cap.stdoutWrite;
60
+ process.stderr.write = cap.stderrWrite;
61
+ process.chdir(root);
62
+ let exit;
63
+ try {
64
+ exit = subcmd.run(argv);
65
+ } finally {
66
+ process.stdout.write = po;
67
+ process.stderr.write = pe;
68
+ process.chdir(prev);
69
+ }
70
+ return { exit, stdout: cap.stdout(), stderr: cap.stderr() };
71
+ }
72
+
73
+ after(() => {
74
+ while (_repos.length) {
75
+ const r = _repos.pop();
76
+ try { fs.rmSync(r, { recursive: true, force: true }); } catch {}
77
+ }
78
+ });
79
+
80
+ test('PDCLI-1: run([phase, planId]) prints combined diff when prior exists; exits 0', () => {
81
+ const root = makeRepo();
82
+ const { abs } = seedPlanCommit(root, '09', 'feature-set', '09-01', planBody([
83
+ { id: '09-01-T01', tier: 'opus' },
84
+ ]));
85
+ fs.writeFileSync(abs, planBody([
86
+ { id: '09-01-T01', tier: 'opus' },
87
+ { id: '09-01-T02', tier: 'sonnet' },
88
+ ]), 'utf-8');
89
+ const result = runInRepo(root, ['09', '09-01']);
90
+ assert.equal(result.exit, 0);
91
+ assert.ok(result.stdout.includes('Semantic diff'));
92
+ assert.ok(result.stdout.includes('Raw git diff'));
93
+ });
94
+
95
+ test('PDCLI-2: run([phase, planId]) prints empty stdout and exits 0 when no prior in HEAD', () => {
96
+ const root = makeRepo();
97
+ const padded = '09';
98
+ const phaseDir = path.join(root, '.nubos-pilot', 'phases', padded + '-feature-set');
99
+ fs.mkdirSync(phaseDir, { recursive: true });
100
+ fs.writeFileSync(path.join(phaseDir, '09-01-PLAN.md'), planBody([{ id: '09-01-T01', tier: 'opus' }]), 'utf-8');
101
+ const result = runInRepo(root, ['09', '09-01']);
102
+ assert.equal(result.exit, 0);
103
+ assert.equal(result.stdout, '');
104
+ });
105
+
106
+ test('PDCLI-3: run([--archive-rejected, phase, planId, --reason, text]) archives + restores, exits 0', () => {
107
+ const root = makeRepo();
108
+ const priorBody = planBody([{ id: '09-01-T01', tier: 'opus' }]);
109
+ const { abs } = seedPlanCommit(root, '09', 'feature-set', '09-01', priorBody);
110
+ fs.writeFileSync(abs, planBody([{ id: '09-01-T02', tier: 'haiku' }]), 'utf-8');
111
+ const result = runInRepo(root, ['--archive-rejected', '09', '09-01', '--reason', 'design flaw']);
112
+ assert.equal(result.exit, 0);
113
+ const archivePath = result.stdout.trim();
114
+ assert.ok(fs.existsSync(archivePath));
115
+ const restored = fs.readFileSync(abs, 'utf-8');
116
+ assert.equal(restored, priorBody);
117
+ });
118
+
119
+ test('PDCLI-4: run([--archive-rejected]) with missing args exits 1 with usage on stderr', () => {
120
+ const root = makeRepo();
121
+ const result = runInRepo(root, ['--archive-rejected']);
122
+ assert.equal(result.exit, 1);
123
+ assert.ok(result.stderr.includes('Usage'));
124
+ });
125
+
126
+ test('PDCLI-5: run([]) prints usage on stderr and exits 1', () => {
127
+ const root = makeRepo();
128
+ const r1 = runInRepo(root, []);
129
+ assert.equal(r1.exit, 1);
130
+ assert.ok(r1.stderr.includes('Usage'));
131
+ const r2 = runInRepo(root, ['--help']);
132
+ assert.equal(r2.exit, 1);
133
+ assert.ok(r2.stderr.includes('Usage'));
134
+ });
@@ -0,0 +1,115 @@
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
+ const YAML = require('yaml');
6
+
7
+ const { scanVerifications, parseAuditFile } = require('../../lib/gaps.cjs');
8
+ const { NubosPilotError, projectStateDir } = require('../../lib/core.cjs');
9
+
10
+ const INLINE_THRESHOLD_BYTES = 16 * 1024;
11
+
12
+ function _parseFlags(args) {
13
+ const flags = { from: null, insertAfter: null };
14
+ for (let i = 0; i < args.length; i++) {
15
+ const a = args[i];
16
+ if (a === '--from') {
17
+ flags.from = args[++i];
18
+ } else if (a === '--insert-after') {
19
+ const raw = args[++i];
20
+ const n = Number(raw);
21
+ if (raw == null || !Number.isInteger(n)) {
22
+ throw new NubosPilotError(
23
+ 'invalid-insert-after',
24
+ '--insert-after requires an integer phase number',
25
+ { value: raw == null ? '' : String(raw) },
26
+ );
27
+ }
28
+ flags.insertAfter = n;
29
+ }
30
+ }
31
+ return flags;
32
+ }
33
+
34
+ function _readMilestoneId(cwd) {
35
+
36
+
37
+ try {
38
+ const { readState } = require('../../lib/state.cjs');
39
+ const st = readState(cwd);
40
+ if (st && st.frontmatter && st.frontmatter.milestone) {
41
+ return String(st.frontmatter.milestone);
42
+ }
43
+ } catch (_err) { }
44
+ let stateDir;
45
+ try { stateDir = projectStateDir(cwd); } catch (_err) {
46
+ stateDir = path.join(path.resolve(cwd), '.nubos-pilot');
47
+ }
48
+ const yamlPath = path.join(stateDir, 'roadmap.yaml');
49
+ const raw = fs.readFileSync(yamlPath, 'utf-8');
50
+ const doc = YAML.parse(raw);
51
+ if (!doc || !Array.isArray(doc.milestones) || doc.milestones.length === 0) {
52
+ throw new NubosPilotError(
53
+ 'gaps-no-milestone',
54
+ 'roadmap.yaml has no milestones',
55
+ { path: yamlPath },
56
+ );
57
+ }
58
+ return String(doc.milestones[0].id);
59
+ }
60
+
61
+ function _agentSkills() {
62
+ try {
63
+ const agents = require('../../lib/agents.cjs');
64
+ if (typeof agents.getAgentSkills === 'function') {
65
+ return { planner: agents.getAgentSkills('np-planner') };
66
+ }
67
+ } catch (_err) { }
68
+ return { planner: null };
69
+ }
70
+
71
+ function _emit(payload, stdout, cwd) {
72
+ const json = JSON.stringify(payload, null, 2);
73
+ if (Buffer.byteLength(json, 'utf-8') <= INLINE_THRESHOLD_BYTES) {
74
+ stdout.write(json);
75
+ return;
76
+ }
77
+
78
+
79
+
80
+ let tmpDir;
81
+ try {
82
+ tmpDir = path.join(projectStateDir(cwd), '.tmp');
83
+ fs.mkdirSync(tmpDir, { recursive: true });
84
+ } catch (_err) {
85
+ tmpDir = os.tmpdir();
86
+ }
87
+ const suffix = process.pid + '-' + crypto.randomBytes(4).toString('hex');
88
+ const tmpPath = path.join(tmpDir, 'init-plan-milestone-gaps-' + suffix + '.json');
89
+ fs.writeFileSync(tmpPath, json, 'utf-8');
90
+ stdout.write('@file:' + tmpPath);
91
+ }
92
+
93
+ function run(args, ctx) {
94
+ const context = ctx || {};
95
+ const cwd = context.cwd || process.cwd();
96
+ const stdout = context.stdout || process.stdout;
97
+ const flags = _parseFlags(args || []);
98
+ const milestoneId = _readMilestoneId(cwd);
99
+ const mode = flags.from ? 'from-file' : 'scan';
100
+ const gaps = flags.from
101
+ ? parseAuditFile(flags.from, cwd)
102
+ : scanVerifications(milestoneId, cwd);
103
+ const payload = {
104
+ _workflow: 'plan-milestone-gaps',
105
+ milestoneId,
106
+ mode,
107
+ gaps,
108
+ insertAfter: flags.insertAfter,
109
+ agent_skills: _agentSkills(),
110
+ };
111
+ _emit(payload, stdout, cwd);
112
+ return payload;
113
+ }
114
+
115
+ module.exports = { run, _parseFlags, INLINE_THRESHOLD_BYTES };
@@ -0,0 +1,122 @@
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('./plan-milestone-gaps.cjs');
9
+
10
+ function _baseRoadmap() {
11
+ return {
12
+ schema_version: 1,
13
+ milestones: [
14
+ {
15
+ id: 'v1.0',
16
+ name: 'first',
17
+ phases: [
18
+ { number: 1, name: 'One', slug: 'one', goal: '', depends_on: [], requirements: [], success_criteria: [], status: 'done', plans: [] },
19
+ { number: 7, name: 'Seven', slug: 'seven', goal: '', depends_on: [6], requirements: [], success_criteria: [], status: 'done', plans: [] },
20
+ ],
21
+ },
22
+ ],
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
+ afterEach(cleanupAll);
33
+
34
+ test('CMD-1: no flags → JSON payload with mode=scan', () => {
35
+ const sandbox = makeSandbox();
36
+ seedRoadmapYaml(sandbox, _baseRoadmap());
37
+ const cap = _captureStdout();
38
+ subcmd.run([], { cwd: sandbox, stdout: cap.stub });
39
+ const raw = cap.get().trim();
40
+ assert.ok(!raw.startsWith('@file:'), 'small payload emitted inline');
41
+ const payload = JSON.parse(raw);
42
+ assert.equal(payload.milestoneId, 'v1.0');
43
+ assert.equal(payload.mode, 'scan');
44
+ assert.ok(Array.isArray(payload.gaps));
45
+ assert.equal(payload.insertAfter, null);
46
+ assert.ok('agent_skills' in payload);
47
+ });
48
+
49
+ test('CMD-2: --from audit-file → mode=from-file, gaps populated', () => {
50
+ const sandbox = makeSandbox();
51
+ seedRoadmapYaml(sandbox, _baseRoadmap());
52
+ const auditPath = path.join(sandbox, 'audit.md');
53
+ fs.copyFileSync(
54
+ path.join(__dirname, '..', '..', 'tests', 'fixtures', 'gaps', 'audit-from-file.md'),
55
+ auditPath,
56
+ );
57
+ const cap = _captureStdout();
58
+ subcmd.run(['--from', auditPath], { cwd: sandbox, stdout: cap.stub });
59
+ const payload = JSON.parse(cap.get().trim());
60
+ assert.equal(payload.mode, 'from-file');
61
+ assert.equal(payload.gaps.length, 2);
62
+ for (const g of payload.gaps) assert.equal(g.source_phase, 7);
63
+ });
64
+
65
+ test('CMD-3: --from /etc/passwd → throws gaps-invalid-audit-path', () => {
66
+ const sandbox = makeSandbox();
67
+ seedRoadmapYaml(sandbox, _baseRoadmap());
68
+ const cap = _captureStdout();
69
+ assert.throws(
70
+ () => subcmd.run(['--from', '/etc/passwd'], { cwd: sandbox, stdout: cap.stub }),
71
+ (err) => err.name === 'NubosPilotError' && err.code === 'gaps-invalid-audit-path',
72
+ );
73
+ });
74
+
75
+ test('CMD-4: --insert-after 7 → payload.insertAfter === 7', () => {
76
+ const sandbox = makeSandbox();
77
+ seedRoadmapYaml(sandbox, _baseRoadmap());
78
+ const cap = _captureStdout();
79
+ subcmd.run(['--insert-after', '7'], { cwd: sandbox, stdout: cap.stub });
80
+ const payload = JSON.parse(cap.get().trim());
81
+ assert.equal(payload.insertAfter, 7);
82
+ });
83
+
84
+ test('CMD-5: --insert-after abc → throws invalid-insert-after', () => {
85
+ const sandbox = makeSandbox();
86
+ seedRoadmapYaml(sandbox, _baseRoadmap());
87
+ const cap = _captureStdout();
88
+ assert.throws(
89
+ () => subcmd.run(['--insert-after', 'abc'], { cwd: sandbox, stdout: cap.stub }),
90
+ (err) => err.name === 'NubosPilotError' && err.code === 'invalid-insert-after',
91
+ );
92
+ });
93
+
94
+ test('CMD-6: oversized payload emits @file:<tmp> pointer', () => {
95
+ const sandbox = makeSandbox();
96
+ seedRoadmapYaml(sandbox, _baseRoadmap());
97
+
98
+
99
+ const big = Array.from({ length: 2000 }, (_, i) =>
100
+ '- [ ] checkbox item number ' + i + ' with filler padding to grow bytes',
101
+ ).join('\n');
102
+ seedPhaseDir(sandbox, 3, 'foo', { '03-VERIFICATION.md': big });
103
+ const cap = _captureStdout();
104
+ subcmd.run([], { cwd: sandbox, stdout: cap.stub });
105
+ const out = cap.get().trim();
106
+ assert.ok(out.startsWith('@file:'), 'large payload produced @file: pointer');
107
+ const tmpPath = out.slice('@file:'.length);
108
+ const body = fs.readFileSync(tmpPath, 'utf-8');
109
+ const payload = JSON.parse(body);
110
+ assert.equal(payload.mode, 'scan');
111
+ assert.ok(payload.gaps.length >= 2000);
112
+ fs.unlinkSync(tmpPath);
113
+ });
114
+
115
+ test('CMD-7: milestoneId defaults to first milestone when STATE.md absent', () => {
116
+ const sandbox = makeSandbox();
117
+ seedRoadmapYaml(sandbox, _baseRoadmap());
118
+ const cap = _captureStdout();
119
+ subcmd.run([], { cwd: sandbox, stdout: cap.stub });
120
+ const payload = JSON.parse(cap.get().trim());
121
+ assert.equal(payload.milestoneId, 'v1.0');
122
+ });