blun-king-cli 9.1.486 → 9.1.489

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 (99) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LIESMICH.txt +28 -7
  3. package/README.md +16 -3
  4. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -0
  5. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -0
  6. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -0
  7. package/agent-spine-plugin/.mcp.json +8 -0
  8. package/agent-spine-plugin/CHANGELOG.md +277 -0
  9. package/agent-spine-plugin/LICENSE +186 -0
  10. package/agent-spine-plugin/README.md +342 -0
  11. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -0
  12. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -0
  13. package/agent-spine-plugin/bin/agentspine.js +7 -0
  14. package/agent-spine-plugin/blun.plugin.json +33 -0
  15. package/agent-spine-plugin/docs/acceptance.md +61 -0
  16. package/agent-spine-plugin/docs/architecture.md +183 -0
  17. package/agent-spine-plugin/docs/attention.md +121 -0
  18. package/agent-spine-plugin/docs/automatic-continuity.md +79 -0
  19. package/agent-spine-plugin/docs/channel-runtime.md +92 -0
  20. package/agent-spine-plugin/docs/coordination.md +138 -0
  21. package/agent-spine-plugin/docs/feed-transport.md +99 -0
  22. package/agent-spine-plugin/docs/gateway-runtime.md +109 -0
  23. package/agent-spine-plugin/docs/harness-reference.md +45 -0
  24. package/agent-spine-plugin/docs/host-integration.md +129 -0
  25. package/agent-spine-plugin/docs/https-transport.md +116 -0
  26. package/agent-spine-plugin/docs/learning.md +98 -0
  27. package/agent-spine-plugin/docs/object-transport.md +93 -0
  28. package/agent-spine-plugin/docs/peer-transport.md +88 -0
  29. package/agent-spine-plugin/docs/preservation-contract.md +53 -0
  30. package/agent-spine-plugin/docs/quality-gates.md +50 -0
  31. package/agent-spine-plugin/docs/relationships.md +67 -0
  32. package/agent-spine-plugin/docs/releasing.md +83 -0
  33. package/agent-spine-plugin/docs/roadmap.md +307 -0
  34. package/agent-spine-plugin/docs/selfstarter.md +88 -0
  35. package/agent-spine-plugin/docs/session-briefing.md +74 -0
  36. package/agent-spine-plugin/docs/shared-memory.md +259 -0
  37. package/agent-spine-plugin/docs/source-roots.md +86 -0
  38. package/agent-spine-plugin/docs/sqlite-transport.md +76 -0
  39. package/agent-spine-plugin/hooks/hooks.json +95 -0
  40. package/agent-spine-plugin/hooks/version.json +5 -0
  41. package/agent-spine-plugin/package.json +69 -0
  42. package/agent-spine-plugin/scripts/check-hosts.js +193 -0
  43. package/agent-spine-plugin/scripts/check-install.js +551 -0
  44. package/agent-spine-plugin/scripts/check-syntax.js +29 -0
  45. package/agent-spine-plugin/scripts/github-actions.js +11 -0
  46. package/agent-spine-plugin/scripts/release-check.js +127 -0
  47. package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
  48. package/agent-spine-plugin/scripts/run-checks.js +46 -0
  49. package/agent-spine-plugin/scripts/run-tests-hermetic.js +73 -0
  50. package/agent-spine-plugin/skill/SKILL.md +76 -0
  51. package/agent-spine-plugin/skills/agent-spine/SKILL.md +84 -0
  52. package/agent-spine-plugin/spine-example/1-identity.md +12 -0
  53. package/agent-spine-plugin/spine-example/2-voice.md +6 -0
  54. package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
  55. package/agent-spine-plugin/spine-example/4-history.md +4 -0
  56. package/agent-spine-plugin/src/cli.js +950 -0
  57. package/agent-spine-plugin/src/hook.js +646 -0
  58. package/agent-spine-plugin/src/index.js +84 -0
  59. package/agent-spine-plugin/src/lib/acceptance.js +294 -0
  60. package/agent-spine-plugin/src/lib/attention.js +755 -0
  61. package/agent-spine-plugin/src/lib/audit.js +308 -0
  62. package/agent-spine-plugin/src/lib/authentication.js +513 -0
  63. package/agent-spine-plugin/src/lib/briefing.js +305 -0
  64. package/agent-spine-plugin/src/lib/catalog.js +142 -0
  65. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -0
  66. package/agent-spine-plugin/src/lib/context.js +154 -0
  67. package/agent-spine-plugin/src/lib/continuity.js +333 -0
  68. package/agent-spine-plugin/src/lib/coordination.js +577 -0
  69. package/agent-spine-plugin/src/lib/documents.js +199 -0
  70. package/agent-spine-plugin/src/lib/feed-transport.js +497 -0
  71. package/agent-spine-plugin/src/lib/filesystem-retry.js +27 -0
  72. package/agent-spine-plugin/src/lib/gateway-runtime.js +760 -0
  73. package/agent-spine-plugin/src/lib/graph.js +284 -0
  74. package/agent-spine-plugin/src/lib/https-transport.js +392 -0
  75. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -0
  76. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -0
  77. package/agent-spine-plugin/src/lib/learning.js +645 -0
  78. package/agent-spine-plugin/src/lib/object-transport.js +206 -0
  79. package/agent-spine-plugin/src/lib/paths.js +60 -0
  80. package/agent-spine-plugin/src/lib/peer-transport.js +283 -0
  81. package/agent-spine-plugin/src/lib/persona-runtime.js +503 -0
  82. package/agent-spine-plugin/src/lib/runtime.js +13 -0
  83. package/agent-spine-plugin/src/lib/selfstarter.js +819 -0
  84. package/agent-spine-plugin/src/lib/sharing.js +969 -0
  85. package/agent-spine-plugin/src/lib/source-roots.js +451 -0
  86. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -0
  87. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -0
  88. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -0
  89. package/agent-spine-plugin/src/mcp.js +526 -0
  90. package/agent-spine-plugin/src/version.js +1 -0
  91. package/agent-spine-plugin/src/worker.js +178 -0
  92. package/bin/identity-context-policy.cjs +51 -1
  93. package/bin/launcher-runtime.js +42 -1
  94. package/bin/plugin-bootstrap.js +68 -27
  95. package/bin/repeated-user-message-projection.cjs +6 -0
  96. package/bin/turn-thinking-policy.cjs +5 -4
  97. package/bin/turn-tool-performance-policy.cjs +16 -2
  98. package/blun.mjs +187 -35
  99. package/package.json +3 -1
@@ -0,0 +1,646 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { isAbsolute, relative, resolve } from "node:path";
4
+ import { recordAttentionEvent } from "./lib/attention.js";
5
+ import { saveCatalog } from "./lib/catalog.js";
6
+ import { loadGraph } from "./lib/graph.js";
7
+ import { canonicalPath } from "./lib/paths.js";
8
+ import { resolveHostSourceCatalog } from "./lib/source-roots.js";
9
+ import { sessionBriefing } from "./lib/briefing.js";
10
+ import { captureContinuityPrompt, loadContinuity } from "./lib/continuity.js";
11
+ import {
12
+ authorizeJobEffect, checkpointJobEffect, closeJobLease, resolveSessionJob, startOrResumeJob
13
+ } from "./lib/selfstarter.js";
14
+ import { claimChannelEvent } from "./lib/channel-runtime.js";
15
+ import { syncPersonaRosterFromEnvironment } from "./lib/persona-runtime.js";
16
+ import { isMainModule } from "./lib/runtime.js";
17
+
18
+ const MAX_STDIN_BYTES = 64 * 1024;
19
+ const CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit", "PreCompact", "PostCompact"]);
20
+ const KNOWN_EVENTS = new Set([
21
+ ...CONTEXT_EVENTS, "PreToolUse", "PostToolUse", "Stop", "SubagentStop"
22
+ ]);
23
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
24
+ const ATTENTION_WRITE_EVENTS = new Set(["UserPromptSubmit", "PostToolUse", "Stop", "SubagentStop"]);
25
+ const SELFSTART_EVENTS = new Set(["SessionStart", "PostCompact"]);
26
+
27
+ async function readStdin() {
28
+ let value = "";
29
+ let bytes = 0;
30
+ for await (const chunk of process.stdin) {
31
+ bytes += Buffer.byteLength(chunk);
32
+ if (bytes > MAX_STDIN_BYTES) throw new Error("hook input exceeds the 64 KiB limit");
33
+ value += chunk;
34
+ }
35
+ const parsed = value.trim() ? JSON.parse(value) : {};
36
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("hook input must be one JSON object");
37
+ return parsed;
38
+ }
39
+
40
+ function boundedId(value, field) {
41
+ if (value === null || value === undefined || value === "") return null;
42
+ if (typeof value !== "string" || !ID_RE.test(value)) throw new Error(`${field} is invalid`);
43
+ return value;
44
+ }
45
+
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
+ }
69
+
70
+ function gatewayEnvironmentContext(env = process.env) {
71
+ if (env.AGENTSPINE_GATEWAY_CONTEXT !== "agentspine.gateway-start/v1") return null;
72
+ return {
73
+ entityId: boundedId(env.AGENTSPINE_ENTITY_ID, "AGENTSPINE_ENTITY_ID"),
74
+ groupId: boundedId(env.AGENTSPINE_GROUP_ID, "AGENTSPINE_GROUP_ID"),
75
+ projectId: boundedId(env.AGENTSPINE_PROJECT_ID, "AGENTSPINE_PROJECT_ID"),
76
+ taskId: boundedId(env.AGENTSPINE_TASK_ID, "AGENTSPINE_TASK_ID"),
77
+ eventId: boundedId(env.AGENTSPINE_CHANNEL_EVENT_ID, "AGENTSPINE_CHANNEL_EVENT_ID"),
78
+ provider: boundedId(env.AGENTSPINE_CHANNEL_PROVIDER, "AGENTSPINE_CHANNEL_PROVIDER")
79
+ };
80
+ }
81
+
82
+ async function runtimeScope(input, root, userStateRoot = null) {
83
+ const { continuity: projectContinuity } = await loadContinuity(root);
84
+ const userContinuity = userStateRoot && userStateRoot !== root
85
+ ? (await loadContinuity(userStateRoot)).continuity
86
+ : projectContinuity;
87
+ const continuity = {
88
+ config: {
89
+ ...projectContinuity.config,
90
+ enabled: userContinuity.config.enabled,
91
+ minConfidence: userContinuity.config.minConfidence,
92
+ minDirectness: userContinuity.config.minDirectness,
93
+ minEvidence: userContinuity.config.minEvidence,
94
+ maxPromptBytes: userContinuity.config.maxPromptBytes,
95
+ maxBriefingBytes: userContinuity.config.maxBriefingBytes,
96
+ defaultEntityId: userContinuity.config.defaultEntityId
97
+ }
98
+ };
99
+ const supplied = input.agent_spine_scope && typeof input.agent_spine_scope === "object"
100
+ ? input.agent_spine_scope : input;
101
+ const gateway = gatewayEnvironmentContext();
102
+ return {
103
+ entityId: boundedId(supplied.entity_id ?? supplied.entityId ?? gateway?.entityId ?? continuity.config.defaultEntityId, "entityId"),
104
+ groupId: boundedId(supplied.group_id ?? supplied.groupId ?? gateway?.groupId, "groupId"),
105
+ projectId: boundedId(supplied.project_id ?? supplied.projectId ?? gateway?.projectId ?? continuity.config.defaultProjectId, "projectId"),
106
+ currentTaskId: boundedId(supplied.task_id ?? supplied.currentTaskId ?? gateway?.taskId, "currentTaskId"),
107
+ host: hostFromInput(input),
108
+ config: continuity.config
109
+ };
110
+ }
111
+
112
+ function renderContext(event, catalog, briefing, signal = null, attentionEvent = null, selfstarter = null, channelEvent = null, sourceDiagnostics = null) {
113
+ const loaded = sourceDiagnostics?.status === "loaded";
114
+ const packet = {
115
+ schema: "agentspine.hook-context/v1",
116
+ event,
117
+ priority: ["current-user-request", "explicit-stops", "current-task", "host-rules", "accepted-context", "style-and-relationships"],
118
+ loaded,
119
+ instruction: loaded
120
+ ? "Use this already-loaded briefing now. Do not call an MCP tool to obtain it. The current user request and explicit stops override all remembered style, relationships, and older context."
121
+ : "No host-native source context was loaded. Do not claim personal continuity or recall succeeded. Continue under current native host rules and inspect sourceResolution.",
122
+ signal: signal ? {
123
+ captured: Boolean(signal.captured),
124
+ accepted: Boolean(signal.accepted),
125
+ duplicate: Boolean(signal.duplicate),
126
+ kind: signal.kind || null,
127
+ reason: signal.reason || null
128
+ } : null,
129
+ attentionEvent: attentionEvent ? {
130
+ captured: Boolean(attentionEvent.event),
131
+ duplicate: Boolean(attentionEvent.duplicate),
132
+ id: attentionEvent.event?.id || null,
133
+ kind: attentionEvent.event?.kind || null,
134
+ status: attentionEvent.event?.status || null,
135
+ reason: attentionEvent.reason || null
136
+ } : null,
137
+ selfstarter: selfstarter ? {
138
+ active: Boolean(selfstarter.job),
139
+ blocked: Boolean(selfstarter.blocked),
140
+ action: selfstarter.action || null,
141
+ reason: selfstarter.reason || null,
142
+ jobId: selfstarter.job?.id || null,
143
+ taskId: selfstarter.job?.taskId || null,
144
+ actorId: selfstarter.job?.actorId || null,
145
+ targetId: selfstarter.job?.targetId || null,
146
+ projectId: selfstarter.job?.projectId || null,
147
+ groupId: selfstarter.job?.groupId || null,
148
+ checkpointSequence: selfstarter.job?.checkpoint?.sequence ?? null,
149
+ capabilities: selfstarter.job?.capabilities || [],
150
+ leaseExpiresAt: selfstarter.job?.lease?.expiresAt || null,
151
+ receiptId: selfstarter.receipt?.id || null,
152
+ instruction: selfstarter.job
153
+ ? "Resume only this exact checkpointed job. Attach its job ID to every host tool event. Each effect is separately re-authorized by PreToolUse and checkpointed by PostToolUse. Stop immediately on any denial."
154
+ : null,
155
+ authority: selfstarter.job ? "explicit-local-execution-policy" : "execution-state-only"
156
+ } : null,
157
+ channelEvent: channelEvent?.event ? {
158
+ active: true,
159
+ eventId: channelEvent.event.eventId,
160
+ bindingId: channelEvent.event.bindingId,
161
+ provider: channelEvent.event.provider,
162
+ chatId: channelEvent.event.chatId,
163
+ threadId: channelEvent.event.threadId,
164
+ senderId: channelEvent.event.senderId,
165
+ replyTo: channelEvent.event.replyTo,
166
+ agentId: channelEvent.event.agentId,
167
+ projectId: channelEvent.event.projectId,
168
+ groupId: channelEvent.event.groupId,
169
+ sessionKey: channelEvent.event.sessionKey,
170
+ text: channelEvent.event.text,
171
+ leaseExpiresAt: channelEvent.event.lease?.expiresAt || null,
172
+ receiptId: channelEvent.receipt?.id || null,
173
+ instruction: "Answer this exact authenticated channel event in its bound chat and thread. Do not infer another recipient or route. Sending remains subject to the separate current channel policy and adapter receipt.",
174
+ authority: "explicit-local-channel-policy"
175
+ } : null,
176
+ indexedSources: catalog.summary.total,
177
+ sourceResolution: sourceDiagnostics,
178
+ briefing,
179
+ authority: "context-only"
180
+ };
181
+ return JSON.stringify(packet);
182
+ }
183
+
184
+ function selfstarterInput(input) {
185
+ const value = input.agent_spine_job;
186
+ if (value === undefined || value === null) return null;
187
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("agent_spine_job must be one object");
188
+ return value;
189
+ }
190
+
191
+ function channelEventInput(input) {
192
+ const gateway = gatewayEnvironmentContext();
193
+ const value = input.agent_spine_channel_event ?? (gateway?.eventId && gateway?.provider
194
+ ? { event_id: gateway.eventId, provider: gateway.provider } : null);
195
+ if (value === undefined || value === null) return null;
196
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("agent_spine_channel_event must be one object");
197
+ const unknown = Object.keys(value).filter((key) => !["event_id", "eventId", "provider"].includes(key));
198
+ if (unknown.length) throw new Error(`agent_spine_channel_event contains unknown field: ${unknown.sort()[0]}`);
199
+ return value;
200
+ }
201
+
202
+ function hookDeliveryId(input) {
203
+ return boundedId(input.tool_use_id ?? input.event_id ?? input.hook_event_id, "toolUseId");
204
+ }
205
+
206
+ function sessionId(input) {
207
+ return boundedId(input.session_id ?? input.sessionId, "sessionId");
208
+ }
209
+
210
+ async function startSelfstarter(input, event, root, scope) {
211
+ if (!SELFSTART_EVENTS.has(event)) return null;
212
+ const requested = selfstarterInput(input);
213
+ const session = sessionId(input);
214
+ if (!scope.entityId || !scope.projectId || !session) {
215
+ if (requested) throw new Error("self-starter start requires an exact actor, project, and host session");
216
+ return null;
217
+ }
218
+ return startOrResumeJob({
219
+ root, actorId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
220
+ taskId: scope.currentTaskId, jobId: boundedId(requested?.job_id ?? requested?.jobId, "jobId"),
221
+ host: scope.host, sessionId: session, now: input.timestamp || new Date()
222
+ });
223
+ }
224
+
225
+ async function startChannelEvent(input, event, root, scope) {
226
+ const requested = channelEventInput(input);
227
+ if (!requested) return null;
228
+ if (event !== "SessionStart") throw new Error("channel event claims are accepted only at SessionStart");
229
+ const session = sessionId(input);
230
+ if (!scope.entityId || !scope.projectId || !session) {
231
+ throw new Error("channel event start requires an exact agent, project, and host session");
232
+ }
233
+ const workerId = `channel-worker:${createHash("sha256").update(`${scope.host}\0${session}`).digest("hex").slice(0, 24)}`;
234
+ const claim = await claimChannelEvent({
235
+ root, eventId: boundedId(requested.event_id ?? requested.eventId, "channelEventId"),
236
+ agentId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
237
+ provider: boundedId(requested.provider, "channelProvider"), workerId,
238
+ now: input.timestamp || new Date()
239
+ });
240
+ if (!claim.event) throw new Error("the exact channel event is unavailable in this agent lane");
241
+ return claim;
242
+ }
243
+
244
+ async function selfstarterScope(input, scope, root, action) {
245
+ const requested = selfstarterInput(input);
246
+ const hostSession = sessionId(input);
247
+ const supplied = {
248
+ actorId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
249
+ taskId: scope.currentTaskId, host: scope.host, sessionId: hostSession
250
+ };
251
+ if (requested) return { ...supplied, jobId: boundedId(requested.job_id ?? requested.jobId, "jobId") };
252
+ if (!hostSession) return null;
253
+ return resolveSessionJob({ root, ...supplied, action, now: input.timestamp || new Date() });
254
+ }
255
+
256
+ function toolSucceeded(input) {
257
+ if (input.success === false || input.is_error === true || input.tool_error) return false;
258
+ if (input.tool_result && typeof input.tool_result === "object" && input.tool_result.isError === true) return false;
259
+ return true;
260
+ }
261
+
262
+ function toolResult(input) {
263
+ return input.tool_result ?? input.tool_response ?? input.result ?? (input.tool_error ? { failed: true } : null);
264
+ }
265
+
266
+ function eventReceipt(input, event, scope, discriminator = "lifecycle") {
267
+ const supplied = input.event_id ?? input.hook_event_id ?? input.tool_use_id ?? input.session_id;
268
+ if (typeof supplied === "string" && ID_RE.test(supplied)) {
269
+ return `receipt:${createHash("sha256").update(`${event}\0${supplied}\0${discriminator}`).digest("hex").slice(0, 24)}`;
270
+ }
271
+ const material = [event, scope.host, scope.entityId, scope.projectId, scope.currentTaskId, discriminator].join("\0");
272
+ return `receipt:${createHash("sha256").update(material).digest("hex").slice(0, 24)}`;
273
+ }
274
+
275
+ function heartbeatReceipt(input, scope) {
276
+ const at = new Date(input.timestamp || Date.now());
277
+ if (!Number.isFinite(at.getTime())) throw new Error("heartbeat timestamp is invalid");
278
+ const minute = at.toISOString().slice(0, 16);
279
+ const material = [scope.host, scope.entityId, scope.groupId, scope.projectId, scope.currentTaskId, minute].join("\0");
280
+ return `receipt:heartbeat:${createHash("sha256").update(material).digest("hex").slice(0, 20)}`;
281
+ }
282
+
283
+ function minimalAttentionSignal(prompt) {
284
+ if (typeof prompt !== "string" || Buffer.byteLength(prompt) > 16384) return null;
285
+ const rules = [
286
+ { kind: "promise", re: /^(?:i promise(?: to)?|i will|ich verspreche(?:,)?|ich werde|prometo(?: que)?|voy a|jag lovar att|jag kommer att)\s+(.+)$/i, prefix: "Promise: " },
287
+ { kind: "blocker", re: /^(?:blocker|blocked(?: by)?|i am blocked(?: by)?|ich bin blockiert(?: durch)?|blockiert durch|bloquead[oa](?: por)?|estoy bloquead[oa](?: por)?|blockerad(?: av)?|jag är blockerad(?: av)?)\s*[:,-]?\s*(.+)$/i, prefix: "Blocker: " }
288
+ ];
289
+ for (const rule of rules) {
290
+ const match = prompt.trim().match(rule.re);
291
+ if (!match) continue;
292
+ const value = match[1].trim().replace(/\s+/g, " ").replace(/[.!?]+$/, "").slice(0, 220);
293
+ if (value) return { kind: rule.kind, summary: `${rule.prefix}${value}` };
294
+ }
295
+ return null;
296
+ }
297
+
298
+ async function captureAttentionLifecycle(input, event, root, scope) {
299
+ if (!ATTENTION_WRITE_EVENTS.has(event)) return null;
300
+ const explicit = input.agent_spine_attention && typeof input.agent_spine_attention === "object"
301
+ && !Array.isArray(input.agent_spine_attention) ? input.agent_spine_attention : null;
302
+ let proposed = explicit;
303
+ let automaticHeartbeat = false;
304
+ if (!proposed && event === "UserPromptSubmit" && scope.config.enabled) {
305
+ proposed = minimalAttentionSignal(promptFromInput(input));
306
+ if (proposed && scope.groupId) return { event: null, duplicate: false, reason: "rejected:group conversation events are never learned automatically" };
307
+ }
308
+ if (!proposed && event === "PostToolUse" && scope.currentTaskId) {
309
+ proposed = { kind: "heartbeat", summary: "Work heartbeat recorded.", status: "active" };
310
+ automaticHeartbeat = true;
311
+ }
312
+ if (!proposed && ["Stop", "SubagentStop"].includes(event) && scope.currentTaskId) {
313
+ proposed = { kind: "heartbeat", summary: "Work heartbeat recorded.", status: "stopped" };
314
+ }
315
+ if (!proposed) return null;
316
+ if (event === "PostToolUse" && ![input.event_id, input.hook_event_id, input.tool_use_id].some((value) => typeof value === "string" && ID_RE.test(value))) {
317
+ throw new Error("PostToolUse attention requires a stable host delivery ID");
318
+ }
319
+ if (!scope.entityId || !scope.projectId || !scope.currentTaskId) {
320
+ if (explicit) throw new Error("lifecycle attention event is missing exact actor, project, or task scope");
321
+ return { event: null, duplicate: false, reason: "missing-exact-scope" };
322
+ }
323
+ const privacy = proposed.privacy || "private";
324
+ if (event === "UserPromptSubmit" && privacy === "group") {
325
+ return { event: null, duplicate: false, reason: "rejected:group conversation events are never learned automatically" };
326
+ }
327
+ const discriminator = proposed.id || `${proposed.kind}:${proposed.status || ""}:${proposed.summary}`;
328
+ return recordAttentionEvent({
329
+ root,
330
+ id: boundedId(proposed.id, "attentionEventId"),
331
+ kind: proposed.kind,
332
+ summary: proposed.summary,
333
+ status: proposed.status || null,
334
+ entityId: scope.entityId,
335
+ groupId: privacy === "group" ? scope.groupId : null,
336
+ projectId: scope.projectId,
337
+ taskId: scope.currentTaskId,
338
+ privacy,
339
+ dueAt: proposed.due_at ?? proposed.dueAt ?? null,
340
+ receiptId: automaticHeartbeat ? heartbeatReceipt(input, scope) : eventReceipt(input, event, scope, discriminator),
341
+ host: scope.host,
342
+ hookEvent: event,
343
+ observedAt: input.timestamp || new Date()
344
+ });
345
+ }
346
+
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
+ }
406
+
407
+ function candidatePaths(value, output = []) {
408
+ if (!value) return output;
409
+ if (typeof value === "string") {
410
+ for (const match of value.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)) output.push(match[1].trim());
411
+ return output;
412
+ }
413
+ if (Array.isArray(value)) {
414
+ for (const item of value) candidatePaths(item, output);
415
+ return output;
416
+ }
417
+ for (const [key, item] of Object.entries(value)) {
418
+ if (["path", "file_path", "target_file", "filename"].includes(key) && typeof item === "string") output.push(item);
419
+ else candidatePaths(item, output);
420
+ }
421
+ return output;
422
+ }
423
+
424
+ function isMutationTool(name = "") {
425
+ return /(^|__)(apply_patch|edit|write|delete|move|rename|bash|exec_command|shell)(_|$)/i.test(name);
426
+ }
427
+
428
+ function stringValues(value, output = []) {
429
+ if (typeof value === "string") output.push(value);
430
+ else if (Array.isArray(value)) value.forEach((item) => stringValues(item, output));
431
+ else if (value && typeof value === "object") Object.values(value).forEach((item) => stringValues(item, output));
432
+ return output;
433
+ }
434
+
435
+ function shellTargetsProtected(input, documents, cwd, root) {
436
+ if (!/(bash|exec_command|shell)/i.test(input.tool_name || "")) return null;
437
+ const command = stringValues(input.tool_input || input.tool_args).join("\n").replaceAll("\\", "/");
438
+ if (!/(?:^|[;&|\s])(?:rm|mv|cp|truncate|tee|sed\s+-i|perl\s+-i)\b|(?:^|[^>])>{1,2}(?!>)/i.test(command)) return null;
439
+ for (const document of documents) {
440
+ const forms = new Set([document.path.replaceAll("\\", "/"), document.relativePath]);
441
+ for (const base of [cwd, root]) {
442
+ const candidate = relative(base, document.path);
443
+ if (candidate && !candidate.startsWith("..") && !isAbsolute(candidate)) forms.add(candidate.replaceAll("\\", "/"));
444
+ }
445
+ if ([...forms].some((form) => form && command.includes(form))) return document;
446
+ }
447
+ return null;
448
+ }
449
+
450
+ function deny(reason) {
451
+ process.stdout.write(`${JSON.stringify({
452
+ decision: "block",
453
+ reason,
454
+ hookSpecificOutput: {
455
+ hookEventName: "PreToolUse",
456
+ permissionDecision: "deny",
457
+ permissionDecisionReason: reason
458
+ }
459
+ })}\n`);
460
+ }
461
+
462
+ export async function runHook(payload = null) {
463
+ const input = payload || await readStdin();
464
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("hook input must be one JSON object");
465
+ const event = input.hook_event_name || input.event_name || "";
466
+ if (!KNOWN_EVENTS.has(event)) throw new Error(`unsupported hook event: ${event || "missing"}`);
467
+ const cwd = await canonicalPath(input.cwd || process.cwd());
468
+ const host = hostFromInput(input);
469
+ let resolvedSources;
470
+ try {
471
+ resolvedSources = await resolveHostSourceCatalog({ host, cwd, input });
472
+ } catch (error) {
473
+ const reason = `AgentSpine source resolution failed closed: ${error.message}`;
474
+ if (event === "PreToolUse" && isMutationTool(input.tool_name)) {
475
+ if (payload) return { blocked: true, failedClosed: true, reason };
476
+ deny(reason);
477
+ return;
478
+ }
479
+ const context = JSON.stringify({
480
+ schema: "agentspine.hook-context/v1", event, loaded: false, failedClosed: true,
481
+ indexedSources: 0, sourceResolution: { status: "failed-closed", reason: error.message },
482
+ instruction: "Do not claim AgentSpine recall succeeded. Continue under current native host rules; no remembered context or automatic effect was applied.",
483
+ authority: "context-only"
484
+ });
485
+ if (payload) return { blocked: false, failedClosed: true, context, error: error.message };
486
+ if (CONTEXT_EVENTS.has(event)) process.stdout.write(`${JSON.stringify(hookOutput(event, context))}\n`);
487
+ else process.stdout.write("{}\n");
488
+ return;
489
+ }
490
+ const root = resolvedSources.projectRoot;
491
+ const catalog = resolvedSources.catalog;
492
+ const catalogPath = await saveCatalog(catalog);
493
+ let scope = null;
494
+ let selfstarter = null;
495
+ let channelEvent = null;
496
+
497
+ if (event === "PreToolUse" && isMutationTool(input.tool_name)) {
498
+ const { graph } = await loadGraph(root, catalog);
499
+ const inferredProtected = new Set(graph.annotations
500
+ .filter((annotation) => ["constitution", "soul", "memory-index", "memory-fact"].includes(annotation.layer))
501
+ .map((annotation) => annotation.path));
502
+ const protectedRelative = new Set(catalog.documents
503
+ .filter((doc) => doc.protected || inferredProtected.has(doc.relativePath))
504
+ .map((doc) => doc.relativePath));
505
+ let expanded = true;
506
+ while (expanded) {
507
+ expanded = false;
508
+ for (const edge of graph.edges) {
509
+ if (protectedRelative.has(edge.from) && !protectedRelative.has(edge.to)) {
510
+ protectedRelative.add(edge.to);
511
+ expanded = true;
512
+ }
513
+ }
514
+ }
515
+ const protectedDocuments = catalog.documents.filter((doc) => protectedRelative.has(doc.relativePath));
516
+ const protectedPaths = new Set(protectedDocuments.map((doc) => resolve(doc.path)));
517
+ const targets = await Promise.all(candidatePaths(input.tool_input || input.tool_args).map(async (path) => {
518
+ const target = resolve(cwd, path);
519
+ try { return await canonicalPath(target); } catch (error) {
520
+ if (error.code !== "ENOENT") throw error;
521
+ return target;
522
+ }
523
+ }));
524
+ const hit = targets.find((path) => protectedPaths.has(path));
525
+ const shellHit = shellTargetsProtected(input, protectedDocuments, cwd, root);
526
+ if (hit || shellHit) {
527
+ const relativePath = shellHit?.relativePath || catalog.documents.find((doc) => resolve(doc.path) === hit)?.relativePath || hit;
528
+ const reason = `AgentSpine protected source: ${relativePath}. Existing identity, rule, soul, and memory documents are read-only to agents.`;
529
+ if (payload) return { blocked: true, reason };
530
+ deny(reason);
531
+ return;
532
+ }
533
+ }
534
+
535
+ if (event === "PreToolUse") {
536
+ try {
537
+ scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot);
538
+ const exact = await selfstarterScope(input, scope, root, "effect");
539
+ if (exact) {
540
+ selfstarter = await authorizeJobEffect({
541
+ root, ...exact, toolName: input.tool_name, toolUseId: hookDeliveryId(input),
542
+ now: input.timestamp || new Date()
543
+ });
544
+ }
545
+ } catch (error) {
546
+ const reason = `AgentSpine self-starter denied this effect: ${error.message}`;
547
+ if (payload) return { blocked: true, reason, selfstarter: { allowed: false, reason: error.message } };
548
+ deny(reason);
549
+ return;
550
+ }
551
+ }
552
+
553
+ let attentionEvent = null;
554
+ if (ATTENTION_WRITE_EVENTS.has(event) && !CONTEXT_EVENTS.has(event)) {
555
+ scope = await runtimeScope(input, root, resolvedSources.userStateRoot);
556
+ attentionEvent = await captureAttentionLifecycle(input, event, root, scope);
557
+ }
558
+
559
+ if (event === "PostToolUse") {
560
+ scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot);
561
+ const exact = await selfstarterScope(input, scope, root, "effect");
562
+ if (exact) {
563
+ selfstarter = await checkpointJobEffect({
564
+ root, ...exact, toolName: input.tool_name, toolUseId: hookDeliveryId(input),
565
+ success: toolSucceeded(input), result: toolResult(input), now: input.timestamp || new Date()
566
+ });
567
+ }
568
+ }
569
+
570
+ if (["Stop", "SubagentStop"].includes(event)) {
571
+ scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot);
572
+ const exact = await selfstarterScope(input, scope, root, "resume");
573
+ const requested = selfstarterInput(input);
574
+ if (exact) {
575
+ selfstarter = await closeJobLease({
576
+ root, ...exact, status: requested?.status || "waiting", now: input.timestamp || new Date()
577
+ });
578
+ }
579
+ }
580
+
581
+ if (CONTEXT_EVENTS.has(event)) {
582
+ let signal = null;
583
+ try {
584
+ await syncPersonaRosterFromEnvironment({ root, env: process.env, now: input.timestamp || new Date() });
585
+ scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot);
586
+ channelEvent = await startChannelEvent(input, event, root, scope);
587
+ selfstarter = await startSelfstarter(input, event, root, scope);
588
+ if (selfstarter?.job && !scope.currentTaskId) scope.currentTaskId = selfstarter.job.taskId;
589
+ if (event === "UserPromptSubmit") {
590
+ const prompt = promptFromInput(input);
591
+ try {
592
+ attentionEvent = await captureAttentionLifecycle(input, event, root, scope);
593
+ } catch (error) {
594
+ attentionEvent = { event: null, duplicate: false, reason: `rejected:${error.message}` };
595
+ }
596
+ if (prompt !== null) {
597
+ try {
598
+ signal = await captureContinuityPrompt({
599
+ root, prompt, entityId: scope.entityId, groupId: scope.groupId,
600
+ projectId: scope.projectId, userStateRoot: resolvedSources.userStateRoot,
601
+ eventId: boundedId(input.event_id ?? input.hook_event_id, "eventId")
602
+ });
603
+ } catch (error) {
604
+ signal = { captured: false, accepted: false, reason: `rejected:${error.message}` };
605
+ }
606
+ }
607
+ }
608
+ const briefing = await sessionBriefing({
609
+ root, cwd, host: scope.host, entityId: scope.entityId, groupId: scope.groupId,
610
+ projectId: scope.projectId, currentTaskId: scope.currentTaskId,
611
+ includePrivate: Boolean(scope.entityId && !scope.groupId),
612
+ focusActive: true, includeSourceContent: !scope.groupId,
613
+ maxBytes: scope.config.maxBriefingBytes,
614
+ now: input.timestamp || new Date(),
615
+ catalog, userStateRoot: resolvedSources.userStateRoot, sourceDiagnostics: resolvedSources.diagnostics,
616
+ prompt: event === "UserPromptSubmit" ? promptFromInput(input) : null
617
+ });
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 };
620
+ process.stdout.write(`${JSON.stringify(hookOutput(event, context))}\n`);
621
+ return;
622
+ } catch (error) {
623
+ const context = JSON.stringify({
624
+ schema: "agentspine.hook-context/v1", event, loaded: false, failedClosed: true,
625
+ indexedSources: catalog.summary.total,
626
+ sourceResolution: resolvedSources.diagnostics,
627
+ error: error.message,
628
+ instruction: "Do not claim AgentSpine recall succeeded. Continue with the current request under native host rules and run agentspine audit before using remembered context.",
629
+ authority: "context-only"
630
+ });
631
+ if (payload) return { blocked: false, failedClosed: true, context, error: error.message, catalogPath };
632
+ process.stdout.write(`${JSON.stringify(hookOutput(event, context))}\n`);
633
+ return;
634
+ }
635
+ }
636
+
637
+ if (payload) return { blocked: false, attentionEvent, selfstarter };
638
+ process.stdout.write("{}\n");
639
+ }
640
+
641
+ if (isMainModule(import.meta.url)) {
642
+ runHook().catch((error) => {
643
+ process.stderr.write(`AgentSpine hook: ${String(error.message).slice(0, 2048)}\n`);
644
+ process.exitCode = 1;
645
+ });
646
+ }