blun-king-cli 9.1.562 → 9.1.564

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.
@@ -5,9 +5,10 @@ import { recordAttentionEvent } from "./lib/attention.js";
5
5
  import { catalogForStateRoot, saveCatalog } from "./lib/catalog.js";
6
6
  import { loadGraph } from "./lib/graph.js";
7
7
  import { canonicalPath } from "./lib/paths.js";
8
- import { resolveHostSourceCatalog } from "./lib/source-roots.js";
8
+ import { isInaccessibleScanError, resolveHostSourceCatalog } from "./lib/source-roots.js";
9
9
  import { sessionBriefing } from "./lib/briefing.js";
10
10
  import { captureContinuityPrompt, loadContinuity } from "./lib/continuity.js";
11
+ import { recordLearningApplications, recordLearningDeliveries } from "./lib/learning.js";
11
12
  import {
12
13
  authorizeJobEffect, checkpointJobEffect, closeJobLease, resolveSessionJob, startOrResumeJob
13
14
  } from "./lib/selfstarter.js";
@@ -15,6 +16,7 @@ import { claimChannelEvent } from "./lib/channel-runtime.js";
15
16
  import { syncPersonaRosterFromEnvironment } from "./lib/persona-runtime.js";
16
17
  import { captureMustRememberPrompt, recordPreflightFailure, runPreflight, verifyPreflightReceipt } from "./lib/preflight.js";
17
18
  import { isMainModule } from "./lib/runtime.js";
19
+ import { recordHookScanAudit } from "./lib/hook-audit.js";
18
20
 
19
21
  const MAX_STDIN_BYTES = 64 * 1024;
20
22
  const STANDARD_HOST_CONTEXT_BYTES = 9500;
@@ -26,15 +28,25 @@ const KNOWN_EVENTS = new Set([
26
28
  const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
27
29
  const ATTENTION_WRITE_EVENTS = new Set(["UserPromptSubmit", "PostToolUse", "Stop", "SubagentStop"]);
28
30
  const SELFSTART_EVENTS = new Set(["SessionStart", "PostCompact"]);
31
+ const SILENT_OVERSIZE_POST_TOOL_USE = Symbol("silent-oversize-post-tool-use");
32
+ const SILENT_OVERSIZE_POST_TOOL_USE_ARG = "--silent-oversize-post-tool-use";
29
33
 
30
- async function readStdin() {
31
- let value = "";
34
+ async function readStdin({ silentOversizePostToolUse = false } = {}) {
35
+ const chunks = [];
32
36
  let bytes = 0;
37
+ let oversized = false;
33
38
  for await (const chunk of process.stdin) {
34
- bytes += Buffer.byteLength(chunk);
35
- if (bytes > MAX_STDIN_BYTES) throw new Error("hook input exceeds the 64 KiB limit");
36
- value += chunk;
39
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
40
+ const remaining = Math.max(0, MAX_STDIN_BYTES - bytes);
41
+ if (remaining) chunks.push(buffer.subarray(0, remaining));
42
+ bytes += buffer.length;
43
+ if (bytes > MAX_STDIN_BYTES) oversized = true;
37
44
  }
45
+ if (oversized) {
46
+ if (silentOversizePostToolUse) return SILENT_OVERSIZE_POST_TOOL_USE;
47
+ throw new Error("hook input exceeds the 64 KiB limit");
48
+ }
49
+ const value = Buffer.concat(chunks, bytes).toString("utf8");
38
50
  const parsed = value.trim() ? JSON.parse(value) : {};
39
51
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("hook input must be one JSON object");
40
52
  return parsed;
@@ -196,6 +208,7 @@ function renderContext(event, catalog, briefing, signal = null, attentionEvent =
196
208
  createdAt: preflight.receipt.createdAt,
197
209
  expiresAt: preflight.receipt.expiresAt,
198
210
  policy: preflight.policy,
211
+ learningApplications: preflight.learningApplications || null,
199
212
  pendingMustRemember: preflight.pendingMustRemember ? {
200
213
  id: preflight.pendingMustRemember.candidate?.id || null,
201
214
  status: preflight.pendingMustRemember.candidate?.status || (preflight.pendingMustRemember.rejected ? "rejected" : null),
@@ -459,6 +472,30 @@ function isMutationTool(name = "") {
459
472
  return /(^|__)(apply_patch|edit|write|delete|move|rename|bash|exec_command|shell)(_|$)/i.test(name);
460
473
  }
461
474
 
475
+ function filesystemScanError(error) {
476
+ return Boolean(error && (isInaccessibleScanError(error)
477
+ || error.code === "AGENTSPINE_SCAN_INCOMPLETE" || error.agentSpineScan === true));
478
+ }
479
+
480
+ async function auditSkippedScans(input, phase, skipped = []) {
481
+ for (const item of skipped) {
482
+ await recordHookScanAudit({
483
+ event: "PreToolUse", toolName: input.tool_name || null, phase,
484
+ error: { code: item.code, message: `${item.code}: ${item.kind || item.operation || "scan"} skipped ${item.relativePath || item.path}` },
485
+ path: item.path || item.relativePath, operation: item.operation || item.kind,
486
+ now: input.timestamp || new Date()
487
+ });
488
+ }
489
+ }
490
+
491
+ async function allowScanFailure(input, phase, error) {
492
+ await recordHookScanAudit({
493
+ event: "PreToolUse", toolName: input.tool_name || null, phase, error,
494
+ path: error?.path || input.cwd || process.cwd(), now: input.timestamp || new Date()
495
+ });
496
+ return { blocked: false, degraded: true, scanFailedOpen: true, phase, error: error.message };
497
+ }
498
+
462
499
  function stringValues(value, output = []) {
463
500
  if (typeof value === "string") output.push(value);
464
501
  else if (Array.isArray(value)) value.forEach((item) => stringValues(item, output));
@@ -501,8 +538,9 @@ function blockPrompt(reason) {
501
538
  })}\n`);
502
539
  }
503
540
 
504
- export async function runHook(payload = null) {
505
- const input = payload || await readStdin();
541
+ export async function runHook(payload = null, options = {}) {
542
+ const input = payload || await readStdin(options);
543
+ if (input === SILENT_OVERSIZE_POST_TOOL_USE) return;
506
544
  if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("hook input must be one JSON object");
507
545
  const event = input.hook_event_name || input.event_name || "";
508
546
  if (!KNOWN_EVENTS.has(event)) throw new Error(`unsupported hook event: ${event || "missing"}`);
@@ -533,6 +571,19 @@ export async function runHook(payload = null) {
533
571
  resolvedSources = await resolveHostSourceCatalog({ host: instructionHost, cwd, input });
534
572
  } catch (error) {
535
573
  const reason = `AgentSpine source resolution failed closed: ${error.message}`;
574
+ if (event === "PreToolUse" && isMutationTool(input.tool_name) && filesystemScanError(error)) {
575
+ const allowed = await allowScanFailure(input, "source-resolution", error);
576
+ const context = JSON.stringify({
577
+ schema: "agentspine.hook-context/v1", event, loaded: false, failedClosed: false,
578
+ indexedSources: 0,
579
+ sourceResolution: { status: "degraded", reason: error.message, auditFinding: "inaccessible-source-scan" },
580
+ instruction: "The filesystem scan was incomplete. The requested tool remains governed by native host permissions.",
581
+ authority: "context-only"
582
+ });
583
+ if (payload) return { ...allowed, context };
584
+ process.stdout.write("{}\n");
585
+ return;
586
+ }
536
587
  if (event === "PreToolUse" && isMutationTool(input.tool_name)) {
537
588
  if (payload) return { blocked: true, failedClosed: true, reason };
538
589
  deny(reason);
@@ -560,6 +611,12 @@ export async function runHook(payload = null) {
560
611
  let scope = null;
561
612
  let selfstarter = null;
562
613
  let channelEvent = null;
614
+ let learningDelivery = null;
615
+
616
+ if (event === "PreToolUse" && isMutationTool(input.tool_name)
617
+ && resolvedSources.diagnostics.skippedInaccessibleDirectories?.length) {
618
+ await auditSkippedScans(input, "source-resolution", resolvedSources.diagnostics.skippedInaccessibleDirectories);
619
+ }
563
620
 
564
621
  if (event === "PreToolUse" && isMutationTool(input.tool_name)) {
565
622
  const { graph } = await loadGraph(root, catalog);
@@ -610,6 +667,12 @@ export async function runHook(payload = null) {
610
667
  });
611
668
  }
612
669
  } catch (error) {
670
+ if (isMutationTool(input.tool_name) && filesystemScanError(error)) {
671
+ const allowed = await allowScanFailure(input, "self-starter", error);
672
+ if (payload) return allowed;
673
+ process.stdout.write("{}\n");
674
+ return;
675
+ }
613
676
  const reason = `AgentSpine self-starter denied this effect: ${error.message}`;
614
677
  if (payload) return { blocked: true, reason, selfstarter: { allowed: false, reason: error.message } };
615
678
  deny(reason);
@@ -637,6 +700,19 @@ export async function runHook(payload = null) {
637
700
  if (["Stop", "SubagentStop"].includes(event)) {
638
701
  scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
639
702
  const exact = await selfstarterScope(input, scope, root, "resume");
703
+ if (exact && !scope.currentTaskId) scope.currentTaskId = exact.taskId;
704
+ try {
705
+ learningDelivery = await recordLearningDeliveries({
706
+ root, sessionId: sessionId(input), hookEvent: event,
707
+ scope: {
708
+ personaId: scope.entityId, userId: scope.userId, tenantId: scope.tenantId,
709
+ projectId: scope.projectId, groupId: scope.groupId, taskId: scope.currentTaskId
710
+ },
711
+ completedAt: input.timestamp || new Date()
712
+ });
713
+ } catch (error) {
714
+ learningDelivery = { status: "degraded", receipts: [], reason: error.message, authority: "context-only" };
715
+ }
640
716
  const requested = selfstarterInput(input);
641
717
  if (exact) {
642
718
  selfstarter = await closeJobLease({
@@ -694,7 +770,7 @@ export async function runHook(payload = null) {
694
770
  catalog, userStateRoot: resolvedSources.userStateRoot, sourceDiagnostics: resolvedSources.diagnostics,
695
771
  prompt: event === "UserPromptSubmit" ? promptFromInput(input) : null
696
772
  });
697
- const context = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent, resolvedSources.diagnostics, preflight);
773
+ let context = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent, resolvedSources.diagnostics, preflight);
698
774
  if (event === "UserPromptSubmit" && Buffer.byteLength(context) > hostContextLimit(preflight)) {
699
775
  throw new Error("mandatory preflight context exceeds the host hook injection limit");
700
776
  }
@@ -702,6 +778,45 @@ export async function runHook(payload = null) {
702
778
  receipt: preflight.receipt, input, scope, resolvedSources, prompt: promptFromInput(input),
703
779
  now: input.timestamp || new Date(), env: process.env, consume: true
704
780
  })) throw new Error("preflight receipt could not be consumed atomically for this exact turn");
781
+ if (event === "UserPromptSubmit") {
782
+ const activeCanaries = briefing.learning.filter((item) => ["active", "revalidating"].includes(item.outcomeStatus));
783
+ if (!activeCanaries.length) {
784
+ preflight.learningApplications = {
785
+ status: "not-applicable", receipts: [], authority: "context-only"
786
+ };
787
+ } else try {
788
+ const application = await recordLearningApplications({
789
+ root, items: briefing.learning,
790
+ scope: {
791
+ personaId: scope.entityId, userId: scope.userId, tenantId: scope.tenantId,
792
+ projectId: scope.projectId, groupId: scope.groupId, taskId: scope.currentTaskId
793
+ },
794
+ preflightReceipt: preflight.receipt,
795
+ sessionBriefingDigest: createHash("sha256").update(JSON.stringify(briefing)).digest("hex"),
796
+ projectedAt: input.timestamp || new Date()
797
+ });
798
+ preflight.learningApplications = {
799
+ status: application.receipts.length ? "recorded" : "not-applicable",
800
+ receipts: application.receipts.map((item) => ({
801
+ id: item.id, learningId: item.learningId, projectedAt: item.projectedAt, expiresAt: item.expiresAt
802
+ })),
803
+ authority: "context-only"
804
+ };
805
+ } catch (error) {
806
+ briefing.learning = briefing.learning.filter((item) => !["active", "revalidating"].includes(item.outcomeStatus));
807
+ preflight.learningApplications = {
808
+ status: "degraded", receipts: [], reason: error.message, authority: "context-only"
809
+ };
810
+ }
811
+ const enriched = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent,
812
+ resolvedSources.diagnostics, preflight);
813
+ if (Buffer.byteLength(enriched) <= hostContextLimit(preflight)) context = enriched;
814
+ else if (preflight.learningApplications.status === "degraded") {
815
+ preflight.learningApplications = null;
816
+ context = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent,
817
+ resolvedSources.diagnostics, preflight);
818
+ }
819
+ }
705
820
  if (payload) return { blocked: false, context, briefing, preflight, signal, attentionEvent, channelEvent, catalogPath };
706
821
  process.stdout.write(`${JSON.stringify(hookOutput(event, context))}\n`);
707
822
  return;
@@ -728,12 +843,16 @@ export async function runHook(payload = null) {
728
843
  }
729
844
  }
730
845
 
731
- if (payload) return { blocked: false, attentionEvent, selfstarter };
846
+ if (payload) return { blocked: false, attentionEvent, selfstarter, learningDelivery };
732
847
  process.stdout.write("{}\n");
733
848
  }
734
849
 
735
850
  if (isMainModule(import.meta.url)) {
736
- runHook().catch((error) => {
851
+ const args = process.argv.slice(2);
852
+ const silentOversizePostToolUse = args.length === 1 && args[0] === SILENT_OVERSIZE_POST_TOOL_USE_ARG;
853
+ const argumentError = args.length && !silentOversizePostToolUse
854
+ ? new Error(`unsupported hook argument: ${args[0]}`) : null;
855
+ (argumentError ? Promise.reject(argumentError) : runHook(null, { silentOversizePostToolUse })).catch((error) => {
737
856
  process.stderr.write(`AgentSpine hook: ${String(error.message).slice(0, 2048)}\n`);
738
857
  // Claude command hooks treat exit 2 as a blocking failure. Exit 1 is
739
858
  // fail-open, which is unsafe for malformed pre-answer payloads.
@@ -12,9 +12,11 @@ export {
12
12
  recordActivity, recordAttentionEvent, resolveAttention, upsertAttention
13
13
  } from "./lib/attention.js";
14
14
  export {
15
- acceptContinuityLearning, addLearningEvidence, configureLearning, deleteLearning, evaluateLearning,
15
+ acceptContinuityLearning, addLearningEvidence, beginLearningRevalidation, configureLearning, deleteLearning, evaluateLearning,
16
16
  learningContext, learningOutcomeStatus, loadLearning, proposeLearning, purgeLearningBySubject,
17
- recordLearningOutcome, reviewLearning, rollbackLearning
17
+ purgeStaleLearningApplications, recordLearningOutcome, registerLearningEvaluation, renewLearningValidation,
18
+ reviewLearning, revokeLearningApplication, revokeLearningDelivery, revokeLearningEvaluation, revokeLearningEvidence, revokeLearningMeasurement, revokeLearningOutcome, revokeLearningTrialFailure, revokeLearningValidation,
19
+ rollbackLearning
18
20
  } from "./lib/learning.js";
19
21
  export {
20
22
  captureContinuityPrompt, configureContinuity, continuityFindings,
@@ -47,6 +47,24 @@ function authorityViolations(graph, attention, learning, continuity, coordinatio
47
47
  ]).filter(Boolean),
48
48
  ...(learning.outcomes || []),
49
49
  ...(learning.outcomes || []).map((outcome) => outcome.measurement).filter(Boolean),
50
+ ...(learning.measurements || []),
51
+ ...(learning.measurements || []).map((measurement) => measurement.measurement).filter(Boolean),
52
+ ...(learning.measurementLineage || []),
53
+ ...(learning.applications || []),
54
+ ...(learning.evaluations || []),
55
+ ...(learning.evaluatorRegistry || []),
56
+ ...(learning.evaluationBindings || []),
57
+ ...(learning.validationLeases || []),
58
+ ...(learning.trialFailures || []),
59
+ ...(learning.trialFailureRevocations || []),
60
+ ...(learning.trialRetryExhaustions || []),
61
+ ...(learning.evaluationRevocations || []),
62
+ ...(learning.validationRevocations || []),
63
+ ...(learning.evidenceRevocations || []),
64
+ ...(learning.measurementRevocations || []),
65
+ ...(learning.applicationRevocations || []),
66
+ ...(learning.deliveryRevocations || []),
67
+ ...(learning.outcomeRevocations || []),
50
68
  ...learning.history,
51
69
  ...learning.history.map((entry) => entry.value).filter(Boolean),
52
70
  ...continuity.signals,
@@ -270,7 +288,7 @@ export async function runAudit(root = process.cwd(), { host = null } = {}) {
270
288
  gate(2, "Discovery", catalog.schema === "agentspine.catalog/v1"
271
289
  && (!host || (sourceResolution?.status === "loaded" && !sourceResolutionError)), sourceResolutionError
272
290
  ? `${catalog.documents.length} project documents; host-native source resolution failed closed: ${sourceResolutionError}`
273
- : sourceResolution ? `${sourceResolution.scopes.user} user, ${sourceResolution.scopes.project} project, and ${sourceResolution.scopes["project-memory"]} memory sources; broad home scan disabled`
291
+ : sourceResolution ? `${sourceResolution.scopes.user} user, ${sourceResolution.scopes.project} project, and ${sourceResolution.scopes["project-memory"]} memory sources; broad home scan disabled; ${(sourceResolution.skippedInaccessibleDirectories || []).length} inaccessible directories skipped`
274
292
  : `${catalog.documents.length} Markdown documents indexed`),
275
293
  gate(3, "State isolation", [catalogPath, graphPath, attentionPath, learningPath, continuityPath,
276
294
  policyPath, coordinationPath, executionPolicyPath, selfstarterPath, channelPolicyPath,
@@ -286,7 +304,7 @@ export async function runAudit(root = process.cwd(), { host = null } = {}) {
286
304
  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
287
305
  ? `preflight policy or receipt state failed closed: ${preflightError}`
288
306
  : `${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}`),
289
- 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 and ${(learning.outcomes || []).length} outcome receipts, ${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`),
307
+ 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, ${(learning.evaluations || []).length} immutable evaluation contracts, ${(learning.evaluations || []).filter((item) => ["agentspine.learning-evaluation/v9", "agentspine.learning-evaluation/v10", "agentspine.learning-evaluation/v11", "agentspine.learning-evaluation/v12", "agentspine.learning-evaluation/v13", "agentspine.learning-evaluation/v14", "agentspine.learning-evaluation/v15", "agentspine.learning-evaluation/v16", "agentspine.learning-evaluation/v17", "agentspine.learning-evaluation/v18", "agentspine.learning-evaluation/v19", "agentspine.learning-evaluation/v20", "agentspine.learning-evaluation/v21", "agentspine.learning-evaluation/v22", "agentspine.learning-evaluation/v23", "agentspine.learning-evaluation/v24", "agentspine.learning-evaluation/v25"].includes(item.schema)).length} exact-target contracts, ${(learning.evaluations || []).filter((item) => ["agentspine.learning-evaluation/v8", "agentspine.learning-evaluation/v9", "agentspine.learning-evaluation/v10", "agentspine.learning-evaluation/v11", "agentspine.learning-evaluation/v12", "agentspine.learning-evaluation/v13", "agentspine.learning-evaluation/v14", "agentspine.learning-evaluation/v15", "agentspine.learning-evaluation/v16", "agentspine.learning-evaluation/v17", "agentspine.learning-evaluation/v18", "agentspine.learning-evaluation/v19", "agentspine.learning-evaluation/v20", "agentspine.learning-evaluation/v21", "agentspine.learning-evaluation/v22", "agentspine.learning-evaluation/v23", "agentspine.learning-evaluation/v24", "agentspine.learning-evaluation/v25"].includes(item.schema)).length} precommitted initial-trial contracts, ${(learning.evaluations || []).filter((item) => ["agentspine.learning-evaluation/v10", "agentspine.learning-evaluation/v11", "agentspine.learning-evaluation/v12", "agentspine.learning-evaluation/v13", "agentspine.learning-evaluation/v14", "agentspine.learning-evaluation/v15", "agentspine.learning-evaluation/v16", "agentspine.learning-evaluation/v17", "agentspine.learning-evaluation/v18", "agentspine.learning-evaluation/v19", "agentspine.learning-evaluation/v20", "agentspine.learning-evaluation/v21", "agentspine.learning-evaluation/v22", "agentspine.learning-evaluation/v23", "agentspine.learning-evaluation/v24", "agentspine.learning-evaluation/v25"].includes(item.schema)).length} deadline-bound contracts, ${(learning.evaluations || []).filter((item) => ["agentspine.learning-evaluation/v14", "agentspine.learning-evaluation/v15", "agentspine.learning-evaluation/v16", "agentspine.learning-evaluation/v17", "agentspine.learning-evaluation/v18", "agentspine.learning-evaluation/v19", "agentspine.learning-evaluation/v20", "agentspine.learning-evaluation/v21", "agentspine.learning-evaluation/v22", "agentspine.learning-evaluation/v23", "agentspine.learning-evaluation/v24", "agentspine.learning-evaluation/v25"].includes(item.schema)).length} staleness-bound contracts, ${(learning.evaluations || []).filter((item) => ["agentspine.learning-evaluation/v16", "agentspine.learning-evaluation/v17", "agentspine.learning-evaluation/v18", "agentspine.learning-evaluation/v19", "agentspine.learning-evaluation/v20", "agentspine.learning-evaluation/v21", "agentspine.learning-evaluation/v22", "agentspine.learning-evaluation/v23", "agentspine.learning-evaluation/v24", "agentspine.learning-evaluation/v25"].includes(item.schema)).length} promotion-bound contracts, ${(learning.evaluations || []).filter((item) => ["agentspine.learning-evaluation/v18", "agentspine.learning-evaluation/v19", "agentspine.learning-evaluation/v20", "agentspine.learning-evaluation/v21", "agentspine.learning-evaluation/v22", "agentspine.learning-evaluation/v23", "agentspine.learning-evaluation/v24", "agentspine.learning-evaluation/v25"].includes(item.schema)).length} candidate-admission contracts, ${(learning.evaluations || []).filter((item) => ["agentspine.learning-evaluation/v20", "agentspine.learning-evaluation/v21", "agentspine.learning-evaluation/v22", "agentspine.learning-evaluation/v23", "agentspine.learning-evaluation/v24", "agentspine.learning-evaluation/v25"].includes(item.schema)).length} candidate-evidence-cohort contracts, ${(learning.evaluations || []).filter((item) => ["agentspine.learning-evaluation/v22", "agentspine.learning-evaluation/v23", "agentspine.learning-evaluation/v24", "agentspine.learning-evaluation/v25"].includes(item.schema)).length} blocking-defect-bound contracts, ${(learning.evaluations || []).filter((item) => ["agentspine.learning-evaluation/v24", "agentspine.learning-evaluation/v25"].includes(item.schema)).length} evidence-source-bound contracts, ${(learning.evaluations || []).filter((item) => ["agentspine.learning-evaluation/v13", "agentspine.learning-evaluation/v15", "agentspine.learning-evaluation/v17", "agentspine.learning-evaluation/v19", "agentspine.learning-evaluation/v21", "agentspine.learning-evaluation/v23", "agentspine.learning-evaluation/v25"].includes(item.schema)).length} bounded retry contracts, ${(learning.trialRetryExhaustions || []).length} terminal retry-exhaustion receipts, ${(learning.applications || []).filter((item) => ["agentspine.learning-application/v5", "agentspine.learning-application/v6", "agentspine.learning-application/v7"].includes(item.schema)).length} immutable initial-trial admissions, ${(learning.applications || []).filter((item) => ["agentspine.learning-application/v6", "agentspine.learning-application/v7"].includes(item.schema)).length} exact-target admissions, ${(learning.applications || []).filter((item) => item.schema === "agentspine.learning-application/v7").length} deadline-bound admissions, ${(learning.trialFailures || []).length} blocking trial-failure receipts, ${(learning.evaluatorRegistry || []).length} locally confirmed evaluator roots, ${(learning.evaluationBindings || []).length} evaluator-registry bindings, ${(learning.validationLeases || []).length} immutable validation leases, ${(learning.measurements || []).length} immutable measurement runs, ${(learning.applications || []).length} turn-bound projections, ${(learning.deliveries || []).length} completed model-turn deliveries, ${(learning.evaluationRevocations || []).length} evaluation revocations, ${(learning.evidenceRevocations || []).length} evidence revocations, ${(learning.measurementRevocations || []).length} measurement revocations, ${(learning.applicationRevocations || []).length} application revocations, ${(learning.deliveryRevocations || []).length} delivery revocations, ${(learning.outcomeRevocations || []).length} outcome revocations, ${(learning.outcomes || []).length} outcome receipts, ${(learning.outcomes || []).filter((item) => ["agentspine.learning-outcome/v5", "agentspine.learning-outcome/v6", "agentspine.learning-outcome/v7", "agentspine.learning-outcome/v8", "agentspine.learning-outcome/v9"].includes(item.schema)).length} case-coverage receipts, ${(learning.outcomes || []).filter((item) => ["agentspine.learning-outcome/v6", "agentspine.learning-outcome/v7", "agentspine.learning-outcome/v8", "agentspine.learning-outcome/v9"].includes(item.schema)).length} provenance-bound receipts, ${(learning.outcomes || []).filter((item) => ["agentspine.learning-outcome/v7", "agentspine.learning-outcome/v8", "agentspine.learning-outcome/v9"].includes(item.schema)).length} lineage-bound receipts, ${(learning.outcomes || []).filter((item) => ["agentspine.learning-outcome/v8", "agentspine.learning-outcome/v9"].includes(item.schema)).length} paired-evaluator receipts and ${(learning.outcomes || []).filter((item) => item.schema === "agentspine.learning-outcome/v9").length} evaluator-root-bound receipts, ${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`),
290
308
  gate(9, "Context budget", loadedBytes <= context.budget.maxBytes && briefingBudgetValid, briefingError
291
309
  ? `${loadedBytes}/${context.budget.maxBytes} source bytes; briefing failed closed: ${briefingError}`
292
310
  : `${loadedBytes}/${context.budget.maxBytes} source bytes; ${briefingBytes}/${briefing.budget.maxBytes} briefing bytes`),
@@ -8,6 +8,11 @@ export function isFileLockContention(error, platform = process.platform) {
8
8
  || (platform === "win32" && ["EACCES", "EPERM"].includes(error?.code));
9
9
  }
10
10
 
11
+ export function isTransientLockMetadataError(error, platform = process.platform) {
12
+ return error?.code === "ENOENT"
13
+ || (platform === "win32" && WINDOWS_TRANSIENT_CODES.has(error?.code));
14
+ }
15
+
11
16
  export async function replaceFileWithRetry(temporary, target, options = {}) {
12
17
  const renameFile = options.renameFile || rename;
13
18
  const wait = options.wait || delay;
@@ -0,0 +1,29 @@
1
+ import { appendFile, mkdir } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { stateRoot } from "./paths.js";
4
+
5
+ export function hookScanAuditPath(env = process.env) {
6
+ return join(stateRoot(env), "hook-scan-audit.jsonl");
7
+ }
8
+
9
+ export async function recordHookScanAudit({ event = "PreToolUse", toolName = null, phase, error = null,
10
+ path = null, operation = null, now = new Date(), env = process.env }) {
11
+ try {
12
+ const target = hookScanAuditPath(env);
13
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
14
+ const record = {
15
+ schema: "agentspine.hook-scan-audit/v1",
16
+ at: new Date(now).toISOString(), event, toolName, phase,
17
+ code: String(error?.code || "SCAN_ERROR").slice(0, 64),
18
+ error: String(error?.message || error || "filesystem scan skipped").slice(0, 2048),
19
+ path: String(path || error?.path || "unknown").slice(0, 4096),
20
+ operation: operation || error?.syscall || null,
21
+ decision: "allow",
22
+ authority: "diagnostic-only"
23
+ };
24
+ await appendFile(target, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }