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,371 @@
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
+ const os = require('node:os');
6
+
7
+ const roadmap = require('./roadmap.cjs');
8
+
9
+ const FIXTURES = path.join(__dirname, 'fixtures', 'roadmap');
10
+ const MINIMAL = fs.readFileSync(path.join(FIXTURES, 'roadmap-minimal.yaml'), 'utf-8');
11
+ const MALFORMED = fs.readFileSync(path.join(FIXTURES, 'roadmap-malformed.yaml'), 'utf-8');
12
+ const TEN_PHASES = fs.readFileSync(path.join(FIXTURES, 'roadmap-ten-phases.yaml'), 'utf-8');
13
+
14
+ const _sandboxes = [];
15
+
16
+ function makeSandbox(roadmapContent) {
17
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-rm-'));
18
+ fs.mkdirSync(path.join(dir, '.nubos-pilot'));
19
+ if (roadmapContent !== null) {
20
+ fs.writeFileSync(path.join(dir, '.nubos-pilot', 'roadmap.yaml'), roadmapContent);
21
+ }
22
+ _sandboxes.push(dir);
23
+ return dir;
24
+ }
25
+
26
+ afterEach(() => {
27
+ while (_sandboxes.length) {
28
+ const dir = _sandboxes.pop();
29
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
30
+ }
31
+ });
32
+
33
+ test('RM-1: parseRoadmap returns 3 phases', () => {
34
+ const sandbox = makeSandbox(MINIMAL);
35
+ const r = roadmap.parseRoadmap(sandbox);
36
+ assert.equal(r.phases.length, 3);
37
+ });
38
+
39
+ test('RM-2: Phase 1 has goal, requirements [F-01, F-02], 2 success criteria', () => {
40
+ const sandbox = makeSandbox(MINIMAL);
41
+ const r = roadmap.parseRoadmap(sandbox);
42
+ const p1 = r.phases.find(p => p.number === '1');
43
+ assert.ok(p1, 'phase 1 present');
44
+ assert.ok(p1.goal && p1.goal.length > 0, 'goal non-empty');
45
+ assert.deepEqual(p1.requirements, ['F-01', 'F-02']);
46
+ assert.equal(p1.success_criteria.length, 2);
47
+ });
48
+
49
+ test('RM-3: Phase 2.1 parsed with number === "2.1" (decimal-safe)', () => {
50
+ const sandbox = makeSandbox(MINIMAL);
51
+ const r = roadmap.parseRoadmap(sandbox);
52
+ const decimal = r.phases.find(p => p.number === '2.1');
53
+ assert.ok(decimal, 'decimal phase present');
54
+ assert.equal(decimal.name, 'Hotfix');
55
+ });
56
+
57
+ test('RM-4: getPhase resolves integer and decimal', () => {
58
+ const sandbox = makeSandbox(MINIMAL);
59
+ assert.equal(roadmap.getPhase(3, sandbox).number, '3');
60
+ assert.equal(roadmap.getPhase('2.1', sandbox).name, 'Hotfix');
61
+ });
62
+
63
+ test('RM-5: phaseComplete reflects table state', () => {
64
+ const sandbox = makeSandbox(MINIMAL);
65
+ assert.equal(roadmap.phaseComplete(1, sandbox), true);
66
+ assert.equal(roadmap.phaseComplete(3, sandbox), false);
67
+ });
68
+
69
+ test('RM-6: listPhases returns 3 phases', () => {
70
+ const sandbox = makeSandbox(MINIMAL);
71
+ assert.equal(roadmap.listPhases(sandbox).length, 3);
72
+ });
73
+
74
+ test('RM-7: Phase 1 plans[0].complete === true (checkbox [x])', () => {
75
+ const sandbox = makeSandbox(MINIMAL);
76
+ const p1 = roadmap.getPhase(1, sandbox);
77
+ assert.ok(Array.isArray(p1.plans));
78
+ assert.ok(p1.plans.length >= 1, 'at least one plan');
79
+ assert.equal(p1.plans[0].complete, true);
80
+ });
81
+
82
+ test('RM-8: Phase 1 depends_on mentions Nothing', () => {
83
+ const sandbox = makeSandbox(MINIMAL);
84
+ const p1 = roadmap.getPhase(1, sandbox);
85
+ assert.ok(p1.depends_on && p1.depends_on.includes('Nothing'));
86
+ });
87
+
88
+ test('RM-9: Phase 3 depends_on mentions Phase 2', () => {
89
+ const sandbox = makeSandbox(MINIMAL);
90
+ const p3 = roadmap.getPhase(3, sandbox);
91
+ assert.ok(p3.depends_on && p3.depends_on.includes('Phase 2'));
92
+ });
93
+
94
+ test('RM-10: missing ROADMAP.md → NubosPilotError roadmap-parse-error', () => {
95
+ const sandbox = makeSandbox(null);
96
+ assert.throws(
97
+ () => roadmap.parseRoadmap(sandbox),
98
+ (err) => err.name === 'NubosPilotError' && err.code === 'roadmap-parse-error',
99
+ );
100
+ });
101
+
102
+ test('RM-11: malformed ROADMAP (no Phase Details) → roadmap-parse-error', () => {
103
+ const sandbox = makeSandbox(MALFORMED);
104
+ assert.throws(
105
+ () => roadmap.parseRoadmap(sandbox),
106
+ (err) => err.name === 'NubosPilotError' && err.code === 'roadmap-parse-error',
107
+ );
108
+ });
109
+
110
+ test('RM-12: getPhase(999) → phase-not-found', () => {
111
+ const sandbox = makeSandbox(MINIMAL);
112
+ assert.throws(
113
+ () => roadmap.getPhase(999, sandbox),
114
+ (err) => err.name === 'NubosPilotError' && err.code === 'phase-not-found',
115
+ );
116
+ });
117
+
118
+ test('RM-13: 10-phase roadmap → parseRoadmap returns 10 phases, getPhase(3) requirements match LIB-03..LIB-08', () => {
119
+ const sandbox = makeSandbox(TEN_PHASES);
120
+ const r = roadmap.parseRoadmap(sandbox);
121
+ assert.equal(r.phases.length, 10);
122
+ const p3 = roadmap.getPhase(3, sandbox);
123
+ assert.deepEqual(p3.requirements, ['LIB-03', 'LIB-04', 'LIB-05', 'LIB-06', 'LIB-07', 'LIB-08']);
124
+ });
125
+
126
+ test('RM-14: 10-phase roadmap → phaseComplete reflects status (Phase 1 done, Phase 2 pending)', () => {
127
+ const sandbox = makeSandbox(TEN_PHASES);
128
+ assert.equal(roadmap.phaseComplete(1, sandbox), true);
129
+ assert.equal(roadmap.phaseComplete(2, sandbox), false);
130
+ });
131
+
132
+ const WRITE_SEED = [
133
+ 'schema_version: 1',
134
+ 'milestones:',
135
+ ' - id: v1.0',
136
+ ' name: first',
137
+ ' phases:',
138
+ ' - number: 1',
139
+ ' name: Foundation',
140
+ ' slug: foundation',
141
+ ' goal: initial',
142
+ ' depends_on: []',
143
+ ' requirements: []',
144
+ ' success_criteria: []',
145
+ ' status: done',
146
+ ' plans: []',
147
+ ' - number: 2',
148
+ ' name: Core',
149
+ ' slug: core',
150
+ ' goal: core',
151
+ ' depends_on: [1]',
152
+ ' requirements: []',
153
+ ' success_criteria: []',
154
+ ' status: pending',
155
+ ' plans: []',
156
+ '',
157
+ ].join('\n');
158
+
159
+ function _readYamlDoc(sandbox) {
160
+ const p = path.join(sandbox, '.nubos-pilot', 'roadmap.yaml');
161
+ const YAML = require('yaml');
162
+ return YAML.parse(fs.readFileSync(p, 'utf-8'));
163
+ }
164
+
165
+ test('WR-1: addMilestone appends a new milestone to roadmap.yaml', () => {
166
+ const sandbox = makeSandbox(WRITE_SEED);
167
+ roadmap.addMilestone({ id: 'v2.0', name: 'second', phases: [] }, sandbox);
168
+ const doc = _readYamlDoc(sandbox);
169
+ assert.equal(doc.milestones.length, 2);
170
+ assert.equal(doc.milestones[1].id, 'v2.0');
171
+ assert.deepEqual(doc.milestones[1].phases, []);
172
+ });
173
+
174
+ test('WR-2: addMilestone with duplicate id throws roadmap-duplicate-milestone', () => {
175
+ const sandbox = makeSandbox(WRITE_SEED);
176
+ assert.throws(
177
+ () => roadmap.addMilestone({ id: 'v1.0', name: 'dup', phases: [] }, sandbox),
178
+ (err) => err.name === 'NubosPilotError' && err.code === 'roadmap-duplicate-milestone',
179
+ );
180
+ });
181
+
182
+ test('WR-3: addPhase appends phase with number = max+1', () => {
183
+ const sandbox = makeSandbox(WRITE_SEED);
184
+ const result = roadmap.addPhase(
185
+ 'v1.0',
186
+ { slug: 'new-phase', goal: 'g', depends_on: [], requirements: [] },
187
+ sandbox,
188
+ );
189
+ assert.equal(result.milestoneId, 'v1.0');
190
+ assert.equal(result.number, 3);
191
+ assert.equal(result.slug, 'new-phase');
192
+ const doc = _readYamlDoc(sandbox);
193
+ const ms = doc.milestones.find((m) => m.id === 'v1.0');
194
+ assert.equal(ms.phases.length, 3);
195
+ assert.equal(ms.phases[2].number, 3);
196
+ });
197
+
198
+ test('WR-4: addPhase with unknown milestone throws roadmap-milestone-not-found', () => {
199
+ const sandbox = makeSandbox(WRITE_SEED);
200
+ assert.throws(
201
+ () => roadmap.addPhase('v9.9', { slug: 'x' }, sandbox),
202
+ (err) => err.name === 'NubosPilotError' && err.code === 'roadmap-milestone-not-found',
203
+ );
204
+ });
205
+
206
+ test('WR-5: addPhase with duplicate slug throws roadmap-duplicate-slug', () => {
207
+ const sandbox = makeSandbox(WRITE_SEED);
208
+ assert.throws(
209
+ () => roadmap.addPhase('v1.0', { slug: 'core' }, sandbox),
210
+ (err) => err.name === 'NubosPilotError' && err.code === 'roadmap-duplicate-slug',
211
+ );
212
+ });
213
+
214
+ test('WR-6: addPhase with empty slug throws roadmap-invalid-slug', () => {
215
+ const sandbox = makeSandbox(WRITE_SEED);
216
+ assert.throws(
217
+ () => roadmap.addPhase('v1.0', { slug: '' }, sandbox),
218
+ (err) => err.name === 'NubosPilotError' && err.code === 'roadmap-invalid-slug',
219
+ );
220
+ });
221
+
222
+ test('WR-7: addPhase with uppercase/special slug chars throws roadmap-invalid-slug', () => {
223
+ const sandbox = makeSandbox(WRITE_SEED);
224
+ assert.throws(
225
+ () => roadmap.addPhase('v1.0', { slug: 'Bad_Slug' }, sandbox),
226
+ (err) => err.name === 'NubosPilotError' && err.code === 'roadmap-invalid-slug',
227
+ );
228
+ });
229
+
230
+ test('WR-8: insertPhaseAfter creates 7.1 then 7.2 leaving downstream depends_on intact', () => {
231
+
232
+ const seed = WRITE_SEED
233
+ + ' - number: 7\n'
234
+ + ' name: Seven\n'
235
+ + ' slug: seven\n'
236
+ + ' goal: s\n'
237
+ + ' depends_on: [6]\n'
238
+ + ' requirements: []\n'
239
+ + ' success_criteria: []\n'
240
+ + ' status: pending\n'
241
+ + ' plans: []\n'
242
+ + ' - number: 8\n'
243
+ + ' name: Eight\n'
244
+ + ' slug: eight\n'
245
+ + ' goal: e\n'
246
+ + ' depends_on: [7]\n'
247
+ + ' requirements: []\n'
248
+ + ' success_criteria: []\n'
249
+ + ' status: pending\n'
250
+ + ' plans: []\n';
251
+ const sandbox = makeSandbox(seed);
252
+ const first = roadmap.insertPhaseAfter(
253
+ 7,
254
+ { slug: 'gap-fix-a', goal: 'a', depends_on: [7], requirements: [] },
255
+ sandbox,
256
+ );
257
+ const second = roadmap.insertPhaseAfter(
258
+ 7,
259
+ { slug: 'gap-fix-b', goal: 'b', depends_on: [7], requirements: [] },
260
+ sandbox,
261
+ );
262
+ assert.equal(String(first.number), '7.1');
263
+ assert.equal(String(second.number), '7.2');
264
+ const doc = _readYamlDoc(sandbox);
265
+ const ms = doc.milestones.find((m) => m.id === 'v1.0');
266
+ const eight = ms.phases.find((p) => String(p.number) === '8');
267
+
268
+ assert.deepEqual(eight.depends_on, [7]);
269
+ });
270
+
271
+ test('WR-9: insertPhaseAfter with unknown base number throws roadmap-base-phase-not-found', () => {
272
+ const sandbox = makeSandbox(WRITE_SEED);
273
+ assert.throws(
274
+ () => roadmap.insertPhaseAfter(99, { slug: 'x' }, sandbox),
275
+ (err) => err.name === 'NubosPilotError' && err.code === 'roadmap-base-phase-not-found',
276
+ );
277
+ });
278
+
279
+ test('WR-10: after write, ROADMAP.md on disk matches rendered markdown of new doc', () => {
280
+ const sandbox = makeSandbox(WRITE_SEED);
281
+ roadmap.addPhase('v1.0', { slug: 'render-check', goal: 'rc' }, sandbox);
282
+ const mdPath = path.join(sandbox, '.nubos-pilot', 'ROADMAP.md');
283
+ const md = fs.readFileSync(mdPath, 'utf-8');
284
+
285
+ assert.ok(md.includes('render-check') || md.includes('3.'));
286
+
287
+ assert.ok(md.includes('Generated from roadmap.yaml'));
288
+ });
289
+
290
+ test('WR-11: concurrent addPhase serialises — both end up with distinct numbers', async () => {
291
+ const sandbox = makeSandbox(WRITE_SEED);
292
+ await Promise.all([
293
+ Promise.resolve().then(() =>
294
+ roadmap.addPhase('v1.0', { slug: 'p-a', goal: 'a' }, sandbox),
295
+ ),
296
+ Promise.resolve().then(() =>
297
+ roadmap.addPhase('v1.0', { slug: 'p-b', goal: 'b' }, sandbox),
298
+ ),
299
+ ]);
300
+ const doc = _readYamlDoc(sandbox);
301
+ const ms = doc.milestones.find((m) => m.id === 'v1.0');
302
+ const numbers = ms.phases.map((p) => Number(p.number)).sort();
303
+
304
+ assert.deepEqual(numbers, [1, 2, 3, 4]);
305
+ const slugs = ms.phases.map((p) => p.slug).sort();
306
+ assert.ok(slugs.includes('p-a') && slugs.includes('p-b'));
307
+ });
308
+
309
+ test('ROAD-ADD-BACKLOG-1: addBacklogEntry creates synthetic backlog milestone and 999.1 phase', () => {
310
+ const sandbox = makeSandbox(WRITE_SEED);
311
+ const res = roadmap.addBacklogEntry('Fix deploy key auth', { cwd: sandbox });
312
+ assert.equal(res.backlog_number, '999.1');
313
+ assert.equal(res.backlog_slug, 'fix-deploy-key-auth');
314
+ const doc = _readYamlDoc(sandbox);
315
+ const backlog = doc.milestones.find((m) => m.id === 'backlog');
316
+ assert.ok(backlog, 'backlog milestone present');
317
+ assert.equal(backlog.phases.length, 1);
318
+ assert.equal(backlog.phases[0].number, '999.1');
319
+ assert.equal(backlog.phases[0].name, 'Fix deploy key auth');
320
+ const md = fs.readFileSync(path.join(sandbox, '.nubos-pilot', 'ROADMAP.md'), 'utf-8');
321
+ assert.match(md, /## Backlog/);
322
+ assert.match(md, /Phase 999\.1: Fix deploy key auth/);
323
+ });
324
+
325
+ test('ROAD-ADD-BACKLOG-2: second call numbers 999.2', () => {
326
+ const sandbox = makeSandbox(WRITE_SEED);
327
+ roadmap.addBacklogEntry('Idea one', { cwd: sandbox });
328
+ const res = roadmap.addBacklogEntry('Idea two', { cwd: sandbox });
329
+ assert.equal(res.backlog_number, '999.2');
330
+ const doc = _readYamlDoc(sandbox);
331
+ const backlog = doc.milestones.find((m) => m.id === 'backlog');
332
+ assert.equal(backlog.phases.length, 2);
333
+ });
334
+
335
+ test('ROAD-ADD-BACKLOG-3: empty description rejected with roadmap-invalid-description', () => {
336
+ const sandbox = makeSandbox(WRITE_SEED);
337
+ assert.throws(
338
+ () => roadmap.addBacklogEntry('', { cwd: sandbox }),
339
+ (err) => err.name === 'NubosPilotError' && err.code === 'roadmap-invalid-description',
340
+ );
341
+ });
342
+
343
+ test('ROAD-COLLAPSE-1: collapseMilestone sets collapsed=true + collapsed_at, emits <details>', () => {
344
+ const sandbox = makeSandbox(WRITE_SEED);
345
+ const res = roadmap.collapseMilestone('v1.0', { cwd: sandbox });
346
+ assert.equal(res.milestoneId, 'v1.0');
347
+ assert.equal(res.already_collapsed, false);
348
+ const doc = _readYamlDoc(sandbox);
349
+ const m = doc.milestones.find((x) => x.id === 'v1.0');
350
+ assert.equal(m.collapsed, true);
351
+ assert.match(m.collapsed_at, /^\d{4}-\d{2}-\d{2}$/);
352
+ const md = fs.readFileSync(path.join(sandbox, '.nubos-pilot', 'ROADMAP.md'), 'utf-8');
353
+ assert.match(md, /<details>/);
354
+ assert.match(md, /<\/details>/);
355
+ assert.match(md, /v1\.0 — completed on \d{4}-\d{2}-\d{2}/);
356
+ });
357
+
358
+ test('ROAD-COLLAPSE-2: second call idempotent — already_collapsed true, no throw', () => {
359
+ const sandbox = makeSandbox(WRITE_SEED);
360
+ roadmap.collapseMilestone('v1.0', { cwd: sandbox });
361
+ const res = roadmap.collapseMilestone('v1.0', { cwd: sandbox });
362
+ assert.equal(res.already_collapsed, true);
363
+ });
364
+
365
+ test('ROAD-COLLAPSE-3: unknown milestoneId throws roadmap-milestone-not-found', () => {
366
+ const sandbox = makeSandbox(WRITE_SEED);
367
+ assert.throws(
368
+ () => roadmap.collapseMilestone('v9.9', { cwd: sandbox }),
369
+ (err) => err.name === 'NubosPilotError' && err.code === 'roadmap-milestone-not-found',
370
+ );
371
+ });
@@ -0,0 +1,61 @@
1
+ const { test } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const { listRuntimes, getAdapter } = require('./index.cjs');
4
+
5
+ const REQUIRED_KEYS = ['name', 'detectHints', 'capabilities', 'paths', 'askUser'];
6
+ const CAP_KEYS = ['askUserQuestion', 'slashCommands', 'agentsMd', 'textMode', 'modelResolution'];
7
+ const VALID_TEXT_MODE = ['auto', 'force', 'off'];
8
+ const VALID_MODEL_RES = ['explicit', 'inherit', 'profile'];
9
+ const VALID_AGENTS_MD = [null, 'AGENTS.md', 'GEMINI.md', 'CLAUDE.md'];
10
+
11
+ for (const name of listRuntimes()) {
12
+ test('RT-contract(' + name + '): exports all required keys', () => {
13
+ const a = getAdapter(name);
14
+ for (const k of REQUIRED_KEYS) {
15
+ assert.ok(k in a, name + ' missing ' + k);
16
+ }
17
+ assert.equal(a.name, name);
18
+ });
19
+
20
+ test('RT-contract(' + name + '): capabilities shape valid', () => {
21
+ const caps = getAdapter(name).capabilities;
22
+ for (const k of CAP_KEYS) {
23
+ assert.ok(k in caps, name + ' missing cap ' + k);
24
+ }
25
+ assert.equal(typeof caps.askUserQuestion, 'boolean');
26
+ assert.equal(typeof caps.slashCommands, 'boolean');
27
+ assert.ok(
28
+ VALID_TEXT_MODE.includes(caps.textMode),
29
+ name + ' bad textMode: ' + caps.textMode,
30
+ );
31
+ assert.ok(
32
+ VALID_MODEL_RES.includes(caps.modelResolution),
33
+ name + ' bad modelResolution: ' + caps.modelResolution,
34
+ );
35
+ assert.ok(
36
+ VALID_AGENTS_MD.includes(caps.agentsMd),
37
+ name + ' bad agentsMd: ' + caps.agentsMd,
38
+ );
39
+ });
40
+
41
+ test('RT-contract(' + name + '): askUser is a function', () => {
42
+ const a = getAdapter(name);
43
+ assert.equal(typeof a.askUser, 'function');
44
+
45
+ });
46
+
47
+ test('RT-contract(' + name + '): detectHints shape valid', () => {
48
+ const dh = getAdapter(name).detectHints;
49
+ assert.ok(dh && typeof dh === 'object', name + ' detectHints not object');
50
+ assert.ok(Array.isArray(dh.env), name + ' detectHints.env not array');
51
+ assert.equal(typeof dh.pathBinary, 'string');
52
+ assert.ok(Array.isArray(dh.diskMarkers), name + ' detectHints.diskMarkers not array');
53
+ });
54
+ }
55
+
56
+ test('RT-contract: getAdapter throws on unknown runtime', () => {
57
+ assert.throws(
58
+ () => getAdapter('nonexistent'),
59
+ (err) => err && err.code === 'runtime-unknown',
60
+ );
61
+ });
@@ -0,0 +1,119 @@
1
+ const readline = require('node:readline');
2
+ const { NubosPilotError } = require('../core.cjs');
3
+
4
+ let _readlineImpl = null;
5
+
6
+ function _setReadlineImplForTests(impl) {
7
+ _readlineImpl = impl || null;
8
+ }
9
+
10
+ function _readOneLine() {
11
+ if (_readlineImpl) return Promise.resolve(_readlineImpl());
12
+ return new Promise((resolve, reject) => {
13
+ const rl = readline.createInterface({
14
+ input: process.stdin,
15
+ output: process.stderr,
16
+ terminal: false,
17
+ });
18
+ let done = false;
19
+ rl.once('line', (line) => {
20
+ if (done) return;
21
+ done = true;
22
+ rl.close();
23
+ resolve(line);
24
+ });
25
+ rl.once('close', () => {
26
+ if (done) return;
27
+ done = true;
28
+ resolve('');
29
+ });
30
+ rl.once('error', (err) => {
31
+ if (done) return;
32
+ done = true;
33
+ reject(err);
34
+ });
35
+ });
36
+ }
37
+
38
+ function _parseAnswer(type, rawLine, options, def) {
39
+ const line = (rawLine == null ? '' : String(rawLine)).trim();
40
+ if (type === 'select') {
41
+ if (line === '' && def != null) return def;
42
+ const n = Number(line);
43
+ if (!Number.isInteger(n) || n < 1 || !options || n > options.length) {
44
+ throw new NubosPilotError(
45
+ 'askuser-invalid-response',
46
+ 'Invalid select index: ' + line,
47
+ { line, optionsCount: options ? options.length : 0 },
48
+ );
49
+ }
50
+ return options[n - 1];
51
+ }
52
+ if (type === 'multiselect') {
53
+ if (line === '' && def != null) return def;
54
+ const parts = line.split(',').map((s) => s.trim()).filter(Boolean);
55
+ const picks = [];
56
+ for (const p of parts) {
57
+ const n = Number(p);
58
+ if (!Number.isInteger(n) || n < 1 || !options || n > options.length) {
59
+ throw new NubosPilotError(
60
+ 'askuser-invalid-response',
61
+ 'Invalid multiselect index: ' + p,
62
+ { line, part: p },
63
+ );
64
+ }
65
+ picks.push(options[n - 1]);
66
+ }
67
+ return picks;
68
+ }
69
+ if (type === 'confirm') {
70
+ if (line === '' && def != null) return def;
71
+ if (/^y(es)?$/i.test(line)) return true;
72
+ if (/^n(o)?$/i.test(line)) return false;
73
+ if (def != null) return def;
74
+ throw new NubosPilotError(
75
+ 'askuser-invalid-response',
76
+ 'Invalid confirm answer: ' + line,
77
+ { line },
78
+ );
79
+ }
80
+ if (type === 'input') {
81
+ if (line === '' && def != null) return def;
82
+ return rawLine == null ? '' : String(rawLine);
83
+ }
84
+ throw new NubosPilotError(
85
+ 'askuser-invalid-type',
86
+ 'Unknown askUser type: ' + type,
87
+ { type },
88
+ );
89
+ }
90
+
91
+ async function askUserReadline({ type, question, options, def }) {
92
+ const hasTTY = !!process.stdin.isTTY;
93
+ if (!hasTTY && !_readlineImpl) {
94
+ if (def != null) return { value: def, source: 'default' };
95
+ throw new NubosPilotError(
96
+ 'askuser-no-tty',
97
+ 'askUser cannot prompt without TTY',
98
+ { question },
99
+ );
100
+ }
101
+ process.stderr.write(question + '\n');
102
+ if (type === 'select' || type === 'multiselect') {
103
+ if (options) {
104
+ for (let i = 0; i < options.length; i++) {
105
+ process.stderr.write(' ' + (i + 1) + ') ' + String(options[i]) + '\n');
106
+ }
107
+ }
108
+ if (type === 'multiselect') process.stderr.write('(comma-separated indices) ');
109
+ else process.stderr.write('> ');
110
+ } else if (type === 'confirm') {
111
+ process.stderr.write('[y/n] ');
112
+ } else {
113
+ process.stderr.write('> ');
114
+ }
115
+ const line = await _readOneLine();
116
+ return { value: _parseAnswer(type, line, options, def), source: 'readline' };
117
+ }
118
+
119
+ module.exports = { askUserReadline, _readOneLine, _parseAnswer, _setReadlineImplForTests };
@@ -0,0 +1,126 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+
4
+ const rl = require('./_readline.cjs');
5
+
6
+ function captureStderr(fn) {
7
+ const chunks = [];
8
+ const orig = process.stderr.write.bind(process.stderr);
9
+ process.stderr.write = (chunk) => { chunks.push(chunk.toString()); return true; };
10
+ return Promise.resolve(fn()).then(
11
+ (val) => { process.stderr.write = orig; return { val, out: chunks.join('') }; },
12
+ (err) => { process.stderr.write = orig; throw err; },
13
+ );
14
+ }
15
+
16
+ test('RL-1: askUserReadline input returns {value, source:readline} when impl injected', async () => {
17
+ rl._setReadlineImplForTests(async () => 'typed');
18
+ try {
19
+ const { val } = await captureStderr(() =>
20
+ rl.askUserReadline({ type: 'input', question: 'Q' }),
21
+ );
22
+ assert.equal(val.value, 'typed');
23
+ assert.equal(val.source, 'readline');
24
+ } finally {
25
+ rl._setReadlineImplForTests(null);
26
+ }
27
+ });
28
+
29
+ test('RL-2: askUserReadline select parses 1-based index into option', async () => {
30
+ rl._setReadlineImplForTests(async () => '2');
31
+ try {
32
+ const { val } = await captureStderr(() =>
33
+ rl.askUserReadline({ type: 'select', question: 'Pick', options: ['A', 'B', 'C'] }),
34
+ );
35
+ assert.equal(val.value, 'B');
36
+ assert.equal(val.source, 'readline');
37
+ } finally {
38
+ rl._setReadlineImplForTests(null);
39
+ }
40
+ });
41
+
42
+ test('RL-3: askUserReadline multiselect parses comma-separated indices', async () => {
43
+ rl._setReadlineImplForTests(async () => '1,3');
44
+ try {
45
+ const { val } = await captureStderr(() =>
46
+ rl.askUserReadline({ type: 'multiselect', question: 'Pick', options: ['A', 'B', 'C'] }),
47
+ );
48
+ assert.deepEqual(val.value, ['A', 'C']);
49
+ assert.equal(val.source, 'readline');
50
+ } finally {
51
+ rl._setReadlineImplForTests(null);
52
+ }
53
+ });
54
+
55
+ test('RL-4: askUserReadline confirm y → true, n → false', async () => {
56
+ rl._setReadlineImplForTests(async () => 'y');
57
+ try {
58
+ const { val: v1 } = await captureStderr(() =>
59
+ rl.askUserReadline({ type: 'confirm', question: 'OK?' }),
60
+ );
61
+ assert.equal(v1.value, true);
62
+
63
+ rl._setReadlineImplForTests(async () => 'n');
64
+ const { val: v2 } = await captureStderr(() =>
65
+ rl.askUserReadline({ type: 'confirm', question: 'OK?' }),
66
+ );
67
+ assert.equal(v2.value, false);
68
+ } finally {
69
+ rl._setReadlineImplForTests(null);
70
+ }
71
+ });
72
+
73
+ test('RL-5: no TTY + no impl + default set → returns {value:def, source:default}', async () => {
74
+ const origIsTTY = process.stdin.isTTY;
75
+ rl._setReadlineImplForTests(null);
76
+ try {
77
+ Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
78
+ const res = await rl.askUserReadline({ type: 'input', question: 'Q', def: 'd' });
79
+ assert.equal(res.value, 'd');
80
+ assert.equal(res.source, 'default');
81
+ } finally {
82
+ Object.defineProperty(process.stdin, 'isTTY', { value: origIsTTY, configurable: true });
83
+ }
84
+ });
85
+
86
+ test('RL-6: no TTY + no impl + no default → throws askuser-no-tty', async () => {
87
+ const origIsTTY = process.stdin.isTTY;
88
+ rl._setReadlineImplForTests(null);
89
+ try {
90
+ Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
91
+ await assert.rejects(
92
+ () => rl.askUserReadline({ type: 'input', question: 'Q' }),
93
+ (err) => err && err.code === 'askuser-no-tty',
94
+ );
95
+ } finally {
96
+ Object.defineProperty(process.stdin, 'isTTY', { value: origIsTTY, configurable: true });
97
+ }
98
+ });
99
+
100
+ test('RL-7: _parseAnswer(input, hello, null, null) returns hello', () => {
101
+ assert.equal(rl._parseAnswer('input', 'hello', null, null), 'hello');
102
+ });
103
+
104
+ test('RL-8: _parseAnswer(select, 99, [A,B], null) throws askuser-invalid-response', () => {
105
+ assert.throws(
106
+ () => rl._parseAnswer('select', '99', ['A', 'B'], null),
107
+ (err) => err && err.code === 'askuser-invalid-response',
108
+ );
109
+ });
110
+
111
+ test('RL-9: _parseAnswer unknown type throws askuser-invalid-type', () => {
112
+ assert.throws(
113
+ () => rl._parseAnswer('mystery', 'x', null, null),
114
+ (err) => err && err.code === 'askuser-invalid-type',
115
+ );
116
+ });
117
+
118
+ test('RL-10: module exports exactly askUserReadline, _readOneLine, _parseAnswer, _setReadlineImplForTests', () => {
119
+ const keys = Object.keys(rl).sort();
120
+ assert.deepEqual(keys, [
121
+ '_parseAnswer',
122
+ '_readOneLine',
123
+ '_setReadlineImplForTests',
124
+ 'askUserReadline',
125
+ ]);
126
+ });