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,416 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const YAML = require('yaml');
4
+ const {
5
+ projectStateDir,
6
+ NubosPilotError,
7
+ withFileLock,
8
+ atomicWriteFileSync,
9
+ } = require('./core.cjs');
10
+ const { renderMarkdown } = require('./roadmap-render.cjs');
11
+
12
+ function roadmapPath(cwd) {
13
+ try {
14
+ return path.join(projectStateDir(cwd), 'roadmap.yaml');
15
+ } catch (err) {
16
+ if (!err || err.code !== 'not-in-project') throw err;
17
+ return path.join(path.resolve(cwd), '.planning', 'roadmap.yaml');
18
+ }
19
+ }
20
+
21
+ function _readRaw(p) {
22
+ try {
23
+ return fs.readFileSync(p, 'utf-8');
24
+ } catch (err) {
25
+ throw new NubosPilotError(
26
+ 'roadmap-parse-error',
27
+ 'roadmap.yaml not readable',
28
+ { path: p, cause: err && err.code },
29
+ );
30
+ }
31
+ }
32
+
33
+ function _normalizeDependsOn(v) {
34
+ if (v == null) return null;
35
+ if (Array.isArray(v)) {
36
+ if (v.length === 0) return null;
37
+ return v.map(String).join(', ');
38
+ }
39
+ return String(v);
40
+ }
41
+
42
+ function _normalizePlans(rawPlans, phaseStatus) {
43
+ if (!Array.isArray(rawPlans)) return [];
44
+ return rawPlans.map((p) => {
45
+ if (p && typeof p === 'object' && 'id' in p) {
46
+ return {
47
+ id: String(p.id),
48
+ title: typeof p.title === 'string' ? p.title : '',
49
+ complete: typeof p.complete === 'boolean' ? p.complete : phaseStatus === 'done',
50
+ };
51
+ }
52
+
53
+ return { id: String(p), title: '', complete: phaseStatus === 'done' };
54
+ });
55
+ }
56
+
57
+ function parseRoadmap(cwd = process.cwd()) {
58
+ const p = roadmapPath(cwd);
59
+ const raw = _readRaw(p);
60
+
61
+ let data;
62
+ try {
63
+ data = YAML.parse(raw);
64
+ } catch (err) {
65
+ throw new NubosPilotError(
66
+ 'roadmap-parse-error',
67
+ 'roadmap.yaml invalid YAML',
68
+ { path: p, cause: err && err.message },
69
+ );
70
+ }
71
+
72
+ if (!data || typeof data !== 'object' || !Array.isArray(data.milestones)) {
73
+ throw new NubosPilotError(
74
+ 'roadmap-parse-error',
75
+ 'roadmap.yaml missing milestones array',
76
+ { path: p },
77
+ );
78
+ }
79
+
80
+ const phases = [];
81
+ for (const ms of data.milestones) {
82
+ if (!ms || !Array.isArray(ms.phases)) continue;
83
+ for (const ph of ms.phases) {
84
+ if (!ph || ph.number == null) continue;
85
+ phases.push({
86
+ number: String(ph.number),
87
+ name: ph.name || '',
88
+ goal: ph.goal || '',
89
+ depends_on: _normalizeDependsOn(ph.depends_on),
90
+ requirements: Array.isArray(ph.requirements) ? ph.requirements.slice() : [],
91
+ success_criteria: Array.isArray(ph.success_criteria) ? ph.success_criteria.slice() : [],
92
+ plans: _normalizePlans(ph.plans, ph.status),
93
+ complete: ph.status === 'done' || ph.status === 'complete',
94
+ });
95
+ }
96
+ }
97
+
98
+ return { phases, raw, doc: data, path: p };
99
+ }
100
+
101
+ function getPhase(n, cwd = process.cwd()) {
102
+ const want = String(n);
103
+ const { phases } = parseRoadmap(cwd);
104
+ const hit = phases.find((p) => p.number === want);
105
+ if (!hit) {
106
+ throw new NubosPilotError(
107
+ 'phase-not-found',
108
+ `Phase ${want} not found in roadmap.yaml`,
109
+ { requested: want },
110
+ );
111
+ }
112
+ return hit;
113
+ }
114
+
115
+ function listPhases(cwd = process.cwd()) {
116
+ return parseRoadmap(cwd).phases;
117
+ }
118
+
119
+ function phaseComplete(n, cwd = process.cwd()) {
120
+ return getPhase(n, cwd).complete;
121
+ }
122
+
123
+ const _MAX_ROADMAP_BYTES = 1024 * 1024;
124
+ const _SLUG_RE = /^[a-z0-9-]+$/;
125
+
126
+ function _mdPath(cwd) {
127
+ try {
128
+ return path.join(projectStateDir(cwd), 'ROADMAP.md');
129
+ } catch (err) {
130
+ if (!err || err.code !== 'not-in-project') throw err;
131
+ return path.join(path.resolve(cwd), '.planning', 'ROADMAP.md');
132
+ }
133
+ }
134
+
135
+ function _mutate(cwd, fn) {
136
+ const yamlPath = roadmapPath(cwd);
137
+ const mdPath = _mdPath(cwd);
138
+ return withFileLock(yamlPath, () => {
139
+ let stat;
140
+ try { stat = fs.statSync(yamlPath); } catch (err) {
141
+ throw new NubosPilotError(
142
+ 'roadmap-write-read-error',
143
+ 'roadmap.yaml not readable',
144
+ { path: yamlPath, cause: err && err.code },
145
+ );
146
+ }
147
+ if (stat.size > _MAX_ROADMAP_BYTES) {
148
+ throw new NubosPilotError(
149
+ 'roadmap-too-large',
150
+ 'roadmap.yaml exceeds 1 MB cap',
151
+ { path: yamlPath, size: stat.size },
152
+ );
153
+ }
154
+ const raw = fs.readFileSync(yamlPath, 'utf-8');
155
+ let doc;
156
+ try { doc = YAML.parse(raw); } catch (err) {
157
+ throw new NubosPilotError(
158
+ 'roadmap-write-parse-error',
159
+ 'roadmap.yaml invalid YAML',
160
+ { path: yamlPath, cause: err && err.message },
161
+ );
162
+ }
163
+ if (!doc || !Array.isArray(doc.milestones)) {
164
+ throw new NubosPilotError(
165
+ 'roadmap-write-parse-error',
166
+ 'roadmap.yaml missing milestones array',
167
+ { path: yamlPath },
168
+ );
169
+ }
170
+ const result = fn(doc);
171
+ atomicWriteFileSync(yamlPath, YAML.stringify(doc, { indent: 2 }));
172
+ atomicWriteFileSync(mdPath, renderMarkdown(doc));
173
+ return result;
174
+ });
175
+ }
176
+
177
+ function _validateSlug(slug) {
178
+ if (slug == null || slug === '' || typeof slug !== 'string') {
179
+ throw new NubosPilotError(
180
+ 'roadmap-invalid-slug',
181
+ 'phase slug required',
182
+ { slug: slug == null ? '' : slug },
183
+ );
184
+ }
185
+ if (!_SLUG_RE.test(slug)) {
186
+ throw new NubosPilotError(
187
+ 'roadmap-invalid-slug',
188
+ 'phase slug must match /^[a-z0-9-]+$/',
189
+ { slug },
190
+ );
191
+ }
192
+ }
193
+
194
+ function _normalizePhaseDef(phaseDef) {
195
+ const def = phaseDef || {};
196
+ _validateSlug(def.slug);
197
+ return {
198
+ slug: def.slug,
199
+ name: def.name || '',
200
+ goal: typeof def.goal === 'string' ? def.goal : '',
201
+ depends_on: Array.isArray(def.depends_on) ? def.depends_on.slice() : [],
202
+ requirements: Array.isArray(def.requirements) ? def.requirements.slice() : [],
203
+ success_criteria: Array.isArray(def.success_criteria) ? def.success_criteria.slice() : [],
204
+ status: typeof def.status === 'string' ? def.status : 'pending',
205
+ plans: Array.isArray(def.plans) ? def.plans.slice() : [],
206
+ };
207
+ }
208
+
209
+ function addMilestone(milestone, cwd = process.cwd()) {
210
+ const m = milestone || {};
211
+ if (!m.id || typeof m.id !== 'string') {
212
+ throw new NubosPilotError(
213
+ 'roadmap-invalid-milestone',
214
+ 'milestone.id required',
215
+ { id: m.id == null ? '' : m.id },
216
+ );
217
+ }
218
+ return _mutate(cwd, (doc) => {
219
+ if (doc.milestones.some((x) => x && x.id === m.id)) {
220
+ throw new NubosPilotError(
221
+ 'roadmap-duplicate-milestone',
222
+ 'milestone id already exists',
223
+ { id: m.id },
224
+ );
225
+ }
226
+ const entry = {
227
+ id: m.id,
228
+ name: m.name || '',
229
+ phases: Array.isArray(m.phases) ? m.phases.slice() : [],
230
+ };
231
+ doc.milestones.push(entry);
232
+ return { milestoneId: entry.id, name: entry.name };
233
+ });
234
+ }
235
+
236
+ function addPhase(milestoneId, phaseDef, cwd = process.cwd()) {
237
+ const def = _normalizePhaseDef(phaseDef);
238
+ return _mutate(cwd, (doc) => {
239
+ const ms = doc.milestones.find((x) => x && x.id === milestoneId);
240
+ if (!ms) {
241
+ throw new NubosPilotError(
242
+ 'roadmap-milestone-not-found',
243
+ 'milestone not found',
244
+ { id: milestoneId },
245
+ );
246
+ }
247
+ if (!Array.isArray(ms.phases)) ms.phases = [];
248
+ if (ms.phases.some((p) => p && p.slug === def.slug)) {
249
+ throw new NubosPilotError(
250
+ 'roadmap-duplicate-slug',
251
+ 'phase slug already used in this milestone',
252
+ { slug: def.slug, milestone: milestoneId },
253
+ );
254
+ }
255
+
256
+
257
+ let maxInt = 0;
258
+ for (const p of ms.phases) {
259
+ if (!p || p.number == null) continue;
260
+ const n = Number(p.number);
261
+ if (Number.isInteger(n) && n > maxInt) maxInt = n;
262
+ }
263
+ const next = maxInt + 1;
264
+ const phase = Object.assign({ number: next }, def);
265
+ ms.phases.push(phase);
266
+ return { milestoneId, number: next, slug: def.slug };
267
+ });
268
+ }
269
+
270
+ function insertPhaseAfter(baseNumber, phaseDef, cwd = process.cwd()) {
271
+ const base = Number(baseNumber);
272
+ if (!Number.isInteger(base)) {
273
+ throw new NubosPilotError(
274
+ 'roadmap-base-phase-not-found',
275
+ 'base phase number must be integer',
276
+ { number: baseNumber },
277
+ );
278
+ }
279
+ const def = _normalizePhaseDef(phaseDef);
280
+ return _mutate(cwd, (doc) => {
281
+
282
+ let target = null;
283
+ for (const ms of doc.milestones) {
284
+ if (!ms || !Array.isArray(ms.phases)) continue;
285
+ if (ms.phases.some((p) => p && Number(p.number) === base)) {
286
+ target = ms;
287
+ break;
288
+ }
289
+ }
290
+ if (!target) {
291
+ throw new NubosPilotError(
292
+ 'roadmap-base-phase-not-found',
293
+ 'base phase not found in any milestone',
294
+ { number: base },
295
+ );
296
+ }
297
+ if (target.phases.some((p) => p && p.slug === def.slug)) {
298
+ throw new NubosPilotError(
299
+ 'roadmap-duplicate-slug',
300
+ 'phase slug already used in this milestone',
301
+ { slug: def.slug, milestone: target.id },
302
+ );
303
+ }
304
+
305
+ let maxSuffix = 0;
306
+ for (const p of target.phases) {
307
+ if (!p || p.number == null) continue;
308
+ const s = String(p.number);
309
+ if (s.startsWith(base + '.')) {
310
+ const suf = Number(s.slice(String(base).length + 1));
311
+ if (Number.isInteger(suf) && suf > maxSuffix) maxSuffix = suf;
312
+ }
313
+ }
314
+
315
+
316
+ const newNumber = base + '.' + (maxSuffix + 1);
317
+ const phase = Object.assign({ number: newNumber }, def);
318
+
319
+
320
+ const baseIdx = target.phases.findIndex((p) => p && Number(p.number) === base);
321
+ target.phases.splice(baseIdx + 1, 0, phase);
322
+ return { milestoneId: target.id, number: newNumber, slug: def.slug };
323
+ });
324
+ }
325
+
326
+ function addBacklogEntry(description, opts) {
327
+ const cwd = (opts && opts.cwd) || process.cwd();
328
+ if (typeof description !== 'string' || !description.trim()) {
329
+ throw new NubosPilotError(
330
+ 'roadmap-invalid-description',
331
+ 'addBacklogEntry: description must be non-empty string',
332
+ { description },
333
+ );
334
+ }
335
+ if (description.length > 500) {
336
+ throw new NubosPilotError(
337
+ 'roadmap-description-too-long',
338
+ 'addBacklogEntry: description must be <= 500 chars',
339
+ { length: description.length },
340
+ );
341
+ }
342
+ if (/\n\n---\n/.test(description)) {
343
+ throw new NubosPilotError(
344
+ 'roadmap-invalid-description',
345
+ 'addBacklogEntry: description must not contain YAML separator pattern',
346
+ { description },
347
+ );
348
+ }
349
+ return _mutate(cwd, (doc) => {
350
+ let m = doc.milestones.find((x) => x && x.id === 'backlog');
351
+ if (!m) {
352
+ m = { id: 'backlog', name: 'Backlog', phases: [] };
353
+ doc.milestones.push(m);
354
+ }
355
+ if (!Array.isArray(m.phases)) m.phases = [];
356
+ const prefix = '999.';
357
+ let max = 0;
358
+ for (const ph of m.phases) {
359
+ if (!ph || ph.number == null) continue;
360
+ const n = String(ph.number);
361
+ if (n.startsWith(prefix)) {
362
+ const suf = Number(n.slice(prefix.length));
363
+ if (Number.isInteger(suf) && suf > max) max = suf;
364
+ }
365
+ }
366
+ const next = '999.' + (max + 1);
367
+ const { phaseSlug: _phaseSlug } = require('./phase.cjs');
368
+ const slug = _phaseSlug(description);
369
+ m.phases.push({
370
+ number: next,
371
+ name: description,
372
+ slug,
373
+ status: 'backlog',
374
+ requirements: [],
375
+ plans: [],
376
+ });
377
+ return { backlog_number: next, backlog_slug: slug };
378
+ });
379
+ }
380
+
381
+ function collapseMilestone(milestoneId, opts) {
382
+ const cwd = (opts && opts.cwd) || process.cwd();
383
+ if (typeof milestoneId !== 'string' || !/^[vV0-9._-]+$/.test(milestoneId)) {
384
+ throw new NubosPilotError(
385
+ 'roadmap-invalid-milestone-id',
386
+ 'collapseMilestone: id must match /^[vV0-9._-]+$/: ' + milestoneId,
387
+ { milestoneId },
388
+ );
389
+ }
390
+ return _mutate(cwd, (doc) => {
391
+ const m = doc.milestones.find((x) => x && x.id === milestoneId);
392
+ if (!m) {
393
+ throw new NubosPilotError(
394
+ 'roadmap-milestone-not-found',
395
+ 'collapseMilestone: milestone "' + milestoneId + '" not found',
396
+ { milestoneId },
397
+ );
398
+ }
399
+ const alreadyCollapsed = m.collapsed === true;
400
+ m.collapsed = true;
401
+ if (!m.collapsed_at) m.collapsed_at = new Date().toISOString().slice(0, 10);
402
+ return { milestoneId, already_collapsed: alreadyCollapsed };
403
+ });
404
+ }
405
+
406
+ module.exports = {
407
+ parseRoadmap,
408
+ getPhase,
409
+ listPhases,
410
+ phaseComplete,
411
+ addMilestone,
412
+ addPhase,
413
+ insertPhaseAfter,
414
+ addBacklogEntry,
415
+ collapseMilestone,
416
+ };