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,177 @@
1
+ const { extractFrontmatter } = require('./frontmatter.cjs');
2
+ const { test } = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+
5
+ test('FM-A1: unquoted string + boolean scalars', () => {
6
+ const raw = '---\nphase: 03\nautonomous: true\n---\nbody';
7
+ const { frontmatter, body } = extractFrontmatter(raw);
8
+ assert.equal(frontmatter.phase, '03');
9
+ assert.equal(frontmatter.autonomous, true);
10
+ assert.equal(body, 'body');
11
+ });
12
+
13
+ test('FM-A2: unquoted numeric scalar → number', () => {
14
+ const raw = '---\nwave: 1\n---\n';
15
+ const { frontmatter } = extractFrontmatter(raw);
16
+ assert.equal(frontmatter.wave, 1);
17
+ assert.equal(typeof frontmatter.wave, 'number');
18
+ });
19
+
20
+ test('FM-A3: empty and tilde → null', () => {
21
+ const raw = '---\nnullable:\nanother: ~\n---\n';
22
+ const { frontmatter } = extractFrontmatter(raw);
23
+ assert.equal(frontmatter.nullable, null);
24
+ assert.equal(frontmatter.another, null);
25
+ });
26
+
27
+ test('FM-A4: missing frontmatter block → empty object, body === raw', () => {
28
+ const raw = 'no frontmatter here\njust body\n';
29
+ const { frontmatter, body } = extractFrontmatter(raw);
30
+ assert.deepEqual(frontmatter, {});
31
+ assert.equal(body, raw);
32
+ });
33
+
34
+ test('FM-B1: double-quoted value with embedded colon', () => {
35
+ const raw = '---\nkey: "value: with colon"\n---\n';
36
+ const { frontmatter } = extractFrontmatter(raw);
37
+ assert.equal(frontmatter.key, 'value: with colon');
38
+ });
39
+
40
+ test('FM-B2: single-quoted value', () => {
41
+ const raw = "---\nkey: 'single-quoted'\n---\n";
42
+ const { frontmatter } = extractFrontmatter(raw);
43
+ assert.equal(frontmatter.key, 'single-quoted');
44
+ });
45
+
46
+ test('FM-C1: inline array parses to JS array, not literal string (RESEARCH Pitfall 2)', () => {
47
+ const raw = '---\ndepends_on: [T-02, T-03]\n---\n';
48
+ const { frontmatter } = extractFrontmatter(raw);
49
+ assert.ok(Array.isArray(frontmatter.depends_on));
50
+ assert.equal(frontmatter.depends_on.length, 2);
51
+ assert.deepEqual(frontmatter.depends_on, ['T-02', 'T-03']);
52
+ });
53
+
54
+ test('FM-C2: empty inline array', () => {
55
+ const raw = '---\nempty: []\n---\n';
56
+ const { frontmatter } = extractFrontmatter(raw);
57
+ assert.ok(Array.isArray(frontmatter.empty));
58
+ assert.equal(frontmatter.empty.length, 0);
59
+ });
60
+
61
+ test('FM-C3: quote-aware comma split inside inline array', () => {
62
+ const raw = '---\nquoted: ["a, b", "c"]\n---\n';
63
+ const { frontmatter } = extractFrontmatter(raw);
64
+ assert.deepEqual(frontmatter.quoted, ['a, b', 'c']);
65
+ });
66
+
67
+ test('FM-D1: nested must_haves with truths + artifacts round-trips to structured JS', () => {
68
+ const raw = [
69
+ '---',
70
+ 'must_haves:',
71
+ ' truths:',
72
+ ' - "first truth"',
73
+ ' - "second truth"',
74
+ ' artifacts:',
75
+ ' - path: lib/plan.cjs',
76
+ ' provides: "parser"',
77
+ ' exports: ["parsePlan", "listPlans"]',
78
+ '---',
79
+ 'body',
80
+ ].join('\n');
81
+ const { frontmatter } = extractFrontmatter(raw);
82
+ assert.ok(frontmatter.must_haves, 'must_haves key present');
83
+ assert.deepEqual(frontmatter.must_haves.truths, ['first truth', 'second truth']);
84
+ assert.ok(Array.isArray(frontmatter.must_haves.artifacts));
85
+ assert.equal(frontmatter.must_haves.artifacts.length, 1);
86
+ assert.equal(frontmatter.must_haves.artifacts[0].path, 'lib/plan.cjs');
87
+ assert.equal(frontmatter.must_haves.artifacts[0].provides, 'parser');
88
+ assert.deepEqual(frontmatter.must_haves.artifacts[0].exports, ['parsePlan', 'listPlans']);
89
+ });
90
+
91
+ test('FM-E1: bullet list under key → array of strings', () => {
92
+ const raw = '---\nfiles_modified:\n - lib/a.cjs\n - lib/b.cjs\n---\n';
93
+ const { frontmatter } = extractFrontmatter(raw);
94
+ assert.deepEqual(frontmatter.files_modified, ['lib/a.cjs', 'lib/b.cjs']);
95
+ });
96
+
97
+ test('FM-F1: unclosed quoted string → NubosPilotError frontmatter-parse-error', () => {
98
+ const raw = '---\nkey: "unclosed\n---\n';
99
+ let thrown = null;
100
+ try {
101
+ extractFrontmatter(raw);
102
+ } catch (err) {
103
+ thrown = err;
104
+ }
105
+ assert.ok(thrown, 'expected throw on unclosed quote');
106
+ assert.equal(thrown.name, 'NubosPilotError');
107
+ assert.equal(thrown.code, 'frontmatter-parse-error');
108
+ assert.ok(thrown.details, 'details object present');
109
+ assert.equal(typeof thrown.details.line, 'number');
110
+ assert.equal(typeof thrown.details.snippet, 'string');
111
+ });
112
+
113
+ test('FM-F2: tab indent in nested block → NubosPilotError frontmatter-parse-error', () => {
114
+ const raw = '---\nmust_haves:\n\ttruths:\n\t\t- "bad tabs"\n---\n';
115
+ let thrown = null;
116
+ try {
117
+ extractFrontmatter(raw);
118
+ } catch (err) {
119
+ thrown = err;
120
+ }
121
+ assert.ok(thrown, 'expected throw on tab-indented nested block');
122
+ assert.equal(thrown.name, 'NubosPilotError');
123
+ assert.equal(thrown.code, 'frontmatter-parse-error');
124
+ });
125
+
126
+ test('FM-G1: nested must_haves (truths/artifacts/key_links) from a real plan parses cleanly', () => {
127
+
128
+
129
+ const raw = [
130
+ '---',
131
+ 'phase: 03',
132
+ 'plan: 01',
133
+ 'type: execute',
134
+ 'wave: 0',
135
+ 'depends_on: []',
136
+ 'files_modified:',
137
+ ' - lib/frontmatter.cjs',
138
+ ' - lib/frontmatter.test.cjs',
139
+ 'autonomous: true',
140
+ 'requirements: [LIB-06, LIB-07]',
141
+ 'must_haves:',
142
+ ' truths:',
143
+ ' - "lib/frontmatter.cjs exists and exports extractFrontmatter"',
144
+ ' - "Nested frontmatter round-trips as a structured JS object"',
145
+ ' - "Inline arrays parse as JS arrays"',
146
+ ' - "Quoted scalars containing colon parse correctly"',
147
+ ' artifacts:',
148
+ ' - path: lib/frontmatter.cjs',
149
+ ' provides: "hand-rolled YAML frontmatter parser"',
150
+ ' exports: ["extractFrontmatter"]',
151
+ ' - path: lib/frontmatter.test.cjs',
152
+ ' provides: "node:test suite"',
153
+ ' contains: "node:test"',
154
+ ' key_links:',
155
+ ' - from: "lib/frontmatter.cjs"',
156
+ ' to: "Node builtins only"',
157
+ ' via: "require(node:fs) / no other requires"',
158
+ ' pattern: "require"',
159
+ '---',
160
+ '<objective>body</objective>',
161
+ ].join('\n');
162
+ const { frontmatter } = extractFrontmatter(raw);
163
+ assert.equal(frontmatter.phase, '03');
164
+ assert.ok(Array.isArray(frontmatter.depends_on));
165
+ assert.equal(frontmatter.depends_on.length, 0);
166
+ assert.ok(Array.isArray(frontmatter.files_modified));
167
+ assert.equal(frontmatter.files_modified.length, 2);
168
+ assert.ok(Array.isArray(frontmatter.requirements));
169
+ assert.deepEqual(frontmatter.requirements, ['LIB-06', 'LIB-07']);
170
+ assert.ok(frontmatter.must_haves, 'must_haves present');
171
+ assert.ok(Array.isArray(frontmatter.must_haves.truths));
172
+ assert.ok(frontmatter.must_haves.truths.length >= 4, 'truths has ≥4 entries');
173
+ assert.ok(Array.isArray(frontmatter.must_haves.artifacts));
174
+ assert.equal(frontmatter.must_haves.artifacts[0].path, 'lib/frontmatter.cjs');
175
+ assert.ok(Array.isArray(frontmatter.must_haves.key_links));
176
+ assert.equal(frontmatter.must_haves.key_links[0].from, 'lib/frontmatter.cjs');
177
+ });
package/lib/gaps.cjs ADDED
@@ -0,0 +1,197 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { NubosPilotError, findProjectRoot } = require('./core.cjs');
4
+ const { addPhase, insertPhaseAfter, parseRoadmap } = require('./roadmap.cjs');
5
+
6
+ const MAX_AUDIT_BYTES = 1024 * 1024;
7
+
8
+ function _projectRoot(cwd) {
9
+ try {
10
+ return findProjectRoot(cwd);
11
+ } catch (err) {
12
+ if (err && err.code === 'not-in-project') return path.resolve(cwd);
13
+ throw err;
14
+ }
15
+ }
16
+
17
+ function _phasesDir(cwd) {
18
+ const root = _projectRoot(cwd);
19
+
20
+ const primary = path.join(root, '.nubos-pilot', 'phases');
21
+ if (fs.existsSync(primary)) return primary;
22
+ return path.join(root, '.planning', 'phases');
23
+ }
24
+
25
+ function _parseDirNumber(name) {
26
+ const m = name.match(/^(\d+(?:\.\d+)?)-/);
27
+ if (!m) return null;
28
+ const n = Number(m[1]);
29
+ return Number.isFinite(n) ? n : null;
30
+ }
31
+
32
+ function scanVerifications(_milestoneId, cwd = process.cwd()) {
33
+ const dir = _phasesDir(cwd);
34
+ if (!fs.existsSync(dir)) return [];
35
+ const entries = fs.readdirSync(dir).sort();
36
+ const gaps = [];
37
+ for (const entry of entries) {
38
+ const sourcePhase = _parseDirNumber(entry);
39
+ if (sourcePhase == null) continue;
40
+
41
+
42
+ const pad = entry.split('-')[0];
43
+ const candidates = [
44
+ path.join(dir, entry, pad + '-VERIFICATION.md'),
45
+ path.join(dir, entry, 'VERIFICATION.md'),
46
+ ];
47
+ const verPath = candidates.find((p) => fs.existsSync(p));
48
+ if (!verPath) continue;
49
+ const body = fs.readFileSync(verPath, 'utf-8');
50
+ for (const m of body.matchAll(/^## Gap:\s*(.+)$/gm)) {
51
+ gaps.push({
52
+ source_phase: sourcePhase,
53
+ gap_type: 'explicit',
54
+ description: m[1].trim(),
55
+ severity: 'major',
56
+ });
57
+ }
58
+ for (const m of body.matchAll(/^- \[ \]\s*(.+)$/gm)) {
59
+ gaps.push({
60
+ source_phase: sourcePhase,
61
+ gap_type: 'unchecked-box',
62
+ description: m[1].trim(),
63
+ severity: 'minor',
64
+ });
65
+ }
66
+ for (const m of body.matchAll(/^(.*(?:❌|FAIL).*)$/gm)) {
67
+ gaps.push({
68
+ source_phase: sourcePhase,
69
+ gap_type: 'fail-marker',
70
+ description: m[1].trim(),
71
+ severity: 'critical',
72
+ });
73
+ }
74
+ }
75
+ return gaps;
76
+ }
77
+
78
+ function parseAuditFile(filepath, cwd = process.cwd()) {
79
+ const root = _projectRoot(cwd);
80
+ const resolved = path.resolve(cwd, filepath);
81
+
82
+
83
+
84
+ const inside = resolved === root || resolved.startsWith(root + path.sep);
85
+ if (!inside) {
86
+ throw new NubosPilotError(
87
+ 'gaps-invalid-audit-path',
88
+ 'audit file must be inside the project root',
89
+ { path: filepath, resolved, projectRoot: root },
90
+ );
91
+ }
92
+ let stat;
93
+ try { stat = fs.statSync(resolved); } catch (err) {
94
+ throw new NubosPilotError(
95
+ 'gaps-audit-not-found',
96
+ 'audit file not found',
97
+ { path: resolved, cause: err && err.code },
98
+ );
99
+ }
100
+ if (stat.size > MAX_AUDIT_BYTES) {
101
+ throw new NubosPilotError(
102
+ 'gaps-audit-too-large',
103
+ 'audit file exceeds ' + MAX_AUDIT_BYTES + ' byte cap',
104
+ { path: resolved, size: stat.size },
105
+ );
106
+ }
107
+ const body = fs.readFileSync(resolved, 'utf-8');
108
+
109
+ const sections = body.split(/^## Gap:\s*/gm).slice(1);
110
+ const gaps = [];
111
+ for (const section of sections) {
112
+ const [titleLine, ...rest] = section.split('\n');
113
+ const title = titleLine.trim();
114
+ const m = rest.join('\n').match(/\*\*Source phase:\*\*\s*(\d+(?:\.\d+)?)/m);
115
+ if (!m) {
116
+ throw new NubosPilotError(
117
+ 'gaps-missing-source-phase',
118
+ '## Gap: section missing "**Source phase:** N" line',
119
+ { path: resolved, gapTitle: title },
120
+ );
121
+ }
122
+ gaps.push({
123
+ source_phase: Number(m[1]),
124
+ gap_type: 'explicit',
125
+ description: title,
126
+ severity: 'major',
127
+ });
128
+ }
129
+ return gaps;
130
+ }
131
+
132
+ function _currentMilestoneId(cwd) {
133
+ try {
134
+ const { readState } = require('./state.cjs');
135
+ const st = readState(cwd);
136
+ if (st && st.frontmatter && st.frontmatter.milestone) {
137
+ return String(st.frontmatter.milestone);
138
+ }
139
+ } catch (_err) {
140
+
141
+ }
142
+ const YAML = require('yaml');
143
+ const { projectStateDir } = require('./core.cjs');
144
+ let stateDir;
145
+ try { stateDir = projectStateDir(cwd); } catch (_err) {
146
+ stateDir = path.join(path.resolve(cwd), '.nubos-pilot');
147
+ }
148
+ const yamlPath = path.join(stateDir, 'roadmap.yaml');
149
+ const raw = fs.readFileSync(yamlPath, 'utf-8');
150
+ const doc = YAML.parse(raw);
151
+ if (!doc || !Array.isArray(doc.milestones) || doc.milestones.length === 0) {
152
+ throw new NubosPilotError(
153
+ 'gaps-no-milestone',
154
+ 'roadmap.yaml has no milestones — cannot resolve current milestone',
155
+ { path: yamlPath },
156
+ );
157
+ }
158
+ return String(doc.milestones[0].id);
159
+ }
160
+
161
+ function gapsToPhases(gaps, opts, cwd = process.cwd()) {
162
+ const options = opts || {};
163
+ if (!gaps || gaps.length === 0) return [];
164
+ const groups = new Map();
165
+ for (const g of gaps) {
166
+ if (!groups.has(g.source_phase)) groups.set(g.source_phase, []);
167
+ groups.get(g.source_phase).push(g);
168
+ }
169
+ const created = [];
170
+ for (const [source, list] of groups) {
171
+ const phaseDef = {
172
+ slug: 'gap-fix-phase-' + String(source).replace(/\./g, '-'),
173
+ name: 'Gap fix for phase ' + source,
174
+ goal: 'Close ' + list.length + ' gap(s) identified in Phase ' + source + ' VERIFICATION.md / audit',
175
+ depends_on: [source],
176
+ requirements: [],
177
+ success_criteria: [],
178
+ status: 'pending',
179
+ plans: [],
180
+ };
181
+ const result = options.insertAfter != null
182
+ ? insertPhaseAfter(options.insertAfter, phaseDef, cwd)
183
+ : addPhase(_currentMilestoneId(cwd), phaseDef, cwd);
184
+ created.push(result);
185
+ }
186
+ return created;
187
+ }
188
+
189
+ module.exports = {
190
+ scanVerifications,
191
+ parseAuditFile,
192
+ gapsToPhases,
193
+
194
+ _currentMilestoneId,
195
+ };
196
+
197
+ void parseRoadmap;
@@ -0,0 +1,200 @@
1
+ const { test, afterEach } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { makeSandbox, seedRoadmapYaml, seedPhaseDir, cleanupAll } = require('../tests/helpers/fixture.cjs');
7
+ const gaps = require('./gaps.cjs');
8
+ const roadmap = require('./roadmap.cjs');
9
+
10
+ const FIXTURE_VERIFICATION = fs.readFileSync(
11
+ path.join(__dirname, '..', 'tests', 'fixtures', 'gaps', 'verification-sample.md'),
12
+ 'utf-8',
13
+ );
14
+ const FIXTURE_AUDIT = fs.readFileSync(
15
+ path.join(__dirname, '..', 'tests', 'fixtures', 'gaps', 'audit-from-file.md'),
16
+ 'utf-8',
17
+ );
18
+
19
+ function _baseRoadmap() {
20
+ return {
21
+ schema_version: 1,
22
+ milestones: [
23
+ {
24
+ id: 'v1.0',
25
+ name: 'first',
26
+ phases: [
27
+ { number: 1, name: 'One', slug: 'one', goal: '', depends_on: [], requirements: [], success_criteria: [], status: 'done', plans: [] },
28
+ { number: 7, name: 'Seven', slug: 'seven', goal: '', depends_on: [6], requirements: [], success_criteria: [], status: 'done', plans: [] },
29
+ { number: 8, name: 'Eight', slug: 'eight', goal: '', depends_on: [7], requirements: [], success_criteria: [], status: 'pending', plans: [] },
30
+ ],
31
+ },
32
+ ],
33
+ };
34
+ }
35
+
36
+ afterEach(cleanupAll);
37
+
38
+ test('GAP-1: scanVerifications returns gap objects with required shape', () => {
39
+ const sandbox = makeSandbox();
40
+ seedRoadmapYaml(sandbox, _baseRoadmap());
41
+ seedPhaseDir(sandbox, 3, 'foo', { '03-VERIFICATION.md': FIXTURE_VERIFICATION });
42
+ const result = gaps.scanVerifications('v1.0', sandbox);
43
+ assert.ok(Array.isArray(result));
44
+ assert.ok(result.length > 0);
45
+ for (const g of result) {
46
+ assert.equal(typeof g.source_phase, 'number');
47
+ assert.equal(typeof g.gap_type, 'string');
48
+ assert.equal(typeof g.description, 'string');
49
+ assert.ok(['critical', 'major', 'minor'].includes(g.severity));
50
+ }
51
+ });
52
+
53
+ test('GAP-2: explicit ## Gap: heading produces gap_type=explicit', () => {
54
+ const sandbox = makeSandbox();
55
+ seedRoadmapYaml(sandbox, _baseRoadmap());
56
+ seedPhaseDir(sandbox, 3, 'foo', { '03-VERIFICATION.md': FIXTURE_VERIFICATION });
57
+ const result = gaps.scanVerifications('v1.0', sandbox);
58
+ const explicit = result.filter((g) => g.gap_type === 'explicit');
59
+ assert.ok(explicit.length >= 1);
60
+ assert.ok(explicit[0].description.toLowerCase().includes('jwt'));
61
+ });
62
+
63
+ test('GAP-3: unchecked checkbox line produces gap_type=unchecked-box', () => {
64
+ const sandbox = makeSandbox();
65
+ seedRoadmapYaml(sandbox, _baseRoadmap());
66
+ seedPhaseDir(sandbox, 3, 'foo', { '03-VERIFICATION.md': FIXTURE_VERIFICATION });
67
+ const result = gaps.scanVerifications('v1.0', sandbox);
68
+ const boxes = result.filter((g) => g.gap_type === 'unchecked-box');
69
+ assert.equal(boxes.length, 2);
70
+ });
71
+
72
+ test('GAP-4: ❌ or FAIL marker produces gap_type=fail-marker', () => {
73
+ const sandbox = makeSandbox();
74
+ seedRoadmapYaml(sandbox, _baseRoadmap());
75
+ seedPhaseDir(sandbox, 3, 'foo', { '03-VERIFICATION.md': FIXTURE_VERIFICATION });
76
+ const result = gaps.scanVerifications('v1.0', sandbox);
77
+ const fails = result.filter((g) => g.gap_type === 'fail-marker');
78
+ assert.ok(fails.length >= 2);
79
+ });
80
+
81
+ test('GAP-5: source_phase extracted from directory name NN-slug (semantic, not positional)', () => {
82
+
83
+
84
+ const sandbox = makeSandbox();
85
+ seedRoadmapYaml(sandbox, _baseRoadmap());
86
+ seedPhaseDir(sandbox, 3, 'foo', { '03-VERIFICATION.md': '## Gap: from three\n' });
87
+ seedPhaseDir(sandbox, 7, 'bar', { '07-VERIFICATION.md': '## Gap: from seven\n' });
88
+ const result = gaps.scanVerifications('v1.0', sandbox);
89
+ const three = result.find((g) => /three/.test(g.description));
90
+ const seven = result.find((g) => /seven/.test(g.description));
91
+ assert.equal(three.source_phase, 3);
92
+ assert.equal(seven.source_phase, 7);
93
+ });
94
+
95
+ test('GAP-6: VERIFICATION.md with no gap signals returns empty array', () => {
96
+ const sandbox = makeSandbox();
97
+ seedRoadmapYaml(sandbox, _baseRoadmap());
98
+ seedPhaseDir(sandbox, 3, 'foo', {
99
+ '03-VERIFICATION.md': '# Verification\n\nAll checks passed.\n- [x] Done\n',
100
+ });
101
+ const result = gaps.scanVerifications('v1.0', sandbox);
102
+ assert.deepEqual(result, []);
103
+ });
104
+
105
+ test('GAP-7: parseAuditFile rejects paths outside project root (ASVS V12)', () => {
106
+ const sandbox = makeSandbox();
107
+ seedRoadmapYaml(sandbox, _baseRoadmap());
108
+ assert.throws(
109
+ () => gaps.parseAuditFile('/etc/passwd', sandbox),
110
+ (err) => err.name === 'NubosPilotError' && err.code === 'gaps-invalid-audit-path',
111
+ );
112
+ });
113
+
114
+ test('GAP-8: parseAuditFile parses ## Gap: sections and reads Source phase line', () => {
115
+ const sandbox = makeSandbox();
116
+ seedRoadmapYaml(sandbox, _baseRoadmap());
117
+ const auditPath = path.join(sandbox, 'audit.md');
118
+ fs.writeFileSync(auditPath, FIXTURE_AUDIT);
119
+ const result = gaps.parseAuditFile(auditPath, sandbox);
120
+ assert.equal(result.length, 2);
121
+ for (const g of result) {
122
+ assert.equal(g.source_phase, 7);
123
+ assert.equal(g.gap_type, 'explicit');
124
+ }
125
+ });
126
+
127
+ test('GAP-9: parseAuditFile throws gaps-missing-source-phase when line missing', () => {
128
+ const sandbox = makeSandbox();
129
+ seedRoadmapYaml(sandbox, _baseRoadmap());
130
+ const auditPath = path.join(sandbox, 'bad-audit.md');
131
+ fs.writeFileSync(auditPath, '# Audit\n\n## Gap: missing source info\n\nNo source phase here.\n');
132
+ assert.throws(
133
+ () => gaps.parseAuditFile(auditPath, sandbox),
134
+ (err) => err.name === 'NubosPilotError' && err.code === 'gaps-missing-source-phase',
135
+ );
136
+ });
137
+
138
+ test('GAP-10: gapsToPhases({insertAfter: null}) appends via addPhase; depends_on=[source]', () => {
139
+ const sandbox = makeSandbox();
140
+ seedRoadmapYaml(sandbox, _baseRoadmap());
141
+ const result = gaps.gapsToPhases(
142
+ [{ source_phase: 7, gap_type: 'explicit', description: 'x', severity: 'major' }],
143
+ { insertAfter: null },
144
+ sandbox,
145
+ );
146
+ assert.equal(result.length, 1);
147
+
148
+ const ms = roadmap.parseRoadmap(sandbox);
149
+ const added = ms.phases.find((p) => p.number === '9');
150
+ assert.ok(added, 'new phase appended at number 9');
151
+ assert.ok(added.depends_on && added.depends_on.includes('7'));
152
+ });
153
+
154
+ test('GAP-11: gapsToPhases({insertAfter: 7}) uses insertPhaseAfter; phase 8 depends_on untouched', () => {
155
+ const sandbox = makeSandbox();
156
+ seedRoadmapYaml(sandbox, _baseRoadmap());
157
+ gaps.gapsToPhases(
158
+ [{ source_phase: 7, gap_type: 'explicit', description: 'x', severity: 'major' }],
159
+ { insertAfter: 7 },
160
+ sandbox,
161
+ );
162
+ const parsed = roadmap.parseRoadmap(sandbox);
163
+ const decimal = parsed.phases.find((p) => p.number === '7.1');
164
+ const eight = parsed.phases.find((p) => p.number === '8');
165
+ assert.ok(decimal, 'decimal phase 7.1 exists');
166
+ assert.ok(eight.depends_on && eight.depends_on.includes('7'), 'phase 8 depends_on still [7]');
167
+ });
168
+
169
+ test('GAP-12: Pitfall 5 — semantic source_phase, NOT positional insertAfter, drives depends_on', () => {
170
+
171
+
172
+
173
+ const sandbox = makeSandbox();
174
+ seedRoadmapYaml(sandbox, _baseRoadmap());
175
+ gaps.gapsToPhases(
176
+ [{ source_phase: 7, gap_type: 'explicit', description: 'semantic test', severity: 'major' }],
177
+ { insertAfter: 8 },
178
+ sandbox,
179
+ );
180
+ const parsed = roadmap.parseRoadmap(sandbox);
181
+
182
+ const created = parsed.phases.find((p) => p.number === '8.1');
183
+ assert.ok(created, 'decimal 8.1 created');
184
+
185
+ assert.ok(
186
+ created.depends_on && created.depends_on.includes('7'),
187
+ 'depends_on includes semantic source 7, not positional base 8',
188
+ );
189
+ });
190
+
191
+ test('GAP-13: empty gaps array returns [] and performs no roadmap mutation', () => {
192
+ const sandbox = makeSandbox();
193
+ seedRoadmapYaml(sandbox, _baseRoadmap());
194
+ const yamlPath = path.join(sandbox, '.nubos-pilot', 'roadmap.yaml');
195
+ const before = fs.readFileSync(yamlPath, 'utf-8');
196
+ const result = gaps.gapsToPhases([], { insertAfter: null }, sandbox);
197
+ assert.deepEqual(result, []);
198
+ const after = fs.readFileSync(yamlPath, 'utf-8');
199
+ assert.equal(before, after, 'roadmap.yaml untouched for empty gaps');
200
+ });