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,40 @@
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 stateCmd = require('./state.cjs');
8
+
9
+ const sandboxes = [];
10
+ function mkTmp() {
11
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-state-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('STATE-CMD-1: run returns full v2 frontmatter', () => {
24
+ const root = mkTmp();
25
+ fs.writeFileSync(path.join(root, '.nubos-pilot', 'STATE.md'),
26
+ '---\nschema_version: 2\nmilestone: v1.0\ncurrent_phase: 4\ncurrent_plan: 04-02\n' +
27
+ 'current_task: null\nlast_updated: 2026-04-15\n---\n\n# s\n');
28
+ const payload = stateCmd.run([], root);
29
+ assert.equal(payload.schema_version, 2);
30
+ assert.equal(payload.current_phase, 4);
31
+ assert.equal(payload.current_plan, '04-02');
32
+ });
33
+
34
+ test('STATE-CMD-2: run on fresh sandbox returns error envelope payload (no STATE.md)', () => {
35
+ const root = mkTmp();
36
+
37
+ const payload = stateCmd.run([], root);
38
+ assert.ok(payload && payload.error);
39
+ assert.match(payload.error.code, /state-not-found|ENOENT/);
40
+ });
@@ -0,0 +1,151 @@
1
+ const path = require('node:path');
2
+ const { execFileSync } = require('node:child_process');
3
+ const { NubosPilotError, findProjectRoot } = require('../../lib/core.cjs');
4
+ const { parseRoadmap } = require('../../lib/roadmap.cjs');
5
+ const { readState } = require('../../lib/state.cjs');
6
+ const { aggregatePhase } = require('../../lib/metrics-aggregate.cjs');
7
+
8
+ const SCHEMA_VERSION = 1;
9
+
10
+ function _usage() {
11
+ return 'Usage:\n np-tools.cjs stats json';
12
+ }
13
+
14
+ function _emitError(err, stderr) {
15
+ const code = err && err.name === 'NubosPilotError' ? err.code : 'stats-internal-error';
16
+ const message = (err && err.message) || String(err);
17
+ const details = (err && err.details) || null;
18
+ stderr.write(JSON.stringify({ code, message, details }) + '\n');
19
+ }
20
+
21
+ function _safeExec(args, cwd) {
22
+ try {
23
+ return execFileSync('git', args, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
24
+ } catch (_err) {
25
+ return '';
26
+ }
27
+ }
28
+
29
+ function _gitStats(cwd) {
30
+ const log = _safeExec(['log', '--oneline', '--all'], cwd);
31
+ const commits = log ? log.split(/\r?\n/).filter((l) => l.length > 0).length : 0;
32
+ const first = _safeExec(['log', '--reverse', '--format=%aI', '--all'], cwd);
33
+ const firstFirst = first ? first.split(/\r?\n/)[0] : '';
34
+ return { commits, first_commit_at: firstFirst || null };
35
+ }
36
+
37
+ function _milestoneEntry(doc) {
38
+ if (!doc || !Array.isArray(doc.milestones) || doc.milestones.length === 0) return null;
39
+ const active = doc.milestones.find((m) => m && m.status === 'active' && m.id !== 'backlog');
40
+ const nonBacklog = doc.milestones.filter((m) => m && m.id !== 'backlog');
41
+ const pick = active || nonBacklog[0] || doc.milestones[0];
42
+ if (!pick) return null;
43
+ return { version: pick.id || '', name: pick.name || '' };
44
+ }
45
+
46
+ function _collectPhases(doc) {
47
+ const out = [];
48
+ if (!doc || !Array.isArray(doc.milestones)) return out;
49
+ for (const ms of doc.milestones) {
50
+ if (!ms || !Array.isArray(ms.phases)) continue;
51
+ if (ms.id === 'backlog') continue;
52
+ for (const ph of ms.phases) {
53
+ if (!ph || ph.number == null) continue;
54
+ const plans = Array.isArray(ph.plans) ? ph.plans : [];
55
+ const completePlans = plans.filter((p) => p && p.complete === true).length;
56
+ const status = ph.status === 'done' || ph.status === 'complete'
57
+ ? 'complete'
58
+ : ph.status === 'in-progress' ? 'in-progress' : 'pending';
59
+ out.push({
60
+ number: String(ph.number),
61
+ name: ph.name || '',
62
+ plans_total: plans.length,
63
+ plans_complete: completePlans,
64
+ status,
65
+ requirements: Array.isArray(ph.requirements) ? ph.requirements.slice() : [],
66
+ });
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+
72
+ async function _buildStats(cwd) {
73
+ const useCwd = cwd || process.cwd();
74
+ const roadmap = parseRoadmap(useCwd);
75
+ const doc = roadmap && roadmap.doc ? roadmap.doc : null;
76
+ const milestone = _milestoneEntry(doc);
77
+ const phases = _collectPhases(doc);
78
+ let plansTotal = 0;
79
+ let plansComplete = 0;
80
+ for (const ph of phases) {
81
+ plansTotal += ph.plans_total;
82
+ plansComplete += ph.plans_complete;
83
+ }
84
+ const percent = plansTotal > 0 ? Math.round((plansComplete / plansTotal) * 100) : 0;
85
+ let lastActivity = null;
86
+ try {
87
+ const st = readState(useCwd);
88
+ if (st && st.frontmatter && st.frontmatter.last_activity) {
89
+ lastActivity = String(st.frontmatter.last_activity);
90
+ }
91
+ } catch (_err) {
92
+ lastActivity = null;
93
+ }
94
+ const git = _gitStats(useCwd);
95
+ const metrics_by_phase = {};
96
+ for (const ph of phases) {
97
+ try {
98
+ const agg = await aggregatePhase(ph.number, { cwd: useCwd });
99
+ metrics_by_phase[ph.number] = agg;
100
+ } catch (_err) {
101
+ metrics_by_phase[ph.number] = null;
102
+ }
103
+ }
104
+ return {
105
+ schema_version: SCHEMA_VERSION,
106
+ milestone,
107
+ phases,
108
+ plans_total: plansTotal,
109
+ plans_complete: plansComplete,
110
+ percent,
111
+ git,
112
+ last_activity: lastActivity,
113
+ metrics_by_phase,
114
+ };
115
+ }
116
+
117
+ async function run(argv, ctx) {
118
+ const context = ctx || {};
119
+ const cwd = context.cwd || process.cwd();
120
+ const stdout = context.stdout || process.stdout;
121
+ const stderr = context.stderr || process.stderr;
122
+ const args = Array.isArray(argv) ? argv.slice() : [];
123
+ const sub = args.shift();
124
+ if (sub !== 'json') {
125
+ stderr.write(_usage() + '\n');
126
+ return 1;
127
+ }
128
+ try {
129
+ findProjectRoot(cwd);
130
+ } catch (err) {
131
+ _emitError(err, stderr);
132
+ return 1;
133
+ }
134
+ try {
135
+ const out = await _buildStats(cwd);
136
+ stdout.write(JSON.stringify(out, null, 2) + '\n');
137
+ return 0;
138
+ } catch (err) {
139
+ _emitError(err, stderr);
140
+ return 1;
141
+ }
142
+ }
143
+
144
+ module.exports = { run, _buildStats, _collectPhases, _milestoneEntry };
145
+
146
+ if (require.main === module) {
147
+ run(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
148
+ process.stderr.write(String((err && err.stack) || err) + '\n');
149
+ process.exit(1);
150
+ });
151
+ }
@@ -0,0 +1,118 @@
1
+ const fs = require('node:fs');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+ const { execFileSync } = require('node:child_process');
5
+ const { test } = require('node:test');
6
+ const assert = require('node:assert/strict');
7
+ const { Writable } = require('node:stream');
8
+
9
+ const statsCli = require('./stats.cjs');
10
+
11
+ const _sandboxes = [];
12
+
13
+ function makeSink() {
14
+ const chunks = [];
15
+ const w = new Writable({
16
+ write(chunk, _enc, cb) { chunks.push(chunk); cb(); },
17
+ });
18
+ w.toString = () => Buffer.concat(chunks.map((c) => Buffer.isBuffer(c) ? c : Buffer.from(String(c)))).toString('utf-8');
19
+ return w;
20
+ }
21
+
22
+ function makeSandbox(yaml, stateMd) {
23
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-stats-'));
24
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
25
+ fs.writeFileSync(path.join(root, '.nubos-pilot', 'roadmap.yaml'), yaml);
26
+ fs.writeFileSync(path.join(root, '.nubos-pilot', 'STATE.md'), stateMd);
27
+ execFileSync('git', ['init', '-q'], { cwd: root });
28
+ execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: root });
29
+ execFileSync('git', ['config', 'user.name', 'Test'], { cwd: root });
30
+ execFileSync('git', ['config', 'commit.gpgsign', 'false'], { cwd: root });
31
+ execFileSync('git', ['add', '.'], { cwd: root });
32
+ execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: root });
33
+ _sandboxes.push(root);
34
+ return root;
35
+ }
36
+
37
+ test.afterEach(() => {
38
+ while (_sandboxes.length) {
39
+ try { fs.rmSync(_sandboxes.pop(), { recursive: true, force: true }); } catch { }
40
+ }
41
+ });
42
+
43
+ const DEMO_YAML = [
44
+ 'milestones:',
45
+ ' - id: v1.0',
46
+ ' name: v1',
47
+ ' phases:',
48
+ ' - number: 1',
49
+ ' name: Foundation',
50
+ ' slug: foundation',
51
+ ' status: done',
52
+ ' plans:',
53
+ ' - id: 01-01',
54
+ ' title: First',
55
+ ' complete: true',
56
+ ' - number: 2',
57
+ ' name: Next',
58
+ ' slug: next',
59
+ ' status: in-progress',
60
+ ' plans:',
61
+ ' - id: 02-01',
62
+ ' title: Second',
63
+ ' complete: false',
64
+ ].join('\n') + '\n';
65
+
66
+ const DEMO_STATE = [
67
+ '---',
68
+ 'schema_version: 2',
69
+ 'milestone: v1.0',
70
+ 'milestone_name: v1',
71
+ 'last_updated: "2026-04-17T10:00:00Z"',
72
+ 'progress:',
73
+ ' total_phases: 2',
74
+ ' completed_phases: 1',
75
+ ' total_plans: 2',
76
+ ' completed_plans: 1',
77
+ ' percent: 50',
78
+ '---',
79
+ '',
80
+ '# STATE',
81
+ ].join('\n') + '\n';
82
+
83
+ test('STATS-1: stats json emits schema_version + phases + git + metrics_by_phase', async () => {
84
+ const sb = makeSandbox(DEMO_YAML, DEMO_STATE);
85
+ const stdout = makeSink();
86
+ const stderr = makeSink();
87
+ const code = await statsCli.run(['json'], { cwd: sb, stdout, stderr });
88
+ assert.equal(code, 0, 'stderr=' + stderr.toString());
89
+ const parsed = JSON.parse(stdout.toString());
90
+ assert.equal(parsed.schema_version, 1);
91
+ assert.ok(parsed.milestone);
92
+ assert.equal(parsed.phases.length, 2);
93
+ assert.equal(parsed.plans_total, 2);
94
+ assert.equal(parsed.plans_complete, 1);
95
+ assert.equal(parsed.percent, 50);
96
+ assert.ok(parsed.git);
97
+ assert.ok(typeof parsed.git.commits === 'number');
98
+ assert.ok(parsed.metrics_by_phase);
99
+ });
100
+
101
+ test('STATS-2: unknown subcommand prints usage', async () => {
102
+ const sb = makeSandbox(DEMO_YAML, DEMO_STATE);
103
+ const stdout = makeSink();
104
+ const stderr = makeSink();
105
+ const code = await statsCli.run(['yolo'], { cwd: sb, stdout, stderr });
106
+ assert.equal(code, 1);
107
+ assert.match(stderr.toString(), /Usage:/);
108
+ });
109
+
110
+ test('STATS-3: outside project emits NubosPilotError envelope', async () => {
111
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'np-stats-outside-'));
112
+ _sandboxes.push(tmp);
113
+ const stdout = makeSink();
114
+ const stderr = makeSink();
115
+ const code = await statsCli.run(['json'], { cwd: tmp, stdout, stderr });
116
+ assert.equal(code, 1);
117
+ assert.match(stderr.toString(), /"code":\s*"not-in-project"/);
118
+ });
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { NubosPilotError, atomicWriteFileSync } = require('../../lib/core.cjs');
7
+ const { askUser: defaultAskUser } = require('../../lib/askuser.cjs');
8
+ const queueMod = require('./queue.cjs');
9
+
10
+ const STATE_DIR_NAME = '.nubos-pilot';
11
+ const TRIAGE_OPTIONS = ['promote-to-todo', 'promote-to-phase', 'keep', 'drop'];
12
+
13
+ function _stateDir(cwd) {
14
+ return path.join(path.resolve(cwd), STATE_DIR_NAME);
15
+ }
16
+
17
+ function _sanitizeId(id) {
18
+ if (typeof id !== 'string' || id.length === 0) return 'item';
19
+ const cleaned = id.replace(/[^a-z0-9\-_.]/gi, '_').slice(0, 100);
20
+ return cleaned || 'item';
21
+ }
22
+
23
+ function _getQueueItems(cwd) {
24
+ const chunks = [];
25
+ const captureStdout = { write: (s) => { chunks.push(String(s)); return true; } };
26
+ queueMod.run([], { cwd, stdout: captureStdout });
27
+ try {
28
+ const parsed = JSON.parse(chunks.join(''));
29
+ return Array.isArray(parsed.items) ? parsed.items : [];
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ function _applyPromoteToTodo(item, cwd, stderr) {
36
+ const stateDir = _stateDir(cwd);
37
+ const todosDir = path.join(stateDir, 'todos', 'pending');
38
+ try { fs.mkdirSync(todosDir, { recursive: true }); } catch {}
39
+ const safeName = _sanitizeId(item.id) + '.md';
40
+ const target = path.join(todosDir, safeName);
41
+ const body = `# ${item.title || item.id}\n\nSource: ${item.source}\n`;
42
+ try {
43
+ atomicWriteFileSync(target, body);
44
+ return { id: item.id, action: 'promote-to-todo', path: target };
45
+ } catch (err) {
46
+ try { stderr.write(`[triage] promote-to-todo failed for ${item.id}: ${err.message}\n`); } catch {}
47
+ return { id: item.id, action: 'promote-to-todo', error: err && err.message };
48
+ }
49
+ }
50
+
51
+ function _applyDrop(item, cwd, stderr) {
52
+ if (item.source !== 'todo') {
53
+ try { stderr.write(`[triage] drop: manual drop required for source=${item.source}, skipped ${item.id}\n`); } catch {}
54
+ return { id: item.id, action: 'drop', deferred: true };
55
+ }
56
+ const stateDir = _stateDir(cwd);
57
+ const todosDir = path.join(stateDir, 'todos', 'pending');
58
+ const explicit = item.path || path.join(todosDir, item.id);
59
+ const resolved = path.resolve(explicit);
60
+ const prefix = path.resolve(todosDir) + path.sep;
61
+ if (!(resolved === path.resolve(todosDir) || resolved.startsWith(prefix))) {
62
+ try { stderr.write(`[triage] drop refused: path outside todos/pending/ (${resolved})\n`); } catch {}
63
+ return { id: item.id, action: 'drop', refused: true };
64
+ }
65
+ try {
66
+ fs.unlinkSync(resolved);
67
+ return { id: item.id, action: 'drop', path: resolved };
68
+ } catch (err) {
69
+ try { stderr.write(`[triage] drop failed for ${item.id}: ${err.message}\n`); } catch {}
70
+ return { id: item.id, action: 'drop', error: err && err.message };
71
+ }
72
+ }
73
+
74
+ function _applyTriage(item, value, cwd, stderr) {
75
+ if (value === 'promote-to-todo') {
76
+ if (item.source === 'todo') {
77
+ try { stderr.write(`[triage] promote-to-todo is a no-op for existing todo ${item.id}\n`); } catch {}
78
+ return { id: item.id, action: 'keep', note: 'already-todo' };
79
+ }
80
+ return _applyPromoteToTodo(item, cwd, stderr);
81
+ }
82
+ if (value === 'promote-to-phase') {
83
+ try { stderr.write(`[triage] promote-to-phase requires manual step (Phase 10 UTIL-05) for ${item.id}\n`); } catch {}
84
+ return { id: item.id, action: 'promote-to-phase', deferred: true };
85
+ }
86
+ if (value === 'drop') {
87
+ return _applyDrop(item, cwd, stderr);
88
+ }
89
+
90
+ return { id: item.id, action: 'keep' };
91
+ }
92
+
93
+ async function run(args, ctx) {
94
+ const context = ctx || {};
95
+ const cwd = context.cwd || process.cwd();
96
+ const stdout = context.stdout || process.stdout;
97
+ const stderr = context.stderr || process.stderr;
98
+ const askUser = typeof context.askUser === 'function' ? context.askUser : defaultAskUser;
99
+
100
+ const items = _getQueueItems(cwd);
101
+
102
+ if (items.length === 0) {
103
+ try { stderr.write('[triage] Queue leer — nichts zu triagen.\n'); } catch {}
104
+ const payload = { ok: true, decisions: [] };
105
+ stdout.write(JSON.stringify(payload));
106
+ return payload;
107
+ }
108
+
109
+ const decisions = [];
110
+ for (const item of items) {
111
+ const answer = await askUser({
112
+ type: 'select',
113
+ question: `Triage: ${item.title || item.id}`,
114
+ options: TRIAGE_OPTIONS.slice(),
115
+ default: 'keep',
116
+ });
117
+ const value = answer && typeof answer.value === 'string' ? answer.value : 'keep';
118
+ decisions.push(_applyTriage(item, value, cwd, stderr));
119
+ }
120
+
121
+ const payload = { ok: true, decisions };
122
+ try { stdout.write(JSON.stringify(payload)); } catch (err) {
123
+ throw new NubosPilotError('triage-emit-failed', err && err.message, {});
124
+ }
125
+ return payload;
126
+ }
127
+
128
+ module.exports = { run };
@@ -0,0 +1,108 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const { NubosPilotError } = require('../../lib/core.cjs');
5
+ const { getPhase } = require('../../lib/roadmap.cjs');
6
+ const { paddedPhase, phaseSlug, findPhaseDir } = require('../../lib/phase.cjs');
7
+ const { detect } = require('../../lib/runtime/index.cjs');
8
+
9
+ function _validatePhaseArg(raw) {
10
+ if (raw == null || raw === '') {
11
+ throw new NubosPilotError(
12
+ 'ui-phase-invalid-arg',
13
+ 'ui-phase requires a phase number argument',
14
+ { value: raw == null ? '' : String(raw) },
15
+ );
16
+ }
17
+ const s = String(raw);
18
+ if (!/^\d+(\.\d+)?$/.test(s)) {
19
+ throw new NubosPilotError(
20
+ 'ui-phase-invalid-arg',
21
+ 'Invalid phase number: ' + s,
22
+ { value: s },
23
+ );
24
+ }
25
+ return s;
26
+ }
27
+
28
+ function _resolvePhaseDir(phaseArg, cwd, slug) {
29
+ const hit = findPhaseDir(phaseArg, cwd);
30
+ if (hit) return hit;
31
+ const padded = paddedPhase(phaseArg);
32
+ return path.join(path.resolve(cwd), '.nubos-pilot', 'phases', padded + '-' + slug);
33
+ }
34
+
35
+ function _buildPayload(phaseArg, cwd) {
36
+ let phase;
37
+ try {
38
+ phase = getPhase(phaseArg, cwd);
39
+ } catch (err) {
40
+ if (err && err.code === 'phase-not-found') {
41
+ throw new NubosPilotError(
42
+ 'ui-phase-not-found',
43
+ 'Phase ' + phaseArg + ' not found in roadmap',
44
+ { number: phaseArg },
45
+ );
46
+ }
47
+ throw err;
48
+ }
49
+
50
+ const padded = paddedPhase(phaseArg);
51
+ const slug = phase.slug || phaseSlug(phase.name);
52
+ const phase_dir = _resolvePhaseDir(phaseArg, cwd, slug);
53
+ const ui_spec_path = path.join(phase_dir, padded + '-UI-SPEC.md');
54
+ const has_ui_spec = fs.existsSync(ui_spec_path);
55
+ const template_path = path.join(path.resolve(cwd), 'templates', 'UI-SPEC.md');
56
+ const { runtime } = detect({ cwd });
57
+
58
+ return {
59
+ _workflow: 'ui-phase',
60
+ phase: phaseArg,
61
+ padded,
62
+ phase_dir,
63
+ ui_spec_path,
64
+ has_ui_spec,
65
+ template_path,
66
+ agents: {
67
+ ui_researcher: 'np-ui-researcher',
68
+ ui_checker: 'np-ui-checker',
69
+ },
70
+ max_iterations: 2,
71
+ runtime,
72
+ };
73
+ }
74
+
75
+ function _emitError(err, stderr) {
76
+ if (err && err.name === 'NubosPilotError') {
77
+ stderr.write(
78
+ JSON.stringify({ code: err.code, message: err.message, details: err.details }) + '\n',
79
+ );
80
+ } else {
81
+ stderr.write(String((err && err.stack) || err) + '\n');
82
+ }
83
+ }
84
+
85
+ function run(args, ctx) {
86
+ const context = ctx || {};
87
+ const cwd = context.cwd || process.cwd();
88
+ const stdout = context.stdout || process.stdout;
89
+ const stderr = context.stderr || process.stderr;
90
+ const list = Array.isArray(args) ? args : [];
91
+
92
+ if (list[0] == null || list[0] === '') {
93
+ stderr.write('Usage: np-tools.cjs init ui-phase <phase>\n');
94
+ return 1;
95
+ }
96
+
97
+ try {
98
+ const phaseArg = _validatePhaseArg(list[0]);
99
+ const payload = _buildPayload(phaseArg, cwd);
100
+ stdout.write(JSON.stringify(payload, null, 2));
101
+ return 0;
102
+ } catch (err) {
103
+ _emitError(err, stderr);
104
+ return 1;
105
+ }
106
+ }
107
+
108
+ module.exports = { run, _buildPayload };
@@ -0,0 +1,121 @@
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 } =
7
+ require('../../tests/helpers/fixture.cjs');
8
+ const subcmd = require('./ui-phase.cjs');
9
+
10
+ function _baseRoadmap() {
11
+ return {
12
+ schema_version: 1,
13
+ milestones: [
14
+ {
15
+ id: 'v1.0',
16
+ name: 'first',
17
+ phases: [
18
+ {
19
+ number: 9,
20
+ name: 'Feature Set',
21
+ slug: 'feature-set',
22
+ goal: 'Ship advanced workflows',
23
+ depends_on: [],
24
+ requirements: ['R-02'],
25
+ success_criteria: ['UI-SPEC produced'],
26
+ status: 'pending',
27
+ plans: [],
28
+ },
29
+ ],
30
+ },
31
+ ],
32
+ };
33
+ }
34
+
35
+ function _capture() {
36
+ let buf = '';
37
+ return { stub: { write: (s) => { buf += s; return true; } }, get: () => buf };
38
+ }
39
+
40
+ afterEach(cleanupAll);
41
+
42
+ test('UIP-1: run(["init", "9"]) returns expected payload shape', () => {
43
+ const sandbox = makeSandbox();
44
+ seedRoadmapYaml(sandbox, _baseRoadmap());
45
+ const dir = seedPhaseDir(sandbox, 9, 'feature-set', {});
46
+ const cap = _capture();
47
+ subcmd.run(['9'], { cwd: sandbox, stdout: cap.stub });
48
+ const payload = JSON.parse(cap.get().trim());
49
+ assert.equal(payload.phase, '9');
50
+ assert.equal(payload.padded, '09');
51
+ assert.equal(payload.phase_dir, dir);
52
+ assert.equal(payload.ui_spec_path, path.join(dir, '09-UI-SPEC.md'));
53
+ assert.equal(payload.has_ui_spec, false);
54
+ assert.match(payload.template_path, /templates\/UI-SPEC\.md$/);
55
+ assert.equal(payload.max_iterations, 2);
56
+ });
57
+
58
+ test('UIP-2: agents declares ui_researcher + ui_checker', () => {
59
+ const sandbox = makeSandbox();
60
+ seedRoadmapYaml(sandbox, _baseRoadmap());
61
+ seedPhaseDir(sandbox, 9, 'feature-set', {});
62
+ const cap = _capture();
63
+ subcmd.run(['9'], { cwd: sandbox, stdout: cap.stub });
64
+ const payload = JSON.parse(cap.get().trim());
65
+ assert.deepEqual(payload.agents, {
66
+ ui_researcher: 'np-ui-researcher',
67
+ ui_checker: 'np-ui-checker',
68
+ });
69
+ });
70
+
71
+ test('UIP-3: has_ui_spec=true when {padded}-UI-SPEC.md exists', () => {
72
+ const sandbox = makeSandbox();
73
+ seedRoadmapYaml(sandbox, _baseRoadmap());
74
+ seedPhaseDir(sandbox, 9, 'feature-set', { '09-UI-SPEC.md': '# UI\n' });
75
+ const cap = _capture();
76
+ subcmd.run(['9'], { cwd: sandbox, stdout: cap.stub });
77
+ const payload = JSON.parse(cap.get().trim());
78
+ assert.equal(payload.has_ui_spec, true);
79
+ });
80
+
81
+ test('UIP-4: runtime populated via detect() and config override', () => {
82
+ const sandbox = makeSandbox();
83
+ seedRoadmapYaml(sandbox, _baseRoadmap());
84
+ seedPhaseDir(sandbox, 9, 'feature-set', {});
85
+ fs.writeFileSync(
86
+ path.join(sandbox, '.nubos-pilot', 'config.json'),
87
+ JSON.stringify({ runtime: 'opencode' }),
88
+ 'utf-8',
89
+ );
90
+ const cap = _capture();
91
+ subcmd.run(['9'], { cwd: sandbox, stdout: cap.stub });
92
+ const payload = JSON.parse(cap.get().trim());
93
+ assert.equal(payload.runtime, 'opencode');
94
+ });
95
+
96
+ test('UIP-5: missing phase arg returns usage error', () => {
97
+ const sandbox = makeSandbox();
98
+ seedRoadmapYaml(sandbox, _baseRoadmap());
99
+ const cap = _capture();
100
+ let errBuf = '';
101
+ const code = subcmd.run([], {
102
+ cwd: sandbox, stdout: cap.stub,
103
+ stderr: { write: (s) => { errBuf += s; return true; } },
104
+ });
105
+ assert.equal(code, 1);
106
+ assert.match(errBuf, /Usage/);
107
+ });
108
+
109
+ test('UIP-6: unknown phase produces ui-phase-not-found error', () => {
110
+ const sandbox = makeSandbox();
111
+ seedRoadmapYaml(sandbox, _baseRoadmap());
112
+ const cap = _capture();
113
+ let errBuf = '';
114
+ const code = subcmd.run(['99'], {
115
+ cwd: sandbox, stdout: cap.stub,
116
+ stderr: { write: (s) => { errBuf += s; return true; } },
117
+ });
118
+ assert.equal(code, 1);
119
+ const parsed = JSON.parse(errBuf.trim());
120
+ assert.equal(parsed.code, 'ui-phase-not-found');
121
+ });