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,284 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const YAML = require('yaml');
4
+
5
+ const {
6
+ NubosPilotError,
7
+ atomicWriteFileSync,
8
+ } = require('../../lib/core.cjs');
9
+ const { addMilestone, addPhase } = require('../../lib/roadmap.cjs');
10
+ const { createPhaseDir, phaseSlug } = require('../../lib/phase.cjs');
11
+ const { writeState } = require('../../lib/state.cjs');
12
+
13
+ const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates');
14
+ const PLACEHOLDER_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
15
+
16
+ function _render(raw, vars, templateName) {
17
+ return raw.replace(PLACEHOLDER_RE, (_match, key) => {
18
+ if (!(key in vars)) {
19
+ throw new NubosPilotError(
20
+ 'template-unresolved-var',
21
+ `Undefined placeholder {{${key}}} in template "${templateName}"`,
22
+ { template: templateName, variable: key, available: Object.keys(vars) },
23
+ );
24
+ }
25
+ return String(vars[key]);
26
+ });
27
+ }
28
+
29
+ function _loadTemplate(name) {
30
+ return fs.readFileSync(path.join(TEMPLATES_DIR, name + '.md'), 'utf-8');
31
+ }
32
+
33
+ function _slugify(raw) {
34
+ return String(raw || '')
35
+ .toLowerCase()
36
+ .replace(/[^a-z0-9]+/g, '-')
37
+ .replace(/^-+|-+$/g, '');
38
+ }
39
+
40
+ function _todayIso() {
41
+ return new Date().toISOString().slice(0, 10);
42
+ }
43
+
44
+ function _interviewPayload() {
45
+ return {
46
+ mode: 'interview',
47
+ questions: [
48
+ { key: 'project_name', type: 'input',
49
+ question: 'Project name?' },
50
+ { key: 'core_value', type: 'input',
51
+ question: 'Core value — one sentence that must stay true if everything else fails?' },
52
+ { key: 'primary_constraints', type: 'input',
53
+ question: 'Primary constraints (comma-separated, e.g. "Node 22; markdown-first")?' },
54
+ { key: 'first_milestone_name', type: 'input',
55
+ question: 'First milestone name (e.g. "v1.0")?' },
56
+ { key: 'first_phase_name', type: 'input',
57
+ question: 'First phase name (will be slugified for the directory)?' },
58
+ ],
59
+ };
60
+ }
61
+
62
+ function _validateAnswers(a) {
63
+ for (const key of [
64
+ 'project_name',
65
+ 'core_value',
66
+ 'primary_constraints',
67
+ 'first_milestone_name',
68
+ 'first_phase_name',
69
+ ]) {
70
+ if (typeof a[key] !== 'string' || a[key].trim() === '') {
71
+ throw new NubosPilotError(
72
+ 'answers-missing-field',
73
+ 'answers JSON missing field: ' + key,
74
+ { field: key },
75
+ );
76
+ }
77
+ }
78
+ }
79
+
80
+ function _emit(stdout, payload) {
81
+ stdout.write(JSON.stringify(payload, null, 2));
82
+ }
83
+
84
+ function _apply(answersPath, cwd, stdout) {
85
+ let raw;
86
+ try {
87
+ raw = fs.readFileSync(answersPath, 'utf-8');
88
+ } catch (err) {
89
+ throw new NubosPilotError(
90
+ 'answers-not-readable',
91
+ 'answers file not readable: ' + answersPath,
92
+ { path: answersPath, cause: err && err.code },
93
+ );
94
+ }
95
+ let answers;
96
+ try {
97
+ answers = JSON.parse(raw);
98
+ } catch (err) {
99
+ throw new NubosPilotError(
100
+ 'answers-parse-error',
101
+ 'answers file is not valid JSON',
102
+ { path: answersPath, cause: err && err.message },
103
+ );
104
+ }
105
+ _validateAnswers(answers);
106
+
107
+ const root = path.resolve(cwd);
108
+ const stateDir = path.join(root, '.nubos-pilot');
109
+ const projectMd = path.join(stateDir, 'PROJECT.md');
110
+
111
+ if (fs.existsSync(projectMd)) {
112
+ throw new NubosPilotError(
113
+ 'project-already-initialized',
114
+ 'PROJECT.md already exists — refusing to overwrite',
115
+ { path: projectMd },
116
+ );
117
+ }
118
+
119
+ const milestoneId = _slugify(answers.first_milestone_name);
120
+ if (milestoneId === '') {
121
+ throw new NubosPilotError(
122
+ 'invalid-slug',
123
+ 'first_milestone_name slugifies to empty string',
124
+ { value: answers.first_milestone_name, field: 'first_milestone_name' },
125
+ );
126
+ }
127
+ const phaseSlugValue = phaseSlug(answers.first_phase_name);
128
+ if (phaseSlugValue === '') {
129
+ throw new NubosPilotError(
130
+ 'invalid-slug',
131
+ 'first_phase_name slugifies to empty string',
132
+ { value: answers.first_phase_name, field: 'first_phase_name' },
133
+ );
134
+ }
135
+
136
+ const createdDate = _todayIso();
137
+
138
+
139
+
140
+
141
+
142
+ fs.mkdirSync(stateDir, { recursive: true });
143
+ const roadmapYamlPath = path.join(stateDir, 'roadmap.yaml');
144
+ const emptyRoadmap = { schema_version: 1, milestones: [] };
145
+ atomicWriteFileSync(roadmapYamlPath, YAML.stringify(emptyRoadmap, { indent: 2 }));
146
+
147
+
148
+
149
+ const projectVars = {
150
+ project_name: answers.project_name,
151
+ core_value: answers.core_value,
152
+ primary_constraints: answers.primary_constraints,
153
+ first_milestone_name: answers.first_milestone_name,
154
+ first_phase_name: answers.first_phase_name,
155
+ created_date: createdDate,
156
+ };
157
+ atomicWriteFileSync(projectMd, _render(_loadTemplate('PROJECT'), projectVars, 'PROJECT'));
158
+
159
+
160
+
161
+ const reqVars = {
162
+ project_name: answers.project_name,
163
+ core_value: answers.core_value,
164
+ first_milestone_name: answers.first_milestone_name,
165
+ created_date: createdDate,
166
+ };
167
+ atomicWriteFileSync(
168
+ path.join(stateDir, 'REQUIREMENTS.md'),
169
+ _render(_loadTemplate('REQUIREMENTS'), reqVars, 'REQUIREMENTS'),
170
+ );
171
+
172
+
173
+
174
+
175
+ addMilestone({ id: milestoneId, name: answers.first_milestone_name, phases: [] }, root);
176
+ const phaseResult = addPhase(
177
+ milestoneId,
178
+ {
179
+ slug: phaseSlugValue,
180
+ name: answers.first_phase_name,
181
+ goal: '',
182
+ depends_on: [],
183
+ requirements: [],
184
+ success_criteria: [],
185
+ status: 'pending',
186
+ plans: [],
187
+ },
188
+ root,
189
+ );
190
+
191
+
192
+
193
+ const phaseDir = createPhaseDir(phaseResult.number, phaseSlugValue, root);
194
+ const ctxVars = {
195
+ phase_number: String(phaseResult.number),
196
+ phase_name: answers.first_phase_name,
197
+ phase_padded: String(phaseResult.number).padStart(2, '0'),
198
+ phase_slug: phaseSlugValue,
199
+ created_date: createdDate,
200
+ domain_text: '<!-- TBD: phase boundary -->',
201
+ decisions_text: '<!-- TBD: decisions -->',
202
+ canonical_refs_text: '<!-- TBD: canonical references -->',
203
+ code_context_text: '<!-- TBD: existing code insights -->',
204
+ specifics_text: '<!-- TBD: specific ideas / references -->',
205
+ deferred_text: '<!-- TBD: deferred ideas -->',
206
+ };
207
+ const contextMdPath = path.join(
208
+ phaseDir,
209
+ String(phaseResult.number).padStart(2, '0') + '-CONTEXT.md',
210
+ );
211
+ atomicWriteFileSync(contextMdPath, _render(_loadTemplate('CONTEXT'), ctxVars, 'CONTEXT'));
212
+
213
+
214
+
215
+ writeState(
216
+ {
217
+ frontmatter: {
218
+ schema_version: 2,
219
+ milestone: milestoneId,
220
+ milestone_name: answers.first_milestone_name,
221
+ current_phase: Number(phaseResult.number),
222
+ current_plan: null,
223
+ current_task: null,
224
+ last_updated: new Date().toISOString(),
225
+ progress: {
226
+ total_phases: 1,
227
+ completed_phases: 0,
228
+ total_plans: 0,
229
+ completed_plans: 0,
230
+ percent: 0,
231
+ },
232
+ session: {
233
+ stopped_at: null,
234
+ resume_file: null,
235
+ last_activity: createdDate + ' -- np:new-project scaffold',
236
+ },
237
+ },
238
+ body: '\n# Project State\n\nInitialized by np:new-project.\n',
239
+ },
240
+ root,
241
+ );
242
+
243
+
244
+
245
+ _emit(stdout, {
246
+ mode: 'apply',
247
+ milestoneId,
248
+ firstPhaseNumber: phaseResult.number,
249
+ firstPhaseSlug: phaseSlugValue,
250
+ created: [
251
+ '.nubos-pilot/PROJECT.md',
252
+ '.nubos-pilot/REQUIREMENTS.md',
253
+ '.nubos-pilot/roadmap.yaml',
254
+ '.nubos-pilot/ROADMAP.md',
255
+ '.nubos-pilot/STATE.md',
256
+ path.relative(root, contextMdPath),
257
+ ],
258
+ });
259
+ }
260
+
261
+ function run(args, ctx) {
262
+ const context = ctx || {};
263
+ const cwd = context.cwd || process.cwd();
264
+ const stdout = context.stdout || process.stdout;
265
+ const argv = args || [];
266
+
267
+ const applyIdx = argv.indexOf('--apply');
268
+ if (applyIdx >= 0) {
269
+ const answersPath = argv[applyIdx + 1];
270
+ if (!answersPath) {
271
+ throw new NubosPilotError(
272
+ 'missing-apply-path',
273
+ '--apply requires a path to the answers JSON file',
274
+ { args: argv.slice() },
275
+ );
276
+ }
277
+ _apply(answersPath, cwd, stdout);
278
+ return;
279
+ }
280
+
281
+ _emit(stdout, _interviewPayload());
282
+ }
283
+
284
+ module.exports = { run, _interviewPayload, _slugify };
@@ -0,0 +1,165 @@
1
+ const { test, afterEach } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const subcmd = require('./new-project.cjs');
8
+
9
+ const _sandboxes = [];
10
+
11
+ function makeEmptySandbox() {
12
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-newproj-'));
13
+ _sandboxes.push(root);
14
+ return root;
15
+ }
16
+
17
+ afterEach(() => {
18
+ while (_sandboxes.length) {
19
+ const p = _sandboxes.pop();
20
+ try { fs.rmSync(p, { recursive: true, force: true }); } catch { }
21
+ }
22
+ });
23
+
24
+ function _captureStdout() {
25
+ let buf = '';
26
+ const stub = { write: (s) => { buf += s; return true; } };
27
+ return { stub, get: () => buf };
28
+ }
29
+
30
+ function _baseAnswers() {
31
+ return {
32
+ project_name: 'Demo Project',
33
+ core_value: 'Ship demos fast.',
34
+ primary_constraints: 'Node 22; markdown-first',
35
+ first_milestone_name: 'v1.0',
36
+ first_phase_name: 'Foundation Phase',
37
+ };
38
+ }
39
+
40
+ function _writeAnswers(root, answers) {
41
+ const p = path.join(root, 'answers.json');
42
+ fs.writeFileSync(p, JSON.stringify(answers), 'utf-8');
43
+ return p;
44
+ }
45
+
46
+ test('NP-1: run([]) emits interview JSON with all 5 questions', () => {
47
+ const sandbox = makeEmptySandbox();
48
+ const cap = _captureStdout();
49
+ subcmd.run([], { cwd: sandbox, stdout: cap.stub });
50
+ const payload = JSON.parse(cap.get().trim());
51
+ assert.equal(payload.mode, 'interview');
52
+ assert.ok(Array.isArray(payload.questions));
53
+ const keys = payload.questions.map((q) => q.key);
54
+ for (const expected of [
55
+ 'project_name',
56
+ 'core_value',
57
+ 'primary_constraints',
58
+ 'first_milestone_name',
59
+ 'first_phase_name',
60
+ ]) {
61
+ assert.ok(keys.includes(expected), 'interview missing ' + expected);
62
+ }
63
+ for (const q of payload.questions) {
64
+ assert.ok(typeof q.question === 'string' && q.question.length > 0);
65
+ assert.ok(typeof q.type === 'string');
66
+ }
67
+ });
68
+
69
+ test('NP-2: --apply creates all 5 files + scaffolds first phase dir', () => {
70
+ const sandbox = makeEmptySandbox();
71
+ const answersPath = _writeAnswers(sandbox, _baseAnswers());
72
+ const cap = _captureStdout();
73
+ subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: cap.stub });
74
+
75
+ assert.ok(fs.existsSync(path.join(sandbox, '.nubos-pilot', 'PROJECT.md')), 'PROJECT.md missing');
76
+ assert.ok(fs.existsSync(path.join(sandbox, '.nubos-pilot', 'REQUIREMENTS.md')), 'REQUIREMENTS.md missing');
77
+ assert.ok(fs.existsSync(path.join(sandbox, '.nubos-pilot', 'roadmap.yaml')), 'roadmap.yaml missing');
78
+ assert.ok(fs.existsSync(path.join(sandbox, '.nubos-pilot', 'ROADMAP.md')), 'ROADMAP.md missing');
79
+ assert.ok(fs.existsSync(path.join(sandbox, '.nubos-pilot', 'STATE.md')), 'STATE.md missing');
80
+
81
+ const phasesRoot = path.join(sandbox, '.nubos-pilot', 'phases');
82
+ const entries = fs.readdirSync(phasesRoot);
83
+ const phaseDir = entries.find((e) => e.startsWith('01-'));
84
+ assert.ok(phaseDir, 'phases/01-<slug>/ not scaffolded');
85
+ assert.ok(
86
+ fs.existsSync(path.join(phasesRoot, phaseDir, '01-CONTEXT.md')),
87
+ '01-CONTEXT.md placeholder missing',
88
+ );
89
+ });
90
+
91
+ test('NP-3: PROJECT.md is rendered with user-supplied values', () => {
92
+ const sandbox = makeEmptySandbox();
93
+ const answersPath = _writeAnswers(sandbox, _baseAnswers());
94
+ const cap = _captureStdout();
95
+ subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: cap.stub });
96
+ const raw = fs.readFileSync(path.join(sandbox, '.nubos-pilot', 'PROJECT.md'), 'utf-8');
97
+ assert.match(raw, /Demo Project/);
98
+ assert.match(raw, /Ship demos fast\./);
99
+ assert.match(raw, /Node 22/);
100
+
101
+ assert.doesNotMatch(raw, /\{\{[a-z_]+\}\}/);
102
+ });
103
+
104
+ test('NP-4: second invocation throws project-already-initialized', () => {
105
+ const sandbox = makeEmptySandbox();
106
+ const answersPath = _writeAnswers(sandbox, _baseAnswers());
107
+ subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub });
108
+
109
+ const diffAnswers = Object.assign({}, _baseAnswers(), { project_name: 'Other' });
110
+ const diffPath = _writeAnswers(sandbox, diffAnswers);
111
+ assert.throws(
112
+ () => subcmd.run(['--apply', diffPath], { cwd: sandbox, stdout: _captureStdout().stub }),
113
+ (err) => err.name === 'NubosPilotError' && err.code === 'project-already-initialized',
114
+ );
115
+ });
116
+
117
+ test('NP-5: shell-metachar project_name is stored literally, no files outside .nubos-pilot', () => {
118
+ const sandbox = makeEmptySandbox();
119
+ const evil = Object.assign({}, _baseAnswers(), {
120
+ project_name: '; rm -rf /tmp/definitely-not-there ; echo PWND',
121
+ });
122
+ const answersPath = _writeAnswers(sandbox, evil);
123
+ subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub });
124
+ const raw = fs.readFileSync(path.join(sandbox, '.nubos-pilot', 'PROJECT.md'), 'utf-8');
125
+ assert.match(raw, /rm -rf/);
126
+
127
+ const entries = fs.readdirSync(sandbox).sort();
128
+ assert.deepEqual(entries, ['.nubos-pilot', 'answers.json']);
129
+ });
130
+
131
+ test('NP-6: slugifies first_phase_name; strips non [a-z0-9-]', () => {
132
+ const sandbox = makeEmptySandbox();
133
+ const answers = Object.assign({}, _baseAnswers(), {
134
+ first_phase_name: 'Foo Bar! 2',
135
+ });
136
+ const answersPath = _writeAnswers(sandbox, answers);
137
+ subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub });
138
+ const phasesRoot = path.join(sandbox, '.nubos-pilot', 'phases');
139
+ const entries = fs.readdirSync(phasesRoot);
140
+ const phaseDir = entries.find((e) => e.startsWith('01-'));
141
+ assert.equal(phaseDir, '01-foo-bar-2');
142
+ });
143
+
144
+ test('NP-7: empty-after-slugify first_phase_name throws invalid-slug', () => {
145
+ const sandbox = makeEmptySandbox();
146
+ const answers = Object.assign({}, _baseAnswers(), {
147
+ first_phase_name: '!!!@@@###',
148
+ });
149
+ const answersPath = _writeAnswers(sandbox, answers);
150
+ assert.throws(
151
+ () => subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub }),
152
+ (err) => err.name === 'NubosPilotError' && err.code === 'invalid-slug',
153
+ );
154
+ });
155
+
156
+ test('NP-8: STATE.md seeded with milestone + current_phase:1 + status', () => {
157
+ const sandbox = makeEmptySandbox();
158
+ const answersPath = _writeAnswers(sandbox, _baseAnswers());
159
+ subcmd.run(['--apply', answersPath], { cwd: sandbox, stdout: _captureStdout().stub });
160
+ const { readState } = require('../../lib/state.cjs');
161
+ const st = readState(sandbox);
162
+ assert.ok(st.frontmatter.milestone, 'STATE.md missing milestone');
163
+ assert.equal(st.frontmatter.current_phase, 1);
164
+ assert.equal(st.frontmatter.current_plan, null);
165
+ });
@@ -0,0 +1,7 @@
1
+ const { computeNextStep } = require('../../lib/next.cjs');
2
+
3
+ function run(_args, cwd) {
4
+ return computeNextStep(cwd || process.cwd());
5
+ }
6
+
7
+ module.exports = { run };
@@ -0,0 +1,30 @@
1
+ const { test, afterEach } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const nextCmd = require('./next.cjs');
8
+
9
+ const sandboxes = [];
10
+ function mkTmp() {
11
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-next-cmd-'));
12
+ fs.mkdirSync(path.join(root, '.nubos-pilot'));
13
+ sandboxes.push(root);
14
+ return root;
15
+ }
16
+ afterEach(() => {
17
+ while (sandboxes.length) {
18
+ const p = sandboxes.pop();
19
+ try { fs.rmSync(p, { recursive: true, force: true }); } catch {}
20
+ }
21
+ });
22
+
23
+ test('NEXT-CMD-1: run on fresh sandbox returns rule-1 JSON payload', () => {
24
+ const root = mkTmp();
25
+ const payload = nextCmd.run([], root);
26
+ assert.ok(payload.next_step);
27
+ assert.equal(payload.next_step.command, '/np:discuss-phase 1');
28
+ assert.equal(payload.task, null);
29
+ assert.equal(payload.phase, 1);
30
+ });
@@ -0,0 +1,48 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const { NubosPilotError, projectStateDir } = require('../../lib/core.cjs');
5
+ const { TASK_ID_RE, setTaskStatus } = require('../../lib/tasks.cjs');
6
+
7
+ function _findPlanDirForTask(taskId, cwd) {
8
+ const phasesRoot = path.join(projectStateDir(cwd), 'phases');
9
+ let entries;
10
+ try {
11
+ entries = fs.readdirSync(phasesRoot, { withFileTypes: true });
12
+ } catch (err) {
13
+ if (err && err.code === 'ENOENT') return null;
14
+ throw err;
15
+ }
16
+ const padded = taskId.slice(0, 2);
17
+ for (const e of entries) {
18
+ if (!e.isDirectory()) continue;
19
+ if (!(e.name === padded || e.name.startsWith(padded + '-'))) continue;
20
+ const candidate = path.join(phasesRoot, e.name, 'tasks', taskId + '.md');
21
+ if (fs.existsSync(candidate)) return path.join(phasesRoot, e.name);
22
+ }
23
+ return null;
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
+ const taskId = list[0];
32
+ if (!taskId) {
33
+ throw new NubosPilotError('park-missing-task-id', 'park requires a task id', {});
34
+ }
35
+ if (!TASK_ID_RE.test(taskId)) {
36
+ throw new NubosPilotError('park-invalid-task-id', 'Invalid task id: ' + taskId, { taskId });
37
+ }
38
+ const planDir = _findPlanDirForTask(taskId, cwd);
39
+ if (!planDir) {
40
+ throw new NubosPilotError('task-not-found', 'No task file found for id ' + taskId, { taskId });
41
+ }
42
+ setTaskStatus(taskId, 'parked', planDir);
43
+ const payload = { ok: true, task_id: taskId, status: 'parked' };
44
+ stdout.write(JSON.stringify(payload));
45
+ return payload;
46
+ }
47
+
48
+ module.exports = { run };
@@ -0,0 +1,50 @@
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('./park.cjs');
8
+
9
+ const _roots = [];
10
+
11
+ function makeRoot(taskId) {
12
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-park-'));
13
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
14
+ const tasksDir = path.join(root, '.nubos-pilot', 'phases', '06-demo', 'tasks');
15
+ fs.mkdirSync(tasksDir, { recursive: true });
16
+ fs.writeFileSync(path.join(tasksDir, taskId + '.md'), [
17
+ '---', `id: ${taskId}`, 'phase: 6', 'plan: "06-01"', 'type: auto',
18
+ 'status: pending', 'tier: sonnet', 'owner: np-executor', 'wave: 1',
19
+ 'depends_on: []', 'files_modified: []', 'autonomous: true',
20
+ 'must_haves:', ' truths: []', '---', '', '# T',
21
+ ].join('\n'), 'utf-8');
22
+ _roots.push(root);
23
+ return root;
24
+ }
25
+
26
+ function _capture() { let b = ''; return { stub: { write: (s) => { b += s; } }, get: () => b }; }
27
+
28
+ after(() => {
29
+ while (_roots.length) {
30
+ const r = _roots.pop();
31
+ try { fs.rmSync(r, { recursive: true, force: true }); } catch {}
32
+ }
33
+ });
34
+
35
+ test('PK-1: park missing id', () => {
36
+ assert.throws(
37
+ () => subcmd.run([], { cwd: process.cwd(), stdout: _capture().stub }),
38
+ (err) => err && err.code === 'park-missing-task-id',
39
+ );
40
+ });
41
+
42
+ test('PK-2: park flips status to parked', () => {
43
+ const root = makeRoot('06-01-T02');
44
+ const cap = _capture();
45
+ subcmd.run(['06-01-T02'], { cwd: root, stdout: cap.stub });
46
+ const payload = JSON.parse(cap.get());
47
+ assert.equal(payload.status, 'parked');
48
+ const tf = path.join(root, '.nubos-pilot', 'phases', '06-demo', 'tasks', '06-01-T02.md');
49
+ assert.match(fs.readFileSync(tf, 'utf-8'), /^status: parked$/m);
50
+ });
@@ -0,0 +1,24 @@
1
+ const { mutateState } = require('../../lib/state.cjs');
2
+
3
+ function run(_args, ctx) {
4
+ const context = ctx || {};
5
+ const cwd = context.cwd || process.cwd();
6
+ const stdout = context.stdout || process.stdout;
7
+ const next = mutateState((s) => {
8
+ s.frontmatter.session = s.frontmatter.session || {};
9
+ s.frontmatter.session.stopped_at = new Date().toISOString();
10
+ s.frontmatter.session.resume_file = s.frontmatter.current_task
11
+ ? '.nubos-pilot/checkpoints/' + s.frontmatter.current_task + '.json'
12
+ : null;
13
+ return s;
14
+ }, cwd);
15
+ const payload = {
16
+ ok: true,
17
+ stopped_at: next.frontmatter.session.stopped_at,
18
+ resume_file: next.frontmatter.session.resume_file,
19
+ };
20
+ stdout.write(JSON.stringify(payload));
21
+ return payload;
22
+ }
23
+
24
+ module.exports = { run };
@@ -0,0 +1,74 @@
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('./pause-work.cjs');
8
+
9
+ const _roots = [];
10
+
11
+ function makeRoot(currentTask) {
12
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-pw-'));
13
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
14
+ const ct = currentTask == null ? 'null' : currentTask;
15
+ fs.writeFileSync(path.join(root, '.nubos-pilot', 'STATE.md'), `---
16
+ schema_version: 2
17
+ milestone: m1
18
+ milestone_name: m1
19
+ current_phase: 6
20
+ current_plan: "06-01"
21
+ current_task: ${ct}
22
+ last_updated: "2026-04-15T00:00:00Z"
23
+ progress:
24
+ total_phases: 0
25
+ completed_phases: 0
26
+ total_plans: 0
27
+ completed_plans: 0
28
+ percent: 0
29
+ session:
30
+ stopped_at: null
31
+ resume_file: null
32
+ last_activity: null
33
+ ---
34
+
35
+ # State
36
+ `, 'utf-8');
37
+ _roots.push(root);
38
+ return root;
39
+ }
40
+
41
+ function _capture() { let b = ''; return { stub: { write: (s) => { b += s; return true; } }, get: () => b }; }
42
+
43
+ after(() => {
44
+ while (_roots.length) {
45
+ const r = _roots.pop();
46
+ try { fs.rmSync(r, { recursive: true, force: true }); } catch {}
47
+ }
48
+ });
49
+
50
+ test('PW-1: pause-work sets stopped_at and resume_file when current_task set', () => {
51
+ const root = makeRoot('06-01-T01');
52
+ const cap = _capture();
53
+ const p = subcmd.run([], { cwd: root, stdout: cap.stub });
54
+ assert.equal(p.ok, true);
55
+ assert.ok(/^\d{4}-\d{2}-\d{2}T/.test(p.stopped_at));
56
+ assert.equal(p.resume_file, '.nubos-pilot/checkpoints/06-01-T01.json');
57
+ });
58
+
59
+ test('PW-2: pause-work resume_file=null when no current_task', () => {
60
+ const root = makeRoot(null);
61
+ const cap = _capture();
62
+ const p = subcmd.run([], { cwd: root, stdout: cap.stub });
63
+ assert.equal(p.resume_file, null);
64
+ assert.ok(p.stopped_at);
65
+ });
66
+
67
+ test('PW-3: STATE.md on disk reflects the mutation', () => {
68
+ const root = makeRoot('06-01-T02');
69
+ const cap = _capture();
70
+ subcmd.run([], { cwd: root, stdout: cap.stub });
71
+ const body = fs.readFileSync(path.join(root, '.nubos-pilot', 'STATE.md'), 'utf-8');
72
+ assert.ok(/stopped_at: "?\d{4}-\d{2}-\d{2}T/.test(body), body);
73
+ assert.ok(body.includes('resume_file: .nubos-pilot/checkpoints/06-01-T02.json'), body);
74
+ });