nubos-pilot 0.5.3 → 0.5.5

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 (40) hide show
  1. package/agents/np-plan-checker.md +1 -0
  2. package/agents/np-planner.md +11 -2
  3. package/bin/install.js +6 -0
  4. package/bin/np-tools/_commands.cjs +2 -0
  5. package/bin/np-tools/add-tests.cjs +4 -0
  6. package/bin/np-tools/add-todo.cjs +4 -0
  7. package/bin/np-tools/detect-runtime.cjs +24 -0
  8. package/bin/np-tools/detect-runtime.test.cjs +47 -0
  9. package/bin/np-tools/discuss-phase.cjs +5 -0
  10. package/bin/np-tools/discuss-phase.test.cjs +75 -18
  11. package/bin/np-tools/discuss-project.cjs +5 -0
  12. package/bin/np-tools/execute-milestone.cjs +5 -0
  13. package/bin/np-tools/new-milestone.cjs +6 -2
  14. package/bin/np-tools/new-project.cjs +6 -2
  15. package/bin/np-tools/plan-milestone.cjs +5 -0
  16. package/bin/np-tools/research-phase.cjs +5 -0
  17. package/bin/np-tools/resume-work.cjs +5 -0
  18. package/bin/np-tools/stats.cjs +77 -5
  19. package/bin/np-tools/stats.test.cjs +83 -1
  20. package/bin/np-tools/text-mode.cjs +56 -0
  21. package/bin/np-tools/text-mode.test.cjs +132 -0
  22. package/bin/np-tools/verify-work.cjs +5 -0
  23. package/lib/runtime/claude.cjs +10 -2
  24. package/lib/runtime/claude.test.cjs +28 -0
  25. package/lib/text-mode.cjs +79 -0
  26. package/lib/text-mode.test.cjs +139 -0
  27. package/np-tools.cjs +2 -0
  28. package/package.json +1 -1
  29. package/workflows/add-todo.md +10 -5
  30. package/workflows/discuss-phase.md +37 -17
  31. package/workflows/execute-phase.md +8 -2
  32. package/workflows/new-milestone.md +6 -0
  33. package/workflows/new-project.md +6 -0
  34. package/workflows/note.md +11 -0
  35. package/workflows/plan-phase.md +7 -2
  36. package/workflows/research-phase.md +6 -1
  37. package/workflows/resume-work.md +5 -0
  38. package/workflows/session-report.md +11 -0
  39. package/workflows/validate-phase.md +7 -2
  40. package/workflows/verify-work.md +7 -1
@@ -89,6 +89,7 @@ Run each dimension below; for every failure, emit one finding using the matching
89
89
  - Every `<task>` MUST have `id="M<NNN>-S<NNN>-T<NNNN>"` matching the enclosing slice (milestone and slice numbers must agree with the file path). Mismatch → `broken-dependency`.
90
90
  - Missing `depends_on`, `wave`, or `tier` attribute on the opening `<task>` tag → the scaffolder will drop it. Emit `fake-promotion-trigger` with a message telling the planner which task is missing which attribute.
91
91
  - `wave="<N>"` should equal the slice's S-number (e.g. S002 → wave="2"). Mismatch is a soft finding (`fake-promotion-trigger`).
92
+ - **Task numbering restarts per slice.** Inside each `S<NNN>-PLAN.md`, the task IDs MUST start at `T0001` and increment contiguously (`T0001, T0002, …`). Counter that continues across slices (e.g. `S002` starting at `T0002` because `S001` used `T0001`) → `broken-dependency` with `target: S<NNN>-PLAN.md task <n>` and a message naming the expected vs. observed T-number. Gaps (`T0001, T0003`) are the same finding.
92
93
 
93
94
  ### Dimension 7: Nyquist Coverage Annotation
94
95
 
@@ -216,7 +216,7 @@ If any check fails, fix before returning. Plan-checker will catch what you miss,
216
216
 
217
217
  Inside each `S<NNN>-PLAN.md`, every `<task>` tag MUST have these four attributes on the opening tag:
218
218
 
219
- - `id="M<NNN>-S<NNN>-T<NNNN>"` — full-id, e.g. `id="M001-S001-T0001"`. Milestone 3 digits, slice 3 digits, task **4 digits**.
219
+ - `id="M<NNN>-S<NNN>-T<NNNN>"` — full-id, e.g. `id="M001-S001-T0001"`. Milestone 3 digits, slice 3 digits, task **4 digits**. **Task numbering restarts at `T0001` inside every slice.** The first task of `S002` is `M<NNN>-S002-T0001`, the first task of `S003` is `M<NNN>-S003-T0001`. Tasks within a slice run `T0001, T0002, T0003, …` without gaps. Never continue the counter across slices (`S001-T0001, S002-T0002` is wrong — it must be `S001-T0001, S002-T0001`).
220
220
  - `depends_on="<id>[,<id>...]"` — comma-separated predecessor task full-ids, or empty string `""`. Must only reference tasks in **earlier slices** (cross-slice forward deps) or be empty (intra-slice tasks are implicitly parallel, never serial).
221
221
  - `wave="<N>"` — integer equal to the slice number. For S001 use `wave="1"`, for S002 use `wave="2"`, etc.
222
222
  - `tier="<haiku|sonnet|opus>"` — executor tier, picks the model via resolve-model.
@@ -257,7 +257,16 @@ Create `LoginForm.tsx` with email + password inputs. Wire it to the
257
257
  </tasks>
258
258
  ```
259
259
 
260
- Note both tasks have `depends_on=""` — they're in the same slice and run in parallel. If `T0002` truly needs `T0001` first, move `T0002` into a new slice `S002` and write `depends_on="M001-S001-T0001" wave="2"`.
260
+ Note both tasks have `depends_on=""` — they're in the same slice and run in parallel. If `T0002` truly needs `T0001` first, move `T0002` into a new slice `S002` and renumber it to `T0001` — each slice owns its own task counter:
261
+
262
+ ```
263
+ <task id="M001-S002-T0001" depends_on="M001-S001-T0001" wave="2" tier="sonnet">
264
+ <name>Use login handler in session flow</name>
265
+ ...
266
+ </task>
267
+ ```
268
+
269
+ The cross-slice dep `M001-S001-T0001` flows forward (S001 → S002); the new task is `T0001` of S002, not `T0003`.
261
270
  </task_format>
262
271
 
263
272
  <tooling_conventions>
package/bin/install.js CHANGED
@@ -663,6 +663,12 @@ function _runUninstallLocked(projectRoot) {
663
663
 
664
664
  async function main() {
665
665
  const rawArgs = process.argv.slice(2);
666
+ if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
667
+ let version = '0.0.0';
668
+ try { version = String(require('../package.json').version || '0.0.0'); } catch {}
669
+ process.stdout.write(version + '\n');
670
+ return;
671
+ }
666
672
  const { flags, rest } = parseInstallFlags(rawArgs);
667
673
  const sub = rest[0];
668
674
  const cwd = process.cwd();
@@ -44,8 +44,10 @@ const COMMANDS = [
44
44
  { name: 'commit', category: 'Utility', description: 'Atomic git commit wrapper with gitignore-guard' },
45
45
  { name: 'config-get', category: 'Utility', description: 'Read value from .nubos-pilot/config.json by dotted key path' },
46
46
  { name: 'lang-directive', category: 'Utility', description: 'Print workflow language directive from config.response_language (SSOT)' },
47
+ { name: 'text-mode', category: 'Utility', description: 'Print whether text mode is active (config.workflow.text_mode ∨ CLAUDECODE)' },
47
48
  { name: 'generate-slug', category: 'Utility', description: 'Slugify text via lib/layout.cjs.slugify' },
48
49
  { name: 'stats', category: 'Utility', description: 'Aggregated project stats (roadmap + STATE + git + metrics JSON shape)' },
50
+ { name: 'detect-runtime', category: 'Utility', description: 'Print detected runtime id (claude, codex, gemini, …) — reads config.json ∨ env ∨ default' },
49
51
 
50
52
  { name: 'thread', category: 'Utility', description: 'Cross-session thread CRUD (create/resume under .nubos-pilot/threads/)' },
51
53
  { name: 'session-report', category: 'Utility', description: 'Generate session report from metrics since .last-session pointer' },
@@ -5,6 +5,7 @@ const { NubosPilotError, atomicWriteFileSync, withFileLock } = require('../../li
5
5
  const { getPhase } = require('../../lib/roadmap.cjs');
6
6
  const layout = require('../../lib/layout.cjs');
7
7
  const { parseVerificationMd, milestoneVerificationPath } = require('../../lib/verify.cjs');
8
+ const textMode = require('../../lib/text-mode.cjs');
8
9
 
9
10
  const BEGIN_MARKER = '// >>> np:add-tests begin';
10
11
  const END_MARKER = '// <<< np:add-tests end';
@@ -138,6 +139,7 @@ function run(args, ctx) {
138
139
  const mNum = _validateMilestoneArg(list[1]);
139
140
  const target = _resolveTestTarget(mNum, cwd);
140
141
  const { passes, skips, verification_path } = _loadCases(mNum, cwd);
142
+ const tmDetail = textMode.resolveTextModeDetail(cwd);
141
143
  const payload = {
142
144
  _workflow: 'add-tests',
143
145
  milestone: mNum,
@@ -146,6 +148,8 @@ function run(args, ctx) {
146
148
  verification_path,
147
149
  pass_cases: passes,
148
150
  skip_cases: skips,
151
+ text_mode: tmDetail.enabled,
152
+ text_mode_source: tmDetail.source,
149
153
  };
150
154
  stdout.write(JSON.stringify(payload, null, 2));
151
155
  return payload;
@@ -3,6 +3,7 @@ const path = require('node:path');
3
3
 
4
4
  const { projectStateDir, NubosPilotError } = require('../../lib/core.cjs');
5
5
  const { slugify } = require('../../lib/layout.cjs');
6
+ const textMode = require('../../lib/text-mode.cjs');
6
7
 
7
8
  const MAX_DESCRIPTION_LENGTH = 500;
8
9
 
@@ -54,6 +55,7 @@ function _buildPayload(description, cwd) {
54
55
  }
55
56
  const todos_dir_exists = fs.existsSync(todosDir);
56
57
  const state_path = path.join(stateDir, 'STATE.md');
58
+ const tmDetail = textMode.resolveTextModeDetail(cwd);
57
59
  return {
58
60
  _workflow: 'add-todo',
59
61
  commit_docs: true,
@@ -66,6 +68,8 @@ function _buildPayload(description, cwd) {
66
68
  todos_dir_exists,
67
69
  todo_count,
68
70
  state_path,
71
+ text_mode: tmDetail.enabled,
72
+ text_mode_source: tmDetail.source,
69
73
  todos: [],
70
74
  };
71
75
  }
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { detect } = require('../../lib/runtime/index.cjs');
4
+
5
+ function run(argv, ctx) {
6
+ const context = ctx || {};
7
+ const cwd = context.cwd || process.cwd();
8
+ const stdout = context.stdout || process.stdout;
9
+ const args = Array.isArray(argv) ? argv.slice() : [];
10
+ const result = detect({ cwd });
11
+ if (args.includes('--json')) {
12
+ stdout.write(JSON.stringify(result) + '\n');
13
+ } else {
14
+ stdout.write(result.runtime + '\n');
15
+ }
16
+ return 0;
17
+ }
18
+
19
+ module.exports = { run };
20
+
21
+ if (require.main === module) {
22
+ const code = run(process.argv.slice(2));
23
+ if (typeof code === 'number' && code !== 0) process.exit(code);
24
+ }
@@ -0,0 +1,47 @@
1
+ const fs = require('node:fs');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+ const { test } = require('node:test');
5
+ const assert = require('node:assert/strict');
6
+ const { Writable } = require('node:stream');
7
+
8
+ const cli = require('./detect-runtime.cjs');
9
+
10
+ function makeSink() {
11
+ const chunks = [];
12
+ const w = new Writable({
13
+ write(chunk, _enc, cb) { chunks.push(chunk); cb(); },
14
+ });
15
+ w.toString = () => Buffer.concat(chunks.map((c) => Buffer.isBuffer(c) ? c : Buffer.from(String(c)))).toString('utf-8');
16
+ return w;
17
+ }
18
+
19
+ function makeSandbox(runtime) {
20
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-detect-rt-'));
21
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
22
+ if (runtime) {
23
+ fs.writeFileSync(
24
+ path.join(root, '.nubos-pilot', 'config.json'),
25
+ JSON.stringify({ runtime, runtime_source: 'config' }),
26
+ );
27
+ }
28
+ return root;
29
+ }
30
+
31
+ test('detect-runtime: reads runtime from .nubos-pilot/config.json', () => {
32
+ const sb = makeSandbox('gemini');
33
+ const stdout = makeSink();
34
+ const code = cli.run([], { cwd: sb, stdout });
35
+ assert.equal(code, 0);
36
+ assert.equal(stdout.toString().trim(), 'gemini');
37
+ });
38
+
39
+ test('detect-runtime --json emits {runtime, source}', () => {
40
+ const sb = makeSandbox('codex');
41
+ const stdout = makeSink();
42
+ const code = cli.run(['--json'], { cwd: sb, stdout });
43
+ assert.equal(code, 0);
44
+ const parsed = JSON.parse(stdout.toString());
45
+ assert.equal(parsed.runtime, 'codex');
46
+ assert.ok(parsed.source);
47
+ });
@@ -8,6 +8,7 @@ const crypto = require('node:crypto');
8
8
  const { NubosPilotError, projectStateDir } = require('../../lib/core.cjs');
9
9
  const { getPhase } = require('../../lib/roadmap.cjs');
10
10
  const layout = require('../../lib/layout.cjs');
11
+ const textMode = require('../../lib/text-mode.cjs');
11
12
 
12
13
  const INLINE_THRESHOLD_BYTES = 16 * 1024;
13
14
 
@@ -96,6 +97,8 @@ function run(args, ctx) {
96
97
  const has_context = fs.existsSync(contextPath);
97
98
  const has_milestone_dir = fs.existsSync(milestoneDir);
98
99
 
100
+ const tmDetail = textMode.resolveTextModeDetail(cwd);
101
+
99
102
  const payload = {
100
103
  _workflow: 'discuss-phase',
101
104
  milestone: mNum,
@@ -109,6 +112,8 @@ function run(args, ctx) {
109
112
  requirements: Array.isArray(def.requirements) ? def.requirements : [],
110
113
  success_criteria: Array.isArray(def.success_criteria) ? def.success_criteria : [],
111
114
  mode: flags.assumptions ? 'assumptions' : 'adaptive',
115
+ text_mode: tmDetail.enabled,
116
+ text_mode_source: tmDetail.source,
112
117
  agent_skills: _agentSkills(),
113
118
  };
114
119
 
@@ -7,6 +7,20 @@ const { makeSandbox, seedRoadmapYaml, cleanupAll } =
7
7
  require('../../tests/helpers/fixture.cjs');
8
8
  const subcmd = require('./discuss-phase.cjs');
9
9
 
10
+ function _clearClaudeEnv() {
11
+ const saved = {};
12
+ for (const k of ['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT']) {
13
+ saved[k] = process.env[k];
14
+ delete process.env[k];
15
+ }
16
+ return () => {
17
+ for (const [k, v] of Object.entries(saved)) {
18
+ if (v === undefined) delete process.env[k];
19
+ else process.env[k] = v;
20
+ }
21
+ };
22
+ }
23
+
10
24
  function _baseRoadmap() {
11
25
  return {
12
26
  schema_version: 1,
@@ -42,24 +56,67 @@ function _captureStdout() {
42
56
  afterEach(cleanupAll);
43
57
 
44
58
  test('DP-1: run(["3"]) on valid milestone returns JSON payload with expected shape', () => {
45
- const sandbox = makeSandbox();
46
- seedRoadmapYaml(sandbox, _baseRoadmap());
47
- const cap = _captureStdout();
48
- subcmd.run(['3'], { cwd: sandbox, stdout: cap.stub });
49
- const raw = cap.get().trim();
50
- assert.ok(!raw.startsWith('@file:'));
51
- const payload = JSON.parse(raw);
52
- assert.equal(payload.milestone, 3);
53
- assert.equal(payload.milestone_id, 'M003');
54
- assert.ok(payload.milestone_dir.endsWith(path.join('.nubos-pilot', 'milestones', 'M003')));
55
- assert.ok(payload.milestone_context_path.endsWith(path.join('M003', 'M003-CONTEXT.md')));
56
- assert.equal(payload.milestone_name, 'Observability');
57
- assert.equal(payload.has_context, false);
58
- assert.equal(payload.has_milestone_dir, false);
59
- assert.equal(payload.goal, 'Ship structured logging + metrics');
60
- assert.deepEqual(payload.requirements, ['OBS-01']);
61
- assert.ok('agent_skills' in payload);
62
- assert.equal(payload.mode, 'adaptive');
59
+ const restore = _clearClaudeEnv();
60
+ try {
61
+ const sandbox = makeSandbox();
62
+ seedRoadmapYaml(sandbox, _baseRoadmap());
63
+ const cap = _captureStdout();
64
+ subcmd.run(['3'], { cwd: sandbox, stdout: cap.stub });
65
+ const raw = cap.get().trim();
66
+ assert.ok(!raw.startsWith('@file:'));
67
+ const payload = JSON.parse(raw);
68
+ assert.equal(payload.milestone, 3);
69
+ assert.equal(payload.milestone_id, 'M003');
70
+ assert.ok(payload.milestone_dir.endsWith(path.join('.nubos-pilot', 'milestones', 'M003')));
71
+ assert.ok(payload.milestone_context_path.endsWith(path.join('M003', 'M003-CONTEXT.md')));
72
+ assert.equal(payload.milestone_name, 'Observability');
73
+ assert.equal(payload.has_context, false);
74
+ assert.equal(payload.has_milestone_dir, false);
75
+ assert.equal(payload.goal, 'Ship structured logging + metrics');
76
+ assert.deepEqual(payload.requirements, ['OBS-01']);
77
+ assert.ok('agent_skills' in payload);
78
+ assert.equal(payload.mode, 'adaptive');
79
+ assert.equal(payload.text_mode, false);
80
+ assert.equal(payload.text_mode_source, 'default');
81
+ } finally {
82
+ restore();
83
+ }
84
+ });
85
+
86
+ test('DP-1b: CLAUDECODE=1 sets text_mode=true with runtime source', () => {
87
+ const restore = _clearClaudeEnv();
88
+ try {
89
+ process.env.CLAUDECODE = '1';
90
+ const sandbox = makeSandbox();
91
+ seedRoadmapYaml(sandbox, _baseRoadmap());
92
+ const cap = _captureStdout();
93
+ subcmd.run(['3'], { cwd: sandbox, stdout: cap.stub });
94
+ const payload = JSON.parse(cap.get().trim());
95
+ assert.equal(payload.text_mode, true);
96
+ assert.equal(payload.text_mode_source, 'runtime');
97
+ } finally {
98
+ restore();
99
+ }
100
+ });
101
+
102
+ test('DP-1c: config workflow.text_mode=false wins over CLAUDECODE', () => {
103
+ const restore = _clearClaudeEnv();
104
+ try {
105
+ process.env.CLAUDECODE = '1';
106
+ const sandbox = makeSandbox();
107
+ seedRoadmapYaml(sandbox, _baseRoadmap());
108
+ fs.writeFileSync(
109
+ path.join(sandbox, '.nubos-pilot', 'config.json'),
110
+ JSON.stringify({ workflow: { text_mode: false } }),
111
+ );
112
+ const cap = _captureStdout();
113
+ subcmd.run(['3'], { cwd: sandbox, stdout: cap.stub });
114
+ const payload = JSON.parse(cap.get().trim());
115
+ assert.equal(payload.text_mode, false);
116
+ assert.equal(payload.text_mode_source, 'config');
117
+ } finally {
118
+ restore();
119
+ }
63
120
  });
64
121
 
65
122
  test('DP-2: run(["nonexistent"]) throws discuss-invalid-phase-arg', () => {
@@ -4,6 +4,7 @@ const path = require('node:path');
4
4
  const { NubosPilotError, atomicWriteFileSync } = require('../../lib/core.cjs');
5
5
  const { scan } = require('../../lib/workspace-scan.cjs');
6
6
  const { workspaceGitInfo } = require('../../lib/git.cjs');
7
+ const textMode = require('../../lib/text-mode.cjs');
7
8
 
8
9
  const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates');
9
10
  const PLACEHOLDER_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
@@ -98,6 +99,8 @@ function _emitPlan(projectRoot, flags, stdout) {
98
99
 
99
100
  const scanContext = _scanContextFor(projectRoot);
100
101
 
102
+ const tmDetail = textMode.resolveTextModeDetail(projectRoot);
103
+
101
104
  stdout.write(JSON.stringify({
102
105
  mode: 'plan',
103
106
  sub_mode: mode,
@@ -107,6 +110,8 @@ function _emitPlan(projectRoot, flags, stdout) {
107
110
  questions: _grayAreas(),
108
111
  required_fields: REQUIRED_FIELDS,
109
112
  requirements_md_path: path.join(projectRoot, '.nubos-pilot', 'REQUIREMENTS.md'),
113
+ text_mode: tmDetail.enabled,
114
+ text_mode_source: tmDetail.source,
110
115
  }, null, 2));
111
116
  }
112
117
 
@@ -13,6 +13,7 @@ const layout = require('../../lib/layout.cjs');
13
13
  const { getPhase } = require('../../lib/roadmap.cjs');
14
14
  const { extractFrontmatter } = require('../../lib/frontmatter.cjs');
15
15
  const { getAgentSkills } = require('../../lib/agents.cjs');
16
+ const textMode = require('../../lib/text-mode.cjs');
16
17
 
17
18
  const INLINE_THRESHOLD_BYTES = 16 * 1024;
18
19
 
@@ -119,6 +120,8 @@ function _initPayload(mNum, cwd) {
119
120
  tasks,
120
121
  });
121
122
  }
123
+ const tmDetail = textMode.resolveTextModeDetail(cwd);
124
+
122
125
  return {
123
126
  _workflow: 'execute-milestone',
124
127
  milestone: mNum,
@@ -132,6 +135,8 @@ function _initPayload(mNum, cwd) {
132
135
  total_tasks: totalTasks,
133
136
  slice_count: slices.length,
134
137
  executor_tier: 'sonnet',
138
+ text_mode: tmDetail.enabled,
139
+ text_mode_source: tmDetail.source,
135
140
  agent_skills: { executor: _safeSkills('np-executor', cwd) },
136
141
  };
137
142
  }
@@ -13,6 +13,7 @@ const {
13
13
  const { parseRoadmap } = require('../../lib/roadmap.cjs');
14
14
  const { mutateState } = require('../../lib/state.cjs');
15
15
  const layout = require('../../lib/layout.cjs');
16
+ const textMode = require('../../lib/text-mode.cjs');
16
17
 
17
18
  const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates', 'milestone');
18
19
  const PLACEHOLDER_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
@@ -49,9 +50,12 @@ function _emit(stdout, payload) {
49
50
  stdout.write(JSON.stringify(payload, null, 2));
50
51
  }
51
52
 
52
- function _interviewPayload() {
53
+ function _interviewPayload(cwd) {
54
+ const tmDetail = textMode.resolveTextModeDetail(cwd);
53
55
  return {
54
56
  mode: 'interview',
57
+ text_mode: tmDetail.enabled,
58
+ text_mode_source: tmDetail.source,
55
59
  questions: [
56
60
  { key: 'milestone_name', type: 'input',
57
61
  question: 'Milestone name (e.g. "Auth & Basic UI")?' },
@@ -282,7 +286,7 @@ function run(args, ctx) {
282
286
  return;
283
287
  }
284
288
 
285
- _emit(stdout, _interviewPayload());
289
+ _emit(stdout, _interviewPayload(cwd));
286
290
  }
287
291
 
288
292
  module.exports = { run, _interviewPayload };
@@ -10,6 +10,7 @@ const {
10
10
  } = require('../../lib/core.cjs');
11
11
  const { writeState } = require('../../lib/state.cjs');
12
12
  const layout = require('../../lib/layout.cjs');
13
+ const textMode = require('../../lib/text-mode.cjs');
13
14
 
14
15
  const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates');
15
16
  const PLACEHOLDER_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
@@ -42,9 +43,12 @@ function _todayIso() {
42
43
  return new Date().toISOString().slice(0, 10);
43
44
  }
44
45
 
45
- function _interviewPayload() {
46
+ function _interviewPayload(cwd) {
47
+ const tmDetail = textMode.resolveTextModeDetail(cwd);
46
48
  return {
47
49
  mode: 'interview',
50
+ text_mode: tmDetail.enabled,
51
+ text_mode_source: tmDetail.source,
48
52
  questions: [
49
53
  { key: 'project_name', type: 'input',
50
54
  question: 'Project name?' },
@@ -286,7 +290,7 @@ function run(args, ctx) {
286
290
  return;
287
291
  }
288
292
 
289
- _emit(stdout, _interviewPayload());
293
+ _emit(stdout, _interviewPayload(cwd));
290
294
  }
291
295
 
292
296
  module.exports = { run, _interviewPayload, _slugify };
@@ -14,6 +14,7 @@ const {
14
14
  const layout = require('../../lib/layout.cjs');
15
15
  const { getPhase } = require('../../lib/roadmap.cjs');
16
16
  const { getAgentSkills } = require('../../lib/agents.cjs');
17
+ const textMode = require('../../lib/text-mode.cjs');
17
18
 
18
19
  const INLINE_THRESHOLD_BYTES = 16 * 1024;
19
20
 
@@ -104,6 +105,8 @@ function _initPayload(mNum, cwd) {
104
105
  };
105
106
  });
106
107
 
108
+ const tmDetail = textMode.resolveTextModeDetail(cwd);
109
+
107
110
  return {
108
111
  _workflow: 'plan-milestone',
109
112
  milestone: mNum,
@@ -122,6 +125,8 @@ function _initPayload(mNum, cwd) {
122
125
  existing_slices: sliceStatus,
123
126
  planner_tier: 'opus',
124
127
  checker_tier: 'opus',
128
+ text_mode: tmDetail.enabled,
129
+ text_mode_source: tmDetail.source,
125
130
  agent_skills: {
126
131
  'np-planner': _safeSkills('np-planner', cwd),
127
132
  'np-plan-checker': _safeSkills('np-plan-checker', cwd),
@@ -7,6 +7,7 @@ const crypto = require('node:crypto');
7
7
 
8
8
  const { NubosPilotError, projectStateDir } = require('../../lib/core.cjs');
9
9
  const layout = require('../../lib/layout.cjs');
10
+ const textMode = require('../../lib/text-mode.cjs');
10
11
 
11
12
  const INLINE_THRESHOLD_BYTES = 16 * 1024;
12
13
 
@@ -107,6 +108,8 @@ function run(args, ctx) {
107
108
  };
108
109
  });
109
110
 
111
+ const tmDetail = textMode.resolveTextModeDetail(cwd);
112
+
110
113
  const payload = {
111
114
  _workflow: 'research-phase',
112
115
  milestone: mNum,
@@ -118,6 +121,8 @@ function run(args, ctx) {
118
121
  has_research,
119
122
  slice_research: sliceResearch,
120
123
  tools_available: _toolsAvailable(),
124
+ text_mode: tmDetail.enabled,
125
+ text_mode_source: tmDetail.source,
121
126
  agent_skills: _agentSkills(cwd),
122
127
  };
123
128
  _emit(payload, stdout, cwd);
@@ -4,6 +4,7 @@ const { NubosPilotError } = require('../../lib/core.cjs');
4
4
  const { readState } = require('../../lib/state.cjs');
5
5
  const { readCheckpoint, listCheckpoints } = require('../../lib/checkpoint.cjs');
6
6
  const { TASK_ID_RE } = require('../../lib/tasks.cjs');
7
+ const textMode = require('../../lib/text-mode.cjs');
7
8
 
8
9
  function _safeReadState(cwd) {
9
10
  try { return readState(cwd); } catch { return null; }
@@ -69,6 +70,10 @@ function run(_args, ctx) {
69
70
  };
70
71
  }
71
72
 
73
+ const tmDetail = textMode.resolveTextModeDetail(cwd);
74
+ payload.text_mode = tmDetail.enabled;
75
+ payload.text_mode_source = tmDetail.source;
76
+
72
77
  stdout.write(JSON.stringify(payload));
73
78
  return payload;
74
79
  }
@@ -1,14 +1,78 @@
1
+ const fs = require('node:fs');
1
2
  const path = require('node:path');
2
3
  const { execFileSync } = require('node:child_process');
3
4
  const { NubosPilotError, findProjectRoot } = require('../../lib/core.cjs');
4
5
  const { parseRoadmap } = require('../../lib/roadmap.cjs');
5
6
  const { readState } = require('../../lib/state.cjs');
6
7
  const { aggregatePhase } = require('../../lib/metrics-aggregate.cjs');
8
+ const { extractFrontmatter } = require('../../lib/frontmatter.cjs');
9
+ const layout = require('../../lib/layout.cjs');
7
10
 
8
- const SCHEMA_VERSION = 1;
11
+ const SCHEMA_VERSION = 2;
9
12
 
10
13
  function _usage() {
11
- return 'Usage:\n np-tools.cjs stats json';
14
+ return 'Usage:\n np-tools.cjs stats json\n np-tools.cjs stats bar';
15
+ }
16
+
17
+ function _percent(num, den) {
18
+ if (!den || den <= 0) return 0;
19
+ return Math.min(100, Math.round((num / den) * 100));
20
+ }
21
+
22
+ function _taskStatus(planPath) {
23
+ try {
24
+ const raw = fs.readFileSync(planPath, 'utf-8');
25
+ const { frontmatter } = extractFrontmatter(raw);
26
+ return frontmatter && typeof frontmatter.status === 'string'
27
+ ? frontmatter.status : null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ function _collectTaskAndSliceStats(cwd) {
34
+ let tasksTotal = 0;
35
+ let tasksComplete = 0;
36
+ let slicesTotal = 0;
37
+ let slicesComplete = 0;
38
+ const milestones = layout.listMilestones(cwd);
39
+ for (const m of milestones) {
40
+ const slices = layout.listSlices(m.number, cwd);
41
+ for (const s of slices) {
42
+ slicesTotal += 1;
43
+ const tasks = layout.listTasks(m.number, s.number, cwd);
44
+ if (tasks.length === 0) continue;
45
+ let doneInSlice = 0;
46
+ for (const t of tasks) {
47
+ if (!fs.existsSync(t.plan_path)) continue;
48
+ tasksTotal += 1;
49
+ if (_taskStatus(t.plan_path) === 'done') {
50
+ tasksComplete += 1;
51
+ doneInSlice += 1;
52
+ }
53
+ }
54
+ if (doneInSlice === tasks.length && tasks.length > 0) slicesComplete += 1;
55
+ }
56
+ }
57
+ return {
58
+ tasks: {
59
+ total: tasksTotal,
60
+ complete: tasksComplete,
61
+ percent: _percent(tasksComplete, tasksTotal),
62
+ },
63
+ slices: {
64
+ total: slicesTotal,
65
+ complete: slicesComplete,
66
+ percent: _percent(slicesComplete, slicesTotal),
67
+ },
68
+ };
69
+ }
70
+
71
+ function _renderBar(label, percent, width) {
72
+ const w = Math.max(4, Math.min(60, width || 24));
73
+ const filled = Math.round((percent / 100) * w);
74
+ const bar = '█'.repeat(filled) + '░'.repeat(w - filled);
75
+ return label + ' [' + bar + '] ' + String(percent).padStart(3, ' ') + '%';
12
76
  }
13
77
 
14
78
  function _emitError(err, stderr) {
@@ -81,7 +145,8 @@ async function _buildStats(cwd) {
81
145
  plansTotal += ph.plans_total;
82
146
  plansComplete += ph.plans_complete;
83
147
  }
84
- const percent = plansTotal > 0 ? Math.round((plansComplete / plansTotal) * 100) : 0;
148
+ const percent = _percent(plansComplete, plansTotal);
149
+ const fs_progress = _collectTaskAndSliceStats(useCwd);
85
150
  let lastActivity = null;
86
151
  try {
87
152
  const st = readState(useCwd);
@@ -108,6 +173,8 @@ async function _buildStats(cwd) {
108
173
  plans_total: plansTotal,
109
174
  plans_complete: plansComplete,
110
175
  percent,
176
+ tasks: fs_progress.tasks,
177
+ slices: fs_progress.slices,
111
178
  git,
112
179
  last_activity: lastActivity,
113
180
  metrics_by_phase,
@@ -121,7 +188,7 @@ async function run(argv, ctx) {
121
188
  const stderr = context.stderr || process.stderr;
122
189
  const args = Array.isArray(argv) ? argv.slice() : [];
123
190
  const sub = args.shift();
124
- if (sub !== 'json') {
191
+ if (sub !== 'json' && sub !== 'bar') {
125
192
  stderr.write(_usage() + '\n');
126
193
  return 1;
127
194
  }
@@ -133,6 +200,11 @@ async function run(argv, ctx) {
133
200
  }
134
201
  try {
135
202
  const out = await _buildStats(cwd);
203
+ if (sub === 'bar') {
204
+ stdout.write(_renderBar('Tasks ', out.tasks.percent) + ' (' + out.tasks.complete + '/' + out.tasks.total + ')\n');
205
+ stdout.write(_renderBar('Slices', out.slices.percent) + ' (' + out.slices.complete + '/' + out.slices.total + ')\n');
206
+ return 0;
207
+ }
136
208
  stdout.write(JSON.stringify(out, null, 2) + '\n');
137
209
  return 0;
138
210
  } catch (err) {
@@ -141,7 +213,7 @@ async function run(argv, ctx) {
141
213
  }
142
214
  }
143
215
 
144
- module.exports = { run, _buildStats, _collectPhases, _milestoneEntry };
216
+ module.exports = { run, _buildStats, _collectPhases, _milestoneEntry, _collectTaskAndSliceStats, _renderBar };
145
217
 
146
218
  if (require.main === module) {
147
219
  run(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {