nubos-pilot 0.6.0 → 0.6.2

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.
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const os = require('node:os');
7
+
8
+ const BAR_WIDTH = 10;
9
+ const AUTOCOMPACT_BUFFER = 0.835;
10
+ const DEFAULT_WINDOW = 200_000;
11
+ const EXTENDED_WINDOW = 1_000_000;
12
+
13
+ function readStdinJson() {
14
+ return new Promise((resolve) => {
15
+ if (process.stdin.isTTY) return resolve({});
16
+ let buf = '';
17
+ process.stdin.setEncoding('utf-8');
18
+ const timer = setTimeout(() => {
19
+ try { process.stdin.removeAllListeners(); } catch {}
20
+ resolve(safeParse(buf));
21
+ }, 500);
22
+ process.stdin.on('data', (chunk) => { buf += chunk; });
23
+ process.stdin.on('end', () => { clearTimeout(timer); resolve(safeParse(buf)); });
24
+ process.stdin.on('error', () => { clearTimeout(timer); resolve(safeParse(buf)); });
25
+ });
26
+ }
27
+
28
+ function safeParse(s) {
29
+ try { return s ? JSON.parse(s) : {}; } catch { return {}; }
30
+ }
31
+
32
+ function modelWindow(payload) {
33
+ const id = String((payload && payload.model && payload.model.id) || '');
34
+ const name = String((payload && payload.model && payload.model.display_name) || '');
35
+ const s = (id + ' ' + name).toLowerCase();
36
+ if (s.includes('[1m]') || /\b1m\b/.test(s) || s.includes('1-m') || s.includes('1000k')) {
37
+ return EXTENDED_WINDOW;
38
+ }
39
+ return DEFAULT_WINDOW;
40
+ }
41
+
42
+ function lastUsage(transcriptPath) {
43
+ if (!transcriptPath) return null;
44
+ let raw;
45
+ try { raw = fs.readFileSync(transcriptPath, 'utf-8'); } catch { return null; }
46
+ const lines = raw.split('\n');
47
+ for (let i = lines.length - 1; i >= 0; i--) {
48
+ const line = lines[i];
49
+ if (!line) continue;
50
+ let obj;
51
+ try { obj = JSON.parse(line); } catch { continue; }
52
+ const usage = obj && obj.message && obj.message.usage;
53
+ if (!usage || typeof usage !== 'object') continue;
54
+ const input = Number(usage.input_tokens || 0);
55
+ const cacheCreation = Number(usage.cache_creation_input_tokens || 0);
56
+ const cacheRead = Number(usage.cache_read_input_tokens || 0);
57
+ const output = Number(usage.output_tokens || 0);
58
+ const total = input + cacheCreation + cacheRead + output;
59
+ if (!Number.isFinite(total) || total <= 0) continue;
60
+ return { input, cacheCreation, cacheRead, output, total };
61
+ }
62
+ return null;
63
+ }
64
+
65
+ function renderBar(used, limit) {
66
+ const fraction = Math.max(0, Math.min(1, used / limit));
67
+ const ofUsable = Math.max(0, Math.min(1, fraction / AUTOCOMPACT_BUFFER));
68
+ const pct = Math.round(ofUsable * 100);
69
+ const filled = Math.round(ofUsable * BAR_WIDTH);
70
+ const bar = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
71
+ let color = '\x1b[32m';
72
+ let suffix = '';
73
+ if (pct >= 80) { color = '\x1b[31m'; suffix = ' 💀'; }
74
+ else if (pct >= 65) { color = '\x1b[38;5;208m'; }
75
+ else if (pct >= 50) { color = '\x1b[33m'; }
76
+ return color + bar + '\x1b[0m ' + pct + '%' + suffix;
77
+ }
78
+
79
+ function terminalWidth() {
80
+ const envCols = Number(process.env.COLUMNS);
81
+ if (Number.isFinite(envCols) && envCols > 0) return envCols;
82
+ if (process.stdout && process.stdout.columns) return process.stdout.columns;
83
+ try {
84
+ const { execSync } = require('node:child_process');
85
+ const out = execSync('tput cols', { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
86
+ const n = Number(out);
87
+ if (Number.isFinite(n) && n > 0) return n;
88
+ } catch {}
89
+ return 120;
90
+ }
91
+
92
+ function visibleLen(s) {
93
+ const stripped = s.replace(/\x1b\[[0-9;]*m/g, '');
94
+ let w = 0;
95
+ for (const ch of stripped) {
96
+ const cp = ch.codePointAt(0);
97
+ if (cp > 0xFFFF) w += 2;
98
+ else w += 1;
99
+ }
100
+ return w;
101
+ }
102
+
103
+ function centerLine(line) {
104
+ const width = terminalWidth();
105
+ const pad = Math.max(0, Math.floor((width - visibleLen(line)) / 2));
106
+ return ' '.repeat(pad) + line;
107
+ }
108
+
109
+ function writeBridge(payload, usage, limit) {
110
+ const sid = payload && payload.session_id;
111
+ if (!sid) return;
112
+ const bridgePath = path.join(os.tmpdir(), 'claude-ctx-' + String(sid).replace(/[^a-zA-Z0-9._-]/g, '_') + '.json');
113
+ const data = {
114
+ session_id: sid,
115
+ used: usage.total,
116
+ limit: limit,
117
+ usable_remaining_pct: Math.max(0, Math.round(((AUTOCOMPACT_BUFFER * limit) - usage.total) / (AUTOCOMPACT_BUFFER * limit) * 100)),
118
+ updated_at: new Date().toISOString(),
119
+ };
120
+ try { fs.writeFileSync(bridgePath, JSON.stringify(data)); } catch {}
121
+ }
122
+
123
+ (async () => {
124
+ let payload = {};
125
+ try { payload = await readStdinJson(); } catch { payload = {}; }
126
+ const limit = modelWindow(payload);
127
+ const usage = lastUsage(payload && payload.transcript_path);
128
+ const prefix = '\x1b[38;5;33mnubos-pilot\x1b[0m';
129
+ if (!usage) {
130
+ process.stdout.write(centerLine(prefix));
131
+ return;
132
+ }
133
+ writeBridge(payload, usage, limit);
134
+ const bar = renderBar(usage.total, limit);
135
+ const modelName = (payload && payload.model && payload.model.display_name) || '';
136
+ const tail = modelName ? ' \x1b[2m' + modelName + '\x1b[0m' : '';
137
+ process.stdout.write(centerLine(prefix + ' ctx ' + bar + tail));
138
+ })().catch(() => {
139
+ process.stdout.write('\x1b[38;5;33mnubos-pilot\x1b[0m');
140
+ });
@@ -47,10 +47,10 @@ through `lib/layout.cjs.slugify` (strips to `[a-z0-9-]` only;
47
47
  filename-injection mitigation) and validates the description length
48
48
  (<= 500 chars) before any filesystem write occurs.
49
49
 
50
- **Text-mode routing.** If `text_mode == true`, skip every `np-tools.cjs askuser`
51
- call below and render questions as plain-text numbered lists in the main
52
- chat. Auto-enabled in Claude Code (CLAUDECODE=1); opt-in via
53
- `.nubos-pilot/config.json` `workflow.text_mode`.
50
+ **Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a spec, not a literal command. Pick the path once at Initialize:
51
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
52
+ - **`text_mode == true`** (INIT payload): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
53
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
54
54
 
55
55
  ## Create Pending Dir
56
56
 
@@ -34,22 +34,20 @@ Parse JSON for: `milestone`, `milestone_id`, `milestone_dir`, `milestone_name`,
34
34
  `milestone_context_path`, `has_context`, `has_milestone_dir`, `goal`,
35
35
  `requirements`, `agent_skills`, `mode`, `text_mode`, `text_mode_source`.
36
36
 
37
- **Text-mode routing (SSOT = INIT payload).** If `text_mode` is `true`, do
38
- **not** shell out to `np-tools.cjs askuser` for any prompt in this workflow.
39
- Present every question inline as a plain-text numbered list and wait for the
40
- user's reply in the main chat. This is the correct path whenever:
41
-
42
- - `text_mode_source == "runtime"` → Claude Code Bash has no TTY and cannot
43
- forward interactive menu selections; the askuser marker-block protocol
44
- never completes.
45
- - `text_mode_source == "config"` → the user explicitly opted into text mode
46
- via `.nubos-pilot/config.json` → `workflow.text_mode`.
47
-
48
- When text mode is active, skip every `node .nubos-pilot/bin/np-tools.cjs
49
- askuser …` block below and substitute the plain-text equivalent. Collect the
50
- answers from the user's reply, then proceed to the next step as normal. The
51
- rest of the workflow (validation, canonical-ref accumulation, template
52
- render, commit) is unchanged.
37
+ **Askuser routing (SSOT = INIT payload).** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a **spec**, not a literal command. Pick the path once at Initialize:
38
+
39
+ - **Claude Code runtime** (you are running inside Claude Code — the `AskUserQuestion` tool is available to you): **do not** shell out to `np-tools.cjs askuser`. Parse the JSON spec inside each askuser block and call the native `AskUserQuestion` tool directly with one question entry:
40
+ - `type: "select"` `{ question, header, multiSelect: false, options: [{label, description}...] }`
41
+ - `type: "multiselect"` → `{ question, header, multiSelect: true, options: [{label, description}...] }`
42
+ - `type: "confirm"` → single question with `options: [{label: "Yes"}, {label: "No"}]`, `multiSelect: false`
43
+ - `type: "input"` → ask as a plain free-form question in the chat; the user replies inline
44
+ Use a short `header` (≤12 chars) that labels the category, e.g. `"Discuss"`, `"Scope"`, `"Overwrite?"`. This is the default path and gives the user a real selection menu.
45
+
46
+ - **`text_mode == true`** (INIT payload): skip every askuser block and render every question inline as a plain-text numbered list; the user replies with the number. This path is opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
47
+
48
+ - **Other runtime with TTY** (Codex, Gemini, …): run the shell `node .nubos-pilot/bin/np-tools.cjs askuser --json '…'` block verbatim.
49
+
50
+ `text_mode_source` in the INIT payload (`config` / `default`) is informational only it does not change the routing above.
53
51
 
54
52
  If the user passed `--assumptions`, route to
55
53
  `workflows/discuss-phase-assumptions.md` and exit this workflow.
@@ -142,33 +140,19 @@ Capture the idea in a "Deferred Ideas" section. Don't lose it, don't act on it.
142
140
  ## Answer Validation
143
141
 
144
142
  <answer_validation>
145
- **Routing decision (made once at Initialize):** If INIT payload
146
- `text_mode == true`, skip every `np-tools.cjs askuser` call in this workflow
147
- and use plain-text numbered lists in the main chat instead. The
148
- `text_mode_source` field (`runtime` / `config` / `default`) tells you why
149
- text mode is active it is informational only and does not change behavior.
150
-
151
- **When text_mode is false and askuser is used per-prompt validation:**
152
- 1. If `askuser` exits with structured error `askuser-no-tty` (exit code 1,
153
- stderr JSON with `"code":"askuser-no-tty"`), that means the runtime
154
- detection missed something; **skip retry** and treat the remainder of the
155
- workflow as text-mode (plain-text numbered lists).
156
- 2. If the response is empty or whitespace-only (exit 0 but no value), retry
157
- the question once with the same parameters.
158
- 3. If still empty, present the options as a plain-text numbered list and ask
159
- the user to type their choice number.
143
+ **Routing was decided at Initialize** (see "Askuser routing" section above). This section documents per-prompt validation only.
144
+
145
+ **Claude Code path (`AskUserQuestion` tool):** the tool guarantees a non-empty selection; no validation needed.
146
+
147
+ **Shell askuser path (other runtimes with TTY):**
148
+ 1. If `askuser` exits with structured error `askuser-no-tty` (exit code 1, stderr JSON with `"code":"askuser-no-tty"`), that means the runtime detection missed something; **skip retry** and treat the remainder of the workflow as text-mode (plain-text numbered lists).
149
+ 2. If the response is empty or whitespace-only (exit 0 but no value), retry the question once with the same parameters.
150
+ 3. If still empty, present the options as a plain-text numbered list and ask the user to type their choice number.
160
151
  Never proceed with an empty answer.
161
152
 
162
- **Enable text mode:**
163
- - Auto-detected: any Claude Code session (`CLAUDECODE=1` /
164
- `CLAUDE_CODE_ENTRYPOINT` set) — default behavior, no user action needed.
165
- - Opt-in per project: set `workflow.text_mode: true` in
166
- `.nubos-pilot/config.json`.
167
- - Opt-out per project: set `workflow.text_mode: false` in
168
- `.nubos-pilot/config.json` (overrides runtime detection).
153
+ **Text-mode (numbered-list path):** user reply must parse as a valid index (1-N) for select/multiselect, `y/n` for confirm, or any non-empty string for input. Re-ask on invalid input.
169
154
 
170
- Text mode applies to ALL workflows that emit `text_mode` in their INIT
171
- payload, not just discuss-phase.
155
+ **Enable text mode** (force the numbered-list path regardless of runtime): set `workflow.text_mode: true` in `.nubos-pilot/config.json`. Useful for remote-control setups or runtimes where neither `AskUserQuestion` nor TTY stdin are reliable.
172
156
  </answer_validation>
173
157
 
174
158
  ## Process
@@ -386,18 +370,66 @@ If the template lacks a key, `render()` throws
386
370
  `NubosPilotError('template-missing-key', …)` — the workflow must not swallow
387
371
  that error. Fix the template or the accumulator, don't mask the failure.
388
372
 
373
+ ### Step 6b: Extract + persist Success Criteria (np-sc-extractor)
374
+
375
+ CONTEXT.md now captures the decisions. Success Criteria in `roadmap.yaml` are still empty for this milestone — downstream `/np:verify-work` reads them from there, so we must persist them now. Spawn the SC-extractor (haiku) to derive observable SCs from goal + requirements + CONTEXT.md + any pre-existing `M<NNN>-ROADMAP.md` / `M<NNN>-META.json` sidecars, and call `update-phase-meta` to write them.
376
+
377
+ ```bash
378
+ SC_START=$(node .nubos-pilot/bin/np-tools.cjs metrics start-timestamp)
379
+ SC_MODEL=$(node .nubos-pilot/bin/np-tools.cjs resolve-model np-sc-extractor --profile balanced)
380
+
381
+ REQS_PATH=".nubos-pilot/REQUIREMENTS.md"
382
+ [[ -f "$REQS_PATH" ]] || REQS_PATH=".planning/REQUIREMENTS.md"
383
+
384
+ EXISTING_SC_JSON=$(node -e '
385
+ const r = require("./lib/roadmap.cjs");
386
+ const p = r.getPhase(process.argv[1]);
387
+ process.stdout.write(JSON.stringify(p.success_criteria || []));
388
+ ' "$PHASE")
389
+
390
+ # Spawn agent=np-sc-extractor tier=haiku model=$SC_MODEL milestone=$PHASE
391
+ # input: milestone=$PHASE, milestone_id=$MILESTONE_ID, milestone_dir=$MILESTONE_DIR,
392
+ # context_path=$CONTEXT_PATH, requirements_path=$REQS_PATH,
393
+ # existing_success_criteria=$EXISTING_SC_JSON
394
+ # output: calls `np-tools.cjs update-phase-meta $PHASE --stdin` with
395
+ # {"success_criteria": [{id:"SC-N", text:"..."}, ...]} and prints summary.
396
+
397
+ SC_END=$(node .nubos-pilot/bin/np-tools.cjs metrics end-timestamp)
398
+ node .nubos-pilot/bin/np-tools.cjs metrics record \
399
+ --agent np-sc-extractor --tier haiku --resolved-model "$SC_MODEL" \
400
+ --phase "$PHASE" --plan "${MILESTONE_ID}-sc" --task "${MILESTONE_ID}-sc-extract" \
401
+ --started "$SC_START" --ended "$SC_END" \
402
+ --tokens-in "${TOKENS_IN:-0}" --tokens-out "${TOKENS_OUT:-0}" \
403
+ --retry-count 0 --status ok --runtime "$RUNTIME"
404
+ ```
405
+
406
+ After the spawn, sanity-check that `success_criteria` is non-empty:
407
+
408
+ ```bash
409
+ SC_COUNT=$(node -e '
410
+ const r = require("./lib/roadmap.cjs");
411
+ const p = r.getPhase(process.argv[1]);
412
+ process.stdout.write(String((p.success_criteria || []).length));
413
+ ' "$PHASE")
414
+ if [[ "$SC_COUNT" -lt 1 ]]; then
415
+ echo "ERROR: np-sc-extractor produced no success_criteria for $MILESTONE_ID — refusing to continue." >&2
416
+ exit 1
417
+ fi
418
+ ```
419
+
420
+ A failure here is loud by design: `/np:verify-work` and `/np:validate-phase` depend on a populated `success_criteria[]`. If the extractor cannot derive any, fix the goal/requirements/CONTEXT.md inputs before retrying.
421
+
389
422
  ### Step 7: Commit respecting config.commit_docs
390
423
 
391
424
  ```bash
392
425
  COMMIT_DOCS=$(node .nubos-pilot/bin/np-tools.cjs config-get workflow.commit_docs 2>/dev/null || echo "true")
393
426
  if [[ "$COMMIT_DOCS" == "true" ]]; then
394
- git add "$CONTEXT_PATH"
395
- git commit -m "docs($MILESTONE_ID): capture milestone context"
427
+ git add "$CONTEXT_PATH" .nubos-pilot/roadmap.yaml .nubos-pilot/ROADMAP.md
428
+ git commit -m "docs($MILESTONE_ID): capture milestone context + success criteria"
396
429
  fi
397
430
  ```
398
431
 
399
- If `workflow.commit_docs` is false, leave the file uncommitted — the user is
400
- opting into manual commit gating.
432
+ If `workflow.commit_docs` is false, leave both CONTEXT.md and the roadmap edits uncommitted — the user is opting into manual commit gating.
401
433
 
402
434
  ### Step 8: Confirm and next steps
403
435
 
@@ -61,6 +61,11 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
61
61
  narrative status updates, and the prose written into PROJECT.md sections.
62
62
  Supersedes CLAUDE.md managed block.
63
63
 
64
+ **Askuser routing.** The "Use `np-tools.cjs askuser` for every prompt" rule below is SC-5 gateway enforcement — the JSON spec must pass through np-tools for logging/validation. Pick the presentation path:
65
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` → `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
66
+ - **`text_mode == true`** (INIT payload): skip shell askuser calls and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
67
+ - **Other runtime with TTY** (Codex, Gemini, …): execute `node .nubos-pilot/bin/np-tools.cjs askuser --json '…'` directly.
68
+
64
69
  Parse: `mode`, `sub_mode` (`bootstrap` or `refresh`), `project_md_exists`,
65
70
  `scan_context`, `questions[]`, `required_fields[]`.
66
71
 
@@ -32,11 +32,10 @@ directive in CLAUDE.md managed block.
32
32
 
33
33
  Parse JSON for: `milestone`, `milestone_id`, `milestone_dir`, `waves[]` (each with `wave` (= slice number), `slice_id`, `slice_full_id`, `slice_dir`, `tasks[]`), `total_tasks`, `slice_count`, `executor_tier`, `text_mode`, `text_mode_source`, `agent_skills`.
34
34
 
35
- **Text-mode routing.** If `text_mode == true`, skip every `np-tools.cjs askuser`
36
- call below (including the orphan-checkpoint and empty-milestone prompts)
37
- and render the options as a plain-text numbered list in the main chat.
38
- Auto-enabled in Claude Code (CLAUDECODE=1); opt-in via
39
- `.nubos-pilot/config.json` → `workflow.text_mode`.
35
+ **Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below (including the orphan-checkpoint and empty-milestone prompts) is a spec, not a literal command. Pick the path once at Initialize:
36
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` → `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
37
+ - **`text_mode == true`** (INIT payload): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
38
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
40
39
 
41
40
  `PLAN_ID` is iterated per slice as `${milestone_id}-${slice_id}` (e.g. `M001-S001`). `TASK_ID` is iterated from each slice's `tasks[]` (e.g. `M001-S001-T0001`).
42
41
 
@@ -62,11 +62,10 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
62
62
  user-facing output, and any prose written into milestone artefacts (YAML
63
63
  keys, IDs, and identifiers stay canonical English). Supersedes CLAUDE.md.
64
64
 
65
- **Text-mode routing.** If INIT payload `text_mode == true`, skip every
66
- `np-tools.cjs askuser` call below and render each question as a plain-text
67
- prompt in the main chat; collect the answer inline. Auto-enabled in Claude
68
- Code (CLAUDECODE=1); opt-in via `.nubos-pilot/config.json`
69
- `workflow.text_mode`.
65
+ **Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a spec, not a literal command. Pick the path once at Initialize:
66
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` → `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
67
+ - **`text_mode == true`** (INIT payload): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
68
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
70
69
 
71
70
  Payload: three questions — `milestone_name`, `milestone_goal`, `create_req_prefix` (confirm).
72
71
 
@@ -126,11 +126,10 @@ user-facing output, and any narrative prose written into PROJECT.md /
126
126
  REQUIREMENTS.md (field names and YAML keys stay canonical English).
127
127
  Supersedes CLAUDE.md.
128
128
 
129
- **Text-mode routing.** If INIT payload `text_mode == true`, skip every
130
- `np-tools.cjs askuser` call below and render each question as a plain-text
131
- prompt in the main chat; collect the user's answer inline and move on.
132
- Auto-enabled in Claude Code (CLAUDECODE=1); opt-in via
133
- `.nubos-pilot/config.json` → `workflow.text_mode`.
129
+ **Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a spec, not a literal command. Pick the path once at Initialize:
130
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` → `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
131
+ - **`text_mode == true`** (INIT payload): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
132
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
134
133
 
135
134
  ```bash
136
135
  ANS_PROJECT_NAME=$(node .nubos-pilot/bin/np-tools.cjs askuser --json '{"type":"input","prompt":"Project name?"}')
package/workflows/note.md CHANGED
@@ -52,16 +52,16 @@ into `$TEXT`. Empty text after stripping is an error — there is no
52
52
  `list` or `promote` subcommand here (deferred to a future
53
53
  capture-management plan).
54
54
 
55
- **Text-mode routing.** Resolve once at the start:
55
+ **Askuser routing.** Resolve once at the start:
56
56
 
57
57
  ```bash
58
58
  TEXT_MODE=$(node .nubos-pilot/bin/np-tools.cjs text-mode 2>/dev/null || echo false)
59
59
  ```
60
60
 
61
- If `$TEXT_MODE == "true"`, skip every `np-tools.cjs askuser` call below and
62
- render questions as plain-text numbered lists in the main chat. Auto-enabled
63
- in Claude Code (CLAUDECODE=1); opt-in via `.nubos-pilot/config.json` →
64
- `workflow.text_mode`.
61
+ Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a spec, not a literal command. Pick the path:
62
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` → `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
63
+ - **`$TEXT_MODE == "true"`**: skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
64
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
65
65
 
66
66
  ## Compute Paths
67
67
 
@@ -88,10 +88,10 @@ prompts as a system-level rule. This supersedes any directive in CLAUDE.md.
88
88
 
89
89
  Parse JSON for: `milestone`, `milestone_id`, `milestone_dir`, `milestone_context_path`, `milestone_roadmap_path`, `milestone_meta_path`, `name`, `goal`, `requirements`, `success_criteria`, `has_context`, `has_roadmap`, `has_meta`, `existing_slices[]`, `planner_tier`, `checker_tier`, `text_mode`, `text_mode_source`, `agent_skills`.
90
90
 
91
- **Text-mode routing.** If `text_mode == true`, skip every `np-tools.cjs askuser`
92
- call below and present questions as plain-text numbered lists in the main
93
- chat. Auto-enabled in Claude Code (CLAUDECODE=1); opt-in per-project via
94
- `.nubos-pilot/config.json` `workflow.text_mode`.
91
+ **Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a spec, not a literal command. Pick the path once at Initialize:
92
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
93
+ - **`text_mode == true`** (INIT payload): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
94
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
95
95
 
96
96
  `PLAN_ID` and `TASK_ID` default to `${milestone_id}-plan` / `${milestone_id}-planner-run` for the metrics records.
97
97
 
@@ -118,6 +118,29 @@ case "$CHOICE" in
118
118
  esac
119
119
  ```
120
120
 
121
+ ### Gate 1b — Empty success_criteria
122
+
123
+ If `success_criteria.length == 0`:
124
+
125
+ ```bash
126
+ CHOICE=$(node .nubos-pilot/bin/np-tools.cjs askuser --json '{
127
+ "type": "select",
128
+ "header": "No SCs in roadmap.yaml",
129
+ "question": "Milestone has no success_criteria in roadmap.yaml. Downstream /np:verify-work will produce an empty VERIFICATION.md. How to proceed?",
130
+ "options": [
131
+ {"label": "Run /np:discuss-phase first", "description": "Recommended — np-sc-extractor derives SCs from CONTEXT.md + goal + requirements and writes them to roadmap.yaml."},
132
+ {"label": "Continue anyway", "description": "Plan the milestone without SCs; you must back-fill them before /np:verify-work."},
133
+ {"label": "Abort", "description": "Exit without changes."}
134
+ ]
135
+ }')
136
+ case "$CHOICE" in
137
+ "Run /np:discuss-phase"*) echo "Run: /np:discuss-phase $PHASE"; exit 0 ;;
138
+ "Abort") exit 0 ;;
139
+ esac
140
+ ```
141
+
142
+ The planner will still emit a plan without SCs, but you are consciously opting into a known-broken verify-work path. The safer default is always to re-run `/np:discuss-phase` — Step 6b there spawns `np-sc-extractor` which populates `roadmap.yaml` directly.
143
+
121
144
  ### Gate 2 — Missing slice RESEARCH.md
122
145
 
123
146
  Research is per-slice (`slices/S<NNN>/S<NNN>-RESEARCH.md`). The planner can plan without research, but if the roadmap config requires it, ask. The `--research` flag auto-dispatches `/np:research-phase` before re-entering.
@@ -70,7 +70,10 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
70
70
  **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
71
71
  `$LANG_DIRECTIVE` is authoritative. Obey it for askuser prompt texts, AI-facing reasoning shown to the user, and any narrative prose. YAML keys, milestone IDs, and status strings stay canonical English.
72
72
 
73
- **Text-mode routing.** If INIT payload `text_mode == true`, skip every `np-tools.cjs askuser` call below and render each question as a plain-text prompt in the main chat; collect the answer inline. Auto-enabled in Claude Code (CLAUDECODE=1); opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
73
+ **Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a spec, not a literal command. Pick the path once at Initialize:
74
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` → `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
75
+ - **`text_mode == true`** (INIT payload): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
76
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
74
77
 
75
78
  Parse INIT for: `milestones[]` (each with `id`, `name`, `goal`, `status`, `classification`, `slice_count`, `context`, `touchable`, `modification_requires_confirm`), `project_md`, `requirements_md`, `project_has_tbd`, `current_state_milestone`, `next_milestone_number`, `guidance`.
76
79
 
@@ -107,10 +107,10 @@ project language. This supersedes CLAUDE.md.
107
107
  `RUNTIME` is resolved once here and reused by the metrics-record call at the
108
108
  researcher spawn site (Step 4) per D-06 workflow-writer pattern.
109
109
 
110
- **Text-mode routing.** If `text_mode == true` in the payload below, skip every
111
- `np-tools.cjs askuser` call in this workflow and render questions as
112
- plain-text numbered lists in the main chat. Auto-enabled in Claude Code
113
- (CLAUDECODE=1); opt-in via `.nubos-pilot/config.json` `workflow.text_mode`.
110
+ **Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a spec, not a literal command. Pick the path once at Initialize:
111
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` → `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
112
+ - **`text_mode == true`** (INIT payload): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` `workflow.text_mode`.
113
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
114
114
 
115
115
  The payload shape:
116
116
 
@@ -24,10 +24,10 @@ askuser prompts. When spawning the np-executor to continue a checkpoint,
24
24
  pass `$LANG_DIRECTIVE` into the spawn prompt so resumed task summaries
25
25
  follow the project language. Supersedes CLAUDE.md.
26
26
 
27
- **Text-mode routing.** If INIT payload `text_mode == true`, skip every
28
- `np-tools.cjs askuser` call below and render prompts as plain-text numbered
29
- lists in the main chat. Auto-enabled in Claude Code (CLAUDECODE=1); opt-in
30
- via `.nubos-pilot/config.json` `workflow.text_mode`.
27
+ **Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a spec, not a literal command. Pick the path once at Initialize:
28
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` → `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
29
+ - **`text_mode == true`** (INIT payload): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` `workflow.text_mode`.
30
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
31
31
 
32
32
  ## Execution
33
33
 
@@ -62,10 +62,10 @@ canonical English. Supersedes CLAUDE.md.
62
62
  TEXT_MODE=$(node .nubos-pilot/bin/np-tools.cjs text-mode 2>/dev/null || echo false)
63
63
  ```
64
64
 
65
- If `$TEXT_MODE == "true"`, skip every `np-tools.cjs askuser` call below and
66
- render questions as plain-text numbered lists in the main chat. Auto-enabled
67
- in Claude Code (CLAUDECODE=1); opt-in via `.nubos-pilot/config.json` →
68
- `workflow.text_mode`.
65
+ **Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a spec, not a literal command. Pick the path once at Initialize:
66
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` → `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
67
+ - **`$TEXT_MODE == "true"`** (from the check above, or INIT payload `text_mode == true`): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
68
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
69
69
 
70
70
  The filename format is `YYYY-MM-DDTHHMM-session-report.md` (D-17 —
71
71
  4-char HHMM, no seconds, local time) so reports sort
@@ -33,10 +33,10 @@ field names stay English. Supersedes CLAUDE.md.
33
33
 
34
34
  Parse JSON for: `milestone`, `milestone_id`, `milestone_dir`, `milestone_name`, `slice_uat`, `text_mode`, `text_mode_source`.
35
35
 
36
- **Text-mode routing.** If `text_mode == true`, skip every `np-tools.cjs askuser`
37
- call in this workflow and render options as plain-text numbered lists in
38
- the main chat. Auto-enabled in Claude Code (CLAUDECODE=1); opt-in via
39
- `.nubos-pilot/config.json` `workflow.text_mode`.
36
+ **Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a spec, not a literal command. Pick the path once at Initialize:
37
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` → `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
38
+ - **`text_mode == true`** (INIT payload): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
39
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
40
40
 
41
41
  ```bash
42
42
  MILESTONE_ID=$(echo "$INIT" | jq -r '.milestone_id')
@@ -31,11 +31,10 @@ CLAUDE.md.
31
31
 
32
32
  Parse: `milestone`, `milestone_id`, `milestone_dir`, `milestone_name`, `success_criteria`, `draft_results`, `verification_path`, `slice_uat`, `verifier_tier`, `text_mode`, `text_mode_source`, `agent_skills`.
33
33
 
34
- **Text-mode routing.** If `text_mode == true`, skip every `np-tools.cjs askuser`
35
- call below (including the Pass-2 `needs_user_confirm` gate) and render the
36
- options as a plain-text numbered list in the main chat. Auto-enabled in
37
- Claude Code (CLAUDECODE=1); opt-in via `.nubos-pilot/config.json`
38
- `workflow.text_mode`.
34
+ **Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below (including the Pass-2 `needs_user_confirm` gate) is a spec, not a literal command. Pick the path once at Initialize:
35
+ - **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
36
+ - **`text_mode == true`** (INIT payload): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` `workflow.text_mode`.
37
+ - **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.
39
38
 
40
39
  ## Pass 1 — verifier agent
41
40