nubos-pilot 0.6.0 → 0.6.1
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 +68 -0
- package/bin/np-tools/discuss-phase.test.cjs +3 -3
- package/bin/np-tools/text-mode.test.cjs +9 -6
- package/lib/install/claude-hooks.cjs +195 -0
- package/lib/install/claude-hooks.test.cjs +163 -0
- package/lib/text-mode.cjs +1 -1
- package/lib/text-mode.test.cjs +11 -22
- package/package.json +1 -1
- package/templates/claude/payload/hooks/np-ctx-monitor.js +94 -0
- package/templates/claude/payload/hooks/np-statusline.js +110 -0
- package/workflows/add-todo.md +4 -4
- package/workflows/discuss-phase.md +24 -40
- package/workflows/discuss-project.md +5 -0
- package/workflows/execute-phase.md +4 -5
- package/workflows/new-milestone.md +4 -5
- package/workflows/new-project.md +4 -5
- package/workflows/note.md +5 -5
- package/workflows/plan-phase.md +4 -4
- package/workflows/propose-milestones.md +4 -1
- package/workflows/research-phase.md +4 -4
- package/workflows/resume-work.md +4 -4
- package/workflows/session-report.md +4 -4
- package/workflows/validate-phase.md +4 -4
- package/workflows/verify-work.md +4 -5
package/bin/install.js
CHANGED
|
@@ -683,6 +683,10 @@ async function main() {
|
|
|
683
683
|
const doctor = require('./np-tools/doctor.cjs');
|
|
684
684
|
return await doctor.run(rest.slice(1), { cwd, stdout: process.stdout });
|
|
685
685
|
}
|
|
686
|
+
case 'install-hooks':
|
|
687
|
+
return await runInstallHooks({ cwd, args: rest.slice(1) });
|
|
688
|
+
case 'uninstall-hooks':
|
|
689
|
+
return await runUninstallHooks({ cwd, args: rest.slice(1) });
|
|
686
690
|
default:
|
|
687
691
|
process.stderr.write(
|
|
688
692
|
red + 'Unbekanntes Subcommand: ' + sub + reset + '\n',
|
|
@@ -692,6 +696,70 @@ async function main() {
|
|
|
692
696
|
}
|
|
693
697
|
}
|
|
694
698
|
|
|
699
|
+
function _parseHookFlags(args) {
|
|
700
|
+
const flags = { scope: null, which: 'both', force: false, dryRun: false };
|
|
701
|
+
for (let i = 0; i < args.length; i++) {
|
|
702
|
+
const a = args[i];
|
|
703
|
+
if (a === '--scope' || a === '-s') { flags.scope = args[++i] || null; continue; }
|
|
704
|
+
if (a.startsWith('--scope=')) { flags.scope = a.slice('--scope='.length); continue; }
|
|
705
|
+
if (a === '--statusline-only') { flags.which = 'statusline'; continue; }
|
|
706
|
+
if (a === '--ctx-monitor-only') { flags.which = 'ctx-monitor'; continue; }
|
|
707
|
+
if (a === '--force' || a === '-f') { flags.force = true; continue; }
|
|
708
|
+
if (a === '--dry-run') { flags.dryRun = true; continue; }
|
|
709
|
+
}
|
|
710
|
+
if (flags.scope && !VALID_SCOPES.includes(flags.scope)) {
|
|
711
|
+
throw new NubosPilotError('invalid-flag',
|
|
712
|
+
'--scope must be one of: ' + VALID_SCOPES.join(', '),
|
|
713
|
+
{ flag: '--scope', got: flags.scope });
|
|
714
|
+
}
|
|
715
|
+
return flags;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function runInstallHooks(opts) {
|
|
719
|
+
const o = opts || {};
|
|
720
|
+
const projectRoot = o.projectRoot || o.cwd || process.cwd();
|
|
721
|
+
const flags = _parseHookFlags(o.args || []);
|
|
722
|
+
const scope = flags.scope || _readExistingScope(projectRoot) || 'local';
|
|
723
|
+
const claudeHooks = require('../lib/install/claude-hooks.cjs');
|
|
724
|
+
const res = claudeHooks.installClaudeHooks({
|
|
725
|
+
projectRoot, scope, which: flags.which, force: flags.force, dryRun: flags.dryRun,
|
|
726
|
+
});
|
|
727
|
+
if (res.dryRun) {
|
|
728
|
+
process.stdout.write(JSON.stringify({ dryRun: true, path: res.path, results: res.results }, null, 2) + '\n');
|
|
729
|
+
return res;
|
|
730
|
+
}
|
|
731
|
+
console.error(green + '✓ Claude Code hooks geschrieben → ' + res.path + reset);
|
|
732
|
+
if (res.results.statusline) {
|
|
733
|
+
console.error(dim + ' statusline: ' + res.results.statusline.action
|
|
734
|
+
+ (res.results.statusline.existingCommand ? ' (existing: ' + res.results.statusline.existingCommand + ')' : '')
|
|
735
|
+
+ reset);
|
|
736
|
+
}
|
|
737
|
+
if (res.results.ctxMonitor) {
|
|
738
|
+
console.error(dim + ' ctx-monitor: ' + res.results.ctxMonitor.action + reset);
|
|
739
|
+
}
|
|
740
|
+
if (res.results.statusline && res.results.statusline.action === 'skipped-existing') {
|
|
741
|
+
console.error(yellow + ' [statusline] existing non-nubos statusLine preserved. Pass --force to overwrite.' + reset);
|
|
742
|
+
}
|
|
743
|
+
return res;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function runUninstallHooks(opts) {
|
|
747
|
+
const o = opts || {};
|
|
748
|
+
const projectRoot = o.projectRoot || o.cwd || process.cwd();
|
|
749
|
+
const flags = _parseHookFlags(o.args || []);
|
|
750
|
+
const scope = flags.scope || _readExistingScope(projectRoot) || 'local';
|
|
751
|
+
const claudeHooks = require('../lib/install/claude-hooks.cjs');
|
|
752
|
+
const res = claudeHooks.uninstallClaudeHooks({ projectRoot, scope, dryRun: flags.dryRun });
|
|
753
|
+
if (res.dryRun) {
|
|
754
|
+
process.stdout.write(JSON.stringify({ dryRun: true, path: res.path, results: res.results }, null, 2) + '\n');
|
|
755
|
+
return res;
|
|
756
|
+
}
|
|
757
|
+
console.error(green + '✓ Claude Code hooks entfernt ← ' + res.path + reset);
|
|
758
|
+
console.error(dim + ' statusline: ' + res.results.statusline.action + reset);
|
|
759
|
+
console.error(dim + ' ctx-monitor: ' + res.results.ctxMonitor.action + reset);
|
|
760
|
+
return res;
|
|
761
|
+
}
|
|
762
|
+
|
|
695
763
|
if (require.main === module) {
|
|
696
764
|
main().catch((err) => {
|
|
697
765
|
if (err && err.code) {
|
|
@@ -83,7 +83,7 @@ test('DP-1: run(["3"]) on valid milestone returns JSON payload with expected sha
|
|
|
83
83
|
}
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
test('DP-1b: CLAUDECODE=1
|
|
86
|
+
test('DP-1b: CLAUDECODE=1 no longer flips text_mode (Claude Code uses AskUserQuestion)', () => {
|
|
87
87
|
const restore = _clearClaudeEnv();
|
|
88
88
|
try {
|
|
89
89
|
process.env.CLAUDECODE = '1';
|
|
@@ -92,8 +92,8 @@ test('DP-1b: CLAUDECODE=1 sets text_mode=true with runtime source', () => {
|
|
|
92
92
|
const cap = _captureStdout();
|
|
93
93
|
subcmd.run(['3'], { cwd: sandbox, stdout: cap.stub });
|
|
94
94
|
const payload = JSON.parse(cap.get().trim());
|
|
95
|
-
assert.equal(payload.text_mode,
|
|
96
|
-
assert.equal(payload.text_mode_source, '
|
|
95
|
+
assert.equal(payload.text_mode, false);
|
|
96
|
+
assert.equal(payload.text_mode_source, 'default');
|
|
97
97
|
} finally {
|
|
98
98
|
restore();
|
|
99
99
|
}
|
|
@@ -56,7 +56,7 @@ test('text-mode CLI: default without config and without Claude env prints "false
|
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
test('text-mode CLI: CLAUDECODE=1 prints "true"', () => {
|
|
59
|
+
test('text-mode CLI: CLAUDECODE=1 no longer prints "true" (AskUserQuestion path)', () => {
|
|
60
60
|
const restore = _clearClaudeEnv();
|
|
61
61
|
try {
|
|
62
62
|
process.env.CLAUDECODE = '1';
|
|
@@ -65,7 +65,7 @@ test('text-mode CLI: CLAUDECODE=1 prints "true"', () => {
|
|
|
65
65
|
const io = _captureIO();
|
|
66
66
|
const rc = subcmd.run([], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
67
67
|
assert.equal(rc, 0);
|
|
68
|
-
assert.equal(io.stdoutText().trim(), '
|
|
68
|
+
assert.equal(io.stdoutText().trim(), 'false');
|
|
69
69
|
} finally {
|
|
70
70
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
71
71
|
}
|
|
@@ -74,18 +74,21 @@ test('text-mode CLI: CLAUDECODE=1 prints "true"', () => {
|
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
test('text-mode CLI: --json emits detail object', () => {
|
|
77
|
+
test('text-mode CLI: --json emits detail object with config source', () => {
|
|
78
78
|
const restore = _clearClaudeEnv();
|
|
79
79
|
try {
|
|
80
|
-
process.env.CLAUDECODE = '1';
|
|
81
80
|
const dir = _mkSandbox();
|
|
82
81
|
try {
|
|
82
|
+
fs.writeFileSync(
|
|
83
|
+
path.join(dir, '.nubos-pilot', 'config.json'),
|
|
84
|
+
JSON.stringify({ workflow: { text_mode: true } }),
|
|
85
|
+
);
|
|
83
86
|
const io = _captureIO();
|
|
84
87
|
const rc = subcmd.run(['--json'], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
85
88
|
assert.equal(rc, 0);
|
|
86
89
|
const payload = JSON.parse(io.stdoutText().trim());
|
|
87
90
|
assert.equal(payload.enabled, true);
|
|
88
|
-
assert.equal(payload.source, '
|
|
91
|
+
assert.equal(payload.source, 'config');
|
|
89
92
|
} finally {
|
|
90
93
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
91
94
|
}
|
|
@@ -94,7 +97,7 @@ test('text-mode CLI: --json emits detail object', () => {
|
|
|
94
97
|
}
|
|
95
98
|
});
|
|
96
99
|
|
|
97
|
-
test('text-mode CLI: config workflow.text_mode=false
|
|
100
|
+
test('text-mode CLI: config workflow.text_mode=false stays false even with CLAUDECODE', () => {
|
|
98
101
|
const restore = _clearClaudeEnv();
|
|
99
102
|
try {
|
|
100
103
|
process.env.CLAUDECODE = '1';
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
|
|
7
|
+
const { atomicWriteFileSync, NubosPilotError } = require('../core.cjs');
|
|
8
|
+
|
|
9
|
+
const STATUSLINE_REL = '.claude/nubos-pilot/hooks/np-statusline.js';
|
|
10
|
+
const CTX_MONITOR_REL = '.claude/nubos-pilot/hooks/np-ctx-monitor.js';
|
|
11
|
+
const NP_STATUSLINE_MARKER = 'np-statusline.js';
|
|
12
|
+
const NP_CTX_MONITOR_MARKER = 'np-ctx-monitor.js';
|
|
13
|
+
|
|
14
|
+
function _settingsPath(scope, projectRoot) {
|
|
15
|
+
if (scope === 'global') return path.join(os.homedir(), '.claude', 'settings.json');
|
|
16
|
+
return path.join(projectRoot, '.claude', 'settings.local.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _readJsonSafe(p) {
|
|
20
|
+
if (!fs.existsSync(p)) return {};
|
|
21
|
+
let raw;
|
|
22
|
+
try { raw = fs.readFileSync(p, 'utf-8'); } catch { return {}; }
|
|
23
|
+
try { return JSON.parse(raw); } catch (err) {
|
|
24
|
+
throw new NubosPilotError(
|
|
25
|
+
'claude-settings-invalid-json',
|
|
26
|
+
'Cannot parse Claude settings: ' + p + ' — ' + err.message,
|
|
27
|
+
{ path: p },
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _hookCommand(rel, scope, projectRoot) {
|
|
33
|
+
if (scope === 'global') {
|
|
34
|
+
return 'node "' + path.join('$HOME', rel) + '"';
|
|
35
|
+
}
|
|
36
|
+
return 'node "' + path.join(projectRoot, rel) + '"';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _containsNpHook(entry, marker) {
|
|
40
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
41
|
+
const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
42
|
+
for (const h of hooks) {
|
|
43
|
+
if (h && typeof h.command === 'string' && h.command.includes(marker)) return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _installStatusLine(settings, cmd, force) {
|
|
49
|
+
const existing = settings.statusLine;
|
|
50
|
+
if (existing && typeof existing === 'object' && existing.command) {
|
|
51
|
+
if (String(existing.command).includes(NP_STATUSLINE_MARKER)) {
|
|
52
|
+
settings.statusLine = { type: 'command', command: cmd };
|
|
53
|
+
return { action: 'updated', existed: true };
|
|
54
|
+
}
|
|
55
|
+
if (!force) {
|
|
56
|
+
return { action: 'skipped-existing', existed: true, existingCommand: existing.command };
|
|
57
|
+
}
|
|
58
|
+
settings.statusLine = { type: 'command', command: cmd };
|
|
59
|
+
return { action: 'overwrote', existed: true };
|
|
60
|
+
}
|
|
61
|
+
settings.statusLine = { type: 'command', command: cmd };
|
|
62
|
+
return { action: 'installed', existed: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _installPostToolUse(settings, cmd) {
|
|
66
|
+
if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
|
|
67
|
+
if (!Array.isArray(settings.hooks.PostToolUse)) settings.hooks.PostToolUse = [];
|
|
68
|
+
const list = settings.hooks.PostToolUse;
|
|
69
|
+
for (const entry of list) {
|
|
70
|
+
if (_containsNpHook(entry, NP_CTX_MONITOR_MARKER)) {
|
|
71
|
+
const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
72
|
+
for (const h of hooks) {
|
|
73
|
+
if (h && typeof h.command === 'string' && h.command.includes(NP_CTX_MONITOR_MARKER)) {
|
|
74
|
+
h.command = cmd;
|
|
75
|
+
h.type = 'command';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { action: 'updated' };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
list.push({
|
|
82
|
+
matcher: '.*',
|
|
83
|
+
hooks: [{ type: 'command', command: cmd }],
|
|
84
|
+
});
|
|
85
|
+
return { action: 'installed' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _removeStatusLine(settings) {
|
|
89
|
+
const existing = settings.statusLine;
|
|
90
|
+
if (existing && typeof existing === 'object'
|
|
91
|
+
&& typeof existing.command === 'string'
|
|
92
|
+
&& existing.command.includes(NP_STATUSLINE_MARKER)) {
|
|
93
|
+
delete settings.statusLine;
|
|
94
|
+
return { action: 'removed' };
|
|
95
|
+
}
|
|
96
|
+
return { action: 'not-ours' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _removePostToolUse(settings) {
|
|
100
|
+
if (!settings.hooks || !Array.isArray(settings.hooks.PostToolUse)) return { action: 'absent' };
|
|
101
|
+
const filtered = [];
|
|
102
|
+
for (const entry of settings.hooks.PostToolUse) {
|
|
103
|
+
if (_containsNpHook(entry, NP_CTX_MONITOR_MARKER)) {
|
|
104
|
+
const hooks = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
105
|
+
const keptHooks = hooks.filter((h) => !(h && typeof h.command === 'string' && h.command.includes(NP_CTX_MONITOR_MARKER)));
|
|
106
|
+
if (keptHooks.length > 0) {
|
|
107
|
+
filtered.push(Object.assign({}, entry, { hooks: keptHooks }));
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
filtered.push(entry);
|
|
112
|
+
}
|
|
113
|
+
settings.hooks.PostToolUse = filtered;
|
|
114
|
+
if (filtered.length === 0) delete settings.hooks.PostToolUse;
|
|
115
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
116
|
+
return { action: 'removed' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function installClaudeHooks(opts) {
|
|
120
|
+
const o = opts || {};
|
|
121
|
+
const projectRoot = o.projectRoot || process.cwd();
|
|
122
|
+
const scope = o.scope === 'global' ? 'global' : 'local';
|
|
123
|
+
const force = !!o.force;
|
|
124
|
+
const which = o.which || 'both';
|
|
125
|
+
const settingsPath = _settingsPath(scope, projectRoot);
|
|
126
|
+
|
|
127
|
+
const statuslineCmd = _hookCommand(STATUSLINE_REL, scope, projectRoot);
|
|
128
|
+
const ctxMonitorCmd = _hookCommand(CTX_MONITOR_REL, scope, projectRoot);
|
|
129
|
+
|
|
130
|
+
const statuslineAbs = path.join(scope === 'global' ? os.homedir() : projectRoot, STATUSLINE_REL);
|
|
131
|
+
const ctxMonitorAbs = path.join(scope === 'global' ? os.homedir() : projectRoot, CTX_MONITOR_REL);
|
|
132
|
+
|
|
133
|
+
if (which === 'statusline' || which === 'both') {
|
|
134
|
+
if (!fs.existsSync(statuslineAbs)) {
|
|
135
|
+
throw new NubosPilotError(
|
|
136
|
+
'claude-hooks-script-missing',
|
|
137
|
+
'Statusline hook script not found: ' + statuslineAbs + '. Run `npx nubos-pilot` install first.',
|
|
138
|
+
{ script: statuslineAbs },
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (which === 'ctx-monitor' || which === 'both') {
|
|
143
|
+
if (!fs.existsSync(ctxMonitorAbs)) {
|
|
144
|
+
throw new NubosPilotError(
|
|
145
|
+
'claude-hooks-script-missing',
|
|
146
|
+
'Ctx-monitor hook script not found: ' + ctxMonitorAbs,
|
|
147
|
+
{ script: ctxMonitorAbs },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const settings = _readJsonSafe(settingsPath);
|
|
153
|
+
const results = {};
|
|
154
|
+
|
|
155
|
+
if (which === 'statusline' || which === 'both') {
|
|
156
|
+
results.statusline = _installStatusLine(settings, statuslineCmd, force);
|
|
157
|
+
}
|
|
158
|
+
if (which === 'ctx-monitor' || which === 'both') {
|
|
159
|
+
results.ctxMonitor = _installPostToolUse(settings, ctxMonitorCmd);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (o.dryRun) return { dryRun: true, path: settingsPath, results, settings };
|
|
163
|
+
|
|
164
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
165
|
+
atomicWriteFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
166
|
+
return { dryRun: false, path: settingsPath, results };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function uninstallClaudeHooks(opts) {
|
|
170
|
+
const o = opts || {};
|
|
171
|
+
const projectRoot = o.projectRoot || process.cwd();
|
|
172
|
+
const scope = o.scope === 'global' ? 'global' : 'local';
|
|
173
|
+
const settingsPath = _settingsPath(scope, projectRoot);
|
|
174
|
+
if (!fs.existsSync(settingsPath)) return { path: settingsPath, results: { statusline: { action: 'absent' }, ctxMonitor: { action: 'absent' } } };
|
|
175
|
+
|
|
176
|
+
const settings = _readJsonSafe(settingsPath);
|
|
177
|
+
const results = {
|
|
178
|
+
statusline: _removeStatusLine(settings),
|
|
179
|
+
ctxMonitor: _removePostToolUse(settings),
|
|
180
|
+
};
|
|
181
|
+
if (o.dryRun) return { dryRun: true, path: settingsPath, results, settings };
|
|
182
|
+
atomicWriteFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
183
|
+
return { dryRun: false, path: settingsPath, results };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
installClaudeHooks,
|
|
188
|
+
uninstallClaudeHooks,
|
|
189
|
+
STATUSLINE_REL,
|
|
190
|
+
CTX_MONITOR_REL,
|
|
191
|
+
NP_STATUSLINE_MARKER,
|
|
192
|
+
NP_CTX_MONITOR_MARKER,
|
|
193
|
+
_settingsPath,
|
|
194
|
+
_hookCommand,
|
|
195
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
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 mod = require('./claude-hooks.cjs');
|
|
10
|
+
|
|
11
|
+
function _mkSandbox() {
|
|
12
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-claude-hooks-'));
|
|
13
|
+
fs.mkdirSync(path.join(dir, '.claude'), { recursive: true });
|
|
14
|
+
fs.mkdirSync(path.join(dir, '.claude', 'nubos-pilot', 'hooks'), { recursive: true });
|
|
15
|
+
fs.writeFileSync(path.join(dir, '.claude', 'nubos-pilot', 'hooks', 'np-statusline.js'), '// stub\n');
|
|
16
|
+
fs.writeFileSync(path.join(dir, '.claude', 'nubos-pilot', 'hooks', 'np-ctx-monitor.js'), '// stub\n');
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('claude-hooks: fresh install writes both hooks to local settings', () => {
|
|
21
|
+
const dir = _mkSandbox();
|
|
22
|
+
try {
|
|
23
|
+
const res = mod.installClaudeHooks({ projectRoot: dir, scope: 'local' });
|
|
24
|
+
assert.equal(res.dryRun, false);
|
|
25
|
+
assert.equal(res.results.statusline.action, 'installed');
|
|
26
|
+
assert.equal(res.results.ctxMonitor.action, 'installed');
|
|
27
|
+
const settings = JSON.parse(fs.readFileSync(res.path, 'utf-8'));
|
|
28
|
+
assert.equal(settings.statusLine.type, 'command');
|
|
29
|
+
assert.ok(settings.statusLine.command.includes('np-statusline.js'));
|
|
30
|
+
assert.ok(Array.isArray(settings.hooks.PostToolUse));
|
|
31
|
+
assert.equal(settings.hooks.PostToolUse[0].matcher, '.*');
|
|
32
|
+
assert.ok(settings.hooks.PostToolUse[0].hooks[0].command.includes('np-ctx-monitor.js'));
|
|
33
|
+
} finally {
|
|
34
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('claude-hooks: existing foreign statusLine is preserved without force', () => {
|
|
39
|
+
const dir = _mkSandbox();
|
|
40
|
+
try {
|
|
41
|
+
const settingsPath = path.join(dir, '.claude', 'settings.local.json');
|
|
42
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
43
|
+
statusLine: { type: 'command', command: 'echo my-custom-bar' },
|
|
44
|
+
}));
|
|
45
|
+
const res = mod.installClaudeHooks({ projectRoot: dir, scope: 'local' });
|
|
46
|
+
assert.equal(res.results.statusline.action, 'skipped-existing');
|
|
47
|
+
assert.equal(res.results.statusline.existingCommand, 'echo my-custom-bar');
|
|
48
|
+
const settings = JSON.parse(fs.readFileSync(res.path, 'utf-8'));
|
|
49
|
+
assert.equal(settings.statusLine.command, 'echo my-custom-bar');
|
|
50
|
+
} finally {
|
|
51
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('claude-hooks: --force overwrites foreign statusLine', () => {
|
|
56
|
+
const dir = _mkSandbox();
|
|
57
|
+
try {
|
|
58
|
+
const settingsPath = path.join(dir, '.claude', 'settings.local.json');
|
|
59
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
60
|
+
statusLine: { type: 'command', command: 'echo other' },
|
|
61
|
+
}));
|
|
62
|
+
const res = mod.installClaudeHooks({ projectRoot: dir, scope: 'local', force: true });
|
|
63
|
+
assert.equal(res.results.statusline.action, 'overwrote');
|
|
64
|
+
const settings = JSON.parse(fs.readFileSync(res.path, 'utf-8'));
|
|
65
|
+
assert.ok(settings.statusLine.command.includes('np-statusline.js'));
|
|
66
|
+
} finally {
|
|
67
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('claude-hooks: re-install is idempotent (updates nubos-pilot hook path)', () => {
|
|
72
|
+
const dir = _mkSandbox();
|
|
73
|
+
try {
|
|
74
|
+
mod.installClaudeHooks({ projectRoot: dir, scope: 'local' });
|
|
75
|
+
const res2 = mod.installClaudeHooks({ projectRoot: dir, scope: 'local' });
|
|
76
|
+
assert.equal(res2.results.statusline.action, 'updated');
|
|
77
|
+
assert.equal(res2.results.ctxMonitor.action, 'updated');
|
|
78
|
+
const settings = JSON.parse(fs.readFileSync(res2.path, 'utf-8'));
|
|
79
|
+
assert.equal(settings.hooks.PostToolUse.length, 1);
|
|
80
|
+
} finally {
|
|
81
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('claude-hooks: preserves unrelated PostToolUse hooks', () => {
|
|
86
|
+
const dir = _mkSandbox();
|
|
87
|
+
try {
|
|
88
|
+
const settingsPath = path.join(dir, '.claude', 'settings.local.json');
|
|
89
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
90
|
+
hooks: {
|
|
91
|
+
PostToolUse: [
|
|
92
|
+
{ matcher: 'Bash', hooks: [{ type: 'command', command: 'echo other-hook' }] },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
const res = mod.installClaudeHooks({ projectRoot: dir, scope: 'local', which: 'ctx-monitor' });
|
|
97
|
+
assert.equal(res.results.ctxMonitor.action, 'installed');
|
|
98
|
+
const settings = JSON.parse(fs.readFileSync(res.path, 'utf-8'));
|
|
99
|
+
assert.equal(settings.hooks.PostToolUse.length, 2);
|
|
100
|
+
assert.equal(settings.hooks.PostToolUse[0].matcher, 'Bash');
|
|
101
|
+
assert.ok(settings.hooks.PostToolUse[1].hooks[0].command.includes('np-ctx-monitor.js'));
|
|
102
|
+
} finally {
|
|
103
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('claude-hooks: uninstall removes only our entries', () => {
|
|
108
|
+
const dir = _mkSandbox();
|
|
109
|
+
try {
|
|
110
|
+
const settingsPath = path.join(dir, '.claude', 'settings.local.json');
|
|
111
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
112
|
+
statusLine: { type: 'command', command: 'echo custom' },
|
|
113
|
+
hooks: { PostToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'echo foreign' }] }] },
|
|
114
|
+
}));
|
|
115
|
+
mod.installClaudeHooks({ projectRoot: dir, scope: 'local', which: 'ctx-monitor' });
|
|
116
|
+
const res = mod.uninstallClaudeHooks({ projectRoot: dir, scope: 'local' });
|
|
117
|
+
assert.equal(res.results.ctxMonitor.action, 'removed');
|
|
118
|
+
assert.equal(res.results.statusline.action, 'not-ours');
|
|
119
|
+
const settings = JSON.parse(fs.readFileSync(res.path, 'utf-8'));
|
|
120
|
+
assert.equal(settings.statusLine.command, 'echo custom');
|
|
121
|
+
assert.equal(settings.hooks.PostToolUse.length, 1);
|
|
122
|
+
assert.equal(settings.hooks.PostToolUse[0].matcher, 'Bash');
|
|
123
|
+
} finally {
|
|
124
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('claude-hooks: missing hook script throws structured error', () => {
|
|
129
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-claude-hooks-no-scripts-'));
|
|
130
|
+
try {
|
|
131
|
+
assert.throws(
|
|
132
|
+
() => mod.installClaudeHooks({ projectRoot: dir, scope: 'local' }),
|
|
133
|
+
(err) => err && err.code === 'claude-hooks-script-missing',
|
|
134
|
+
);
|
|
135
|
+
} finally {
|
|
136
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('claude-hooks: dryRun returns planned settings without writing', () => {
|
|
141
|
+
const dir = _mkSandbox();
|
|
142
|
+
try {
|
|
143
|
+
const res = mod.installClaudeHooks({ projectRoot: dir, scope: 'local', dryRun: true });
|
|
144
|
+
assert.equal(res.dryRun, true);
|
|
145
|
+
assert.ok(res.settings.statusLine);
|
|
146
|
+
assert.equal(fs.existsSync(res.path), false);
|
|
147
|
+
} finally {
|
|
148
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('claude-hooks: invalid JSON in settings yields structured error', () => {
|
|
153
|
+
const dir = _mkSandbox();
|
|
154
|
+
try {
|
|
155
|
+
fs.writeFileSync(path.join(dir, '.claude', 'settings.local.json'), '{broken');
|
|
156
|
+
assert.throws(
|
|
157
|
+
() => mod.installClaudeHooks({ projectRoot: dir, scope: 'local' }),
|
|
158
|
+
(err) => err && err.code === 'claude-settings-invalid-json',
|
|
159
|
+
);
|
|
160
|
+
} finally {
|
|
161
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
package/lib/text-mode.cjs
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('node:path');
|
|
|
5
5
|
const { findProjectRoot, NubosPilotError } = require('./core.cjs');
|
|
6
6
|
|
|
7
7
|
const DEFAULT_TEXT_MODE = false;
|
|
8
|
-
const CLAUDE_ENV_KEYS = [
|
|
8
|
+
const CLAUDE_ENV_KEYS = [];
|
|
9
9
|
|
|
10
10
|
function _coerceBool(raw) {
|
|
11
11
|
if (raw === true || raw === false) return raw;
|
package/lib/text-mode.test.cjs
CHANGED
|
@@ -29,32 +29,21 @@ test('text-mode: default without config or runtime env is false', () => {
|
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
test('text-mode: CLAUDECODE=1
|
|
32
|
+
test('text-mode: CLAUDECODE=1 no longer flips to true (Claude Code uses AskUserQuestion)', () => {
|
|
33
33
|
const dir = _mkSandbox();
|
|
34
34
|
try {
|
|
35
|
-
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }),
|
|
35
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }), false);
|
|
36
36
|
const detail = tm.resolveTextModeDetail(dir, { CLAUDECODE: '1' });
|
|
37
|
-
assert.deepEqual(detail, { enabled:
|
|
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);
|
|
37
|
+
assert.deepEqual(detail, { enabled: false, source: 'default' });
|
|
47
38
|
} finally {
|
|
48
39
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
49
40
|
}
|
|
50
41
|
});
|
|
51
42
|
|
|
52
|
-
test('text-mode:
|
|
43
|
+
test('text-mode: CLAUDE_CODE_ENTRYPOINT no longer flips to true', () => {
|
|
53
44
|
const dir = _mkSandbox();
|
|
54
45
|
try {
|
|
55
|
-
assert.equal(tm.resolveTextMode(dir, {
|
|
56
|
-
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: 'false' }), false);
|
|
57
|
-
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '' }), false);
|
|
46
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDE_CODE_ENTRYPOINT: 'cli' }), false);
|
|
58
47
|
} finally {
|
|
59
48
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
60
49
|
}
|
|
@@ -71,7 +60,7 @@ test('text-mode: config workflow.text_mode=true wins over absent runtime', () =>
|
|
|
71
60
|
}
|
|
72
61
|
});
|
|
73
62
|
|
|
74
|
-
test('text-mode: config workflow.text_mode=false
|
|
63
|
+
test('text-mode: config workflow.text_mode=false stays false under CLAUDECODE env', () => {
|
|
75
64
|
const dir = _mkSandbox();
|
|
76
65
|
try {
|
|
77
66
|
_writeConfig(dir, { workflow: { text_mode: false } });
|
|
@@ -97,11 +86,11 @@ test('text-mode: config workflow.text_mode="true" string coerced to boolean', ()
|
|
|
97
86
|
}
|
|
98
87
|
});
|
|
99
88
|
|
|
100
|
-
test('text-mode: config missing workflow.text_mode falls through to
|
|
89
|
+
test('text-mode: config missing workflow.text_mode falls through to default (false)', () => {
|
|
101
90
|
const dir = _mkSandbox();
|
|
102
91
|
try {
|
|
103
92
|
_writeConfig(dir, { response_language: 'de' });
|
|
104
|
-
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }),
|
|
93
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }), false);
|
|
105
94
|
assert.equal(tm.resolveTextMode(dir, {}), false);
|
|
106
95
|
} finally {
|
|
107
96
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -131,9 +120,9 @@ test('text-mode: readConfigTextMode throws on invalid JSON', () => {
|
|
|
131
120
|
}
|
|
132
121
|
});
|
|
133
122
|
|
|
134
|
-
test('text-mode: detectRuntimeTextMode
|
|
135
|
-
assert.equal(tm.detectRuntimeTextMode({ CLAUDECODE: '1' }),
|
|
136
|
-
assert.equal(tm.detectRuntimeTextMode({ CLAUDE_CODE_ENTRYPOINT: 'cli' }),
|
|
123
|
+
test('text-mode: detectRuntimeTextMode returns false for all envs (no runtime auto-flip anymore)', () => {
|
|
124
|
+
assert.equal(tm.detectRuntimeTextMode({ CLAUDECODE: '1' }), false);
|
|
125
|
+
assert.equal(tm.detectRuntimeTextMode({ CLAUDE_CODE_ENTRYPOINT: 'cli' }), false);
|
|
137
126
|
assert.equal(tm.detectRuntimeTextMode({ OTHER: '1' }), false);
|
|
138
127
|
assert.equal(tm.detectRuntimeTextMode({}), false);
|
|
139
128
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
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 WARN_THRESHOLD = 35;
|
|
9
|
+
const CRITICAL_THRESHOLD = 25;
|
|
10
|
+
const DEBOUNCE_TOOLS = 5;
|
|
11
|
+
|
|
12
|
+
function readStdinJson() {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
if (process.stdin.isTTY) return resolve({});
|
|
15
|
+
let buf = '';
|
|
16
|
+
process.stdin.setEncoding('utf-8');
|
|
17
|
+
const timer = setTimeout(() => {
|
|
18
|
+
try { process.stdin.removeAllListeners(); } catch {}
|
|
19
|
+
resolve(safeParse(buf));
|
|
20
|
+
}, 500);
|
|
21
|
+
process.stdin.on('data', (c) => { buf += c; });
|
|
22
|
+
process.stdin.on('end', () => { clearTimeout(timer); resolve(safeParse(buf)); });
|
|
23
|
+
process.stdin.on('error', () => { clearTimeout(timer); resolve(safeParse(buf)); });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeParse(s) {
|
|
28
|
+
try { return s ? JSON.parse(s) : {}; } catch { return {}; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function severityFor(remaining) {
|
|
32
|
+
if (remaining <= CRITICAL_THRESHOLD) return 'critical';
|
|
33
|
+
if (remaining <= WARN_THRESHOLD) return 'warning';
|
|
34
|
+
return 'normal';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sanitizeSid(sid) {
|
|
38
|
+
return String(sid || '').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
(async () => {
|
|
42
|
+
let payload = {};
|
|
43
|
+
try { payload = await readStdinJson(); } catch { payload = {}; }
|
|
44
|
+
const sid = payload && payload.session_id;
|
|
45
|
+
if (!sid) { process.exit(0); return; }
|
|
46
|
+
const safeSid = sanitizeSid(sid);
|
|
47
|
+
const bridgePath = path.join(os.tmpdir(), 'claude-ctx-' + safeSid + '.json');
|
|
48
|
+
if (!fs.existsSync(bridgePath)) { process.exit(0); return; }
|
|
49
|
+
|
|
50
|
+
let bridge;
|
|
51
|
+
try { bridge = JSON.parse(fs.readFileSync(bridgePath, 'utf-8')); } catch { process.exit(0); return; }
|
|
52
|
+
const remaining = Number(bridge && bridge.usable_remaining_pct);
|
|
53
|
+
if (!Number.isFinite(remaining)) { process.exit(0); return; }
|
|
54
|
+
|
|
55
|
+
const statePath = path.join(os.tmpdir(), 'claude-ctx-warn-' + safeSid + '.json');
|
|
56
|
+
let state = { tool_count: 0, last_warn_at: -999, last_severity: 'normal' };
|
|
57
|
+
try {
|
|
58
|
+
const existing = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
59
|
+
state = Object.assign(state, existing);
|
|
60
|
+
} catch {}
|
|
61
|
+
state.tool_count = Number(state.tool_count || 0) + 1;
|
|
62
|
+
|
|
63
|
+
const sev = severityFor(remaining);
|
|
64
|
+
const escalated = sev !== 'normal' && sev !== state.last_severity && state.last_severity !== 'critical';
|
|
65
|
+
const firstWarn = state.last_severity === 'normal' && sev !== 'normal';
|
|
66
|
+
const enoughGap = (state.tool_count - Number(state.last_warn_at || -999)) >= DEBOUNCE_TOOLS;
|
|
67
|
+
|
|
68
|
+
if (sev === 'normal') {
|
|
69
|
+
state.last_severity = 'normal';
|
|
70
|
+
try { fs.writeFileSync(statePath, JSON.stringify(state)); } catch {}
|
|
71
|
+
process.exit(0); return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!firstWarn && !escalated && !enoughGap) {
|
|
75
|
+
try { fs.writeFileSync(statePath, JSON.stringify(state)); } catch {}
|
|
76
|
+
process.exit(0); return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
state.last_warn_at = state.tool_count;
|
|
80
|
+
state.last_severity = sev;
|
|
81
|
+
try { fs.writeFileSync(statePath, JSON.stringify(state)); } catch {}
|
|
82
|
+
|
|
83
|
+
const msg = sev === 'critical'
|
|
84
|
+
? 'CONTEXT CRITICAL: only ' + remaining + '% of usable context remaining. Stop taking new work — save state now with /np:pause-work before autocompact triggers.'
|
|
85
|
+
: 'CONTEXT LOW: ' + remaining + '% of usable context remaining. Wrap up the current task and consider /np:pause-work soon.';
|
|
86
|
+
|
|
87
|
+
const output = {
|
|
88
|
+
hookSpecificOutput: {
|
|
89
|
+
hookEventName: 'PostToolUse',
|
|
90
|
+
additionalContext: '[nubos-pilot ctx-monitor] ' + msg,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
process.stdout.write(JSON.stringify(output));
|
|
94
|
+
})().catch(() => { process.exit(0); });
|
|
@@ -0,0 +1,110 @@
|
|
|
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 writeBridge(payload, usage, limit) {
|
|
80
|
+
const sid = payload && payload.session_id;
|
|
81
|
+
if (!sid) return;
|
|
82
|
+
const bridgePath = path.join(os.tmpdir(), 'claude-ctx-' + String(sid).replace(/[^a-zA-Z0-9._-]/g, '_') + '.json');
|
|
83
|
+
const data = {
|
|
84
|
+
session_id: sid,
|
|
85
|
+
used: usage.total,
|
|
86
|
+
limit: limit,
|
|
87
|
+
usable_remaining_pct: Math.max(0, Math.round(((AUTOCOMPACT_BUFFER * limit) - usage.total) / (AUTOCOMPACT_BUFFER * limit) * 100)),
|
|
88
|
+
updated_at: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
try { fs.writeFileSync(bridgePath, JSON.stringify(data)); } catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
(async () => {
|
|
94
|
+
let payload = {};
|
|
95
|
+
try { payload = await readStdinJson(); } catch { payload = {}; }
|
|
96
|
+
const limit = modelWindow(payload);
|
|
97
|
+
const usage = lastUsage(payload && payload.transcript_path);
|
|
98
|
+
const prefix = '\x1b[38;5;33mnubos-pilot\x1b[0m';
|
|
99
|
+
if (!usage) {
|
|
100
|
+
process.stdout.write(prefix);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
writeBridge(payload, usage, limit);
|
|
104
|
+
const bar = renderBar(usage.total, limit);
|
|
105
|
+
const modelName = (payload && payload.model && payload.model.display_name) || '';
|
|
106
|
+
const tail = modelName ? ' \x1b[2m' + modelName + '\x1b[0m' : '';
|
|
107
|
+
process.stdout.write(prefix + ' ctx ' + bar + tail);
|
|
108
|
+
})().catch(() => {
|
|
109
|
+
process.stdout.write('\x1b[38;5;33mnubos-pilot\x1b[0m');
|
|
110
|
+
});
|
package/workflows/add-todo.md
CHANGED
|
@@ -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
|
-
**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- `
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
**
|
|
36
|
-
|
|
37
|
-
and render
|
|
38
|
-
|
|
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
|
-
**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
package/workflows/new-project.md
CHANGED
|
@@ -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
|
-
**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`
|
|
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
|
|
package/workflows/plan-phase.md
CHANGED
|
@@ -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
|
-
**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
|
@@ -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
|
-
**
|
|
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
|
-
**
|
|
111
|
-
|
|
112
|
-
plain-text numbered lists in
|
|
113
|
-
(
|
|
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
|
|
package/workflows/resume-work.md
CHANGED
|
@@ -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
|
-
**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`
|
|
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
|
-
**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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')
|
package/workflows/verify-work.md
CHANGED
|
@@ -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
|
-
**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|