blun-king-cli 9.1.527 → 9.1.550

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.
Files changed (108) hide show
  1. package/LIESMICH.txt +13 -834
  2. package/README.md +41 -799
  3. package/bin/agent-resume-snapshot.cjs +31 -0
  4. package/bin/assistant-message-offload-policy.cjs +23 -2
  5. package/bin/codebase-search-runtime.cjs +23 -0
  6. package/bin/context-performance-policy.cjs +2 -5
  7. package/bin/context-pressure-policy.cjs +20 -0
  8. package/bin/cron-run-output.cjs +45 -0
  9. package/bin/cron-run-store.cjs +145 -0
  10. package/bin/durable-task-resume-policy.cjs +130 -0
  11. package/bin/durable-task-resume-runtime.cjs +117 -0
  12. package/bin/durable-task-resume-store.cjs +88 -0
  13. package/bin/editable-tool-approval-policy.cjs +540 -0
  14. package/bin/editable-tool-approval-runtime.cjs +99 -0
  15. package/bin/empty-response-retry-policy.cjs +29 -0
  16. package/bin/history-offload-pressure-policy.cjs +33 -0
  17. package/bin/html-to-research-markdown.cjs +146 -0
  18. package/bin/programmatic-context-isolation.cjs +25 -0
  19. package/bin/programmatic-tool-runtime.mjs +627 -0
  20. package/bin/read-continuation-policy.cjs +36 -5
  21. package/bin/scoped-cron-run-policy.cjs +358 -0
  22. package/bin/skill-activation-performance-policy.cjs +9 -0
  23. package/bin/startup-preferences.cjs +3 -3
  24. package/bin/structured-agent-swarm-output.cjs +325 -0
  25. package/bin/structured-subagent-output.cjs +252 -0
  26. package/bin/subagent-context-fork-policy.cjs +155 -0
  27. package/bin/subagent-skill-policy.cjs +204 -0
  28. package/bin/telegram-approval-relay.cjs +2 -1
  29. package/bin/telegram-direct-focus-policy.cjs +25 -1
  30. package/bin/todo-list-turn-policy.cjs +111 -1
  31. package/bin/tool-result-offload-policy.cjs +43 -1
  32. package/bin/turn-thinking-policy.cjs +6 -15
  33. package/bin/turn-tool-performance-policy.cjs +5 -4
  34. package/bin/update-notice.js +14 -18
  35. package/bin/user-message-offload-policy.cjs +12 -1
  36. package/blun.mjs +1406 -265
  37. package/codebase-index/README.md +82 -0
  38. package/codebase-index/codebase_index.py +469 -0
  39. package/package.json +24 -38
  40. package/telegram-plugin/bin/telegram-typing-keepalive.cjs +89 -0
  41. package/telegram-plugin/dist/bridge.mjs +8 -1
  42. package/CHANGELOG.md +0 -260
  43. package/agent-spine-plugin/CHANGELOG.md +0 -406
  44. package/agent-spine-plugin/CONTRIBUTING.md +0 -52
  45. package/agent-spine-plugin/README.md +0 -344
  46. package/agent-spine-plugin/SECURITY.md +0 -47
  47. package/agent-spine-plugin/docs/acceptance.md +0 -61
  48. package/agent-spine-plugin/docs/architecture.md +0 -183
  49. package/agent-spine-plugin/docs/attention.md +0 -121
  50. package/agent-spine-plugin/docs/automatic-continuity.md +0 -79
  51. package/agent-spine-plugin/docs/channel-runtime.md +0 -92
  52. package/agent-spine-plugin/docs/coordination.md +0 -138
  53. package/agent-spine-plugin/docs/feed-transport.md +0 -99
  54. package/agent-spine-plugin/docs/gateway-runtime.md +0 -116
  55. package/agent-spine-plugin/docs/harness-reference.md +0 -45
  56. package/agent-spine-plugin/docs/host-integration.md +0 -129
  57. package/agent-spine-plugin/docs/https-transport.md +0 -116
  58. package/agent-spine-plugin/docs/learning.md +0 -133
  59. package/agent-spine-plugin/docs/object-transport.md +0 -93
  60. package/agent-spine-plugin/docs/peer-transport.md +0 -88
  61. package/agent-spine-plugin/docs/preflight-recall.md +0 -69
  62. package/agent-spine-plugin/docs/preservation-contract.md +0 -53
  63. package/agent-spine-plugin/docs/quality-gates.md +0 -50
  64. package/agent-spine-plugin/docs/relationships.md +0 -73
  65. package/agent-spine-plugin/docs/releasing.md +0 -83
  66. package/agent-spine-plugin/docs/roadmap.md +0 -307
  67. package/agent-spine-plugin/docs/selfstarter.md +0 -88
  68. package/agent-spine-plugin/docs/session-briefing.md +0 -74
  69. package/agent-spine-plugin/docs/shared-memory.md +0 -259
  70. package/agent-spine-plugin/docs/source-roots.md +0 -86
  71. package/agent-spine-plugin/docs/sqlite-transport.md +0 -76
  72. package/agent-spine-plugin/scripts/check-hosts.js +0 -195
  73. package/agent-spine-plugin/scripts/check-install.js +0 -569
  74. package/agent-spine-plugin/scripts/check-syntax.js +0 -29
  75. package/agent-spine-plugin/scripts/github-actions.js +0 -11
  76. package/agent-spine-plugin/scripts/release-check.js +0 -128
  77. package/agent-spine-plugin/scripts/run-acceptance.js +0 -19
  78. package/agent-spine-plugin/scripts/run-checks.js +0 -46
  79. package/agent-spine-plugin/scripts/run-tests-hermetic.js +0 -73
  80. package/agent-spine-plugin/spine-example/1-identity.md +0 -12
  81. package/agent-spine-plugin/spine-example/2-voice.md +0 -6
  82. package/agent-spine-plugin/spine-example/3-conduct.md +0 -8
  83. package/agent-spine-plugin/spine-example/4-history.md +0 -4
  84. package/bin/package-regression-policy.cjs +0 -77
  85. package/release-planned-removals.json +0 -15
  86. package/scripts/check-active-profile-plugin-startup.js +0 -36
  87. package/scripts/check-approval-observability-regression.js +0 -111
  88. package/scripts/check-approval-queue-shortcuts-regression.js +0 -65
  89. package/scripts/check-bundled-agent-spine-regression.js +0 -48
  90. package/scripts/check-copy-command-regression.js +0 -74
  91. package/scripts/check-historical-tool-result-preview-regression.js +0 -77
  92. package/scripts/check-mcp-startup-wait-budget.js +0 -48
  93. package/scripts/check-package-regression.js +0 -38
  94. package/scripts/check-plugin-startup-regression.js +0 -53
  95. package/scripts/check-queue-controls-regression.js +0 -189
  96. package/scripts/check-release-metadata.js +0 -103
  97. package/scripts/check-reload-agent-spine-regression.js +0 -76
  98. package/scripts/check-resume-replay-regression.js +0 -100
  99. package/scripts/check-session-cancel-regression.js +0 -43
  100. package/scripts/check-session-picker-resume-metrics-regression.js +0 -97
  101. package/scripts/check-session-start-hook-context-regression.js +0 -228
  102. package/scripts/check-shell-terminal-isolation-regression.js +0 -81
  103. package/scripts/check-slash-escape-regression.js +0 -89
  104. package/scripts/check-telegram-bridge-watchdog.js +0 -60
  105. package/scripts/check-telegram-loop-exactly-once-regression.js +0 -71
  106. package/scripts/check-todo-loop-regression.js +0 -78
  107. package/scripts/check-todo-recovery-catalog-regression.js +0 -50
  108. /package/{scripts → bin}/fix-node-pty-perms.js +0 -0
@@ -1,100 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const assert = require('node:assert/strict');
5
- const fs = require('node:fs');
6
- const path = require('node:path');
7
-
8
- const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
9
- ? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
10
- : path.join(__dirname, '..', 'blun.mjs');
11
- const bundle = fs.readFileSync(bundlePath, 'utf8');
12
-
13
- function between(start, end) {
14
- const startIndex = bundle.indexOf(start);
15
- assert.notEqual(startIndex, -1, `missing bundle marker: ${start}`);
16
- const endIndex = bundle.indexOf(end, startIndex + start.length);
17
- assert.notEqual(endIndex, -1, `missing bundle marker: ${end}`);
18
- return bundle.slice(startIndex, endIndex);
19
- }
20
-
21
- function compileMethod(name, nextName, dependencies = {}) {
22
- const source = between(`\n\t\t${name}(`, `\n\t\t${nextName}(`).trim();
23
- const names = Object.keys(dependencies);
24
- const values = Object.values(dependencies);
25
- return Function(...names, `"use strict"; return ({${source}}).${name};`)(...values);
26
- }
27
-
28
- function resolveStepEventUuid(openSteps, event) {
29
- if (event.stepUuid !== undefined) return event.stepUuid;
30
- let latest;
31
- for (const uuid of openSteps.keys()) latest = uuid;
32
- return latest;
33
- }
34
-
35
- function resolveCompletedStepUuid(openSteps, event) {
36
- if (event.uuid !== undefined) return event.uuid;
37
- let latest;
38
- for (const uuid of openSteps.keys()) latest = uuid;
39
- return latest;
40
- }
41
-
42
- const recoverMissingOpenStep = compileMethod(
43
- 'recoverMissingOpenStep',
44
- 'appendLoopEvent',
45
- { resolveStepEventUuid },
46
- );
47
- const appendLoopEvent = compileMethod(
48
- 'appendLoopEvent',
49
- 'appendMessage',
50
- {
51
- projectLoopEventForRecord: (event) => event,
52
- resolveCompletedStepUuid,
53
- createToolMessage: (toolCallId, content) => ({ toolCallId, content, toolCalls: [] }),
54
- toolResultOutputForModel: (result) => result.output,
55
- estimateTokensForMessages: () => 0,
56
- },
57
- );
58
-
59
- const records = [];
60
- const warnings = [];
61
- const host = {
62
- _history: [],
63
- _tokenCount: 0,
64
- tokenCountCoveredMessageCount: 0,
65
- openSteps: new Map(),
66
- pendingToolResultIds: new Set(),
67
- agent: {
68
- records: {
69
- restoring: { time: Date.now() },
70
- logRecord(record) { records.push(record); },
71
- },
72
- log: { warn(message, fields) { warnings.push({ message, fields }); } },
73
- },
74
- recoverMissingOpenStep,
75
- pushHistory(...messages) { this._history.push(...messages); },
76
- markPendingTokenEstimateDirty() {},
77
- flushDeferredMessagesIfToolExchangeClosed() {},
78
- closePendingToolResults() { return []; },
79
- };
80
-
81
- // Exact shape measured after context.apply_compaction: no step.begin survives,
82
- // then text, a tool call/result pair, and step.end continue the interrupted turn.
83
- appendLoopEvent.call(host, { type: 'content.part', part: { type: 'text', text: 'continued after compaction' } });
84
- appendLoopEvent.call(host, { type: 'tool.call', toolCallId: 'tool-1', name: 'Read', args: { path: 'SPEC.md' } });
85
- appendLoopEvent.call(host, { type: 'tool.result', toolCallId: 'tool-1', result: { output: 'ok', isError: false } });
86
- appendLoopEvent.call(host, { type: 'step.end', contextTokens: 100 });
87
-
88
- assert.equal(warnings.length, 1, 'one missing step must produce one bounded repair warning');
89
- assert.equal(warnings[0].fields.eventType, 'content.part');
90
- assert.equal(warnings[0].fields.restoring, true);
91
- assert.equal(host._history.length, 2, 'repair must retain the assistant continuation and its tool result');
92
- assert.deepEqual(host._history[0].content, [{ type: 'text', text: 'continued after compaction' }]);
93
- assert.equal(host._history[0].toolCalls.length, 1);
94
- assert.equal(host._history[0].toolCalls[0].id, 'tool-1');
95
- assert.equal(host._history[1].toolCallId, 'tool-1');
96
- assert.equal(host.pendingToolResultIds.size, 0);
97
- assert.equal(host.openSteps.size, 0);
98
- assert.equal(records.filter((record) => record.type === 'context.append_loop_event').length, 4);
99
-
100
- process.stdout.write('resume-replay-regression PASS\n');
@@ -1,43 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const assert = require('node:assert/strict');
5
- const fs = require('node:fs');
6
- const path = require('node:path');
7
-
8
- const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
9
- ? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
10
- : path.join(__dirname, '..', 'blun.mjs');
11
- const bundle = fs.readFileSync(bundlePath, 'utf8');
12
-
13
- function between(start, end) {
14
- const startIndex = bundle.indexOf(start);
15
- assert.notEqual(startIndex, -1, `missing bundle marker: ${start}`);
16
- const endIndex = bundle.indexOf(end, startIndex + start.length);
17
- assert.notEqual(endIndex, -1, `missing bundle marker: ${end}`);
18
- return bundle.slice(startIndex, endIndex);
19
- }
20
-
21
- function assertSafeCancel(source, label) {
22
- assert.match(source, /\.cancel\(\)\.catch\(\(error\)\s*=>/,
23
- `${label} must observe the cancellation promise`);
24
- assert.match(source, /error instanceof BlunError && error\.code === ErrorCodes\.SESSION_NOT_FOUND/,
25
- `${label} must treat an already-missing session as cancelled`);
26
- assert.match(source, /showError\(formatErrorMessage\$2\(error\)\)/,
27
- `${label} must still surface non-session errors`);
28
- }
29
-
30
- assertSafeCancel(
31
- between('\n\tcancelCurrentStream() {', '\n\tcancelCurrentCompaction() {'),
32
- 'keyboard stream cancellation',
33
- );
34
- assertSafeCancel(
35
- between('\n\tinjectTelegramRemoteCommand(', '\n\trunTelegramRemoteCommand('),
36
- 'preemptive Telegram reload cancellation',
37
- );
38
- assertSafeCancel(
39
- between('\n\tenqueueSlashCommand(', '\n\tenqueueNormalUserInput('),
40
- 'preemptive slash-command cancellation',
41
- );
42
-
43
- process.stdout.write('session-cancel-regression PASS\n');
@@ -1,97 +0,0 @@
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
-
10
- const rowsMatch = /function sessionRowsForPicker\(sessions, currentSessionId, currentSessionHasContent\) \{([\s\S]*?)\n\}/.exec(bundle);
11
- assert(rowsMatch, 'SESSION_PICKER_RESUME_METRICS: sessionRowsForPicker is missing');
12
- const sessionRowsForPicker = Function(
13
- `return function sessionRowsForPicker(sessions, currentSessionId, currentSessionHasContent) {${rowsMatch[1]}\n};`,
14
- )();
15
-
16
- const rows = sessionRowsForPicker([{
17
- id: 'session-a',
18
- title: 'A',
19
- lastPrompt: 'work',
20
- workDir: 'C:/work',
21
- updatedAt: 1,
22
- metadata: {},
23
- sessionDir: 'C:/state/session-a',
24
- }], 'other', true);
25
- assert.equal(rows[0].session_dir, 'C:/state/session-a');
26
-
27
- const metricsMatch = /async function readSessionResumeMetrics\(session\) \{([\s\S]*?)\n\}\nasync function enrichSessionRowsForPicker/.exec(bundle);
28
- assert(metricsMatch, 'SESSION_PICKER_RESUME_METRICS: readSessionResumeMetrics is missing');
29
-
30
- function buildReader({ resumeStat, wireStat, checkpoint }) {
31
- return Function(
32
- 'join',
33
- 'statSessionFile',
34
- 'loadResumeCheckpoint',
35
- 'SESSION_RESUME_STALE_AFTER_MS',
36
- `return async function readSessionResumeMetrics(session) {${metricsMatch[1]}\n};`,
37
- )(
38
- path.join,
39
- async (filePath) => filePath.endsWith('resume-checkpoint.json') ? resumeStat : wireStat,
40
- async () => checkpoint,
41
- 60_000,
42
- );
43
- }
44
-
45
- (async () => {
46
- const missing = await buildReader({ resumeStat: null, wireStat: null, checkpoint: null })({
47
- session_dir: 'C:/state/session-a',
48
- });
49
- assert.deepEqual(missing, { status: 'missing' });
50
-
51
- const invalid = await buildReader({
52
- resumeStat: { size: 1_024, mtimeMs: 1_000 },
53
- wireStat: { size: 2_048, mtimeMs: 1_000 },
54
- checkpoint: { ok: false },
55
- })({ session_dir: 'C:/state/session-a' });
56
- assert.deepEqual(invalid, { status: 'invalid' });
57
-
58
- const ok = await buildReader({
59
- resumeStat: { size: 1_024, mtimeMs: 1_000 },
60
- wireStat: { size: 2_048, mtimeMs: 1_500 },
61
- checkpoint: {
62
- ok: true,
63
- offset: 2_048,
64
- snapshot: { context: { history: [{}, {}, {}], tokenCount: 1_200 } },
65
- },
66
- })({ session_dir: 'C:/state/session-a' });
67
- assert.deepEqual(ok, {
68
- status: 'ok',
69
- resumeBytes: 1_024,
70
- wireBytes: 2_048,
71
- messages: 3,
72
- tokens: 1_200,
73
- });
74
-
75
- const stale = await buildReader({
76
- resumeStat: { size: 1_024, mtimeMs: 1_000 },
77
- wireStat: { size: 4_096, mtimeMs: 62_000 },
78
- checkpoint: {
79
- ok: true,
80
- offset: 2_048,
81
- snapshot: { context: { history: [{}], tokenCount: 500 } },
82
- },
83
- })({ session_dir: 'C:/state/session-a' });
84
- assert.equal(stale.status, 'stale');
85
- assert.equal(stale.wireBytes, 4_096);
86
-
87
- const renderBlock = /const resume = session\.resume;([\s\S]*?)\n\t\treturn card;/.exec(bundle);
88
- assert(renderBlock, 'SESSION_PICKER_RESUME_METRICS: resume metrics are not rendered');
89
- assert.match(renderBlock[1], /formatByteSize\(resume\.resumeBytes\)/);
90
- assert.match(renderBlock[1], /formatByteSize\(resume\.wireBytes\)/);
91
- assert.match(renderBlock[1], /sessionPicker\.resume\.stale/);
92
-
93
- console.log('session-picker-resume-metrics-regression PASS');
94
- })().catch((error) => {
95
- console.error(error);
96
- process.exitCode = 1;
97
- });
@@ -1,228 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('node:fs');
5
- const os = require('node:os');
6
- const path = require('node:path');
7
- const { spawnSync } = require('node:child_process');
8
-
9
- const bundlePath = process.env.BLUN_BUNDLE_UNDER_TEST
10
- ? path.resolve(process.env.BLUN_BUNDLE_UNDER_TEST)
11
- : path.resolve(__dirname, '..', 'blun.mjs');
12
- const bundle = fs.readFileSync(bundlePath, 'utf8');
13
-
14
- function assert(condition, message) {
15
- if (!condition) throw new Error(message);
16
- }
17
-
18
- const renderer = bundle.match(
19
- /function renderSessionStartHookResult\(results\) \{[\s\S]*?\n\}/,
20
- );
21
- const allowRenderer = bundle.match(
22
- /function renderAllowHookResult\(event, results\) \{[\s\S]*?\n\}/,
23
- );
24
- const wrapperRenderer = bundle.match(
25
- /function renderHookResult\(event, message\) \{[\s\S]*?\n\}/,
26
- );
27
- const messageSelector = bundle.match(
28
- /function userPromptHookMessage\(result\) \{[\s\S]*?\n\}/,
29
- );
30
- const stringPredicate = bundle.match(
31
- /function isNonEmptyString\$1\(value\) \{[\s\S]*?\n\}/,
32
- );
33
- const trigger = bundle.match(
34
- /async triggerSessionStart\(source\) \{[\s\S]*?\n\t\t\}/,
35
- );
36
- const scheduler = bundle.match(
37
- /scheduleSessionStart\(source\) \{[\s\S]*?\n\t\t\}/,
38
- );
39
- const createMain = bundle.match(
40
- /async createMain\(\) \{[\s\S]*?\n\t\t\}/,
41
- );
42
- const resume = bundle.match(
43
- /async resume\(\) \{[\s\S]*?\n\t\t\}/,
44
- );
45
-
46
- assert(renderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart renderer is missing');
47
- assert(allowRenderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: shared allow renderer is missing');
48
- assert(wrapperRenderer, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook wrapper renderer is missing');
49
- assert(messageSelector, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook message selector is missing');
50
- assert(stringPredicate, 'SESSION_START_HOOK_CONTEXT_REGRESSION: hook string predicate is missing');
51
- assert(trigger, 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart trigger is missing');
52
- assert(scheduler, 'SESSION_START_HOOK_CONTEXT_REGRESSION: non-blocking SessionStart scheduler is missing');
53
- assert(createMain, 'SESSION_START_HOOK_CONTEXT_REGRESSION: createMain is missing');
54
- assert(resume, 'SESSION_START_HOOK_CONTEXT_REGRESSION: resume is missing');
55
- assert(
56
- /renderAllowHookResult\("SessionStart", results\)/.test(renderer[0]),
57
- 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart output is not rendered',
58
- );
59
- assert(
60
- /const results = await this\.hookEngine\.trigger\("SessionStart"/.test(trigger[0]),
61
- 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart results are still discarded',
62
- );
63
- assert(
64
- /renderSessionStartHookResult\(results\)/.test(trigger[0]),
65
- 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart result does not reach the context path',
66
- );
67
- assert(
68
- /appendSystemReminder\([\s\S]*variant: "session_start_hook"/.test(trigger[0]),
69
- 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart result is not persisted as a scoped reminder',
70
- );
71
- assert(
72
- /await mainAgent\.records\.flush\(\)/.test(trigger[0]),
73
- 'SESSION_START_HOOK_CONTEXT_REGRESSION: completed SessionStart reminder is not flushed',
74
- );
75
- assert(
76
- /this\.scheduleSessionStart\("startup"\)/.test(createMain[0])
77
- && !/await this\.triggerSessionStart\("startup"\)/.test(createMain[0]),
78
- 'SESSION_START_HOOK_CONTEXT_REGRESSION: fresh-session input is still blocked by SessionStart',
79
- );
80
- assert(
81
- /this\.scheduleSessionStart\("resume"\)/.test(resume[0])
82
- && !/await this\.triggerSessionStart\("resume"\)/.test(resume[0]),
83
- 'SESSION_START_HOOK_CONTEXT_REGRESSION: resumed-session input is still blocked by SessionStart',
84
- );
85
- assert(
86
- /void this\.triggerSessionStart\(source\)\.catch\(/.test(scheduler[0]),
87
- 'SESSION_START_HOOK_CONTEXT_REGRESSION: SessionStart is not detached from interactive startup',
88
- );
89
-
90
- const packageRoot = path.resolve(__dirname, '..');
91
- const agentSpineRoot = path.join(packageRoot, 'agent-spine-plugin');
92
- const agentSpineManifest = JSON.parse(
93
- fs.readFileSync(path.join(agentSpineRoot, 'blun.plugin.json'), 'utf8'),
94
- );
95
- const agentSpineCodexHooks = JSON.parse(
96
- fs.readFileSync(path.join(agentSpineRoot, 'hooks', 'codex.json'), 'utf8'),
97
- );
98
- const blunSessionStart = agentSpineManifest.hooks.find(
99
- (hook) => hook.event === 'SessionStart',
100
- );
101
- const codexSessionStart = agentSpineCodexHooks.hooks.SessionStart?.[0]?.hooks?.[0];
102
-
103
- assert(
104
- blunSessionStart?.timeout >= 180,
105
- 'SESSION_START_HOOK_CONTEXT_REGRESSION: BLUN AgentSpine SessionStart timeout must cover the measured 123.6 s cold scan',
106
- );
107
- assert(
108
- codexSessionStart?.timeout >= 180,
109
- 'SESSION_START_HOOK_CONTEXT_REGRESSION: Codex AgentSpine SessionStart timeout must match the BLUN runtime budget',
110
- );
111
-
112
- const probeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-session-start-hook-'));
113
- try {
114
- fs.mkdirSync(path.join(probeRoot, '.git'));
115
- fs.writeFileSync(
116
- path.join(probeRoot, 'AGENTS.md'),
117
- '# SessionStart probe\n\nLoad this project instruction automatically.\n',
118
- );
119
- const stateRoot = path.join(probeRoot, '.agentspine-state');
120
- const hookRun = spawnSync(process.execPath, [path.join(agentSpineRoot, 'src', 'hook.js')], {
121
- cwd: agentSpineRoot,
122
- input: JSON.stringify({
123
- hook_event_name: 'SessionStart',
124
- session_id: 'session-start-regression',
125
- cwd: probeRoot,
126
- source: 'resume',
127
- }),
128
- encoding: 'utf8',
129
- timeout: 30_000,
130
- env: {
131
- ...process.env,
132
- BLUN_HOME: probeRoot,
133
- BLUN_PLUGIN_ROOT: agentSpineRoot,
134
- AGENTSPINE_STATE_DIR: stateRoot,
135
- },
136
- });
137
- assert(
138
- hookRun.error === undefined && hookRun.status === 0,
139
- `SESSION_START_HOOK_CONTEXT_REGRESSION: real AgentSpine hook failed: ${hookRun.error?.message || hookRun.stderr}`,
140
- );
141
- const hookOutput = JSON.parse(hookRun.stdout.trim());
142
- assert(
143
- hookOutput.hookSpecificOutput?.hookEventName === 'SessionStart'
144
- && hookOutput.hookSpecificOutput?.message?.includes('AgentSpine'),
145
- `SESSION_START_HOOK_CONTEXT_REGRESSION: real AgentSpine process output is not injectable: ${hookRun.stdout.trim()}`,
146
- );
147
- } finally {
148
- fs.rmSync(probeRoot, { recursive: true, force: true });
149
- }
150
-
151
- const triggerFunction = trigger[0].replace(
152
- /^async triggerSessionStart/,
153
- 'async function triggerSessionStart',
154
- );
155
- const schedulerFunction = scheduler[0].replace(
156
- /^scheduleSessionStart/,
157
- 'function scheduleSessionStart',
158
- );
159
- const runTrigger = Function(
160
- `${wrapperRenderer[0]}\n${messageSelector[0]}\n${stringPredicate[0]}\n${allowRenderer[0]}\n${renderer[0]}\n${triggerFunction}\nreturn triggerSessionStart;`,
161
- )();
162
- const scheduleTrigger = Function(
163
- `${schedulerFunction}\nreturn scheduleSessionStart;`,
164
- )();
165
-
166
- (async () => {
167
- const reminders = [];
168
- let flushes = 0;
169
- const fakeSession = {
170
- hookEngine: {
171
- async trigger(event, args) {
172
- assert(event === 'SessionStart', 'runtime probe called the wrong hook event');
173
- assert(args.matcherValue === 'resume', 'runtime probe lost the reload matcher');
174
- return [{ action: 'allow', message: 'AgentSpine ready: 152 sources indexed.' }];
175
- },
176
- },
177
- requireMainAgent() {
178
- return {
179
- context: {
180
- appendSystemReminder(text, origin) {
181
- reminders.push({ text, origin });
182
- },
183
- },
184
- records: {
185
- async flush() {
186
- flushes += 1;
187
- },
188
- },
189
- };
190
- },
191
- };
192
-
193
- await runTrigger.call(fakeSession, 'resume');
194
- assert(reminders.length === 1, 'runtime probe did not append exactly one reminder');
195
- assert(flushes === 1, 'runtime probe did not flush exactly once');
196
- assert(
197
- reminders[0].text.includes('<hook_result hook_event="SessionStart">')
198
- && reminders[0].text.includes('AgentSpine ready: 152 sources indexed.'),
199
- 'runtime probe lost the AgentSpine SessionStart message',
200
- );
201
- assert(
202
- reminders[0].origin?.kind === 'injection'
203
- && reminders[0].origin?.variant === 'session_start_hook',
204
- 'runtime probe used the wrong reminder origin',
205
- );
206
-
207
- let detachedCompleted = false;
208
- const detachedStartedAt = Date.now();
209
- scheduleTrigger.call({
210
- async triggerSessionStart(source) {
211
- assert(source === 'startup', 'detached runtime probe lost the startup source');
212
- await new Promise((resolve) => setTimeout(resolve, 75));
213
- detachedCompleted = true;
214
- },
215
- log: { warn() {} },
216
- }, 'startup');
217
- assert(
218
- Date.now() - detachedStartedAt < 50 && detachedCompleted === false,
219
- 'SessionStart scheduler still waits for the hook before returning',
220
- );
221
- await new Promise((resolve) => setTimeout(resolve, 100));
222
- assert(detachedCompleted, 'detached SessionStart hook did not finish in the background');
223
-
224
- process.stdout.write('session-start-hook-context-regression PASS\n');
225
- })().catch((error) => {
226
- process.stderr.write(`${error.stack || error.message}\n`);
227
- process.exitCode = 1;
228
- });
@@ -1,81 +0,0 @@
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
- });
@@ -1,89 +0,0 @@
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)');