blun-king-cli 9.1.510 → 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.
Files changed (34) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LIESMICH.txt +12 -1
  3. package/README.md +12 -1
  4. package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
  5. package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
  6. package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
  7. package/agent-spine-plugin/CHANGELOG.md +70 -8
  8. package/agent-spine-plugin/README.md +1 -1
  9. package/agent-spine-plugin/blun.plugin.json +33 -33
  10. package/agent-spine-plugin/docs/acceptance.md +2 -2
  11. package/agent-spine-plugin/docs/gateway-runtime.md +8 -1
  12. package/agent-spine-plugin/docs/host-integration.md +34 -34
  13. package/agent-spine-plugin/docs/preflight-recall.md +69 -0
  14. package/agent-spine-plugin/docs/relationships.md +6 -0
  15. package/agent-spine-plugin/hooks/codex.json +47 -0
  16. package/agent-spine-plugin/hooks/hooks.json +11 -0
  17. package/agent-spine-plugin/hooks/version.json +2 -2
  18. package/agent-spine-plugin/package.json +4 -4
  19. package/agent-spine-plugin/scripts/check-hosts.js +53 -51
  20. package/agent-spine-plugin/scripts/check-install.js +53 -35
  21. package/agent-spine-plugin/scripts/release-check.js +11 -10
  22. package/agent-spine-plugin/skills/agent-spine/SKILL.md +1 -1
  23. package/agent-spine-plugin/src/cli.js +46 -1
  24. package/agent-spine-plugin/src/hook.js +168 -90
  25. package/agent-spine-plugin/src/index.js +6 -0
  26. package/agent-spine-plugin/src/lib/acceptance.js +40 -0
  27. package/agent-spine-plugin/src/lib/audit.js +9 -2
  28. package/agent-spine-plugin/src/lib/graph.js +22 -4
  29. package/agent-spine-plugin/src/lib/persona-runtime.js +103 -31
  30. package/agent-spine-plugin/src/lib/preflight.js +678 -0
  31. package/agent-spine-plugin/src/lib/source-roots.js +32 -32
  32. package/agent-spine-plugin/src/version.js +1 -1
  33. package/agent-spine-plugin/src/worker.js +20 -3
  34. package/package.json +1 -1
@@ -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 (payload) return { blocked: false, context, briefing, signal, attentionEvent, channelEvent, catalogPath };
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
- process.exitCode = 1;
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, `${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`),
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
- export async function relationshipContext({
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 loadGraph(root, providedCatalog);
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 edges = graph.entityEdges.filter((edge) => (edge.from === entityId || edge.to === entityId) && visibleEdge(edge));
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
+ }