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,350 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const os = require('node:os');
4
+ const crypto = require('node:crypto');
5
+
6
+ const {
7
+ NubosPilotError,
8
+ projectStateDir,
9
+ findProjectRoot,
10
+ atomicWriteFileSync,
11
+ withFileLock,
12
+ } = require('../../lib/core.cjs');
13
+ const { getPhase } = require('../../lib/roadmap.cjs');
14
+ const { paddedPhase, phaseSlug, findPhaseDir } = require('../../lib/phase.cjs');
15
+ const { listPlans, parsePlan, shouldPromoteToTasks } = require('../../lib/plan.cjs');
16
+ const { getAgentSkills } = require('../../lib/agents.cjs');
17
+ const { gitShowSafe } = require('../../lib/git.cjs');
18
+
19
+ const INLINE_THRESHOLD_BYTES = 16 * 1024;
20
+
21
+ function _validatePhaseArg(raw) {
22
+ if (raw == null || raw === '') {
23
+ throw new NubosPilotError(
24
+ 'plan-phase-invalid-phase-arg',
25
+ 'plan-phase requires a phase number (integer or decimal)',
26
+ { value: raw == null ? '' : String(raw) },
27
+ );
28
+ }
29
+ const s = String(raw);
30
+ if (!/^\d+(\.\d+)?$/.test(s)) {
31
+ throw new NubosPilotError(
32
+ 'plan-phase-invalid-phase-arg',
33
+ 'Invalid phase number: ' + s,
34
+ { value: s },
35
+ );
36
+ }
37
+ return s;
38
+ }
39
+
40
+ function _resolvePhaseDir(n, cwd, slug) {
41
+ const hit = findPhaseDir(n, cwd);
42
+ if (hit) return hit;
43
+ const padded = paddedPhase(n);
44
+ let stateDir;
45
+ try { stateDir = projectStateDir(cwd); } catch { stateDir = path.join(path.resolve(cwd), '.nubos-pilot'); }
46
+ return path.join(stateDir, 'phases', padded + '-' + slug);
47
+ }
48
+
49
+ function _safeSkills(name, cwd) {
50
+ try { return getAgentSkills(name, cwd); } catch { return []; }
51
+ }
52
+
53
+ function _emit(payload, stdout, cwd) {
54
+ const json = JSON.stringify(payload, null, 2);
55
+ if (Buffer.byteLength(json, 'utf-8') <= INLINE_THRESHOLD_BYTES) {
56
+ stdout.write(json);
57
+ return;
58
+ }
59
+ let tmpDir;
60
+ try {
61
+ tmpDir = path.join(projectStateDir(cwd), '.tmp');
62
+ fs.mkdirSync(tmpDir, { recursive: true });
63
+ } catch { tmpDir = os.tmpdir(); }
64
+ const suffix = process.pid + '-' + crypto.randomBytes(4).toString('hex');
65
+ const tmpPath = path.join(tmpDir, 'init-plan-phase-' + suffix + '.json');
66
+ fs.writeFileSync(tmpPath, json, 'utf-8');
67
+ stdout.write('@file:' + tmpPath);
68
+ }
69
+
70
+ function _initPayload(phaseArg, cwd) {
71
+ let phase;
72
+ try {
73
+ phase = getPhase(phaseArg, cwd);
74
+ } catch (err) {
75
+ if (err && err.code === 'phase-not-found') {
76
+ throw new NubosPilotError(
77
+ 'plan-phase-not-found',
78
+ 'Phase ' + phaseArg + ' not found in roadmap.yaml',
79
+ { number: phaseArg },
80
+ );
81
+ }
82
+ throw err;
83
+ }
84
+ const padded = paddedPhase(phaseArg);
85
+ const slug = phase.slug || phaseSlug(phase.name);
86
+ const phase_dir = _resolvePhaseDir(phaseArg, cwd, slug);
87
+ const contextPath = path.join(phase_dir, padded + '-CONTEXT.md');
88
+ const researchPath = path.join(phase_dir, padded + '-RESEARCH.md');
89
+ const plan_review_path = path.join(phase_dir, padded + '-PLAN-REVIEW.md');
90
+
91
+ let has_plan = false;
92
+ try { has_plan = listPlans(phase_dir).length > 0; } catch { has_plan = false; }
93
+
94
+ const { plan_diff_required, plan_diff_plan_path } = _probePlanDiff(phase_dir, padded, cwd);
95
+
96
+ return {
97
+ _workflow: 'plan-phase',
98
+ phase: phaseArg,
99
+ padded,
100
+ phase_dir,
101
+ phase_slug: slug,
102
+ phase_name: phase.name,
103
+ goal: phase.goal || '',
104
+ requirements: Array.isArray(phase.requirements) ? phase.requirements : [],
105
+ success_criteria: Array.isArray(phase.success_criteria) ? phase.success_criteria : [],
106
+ has_context: fs.existsSync(contextPath),
107
+ has_research: fs.existsSync(researchPath),
108
+ has_plan,
109
+ context_path: fs.existsSync(contextPath) ? contextPath : null,
110
+ research_path: fs.existsSync(researchPath) ? researchPath : null,
111
+ plan_review_path,
112
+ planner_tier: 'opus',
113
+ checker_tier: 'opus',
114
+ plan_diff_required,
115
+ plan_diff_plan_path,
116
+ agent_skills: {
117
+ 'np-planner': _safeSkills('np-planner', cwd),
118
+ 'np-plan-checker': _safeSkills('np-plan-checker', cwd),
119
+ },
120
+ };
121
+ }
122
+
123
+ function _probePlanDiff(phaseDir, padded, cwd) {
124
+ const firstPlanAbs = path.join(phaseDir, padded + '-01-PLAN.md');
125
+ let root;
126
+ try { root = findProjectRoot(cwd); } catch { root = path.resolve(cwd); }
127
+ const rel = path.relative(root, firstPlanAbs);
128
+ const prev = process.cwd();
129
+ process.chdir(root);
130
+ let prior = null;
131
+ try {
132
+ prior = gitShowSafe('HEAD', rel);
133
+ } catch {
134
+ prior = null;
135
+ } finally {
136
+ process.chdir(prev);
137
+ }
138
+ return {
139
+ plan_diff_required: prior !== null,
140
+ plan_diff_plan_path: rel,
141
+ };
142
+ }
143
+
144
+ function _readVerdict(verdictPath) {
145
+ let raw;
146
+ try {
147
+ raw = fs.readFileSync(verdictPath, 'utf-8');
148
+ } catch (err) {
149
+ throw new NubosPilotError(
150
+ 'plan-phase-verdict-unreadable',
151
+ 'Verdict file not readable: ' + verdictPath,
152
+ { path: verdictPath, cause: err && err.code },
153
+ );
154
+ }
155
+ try { return JSON.parse(raw); } catch (err) {
156
+ throw new NubosPilotError(
157
+ 'plan-phase-verdict-invalid',
158
+ 'Verdict file is not valid JSON',
159
+ { path: verdictPath, cause: err && err.message },
160
+ );
161
+ }
162
+ }
163
+
164
+ function _renderVerdictYaml(verdict) {
165
+ const status = verdict.status || 'unknown';
166
+ const findings = Array.isArray(verdict.findings) ? verdict.findings : [];
167
+ const lines = ['status: ' + status, 'findings:'];
168
+ if (findings.length === 0) {
169
+ lines[1] = 'findings: []';
170
+ } else {
171
+ for (const f of findings) {
172
+ lines.push(' - category: ' + (f.category || 'unknown'));
173
+ lines.push(' severity: ' + (f.severity || 'minor'));
174
+ if (f.target) lines.push(' target: ' + JSON.stringify(String(f.target)));
175
+ if (f.message) lines.push(' message: ' + JSON.stringify(String(f.message)));
176
+ }
177
+ }
178
+ return lines.join('\n');
179
+ }
180
+
181
+ function _renderIterationSection(iter, verdict) {
182
+ const ts = new Date().toISOString();
183
+ const status = verdict.status || 'unknown';
184
+ const parts = [
185
+ '',
186
+ '## Iteration ' + iter + ' - ' + ts,
187
+ '',
188
+ '**Planner output:** PLAN.md committed at pending',
189
+ '**Checker verdict:** ' + status,
190
+ '**Findings:**',
191
+ '',
192
+ '```yaml',
193
+ _renderVerdictYaml(verdict),
194
+ '```',
195
+ '',
196
+ '**Planner response:** ' + (status === 'passed' ? 'done' : 'revision'),
197
+ '',
198
+ ];
199
+ return parts.join('\n');
200
+ }
201
+
202
+ function _planReviewAppend(phaseArg, iter, verdictPath, cwd) {
203
+ const padded = paddedPhase(phaseArg);
204
+ const phase = getPhase(phaseArg, cwd);
205
+ const slug = phase.slug || phaseSlug(phase.name);
206
+ const phase_dir = _resolvePhaseDir(phaseArg, cwd, slug);
207
+ fs.mkdirSync(phase_dir, { recursive: true });
208
+ const reviewPath = path.join(phase_dir, padded + '-PLAN-REVIEW.md');
209
+ const verdict = _readVerdict(verdictPath);
210
+
211
+ return withFileLock(reviewPath, () => {
212
+ let existing = '';
213
+ try { existing = fs.readFileSync(reviewPath, 'utf-8'); } catch { existing = ''; }
214
+ if (existing === '') {
215
+ existing = '# PLAN-REVIEW.md — Phase ' + phaseArg + ' (' + phase.name + ')\n'
216
+ + '\nAppend-only audit trail of plan-checker iterations. Never truncate.\n';
217
+ }
218
+ const section = _renderIterationSection(iter, verdict);
219
+ const next = existing + section;
220
+ atomicWriteFileSync(reviewPath, next);
221
+ return { appended: true, path: reviewPath, iteration: Number(iter) };
222
+ });
223
+ }
224
+
225
+ function _rmRecursive(target) {
226
+ try { fs.rmSync(target, { recursive: true, force: true }); } catch { }
227
+ }
228
+
229
+ function _planPhaseAbort(phaseArg, cwd) {
230
+ const padded = paddedPhase(phaseArg);
231
+ const phase = getPhase(phaseArg, cwd);
232
+ const slug = phase.slug || phaseSlug(phase.name);
233
+ const phase_dir = _resolvePhaseDir(phaseArg, cwd, slug);
234
+ const removed = [];
235
+
236
+ let entries = [];
237
+ try { entries = fs.readdirSync(phase_dir); } catch { entries = []; }
238
+ for (const name of entries) {
239
+ if (name === 'PLAN.md' || /^\d{2}(\.\d+)?-\d{2}-PLAN\.md$/.test(name)) {
240
+ const p = path.join(phase_dir, name);
241
+ _rmRecursive(p);
242
+ removed.push(p);
243
+ }
244
+ }
245
+ const tasksDir = path.join(phase_dir, 'tasks');
246
+ if (fs.existsSync(tasksDir)) {
247
+ _rmRecursive(tasksDir);
248
+ removed.push(tasksDir);
249
+ }
250
+
251
+ const preserved = path.join(phase_dir, padded + '-PLAN-REVIEW.md');
252
+ return { aborted: true, removed, preserved: fs.existsSync(preserved) ? preserved : null };
253
+ }
254
+
255
+ function _extractTasksFromPlan(planPath) {
256
+
257
+
258
+ const raw = fs.readFileSync(planPath, 'utf-8');
259
+ const tagRe = /<task\s+([^>]+?)(?:\/>|>[\s\S]*?<\/task>)/g;
260
+ const attrRe = /([a-zA-Z_][a-zA-Z0-9_-]*)\s*=\s*"([^"]*)"/g;
261
+ const out = [];
262
+ let m;
263
+ while ((m = tagRe.exec(raw)) !== null) {
264
+ const attrs = {};
265
+ let a;
266
+ while ((a = attrRe.exec(m[1])) !== null) attrs[a[1]] = a[2];
267
+ const depsRaw = attrs.depends_on || '';
268
+ const deps = depsRaw
269
+ .replace(/^\[|\]$/g, '')
270
+ .split(',')
271
+ .map((s) => s.trim())
272
+ .filter(Boolean);
273
+ out.push({
274
+ id: attrs.id || '',
275
+ frontmatter: {
276
+ depends_on: deps,
277
+ wave: attrs.wave ? Number(attrs.wave) : undefined,
278
+ tier: attrs.tier || undefined,
279
+ },
280
+ });
281
+ }
282
+ return out;
283
+ }
284
+
285
+ function _planPhasePromoteCheck(phaseArg, cwd) {
286
+ const phase = getPhase(phaseArg, cwd);
287
+ const slug = phase.slug || phaseSlug(phase.name);
288
+ const phase_dir = _resolvePhaseDir(phaseArg, cwd, slug);
289
+ const plans = listPlans(phase_dir);
290
+ if (plans.length === 0) return { promote: false, triggers: [] };
291
+
292
+
293
+ const planPath = plans[0];
294
+
295
+ parsePlan(planPath);
296
+ const tasks = _extractTasksFromPlan(planPath);
297
+ return shouldPromoteToTasks({ tasks });
298
+ }
299
+
300
+ function run(args, ctx) {
301
+ const context = ctx || {};
302
+ const cwd = context.cwd || process.cwd();
303
+ const stdout = context.stdout || process.stdout;
304
+ const list = Array.isArray(args) ? args : [];
305
+ const verb = list[0];
306
+
307
+ switch (verb) {
308
+ case 'init': {
309
+ const phaseArg = _validatePhaseArg(list[1]);
310
+ const payload = _initPayload(phaseArg, cwd);
311
+ _emit(payload, stdout, cwd);
312
+ return payload;
313
+ }
314
+ case 'plan-review-append': {
315
+ const phaseArg = _validatePhaseArg(list[1]);
316
+ const iter = list[2];
317
+ const verdictPath = list[3];
318
+ if (!iter || !verdictPath) {
319
+ throw new NubosPilotError(
320
+ 'plan-phase-missing-args',
321
+ 'plan-review-append requires <phase> <iter> <verdictJsonPath>',
322
+ { got: list.slice(1) },
323
+ );
324
+ }
325
+ const result = _planReviewAppend(phaseArg, iter, verdictPath, cwd);
326
+ _emit(result, stdout, cwd);
327
+ return result;
328
+ }
329
+ case 'plan-phase-abort': {
330
+ const phaseArg = _validatePhaseArg(list[1]);
331
+ const result = _planPhaseAbort(phaseArg, cwd);
332
+ _emit(result, stdout, cwd);
333
+ return result;
334
+ }
335
+ case 'plan-phase-promote-check': {
336
+ const phaseArg = _validatePhaseArg(list[1]);
337
+ const result = _planPhasePromoteCheck(phaseArg, cwd);
338
+ _emit(result, stdout, cwd);
339
+ return result;
340
+ }
341
+ default:
342
+ throw new NubosPilotError(
343
+ 'plan-phase-unknown-verb',
344
+ 'plan-phase: unknown verb: ' + String(verb),
345
+ { verb: verb },
346
+ );
347
+ }
348
+ }
349
+
350
+ module.exports = { run, INLINE_THRESHOLD_BYTES };
@@ -0,0 +1,263 @@
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 crypto = require('node:crypto');
6
+
7
+ const { makeSandbox, seedRoadmapYaml, seedPhaseDir, cleanupAll } =
8
+ require('../../tests/helpers/fixture.cjs');
9
+ const subcmd = require('./plan-phase.cjs');
10
+
11
+ function _baseRoadmap() {
12
+ return {
13
+ schema_version: 1,
14
+ milestones: [
15
+ {
16
+ id: 'v1.0',
17
+ name: 'first',
18
+ phases: [
19
+ {
20
+ number: 5,
21
+ name: 'Planning Workflows',
22
+ slug: 'planning-workflows',
23
+ goal: 'Ship the plan-phase orchestrator',
24
+ depends_on: [],
25
+ requirements: ['PLAN-04'],
26
+ success_criteria: ['plan-phase workflow exists'],
27
+ status: 'planned',
28
+ plans: [],
29
+ },
30
+ ],
31
+ },
32
+ ],
33
+ };
34
+ }
35
+
36
+ function _capture() {
37
+ let buf = '';
38
+ const stub = { write: (s) => { buf += s; return true; } };
39
+ return { stub, get: () => buf };
40
+ }
41
+
42
+ function _seed(phaseFiles) {
43
+ const sandbox = makeSandbox();
44
+ seedRoadmapYaml(sandbox, _baseRoadmap());
45
+ const dir = seedPhaseDir(sandbox, 5, 'planning-workflows', phaseFiles || {});
46
+ return { sandbox, phaseDir: dir };
47
+ }
48
+
49
+ afterEach(cleanupAll);
50
+
51
+ test('PP-1: run(["init", "5"]) returns payload with expected shape', () => {
52
+ const { sandbox, phaseDir } = _seed({
53
+ '05-CONTEXT.md': '# ctx',
54
+ '05-RESEARCH.md': '# res',
55
+ });
56
+ const cap = _capture();
57
+ const payload = subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap.stub });
58
+ const raw = cap.get().trim();
59
+ assert.ok(!raw.startsWith('@file:'));
60
+ const parsed = JSON.parse(raw);
61
+ assert.equal(parsed.phase, '5');
62
+ assert.equal(parsed.padded, '05');
63
+ assert.equal(parsed.phase_dir, phaseDir);
64
+ assert.equal(parsed.phase_name, 'Planning Workflows');
65
+ assert.equal(parsed.goal, 'Ship the plan-phase orchestrator');
66
+ assert.deepEqual(parsed.requirements, ['PLAN-04']);
67
+ assert.equal(parsed.has_context, true);
68
+ assert.equal(parsed.has_research, true);
69
+ assert.equal(parsed.has_plan, false);
70
+ assert.equal(parsed.planner_tier, 'opus');
71
+ assert.equal(parsed.checker_tier, 'opus');
72
+ assert.ok(parsed.plan_review_path.endsWith('05-PLAN-REVIEW.md'));
73
+ assert.ok(parsed.agent_skills && 'np-planner' in parsed.agent_skills);
74
+ assert.ok(parsed.agent_skills && 'np-plan-checker' in parsed.agent_skills);
75
+
76
+ assert.equal(payload.phase, '5');
77
+ });
78
+
79
+ test('PP-2: init payload has_context/has_research/has_plan reflect actual disk state', () => {
80
+ const { sandbox } = _seed({});
81
+ const cap = _capture();
82
+ subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap.stub });
83
+ const parsed = JSON.parse(cap.get().trim());
84
+ assert.equal(parsed.has_context, false);
85
+ assert.equal(parsed.has_research, false);
86
+ assert.equal(parsed.has_plan, false);
87
+
88
+ fs.writeFileSync(path.join(parsed.phase_dir, '05-01-PLAN.md'), '---\nphase: "5"\n---\n');
89
+ const cap2 = _capture();
90
+ subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap2.stub });
91
+ const p2 = JSON.parse(cap2.get().trim());
92
+ assert.equal(p2.has_plan, true);
93
+ });
94
+
95
+ test('PP-3: plan-review-append creates PLAN-REVIEW.md with dated iteration section', () => {
96
+ const { sandbox, phaseDir } = _seed({});
97
+ const verdict = { status: 'issues_found', findings: [
98
+ { category: 'missing-success-criterion', severity: 'critical',
99
+ target: 'PLAN.md §SC-3', message: 'No task addresses SC-3.' },
100
+ ]};
101
+ const verdictPath = path.join(sandbox, 'verdict-1.json');
102
+ fs.writeFileSync(verdictPath, JSON.stringify(verdict), 'utf-8');
103
+
104
+ const cap = _capture();
105
+ subcmd.run(['plan-review-append', '5', '1', verdictPath], { cwd: sandbox, stdout: cap.stub });
106
+
107
+ const reviewPath = path.join(phaseDir, '05-PLAN-REVIEW.md');
108
+ assert.ok(fs.existsSync(reviewPath));
109
+ const body = fs.readFileSync(reviewPath, 'utf-8');
110
+ assert.match(body, /## Iteration 1 - \d{4}-\d{2}-\d{2}T/);
111
+ assert.match(body, /\*\*Checker verdict:\*\* issues_found/);
112
+ assert.match(body, /```yaml[\s\S]*missing-success-criterion[\s\S]*```/);
113
+ });
114
+
115
+ test('PP-4: plan-review-append is append-only (iteration 1 bytes preserved in iter 2)', () => {
116
+ const { sandbox, phaseDir } = _seed({});
117
+ const reviewPath = path.join(phaseDir, '05-PLAN-REVIEW.md');
118
+
119
+ const v1 = { status: 'issues_found', findings: [
120
+ { category: 'non-atomic-task', severity: 'major', target: 'T02', message: 'T02 bundles concerns' },
121
+ ]};
122
+ const v2 = { status: 'passed', findings: [] };
123
+ const v1Path = path.join(sandbox, 'v1.json');
124
+ const v2Path = path.join(sandbox, 'v2.json');
125
+ fs.writeFileSync(v1Path, JSON.stringify(v1), 'utf-8');
126
+ fs.writeFileSync(v2Path, JSON.stringify(v2), 'utf-8');
127
+
128
+ subcmd.run(['plan-review-append', '5', '1', v1Path], { cwd: sandbox, stdout: _capture().stub });
129
+ const afterIter1 = fs.readFileSync(reviewPath, 'utf-8');
130
+ const iter1Sha = crypto.createHash('sha256').update(afterIter1).digest('hex');
131
+
132
+ subcmd.run(['plan-review-append', '5', '2', v2Path], { cwd: sandbox, stdout: _capture().stub });
133
+ const afterIter2 = fs.readFileSync(reviewPath, 'utf-8');
134
+
135
+ assert.ok(afterIter2.startsWith(afterIter1),
136
+ 'iter 1 bytes must be a prefix of iter 2 contents (append-only invariant)');
137
+
138
+ const iter1Sha2 = crypto.createHash('sha256').update(afterIter2.slice(0, afterIter1.length)).digest('hex');
139
+ assert.equal(iter1Sha, iter1Sha2);
140
+ assert.match(afterIter2, /## Iteration 2 - /);
141
+ });
142
+
143
+ test('PP-5: plan-phase-abort deletes PLAN.md + tasks/ but preserves PLAN-REVIEW.md', () => {
144
+ const { sandbox, phaseDir } = _seed({
145
+ '05-01-PLAN.md': '---\nphase: "5"\n---\n',
146
+ '05-PLAN-REVIEW.md': '## Iteration 1 - 2026-01-01T00:00:00Z\npreserve me\n',
147
+ });
148
+ const tasksDir = path.join(phaseDir, 'tasks');
149
+ fs.mkdirSync(tasksDir, { recursive: true });
150
+ fs.writeFileSync(path.join(tasksDir, 'T01.md'), 'x', 'utf-8');
151
+
152
+ const cap = _capture();
153
+ subcmd.run(['plan-phase-abort', '5'], { cwd: sandbox, stdout: cap.stub });
154
+
155
+ assert.ok(!fs.existsSync(path.join(phaseDir, '05-01-PLAN.md')));
156
+ assert.ok(!fs.existsSync(tasksDir));
157
+ assert.ok(fs.existsSync(path.join(phaseDir, '05-PLAN-REVIEW.md')));
158
+ const body = fs.readFileSync(path.join(phaseDir, '05-PLAN-REVIEW.md'), 'utf-8');
159
+ assert.match(body, /preserve me/);
160
+ });
161
+
162
+ test('PP-6: plan-phase-promote-check returns {promote:false} on linear same-tier plan', () => {
163
+ const plan = [
164
+ '---',
165
+ 'phase: "5"',
166
+ 'plan: "05-01"',
167
+ '---',
168
+ '',
169
+ '<tasks>',
170
+ '<task id="T01" wave="1" tier="sonnet" depends_on="[]">one</task>',
171
+ '<task id="T02" wave="2" tier="sonnet" depends_on="[T01]">two</task>',
172
+ '</tasks>',
173
+ ].join('\n');
174
+ const { sandbox, phaseDir } = _seed({ '05-01-PLAN.md': plan });
175
+ const cap = _capture();
176
+ subcmd.run(['plan-phase-promote-check', '5'], { cwd: sandbox, stdout: cap.stub });
177
+ const out = JSON.parse(cap.get().trim());
178
+ assert.equal(out.promote, false);
179
+ assert.deepEqual(out.triggers, []);
180
+
181
+ void phaseDir;
182
+ });
183
+
184
+ test('PP-7: plan-phase-promote-check flags parallelism on parallel-tier plan', () => {
185
+ const plan = [
186
+ '---',
187
+ 'phase: "5"',
188
+ 'plan: "05-01"',
189
+ '---',
190
+ '',
191
+ '<tasks>',
192
+ '<task id="T01" wave="1" tier="sonnet" depends_on="[]">one</task>',
193
+ '<task id="T02" wave="1" tier="sonnet" depends_on="[]">two</task>',
194
+ '<task id="T03" wave="2" tier="opus" depends_on="[T01, T02]">three</task>',
195
+ '</tasks>',
196
+ ].join('\n');
197
+ const { sandbox } = _seed({ '05-01-PLAN.md': plan });
198
+ const cap = _capture();
199
+ subcmd.run(['plan-phase-promote-check', '5'], { cwd: sandbox, stdout: cap.stub });
200
+ const out = JSON.parse(cap.get().trim());
201
+ assert.equal(out.promote, true);
202
+ assert.ok(out.triggers.includes('parallelism'));
203
+ });
204
+
205
+ test('PP-8: unknown verb throws NubosPilotError("plan-phase-unknown-verb")', () => {
206
+ const { sandbox } = _seed({});
207
+ const cap = _capture();
208
+ assert.throws(
209
+ () => subcmd.run(['bad', '5'], { cwd: sandbox, stdout: cap.stub }),
210
+ (err) => err && err.name === 'NubosPilotError' && err.code === 'plan-phase-unknown-verb'
211
+ && err.details && err.details.verb === 'bad',
212
+ );
213
+ });
214
+
215
+ test('PP-9: oversized payload emits @file: pointer', () => {
216
+ const { sandbox } = _seed({});
217
+
218
+ const cfgDir = path.join(sandbox, '.nubos-pilot');
219
+ const big = [];
220
+ for (let i = 0; i < 3000; i++) big.push('skill-' + i);
221
+ fs.writeFileSync(
222
+ path.join(cfgDir, 'config.json'),
223
+ JSON.stringify({ agent_skills: { planner: big, 'np-plan-checker': big } }),
224
+ 'utf-8',
225
+ );
226
+ const cap = _capture();
227
+ subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap.stub });
228
+ const raw = cap.get().trim();
229
+ assert.ok(raw.startsWith('@file:'), 'expected @file: pointer for oversized payload');
230
+ });
231
+
232
+ const { execFileSync } = require('node:child_process');
233
+
234
+ function _initGitRepo(root) {
235
+ execFileSync('git', ['-C', root, 'init', '-q', '-b', 'main'], { stdio: 'pipe' });
236
+ execFileSync('git', ['-C', root, 'config', 'user.email', 'test@nubos-pilot.local']);
237
+ execFileSync('git', ['-C', root, 'config', 'user.name', 'nubos-test']);
238
+ execFileSync('git', ['-C', root, 'commit', '--allow-empty', '-q', '-m', 'chore: init'], { stdio: 'pipe' });
239
+ }
240
+
241
+ test('PP-PD-1: init payload has plan_diff_required=true when HEAD has 05-01-PLAN.md committed', () => {
242
+ const { sandbox, phaseDir } = _seed({});
243
+ _initGitRepo(sandbox);
244
+ const rel = path.relative(sandbox, path.join(phaseDir, '05-01-PLAN.md'));
245
+ fs.writeFileSync(path.join(sandbox, rel), '---\nphase: "5"\n---\n', 'utf-8');
246
+ execFileSync('git', ['-C', sandbox, 'add', '--', rel], { stdio: 'pipe' });
247
+ execFileSync('git', ['-C', sandbox, 'commit', '-q', '-m', 'seed PLAN.md'], { stdio: 'pipe' });
248
+ const cap = _capture();
249
+ subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap.stub });
250
+ const parsed = JSON.parse(cap.get().trim());
251
+ assert.equal(parsed.plan_diff_required, true);
252
+ assert.equal(parsed.plan_diff_plan_path, rel);
253
+ });
254
+
255
+ test('PP-PD-2: init payload has plan_diff_required=false for first-time planning', () => {
256
+ const { sandbox } = _seed({});
257
+ _initGitRepo(sandbox);
258
+ const cap = _capture();
259
+ subcmd.run(['init', '5'], { cwd: sandbox, stdout: cap.stub });
260
+ const parsed = JSON.parse(cap.get().trim());
261
+ assert.equal(parsed.plan_diff_required, false);
262
+ assert.match(parsed.plan_diff_plan_path, /05-01-PLAN\.md$/);
263
+ });
@@ -0,0 +1,7 @@
1
+ const { readProgress } = require('../../lib/progress.cjs');
2
+
3
+ function run(_args, cwd) {
4
+ return readProgress(cwd || process.cwd());
5
+ }
6
+
7
+ module.exports = { run };
@@ -0,0 +1,44 @@
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 progressCmd = require('./progress.cjs');
8
+
9
+ const sandboxes = [];
10
+ function mkTmp() {
11
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-progress-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('PROG-CMD-1: run returns persisted progress block from STATE.md', () => {
24
+ const root = mkTmp();
25
+ fs.writeFileSync(path.join(root, '.nubos-pilot', 'STATE.md'),
26
+ '---\nschema_version: 2\ncurrent_phase: 1\ncurrent_plan: null\ncurrent_task: null\n' +
27
+ 'last_updated: 2026-04-15\n' +
28
+ 'progress:\n' +
29
+ ' total_phases: 5\n completed_phases: 2\n total_plans: 11\n completed_plans: 4\n percent: 36\n' +
30
+ '---\n\n# S\n');
31
+ const payload = progressCmd.run([], root);
32
+ assert.equal(payload.total_phases, 5);
33
+ assert.equal(payload.completed_phases, 2);
34
+ assert.equal(payload.total_plans, 11);
35
+ assert.equal(payload.completed_plans, 4);
36
+ assert.equal(payload.percent, 36);
37
+ });
38
+
39
+ test('PROG-CMD-2: run on fresh sandbox (no STATE.md) returns zero-block', () => {
40
+ const root = mkTmp();
41
+ const payload = progressCmd.run([], root);
42
+ assert.equal(payload.total_phases, 0);
43
+ assert.equal(payload.percent, 0);
44
+ });