nubos-pilot 0.5.3 → 0.5.4
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/np-tools/_commands.cjs +1 -0
- package/bin/np-tools/add-tests.cjs +4 -0
- package/bin/np-tools/add-todo.cjs +4 -0
- package/bin/np-tools/discuss-phase.cjs +5 -0
- package/bin/np-tools/discuss-phase.test.cjs +75 -18
- package/bin/np-tools/discuss-project.cjs +5 -0
- package/bin/np-tools/execute-milestone.cjs +5 -0
- package/bin/np-tools/new-milestone.cjs +6 -2
- package/bin/np-tools/new-project.cjs +6 -2
- package/bin/np-tools/plan-milestone.cjs +5 -0
- package/bin/np-tools/research-phase.cjs +5 -0
- package/bin/np-tools/resume-work.cjs +5 -0
- package/bin/np-tools/text-mode.cjs +56 -0
- package/bin/np-tools/text-mode.test.cjs +132 -0
- package/bin/np-tools/verify-work.cjs +5 -0
- package/lib/runtime/claude.cjs +10 -2
- package/lib/runtime/claude.test.cjs +28 -0
- package/lib/text-mode.cjs +79 -0
- package/lib/text-mode.test.cjs +139 -0
- package/np-tools.cjs +1 -0
- package/package.json +1 -1
- package/workflows/add-todo.md +10 -5
- package/workflows/discuss-phase.md +37 -17
- package/workflows/execute-phase.md +7 -1
- package/workflows/new-milestone.md +6 -0
- package/workflows/new-project.md +6 -0
- package/workflows/note.md +11 -0
- package/workflows/plan-phase.md +6 -1
- package/workflows/research-phase.md +5 -0
- package/workflows/resume-work.md +5 -0
- package/workflows/session-report.md +11 -0
- package/workflows/validate-phase.md +6 -1
- package/workflows/verify-work.md +7 -1
|
@@ -44,6 +44,7 @@ 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)' },
|
|
49
50
|
|
|
@@ -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
|
}
|
|
@@ -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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { resolveTextModeDetail } = require('../../lib/text-mode.cjs');
|
|
4
|
+
|
|
5
|
+
function _usage() {
|
|
6
|
+
return 'Usage:\n np-tools.cjs text-mode [--json]';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function _emitError(err, stderr) {
|
|
10
|
+
const code = err && err.name === 'NubosPilotError' ? err.code : 'text-mode-internal-error';
|
|
11
|
+
const message = (err && err.message) || String(err);
|
|
12
|
+
const details = (err && err.details) || null;
|
|
13
|
+
stderr.write(JSON.stringify({ code, message, details }) + '\n');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function run(argv, ctx) {
|
|
17
|
+
const context = ctx || {};
|
|
18
|
+
const cwd = context.cwd || process.cwd();
|
|
19
|
+
const stdout = context.stdout || process.stdout;
|
|
20
|
+
const stderr = context.stderr || process.stderr;
|
|
21
|
+
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
22
|
+
|
|
23
|
+
let wantJson = false;
|
|
24
|
+
for (const a of args) {
|
|
25
|
+
if (a === '--json') { wantJson = true; continue; }
|
|
26
|
+
if (a === '-h' || a === '--help') {
|
|
27
|
+
stdout.write(_usage() + '\n');
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
stderr.write(JSON.stringify({
|
|
31
|
+
code: 'text-mode-unknown-arg',
|
|
32
|
+
message: 'Unknown argument: ' + a,
|
|
33
|
+
details: { arg: a },
|
|
34
|
+
}) + '\n');
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const detail = resolveTextModeDetail(cwd);
|
|
40
|
+
if (wantJson) {
|
|
41
|
+
stdout.write(JSON.stringify(detail) + '\n');
|
|
42
|
+
} else {
|
|
43
|
+
stdout.write(String(detail.enabled) + '\n');
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
_emitError(err, stderr);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { run };
|
|
53
|
+
|
|
54
|
+
if (require.main === module) {
|
|
55
|
+
process.exit(run(process.argv.slice(2)));
|
|
56
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
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('./text-mode.cjs');
|
|
10
|
+
|
|
11
|
+
function _mkSandbox() {
|
|
12
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-text-mode-cli-'));
|
|
13
|
+
fs.mkdirSync(path.join(dir, '.nubos-pilot'), { recursive: true });
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function _captureIO() {
|
|
18
|
+
const out = [];
|
|
19
|
+
const err = [];
|
|
20
|
+
return {
|
|
21
|
+
stdout: { write: (s) => { out.push(String(s)); return true; } },
|
|
22
|
+
stderr: { write: (s) => { err.push(String(s)); return true; } },
|
|
23
|
+
stdoutText: () => out.join(''),
|
|
24
|
+
stderrText: () => err.join(''),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function _clearClaudeEnv() {
|
|
29
|
+
const saved = {};
|
|
30
|
+
for (const k of ['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT']) {
|
|
31
|
+
saved[k] = process.env[k];
|
|
32
|
+
delete process.env[k];
|
|
33
|
+
}
|
|
34
|
+
return () => {
|
|
35
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
36
|
+
if (v === undefined) delete process.env[k];
|
|
37
|
+
else process.env[k] = v;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('text-mode CLI: default without config and without Claude env prints "false"', () => {
|
|
43
|
+
const restore = _clearClaudeEnv();
|
|
44
|
+
try {
|
|
45
|
+
const dir = _mkSandbox();
|
|
46
|
+
try {
|
|
47
|
+
const io = _captureIO();
|
|
48
|
+
const rc = subcmd.run([], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
49
|
+
assert.equal(rc, 0);
|
|
50
|
+
assert.equal(io.stdoutText().trim(), 'false');
|
|
51
|
+
} finally {
|
|
52
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
} finally {
|
|
55
|
+
restore();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('text-mode CLI: CLAUDECODE=1 prints "true"', () => {
|
|
60
|
+
const restore = _clearClaudeEnv();
|
|
61
|
+
try {
|
|
62
|
+
process.env.CLAUDECODE = '1';
|
|
63
|
+
const dir = _mkSandbox();
|
|
64
|
+
try {
|
|
65
|
+
const io = _captureIO();
|
|
66
|
+
const rc = subcmd.run([], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
67
|
+
assert.equal(rc, 0);
|
|
68
|
+
assert.equal(io.stdoutText().trim(), 'true');
|
|
69
|
+
} finally {
|
|
70
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
restore();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('text-mode CLI: --json emits detail object', () => {
|
|
78
|
+
const restore = _clearClaudeEnv();
|
|
79
|
+
try {
|
|
80
|
+
process.env.CLAUDECODE = '1';
|
|
81
|
+
const dir = _mkSandbox();
|
|
82
|
+
try {
|
|
83
|
+
const io = _captureIO();
|
|
84
|
+
const rc = subcmd.run(['--json'], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
85
|
+
assert.equal(rc, 0);
|
|
86
|
+
const payload = JSON.parse(io.stdoutText().trim());
|
|
87
|
+
assert.equal(payload.enabled, true);
|
|
88
|
+
assert.equal(payload.source, 'runtime');
|
|
89
|
+
} finally {
|
|
90
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
restore();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('text-mode CLI: config workflow.text_mode=false wins over CLAUDECODE', () => {
|
|
98
|
+
const restore = _clearClaudeEnv();
|
|
99
|
+
try {
|
|
100
|
+
process.env.CLAUDECODE = '1';
|
|
101
|
+
const dir = _mkSandbox();
|
|
102
|
+
try {
|
|
103
|
+
fs.writeFileSync(
|
|
104
|
+
path.join(dir, '.nubos-pilot', 'config.json'),
|
|
105
|
+
JSON.stringify({ workflow: { text_mode: false } }),
|
|
106
|
+
);
|
|
107
|
+
const io = _captureIO();
|
|
108
|
+
const rc = subcmd.run(['--json'], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
109
|
+
assert.equal(rc, 0);
|
|
110
|
+
const payload = JSON.parse(io.stdoutText().trim());
|
|
111
|
+
assert.equal(payload.enabled, false);
|
|
112
|
+
assert.equal(payload.source, 'config');
|
|
113
|
+
} finally {
|
|
114
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
restore();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('text-mode CLI: unknown flag exits 1 with structured error', () => {
|
|
122
|
+
const dir = _mkSandbox();
|
|
123
|
+
try {
|
|
124
|
+
const io = _captureIO();
|
|
125
|
+
const rc = subcmd.run(['--wat'], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
126
|
+
assert.equal(rc, 1);
|
|
127
|
+
const payload = JSON.parse(io.stderrText().trim());
|
|
128
|
+
assert.equal(payload.code, 'text-mode-unknown-arg');
|
|
129
|
+
} finally {
|
|
130
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
milestoneVerificationPath,
|
|
20
20
|
} = require('../../lib/verify.cjs');
|
|
21
21
|
const { getAgentSkills } = require('../../lib/agents.cjs');
|
|
22
|
+
const textMode = require('../../lib/text-mode.cjs');
|
|
22
23
|
|
|
23
24
|
const INLINE_THRESHOLD_BYTES = 16 * 1024;
|
|
24
25
|
const _VALID_SC_STATUSES = new Set(['Pass', 'Fail', 'Defer', 'Pending']);
|
|
@@ -90,6 +91,8 @@ function _initPayload(mNum, cwd) {
|
|
|
90
91
|
};
|
|
91
92
|
});
|
|
92
93
|
|
|
94
|
+
const tmDetail = textMode.resolveTextModeDetail(cwd);
|
|
95
|
+
|
|
93
96
|
return {
|
|
94
97
|
_workflow: 'verify-work',
|
|
95
98
|
milestone: mNum,
|
|
@@ -101,6 +104,8 @@ function _initPayload(mNum, cwd) {
|
|
|
101
104
|
verification_path: verificationPath,
|
|
102
105
|
slice_uat: sliceUat,
|
|
103
106
|
verifier_tier: 'sonnet',
|
|
107
|
+
text_mode: tmDetail.enabled,
|
|
108
|
+
text_mode_source: tmDetail.source,
|
|
104
109
|
agent_skills: { verifier: _safeSkills('np-verifier', cwd) },
|
|
105
110
|
};
|
|
106
111
|
}
|
package/lib/runtime/claude.cjs
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
_readOneLine,
|
|
3
|
+
_parseAnswer,
|
|
4
|
+
_hasReadlineImplForTests,
|
|
5
|
+
askUserReadline,
|
|
6
|
+
} = require('./_readline.cjs');
|
|
2
7
|
const { NubosPilotError } = require('../core.cjs');
|
|
3
8
|
|
|
4
9
|
function _emitClaudeMarkerBlock({ type, question, options, def }) {
|
|
@@ -19,7 +24,10 @@ async function askUser(spec) {
|
|
|
19
24
|
const options = spec && spec.options;
|
|
20
25
|
const def = spec ? spec.default : undefined;
|
|
21
26
|
const hasTTY = !!process.stdin.isTTY;
|
|
22
|
-
if (
|
|
27
|
+
if (hasTTY) {
|
|
28
|
+
return askUserReadline({ type, question, options, def });
|
|
29
|
+
}
|
|
30
|
+
if (!_hasReadlineImplForTests()) {
|
|
23
31
|
if (def !== undefined && def !== null) {
|
|
24
32
|
return { value: def, source: 'default' };
|
|
25
33
|
}
|
|
@@ -118,6 +118,34 @@ test('claude-adapter: askUser without TTY and without default throws askuser-no-
|
|
|
118
118
|
}
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
test('claude-adapter: askUser with TTY stdin uses readline UI, no marker block', async () => {
|
|
122
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
123
|
+
_setReadlineImplForTests(async () => '2');
|
|
124
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
125
|
+
const stderrChunks = [];
|
|
126
|
+
process.stderr.write = (chunk) => { stderrChunks.push(String(chunk)); return true; };
|
|
127
|
+
try {
|
|
128
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
|
129
|
+
const { val, out } = await captureStdout(() =>
|
|
130
|
+
claude.askUser({ type: 'select', question: 'Q', options: ['A', 'B', 'C'] })
|
|
131
|
+
);
|
|
132
|
+
assert.ok(!/<!-- askUser v1 -->/.test(out),
|
|
133
|
+
'must not emit marker block when stdin is TTY');
|
|
134
|
+
assert.equal(val.source, 'readline');
|
|
135
|
+
assert.equal(val.value, 'B');
|
|
136
|
+
const stderrJoined = stderrChunks.join('');
|
|
137
|
+
assert.match(stderrJoined, /Q/, 'readline UI should render the question on stderr');
|
|
138
|
+
} finally {
|
|
139
|
+
process.stderr.write = originalStderrWrite;
|
|
140
|
+
_setReadlineImplForTests(null);
|
|
141
|
+
if (originalIsTTY === undefined) {
|
|
142
|
+
delete process.stdin.isTTY;
|
|
143
|
+
} else {
|
|
144
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
121
149
|
test('claude-adapter: askUser without TTY but with default returns default', async () => {
|
|
122
150
|
const originalIsTTY = process.stdin.isTTY;
|
|
123
151
|
try {
|
|
@@ -0,0 +1,79 @@
|
|
|
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 DEFAULT_TEXT_MODE = false;
|
|
8
|
+
const CLAUDE_ENV_KEYS = ['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT'];
|
|
9
|
+
|
|
10
|
+
function _coerceBool(raw) {
|
|
11
|
+
if (raw === true || raw === false) return raw;
|
|
12
|
+
if (raw == null) return null;
|
|
13
|
+
const s = String(raw).trim().toLowerCase();
|
|
14
|
+
if (s === 'true' || s === '1' || s === 'yes' || s === 'on') return true;
|
|
15
|
+
if (s === 'false' || s === '0' || s === 'no' || s === 'off') return false;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readConfigTextMode(cwd) {
|
|
20
|
+
let root;
|
|
21
|
+
try {
|
|
22
|
+
root = findProjectRoot(cwd || process.cwd());
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err && err.code === 'not-in-project') return null;
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
const p = path.join(root, '.nubos-pilot', 'config.json');
|
|
28
|
+
if (!fs.existsSync(p)) return null;
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw new NubosPilotError('text-mode-config-parse-error', 'config.json invalid JSON', { cause: err && err.message });
|
|
34
|
+
}
|
|
35
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
36
|
+
const workflow = parsed.workflow;
|
|
37
|
+
if (!workflow || typeof workflow !== 'object') return null;
|
|
38
|
+
if (!Object.prototype.hasOwnProperty.call(workflow, 'text_mode')) return null;
|
|
39
|
+
const coerced = _coerceBool(workflow.text_mode);
|
|
40
|
+
return coerced;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function detectRuntimeTextMode(env) {
|
|
44
|
+
const source = env || process.env;
|
|
45
|
+
for (const key of CLAUDE_ENV_KEYS) {
|
|
46
|
+
const v = source[key];
|
|
47
|
+
if (v != null && String(v) !== '' && String(v) !== '0' && String(v).toLowerCase() !== 'false') {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveTextMode(cwd, env) {
|
|
55
|
+
const fromConfig = readConfigTextMode(cwd);
|
|
56
|
+
if (fromConfig !== null) return fromConfig;
|
|
57
|
+
if (detectRuntimeTextMode(env)) return true;
|
|
58
|
+
return DEFAULT_TEXT_MODE;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveTextModeDetail(cwd, env) {
|
|
62
|
+
const fromConfig = readConfigTextMode(cwd);
|
|
63
|
+
if (fromConfig !== null) {
|
|
64
|
+
return { enabled: fromConfig, source: 'config' };
|
|
65
|
+
}
|
|
66
|
+
if (detectRuntimeTextMode(env)) {
|
|
67
|
+
return { enabled: true, source: 'runtime' };
|
|
68
|
+
}
|
|
69
|
+
return { enabled: DEFAULT_TEXT_MODE, source: 'default' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
DEFAULT_TEXT_MODE,
|
|
74
|
+
CLAUDE_ENV_KEYS,
|
|
75
|
+
readConfigTextMode,
|
|
76
|
+
detectRuntimeTextMode,
|
|
77
|
+
resolveTextMode,
|
|
78
|
+
resolveTextModeDetail,
|
|
79
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
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 tm = require('./text-mode.cjs');
|
|
10
|
+
|
|
11
|
+
function _mkSandbox() {
|
|
12
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-textmode-'));
|
|
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('text-mode: default without config or runtime env is false', () => {
|
|
22
|
+
const dir = _mkSandbox();
|
|
23
|
+
try {
|
|
24
|
+
assert.equal(tm.resolveTextMode(dir, {}), false);
|
|
25
|
+
const detail = tm.resolveTextModeDetail(dir, {});
|
|
26
|
+
assert.deepEqual(detail, { enabled: false, source: 'default' });
|
|
27
|
+
} finally {
|
|
28
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('text-mode: CLAUDECODE=1 in env flips default to true', () => {
|
|
33
|
+
const dir = _mkSandbox();
|
|
34
|
+
try {
|
|
35
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }), true);
|
|
36
|
+
const detail = tm.resolveTextModeDetail(dir, { CLAUDECODE: '1' });
|
|
37
|
+
assert.deepEqual(detail, { enabled: true, source: 'runtime' });
|
|
38
|
+
} finally {
|
|
39
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('text-mode: CLAUDE_CODE_ENTRYPOINT non-empty flips to true', () => {
|
|
44
|
+
const dir = _mkSandbox();
|
|
45
|
+
try {
|
|
46
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDE_CODE_ENTRYPOINT: 'cli' }), true);
|
|
47
|
+
} finally {
|
|
48
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('text-mode: env value "0" or "false" does not flip', () => {
|
|
53
|
+
const dir = _mkSandbox();
|
|
54
|
+
try {
|
|
55
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '0' }), false);
|
|
56
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: 'false' }), false);
|
|
57
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '' }), false);
|
|
58
|
+
} finally {
|
|
59
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('text-mode: config workflow.text_mode=true wins over absent runtime', () => {
|
|
64
|
+
const dir = _mkSandbox();
|
|
65
|
+
try {
|
|
66
|
+
_writeConfig(dir, { workflow: { text_mode: true } });
|
|
67
|
+
assert.equal(tm.resolveTextMode(dir, {}), true);
|
|
68
|
+
assert.deepEqual(tm.resolveTextModeDetail(dir, {}), { enabled: true, source: 'config' });
|
|
69
|
+
} finally {
|
|
70
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('text-mode: config workflow.text_mode=false overrides CLAUDECODE runtime', () => {
|
|
75
|
+
const dir = _mkSandbox();
|
|
76
|
+
try {
|
|
77
|
+
_writeConfig(dir, { workflow: { text_mode: false } });
|
|
78
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }), false);
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
tm.resolveTextModeDetail(dir, { CLAUDECODE: '1' }),
|
|
81
|
+
{ enabled: false, source: 'config' },
|
|
82
|
+
);
|
|
83
|
+
} finally {
|
|
84
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('text-mode: config workflow.text_mode="true" string coerced to boolean', () => {
|
|
89
|
+
const dir = _mkSandbox();
|
|
90
|
+
try {
|
|
91
|
+
_writeConfig(dir, { workflow: { text_mode: 'true' } });
|
|
92
|
+
assert.equal(tm.resolveTextMode(dir, {}), true);
|
|
93
|
+
_writeConfig(dir, { workflow: { text_mode: '0' } });
|
|
94
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }), false);
|
|
95
|
+
} finally {
|
|
96
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('text-mode: config missing workflow.text_mode falls through to runtime detection', () => {
|
|
101
|
+
const dir = _mkSandbox();
|
|
102
|
+
try {
|
|
103
|
+
_writeConfig(dir, { response_language: 'de' });
|
|
104
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }), true);
|
|
105
|
+
assert.equal(tm.resolveTextMode(dir, {}), false);
|
|
106
|
+
} finally {
|
|
107
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('text-mode: readConfigTextMode returns null outside project root', () => {
|
|
112
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'np-outside-'));
|
|
113
|
+
try {
|
|
114
|
+
assert.equal(tm.readConfigTextMode(outside), null);
|
|
115
|
+
assert.equal(tm.resolveTextMode(outside, {}), false);
|
|
116
|
+
} finally {
|
|
117
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('text-mode: readConfigTextMode throws on invalid JSON', () => {
|
|
122
|
+
const dir = _mkSandbox();
|
|
123
|
+
try {
|
|
124
|
+
fs.writeFileSync(path.join(dir, '.nubos-pilot', 'config.json'), '{not json');
|
|
125
|
+
assert.throws(
|
|
126
|
+
() => tm.readConfigTextMode(dir),
|
|
127
|
+
(err) => err && err.code === 'text-mode-config-parse-error',
|
|
128
|
+
);
|
|
129
|
+
} finally {
|
|
130
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('text-mode: detectRuntimeTextMode honors multiple env keys', () => {
|
|
135
|
+
assert.equal(tm.detectRuntimeTextMode({ CLAUDECODE: '1' }), true);
|
|
136
|
+
assert.equal(tm.detectRuntimeTextMode({ CLAUDE_CODE_ENTRYPOINT: 'cli' }), true);
|
|
137
|
+
assert.equal(tm.detectRuntimeTextMode({ OTHER: '1' }), false);
|
|
138
|
+
assert.equal(tm.detectRuntimeTextMode({}), false);
|
|
139
|
+
});
|
package/np-tools.cjs
CHANGED
|
@@ -46,6 +46,7 @@ const topLevelCommands = {
|
|
|
46
46
|
'resolve-model': require('./bin/np-tools/resolve-model.cjs'),
|
|
47
47
|
'stats': require('./bin/np-tools/stats.cjs'),
|
|
48
48
|
'lang-directive': require('./bin/np-tools/lang-directive.cjs'),
|
|
49
|
+
'text-mode': require('./bin/np-tools/text-mode.cjs'),
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
const THRESHOLD = 16 * 1024;
|
package/package.json
CHANGED
package/workflows/add-todo.md
CHANGED
|
@@ -41,11 +41,16 @@ TODO_PATH="${PENDING_DIR}/${DATE}-${SLUG}.md"
|
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Extract from init JSON: `commit_docs`, `date`, `timestamp`, `slug`,
|
|
44
|
-
`todo_count`, `todos_dir_exists`, `pending_dir`, `state_path
|
|
45
|
-
init handler sanitises the slug
|
|
46
|
-
(strips to `[a-z0-9-]` only;
|
|
47
|
-
validates the description length
|
|
48
|
-
write occurs.
|
|
44
|
+
`todo_count`, `todos_dir_exists`, `pending_dir`, `state_path`,
|
|
45
|
+
`text_mode`, `text_mode_source`. The init handler sanitises the slug
|
|
46
|
+
through `lib/layout.cjs.slugify` (strips to `[a-z0-9-]` only;
|
|
47
|
+
filename-injection mitigation) and validates the description length
|
|
48
|
+
(<= 500 chars) before any filesystem write occurs.
|
|
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`.
|
|
49
54
|
|
|
50
55
|
## Create Pending Dir
|
|
51
56
|
|
|
@@ -32,7 +32,24 @@ conflict — the config is the single source of truth.
|
|
|
32
32
|
|
|
33
33
|
Parse JSON for: `milestone`, `milestone_id`, `milestone_dir`, `milestone_name`,
|
|
34
34
|
`milestone_context_path`, `has_context`, `has_milestone_dir`, `goal`,
|
|
35
|
-
`requirements`, `agent_skills`, `mode`.
|
|
35
|
+
`requirements`, `agent_skills`, `mode`, `text_mode`, `text_mode_source`.
|
|
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.
|
|
36
53
|
|
|
37
54
|
If the user passed `--assumptions`, route to
|
|
38
55
|
`workflows/discuss-phase-assumptions.md` and exit this workflow.
|
|
@@ -125,30 +142,33 @@ Capture the idea in a "Deferred Ideas" section. Don't lose it, don't act on it.
|
|
|
125
142
|
## Answer Validation
|
|
126
143
|
|
|
127
144
|
<answer_validation>
|
|
128
|
-
**
|
|
129
|
-
|
|
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:**
|
|
130
152
|
1. If `askuser` exits with structured error `askuser-no-tty` (exit code 1,
|
|
131
|
-
stderr JSON with `"code":"askuser-no-tty"`),
|
|
132
|
-
|
|
133
|
-
|
|
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).
|
|
134
156
|
2. If the response is empty or whitespace-only (exit 0 but no value), retry
|
|
135
157
|
the question once with the same parameters.
|
|
136
158
|
3. If still empty, present the options as a plain-text numbered list and ask
|
|
137
159
|
the user to type their choice number.
|
|
138
160
|
Never proceed with an empty answer.
|
|
139
161
|
|
|
140
|
-
**
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
Enable text mode:
|
|
148
|
-
- Per-session: pass `--text` flag
|
|
149
|
-
- Per-project: `np-tools.cjs config-set workflow.text_mode true`
|
|
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).
|
|
150
169
|
|
|
151
|
-
Text mode applies to ALL workflows
|
|
170
|
+
Text mode applies to ALL workflows that emit `text_mode` in their INIT
|
|
171
|
+
payload, not just discuss-phase.
|
|
152
172
|
</answer_validation>
|
|
153
173
|
|
|
154
174
|
## Process
|
|
@@ -30,7 +30,13 @@ into every np-executor spawn prompt as a system-level rule so task summaries
|
|
|
30
30
|
and checkpoint notes follow the project language. This supersedes any
|
|
31
31
|
directive in CLAUDE.md managed block.
|
|
32
32
|
|
|
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`.
|
|
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
|
+
|
|
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`.
|
|
34
40
|
|
|
35
41
|
`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`).
|
|
36
42
|
|
|
@@ -62,6 +62,12 @@ 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`.
|
|
70
|
+
|
|
65
71
|
Payload: three questions — `milestone_name`, `milestone_goal`, `create_req_prefix` (confirm).
|
|
66
72
|
|
|
67
73
|
## Interview
|
package/workflows/new-project.md
CHANGED
|
@@ -117,6 +117,12 @@ user-facing output, and any narrative prose written into PROJECT.md /
|
|
|
117
117
|
REQUIREMENTS.md (field names and YAML keys stay canonical English).
|
|
118
118
|
Supersedes CLAUDE.md.
|
|
119
119
|
|
|
120
|
+
**Text-mode routing.** If INIT payload `text_mode == true`, skip every
|
|
121
|
+
`np-tools.cjs askuser` call below and render each question as a plain-text
|
|
122
|
+
prompt in the main chat; collect the user's answer inline and move on.
|
|
123
|
+
Auto-enabled in Claude Code (CLAUDECODE=1); opt-in via
|
|
124
|
+
`.nubos-pilot/config.json` → `workflow.text_mode`.
|
|
125
|
+
|
|
120
126
|
```bash
|
|
121
127
|
ANS_PROJECT_NAME=$(node .nubos-pilot/bin/np-tools.cjs askuser --json '{"type":"input","prompt":"Project name?"}')
|
|
122
128
|
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?"}')
|
package/workflows/note.md
CHANGED
|
@@ -52,6 +52,17 @@ 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:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
TEXT_MODE=$(node .nubos-pilot/bin/np-tools.cjs text-mode 2>/dev/null || echo false)
|
|
59
|
+
```
|
|
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`.
|
|
65
|
+
|
|
55
66
|
## Compute Paths
|
|
56
67
|
|
|
57
68
|
Scope branching is explicit (T-10-05-02 mitigation + Pitfall 10
|
package/workflows/plan-phase.md
CHANGED
|
@@ -86,7 +86,12 @@ askuser prompts, status updates, and any narrative text the spawned planner
|
|
|
86
86
|
or plan-checker subagents emit. Pass `$LANG_DIRECTIVE` into their spawn
|
|
87
87
|
prompts as a system-level rule. This supersedes any directive in CLAUDE.md.
|
|
88
88
|
|
|
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`.
|
|
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
|
+
|
|
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`.
|
|
90
95
|
|
|
91
96
|
`PLAN_ID` and `TASK_ID` default to `${milestone_id}-plan` / `${milestone_id}-planner-run` for the metrics records.
|
|
92
97
|
|
|
@@ -107,6 +107,11 @@ 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`.
|
|
114
|
+
|
|
110
115
|
The payload shape:
|
|
111
116
|
|
|
112
117
|
```json
|
package/workflows/resume-work.md
CHANGED
|
@@ -24,6 +24,11 @@ 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`.
|
|
31
|
+
|
|
27
32
|
## Execution
|
|
28
33
|
|
|
29
34
|
### status: resume
|
|
@@ -56,6 +56,17 @@ sections (summary, highlights, notable events) and any askuser prompts.
|
|
|
56
56
|
Task IDs, milestone IDs, commit SHAs, metrics keys, and file paths stay
|
|
57
57
|
canonical English. Supersedes CLAUDE.md.
|
|
58
58
|
|
|
59
|
+
**Text-mode routing.** Resolve once:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
TEXT_MODE=$(node .nubos-pilot/bin/np-tools.cjs text-mode 2>/dev/null || echo false)
|
|
63
|
+
```
|
|
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`.
|
|
69
|
+
|
|
59
70
|
The filename format is `YYYY-MM-DDTHHMM-session-report.md` (D-17 —
|
|
60
71
|
4-char HHMM, no seconds, local time) so reports sort
|
|
61
72
|
lexicographically by invocation order.
|
|
@@ -31,7 +31,12 @@ prompts, and pass it into the np-nyquist-auditor spawn prompt so gap-fill
|
|
|
31
31
|
narrative follows the project language. Test IDs, file paths, and canonical
|
|
32
32
|
field names stay English. Supersedes CLAUDE.md.
|
|
33
33
|
|
|
34
|
-
Parse JSON for: `milestone`, `milestone_id`, `milestone_dir`, `milestone_name`, `slice_uat`.
|
|
34
|
+
Parse JSON for: `milestone`, `milestone_id`, `milestone_dir`, `milestone_name`, `slice_uat`, `text_mode`, `text_mode_source`.
|
|
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`.
|
|
35
40
|
|
|
36
41
|
```bash
|
|
37
42
|
MILESTONE_ID=$(echo "$INIT" | jq -r '.milestone_id')
|
package/workflows/verify-work.md
CHANGED
|
@@ -29,7 +29,13 @@ prose (Pass/Fail findings, root-cause notes) follows the project language.
|
|
|
29
29
|
Test-case IDs, file paths, and stack traces stay canonical. Supersedes
|
|
30
30
|
CLAUDE.md.
|
|
31
31
|
|
|
32
|
-
Parse: `milestone`, `milestone_id`, `milestone_dir`, `milestone_name`, `success_criteria`, `draft_results`, `verification_path`, `slice_uat`, `verifier_tier`, `agent_skills`.
|
|
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
|
+
|
|
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`.
|
|
33
39
|
|
|
34
40
|
## Pass 1 — verifier agent
|
|
35
41
|
|