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,310 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+
4
+ const au = require('./askuser.cjs');
5
+
6
+ const RUNTIME_ENV_KEYS = [
7
+ 'NUBOS_RUNTIME',
8
+ 'CLAUDECODE',
9
+ 'CLAUDE_CODE_ENTRYPOINT',
10
+ 'CODEX_HOME',
11
+ 'CODEX_VERSION',
12
+ 'GEMINI_CLI',
13
+ 'GEMINI_VERSION',
14
+ 'OPENCODE',
15
+ 'OPENCODE_VERSION',
16
+ 'NUBOS_PILOT_REDETECT_RUNTIME',
17
+ ];
18
+
19
+ function snapshotEnv() {
20
+ const snap = {};
21
+ for (const k of RUNTIME_ENV_KEYS) snap[k] = process.env[k];
22
+ return snap;
23
+ }
24
+ function restoreEnv(snap) {
25
+ for (const k of RUNTIME_ENV_KEYS) {
26
+ if (snap[k] === undefined) delete process.env[k];
27
+ else process.env[k] = snap[k];
28
+ }
29
+ }
30
+ function clearAllRuntimeEnv() {
31
+ for (const k of RUNTIME_ENV_KEYS) delete process.env[k];
32
+ }
33
+ function forceRedetect() {
34
+ process.env.NUBOS_PILOT_REDETECT_RUNTIME = '1';
35
+ }
36
+
37
+ function withEnv(mutator, fn) {
38
+ const snap = snapshotEnv();
39
+ try {
40
+ clearAllRuntimeEnv();
41
+ mutator();
42
+ forceRedetect();
43
+ return fn();
44
+ } finally {
45
+ restoreEnv(snap);
46
+ au._setReadlineImplForTests(null);
47
+ }
48
+ }
49
+
50
+ test('AU-1: NUBOS_RUNTIME=claude explicit override → claude', () => {
51
+ withEnv(() => { process.env.NUBOS_RUNTIME = 'claude'; }, () => {
52
+ assert.equal(au._detectRuntime(), 'claude');
53
+ });
54
+ });
55
+
56
+ test('AU-2: NUBOS_RUNTIME=generic-readline beats CLAUDECODE=1 (Pitfall 4)', () => {
57
+ withEnv(() => {
58
+ process.env.NUBOS_RUNTIME = 'generic-readline';
59
+ process.env.CLAUDECODE = '1';
60
+ }, () => {
61
+ assert.equal(au._detectRuntime(), 'generic-readline');
62
+ });
63
+ });
64
+
65
+ test('AU-3: CLAUDECODE=1 → claude', () => {
66
+ withEnv(() => { process.env.CLAUDECODE = '1'; }, () => {
67
+ assert.equal(au._detectRuntime(), 'claude');
68
+ });
69
+ });
70
+
71
+ test('AU-4: CODEX_HOME set → codex', () => {
72
+ withEnv(() => { process.env.CODEX_HOME = '/foo'; }, () => {
73
+ assert.equal(au._detectRuntime(), 'codex');
74
+ });
75
+ });
76
+
77
+ test('AU-5: GEMINI_CLI set → gemini', () => {
78
+ withEnv(() => { process.env.GEMINI_CLI = '1'; }, () => {
79
+ assert.equal(au._detectRuntime(), 'gemini');
80
+ });
81
+ });
82
+
83
+ test('AU-6: OPENCODE_VERSION set → opencode', () => {
84
+ withEnv(() => { process.env.OPENCODE_VERSION = 'x'; }, () => {
85
+ assert.equal(au._detectRuntime(), 'opencode');
86
+ });
87
+ });
88
+
89
+ test('AU-7: no env vars → generic-readline', () => {
90
+ withEnv(() => {}, () => {
91
+ assert.equal(au._detectRuntime(), 'generic-readline');
92
+ });
93
+ });
94
+
95
+ test('AU-8: getRuntime caches — mutation without redetect flag does not change result', () => {
96
+ const snap = snapshotEnv();
97
+ try {
98
+ clearAllRuntimeEnv();
99
+ process.env.CLAUDECODE = '1';
100
+ forceRedetect();
101
+ const first = au.getRuntime();
102
+ assert.equal(first, 'claude');
103
+ delete process.env.NUBOS_PILOT_REDETECT_RUNTIME;
104
+ delete process.env.CLAUDECODE;
105
+ process.env.CODEX_HOME = '/y';
106
+ const second = au.getRuntime();
107
+ assert.equal(second, 'claude');
108
+ } finally {
109
+ restoreEnv(snap);
110
+ }
111
+ });
112
+
113
+ test('AU-9: getRuntime re-detects when NUBOS_PILOT_REDETECT_RUNTIME=1', () => {
114
+ const snap = snapshotEnv();
115
+ try {
116
+ clearAllRuntimeEnv();
117
+ process.env.CLAUDECODE = '1';
118
+ forceRedetect();
119
+ assert.equal(au.getRuntime(), 'claude');
120
+ delete process.env.CLAUDECODE;
121
+ process.env.CODEX_HOME = '/z';
122
+ process.env.NUBOS_PILOT_REDETECT_RUNTIME = '1';
123
+ assert.equal(au.getRuntime(), 'codex');
124
+ } finally {
125
+ restoreEnv(snap);
126
+ }
127
+ });
128
+
129
+ function captureStdout(fn) {
130
+ const chunks = [];
131
+ const orig = process.stdout.write.bind(process.stdout);
132
+ process.stdout.write = (chunk) => { chunks.push(chunk.toString()); return true; };
133
+ return Promise.resolve(fn()).then(
134
+ (val) => { process.stdout.write = orig; return { val, out: chunks.join('') }; },
135
+ (err) => { process.stdout.write = orig; throw err; },
136
+ );
137
+ }
138
+ function captureStderr(fn) {
139
+ const chunks = [];
140
+ const orig = process.stderr.write.bind(process.stderr);
141
+ process.stderr.write = (chunk) => { chunks.push(chunk.toString()); return true; };
142
+ return Promise.resolve(fn()).then(
143
+ (val) => { process.stderr.write = orig; return { val, out: chunks.join('') }; },
144
+ (err) => { process.stderr.write = orig; throw err; },
145
+ );
146
+ }
147
+
148
+ test('AU-10: Claude branch emits askUser v1 marker-block to stdout', async () => {
149
+ const snap = snapshotEnv();
150
+ try {
151
+ clearAllRuntimeEnv();
152
+ process.env.NUBOS_RUNTIME = 'claude';
153
+ forceRedetect();
154
+ au.getRuntime();
155
+ au._setReadlineImplForTests(async () => 'chosen');
156
+ const { val, out } = await captureStdout(() =>
157
+ au.askUser({ type: 'input', question: 'Q' })
158
+ );
159
+ assert.match(out, /<!-- askUser v1 -->/);
160
+ assert.equal(val.source, 'askUserQuestion');
161
+ assert.equal(val.value, 'chosen');
162
+ } finally {
163
+ au._setReadlineImplForTests(null);
164
+ restoreEnv(snap);
165
+ }
166
+ });
167
+
168
+ test('AU-11: marker-block inner JSON contains type, question, options, default keys', async () => {
169
+ const snap = snapshotEnv();
170
+ try {
171
+ clearAllRuntimeEnv();
172
+ process.env.NUBOS_RUNTIME = 'claude';
173
+ forceRedetect();
174
+ au.getRuntime();
175
+ au._setReadlineImplForTests(async () => '1');
176
+ const { out } = await captureStdout(() =>
177
+ au.askUser({ type: 'select', question: 'Pick', options: ['A', 'B'], default: null })
178
+ );
179
+ const match = out.match(/<!--\s*(\{[^]*?\})\s*-->/);
180
+ assert.ok(match, 'inner JSON comment missing');
181
+ const parsed = JSON.parse(match[1]);
182
+ assert.equal(parsed.type, 'select');
183
+ assert.equal(parsed.question, 'Pick');
184
+ assert.deepEqual(parsed.options, ['A', 'B']);
185
+ assert.equal('default' in parsed, true);
186
+ } finally {
187
+ au._setReadlineImplForTests(null);
188
+ restoreEnv(snap);
189
+ }
190
+ });
191
+
192
+ test('AU-12: readline fallback select parses 1-based index', async () => {
193
+ const snap = snapshotEnv();
194
+ try {
195
+ clearAllRuntimeEnv();
196
+ forceRedetect();
197
+ au.getRuntime();
198
+ au._setReadlineImplForTests(async () => '2');
199
+ const { val } = await captureStderr(() =>
200
+ au.askUser({ type: 'select', question: 'P', options: ['A', 'B', 'C'] })
201
+ );
202
+ assert.equal(val.value, 'B');
203
+ assert.equal(val.source, 'readline');
204
+ } finally {
205
+ au._setReadlineImplForTests(null);
206
+ restoreEnv(snap);
207
+ }
208
+ });
209
+
210
+ test('AU-13: readline fallback confirm y/n/default', async () => {
211
+ const snap = snapshotEnv();
212
+ try {
213
+ clearAllRuntimeEnv();
214
+ forceRedetect();
215
+ au.getRuntime();
216
+ au._setReadlineImplForTests(async () => 'y');
217
+ const { val: v1 } = await captureStderr(() =>
218
+ au.askUser({ type: 'confirm', question: 'OK?' })
219
+ );
220
+ assert.equal(v1.value, true);
221
+
222
+ au._setReadlineImplForTests(async () => 'n');
223
+ const { val: v2 } = await captureStderr(() =>
224
+ au.askUser({ type: 'confirm', question: 'OK?' })
225
+ );
226
+ assert.equal(v2.value, false);
227
+
228
+ au._setReadlineImplForTests(async () => '');
229
+ const { val: v3 } = await captureStderr(() =>
230
+ au.askUser({ type: 'confirm', question: 'OK?', default: true })
231
+ );
232
+ assert.equal(v3.value, true);
233
+ } finally {
234
+ au._setReadlineImplForTests(null);
235
+ restoreEnv(snap);
236
+ }
237
+ });
238
+
239
+ test('AU-14: readline fallback input returns line verbatim', async () => {
240
+ const snap = snapshotEnv();
241
+ try {
242
+ clearAllRuntimeEnv();
243
+ forceRedetect();
244
+ au.getRuntime();
245
+ au._setReadlineImplForTests(async () => 'hello');
246
+ const { val } = await captureStderr(() =>
247
+ au.askUser({ type: 'input', question: 'Name?' })
248
+ );
249
+ assert.equal(val.value, 'hello');
250
+ assert.equal(val.source, 'readline');
251
+ } finally {
252
+ au._setReadlineImplForTests(null);
253
+ restoreEnv(snap);
254
+ }
255
+ });
256
+
257
+ test('AU-15: readline multiselect parses comma-separated 1-based indices', async () => {
258
+ const snap = snapshotEnv();
259
+ try {
260
+ clearAllRuntimeEnv();
261
+ forceRedetect();
262
+ au.getRuntime();
263
+ au._setReadlineImplForTests(async () => '1,3');
264
+ const { val } = await captureStderr(() =>
265
+ au.askUser({ type: 'multiselect', question: 'Pick', options: ['A', 'B', 'C'] })
266
+ );
267
+ assert.deepEqual(val.value, ['A', 'C']);
268
+ assert.equal(val.source, 'readline');
269
+ } finally {
270
+ au._setReadlineImplForTests(null);
271
+ restoreEnv(snap);
272
+ }
273
+ });
274
+
275
+ test('AU-16: no TTY, no injected impl, no default → throws askuser-no-tty', async () => {
276
+ const snap = snapshotEnv();
277
+ const origIsTTY = process.stdin.isTTY;
278
+ try {
279
+ clearAllRuntimeEnv();
280
+ forceRedetect();
281
+ au.getRuntime();
282
+ au._setReadlineImplForTests(null);
283
+ Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
284
+ await assert.rejects(
285
+ () => au.askUser({ type: 'input', question: 'Q' }),
286
+ (err) => err && err.code === 'askuser-no-tty',
287
+ );
288
+ } finally {
289
+ Object.defineProperty(process.stdin, 'isTTY', { value: origIsTTY, configurable: true });
290
+ restoreEnv(snap);
291
+ }
292
+ });
293
+
294
+ test('AU-17: no TTY with default provided → returns default', async () => {
295
+ const snap = snapshotEnv();
296
+ const origIsTTY = process.stdin.isTTY;
297
+ try {
298
+ clearAllRuntimeEnv();
299
+ forceRedetect();
300
+ au.getRuntime();
301
+ au._setReadlineImplForTests(null);
302
+ Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
303
+ const res = await au.askUser({ type: 'input', question: 'Q', default: 'fallback' });
304
+ assert.equal(res.value, 'fallback');
305
+ assert.equal(res.source, 'default');
306
+ } finally {
307
+ Object.defineProperty(process.stdin, 'isTTY', { value: origIsTTY, configurable: true });
308
+ restoreEnv(snap);
309
+ }
310
+ });
@@ -0,0 +1,135 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const {
4
+ withFileLocks,
5
+ withFileLock,
6
+ atomicWriteFileSync,
7
+ projectStateDir,
8
+ NubosPilotError,
9
+ } = require('./core.cjs');
10
+ const { parseState, serializeState } = require('./state.cjs');
11
+
12
+ const CHECKPOINT_SCHEMA_VERSION = 1;
13
+
14
+ function checkpointPath(taskId, cwd = process.cwd()) {
15
+ return path.join(projectStateDir(cwd), 'checkpoints', `${taskId}.json`);
16
+ }
17
+
18
+ function _statePath(cwd) {
19
+ return path.join(projectStateDir(cwd), 'STATE.md');
20
+ }
21
+
22
+ function _nowIso() {
23
+ return new Date().toISOString();
24
+ }
25
+
26
+ function startTask(task, cwd = process.cwd()) {
27
+ if (!task || typeof task.id !== 'string' || task.id.length === 0) {
28
+ throw new NubosPilotError(
29
+ 'checkpoint-invalid-task',
30
+ 'startTask requires a task object with non-empty .id',
31
+ { task },
32
+ );
33
+ }
34
+ const cpPath = checkpointPath(task.id, cwd);
35
+ const statePath = _statePath(cwd);
36
+ fs.mkdirSync(path.dirname(cpPath), { recursive: true });
37
+
38
+
39
+
40
+ return withFileLocks([statePath, cpPath], () => {
41
+ const cp = {
42
+ schema_version: CHECKPOINT_SCHEMA_VERSION,
43
+ task_id: task.id,
44
+ phase: task.phase == null ? null : task.phase,
45
+ plan: task.plan == null ? null : task.plan,
46
+ wave: task.wave == null ? null : task.wave,
47
+ status: 'in-progress',
48
+ started_at: _nowIso(),
49
+ last_update: _nowIso(),
50
+ files_touched: [],
51
+ resume_hint: null,
52
+ };
53
+ atomicWriteFileSync(cpPath, JSON.stringify(cp, null, 2));
54
+
55
+
56
+
57
+ const current = parseState(fs.readFileSync(statePath, 'utf-8'));
58
+ current.frontmatter.current_task = task.id;
59
+ if (task.plan != null) current.frontmatter.current_plan = task.plan;
60
+ if (task.phase != null) current.frontmatter.current_phase = task.phase;
61
+ atomicWriteFileSync(statePath, serializeState(current));
62
+ return cp;
63
+ });
64
+ }
65
+
66
+ function readCheckpoint(taskId, cwd = process.cwd()) {
67
+ const p = checkpointPath(taskId, cwd);
68
+ try {
69
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
70
+ } catch (err) {
71
+ if (err && err.code === 'ENOENT') return null;
72
+ throw err;
73
+ }
74
+ }
75
+
76
+ function writeCheckpoint(taskId, partial, cwd = process.cwd()) {
77
+ if (typeof taskId !== 'string' || taskId.length === 0) {
78
+ throw new NubosPilotError(
79
+ 'checkpoint-invalid-task-id',
80
+ 'writeCheckpoint requires a non-empty taskId',
81
+ { taskId },
82
+ );
83
+ }
84
+ const cpPath = checkpointPath(taskId, cwd);
85
+ fs.mkdirSync(path.dirname(cpPath), { recursive: true });
86
+ return withFileLock(cpPath, () => {
87
+ let existing = {};
88
+ try {
89
+ existing = JSON.parse(fs.readFileSync(cpPath, 'utf-8'));
90
+ } catch (err) {
91
+ if (!err || err.code !== 'ENOENT') throw err;
92
+ }
93
+ const merged = {
94
+ ...existing,
95
+ ...(partial || {}),
96
+ schema_version: CHECKPOINT_SCHEMA_VERSION,
97
+ last_update: _nowIso(),
98
+ };
99
+ atomicWriteFileSync(cpPath, JSON.stringify(merged, null, 2));
100
+ return merged;
101
+ });
102
+ }
103
+
104
+ function deleteCheckpoint(taskId, cwd = process.cwd()) {
105
+ const p = checkpointPath(taskId, cwd);
106
+ try {
107
+ fs.unlinkSync(p);
108
+ } catch (err) {
109
+ if (!err || err.code !== 'ENOENT') throw err;
110
+ }
111
+ }
112
+
113
+ function listCheckpoints(cwd = process.cwd()) {
114
+ const dir = path.join(projectStateDir(cwd), 'checkpoints');
115
+ try {
116
+ return fs
117
+ .readdirSync(dir)
118
+ .filter((f) => f.endsWith('.json'))
119
+ .map((f) => path.join(dir, f))
120
+ .sort();
121
+ } catch (err) {
122
+ if (err && err.code === 'ENOENT') return [];
123
+ throw err;
124
+ }
125
+ }
126
+
127
+ module.exports = {
128
+ CHECKPOINT_SCHEMA_VERSION,
129
+ checkpointPath,
130
+ startTask,
131
+ writeCheckpoint,
132
+ readCheckpoint,
133
+ deleteCheckpoint,
134
+ listCheckpoints,
135
+ };
@@ -0,0 +1,184 @@
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 checkpoint = require('./checkpoint.cjs');
8
+ const { readState } = require('./state.cjs');
9
+
10
+ const MIN_STATE = `---
11
+ schema_version: 2
12
+ milestone: m1
13
+ milestone_name: m1
14
+ current_phase: null
15
+ current_plan: null
16
+ current_task: null
17
+ last_updated: "2026-04-15T00:00:00Z"
18
+ progress:
19
+ total_phases: 0
20
+ completed_phases: 0
21
+ total_plans: 0
22
+ completed_plans: 0
23
+ percent: 0
24
+ session:
25
+ stopped_at: null
26
+ resume_file: null
27
+ last_activity: null
28
+ ---
29
+
30
+ # Project State
31
+ `;
32
+
33
+ const _sandboxes = [];
34
+
35
+ function makeSandbox() {
36
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-cp-'));
37
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
38
+ fs.writeFileSync(path.join(root, '.nubos-pilot', 'STATE.md'), MIN_STATE, 'utf-8');
39
+ _sandboxes.push(root);
40
+ return root;
41
+ }
42
+
43
+ after(() => {
44
+ while (_sandboxes.length) {
45
+ const r = _sandboxes.pop();
46
+ try { fs.rmSync(r, { recursive: true, force: true }); } catch {}
47
+ }
48
+ });
49
+
50
+ test('CP-1: exports CHECKPOINT_SCHEMA_VERSION = 1 + the documented 6 functions', () => {
51
+ assert.equal(checkpoint.CHECKPOINT_SCHEMA_VERSION, 1);
52
+ for (const fn of [
53
+ 'startTask',
54
+ 'writeCheckpoint',
55
+ 'readCheckpoint',
56
+ 'deleteCheckpoint',
57
+ 'listCheckpoints',
58
+ 'checkpointPath',
59
+ ]) {
60
+ assert.equal(typeof checkpoint[fn], 'function', `missing export: ${fn}`);
61
+ }
62
+ });
63
+
64
+ test('CP-2: checkpointPath resolves to .nubos-pilot/checkpoints/<id>.json', () => {
65
+ const root = makeSandbox();
66
+ const p = checkpoint.checkpointPath('06-01-T01', root);
67
+ assert.equal(p, path.join(root, '.nubos-pilot', 'checkpoints', '06-01-T01.json'));
68
+ });
69
+
70
+ test('CP-3: startTask writes checkpoint file with D-07 schema fields', () => {
71
+ const root = makeSandbox();
72
+ const cp = checkpoint.startTask({ id: '06-01-T01', phase: 6, plan: '06-01', wave: 1 }, root);
73
+ const onDisk = JSON.parse(fs.readFileSync(checkpoint.checkpointPath('06-01-T01', root), 'utf-8'));
74
+ assert.equal(onDisk.schema_version, 1);
75
+ assert.equal(onDisk.task_id, '06-01-T01');
76
+ assert.equal(onDisk.phase, 6);
77
+ assert.equal(onDisk.plan, '06-01');
78
+ assert.equal(onDisk.wave, 1);
79
+ assert.equal(onDisk.status, 'in-progress');
80
+ assert.match(onDisk.started_at, /^\d{4}-\d{2}-\d{2}T/);
81
+ assert.deepEqual(onDisk.files_touched, []);
82
+ assert.equal(onDisk.resume_hint, null);
83
+
84
+ assert.equal(cp.task_id, onDisk.task_id);
85
+ });
86
+
87
+ test('CP-4: startTask updates STATE.md current_task/current_plan/current_phase atomically with checkpoint (D-08)', () => {
88
+ const root = makeSandbox();
89
+ checkpoint.startTask({ id: '06-01-T02', phase: 6, plan: '06-01', wave: 1 }, root);
90
+ const state = readState(root);
91
+ assert.equal(state.frontmatter.current_task, '06-01-T02');
92
+ assert.equal(state.frontmatter.current_plan, '06-01');
93
+ assert.equal(state.frontmatter.current_phase, 6);
94
+ });
95
+
96
+ test('CP-5: startTask creates checkpoints/ directory if missing', () => {
97
+ const root = makeSandbox();
98
+
99
+ assert.equal(fs.existsSync(path.join(root, '.nubos-pilot', 'checkpoints')), false);
100
+ checkpoint.startTask({ id: '06-01-T03', phase: 6, plan: '06-01', wave: 1 }, root);
101
+ assert.equal(fs.existsSync(path.join(root, '.nubos-pilot', 'checkpoints')), true);
102
+ });
103
+
104
+ test('CP-6: readCheckpoint returns parsed JSON for an existing checkpoint', () => {
105
+ const root = makeSandbox();
106
+ checkpoint.startTask({ id: '06-01-T04', phase: 6, plan: '06-01', wave: 1 }, root);
107
+ const cp = checkpoint.readCheckpoint('06-01-T04', root);
108
+ assert.equal(cp.task_id, '06-01-T04');
109
+ assert.equal(cp.schema_version, 1);
110
+ });
111
+
112
+ test('CP-7: readCheckpoint returns null for nonexistent task (ENOENT graceful)', () => {
113
+ const root = makeSandbox();
114
+ assert.equal(checkpoint.readCheckpoint('06-01-T99', root), null);
115
+ });
116
+
117
+ test('CP-8: writeCheckpoint merges partial patch and bumps last_update', async () => {
118
+ const root = makeSandbox();
119
+ checkpoint.startTask({ id: '06-01-T05', phase: 6, plan: '06-01', wave: 1 }, root);
120
+ const before = checkpoint.readCheckpoint('06-01-T05', root);
121
+
122
+ await new Promise((r) => setTimeout(r, 5));
123
+ checkpoint.writeCheckpoint('06-01-T05', {
124
+ files_touched: ['lib/git.cjs'],
125
+ resume_hint: 'continue from line 42',
126
+ }, root);
127
+ const after = checkpoint.readCheckpoint('06-01-T05', root);
128
+ assert.deepEqual(after.files_touched, ['lib/git.cjs']);
129
+ assert.equal(after.resume_hint, 'continue from line 42');
130
+ assert.equal(after.task_id, '06-01-T05');
131
+ assert.equal(after.schema_version, 1);
132
+ assert.notEqual(after.last_update, before.last_update);
133
+ });
134
+
135
+ test('CP-9: writeCheckpoint on missing checkpoint creates a new one with schema_version=1', () => {
136
+ const root = makeSandbox();
137
+ fs.mkdirSync(path.join(root, '.nubos-pilot', 'checkpoints'), { recursive: true });
138
+ checkpoint.writeCheckpoint('06-01-T06', { task_id: '06-01-T06', status: 'in-progress' }, root);
139
+ const cp = checkpoint.readCheckpoint('06-01-T06', root);
140
+ assert.equal(cp.schema_version, 1);
141
+ assert.equal(cp.task_id, '06-01-T06');
142
+ });
143
+
144
+ test('CP-10: deleteCheckpoint removes file', () => {
145
+ const root = makeSandbox();
146
+ checkpoint.startTask({ id: '06-01-T07', phase: 6, plan: '06-01', wave: 1 }, root);
147
+ checkpoint.deleteCheckpoint('06-01-T07', root);
148
+ assert.equal(checkpoint.readCheckpoint('06-01-T07', root), null);
149
+ });
150
+
151
+ test('CP-11: deleteCheckpoint on nonexistent file is a graceful no-op (ENOENT swallowed)', () => {
152
+ const root = makeSandbox();
153
+ assert.doesNotThrow(() => checkpoint.deleteCheckpoint('06-01-T99', root));
154
+ });
155
+
156
+ test('CP-12: listCheckpoints returns sorted absolute paths; empty on missing dir', () => {
157
+ const root = makeSandbox();
158
+ assert.deepEqual(checkpoint.listCheckpoints(root), []);
159
+ checkpoint.startTask({ id: '06-01-T09', phase: 6, plan: '06-01', wave: 1 }, root);
160
+ checkpoint.startTask({ id: '06-01-T08', phase: 6, plan: '06-01', wave: 1 }, root);
161
+ const list = checkpoint.listCheckpoints(root);
162
+ assert.equal(list.length, 2);
163
+
164
+ assert.ok(list[0].endsWith('06-01-T08.json'));
165
+ assert.ok(list[1].endsWith('06-01-T09.json'));
166
+ });
167
+
168
+ test('CP-13: startTask serializes concurrent writes — final STATE matches one of the writers, no torn JSON', async () => {
169
+ const root = makeSandbox();
170
+
171
+
172
+ await Promise.all([
173
+ Promise.resolve().then(() => checkpoint.startTask({ id: '06-01-T20', phase: 6, plan: '06-01', wave: 1 }, root)),
174
+ Promise.resolve().then(() => checkpoint.startTask({ id: '06-01-T21', phase: 6, plan: '06-01', wave: 1 }, root)),
175
+ ]);
176
+ const state = readState(root);
177
+
178
+ assert.ok(['06-01-T20', '06-01-T21'].includes(state.frontmatter.current_task));
179
+
180
+ const cp20 = checkpoint.readCheckpoint('06-01-T20', root);
181
+ const cp21 = checkpoint.readCheckpoint('06-01-T21', root);
182
+ assert.equal(cp20.task_id, '06-01-T20');
183
+ assert.equal(cp21.task_id, '06-01-T21');
184
+ });