blun-king-cli 9.1.509 → 9.1.511
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 +12 -0
- package/LIESMICH.txt +12 -1
- package/README.md +12 -1
- 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 +2 -1
- package/agent-spine-plugin/CHANGELOG.md +70 -8
- package/agent-spine-plugin/README.md +1 -1
- package/agent-spine-plugin/blun.plugin.json +33 -33
- package/agent-spine-plugin/docs/acceptance.md +2 -2
- package/agent-spine-plugin/docs/gateway-runtime.md +8 -1
- package/agent-spine-plugin/docs/host-integration.md +34 -34
- package/agent-spine-plugin/docs/preflight-recall.md +69 -0
- package/agent-spine-plugin/docs/relationships.md +6 -0
- package/agent-spine-plugin/hooks/codex.json +47 -0
- package/agent-spine-plugin/hooks/hooks.json +11 -0
- package/agent-spine-plugin/hooks/version.json +2 -2
- package/agent-spine-plugin/package.json +4 -4
- package/agent-spine-plugin/scripts/check-hosts.js +53 -51
- package/agent-spine-plugin/scripts/check-install.js +53 -35
- package/agent-spine-plugin/scripts/release-check.js +11 -10
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +1 -1
- package/agent-spine-plugin/src/cli.js +46 -1
- package/agent-spine-plugin/src/hook.js +168 -90
- package/agent-spine-plugin/src/index.js +6 -0
- package/agent-spine-plugin/src/lib/acceptance.js +40 -0
- package/agent-spine-plugin/src/lib/audit.js +9 -2
- package/agent-spine-plugin/src/lib/graph.js +22 -4
- package/agent-spine-plugin/src/lib/persona-runtime.js +103 -31
- package/agent-spine-plugin/src/lib/preflight.js +678 -0
- package/agent-spine-plugin/src/lib/source-roots.js +32 -32
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +20 -3
- package/bin/read-batch-policy.cjs +32 -0
- package/bin/turn-tool-performance-policy.cjs +1 -0
- package/blun.mjs +58 -2
- package/package.json +3 -2
|
@@ -13,12 +13,13 @@ import {
|
|
|
13
13
|
} from "./lib/selfstarter.js";
|
|
14
14
|
import { claimChannelEvent } from "./lib/channel-runtime.js";
|
|
15
15
|
import { syncPersonaRosterFromEnvironment } from "./lib/persona-runtime.js";
|
|
16
|
+
import { captureMustRememberPrompt, recordPreflightFailure, runPreflight, verifyPreflightReceipt } from "./lib/preflight.js";
|
|
16
17
|
import { isMainModule } from "./lib/runtime.js";
|
|
17
18
|
|
|
18
19
|
const MAX_STDIN_BYTES = 64 * 1024;
|
|
19
20
|
const CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit", "PreCompact", "PostCompact"]);
|
|
20
21
|
const KNOWN_EVENTS = new Set([
|
|
21
|
-
...CONTEXT_EVENTS, "PreToolUse", "PostToolUse", "Stop", "SubagentStop"
|
|
22
|
+
...CONTEXT_EVENTS, "InstructionsLoaded", "PreToolUse", "PostToolUse", "Stop", "SubagentStop"
|
|
22
23
|
]);
|
|
23
24
|
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
|
|
24
25
|
const ATTENTION_WRITE_EVENTS = new Set(["UserPromptSubmit", "PostToolUse", "Stop", "SubagentStop"]);
|
|
@@ -43,29 +44,29 @@ function boundedId(value, field) {
|
|
|
43
44
|
return value;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function promptFromInput(input) {
|
|
47
|
-
for (const key of ["prompt", "user_prompt", "message", "input"]) {
|
|
48
|
-
const value = input[key];
|
|
49
|
-
if (typeof value === "string") return value;
|
|
50
|
-
if (Array.isArray(value)) {
|
|
51
|
-
const text = value
|
|
52
|
-
.filter((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string")
|
|
53
|
-
.map((part) => part.text)
|
|
54
|
-
.join("\n")
|
|
55
|
-
.trim();
|
|
56
|
-
if (text) return text;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function hostFromInput(input) {
|
|
63
|
-
const explicit = input.host || input.provider || process.env.AGENTSPINE_HOST;
|
|
64
|
-
if (["claude", "codex", "generic"].includes(explicit)) return explicit;
|
|
65
|
-
if ((typeof input.model === "string" && input.model.trim()) || process.env.PLUGIN_ROOT || process.env.CODEX_HOME
|
|
66
|
-
|| process.env.BLUN_PLUGIN_ROOT || process.env.BLUN_HOME) return "codex";
|
|
67
|
-
return "claude";
|
|
68
|
-
}
|
|
47
|
+
function promptFromInput(input) {
|
|
48
|
+
for (const key of ["prompt", "user_prompt", "message", "input"]) {
|
|
49
|
+
const value = input[key];
|
|
50
|
+
if (typeof value === "string") return value;
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
const text = value
|
|
53
|
+
.filter((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string")
|
|
54
|
+
.map((part) => part.text)
|
|
55
|
+
.join("\n")
|
|
56
|
+
.trim();
|
|
57
|
+
if (text) return text;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hostFromInput(input) {
|
|
64
|
+
const explicit = input.host || input.provider || process.env.AGENTSPINE_HOST;
|
|
65
|
+
if (["claude", "codex", "generic"].includes(explicit)) return explicit;
|
|
66
|
+
if ((typeof input.model === "string" && input.model.trim()) || process.env.PLUGIN_ROOT || process.env.CODEX_HOME
|
|
67
|
+
|| process.env.BLUN_PLUGIN_ROOT || process.env.BLUN_HOME) return "codex";
|
|
68
|
+
return "claude";
|
|
69
|
+
}
|
|
69
70
|
|
|
70
71
|
function gatewayEnvironmentContext(env = process.env) {
|
|
71
72
|
if (env.AGENTSPINE_GATEWAY_CONTEXT !== "agentspine.gateway-start/v1") return null;
|
|
@@ -109,7 +110,7 @@ async function runtimeScope(input, root, userStateRoot = null) {
|
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
function renderContext(event, catalog, briefing, signal = null, attentionEvent = null, selfstarter = null, channelEvent = null, sourceDiagnostics = null) {
|
|
113
|
+
function renderContext(event, catalog, briefing, signal = null, attentionEvent = null, selfstarter = null, channelEvent = null, sourceDiagnostics = null, preflight = null) {
|
|
113
114
|
const loaded = sourceDiagnostics?.status === "loaded";
|
|
114
115
|
const packet = {
|
|
115
116
|
schema: "agentspine.hook-context/v1",
|
|
@@ -175,6 +176,23 @@ function renderContext(event, catalog, briefing, signal = null, attentionEvent =
|
|
|
175
176
|
} : null,
|
|
176
177
|
indexedSources: catalog.summary.total,
|
|
177
178
|
sourceResolution: sourceDiagnostics,
|
|
179
|
+
preflight: preflight ? {
|
|
180
|
+
schema: preflight.receipt.schema,
|
|
181
|
+
receiptId: preflight.receipt.id,
|
|
182
|
+
promptDigest: preflight.receipt.promptDigest,
|
|
183
|
+
briefingDigest: preflight.receipt.briefingDigest,
|
|
184
|
+
createdAt: preflight.receipt.createdAt,
|
|
185
|
+
expiresAt: preflight.receipt.expiresAt,
|
|
186
|
+
policy: preflight.policy,
|
|
187
|
+
pendingMustRemember: preflight.pendingMustRemember ? {
|
|
188
|
+
id: preflight.pendingMustRemember.candidate?.id || null,
|
|
189
|
+
status: preflight.pendingMustRemember.candidate?.status || (preflight.pendingMustRemember.rejected ? "rejected" : null),
|
|
190
|
+
reason: preflight.pendingMustRemember.reason || null
|
|
191
|
+
} : null,
|
|
192
|
+
briefing: preflight.briefing,
|
|
193
|
+
instruction: "This exact turn passed the mandatory pre-answer gate. Host-native instruction files were verified but not duplicated; apply the host-loaded instructions and the remaining preflight briefing before answering.",
|
|
194
|
+
authority: "preflight-proof-only"
|
|
195
|
+
} : null,
|
|
178
196
|
briefing,
|
|
179
197
|
authority: "context-only"
|
|
180
198
|
};
|
|
@@ -344,65 +362,65 @@ async function captureAttentionLifecycle(input, event, root, scope) {
|
|
|
344
362
|
});
|
|
345
363
|
}
|
|
346
364
|
|
|
347
|
-
export function blunRuntimeContext(context) {
|
|
348
|
-
const detailed = JSON.parse(context);
|
|
349
|
-
const sourceResolution = detailed.sourceResolution ? {
|
|
350
|
-
status: detailed.sourceResolution.status || null,
|
|
351
|
-
reason: detailed.sourceResolution.reason || null
|
|
352
|
-
} : null;
|
|
353
|
-
const runtime = {
|
|
354
|
-
schema: "agentspine.blun-runtime-context/v1",
|
|
355
|
-
event: detailed.event,
|
|
356
|
-
loaded: Boolean(detailed.loaded),
|
|
357
|
-
failedClosed: detailed.failedClosed ? true : undefined,
|
|
358
|
-
indexedSources: detailed.indexedSources || 0,
|
|
359
|
-
sourceResolution,
|
|
360
|
-
instruction: detailed.loaded
|
|
361
|
-
? "Detailed AgentSpine context is available on demand through session_briefing. Load it only when the current request needs continuity."
|
|
362
|
-
: detailed.instruction,
|
|
363
|
-
authority: "context-only"
|
|
364
|
-
};
|
|
365
|
-
if (detailed.signal && (detailed.signal.captured || detailed.signal.accepted || detailed.signal.reason)) {
|
|
366
|
-
runtime.signal = detailed.signal;
|
|
367
|
-
}
|
|
368
|
-
if (detailed.attentionEvent
|
|
369
|
-
&& (detailed.attentionEvent.captured || detailed.attentionEvent.duplicate || detailed.attentionEvent.reason)) {
|
|
370
|
-
runtime.attentionEvent = detailed.attentionEvent;
|
|
371
|
-
}
|
|
372
|
-
if (detailed.selfstarter && (detailed.selfstarter.active || detailed.selfstarter.blocked)) {
|
|
373
|
-
runtime.selfstarter = detailed.selfstarter;
|
|
374
|
-
}
|
|
375
|
-
if (detailed.channelEvent?.active) runtime.channelEvent = detailed.channelEvent;
|
|
376
|
-
return JSON.stringify(runtime);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export function blunRuntimeMessage(context) {
|
|
380
|
-
const runtime = JSON.parse(blunRuntimeContext(context));
|
|
381
|
-
const base = runtime.loaded
|
|
382
|
-
? `AgentSpine ready: ${runtime.indexedSources} sources indexed. Load detailed continuity only on demand through session_briefing.`
|
|
383
|
-
: `AgentSpine unavailable${runtime.sourceResolution?.reason ? `: ${runtime.sourceResolution.reason}` : ""}. ${runtime.instruction}`;
|
|
384
|
-
const active = {};
|
|
385
|
-
if (runtime.signal && (runtime.signal.captured || runtime.signal.accepted
|
|
386
|
-
|| String(runtime.signal.reason || "").startsWith("rejected:"))) {
|
|
387
|
-
active.signal = runtime.signal;
|
|
388
|
-
}
|
|
389
|
-
if (runtime.attentionEvent && (runtime.attentionEvent.captured || runtime.attentionEvent.duplicate
|
|
390
|
-
|| String(runtime.attentionEvent.reason || "").startsWith("rejected:"))) {
|
|
391
|
-
active.attentionEvent = runtime.attentionEvent;
|
|
392
|
-
}
|
|
393
|
-
if (runtime.selfstarter) active.selfstarter = runtime.selfstarter;
|
|
394
|
-
if (runtime.channelEvent) active.channelEvent = runtime.channelEvent;
|
|
395
|
-
return Object.keys(active).length === 0
|
|
396
|
-
? base
|
|
397
|
-
: `${base}\nActive AgentSpine runtime data: ${JSON.stringify(active)}`;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function hookOutput(event, context) {
|
|
401
|
-
if (process.env.BLUN_PLUGIN_ROOT) {
|
|
402
|
-
return { hookSpecificOutput: { hookEventName: event, message: blunRuntimeMessage(context) } };
|
|
403
|
-
}
|
|
404
|
-
return { hookSpecificOutput: { hookEventName: event, additionalContext: context } };
|
|
405
|
-
}
|
|
365
|
+
export function blunRuntimeContext(context) {
|
|
366
|
+
const detailed = JSON.parse(context);
|
|
367
|
+
const sourceResolution = detailed.sourceResolution ? {
|
|
368
|
+
status: detailed.sourceResolution.status || null,
|
|
369
|
+
reason: detailed.sourceResolution.reason || null
|
|
370
|
+
} : null;
|
|
371
|
+
const runtime = {
|
|
372
|
+
schema: "agentspine.blun-runtime-context/v1",
|
|
373
|
+
event: detailed.event,
|
|
374
|
+
loaded: Boolean(detailed.loaded),
|
|
375
|
+
failedClosed: detailed.failedClosed ? true : undefined,
|
|
376
|
+
indexedSources: detailed.indexedSources || 0,
|
|
377
|
+
sourceResolution,
|
|
378
|
+
instruction: detailed.loaded
|
|
379
|
+
? "Detailed AgentSpine context is available on demand through session_briefing. Load it only when the current request needs continuity."
|
|
380
|
+
: detailed.instruction,
|
|
381
|
+
authority: "context-only"
|
|
382
|
+
};
|
|
383
|
+
if (detailed.signal && (detailed.signal.captured || detailed.signal.accepted || detailed.signal.reason)) {
|
|
384
|
+
runtime.signal = detailed.signal;
|
|
385
|
+
}
|
|
386
|
+
if (detailed.attentionEvent
|
|
387
|
+
&& (detailed.attentionEvent.captured || detailed.attentionEvent.duplicate || detailed.attentionEvent.reason)) {
|
|
388
|
+
runtime.attentionEvent = detailed.attentionEvent;
|
|
389
|
+
}
|
|
390
|
+
if (detailed.selfstarter && (detailed.selfstarter.active || detailed.selfstarter.blocked)) {
|
|
391
|
+
runtime.selfstarter = detailed.selfstarter;
|
|
392
|
+
}
|
|
393
|
+
if (detailed.channelEvent?.active) runtime.channelEvent = detailed.channelEvent;
|
|
394
|
+
return JSON.stringify(runtime);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function blunRuntimeMessage(context) {
|
|
398
|
+
const runtime = JSON.parse(blunRuntimeContext(context));
|
|
399
|
+
const base = runtime.loaded
|
|
400
|
+
? `AgentSpine ready: ${runtime.indexedSources} sources indexed. Load detailed continuity only on demand through session_briefing.`
|
|
401
|
+
: `AgentSpine unavailable${runtime.sourceResolution?.reason ? `: ${runtime.sourceResolution.reason}` : ""}. ${runtime.instruction}`;
|
|
402
|
+
const active = {};
|
|
403
|
+
if (runtime.signal && (runtime.signal.captured || runtime.signal.accepted
|
|
404
|
+
|| String(runtime.signal.reason || "").startsWith("rejected:"))) {
|
|
405
|
+
active.signal = runtime.signal;
|
|
406
|
+
}
|
|
407
|
+
if (runtime.attentionEvent && (runtime.attentionEvent.captured || runtime.attentionEvent.duplicate
|
|
408
|
+
|| String(runtime.attentionEvent.reason || "").startsWith("rejected:"))) {
|
|
409
|
+
active.attentionEvent = runtime.attentionEvent;
|
|
410
|
+
}
|
|
411
|
+
if (runtime.selfstarter) active.selfstarter = runtime.selfstarter;
|
|
412
|
+
if (runtime.channelEvent) active.channelEvent = runtime.channelEvent;
|
|
413
|
+
return Object.keys(active).length === 0
|
|
414
|
+
? base
|
|
415
|
+
: `${base}\nActive AgentSpine runtime data: ${JSON.stringify(active)}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function hookOutput(event, context) {
|
|
419
|
+
if (process.env.BLUN_PLUGIN_ROOT) {
|
|
420
|
+
return { hookSpecificOutput: { hookEventName: event, message: blunRuntimeMessage(context) } };
|
|
421
|
+
}
|
|
422
|
+
return { hookSpecificOutput: { hookEventName: event, additionalContext: context } };
|
|
423
|
+
}
|
|
406
424
|
|
|
407
425
|
function candidatePaths(value, output = []) {
|
|
408
426
|
if (!value) return output;
|
|
@@ -459,16 +477,44 @@ function deny(reason) {
|
|
|
459
477
|
})}\n`);
|
|
460
478
|
}
|
|
461
479
|
|
|
480
|
+
function blockPrompt(reason) {
|
|
481
|
+
process.stdout.write(`${JSON.stringify({
|
|
482
|
+
decision: "block",
|
|
483
|
+
reason,
|
|
484
|
+
hookSpecificOutput: { hookEventName: "UserPromptSubmit", decision: "block", reason }
|
|
485
|
+
})}\n`);
|
|
486
|
+
}
|
|
487
|
+
|
|
462
488
|
export async function runHook(payload = null) {
|
|
463
489
|
const input = payload || await readStdin();
|
|
464
490
|
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("hook input must be one JSON object");
|
|
465
491
|
const event = input.hook_event_name || input.event_name || "";
|
|
466
492
|
if (!KNOWN_EVENTS.has(event)) throw new Error(`unsupported hook event: ${event || "missing"}`);
|
|
493
|
+
if (event === "InstructionsLoaded") {
|
|
494
|
+
const file = input.file_path;
|
|
495
|
+
if (typeof file !== "string" || !file || !["User", "Project", "Local", "Managed"].includes(input.memory_type)
|
|
496
|
+
|| !["session_start", "nested_traversal", "path_glob_match", "include", "compact"].includes(input.load_reason)) {
|
|
497
|
+
throw new Error("InstructionsLoaded payload is invalid");
|
|
498
|
+
}
|
|
499
|
+
if (payload) return { blocked: false, observed: true };
|
|
500
|
+
process.stdout.write("{}\n");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
467
503
|
const cwd = await canonicalPath(input.cwd || process.cwd());
|
|
468
504
|
const host = hostFromInput(input);
|
|
505
|
+
const instructionHost = host === "generic" ? input.instruction_host : host;
|
|
506
|
+
if (host === "generic" && !["claude", "codex"].includes(instructionHost)) {
|
|
507
|
+
const reason = "AgentSpine generic hosts must bind instruction_host to claude or codex";
|
|
508
|
+
if (event === "UserPromptSubmit") {
|
|
509
|
+
if (payload) return { blocked: true, failedClosed: true, reason };
|
|
510
|
+
blockPrompt(reason);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
throw new Error(reason);
|
|
514
|
+
}
|
|
469
515
|
let resolvedSources;
|
|
470
516
|
try {
|
|
471
|
-
resolvedSources = await resolveHostSourceCatalog({ host, cwd, input });
|
|
517
|
+
resolvedSources = await resolveHostSourceCatalog({ host: instructionHost, cwd, input });
|
|
472
518
|
} catch (error) {
|
|
473
519
|
const reason = `AgentSpine source resolution failed closed: ${error.message}`;
|
|
474
520
|
if (event === "PreToolUse" && isMutationTool(input.tool_name)) {
|
|
@@ -476,6 +522,11 @@ export async function runHook(payload = null) {
|
|
|
476
522
|
deny(reason);
|
|
477
523
|
return;
|
|
478
524
|
}
|
|
525
|
+
if (event === "UserPromptSubmit") {
|
|
526
|
+
if (payload) return { blocked: true, failedClosed: true, reason };
|
|
527
|
+
blockPrompt(reason);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
479
530
|
const context = JSON.stringify({
|
|
480
531
|
schema: "agentspine.hook-context/v1", event, loaded: false, failedClosed: true,
|
|
481
532
|
indexedSources: 0, sourceResolution: { status: "failed-closed", reason: error.message },
|
|
@@ -580,6 +631,7 @@ export async function runHook(payload = null) {
|
|
|
580
631
|
|
|
581
632
|
if (CONTEXT_EVENTS.has(event)) {
|
|
582
633
|
let signal = null;
|
|
634
|
+
let preflight = null;
|
|
583
635
|
try {
|
|
584
636
|
await syncPersonaRosterFromEnvironment({ root, env: process.env, now: input.timestamp || new Date() });
|
|
585
637
|
scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot);
|
|
@@ -588,6 +640,15 @@ export async function runHook(payload = null) {
|
|
|
588
640
|
if (selfstarter?.job && !scope.currentTaskId) scope.currentTaskId = selfstarter.job.taskId;
|
|
589
641
|
if (event === "UserPromptSubmit") {
|
|
590
642
|
const prompt = promptFromInput(input);
|
|
643
|
+
if (prompt === null) throw new Error("mandatory preflight requires the exact current prompt");
|
|
644
|
+
preflight = await runPreflight({
|
|
645
|
+
input, scope, resolvedSources, prompt, now: input.timestamp || new Date(), env: process.env
|
|
646
|
+
});
|
|
647
|
+
if (!await verifyPreflightReceipt({
|
|
648
|
+
receipt: preflight.receipt, input, scope, resolvedSources, prompt,
|
|
649
|
+
now: input.timestamp || new Date(), env: process.env
|
|
650
|
+
})) throw new Error("newly created preflight receipt failed exact turn verification");
|
|
651
|
+
preflight.pendingMustRemember = await captureMustRememberPrompt({ prompt, receipt: preflight.receipt, env: process.env });
|
|
591
652
|
try {
|
|
592
653
|
attentionEvent = await captureAttentionLifecycle(input, event, root, scope);
|
|
593
654
|
} catch (error) {
|
|
@@ -609,17 +670,32 @@ export async function runHook(payload = null) {
|
|
|
609
670
|
root, cwd, host: scope.host, entityId: scope.entityId, groupId: scope.groupId,
|
|
610
671
|
projectId: scope.projectId, currentTaskId: scope.currentTaskId,
|
|
611
672
|
includePrivate: Boolean(scope.entityId && !scope.groupId),
|
|
612
|
-
focusActive: true, includeSourceContent: !scope.groupId,
|
|
613
|
-
maxBytes: scope.config.maxBriefingBytes,
|
|
673
|
+
focusActive: true, includeSourceContent: event === "UserPromptSubmit" ? false : !scope.groupId,
|
|
674
|
+
maxBytes: event === "UserPromptSubmit" ? 4096 : scope.config.maxBriefingBytes,
|
|
614
675
|
now: input.timestamp || new Date(),
|
|
615
676
|
catalog, userStateRoot: resolvedSources.userStateRoot, sourceDiagnostics: resolvedSources.diagnostics,
|
|
616
677
|
prompt: event === "UserPromptSubmit" ? promptFromInput(input) : null
|
|
617
678
|
});
|
|
618
|
-
const context = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent, resolvedSources.diagnostics);
|
|
619
|
-
if (
|
|
679
|
+
const context = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent, resolvedSources.diagnostics, preflight);
|
|
680
|
+
if (event === "UserPromptSubmit" && Buffer.byteLength(context) > 9500) {
|
|
681
|
+
throw new Error("mandatory preflight context exceeds the host hook injection limit");
|
|
682
|
+
}
|
|
683
|
+
if (event === "UserPromptSubmit" && !await verifyPreflightReceipt({
|
|
684
|
+
receipt: preflight.receipt, input, scope, resolvedSources, prompt: promptFromInput(input),
|
|
685
|
+
now: input.timestamp || new Date(), env: process.env, consume: true
|
|
686
|
+
})) throw new Error("preflight receipt could not be consumed atomically for this exact turn");
|
|
687
|
+
if (payload) return { blocked: false, context, briefing, preflight, signal, attentionEvent, channelEvent, catalogPath };
|
|
620
688
|
process.stdout.write(`${JSON.stringify(hookOutput(event, context))}\n`);
|
|
621
689
|
return;
|
|
622
690
|
} catch (error) {
|
|
691
|
+
if (event === "UserPromptSubmit") {
|
|
692
|
+
await recordPreflightFailure({ receiptId: preflight?.receipt?.id || null, input, host,
|
|
693
|
+
error, now: input.timestamp || new Date(), env: process.env }).catch(() => {});
|
|
694
|
+
const reason = `AgentSpine pre-answer preflight blocked this turn: ${error.message}`;
|
|
695
|
+
if (payload) return { blocked: true, failedClosed: true, reason, error: error.message, catalogPath };
|
|
696
|
+
blockPrompt(reason);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
623
699
|
const context = JSON.stringify({
|
|
624
700
|
schema: "agentspine.hook-context/v1", event, loaded: false, failedClosed: true,
|
|
625
701
|
indexedSources: catalog.summary.total,
|
|
@@ -641,6 +717,8 @@ export async function runHook(payload = null) {
|
|
|
641
717
|
if (isMainModule(import.meta.url)) {
|
|
642
718
|
runHook().catch((error) => {
|
|
643
719
|
process.stderr.write(`AgentSpine hook: ${String(error.message).slice(0, 2048)}\n`);
|
|
644
|
-
|
|
720
|
+
// Claude command hooks treat exit 2 as a blocking failure. Exit 1 is
|
|
721
|
+
// fail-open, which is unsafe for malformed pre-answer payloads.
|
|
722
|
+
process.exitCode = 2;
|
|
645
723
|
});
|
|
646
724
|
}
|
|
@@ -49,6 +49,12 @@ export {
|
|
|
49
49
|
} from "./lib/gateway-runtime.js";
|
|
50
50
|
export { createTelegramAdapter } from "./lib/telegram-adapter.js";
|
|
51
51
|
export { evaluateVoiceOutput, voiceCue } from "./lib/voice-runtime.js";
|
|
52
|
+
export {
|
|
53
|
+
MUST_REMEMBER_SCHEMA, PREFLIGHT_POLICY_SCHEMA, PREFLIGHT_SCHEMA, RETRIEVAL_QUERY_SCHEMA,
|
|
54
|
+
RETRIEVAL_RESULT_SCHEMA, captureMustRememberPrompt, configurePreflightPolicy, confirmMustRemember, preflightStatus,
|
|
55
|
+
recordPreflightFailure,
|
|
56
|
+
proposeMustRemember, purgeMustRemember, rollbackMustRemember, runPreflight, verifyPreflightReceipt
|
|
57
|
+
} from "./lib/preflight.js";
|
|
52
58
|
export { runWorker, runWorkerTick } from "./worker.js";
|
|
53
59
|
export {
|
|
54
60
|
configureSharing, deleteShared, initDirectoryAdapter, inspectSharing, loadSharing,
|
|
@@ -8,6 +8,7 @@ import { configureContinuity, purgeContinuity } from "./continuity.js";
|
|
|
8
8
|
import { createTask } from "./coordination.js";
|
|
9
9
|
import { linkEntities, upsertEntity } from "./graph.js";
|
|
10
10
|
import { loadLearning, rollbackLearning } from "./learning.js";
|
|
11
|
+
import { configurePreflightPolicy } from "./preflight.js";
|
|
11
12
|
import {
|
|
12
13
|
grantExecution, loadSelfstarter, registerJob
|
|
13
14
|
} from "./selfstarter.js";
|
|
@@ -74,6 +75,28 @@ export async function runVisibleAcceptance() {
|
|
|
74
75
|
const expectedSources = sourceHashes(sources);
|
|
75
76
|
try {
|
|
76
77
|
for (const [name, content] of Object.entries(sources)) await writeFile(join(projectRoot, name), content, "utf8");
|
|
78
|
+
const adapterPath = join(projectRoot, "mnemo-acceptance.mjs");
|
|
79
|
+
const adapterSource = `let body = "";
|
|
80
|
+
process.stdin.setEncoding("utf8");
|
|
81
|
+
process.stdin.on("data", (chunk) => { body += chunk; });
|
|
82
|
+
process.stdin.on("end", () => {
|
|
83
|
+
const query = JSON.parse(body);
|
|
84
|
+
process.stdout.write(JSON.stringify({
|
|
85
|
+
schema: "agentspine.retrieval-result/v1", providerId: query.providerId,
|
|
86
|
+
queryDigest: query.queryDigest, status: "ok", rejected: 1,
|
|
87
|
+
items: [{ id: "mnemo:aurora-finding", revision: "1", claim: "Prüfe die bestehenden Findings vor neuen Änderungen.",
|
|
88
|
+
source: "mnemo", scope: { agentId: query.agentId, userId: query.userId, tenantId: query.tenantId,
|
|
89
|
+
projectId: query.projectId }, validity: "current", confidence: 1, whyLoaded: "exact project scope" }]
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
`;
|
|
93
|
+
await writeFile(adapterPath, adapterSource, "utf8");
|
|
94
|
+
await configurePreflightPolicy({ confirmation: "local-owner-confirmed", env: process.env, profile: {
|
|
95
|
+
id: "preflight-policy:acceptance-freja", agentId: "person:freja", host: "claude", profileId: "default",
|
|
96
|
+
tenantId: "local-tenant", enabled: true, providers: [{ schema: "agentspine.retrieval-provider/v1",
|
|
97
|
+
id: "mnemo:acceptance", adapter: "mnemo-command/v1", required: true, failClosed: true,
|
|
98
|
+
timeoutMs: 2000, command: process.execPath, args: [adapterPath], credentialEnv: [] }]
|
|
99
|
+
} });
|
|
77
100
|
const entities = [
|
|
78
101
|
["person:freja", "person", "Freja Åström"],
|
|
79
102
|
["person:lucia", "person", "Lucía Ortega"],
|
|
@@ -109,6 +132,23 @@ export async function runVisibleAcceptance() {
|
|
|
109
132
|
prompt: "Responde siempre de forma clara y breve."
|
|
110
133
|
});
|
|
111
134
|
requireCondition(swedish.signal?.accepted && spanish.signal?.accepted, "multilingual style signals were not accepted");
|
|
135
|
+
requireCondition(swedish.preflight?.briefing.instructions.some((item) => item.displayPath.endsWith("CLAUDE.md")
|
|
136
|
+
&& item.delivery === "host-native" && item.bytes === Buffer.byteLength(sources["CLAUDE.md"]) && !("content" in item)),
|
|
137
|
+
"mandatory CLAUDE.md identity was not verified without duplicate injection");
|
|
138
|
+
requireCondition(swedish.preflight?.briefing.retrieval[0]?.items[0]?.id === "mnemo:aurora-finding",
|
|
139
|
+
"required Mnemo retrieval was not present before the turn");
|
|
140
|
+
await rm(adapterPath);
|
|
141
|
+
const blockedRecall = await runHook({
|
|
142
|
+
hook_event_name: "UserPromptSubmit", host: "claude", cwd: projectRoot,
|
|
143
|
+
entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
|
|
144
|
+
session_id: "session:freja:blocked", event_id: "prompt:freja:blocked", timestamp: "2031-04-05T09:00:30.000Z",
|
|
145
|
+
prompt: "Det här svaret får inte skapas utan minne."
|
|
146
|
+
});
|
|
147
|
+
requireCondition(blockedRecall.blocked && blockedRecall.failedClosed, "missing required recall did not block the turn");
|
|
148
|
+
await writeFile(adapterPath, adapterSource, "utf8");
|
|
149
|
+
addCheck(checks, "pre-answer-recall", "Verpflichtender Pre-Answer-Recall",
|
|
150
|
+
"Vollständige CLAUDE.md und Mnemo wurden vor dem Turn geladen; fehlendes Mnemo blockierte sichtbar.",
|
|
151
|
+
[swedish.preflight.receipt.id, swedish.preflight.receipt.briefingDigest, blockedRecall.blocked]);
|
|
112
152
|
addCheck(checks, "languages", "Mehrsprachige Stilkontinuität", "Schwedische und spanische Stilwünsche wurden nach Opt-in minimal und belegt angenommen.", [swedish.signal.learningId, spanish.signal.learningId]);
|
|
113
153
|
|
|
114
154
|
await runHook({
|
|
@@ -21,6 +21,7 @@ import { gatewayHealthFindings, gatewayRuntimeFindings, inspectGatewayRuntime }
|
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
import { checkHosts } from "../../scripts/check-hosts.js";
|
|
23
23
|
import { resolveHostSourceCatalog } from "./source-roots.js";
|
|
24
|
+
import { preflightStatus } from "./preflight.js";
|
|
24
25
|
|
|
25
26
|
function gate(id, name, ok, detail, severity = "error") {
|
|
26
27
|
return { id, name, ok, severity, detail };
|
|
@@ -73,6 +74,9 @@ function forbiddenEntityKeys(graph) {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
export async function runAudit(root = process.cwd(), { host = null } = {}) {
|
|
77
|
+
let preflight = null;
|
|
78
|
+
let preflightError = null;
|
|
79
|
+
try { preflight = await preflightStatus(); } catch (error) { preflightError = error.message; }
|
|
76
80
|
let sourceResolution = null;
|
|
77
81
|
let sourceResolutionError = null;
|
|
78
82
|
let resolvedHostSources = null;
|
|
@@ -270,7 +274,9 @@ export async function runAudit(root = process.cwd(), { host = null } = {}) {
|
|
|
270
274
|
gate(4, "Native hierarchy", nativeMapped.every((document) => document.hosts.length > 0), `${nativeMapped.length} host-native documents mapped`),
|
|
271
275
|
gate(5, "Link integrity", brokenLinks.length === 0, brokenLinks.length ? `${brokenLinks.length} broken Markdown links` : "All indexed Markdown links resolve"),
|
|
272
276
|
gate(6, "Conflict visibility", Array.isArray(catalog.conflicts), `${reviewConflicts.length} precedence or classification findings exposed`, "warning"),
|
|
273
|
-
gate(7, "Authority boundary", authority.length === 0 && forbidden.length === 0 && policyIssues.length === 0 && coordinationAuthorityIssues.length === 0 && executionPolicyIssues.length === 0 && selfstarterAuthorityIssues.length === 0 && channelPolicyIssues.length === 0 && personaPolicyIssues.length === 0 && gatewayPolicyIssues.length === 0 && sharingAuthorityIssues.length === 0
|
|
277
|
+
gate(7, "Authority boundary", authority.length === 0 && forbidden.length === 0 && policyIssues.length === 0 && coordinationAuthorityIssues.length === 0 && executionPolicyIssues.length === 0 && selfstarterAuthorityIssues.length === 0 && channelPolicyIssues.length === 0 && personaPolicyIssues.length === 0 && gatewayPolicyIssues.length === 0 && sharingAuthorityIssues.length === 0 && !preflightError, preflightError
|
|
278
|
+
? `preflight policy or receipt state failed closed: ${preflightError}`
|
|
279
|
+
: `${authority.length} context authority violations; ${forbidden.length} forbidden entity records; ${policyIssues.length} delegation policy findings; ${coordinationAuthorityIssues.length} assignment findings; ${executionPolicyIssues.length} execution policy findings; ${selfstarterAuthorityIssues.length} self-starter authority findings; ${channelPolicyIssues.length} channel policy findings; ${personaPolicyIssues.length} persona policy findings; ${gatewayPolicyIssues.length} gateway policy findings; ${sharingAuthorityIssues.length} shared authority findings; preflight ${preflight.status}`),
|
|
274
280
|
gate(8, "Context privacy", privacyInvalid.length === 0 && attentionGroupInvalid.length === 0 && attentionConfigValid && attentionIssues.length === 0 && learningIssues.length === 0 && continuityIssues.length === 0 && coordinationContextIssues.length === 0 && selfstarterStateIssues.length === 0 && channelRuntimeIssues.length === 0 && personaStateIssues.length === 0 && gatewayStateIssues.length === 0 && sharingContextIssues.length === 0 && authenticationIssues.length === 0, `${graph.entities.length} entities, ${graph.entityEdges.length} relationships, ${attention.signals.length} attention cues, ${attention.events.length} lifecycle events, ${learning.candidates.length} learning records, ${continuity.signals.length} continuity signals, ${coordination.tasks.length} coordination items, ${selfstarter.jobs.length} self-starter jobs, ${channelRuntime.events.length} channel events, ${personaRuntime.personas.length} authenticated personas, ${gatewayRuntime.queue.length} gateway queue items, ${gatewayRuntime.outbox.length} delivery records, ${sharing.records.length} shared records, ${trust.records.length} trusted keys, ${registry.signers.length} local signers, and ${feedState.feeds.length} feed receipts checked`),
|
|
275
281
|
gate(9, "Context budget", loadedBytes <= context.budget.maxBytes && briefingBudgetValid, briefingError
|
|
276
282
|
? `${loadedBytes}/${context.budget.maxBytes} source bytes; briefing failed closed: ${briefingError}`
|
|
@@ -303,6 +309,7 @@ export async function runAudit(root = process.cwd(), { host = null } = {}) {
|
|
|
303
309
|
trustPath,
|
|
304
310
|
registryPath,
|
|
305
311
|
feedStatePath,
|
|
306
|
-
sourceResolution
|
|
312
|
+
sourceResolution,
|
|
313
|
+
preflight
|
|
307
314
|
};
|
|
308
315
|
}
|
|
@@ -239,11 +239,11 @@ function relationshipAudience(graph, groupId) {
|
|
|
239
239
|
return ids;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
async function assembleRelationshipContext({
|
|
243
243
|
root = process.cwd(), entityId, includePrivate = false, groupId = null, catalog: providedCatalog = null
|
|
244
|
-
}) {
|
|
244
|
+
}, loadGraphImpl) {
|
|
245
245
|
if (!entityId) throw new Error("entityId is required");
|
|
246
|
-
const { graph } = await
|
|
246
|
+
const { graph } = await loadGraphImpl(root, providedCatalog);
|
|
247
247
|
if (groupId !== null) {
|
|
248
248
|
const group = graph.entities.find((item) => item.id === groupId && item.kind === "group");
|
|
249
249
|
if (!group) throw new Error(`unknown group entity: ${groupId}`);
|
|
@@ -254,6 +254,7 @@ export async function relationshipContext({
|
|
|
254
254
|
const audience = groupId === null ? null : relationshipAudience(graph, groupId);
|
|
255
255
|
if (audience && !audience.has(entityId)) throw new Error(`entity is not a visible member of group: ${groupId}`);
|
|
256
256
|
const visible = (item) => {
|
|
257
|
+
if (item.attributes?.identityBindingId && item.attributes.identityStatus !== "active") return false;
|
|
257
258
|
if (item.privacy === "private") return includePrivate && groupId === null;
|
|
258
259
|
if (item.privacy === "group" && !audience) return false;
|
|
259
260
|
if (audience && item.id && !audience.has(item.id)) return false;
|
|
@@ -264,7 +265,12 @@ export async function relationshipContext({
|
|
|
264
265
|
const visibleEdge = (edge) => visible(edge)
|
|
265
266
|
&& (!audience || [edge.from, edge.to].every((id) => audience.has(id)))
|
|
266
267
|
&& [edge.from, edge.to].every((id) => !entities.has(id) || visible(entities.get(id)));
|
|
267
|
-
const
|
|
268
|
+
const directEdges = graph.entityEdges.filter((edge) => (edge.from === entityId || edge.to === entityId) && visibleEdge(edge));
|
|
269
|
+
const teamEdges = groupId === null ? [] : graph.entityEdges.filter((edge) => edge.relation === "member-of"
|
|
270
|
+
&& (edge.from === groupId || edge.to === groupId) && visibleEdge(edge));
|
|
271
|
+
const edgeKey = (edge) => `${edge.from}\0${edge.to}\0${edge.relation}`;
|
|
272
|
+
const edges = [...new Map([...directEdges, ...teamEdges].map((edge) => [edgeKey(edge), edge])).values()]
|
|
273
|
+
.sort((left, right) => edgeKey(left).localeCompare(edgeKey(right)));
|
|
268
274
|
const ids = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
|
|
269
275
|
ids.add(entityId);
|
|
270
276
|
return {
|
|
@@ -282,3 +288,15 @@ export async function relationshipContext({
|
|
|
282
288
|
authority: "context-only"
|
|
283
289
|
};
|
|
284
290
|
}
|
|
291
|
+
|
|
292
|
+
export async function relationshipContext(options = {}, { loadGraphImpl = loadGraph, timeoutMs = 5000 } = {}) {
|
|
293
|
+
let timer;
|
|
294
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
295
|
+
timer = setTimeout(() => reject(new Error(`relationship context exceeded its ${timeoutMs} ms local read limit`)), timeoutMs);
|
|
296
|
+
});
|
|
297
|
+
try {
|
|
298
|
+
return await Promise.race([assembleRelationshipContext(options, loadGraphImpl), deadline]);
|
|
299
|
+
} finally {
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
}
|
|
302
|
+
}
|