blun-king-cli 9.1.520 → 9.1.525
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/CHANGELOG.md +32 -0
- package/LIESMICH.txt +4 -2
- package/README.md +4 -2
- package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
- package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
- package/agent-spine-plugin/.codex-plugin/plugin.json +1 -2
- package/agent-spine-plugin/CHANGELOG.md +81 -14
- package/agent-spine-plugin/CONTRIBUTING.md +52 -0
- package/agent-spine-plugin/README.md +6 -4
- package/agent-spine-plugin/SECURITY.md +47 -0
- package/agent-spine-plugin/blun.plugin.json +2 -2
- package/agent-spine-plugin/docs/acceptance.md +2 -2
- package/agent-spine-plugin/docs/architecture.md +1 -1
- package/agent-spine-plugin/docs/gateway-runtime.md +2 -2
- package/agent-spine-plugin/docs/host-integration.md +2 -2
- package/agent-spine-plugin/docs/learning.md +37 -2
- package/agent-spine-plugin/docs/preflight-recall.md +1 -1
- package/agent-spine-plugin/docs/quality-gates.md +1 -1
- package/agent-spine-plugin/docs/relationships.md +1 -1
- package/agent-spine-plugin/docs/session-briefing.md +1 -1
- package/agent-spine-plugin/docs/source-roots.md +1 -1
- package/agent-spine-plugin/hooks/codex.json +1 -1
- package/agent-spine-plugin/hooks/version.json +1 -1
- package/agent-spine-plugin/package.json +1 -1
- package/agent-spine-plugin/scripts/check-hosts.js +2 -2
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +3 -3
- package/agent-spine-plugin/src/cli.js +69 -5
- package/agent-spine-plugin/src/hook.js +41 -23
- package/agent-spine-plugin/src/index.js +2 -1
- package/agent-spine-plugin/src/lib/acceptance.js +2 -3
- package/agent-spine-plugin/src/lib/attention.js +4 -4
- package/agent-spine-plugin/src/lib/audit.js +12 -3
- package/agent-spine-plugin/src/lib/authentication.js +4 -2
- package/agent-spine-plugin/src/lib/briefing.js +20 -8
- package/agent-spine-plugin/src/lib/catalog.js +28 -3
- package/agent-spine-plugin/src/lib/channel-runtime.js +2 -2
- package/agent-spine-plugin/src/lib/continuity.js +18 -13
- package/agent-spine-plugin/src/lib/documents.js +23 -5
- package/agent-spine-plugin/src/lib/feed-transport.js +4 -2
- package/agent-spine-plugin/src/lib/graph.js +47 -12
- package/agent-spine-plugin/src/lib/learning.js +422 -48
- package/agent-spine-plugin/src/lib/paths.js +50 -1
- package/agent-spine-plugin/src/lib/persona-runtime.js +20 -14
- package/agent-spine-plugin/src/lib/preflight.js +50 -26
- package/agent-spine-plugin/src/lib/source-roots.js +38 -7
- package/agent-spine-plugin/src/mcp.js +50 -4
- package/agent-spine-plugin/src/version.js +1 -1
- package/bin/telegram-approval-relay.cjs +2 -0
- package/blun.mjs +111 -8
- package/package.json +8 -2
- package/scripts/check-approval-observability-regression.js +111 -0
- package/scripts/check-bundled-agent-spine-regression.js +48 -0
- package/scripts/check-session-picker-resume-metrics-regression.js +97 -0
- package/scripts/check-session-start-hook-context-regression.js +54 -4
- package/scripts/check-shell-terminal-isolation-regression.js +81 -0
- package/scripts/check-slash-escape-regression.js +89 -0
- package/scripts/check-telegram-loop-exactly-once-regression.js +71 -0
- package/telegram-plugin/bin/telegram-approval-relay.cjs +2 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const vm = require('node:vm');
|
|
7
|
+
const { spawn } = require('node:child_process');
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
10
|
+
const bundlePath = process.argv[2] === undefined
|
|
11
|
+
? path.join(packageRoot, 'blun.mjs')
|
|
12
|
+
: path.resolve(process.argv[2]);
|
|
13
|
+
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
14
|
+
const source = /function buildLocalSpawnOptions\(isWindows, cwd, env\) \{[\s\S]*?\n\}/.exec(bundle)?.[0];
|
|
15
|
+
|
|
16
|
+
assert(source, 'SHELL_TERMINAL_ISOLATION: buildLocalSpawnOptions is missing');
|
|
17
|
+
|
|
18
|
+
const context = {};
|
|
19
|
+
vm.runInNewContext(`${source}; globalThis.buildLocalSpawnOptions = buildLocalSpawnOptions;`, context);
|
|
20
|
+
const buildLocalSpawnOptions = context.buildLocalSpawnOptions;
|
|
21
|
+
const cwd = packageRoot;
|
|
22
|
+
const env = { ...process.env, BLUN_SHELL_ISOLATION_PROBE: '1' };
|
|
23
|
+
|
|
24
|
+
for (const isWindows of [false, true]) {
|
|
25
|
+
const options = buildLocalSpawnOptions(isWindows, cwd, env);
|
|
26
|
+
assert.deepEqual(Array.from(options.stdio), ['pipe', 'pipe', 'pipe']);
|
|
27
|
+
assert.equal(options.detached, true,
|
|
28
|
+
`local shell must start in a separate process group when isWindows=${String(isWindows)}`);
|
|
29
|
+
assert.equal(options.windowsHide, true);
|
|
30
|
+
assert.equal(options.cwd, cwd);
|
|
31
|
+
assert.equal(options.env, env);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function runCapturedProcessProbe() {
|
|
35
|
+
const options = buildLocalSpawnOptions(process.platform === 'win32', cwd, env);
|
|
36
|
+
const child = spawn(process.execPath, [
|
|
37
|
+
'-e',
|
|
38
|
+
[
|
|
39
|
+
"process.stdin.setEncoding('utf8')",
|
|
40
|
+
"process.stdin.on('data', () => { process.exitCode = 90 })",
|
|
41
|
+
"process.stdin.on('end', () => {",
|
|
42
|
+
" process.stdout.write('stdout-ok')",
|
|
43
|
+
" process.stderr.write('stderr-ok')",
|
|
44
|
+
'})',
|
|
45
|
+
].join(';'),
|
|
46
|
+
], options);
|
|
47
|
+
|
|
48
|
+
let stdout = '';
|
|
49
|
+
let stderr = '';
|
|
50
|
+
child.stdout.setEncoding('utf8');
|
|
51
|
+
child.stderr.setEncoding('utf8');
|
|
52
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
53
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
54
|
+
child.stdin.end();
|
|
55
|
+
|
|
56
|
+
const exit = await new Promise((resolve, reject) => {
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
child.kill();
|
|
59
|
+
reject(new Error('captured child did not observe stdin EOF'));
|
|
60
|
+
}, 10_000);
|
|
61
|
+
child.once('error', (error) => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
reject(error);
|
|
64
|
+
});
|
|
65
|
+
child.once('close', (code, signal) => {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
resolve({ code, signal });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert.deepEqual(exit, { code: 0, signal: null });
|
|
72
|
+
assert.equal(stdout, 'stdout-ok');
|
|
73
|
+
assert.equal(stderr, 'stderr-ok');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
runCapturedProcessProbe().then(() => {
|
|
77
|
+
process.stdout.write('shell-terminal-isolation-regression PASS\n');
|
|
78
|
+
}).catch((error) => {
|
|
79
|
+
console.error(error);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
6
|
+
const bundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
|
|
7
|
+
const match = /editor\.onEscape = \(autocompleteCancelled = false\) => \{([\s\S]*?)\n\t\t\};\n\t\teditor\.onShiftTab =/.exec(bundle);
|
|
8
|
+
assert(match, 'SLASH_ESCAPE_REGRESSION: editor.onEscape handler is missing');
|
|
9
|
+
|
|
10
|
+
const runEscape = Function('host', 'editor', 'autocompleteCancelled', match[1]);
|
|
11
|
+
const ctrlSMatch = /editor\.onCtrlS = \(\) => \{([\s\S]*?)\n\t\t\};\n\t\teditor\.onCtrlB =/.exec(bundle);
|
|
12
|
+
assert(ctrlSMatch, 'SLASH_ESCAPE_REGRESSION: editor.onCtrlS handler is missing');
|
|
13
|
+
const runCtrlS = Function('host', 'editor', ctrlSMatch[1]);
|
|
14
|
+
|
|
15
|
+
function probe(initialText, autocompleteCancelled) {
|
|
16
|
+
let text = initialText;
|
|
17
|
+
const editor = {
|
|
18
|
+
inputMode: 'prompt',
|
|
19
|
+
getText: () => text,
|
|
20
|
+
setText: (value) => { text = value; },
|
|
21
|
+
};
|
|
22
|
+
const host = {
|
|
23
|
+
cancelInFlight: undefined,
|
|
24
|
+
queueSteerInFlight: undefined,
|
|
25
|
+
state: {
|
|
26
|
+
activeDialog: null,
|
|
27
|
+
appState: {
|
|
28
|
+
isCompacting: false,
|
|
29
|
+
streamingPhase: 'idle',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
streamingUI: { hasActiveTurn: () => false },
|
|
33
|
+
btwPanelController: { closeOrCancel: () => false },
|
|
34
|
+
};
|
|
35
|
+
const controller = {
|
|
36
|
+
pendingExit: null,
|
|
37
|
+
pendingUndoEsc: null,
|
|
38
|
+
clearPendingExit() {},
|
|
39
|
+
clearPendingUndoEsc() {},
|
|
40
|
+
armPendingUndoEsc() {},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
runEscape.call(controller, host, editor, autocompleteCancelled);
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
assert.equal(
|
|
48
|
+
probe('/reload', true),
|
|
49
|
+
'',
|
|
50
|
+
'SLASH_ESCAPE_REGRESSION: Escape must cancel the open slash-command draft together with its menu',
|
|
51
|
+
);
|
|
52
|
+
assert.equal(
|
|
53
|
+
probe('ordinary draft', true),
|
|
54
|
+
'ordinary draft',
|
|
55
|
+
'SLASH_ESCAPE_REGRESSION: cancelling ordinary autocomplete must preserve the draft',
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
let flushed = null;
|
|
59
|
+
const idleHost = {
|
|
60
|
+
streamingUI: { hasActiveTurn: () => false },
|
|
61
|
+
state: {
|
|
62
|
+
appState: { streamingPhase: 'idle' },
|
|
63
|
+
ui: { requestRender() {} },
|
|
64
|
+
},
|
|
65
|
+
flushQueuedMessages: (draft, mode) => { flushed = { draft, mode }; },
|
|
66
|
+
updateQueueDisplay() {},
|
|
67
|
+
};
|
|
68
|
+
const slashEditor = {
|
|
69
|
+
inputMode: 'prompt',
|
|
70
|
+
getText: () => '/reload',
|
|
71
|
+
};
|
|
72
|
+
runCtrlS(idleHost, slashEditor);
|
|
73
|
+
assert.deepEqual(
|
|
74
|
+
flushed,
|
|
75
|
+
{ draft: '/reload', mode: 'prompt' },
|
|
76
|
+
'SLASH_ESCAPE_REGRESSION: Ctrl+S must hand an idle slash draft to the existing queue drain',
|
|
77
|
+
);
|
|
78
|
+
flushed = null;
|
|
79
|
+
runCtrlS(idleHost, {
|
|
80
|
+
inputMode: 'prompt',
|
|
81
|
+
getText: () => 'ordinary draft',
|
|
82
|
+
});
|
|
83
|
+
assert.equal(
|
|
84
|
+
flushed,
|
|
85
|
+
null,
|
|
86
|
+
'SLASH_ESCAPE_REGRESSION: the existing idle behavior for ordinary drafts must stay unchanged',
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
console.log('slash-escape-regression PASS (Escape + Ctrl+S)');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
|
|
6
|
+
? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
|
|
7
|
+
: path.join(__dirname, '..', 'blun.mjs');
|
|
8
|
+
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
9
|
+
const match = /async function handleLoopCommand\(host, args\) \{([\s\S]*?)\n\}\nfunction hasRunnableSession/.exec(bundle);
|
|
10
|
+
assert(match, 'TELEGRAM_LOOP_EXACTLY_ONCE: handleLoopCommand is missing');
|
|
11
|
+
|
|
12
|
+
const handleLoopCommand = Function(
|
|
13
|
+
'parseLoopCommand',
|
|
14
|
+
'hasRunnableSession',
|
|
15
|
+
'llmNotSetMessage',
|
|
16
|
+
'isStreaming',
|
|
17
|
+
`return async function handleLoopCommand(host, args) {${match[1]}\n};`,
|
|
18
|
+
)(
|
|
19
|
+
() => ({ kind: 'create', interval: '*/5 * * * *', prompt: 'work once' }),
|
|
20
|
+
() => true,
|
|
21
|
+
() => 'missing model',
|
|
22
|
+
(host) => host.streaming,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function createHost({ telegram = false, streaming = true } = {}) {
|
|
26
|
+
const calls = { send: [], enqueue: [] };
|
|
27
|
+
const session = {
|
|
28
|
+
async createLoop({ interval, prompt }) {
|
|
29
|
+
return { interval, prompt, status: 'active' };
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const host = {
|
|
33
|
+
streaming,
|
|
34
|
+
session,
|
|
35
|
+
telegramRemoteCommandContext: telegram ? { item: { telegramCommandName: 'loop' } } : undefined,
|
|
36
|
+
state: { appState: { isCompacting: false, model: 'blun/king' } },
|
|
37
|
+
requireSession() { return session; },
|
|
38
|
+
setAppState() {},
|
|
39
|
+
track() {},
|
|
40
|
+
showStatus() {},
|
|
41
|
+
showError(message) { throw new Error(message); },
|
|
42
|
+
sendNormalUserInput(text) { calls.send.push(text); },
|
|
43
|
+
enqueueNormalUserInput(text) { calls.enqueue.push(text); },
|
|
44
|
+
};
|
|
45
|
+
return { host, calls };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
(async () => {
|
|
49
|
+
const telegram = createHost({ telegram: true, streaming: true });
|
|
50
|
+
await handleLoopCommand(telegram.host, '5 min work once');
|
|
51
|
+
assert.deepEqual(telegram.calls.send, ['work once']);
|
|
52
|
+
assert.deepEqual(
|
|
53
|
+
telegram.calls.enqueue,
|
|
54
|
+
[],
|
|
55
|
+
'TELEGRAM_LOOP_EXACTLY_ONCE: Telegram loop payload must not survive in the normal queue',
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const local = createHost({ telegram: false, streaming: true });
|
|
59
|
+
await handleLoopCommand(local.host, '5 min work once');
|
|
60
|
+
assert.deepEqual(local.calls.send, []);
|
|
61
|
+
assert.deepEqual(
|
|
62
|
+
local.calls.enqueue,
|
|
63
|
+
['work once'],
|
|
64
|
+
'TELEGRAM_LOOP_EXACTLY_ONCE: local busy-loop behavior must stay queued',
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
console.log('telegram-loop-exactly-once-regression PASS');
|
|
68
|
+
})().catch((error) => {
|
|
69
|
+
console.error(error);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
});
|
|
@@ -68,6 +68,8 @@ function resolveTelegramApprovalTarget(allowFrom, configuredChatId) {
|
|
|
68
68
|
: [];
|
|
69
69
|
const configured = String(configuredChatId ?? '').trim();
|
|
70
70
|
if (configured.length > 0) return allowed.includes(configured) ? configured : null;
|
|
71
|
+
const privateChats = allowed.filter((value) => /^[1-9]\d*$/u.test(value));
|
|
72
|
+
if (privateChats.length === 1) return privateChats[0];
|
|
71
73
|
return allowed.length === 1 ? allowed[0] : null;
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -280,4 +282,3 @@ module.exports = {
|
|
|
280
282
|
wasTelegramApprovalSent,
|
|
281
283
|
writeTelegramApprovalResponse,
|
|
282
284
|
};
|
|
283
|
-
|