nubos-pilot 0.5.2 → 0.5.3

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.
package/bin/install.js CHANGED
@@ -16,6 +16,7 @@ const runtimeDetectMod = require('../lib/install/runtime-detect.cjs');
16
16
  const backupMod = require('../lib/install/backup.cjs');
17
17
  const registryMod = require('../lib/install/runtimes-registry.cjs');
18
18
  const runtimeAssetsMod = require('../lib/install/runtime-assets.cjs');
19
+ const languageMod = require('../lib/language.cjs');
19
20
 
20
21
  const cyan = '\x1b[36m', green = '\x1b[32m', yellow = '\x1b[33m',
21
22
  red = '\x1b[31m', blue = '\x1b[38;5;33m',
@@ -64,22 +65,11 @@ function _autoAskUser(spec) {
64
65
  });
65
66
  }
66
67
 
67
- const LANG_DIRECTIVES = {
68
- de: 'Sprache: **Deutsch.** Jede nubos-pilot Slash-Command-Ausgabe, jede Frage an den User und jedes Statusupdate in allen `/np:*` Workflows ist auf Deutsch zu schreiben — inklusive Fehlermeldungen und Klärungsfragen. Nur Code, Bash-Kommandos, Tool-Outputs und Commit-Messages bleiben wie sie sind.',
69
- en: 'Language: **English.** All `/np:*` slash-command output, askuser prompts and status updates respond in English.',
70
- };
71
-
72
- function _langDirective(responseLanguage) {
73
- const lang = String(responseLanguage || 'en').toLowerCase();
74
- if (LANG_DIRECTIVES[lang]) return LANG_DIRECTIVES[lang];
75
- return 'Language: respond in the ISO-639 language `' + lang + '` for all `/np:*` slash-command output, askuser prompts and status updates.';
76
- }
77
-
78
68
  function _managedBlockInner(responseLanguage) {
79
69
  return (
80
70
  'This project uses [nubos-pilot](https://github.com/nubos/nubos-pilot)'
81
71
  + ' for planning and execution.\n\n'
82
- + _langDirective(responseLanguage)
72
+ + languageMod.buildDirective(responseLanguage)
83
73
  + '\n\nRun `npx nubos-pilot doctor` to check install integrity.'
84
74
  );
85
75
  }
@@ -43,6 +43,7 @@ const COMMANDS = [
43
43
  { name: 'askuser', category: 'Utility', description: 'Capability-layer prompt wrapper (reads spec JSON, returns chosen label)' },
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
+ { name: 'lang-directive', category: 'Utility', description: 'Print workflow language directive from config.response_language (SSOT)' },
46
47
  { name: 'generate-slug', category: 'Utility', description: 'Slugify text via lib/layout.cjs.slugify' },
47
48
  { name: 'stats', category: 'Utility', description: 'Aggregated project stats (roadmap + STATE + git + metrics JSON shape)' },
48
49
 
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const languageMod = require('../../lib/language.cjs');
4
+ const { NubosPilotError } = require('../../lib/core.cjs');
5
+
6
+ function _usage() {
7
+ return 'Usage:\n np-tools.cjs lang-directive [--json] [--lang <code>]';
8
+ }
9
+
10
+ function _emitError(err, stderr) {
11
+ const code = err && err.name === 'NubosPilotError' ? err.code : 'lang-directive-internal-error';
12
+ const message = (err && err.message) || String(err);
13
+ const details = (err && err.details) || null;
14
+ stderr.write(JSON.stringify({ code, message, details }) + '\n');
15
+ }
16
+
17
+ function run(argv, ctx) {
18
+ const context = ctx || {};
19
+ const cwd = context.cwd || process.cwd();
20
+ const stdout = context.stdout || process.stdout;
21
+ const stderr = context.stderr || process.stderr;
22
+ const args = Array.isArray(argv) ? argv.slice() : [];
23
+
24
+ let wantJson = false;
25
+ let override = null;
26
+ for (let i = 0; i < args.length; i++) {
27
+ const a = args[i];
28
+ if (a === '--json') { wantJson = true; continue; }
29
+ if (a === '--lang') { override = args[++i] || null; continue; }
30
+ if (a.startsWith('--lang=')) { override = a.slice('--lang='.length); continue; }
31
+ if (a === '-h' || a === '--help') {
32
+ stdout.write(_usage() + '\n');
33
+ return 0;
34
+ }
35
+ stderr.write(JSON.stringify({
36
+ code: 'lang-directive-unknown-arg',
37
+ message: 'Unknown argument: ' + a,
38
+ details: { arg: a },
39
+ }) + '\n');
40
+ return 1;
41
+ }
42
+
43
+ try {
44
+ const language = override != null
45
+ ? languageMod.normalizeLanguage(override)
46
+ : languageMod.resolveLanguage(cwd);
47
+ const directive = languageMod.buildDirective(language);
48
+ if (wantJson) {
49
+ stdout.write(JSON.stringify({ language, directive }) + '\n');
50
+ } else {
51
+ stdout.write(directive + '\n');
52
+ }
53
+ return 0;
54
+ } catch (err) {
55
+ _emitError(err, stderr);
56
+ return 1;
57
+ }
58
+ }
59
+
60
+ module.exports = { run };
61
+
62
+ if (require.main === module) {
63
+ process.exit(run(process.argv.slice(2)));
64
+ }
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const os = require('node:os');
8
+
9
+ const subcmd = require('./lang-directive.cjs');
10
+
11
+ function _capture() {
12
+ let out = '';
13
+ let err = '';
14
+ const stdout = { write: (s) => { out += s; return true; } };
15
+ const stderr = { write: (s) => { err += s; return true; } };
16
+ return { stdout, stderr, getOut: () => out, getErr: () => err };
17
+ }
18
+
19
+ function _mkProject(language) {
20
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-lang-cli-'));
21
+ fs.mkdirSync(path.join(dir, '.nubos-pilot'), { recursive: true });
22
+ const cfg = language ? { response_language: language } : {};
23
+ fs.writeFileSync(path.join(dir, '.nubos-pilot', 'config.json'), JSON.stringify(cfg));
24
+ return dir;
25
+ }
26
+
27
+ test('lang-directive: default prints plain directive text', () => {
28
+ const dir = _mkProject('de');
29
+ try {
30
+ const cap = _capture();
31
+ const rc = subcmd.run([], { cwd: dir, stdout: cap.stdout, stderr: cap.stderr });
32
+ assert.equal(rc, 0);
33
+ assert.match(cap.getOut(), /Sprache: \*\*Deutsch\.\*\*/);
34
+ } finally {
35
+ fs.rmSync(dir, { recursive: true, force: true });
36
+ }
37
+ });
38
+
39
+ test('lang-directive: --json emits language+directive payload', () => {
40
+ const dir = _mkProject('de');
41
+ try {
42
+ const cap = _capture();
43
+ const rc = subcmd.run(['--json'], { cwd: dir, stdout: cap.stdout, stderr: cap.stderr });
44
+ assert.equal(rc, 0);
45
+ const parsed = JSON.parse(cap.getOut());
46
+ assert.equal(parsed.language, 'de');
47
+ assert.match(parsed.directive, /Sprache: \*\*Deutsch\.\*\*/);
48
+ } finally {
49
+ fs.rmSync(dir, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ test('lang-directive: --lang overrides config', () => {
54
+ const dir = _mkProject('de');
55
+ try {
56
+ const cap = _capture();
57
+ const rc = subcmd.run(['--lang', 'en'], { cwd: dir, stdout: cap.stdout, stderr: cap.stderr });
58
+ assert.equal(rc, 0);
59
+ assert.match(cap.getOut(), /Language: \*\*English\.\*\*/);
60
+ } finally {
61
+ fs.rmSync(dir, { recursive: true, force: true });
62
+ }
63
+ });
64
+
65
+ test('lang-directive: defaults to en when no config entry', () => {
66
+ const dir = _mkProject(null);
67
+ try {
68
+ const cap = _capture();
69
+ const rc = subcmd.run([], { cwd: dir, stdout: cap.stdout, stderr: cap.stderr });
70
+ assert.equal(rc, 0);
71
+ assert.match(cap.getOut(), /Language: \*\*English\.\*\*/);
72
+ } finally {
73
+ fs.rmSync(dir, { recursive: true, force: true });
74
+ }
75
+ });
76
+
77
+ test('lang-directive: unknown arg returns error code', () => {
78
+ const dir = _mkProject(null);
79
+ try {
80
+ const cap = _capture();
81
+ const rc = subcmd.run(['--nope'], { cwd: dir, stdout: cap.stdout, stderr: cap.stderr });
82
+ assert.equal(rc, 1);
83
+ const parsed = JSON.parse(cap.getErr().trim());
84
+ assert.equal(parsed.code, 'lang-directive-unknown-arg');
85
+ } finally {
86
+ fs.rmSync(dir, { recursive: true, force: true });
87
+ }
88
+ });
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { findProjectRoot, NubosPilotError } = require('./core.cjs');
6
+
7
+ const LANG_DIRECTIVES = {
8
+ de: 'Sprache: **Deutsch.** Jede nubos-pilot Slash-Command-Ausgabe, jede Frage an den User und jedes Statusupdate in allen `/np:*` Workflows ist auf Deutsch zu schreiben — inklusive Fehlermeldungen und Klärungsfragen. Nur Code, Bash-Kommandos, Tool-Outputs und Commit-Messages bleiben wie sie sind.',
9
+ en: 'Language: **English.** All `/np:*` slash-command output, askuser prompts and status updates respond in English.',
10
+ };
11
+
12
+ const DEFAULT_LANGUAGE = 'en';
13
+
14
+ function normalizeLanguage(raw) {
15
+ const s = String(raw || '').trim().toLowerCase();
16
+ return s || DEFAULT_LANGUAGE;
17
+ }
18
+
19
+ function buildDirective(language) {
20
+ const lang = normalizeLanguage(language);
21
+ if (LANG_DIRECTIVES[lang]) return LANG_DIRECTIVES[lang];
22
+ return 'Language: respond in the ISO-639 language `' + lang + '` for all `/np:*` slash-command output, askuser prompts and status updates.';
23
+ }
24
+
25
+ function readConfigLanguage(cwd) {
26
+ let root;
27
+ try {
28
+ root = findProjectRoot(cwd || process.cwd());
29
+ } catch (err) {
30
+ if (err && err.code === 'not-in-project') return null;
31
+ throw err;
32
+ }
33
+ const p = path.join(root, '.nubos-pilot', 'config.json');
34
+ if (!fs.existsSync(p)) return null;
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
38
+ } catch (err) {
39
+ throw new NubosPilotError('language-config-parse-error', 'config.json invalid JSON', { cause: err && err.message });
40
+ }
41
+ if (!parsed || typeof parsed !== 'object') return null;
42
+ const raw = parsed.response_language;
43
+ if (raw == null || raw === '') return null;
44
+ return normalizeLanguage(raw);
45
+ }
46
+
47
+ function resolveLanguage(cwd) {
48
+ return readConfigLanguage(cwd) || DEFAULT_LANGUAGE;
49
+ }
50
+
51
+ function resolveDirective(cwd) {
52
+ return buildDirective(resolveLanguage(cwd));
53
+ }
54
+
55
+ module.exports = {
56
+ LANG_DIRECTIVES,
57
+ DEFAULT_LANGUAGE,
58
+ normalizeLanguage,
59
+ buildDirective,
60
+ readConfigLanguage,
61
+ resolveLanguage,
62
+ resolveDirective,
63
+ };
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const os = require('node:os');
8
+
9
+ const lang = require('./language.cjs');
10
+
11
+ function _mkSandbox() {
12
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-lang-'));
13
+ fs.mkdirSync(path.join(dir, '.nubos-pilot'), { recursive: true });
14
+ return dir;
15
+ }
16
+
17
+ function _writeConfig(dir, obj) {
18
+ fs.writeFileSync(path.join(dir, '.nubos-pilot', 'config.json'), JSON.stringify(obj, null, 2));
19
+ }
20
+
21
+ test('language: normalizeLanguage lowercases and trims', () => {
22
+ assert.equal(lang.normalizeLanguage('DE'), 'de');
23
+ assert.equal(lang.normalizeLanguage(' En '), 'en');
24
+ assert.equal(lang.normalizeLanguage(''), 'en');
25
+ assert.equal(lang.normalizeLanguage(null), 'en');
26
+ });
27
+
28
+ test('language: buildDirective returns known de/en strings', () => {
29
+ const de = lang.buildDirective('de');
30
+ assert.match(de, /Sprache: \*\*Deutsch\.\*\*/);
31
+ const en = lang.buildDirective('en');
32
+ assert.match(en, /Language: \*\*English\.\*\*/);
33
+ });
34
+
35
+ test('language: buildDirective falls back to ISO-639 template for unknown code', () => {
36
+ const fr = lang.buildDirective('fr');
37
+ assert.match(fr, /ISO-639 language `fr`/);
38
+ });
39
+
40
+ test('language: readConfigLanguage returns null when no config', () => {
41
+ const dir = _mkSandbox();
42
+ try {
43
+ assert.equal(lang.readConfigLanguage(dir), null);
44
+ } finally {
45
+ fs.rmSync(dir, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ test('language: readConfigLanguage returns normalized value from config', () => {
50
+ const dir = _mkSandbox();
51
+ try {
52
+ _writeConfig(dir, { response_language: 'DE' });
53
+ assert.equal(lang.readConfigLanguage(dir), 'de');
54
+ } finally {
55
+ fs.rmSync(dir, { recursive: true, force: true });
56
+ }
57
+ });
58
+
59
+ test('language: readConfigLanguage returns null when response_language absent', () => {
60
+ const dir = _mkSandbox();
61
+ try {
62
+ _writeConfig(dir, { runtime: 'claude' });
63
+ assert.equal(lang.readConfigLanguage(dir), null);
64
+ } finally {
65
+ fs.rmSync(dir, { recursive: true, force: true });
66
+ }
67
+ });
68
+
69
+ test('language: resolveLanguage defaults to en when no project root', () => {
70
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'np-outside-'));
71
+ try {
72
+ assert.equal(lang.resolveLanguage(outside), 'en');
73
+ } finally {
74
+ fs.rmSync(outside, { recursive: true, force: true });
75
+ }
76
+ });
77
+
78
+ test('language: resolveDirective uses config language', () => {
79
+ const dir = _mkSandbox();
80
+ try {
81
+ _writeConfig(dir, { response_language: 'de' });
82
+ assert.match(lang.resolveDirective(dir), /Sprache: \*\*Deutsch\.\*\*/);
83
+ } finally {
84
+ fs.rmSync(dir, { recursive: true, force: true });
85
+ }
86
+ });
87
+
88
+ test('language: readConfigLanguage throws on invalid JSON', () => {
89
+ const dir = _mkSandbox();
90
+ try {
91
+ fs.writeFileSync(path.join(dir, '.nubos-pilot', 'config.json'), '{not json');
92
+ assert.throws(
93
+ () => lang.readConfigLanguage(dir),
94
+ (err) => err && err.code === 'language-config-parse-error',
95
+ );
96
+ } finally {
97
+ fs.rmSync(dir, { recursive: true, force: true });
98
+ }
99
+ });
@@ -7,6 +7,10 @@ function _setReadlineImplForTests(impl) {
7
7
  _readlineImpl = impl || null;
8
8
  }
9
9
 
10
+ function _hasReadlineImplForTests() {
11
+ return _readlineImpl != null;
12
+ }
13
+
10
14
  function _readOneLine() {
11
15
  if (_readlineImpl) return Promise.resolve(_readlineImpl());
12
16
  return new Promise((resolve, reject) => {
@@ -156,4 +160,4 @@ async function askUserReadline({ type, question, options, def }) {
156
160
  return { value: _parseAnswer(type, line, options, def), source: 'readline' };
157
161
  }
158
162
 
159
- module.exports = { askUserReadline, _readOneLine, _parseAnswer, _setReadlineImplForTests };
163
+ module.exports = { askUserReadline, _readOneLine, _parseAnswer, _setReadlineImplForTests, _hasReadlineImplForTests };
@@ -115,9 +115,10 @@ test('RL-9: _parseAnswer unknown type throws askuser-invalid-type', () => {
115
115
  );
116
116
  });
117
117
 
118
- test('RL-10: module exports exactly askUserReadline, _readOneLine, _parseAnswer, _setReadlineImplForTests', () => {
118
+ test('RL-10: module exports readline helpers + claude-runtime TTY probe', () => {
119
119
  const keys = Object.keys(rl).sort();
120
120
  assert.deepEqual(keys, [
121
+ '_hasReadlineImplForTests',
121
122
  '_parseAnswer',
122
123
  '_readOneLine',
123
124
  '_setReadlineImplForTests',
@@ -1,4 +1,5 @@
1
- const { _readOneLine, _parseAnswer } = require('./_readline.cjs');
1
+ const { _readOneLine, _parseAnswer, _hasReadlineImplForTests } = require('./_readline.cjs');
2
+ const { NubosPilotError } = require('../core.cjs');
2
3
 
3
4
  function _emitClaudeMarkerBlock({ type, question, options, def }) {
4
5
  const payload = {
@@ -17,6 +18,17 @@ async function askUser(spec) {
17
18
  const question = spec && spec.question;
18
19
  const options = spec && spec.options;
19
20
  const def = spec ? spec.default : undefined;
21
+ const hasTTY = !!process.stdin.isTTY;
22
+ if (!hasTTY && !_hasReadlineImplForTests()) {
23
+ if (def !== undefined && def !== null) {
24
+ return { value: def, source: 'default' };
25
+ }
26
+ throw new NubosPilotError(
27
+ 'askuser-no-tty',
28
+ 'askUser cannot prompt without TTY (Claude Code Bash has no interactive stdin). Fall back to plain-text numbered list.',
29
+ { question, type },
30
+ );
31
+ }
20
32
  const line = await _emitClaudeMarkerBlock({ type, question, options, def });
21
33
  return { value: _parseAnswer(type, line, options, def), source: 'askUserQuestion' };
22
34
  }
@@ -99,3 +99,38 @@ test('claude-adapter: exports runtimeNotice compatible with agents-md SC-5 check
99
99
  test('claude-adapter: runtimeNotice does not contain the forbidden joined Claude-tool literal (SC-5 guard)', () => {
100
100
  assert.ok(!/Ask-User-Question/.test(claude.runtimeNotice));
101
101
  });
102
+
103
+ test('claude-adapter: askUser without TTY and without default throws askuser-no-tty', async () => {
104
+ const originalIsTTY = process.stdin.isTTY;
105
+ try {
106
+ Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
107
+ _setReadlineImplForTests(null);
108
+ await assert.rejects(
109
+ () => claude.askUser({ type: 'select', question: 'P', options: ['A', 'B'] }),
110
+ (err) => err && err.code === 'askuser-no-tty',
111
+ );
112
+ } finally {
113
+ if (originalIsTTY === undefined) {
114
+ delete process.stdin.isTTY;
115
+ } else {
116
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
117
+ }
118
+ }
119
+ });
120
+
121
+ test('claude-adapter: askUser without TTY but with default returns default', async () => {
122
+ const originalIsTTY = process.stdin.isTTY;
123
+ try {
124
+ Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
125
+ _setReadlineImplForTests(null);
126
+ const res = await claude.askUser({ type: 'confirm', question: 'OK?', default: true });
127
+ assert.equal(res.value, true);
128
+ assert.equal(res.source, 'default');
129
+ } finally {
130
+ if (originalIsTTY === undefined) {
131
+ delete process.stdin.isTTY;
132
+ } else {
133
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
134
+ }
135
+ }
136
+ });
package/np-tools.cjs CHANGED
@@ -45,6 +45,7 @@ const topLevelCommands = {
45
45
  'metrics': require('./bin/np-tools/metrics.cjs'),
46
46
  'resolve-model': require('./bin/np-tools/resolve-model.cjs'),
47
47
  'stats': require('./bin/np-tools/stats.cjs'),
48
+ 'lang-directive': require('./bin/np-tools/lang-directive.cjs'),
48
49
  };
49
50
 
50
51
  const THRESHOLD = 16 * 1024;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "AI-driven planning and execution tool for code projects",
5
5
  "homepage": "https://github.com/Nubos-AI/nubos-pilot",
6
6
  "repository": {
@@ -19,10 +19,17 @@ workflow delivers PLAN-01 and nothing beyond it.
19
19
  ## Initialize
20
20
 
21
21
  ```bash
22
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
22
23
  INIT=$(node .nubos-pilot/bin/np-tools.cjs init discuss-phase "$PHASE")
23
24
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
24
25
  ```
25
26
 
27
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
28
+ `$LANG_DIRECTIVE` is authoritative for this workflow. Obey it for ALL
29
+ subsequent output, askuser prompt texts, status updates, and the CONTEXT.md
30
+ rendering. This supersedes any directive in CLAUDE.md managed block if they
31
+ conflict — the config is the single source of truth.
32
+
26
33
  Parse JSON for: `milestone`, `milestone_id`, `milestone_dir`, `milestone_name`,
27
34
  `milestone_context_path`, `has_context`, `has_milestone_dir`, `goal`,
28
35
  `requirements`, `agent_skills`, `mode`.
@@ -118,11 +125,16 @@ Capture the idea in a "Deferred Ideas" section. Don't lose it, don't act on it.
118
125
  ## Answer Validation
119
126
 
120
127
  <answer_validation>
121
- **IMPORTANT: Answer validation** — After every interactive prompt, check if the
122
- response is empty or whitespace-only. If so:
123
- 1. Retry the question once with the same parameters
124
- 2. If still empty, present the options as a plain-text numbered list and ask
125
- the user to type their choice number
128
+ **IMPORTANT: Answer validation** — After every interactive prompt, check the
129
+ exit code and the response:
130
+ 1. If `askuser` exits with structured error `askuser-no-tty` (exit code 1,
131
+ stderr JSON with `"code":"askuser-no-tty"`), **skip retry** and fall back
132
+ immediately to the plain-text numbered list described below — the runtime
133
+ cannot prompt interactively in this session.
134
+ 2. If the response is empty or whitespace-only (exit 0 but no value), retry
135
+ the question once with the same parameters.
136
+ 3. If still empty, present the options as a plain-text numbered list and ask
137
+ the user to type their choice number.
126
138
  Never proceed with an empty answer.
127
139
 
128
140
  **Text mode (`workflow.text_mode: true` in config or `--text` flag):**
@@ -51,10 +51,16 @@ Never:
51
51
  ## Single-Call Init
52
52
 
53
53
  ```bash
54
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
54
55
  INIT=$(node .nubos-pilot/bin/np-tools.cjs init discuss-project ${BOOTSTRAP:+--bootstrap})
55
56
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
56
57
  ```
57
58
 
59
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
60
+ `$LANG_DIRECTIVE` is authoritative. Obey it for all askuser prompt texts,
61
+ narrative status updates, and the prose written into PROJECT.md sections.
62
+ Supersedes CLAUDE.md managed block.
63
+
58
64
  Parse: `mode`, `sub_mode` (`bootstrap` or `refresh`), `project_md_exists`,
59
65
  `scan_context`, `questions[]`, `required_fields[]`.
60
66
 
@@ -16,12 +16,20 @@ Execute every slice of a milestone in wave order: slice S001 first (all its task
16
16
 
17
17
  ```bash
18
18
  PHASE="$1"
19
- INIT=$(node .nubos-pilot/bin/np-tools.cjs init execute-milestone "$PHASE")
19
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
20
+ INIT=$(node .nubos-pilot/bin/np-tools.cjs init execute-milestone init "$PHASE")
20
21
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
21
22
  AGENT_SKILLS_EXECUTOR=$(node .nubos-pilot/bin/np-tools.cjs agent-skills executor 2>/dev/null)
22
23
  RUNTIME=$(node -e "console.log(require('./lib/runtime/index.cjs').detect().runtime)")
23
24
  ```
24
25
 
26
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
27
+ `$LANG_DIRECTIVE` is authoritative for this workflow. Obey it for all user-
28
+ facing output, askuser prompts, and status updates. Pass `$LANG_DIRECTIVE`
29
+ into every np-executor spawn prompt as a system-level rule so task summaries
30
+ and checkpoint notes follow the project language. This supersedes any
31
+ directive in CLAUDE.md managed block.
32
+
25
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`, `agent_skills`.
26
34
 
27
35
  `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`).
@@ -52,10 +52,16 @@ The subcommand raises `project-not-initialized` anyway, but the shell check give
52
52
  ## Single-Call Init
53
53
 
54
54
  ```bash
55
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
55
56
  INIT=$(node .nubos-pilot/bin/np-tools.cjs init new-milestone)
56
57
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
57
58
  ```
58
59
 
60
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
61
+ `$LANG_DIRECTIVE` is authoritative. Obey it for askuser prompt texts,
62
+ user-facing output, and any prose written into milestone artefacts (YAML
63
+ keys, IDs, and identifiers stay canonical English). Supersedes CLAUDE.md.
64
+
59
65
  Payload: three questions — `milestone_name`, `milestone_goal`, `create_req_prefix` (confirm).
60
66
 
61
67
  ## Interview
@@ -106,10 +106,17 @@ Use the scan to propose:
106
106
  The 5 structural questions. All prompts go through the askuser gateway.
107
107
 
108
108
  ```bash
109
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
109
110
  INIT=$(node .nubos-pilot/bin/np-tools.cjs init new-project)
110
111
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
111
112
  ```
112
113
 
114
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
115
+ `$LANG_DIRECTIVE` is authoritative. Obey it for all askuser prompt texts,
116
+ user-facing output, and any narrative prose written into PROJECT.md /
117
+ REQUIREMENTS.md (field names and YAML keys stay canonical English).
118
+ Supersedes CLAUDE.md.
119
+
113
120
  ```bash
114
121
  ANS_PROJECT_NAME=$(node .nubos-pilot/bin/np-tools.cjs askuser --json '{"type":"input","prompt":"Project name?"}')
115
122
  ANS_CORE_VALUE=$(node .nubos-pilot/bin/np-tools.cjs askuser --json '{"type":"input","prompt":"Core value — one sentence that must stay true if everything else fails?"}')
@@ -15,9 +15,14 @@ progress.
15
15
  ## Execution
16
16
 
17
17
  ```bash
18
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
18
19
  node .nubos-pilot/bin/np-tools.cjs init pause-work
19
20
  ```
20
21
 
22
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
23
+ Obey `$LANG_DIRECTIVE` for the resume-hint narration and any status lines
24
+ printed around the JSON payload. Supersedes CLAUDE.md.
25
+
21
26
  Output is a small JSON payload `{ ok, stopped_at, resume_file }`. The
22
27
  workflow simply displays it.
23
28
 
@@ -72,6 +72,7 @@ fi
72
72
  ### Read milestone state
73
73
 
74
74
  ```bash
75
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
75
76
  INIT=$(node .nubos-pilot/bin/np-tools.cjs init plan-milestone init "$PHASE")
76
77
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
77
78
  AGENT_SKILLS_PLANNER=$(node .nubos-pilot/bin/np-tools.cjs agent-skills planner 2>/dev/null)
@@ -79,6 +80,12 @@ AGENT_SKILLS_CHECKER=$(node .nubos-pilot/bin/np-tools.cjs agent-skills plan-chec
79
80
  RUNTIME=$(node -e "console.log(require('./lib/runtime/index.cjs').detect().runtime)")
80
81
  ```
81
82
 
83
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
84
+ `$LANG_DIRECTIVE` is authoritative. Obey it for all user-facing output,
85
+ askuser prompts, status updates, and any narrative text the spawned planner
86
+ or plan-checker subagents emit. Pass `$LANG_DIRECTIVE` into their spawn
87
+ prompts as a system-level rule. This supersedes any directive in CLAUDE.md.
88
+
82
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`, `agent_skills`.
83
90
 
84
91
  `PLAN_ID` and `TASK_ID` default to `${milestone_id}-plan` / `${milestone_id}-planner-run` for the metrics records.
@@ -92,11 +92,18 @@ payload; larger payloads are written to a tmp file and referenced via
92
92
  `@file:<path>`.
93
93
 
94
94
  ```bash
95
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
95
96
  INIT=$(node .nubos-pilot/bin/np-tools.cjs init research-phase "$PHASE")
96
97
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
97
98
  RUNTIME=$(node -e "console.log(require('./lib/runtime/index.cjs').detect().runtime)")
98
99
  ```
99
100
 
101
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
102
+ `$LANG_DIRECTIVE` is authoritative. Obey it for user-facing output and
103
+ askuser prompts, and pass it into the np-researcher spawn prompt so
104
+ RESEARCH.md prose (not URLs, citations, or code snippets) follows the
105
+ project language. This supersedes CLAUDE.md.
106
+
100
107
  `RUNTIME` is resolved once here and reused by the metrics-record call at the
101
108
  researcher spawn site (Step 4) per D-06 workflow-writer pattern.
102
109
 
@@ -13,10 +13,17 @@ on each accordingly.
13
13
  ## Initialize
14
14
 
15
15
  ```bash
16
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
16
17
  INIT=$(node .nubos-pilot/bin/np-tools.cjs init resume-work)
17
18
  STATUS=$(echo "$INIT" | node -e "process.stdin.on('data', d => console.log(JSON.parse(d).status))")
18
19
  ```
19
20
 
21
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
22
+ `$LANG_DIRECTIVE` is authoritative. Obey it for user-facing output and
23
+ askuser prompts. When spawning the np-executor to continue a checkpoint,
24
+ pass `$LANG_DIRECTIVE` into the spawn prompt so resumed task summaries
25
+ follow the project language. Supersedes CLAUDE.md.
26
+
20
27
  ## Execution
21
28
 
22
29
  ### status: resume
@@ -60,10 +60,18 @@ end.
60
60
  Scan, group, write manifest + stubs, emit module-facts in one call:
61
61
 
62
62
  ```bash
63
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
63
64
  PLAN=$(node .nubos-pilot/bin/np-tools.cjs scan-codebase --project-name "$PROJECT_NAME")
64
65
  if [[ "$PLAN" == @file:* ]]; then PLAN=$(cat "${PLAN#@file:}"); fi
65
66
  ```
66
67
 
68
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
69
+ `$LANG_DIRECTIVE` is authoritative. Obey it for user-facing output and
70
+ askuser prompts. Pass it into any writer-subagent spawn so module-doc
71
+ prose (overview, responsibilities, notes) follows the project language.
72
+ Module IDs, file paths, symbol names, and language labels stay canonical
73
+ English. Supersedes CLAUDE.md.
74
+
67
75
  `--project-name` is optional; when provided it goes into `INDEX.md`. Other
68
76
  flags: `--batch-size N` (default 500), `--max-files N`.
69
77
 
@@ -32,6 +32,7 @@ record. Pitfall 9 / `workflow-missing-metrics` is exempt.
32
32
  ## Initialize
33
33
 
34
34
  ```bash
35
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
35
36
  SINCE_OVERRIDE=""
36
37
  for arg in "$@"; do
37
38
  case "$arg" in
@@ -49,6 +50,12 @@ LOCAL_FILENAME_TS=$(date +"%Y-%m-%dT%H%M")
49
50
  REPORT_PATH="${REPORTS_DIR}/${LOCAL_FILENAME_TS}-session-report.md"
50
51
  ```
51
52
 
53
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
54
+ `$LANG_DIRECTIVE` is authoritative. Obey it for the report's narrative
55
+ sections (summary, highlights, notable events) and any askuser prompts.
56
+ Task IDs, milestone IDs, commit SHAs, metrics keys, and file paths stay
57
+ canonical English. Supersedes CLAUDE.md.
58
+
52
59
  The filename format is `YYYY-MM-DDTHHMM-session-report.md` (D-17 —
53
60
  4-char HHMM, no seconds, local time) so reports sort
54
61
  lexicographically by invocation order.
@@ -51,10 +51,17 @@ If `.doc-index.json` is missing, fall back to a full rescan via
51
51
  ## Single-Call Init
52
52
 
53
53
  ```bash
54
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
54
55
  PLAN=$(node .nubos-pilot/bin/np-tools.cjs update-docs)
55
56
  if [[ "$PLAN" == @file:* ]]; then PLAN=$(cat "${PLAN#@file:}"); fi
56
57
  ```
57
58
 
59
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
60
+ `$LANG_DIRECTIVE` is authoritative. Obey it for user-facing output and
61
+ askuser prompts. Pass it into any writer-subagent spawn so refreshed
62
+ module-doc prose follows the project language. Module IDs, file paths,
63
+ and symbol names stay canonical English. Supersedes CLAUDE.md.
64
+
58
65
  Parse: `mode`, `diff_summary` (added/removed/changed/unchanged counts),
59
66
  `stale_modules[]`, `added_modules[]`, `removed_modules[]`.
60
67
 
@@ -19,11 +19,18 @@ if [[ -z "$PHASE" ]]; then
19
19
  exit 2
20
20
  fi
21
21
 
22
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
22
23
  INIT=$(node .nubos-pilot/bin/np-tools.cjs init verify-work "$PHASE")
23
24
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
24
25
  RUNTIME=$(node -e "console.log(require('./lib/runtime/index.cjs').detect().runtime)")
25
26
  ```
26
27
 
28
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
29
+ `$LANG_DIRECTIVE` is authoritative. Obey it for user-facing output, askuser
30
+ prompts, and pass it into the np-nyquist-auditor spawn prompt so gap-fill
31
+ narrative follows the project language. Test IDs, file paths, and canonical
32
+ field names stay English. Supersedes CLAUDE.md.
33
+
27
34
  Parse JSON for: `milestone`, `milestone_id`, `milestone_dir`, `milestone_name`, `slice_uat`.
28
35
 
29
36
  ```bash
@@ -16,11 +16,19 @@ Slice-level acceptance (UAT) is validated separately by `/np:validate-phase <N>`
16
16
 
17
17
  ```bash
18
18
  PHASE="$1"
19
+ LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
19
20
  INIT=$(node .nubos-pilot/bin/np-tools.cjs init verify-work "$PHASE")
20
21
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
21
22
  AGENT_SKILLS_VERIFIER=$(node .nubos-pilot/bin/np-tools.cjs agent-skills verifier 2>/dev/null)
22
23
  ```
23
24
 
25
+ **Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
26
+ `$LANG_DIRECTIVE` is authoritative. Obey it for user-facing output, askuser
27
+ prompts, and pass it into the np-verifier spawn prompt so VERIFICATION.md
28
+ prose (Pass/Fail findings, root-cause notes) follows the project language.
29
+ Test-case IDs, file paths, and stack traces stay canonical. Supersedes
30
+ CLAUDE.md.
31
+
24
32
  Parse: `milestone`, `milestone_id`, `milestone_dir`, `milestone_name`, `success_criteria`, `draft_results`, `verification_path`, `slice_uat`, `verifier_tier`, `agent_skills`.
25
33
 
26
34
  ## Pass 1 — verifier agent