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,123 @@
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('./ai-integration-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: 9,
20
+ name: 'Feature Set',
21
+ slug: 'feature-set',
22
+ goal: 'Ship advanced workflows',
23
+ depends_on: [],
24
+ requirements: ['R-01'],
25
+ success_criteria: ['AI-SPEC produced'],
26
+ status: 'pending',
27
+ plans: [],
28
+ },
29
+ ],
30
+ },
31
+ ],
32
+ };
33
+ }
34
+
35
+ function _capture() {
36
+ let buf = '';
37
+ return { stub: { write: (s) => { buf += s; return true; } }, get: () => buf };
38
+ }
39
+
40
+ afterEach(cleanupAll);
41
+
42
+ test('AIP-1: run(["init", "9"]) returns payload with expected shape', () => {
43
+ const sandbox = makeSandbox();
44
+ seedRoadmapYaml(sandbox, _baseRoadmap());
45
+ const dir = seedPhaseDir(sandbox, 9, 'feature-set', {});
46
+ const cap = _capture();
47
+ subcmd.run(['9'], { cwd: sandbox, stdout: cap.stub });
48
+ const payload = JSON.parse(cap.get().trim());
49
+ assert.equal(payload.phase, '9');
50
+ assert.equal(payload.padded, '09');
51
+ assert.equal(payload.phase_dir, dir);
52
+ assert.equal(
53
+ payload.ai_spec_path,
54
+ path.join(dir, '09-AI-SPEC.md'),
55
+ );
56
+ assert.equal(payload.has_ai_spec, false);
57
+ assert.match(payload.template_path, /templates\/AI-SPEC\.md$/);
58
+ });
59
+
60
+ test('AIP-2: agents.* declares the 4 AI-integration subagents', () => {
61
+ const sandbox = makeSandbox();
62
+ seedRoadmapYaml(sandbox, _baseRoadmap());
63
+ seedPhaseDir(sandbox, 9, 'feature-set', {});
64
+ const cap = _capture();
65
+ subcmd.run(['9'], { cwd: sandbox, stdout: cap.stub });
66
+ const payload = JSON.parse(cap.get().trim());
67
+ assert.deepEqual(payload.agents, {
68
+ framework_selector: 'np-framework-selector',
69
+ ai_researcher: 'np-ai-researcher',
70
+ domain_researcher: 'np-domain-researcher',
71
+ eval_planner: 'np-eval-planner',
72
+ });
73
+ });
74
+
75
+ test('AIP-3: has_ai_spec=true when {padded}-AI-SPEC.md exists', () => {
76
+ const sandbox = makeSandbox();
77
+ seedRoadmapYaml(sandbox, _baseRoadmap());
78
+ seedPhaseDir(sandbox, 9, 'feature-set', {
79
+ '09-AI-SPEC.md': '# AI Spec\n',
80
+ });
81
+ const cap = _capture();
82
+ subcmd.run(['9'], { cwd: sandbox, stdout: cap.stub });
83
+ const payload = JSON.parse(cap.get().trim());
84
+ assert.equal(payload.has_ai_spec, true);
85
+ });
86
+
87
+ test('AIP-4: runtime field populated from lib/runtime detect()', () => {
88
+ const sandbox = makeSandbox();
89
+ seedRoadmapYaml(sandbox, _baseRoadmap());
90
+ seedPhaseDir(sandbox, 9, 'feature-set', {});
91
+ fs.writeFileSync(
92
+ path.join(sandbox, '.nubos-pilot', 'config.json'),
93
+ JSON.stringify({ runtime: 'claude' }),
94
+ 'utf-8',
95
+ );
96
+ const cap = _capture();
97
+ subcmd.run(['9'], { cwd: sandbox, stdout: cap.stub });
98
+ const payload = JSON.parse(cap.get().trim());
99
+ assert.equal(payload.runtime, 'claude');
100
+ });
101
+
102
+ test('AIP-5: missing phase arg exits with usage error', () => {
103
+ const sandbox = makeSandbox();
104
+ seedRoadmapYaml(sandbox, _baseRoadmap());
105
+ const cap = _capture();
106
+ let errBuf = '';
107
+ const errStub = { write: (s) => { errBuf += s; return true; } };
108
+ const code = subcmd.run([], { cwd: sandbox, stdout: cap.stub, stderr: errStub });
109
+ assert.equal(code, 1);
110
+ assert.match(errBuf, /Usage/);
111
+ });
112
+
113
+ test('AIP-6: phase not in roadmap produces NubosPilotError JSON on stderr', () => {
114
+ const sandbox = makeSandbox();
115
+ seedRoadmapYaml(sandbox, _baseRoadmap());
116
+ const cap = _capture();
117
+ let errBuf = '';
118
+ const errStub = { write: (s) => { errBuf += s; return true; } };
119
+ const code = subcmd.run(['99'], { cwd: sandbox, stdout: cap.stub, stderr: errStub });
120
+ assert.equal(code, 1);
121
+ const parsed = JSON.parse(errBuf.trim());
122
+ assert.equal(parsed.code, 'ai-integration-phase-not-found');
123
+ });
@@ -0,0 +1,53 @@
1
+ const { askUser } = require('../../lib/askuser.cjs');
2
+
3
+ function _usage() {
4
+ return 'Usage:\n np-tools.cjs askuser --json \'{...spec...}\'';
5
+ }
6
+
7
+ function _emitError(err, stderr) {
8
+ const code = err && err.name === 'NubosPilotError' ? err.code : 'askuser-internal-error';
9
+ const message = (err && err.message) || String(err);
10
+ const details = (err && err.details) || null;
11
+ stderr.write(JSON.stringify({ code, message, details }) + '\n');
12
+ }
13
+
14
+ async function run(argv, ctx) {
15
+ const context = ctx || {};
16
+ const stdout = context.stdout || process.stdout;
17
+ const stderr = context.stderr || process.stderr;
18
+ const args = Array.isArray(argv) ? argv.slice() : [];
19
+ const idx = args.indexOf('--json');
20
+ if (idx < 0 || idx + 1 >= args.length) {
21
+ stderr.write(_usage() + '\n');
22
+ return 1;
23
+ }
24
+ let spec;
25
+ try {
26
+ spec = JSON.parse(args[idx + 1]);
27
+ } catch (err) {
28
+ stderr.write(JSON.stringify({ code: 'askuser-invalid-json', message: err.message, details: null }) + '\n');
29
+ return 1;
30
+ }
31
+ try {
32
+ const result = await askUser(spec);
33
+ const value = result && typeof result === 'object' && 'value' in result ? result.value : result;
34
+ let out;
35
+ if (value == null) out = '';
36
+ else if (typeof value === 'string') out = value;
37
+ else out = JSON.stringify(value);
38
+ stdout.write(out + '\n');
39
+ return 0;
40
+ } catch (err) {
41
+ _emitError(err, stderr);
42
+ return 1;
43
+ }
44
+ }
45
+
46
+ module.exports = { run };
47
+
48
+ if (require.main === module) {
49
+ run(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
50
+ process.stderr.write(String((err && err.stack) || err) + '\n');
51
+ process.exit(1);
52
+ });
53
+ }
@@ -0,0 +1,49 @@
1
+ const { test } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const { Writable } = require('node:stream');
4
+
5
+ const askuserCli = require('./askuser.cjs');
6
+ const askuserLib = require('../../lib/askuser.cjs');
7
+
8
+ function makeSink() {
9
+ const chunks = [];
10
+ const w = new Writable({
11
+ write(chunk, _enc, cb) { chunks.push(chunk); cb(); },
12
+ });
13
+ w.toString = () => Buffer.concat(chunks.map((c) => Buffer.isBuffer(c) ? c : Buffer.from(String(c)))).toString('utf-8');
14
+ return w;
15
+ }
16
+
17
+ test('ASKUSER-1: happy path returns chosen label via readline test-hook', async (t) => {
18
+ askuserLib._setReadlineImplForTests(() => '2');
19
+ t.after(() => askuserLib._setReadlineImplForTests(null));
20
+ const stdout = makeSink();
21
+ const stderr = makeSink();
22
+ const spec = { type: 'select', question: 'Choose', options: ['alpha', 'beta', 'gamma'] };
23
+ const origStdoutWrite = process.stdout.write.bind(process.stdout);
24
+ process.stdout.write = () => true;
25
+ let code;
26
+ try {
27
+ code = await askuserCli.run(['--json', JSON.stringify(spec)], { stdout, stderr });
28
+ } finally {
29
+ process.stdout.write = origStdoutWrite;
30
+ }
31
+ assert.equal(code, 0);
32
+ assert.match(stdout.toString(), /beta/);
33
+ });
34
+
35
+ test('ASKUSER-2: invalid JSON emits askuser-invalid-json on stderr', async () => {
36
+ const stdout = makeSink();
37
+ const stderr = makeSink();
38
+ const code = await askuserCli.run(['--json', '{not json'], { stdout, stderr });
39
+ assert.equal(code, 1);
40
+ assert.match(stderr.toString(), /"code":\s*"askuser-invalid-json"/);
41
+ });
42
+
43
+ test('ASKUSER-3: missing --json prints usage to stderr', async () => {
44
+ const stdout = makeSink();
45
+ const stderr = makeSink();
46
+ const code = await askuserCli.run([], { stdout, stderr });
47
+ assert.equal(code, 1);
48
+ assert.match(stderr.toString(), /Usage:/);
49
+ });
@@ -0,0 +1,69 @@
1
+ const path = require('node:path');
2
+ const { NubosPilotError } = require('../../lib/core.cjs');
3
+ const { resolveGate } = require('../../lib/next.cjs');
4
+ const { findPhaseDir } = require('../../lib/phase.cjs');
5
+ const { listPlans } = require('../../lib/plan.cjs');
6
+ const { loadTaskGraph } = require('../../lib/tasks.cjs');
7
+
8
+ function _firstWaveAllSkippedOrParked(phaseN, cwd) {
9
+ let phaseDir;
10
+ try { phaseDir = findPhaseDir(phaseN, cwd); } catch { return false; }
11
+ if (!phaseDir) return false;
12
+ const plans = listPlans(phaseDir);
13
+ if (plans.length === 0) return false;
14
+ const planDir = path.dirname(plans[0]);
15
+ let tg;
16
+ try { tg = loadTaskGraph(planDir); } catch { return false; }
17
+ if (!tg.waves || tg.waves.length === 0) return false;
18
+ const firstWave = tg.waves[0];
19
+ const pending = firstWave
20
+ .map((id) => tg.tasks.find((t) => t.id === id))
21
+ .filter((t) => t && t.frontmatter && t.frontmatter.status !== 'done');
22
+ if (pending.length === 0) return false;
23
+ return pending.every((t) => t.frontmatter.status === 'skipped' || t.frontmatter.status === 'parked');
24
+ }
25
+
26
+ function run(args, ctx) {
27
+ const context = ctx || {};
28
+ const cwd = context.cwd || process.cwd();
29
+ const stdout = context.stdout || process.stdout;
30
+ const list = Array.isArray(args) ? args : [];
31
+
32
+
33
+ const verb = list[0];
34
+ const phaseArg = verb === 'init' ? list[1] : verb;
35
+ if (phaseArg == null || phaseArg === '' || !/^\d+(\.\d+)?$/.test(String(phaseArg))) {
36
+ throw new NubosPilotError(
37
+ 'autonomous-invalid-phase',
38
+ 'autonomous requires a numeric phase argument',
39
+ { value: phaseArg == null ? '' : String(phaseArg) },
40
+ );
41
+ }
42
+ const phaseN = Number(phaseArg);
43
+ const gate = resolveGate(phaseN, cwd);
44
+ let payload;
45
+ const blockedNullTask = gate.rule === 3 && !gate.task;
46
+ const blockedSkippedOnly = gate.rule === 3 && _firstWaveAllSkippedOrParked(phaseN, cwd);
47
+ if (blockedNullTask || blockedSkippedOnly) {
48
+
49
+
50
+ payload = {
51
+ _workflow: 'autonomous',
52
+ status: 'advancement-blocked',
53
+ phase: phaseN,
54
+ reason: 'rule-3-null-task',
55
+ gate,
56
+ };
57
+ } else {
58
+ payload = {
59
+ _workflow: 'autonomous',
60
+ status: 'ok',
61
+ phase: phaseN,
62
+ gate,
63
+ };
64
+ }
65
+ stdout.write(JSON.stringify(payload));
66
+ return payload;
67
+ }
68
+
69
+ module.exports = { run };
@@ -0,0 +1,74 @@
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('./autonomous.cjs');
9
+
10
+ function _baseRoadmap() {
11
+ return {
12
+ schema_version: 1,
13
+ milestones: [{ id: 'v1.0', name: 'm1', phases: [
14
+ { number: 6, name: 'Execution', slug: 'execution', goal: '', depends_on: [],
15
+ requirements: [], success_criteria: [], status: 'planned', plans: [] },
16
+ ]}],
17
+ };
18
+ }
19
+
20
+ function _capture() { let b = ''; return { stub: { write: (s) => { b += s; return true; } }, get: () => b }; }
21
+
22
+ afterEach(cleanupAll);
23
+
24
+ test('AU-1: advancement-blocked when rule 3 + skipped-only wave (Pitfall 6 Guard)', () => {
25
+ const sandbox = makeSandbox();
26
+ seedRoadmapYaml(sandbox, _baseRoadmap());
27
+ const phaseDir = seedPhaseDir(sandbox, 6, 'execution', {
28
+ '06-CONTEXT.md': '# ctx',
29
+ '06-01-PLAN.md': '---\nphase: 6\nplan: 01\n---\n# p\n',
30
+ });
31
+
32
+ fs.mkdirSync(path.join(phaseDir, 'tasks'), { recursive: true });
33
+ const taskMd = [
34
+ '---', 'id: 06-01-T01', 'phase: 6', 'plan: "06-01"', 'type: auto',
35
+ 'status: skipped', 'tier: sonnet', 'owner: np-executor', 'wave: 1',
36
+ 'depends_on: []', 'files_modified: []', 'autonomous: true',
37
+ 'must_haves:', ' truths: []', '---', '',
38
+ ].join('\n');
39
+ fs.writeFileSync(path.join(phaseDir, 'tasks', '06-01-T01.md'), taskMd, 'utf-8');
40
+
41
+ const cap = _capture();
42
+ const p = subcmd.run(['6'], { cwd: sandbox, stdout: cap.stub });
43
+ assert.equal(p.status, 'advancement-blocked');
44
+ assert.equal(p.reason, 'rule-3-null-task');
45
+ });
46
+
47
+ test('AU-2: ok status when gate is not rule-3-null-task', () => {
48
+ const sandbox = makeSandbox();
49
+ seedRoadmapYaml(sandbox, _baseRoadmap());
50
+
51
+ seedPhaseDir(sandbox, 6, 'execution', {});
52
+ const cap = _capture();
53
+ const p = subcmd.run(['6'], { cwd: sandbox, stdout: cap.stub });
54
+ assert.equal(p.status, 'ok');
55
+ assert.equal(p.gate.rule, 1);
56
+ });
57
+
58
+ test('AU-3: accepts `init <phase>` verb form', () => {
59
+ const sandbox = makeSandbox();
60
+ seedRoadmapYaml(sandbox, _baseRoadmap());
61
+ seedPhaseDir(sandbox, 6, 'execution', {});
62
+ const cap = _capture();
63
+ const p = subcmd.run(['init', '6'], { cwd: sandbox, stdout: cap.stub });
64
+ assert.equal(p.status, 'ok');
65
+ });
66
+
67
+ test('AU-4: missing phase arg → autonomous-invalid-phase', () => {
68
+ const sandbox = makeSandbox();
69
+ const cap = _capture();
70
+ assert.throws(
71
+ () => subcmd.run([], { cwd: sandbox, stdout: cap.stub }),
72
+ (err) => err && err.code === 'autonomous-invalid-phase',
73
+ );
74
+ });
@@ -0,0 +1,101 @@
1
+ const { NubosPilotError } = require('../../lib/core.cjs');
2
+ const { TASK_ID_RE } = require('../../lib/tasks.cjs');
3
+ const {
4
+ startTask,
5
+ writeCheckpoint,
6
+ readCheckpoint,
7
+ } = require('../../lib/checkpoint.cjs');
8
+
9
+ const _VALID_STATUSES = new Set(['in-progress', 'verifying', 'pre-commit', 'done']);
10
+
11
+ function _parseFlags(rest) {
12
+ const flags = {};
13
+ for (let i = 0; i < rest.length; i++) {
14
+ const a = rest[i];
15
+ if (a === '--phase' || a === '--plan' || a === '--wave') {
16
+ flags[a.slice(2)] = rest[i + 1];
17
+ i += 1;
18
+ }
19
+ }
20
+ return flags;
21
+ }
22
+
23
+ function _assertTaskId(id) {
24
+ if (typeof id !== 'string' || !TASK_ID_RE.test(id)) {
25
+ throw new NubosPilotError(
26
+ 'checkpoint-invalid-task-id',
27
+ 'Invalid task-id format: ' + id + ' (expected <NN-NN-TNN>)',
28
+ { id },
29
+ );
30
+ }
31
+ }
32
+
33
+ function run(args, ctx) {
34
+ const context = ctx || {};
35
+ const cwd = context.cwd || process.cwd();
36
+ const stdout = context.stdout || process.stdout;
37
+ const list = Array.isArray(args) ? args : [];
38
+ const verb = list[0];
39
+ const taskId = list[1];
40
+
41
+ switch (verb) {
42
+ case 'start': {
43
+ _assertTaskId(taskId);
44
+ const flags = _parseFlags(list.slice(2));
45
+ const task = {
46
+ id: taskId,
47
+ phase: flags.phase != null ? Number(flags.phase) : null,
48
+ plan: flags.plan != null ? String(flags.plan) : null,
49
+ wave: flags.wave != null ? Number(flags.wave) : null,
50
+ };
51
+ const cp = startTask(task, cwd);
52
+ stdout.write(JSON.stringify(cp));
53
+ return cp;
54
+ }
55
+ case 'transition': {
56
+ _assertTaskId(taskId);
57
+ const status = list[2];
58
+ if (!_VALID_STATUSES.has(status)) {
59
+ throw new NubosPilotError(
60
+ 'checkpoint-invalid-status',
61
+ 'Invalid checkpoint status: ' + status + ' (allowed: ' + [..._VALID_STATUSES].join(', ') + ')',
62
+ { status, allowed: [..._VALID_STATUSES] },
63
+ );
64
+ }
65
+ const cp = writeCheckpoint(taskId, { status }, cwd);
66
+ stdout.write(JSON.stringify(cp));
67
+ return cp;
68
+ }
69
+ case 'touch': {
70
+ _assertTaskId(taskId);
71
+ const file = list[2];
72
+ if (!file) {
73
+ throw new NubosPilotError(
74
+ 'checkpoint-missing-file',
75
+ 'checkpoint touch requires <file>',
76
+ { taskId },
77
+ );
78
+ }
79
+ const existing = readCheckpoint(taskId, cwd) || { files_touched: [] };
80
+ const touched = Array.isArray(existing.files_touched) ? existing.files_touched.slice() : [];
81
+ if (!touched.includes(file)) touched.push(file);
82
+ const cp = writeCheckpoint(taskId, { files_touched: touched }, cwd);
83
+ stdout.write(JSON.stringify(cp));
84
+ return cp;
85
+ }
86
+ case 'show': {
87
+ _assertTaskId(taskId);
88
+ const cp = readCheckpoint(taskId, cwd);
89
+ stdout.write(JSON.stringify(cp));
90
+ return cp;
91
+ }
92
+ default:
93
+ throw new NubosPilotError(
94
+ 'checkpoint-unknown-verb',
95
+ 'checkpoint: unknown verb: ' + String(verb) + ' (allowed: start, transition, touch, show)',
96
+ { verb },
97
+ );
98
+ }
99
+ }
100
+
101
+ module.exports = { run };
@@ -0,0 +1,119 @@
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
+
7
+ const subcmd = require('./checkpoint.cjs');
8
+
9
+ const _roots = [];
10
+
11
+ function makeRoot() {
12
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-cp-cli-'));
13
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
14
+ fs.writeFileSync(path.join(root, '.nubos-pilot', 'STATE.md'), `---
15
+ schema_version: 2
16
+ milestone: m1
17
+ milestone_name: m1
18
+ current_phase: null
19
+ current_plan: null
20
+ current_task: null
21
+ last_updated: "2026-04-15T00:00:00Z"
22
+ progress:
23
+ total_phases: 0
24
+ completed_phases: 0
25
+ total_plans: 0
26
+ completed_plans: 0
27
+ percent: 0
28
+ session:
29
+ stopped_at: null
30
+ resume_file: null
31
+ last_activity: null
32
+ ---
33
+
34
+ # State
35
+ `, 'utf-8');
36
+ _roots.push(root);
37
+ return root;
38
+ }
39
+
40
+ function _capture() {
41
+ let buf = '';
42
+ const stub = { write: (s) => { buf += s; return true; } };
43
+ return { stub, get: () => buf };
44
+ }
45
+
46
+ after(() => {
47
+ while (_roots.length) {
48
+ const r = _roots.pop();
49
+ try { fs.rmSync(r, { recursive: true, force: true }); } catch {}
50
+ }
51
+ });
52
+
53
+ test('CPT-1: checkpoint start writes file and updates STATE.current_task', () => {
54
+ const root = makeRoot();
55
+ const cap = _capture();
56
+ subcmd.run(['start', '06-01-T01', '--phase', '6', '--plan', '06-01', '--wave', '1'], { cwd: root, stdout: cap.stub });
57
+ const cp = JSON.parse(fs.readFileSync(path.join(root, '.nubos-pilot', 'checkpoints', '06-01-T01.json'), 'utf-8'));
58
+ assert.equal(cp.task_id, '06-01-T01');
59
+ assert.equal(cp.status, 'in-progress');
60
+ const state = fs.readFileSync(path.join(root, '.nubos-pilot', 'STATE.md'), 'utf-8');
61
+ assert.ok(state.includes('current_task: 06-01-T01'), state);
62
+ });
63
+
64
+ test('CPT-2: checkpoint transition updates status', () => {
65
+ const root = makeRoot();
66
+ const cap = _capture();
67
+ subcmd.run(['start', '06-01-T02', '--phase', '6', '--plan', '06-01', '--wave', '1'], { cwd: root, stdout: cap.stub });
68
+ subcmd.run(['transition', '06-01-T02', 'verifying'], { cwd: root, stdout: cap.stub });
69
+ const cp = JSON.parse(fs.readFileSync(path.join(root, '.nubos-pilot', 'checkpoints', '06-01-T02.json'), 'utf-8'));
70
+ assert.equal(cp.status, 'verifying');
71
+ });
72
+
73
+ test('CPT-3: checkpoint transition rejects unknown status', () => {
74
+ const root = makeRoot();
75
+ const cap = _capture();
76
+ subcmd.run(['start', '06-01-T03', '--phase', '6', '--plan', '06-01', '--wave', '1'], { cwd: root, stdout: cap.stub });
77
+ assert.throws(
78
+ () => subcmd.run(['transition', '06-01-T03', 'weird'], { cwd: root, stdout: cap.stub }),
79
+ (err) => err && err.code === 'checkpoint-invalid-status',
80
+ );
81
+ });
82
+
83
+ test('CPT-4: checkpoint touch appends to files_touched', () => {
84
+ const root = makeRoot();
85
+ const cap = _capture();
86
+ subcmd.run(['start', '06-01-T04', '--phase', '6', '--plan', '06-01', '--wave', '1'], { cwd: root, stdout: cap.stub });
87
+ subcmd.run(['touch', '06-01-T04', 'src/a.ts'], { cwd: root, stdout: cap.stub });
88
+ subcmd.run(['touch', '06-01-T04', 'src/b.ts'], { cwd: root, stdout: cap.stub });
89
+ const cp = JSON.parse(fs.readFileSync(path.join(root, '.nubos-pilot', 'checkpoints', '06-01-T04.json'), 'utf-8'));
90
+ assert.deepEqual(cp.files_touched, ['src/a.ts', 'src/b.ts']);
91
+ });
92
+
93
+ test('CPT-5: checkpoint show emits JSON', () => {
94
+ const root = makeRoot();
95
+ const cap1 = _capture();
96
+ subcmd.run(['start', '06-01-T05', '--phase', '6', '--plan', '06-01', '--wave', '1'], { cwd: root, stdout: cap1.stub });
97
+ const cap2 = _capture();
98
+ subcmd.run(['show', '06-01-T05'], { cwd: root, stdout: cap2.stub });
99
+ const json = JSON.parse(cap2.get());
100
+ assert.equal(json.task_id, '06-01-T05');
101
+ });
102
+
103
+ test('CPT-6: unknown verb throws', () => {
104
+ const root = makeRoot();
105
+ const cap = _capture();
106
+ assert.throws(
107
+ () => subcmd.run(['bogus', '06-01-T01'], { cwd: root, stdout: cap.stub }),
108
+ (err) => err && err.code === 'checkpoint-unknown-verb',
109
+ );
110
+ });
111
+
112
+ test('CPT-7: invalid task-id format rejected (defense-in-depth)', () => {
113
+ const root = makeRoot();
114
+ const cap = _capture();
115
+ assert.throws(
116
+ () => subcmd.run(['start', 'not-a-task-id'], { cwd: root, stdout: cap.stub }),
117
+ (err) => err && err.code === 'checkpoint-invalid-task-id',
118
+ );
119
+ });