blun-king-cli 9.1.563 → 9.1.565
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
- package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
- package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
- package/agent-spine-plugin/.mcp.json +8 -8
- package/agent-spine-plugin/CONTRIBUTING.md +52 -0
- package/agent-spine-plugin/LICENSE +186 -186
- package/agent-spine-plugin/README.md +394 -0
- package/agent-spine-plugin/SECURITY.md +47 -0
- package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
- package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
- package/agent-spine-plugin/bin/agentspine.js +7 -7
- package/agent-spine-plugin/blun.plugin.json +33 -33
- package/agent-spine-plugin/hooks/codex.json +47 -47
- package/agent-spine-plugin/hooks/hooks.json +106 -106
- package/agent-spine-plugin/hooks/version.json +5 -5
- package/agent-spine-plugin/package.json +69 -69
- package/agent-spine-plugin/scripts/check-hosts.js +199 -199
- package/agent-spine-plugin/skill/SKILL.md +76 -76
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
- package/agent-spine-plugin/src/cli.js +1488 -1442
- package/agent-spine-plugin/src/hook.js +886 -812
- package/agent-spine-plugin/src/index.js +93 -93
- package/agent-spine-plugin/src/lib/acceptance.js +333 -333
- package/agent-spine-plugin/src/lib/attention.js +755 -755
- package/agent-spine-plugin/src/lib/audit.js +351 -342
- package/agent-spine-plugin/src/lib/authentication.js +515 -515
- package/agent-spine-plugin/src/lib/briefing.js +317 -317
- package/agent-spine-plugin/src/lib/catalog.js +167 -167
- package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
- package/agent-spine-plugin/src/lib/context.js +154 -154
- package/agent-spine-plugin/src/lib/continuity.js +338 -338
- package/agent-spine-plugin/src/lib/coordination.js +577 -577
- package/agent-spine-plugin/src/lib/documents.js +217 -217
- package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
- package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
- package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
- package/agent-spine-plugin/src/lib/graph.js +337 -337
- package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
- package/agent-spine-plugin/src/lib/https-transport.js +392 -392
- package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
- package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
- package/agent-spine-plugin/src/lib/learning.js +6923 -6466
- package/agent-spine-plugin/src/lib/object-transport.js +206 -206
- package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
- package/agent-spine-plugin/src/lib/paths.js +109 -109
- package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
- package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
- package/agent-spine-plugin/src/lib/preflight.js +702 -702
- package/agent-spine-plugin/src/lib/runtime.js +13 -13
- package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
- package/agent-spine-plugin/src/lib/sharing.js +969 -969
- package/agent-spine-plugin/src/lib/source-roots.js +529 -482
- package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
- package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
- package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
- package/agent-spine-plugin/src/mcp.js +597 -572
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +202 -195
- package/bin/active-steer-priority-policy.cjs +24 -0
- package/bin/launcher-runtime.js +8 -1
- package/bin/mnemo-tool-agent-policy.cjs +22 -0
- package/bin/thinking-activity-status-policy.cjs +132 -0
- package/bin/thinking-only-guard.cjs +75 -0
- package/bin/tool-call-loop-policy.cjs +53 -0
- package/bin/turn-thinking-policy.cjs +25 -1
- package/blun.mjs +554 -64
- package/package.json +4 -1
- package/standard-skills/translate-native/LICENSE +21 -21
- package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
- package/standard-skills/translate-native/references/native-orthography.md +79 -79
- package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
- package/standard-skills/translate-native/references/structured-content.md +72 -72
- package/standard-skills/translate-native/references/translationese-review.md +77 -77
- package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
- package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
- package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
- package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
- package/standard-skills/translate-native/scripts/language_quality.py +377 -377
- package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
- package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
- package/standard-tools/language-guard/blun_language_guard.py +697 -697
- package/standard-tools/language-guard/check_diacritics.py +353 -353
- package/standard-tools/language-guard/guard_service_client.py +264 -264
- package/standard-tools/language-guard/language_quality.py +377 -377
- package/standard-tools/language-guard/translation_guard.py +916 -916
|
@@ -1,812 +1,886 @@
|
|
|
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 { catalogForStateRoot, 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 { recordLearningApplications, recordLearningDeliveries } from "./lib/learning.js";
|
|
12
|
-
import {
|
|
13
|
-
authorizeJobEffect, checkpointJobEffect, closeJobLease, resolveSessionJob, startOrResumeJob
|
|
14
|
-
} from "./lib/selfstarter.js";
|
|
15
|
-
import { claimChannelEvent } from "./lib/channel-runtime.js";
|
|
16
|
-
import { syncPersonaRosterFromEnvironment } from "./lib/persona-runtime.js";
|
|
17
|
-
import { captureMustRememberPrompt, recordPreflightFailure, runPreflight, verifyPreflightReceipt } from "./lib/preflight.js";
|
|
18
|
-
import { isMainModule } from "./lib/runtime.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
let
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.
|
|
68
|
-
.
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (input.
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
{ kind: "
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
let
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (runtime.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (
|
|
567
|
-
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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 { catalogForStateRoot, 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 { recordLearningApplications, recordLearningDeliveries } from "./lib/learning.js";
|
|
12
|
+
import {
|
|
13
|
+
authorizeJobEffect, checkpointJobEffect, closeJobLease, resolveSessionJob, startOrResumeJob
|
|
14
|
+
} from "./lib/selfstarter.js";
|
|
15
|
+
import { claimChannelEvent } from "./lib/channel-runtime.js";
|
|
16
|
+
import { syncPersonaRosterFromEnvironment } from "./lib/persona-runtime.js";
|
|
17
|
+
import { captureMustRememberPrompt, recordPreflightFailure, runPreflight, verifyPreflightReceipt } from "./lib/preflight.js";
|
|
18
|
+
import { isMainModule } from "./lib/runtime.js";
|
|
19
|
+
import { recordHookScanAudit } from "./lib/hook-audit.js";
|
|
20
|
+
|
|
21
|
+
const MAX_STDIN_BYTES = 64 * 1024;
|
|
22
|
+
const STANDARD_HOST_CONTEXT_BYTES = 9500;
|
|
23
|
+
const MAX_CLAUDE_OVERFLOW_CONTEXT_BYTES = 32 * 1024;
|
|
24
|
+
const CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit", "PreCompact", "PostCompact"]);
|
|
25
|
+
const KNOWN_EVENTS = new Set([
|
|
26
|
+
...CONTEXT_EVENTS, "InstructionsLoaded", "PreToolUse", "PostToolUse", "Stop", "SubagentStop"
|
|
27
|
+
]);
|
|
28
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
|
|
29
|
+
const ATTENTION_WRITE_EVENTS = new Set(["UserPromptSubmit", "PostToolUse", "Stop", "SubagentStop"]);
|
|
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";
|
|
33
|
+
|
|
34
|
+
async function readStdin({ silentOversizePostToolUse = false } = {}) {
|
|
35
|
+
const chunks = [];
|
|
36
|
+
let bytes = 0;
|
|
37
|
+
let oversized = false;
|
|
38
|
+
for await (const chunk of process.stdin) {
|
|
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;
|
|
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");
|
|
50
|
+
const parsed = value.trim() ? JSON.parse(value) : {};
|
|
51
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("hook input must be one JSON object");
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function boundedId(value, field) {
|
|
56
|
+
if (value === null || value === undefined || value === "") return null;
|
|
57
|
+
if (typeof value !== "string" || !ID_RE.test(value)) throw new Error(`${field} is invalid`);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function promptFromInput(input) {
|
|
62
|
+
for (const key of ["prompt", "user_prompt", "message", "input"]) {
|
|
63
|
+
const value = input[key];
|
|
64
|
+
if (typeof value === "string") return value;
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
const text = value
|
|
67
|
+
.filter((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string")
|
|
68
|
+
.map((part) => part.text)
|
|
69
|
+
.join("\n")
|
|
70
|
+
.trim();
|
|
71
|
+
if (text) return text;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hostContextLimit(preflight) {
|
|
78
|
+
return preflight?.receipt?.instructionBudget?.mode === "claude-required-overflow"
|
|
79
|
+
? MAX_CLAUDE_OVERFLOW_CONTEXT_BYTES
|
|
80
|
+
: STANDARD_HOST_CONTEXT_BYTES;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hostFromInput(input) {
|
|
84
|
+
const explicit = input.host || input.provider || process.env.AGENTSPINE_HOST;
|
|
85
|
+
if (["claude", "codex", "generic"].includes(explicit)) return explicit;
|
|
86
|
+
if ((typeof input.model === "string" && input.model.trim()) || process.env.PLUGIN_ROOT || process.env.CODEX_HOME
|
|
87
|
+
|| process.env.BLUN_PLUGIN_ROOT || process.env.BLUN_HOME) return "codex";
|
|
88
|
+
return "claude";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function gatewayEnvironmentContext(env = process.env) {
|
|
92
|
+
if (env.AGENTSPINE_GATEWAY_CONTEXT !== "agentspine.gateway-start/v1") return null;
|
|
93
|
+
return {
|
|
94
|
+
entityId: boundedId(env.AGENTSPINE_ENTITY_ID, "AGENTSPINE_ENTITY_ID"),
|
|
95
|
+
groupId: boundedId(env.AGENTSPINE_GROUP_ID, "AGENTSPINE_GROUP_ID"),
|
|
96
|
+
projectId: boundedId(env.AGENTSPINE_PROJECT_ID, "AGENTSPINE_PROJECT_ID"),
|
|
97
|
+
taskId: boundedId(env.AGENTSPINE_TASK_ID, "AGENTSPINE_TASK_ID"),
|
|
98
|
+
eventId: boundedId(env.AGENTSPINE_CHANNEL_EVENT_ID, "AGENTSPINE_CHANNEL_EVENT_ID"),
|
|
99
|
+
provider: boundedId(env.AGENTSPINE_CHANNEL_PROVIDER, "AGENTSPINE_CHANNEL_PROVIDER")
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function runtimeScope(input, root, userStateRoot = null, catalog) {
|
|
104
|
+
const { continuity: projectContinuity } = await loadContinuity(root, catalog);
|
|
105
|
+
const userCatalog = userStateRoot && userStateRoot !== root
|
|
106
|
+
? catalogForStateRoot(catalog, userStateRoot) : catalog;
|
|
107
|
+
const userContinuity = userStateRoot && userStateRoot !== root
|
|
108
|
+
? (await loadContinuity(userStateRoot, userCatalog)).continuity
|
|
109
|
+
: projectContinuity;
|
|
110
|
+
const continuity = {
|
|
111
|
+
config: {
|
|
112
|
+
...projectContinuity.config,
|
|
113
|
+
enabled: userContinuity.config.enabled,
|
|
114
|
+
minConfidence: userContinuity.config.minConfidence,
|
|
115
|
+
minDirectness: userContinuity.config.minDirectness,
|
|
116
|
+
minEvidence: userContinuity.config.minEvidence,
|
|
117
|
+
maxPromptBytes: userContinuity.config.maxPromptBytes,
|
|
118
|
+
maxBriefingBytes: userContinuity.config.maxBriefingBytes,
|
|
119
|
+
defaultEntityId: userContinuity.config.defaultEntityId
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const supplied = input.agent_spine_scope && typeof input.agent_spine_scope === "object"
|
|
123
|
+
? input.agent_spine_scope : input;
|
|
124
|
+
const gateway = gatewayEnvironmentContext();
|
|
125
|
+
return {
|
|
126
|
+
entityId: boundedId(supplied.entity_id ?? supplied.entityId ?? gateway?.entityId ?? continuity.config.defaultEntityId, "entityId"),
|
|
127
|
+
userId: boundedId(supplied.user_id ?? supplied.userId ?? process.env.AGENTSPINE_USER_ID, "userId"),
|
|
128
|
+
tenantId: boundedId(supplied.tenant_id ?? supplied.tenantId ?? process.env.AGENTSPINE_TENANT_ID, "tenantId"),
|
|
129
|
+
groupId: boundedId(supplied.group_id ?? supplied.groupId ?? gateway?.groupId, "groupId"),
|
|
130
|
+
projectId: boundedId(supplied.project_id ?? supplied.projectId ?? gateway?.projectId ?? continuity.config.defaultProjectId, "projectId"),
|
|
131
|
+
currentTaskId: boundedId(supplied.task_id ?? supplied.currentTaskId ?? gateway?.taskId, "currentTaskId"),
|
|
132
|
+
host: hostFromInput(input),
|
|
133
|
+
config: continuity.config
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderContext(event, catalog, briefing, signal = null, attentionEvent = null, selfstarter = null, channelEvent = null, sourceDiagnostics = null, preflight = null) {
|
|
138
|
+
const loaded = sourceDiagnostics?.status === "loaded";
|
|
139
|
+
const packet = {
|
|
140
|
+
schema: "agentspine.hook-context/v1",
|
|
141
|
+
event,
|
|
142
|
+
priority: ["current-user-request", "explicit-stops", "current-task", "host-rules", "accepted-context", "style-and-relationships"],
|
|
143
|
+
loaded,
|
|
144
|
+
instruction: loaded
|
|
145
|
+
? "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."
|
|
146
|
+
: "No host-native source context was loaded. Do not claim personal continuity or recall succeeded. Continue under current native host rules and inspect sourceResolution.",
|
|
147
|
+
signal: signal ? {
|
|
148
|
+
captured: Boolean(signal.captured),
|
|
149
|
+
accepted: Boolean(signal.accepted),
|
|
150
|
+
duplicate: Boolean(signal.duplicate),
|
|
151
|
+
kind: signal.kind || null,
|
|
152
|
+
reason: signal.reason || null
|
|
153
|
+
} : null,
|
|
154
|
+
attentionEvent: attentionEvent ? {
|
|
155
|
+
captured: Boolean(attentionEvent.event),
|
|
156
|
+
duplicate: Boolean(attentionEvent.duplicate),
|
|
157
|
+
id: attentionEvent.event?.id || null,
|
|
158
|
+
kind: attentionEvent.event?.kind || null,
|
|
159
|
+
status: attentionEvent.event?.status || null,
|
|
160
|
+
reason: attentionEvent.reason || null
|
|
161
|
+
} : null,
|
|
162
|
+
selfstarter: selfstarter ? {
|
|
163
|
+
active: Boolean(selfstarter.job),
|
|
164
|
+
blocked: Boolean(selfstarter.blocked),
|
|
165
|
+
action: selfstarter.action || null,
|
|
166
|
+
reason: selfstarter.reason || null,
|
|
167
|
+
jobId: selfstarter.job?.id || null,
|
|
168
|
+
taskId: selfstarter.job?.taskId || null,
|
|
169
|
+
actorId: selfstarter.job?.actorId || null,
|
|
170
|
+
targetId: selfstarter.job?.targetId || null,
|
|
171
|
+
projectId: selfstarter.job?.projectId || null,
|
|
172
|
+
groupId: selfstarter.job?.groupId || null,
|
|
173
|
+
checkpointSequence: selfstarter.job?.checkpoint?.sequence ?? null,
|
|
174
|
+
capabilities: selfstarter.job?.capabilities || [],
|
|
175
|
+
leaseExpiresAt: selfstarter.job?.lease?.expiresAt || null,
|
|
176
|
+
receiptId: selfstarter.receipt?.id || null,
|
|
177
|
+
instruction: selfstarter.job
|
|
178
|
+
? "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."
|
|
179
|
+
: null,
|
|
180
|
+
authority: selfstarter.job ? "explicit-local-execution-policy" : "execution-state-only"
|
|
181
|
+
} : null,
|
|
182
|
+
channelEvent: channelEvent?.event ? {
|
|
183
|
+
active: true,
|
|
184
|
+
eventId: channelEvent.event.eventId,
|
|
185
|
+
bindingId: channelEvent.event.bindingId,
|
|
186
|
+
provider: channelEvent.event.provider,
|
|
187
|
+
chatId: channelEvent.event.chatId,
|
|
188
|
+
threadId: channelEvent.event.threadId,
|
|
189
|
+
senderId: channelEvent.event.senderId,
|
|
190
|
+
replyTo: channelEvent.event.replyTo,
|
|
191
|
+
agentId: channelEvent.event.agentId,
|
|
192
|
+
projectId: channelEvent.event.projectId,
|
|
193
|
+
groupId: channelEvent.event.groupId,
|
|
194
|
+
sessionKey: channelEvent.event.sessionKey,
|
|
195
|
+
text: channelEvent.event.text,
|
|
196
|
+
leaseExpiresAt: channelEvent.event.lease?.expiresAt || null,
|
|
197
|
+
receiptId: channelEvent.receipt?.id || null,
|
|
198
|
+
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.",
|
|
199
|
+
authority: "explicit-local-channel-policy"
|
|
200
|
+
} : null,
|
|
201
|
+
indexedSources: catalog.summary.total,
|
|
202
|
+
sourceResolution: sourceDiagnostics,
|
|
203
|
+
preflight: preflight ? {
|
|
204
|
+
schema: preflight.receipt.schema,
|
|
205
|
+
receiptId: preflight.receipt.id,
|
|
206
|
+
promptDigest: preflight.receipt.promptDigest,
|
|
207
|
+
briefingDigest: preflight.receipt.briefingDigest,
|
|
208
|
+
createdAt: preflight.receipt.createdAt,
|
|
209
|
+
expiresAt: preflight.receipt.expiresAt,
|
|
210
|
+
policy: preflight.policy,
|
|
211
|
+
learningApplications: preflight.learningApplications || null,
|
|
212
|
+
pendingMustRemember: preflight.pendingMustRemember ? {
|
|
213
|
+
id: preflight.pendingMustRemember.candidate?.id || null,
|
|
214
|
+
status: preflight.pendingMustRemember.candidate?.status || (preflight.pendingMustRemember.rejected ? "rejected" : null),
|
|
215
|
+
reason: preflight.pendingMustRemember.reason || null
|
|
216
|
+
} : null,
|
|
217
|
+
briefing: preflight.briefing,
|
|
218
|
+
instruction: "This exact turn passed the mandatory pre-answer gate. Apply the complete preflight briefing before answering.",
|
|
219
|
+
authority: "preflight-proof-only"
|
|
220
|
+
} : null,
|
|
221
|
+
briefing,
|
|
222
|
+
authority: "context-only"
|
|
223
|
+
};
|
|
224
|
+
return JSON.stringify(packet);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function selfstarterInput(input) {
|
|
228
|
+
const value = input.agent_spine_job;
|
|
229
|
+
if (value === undefined || value === null) return null;
|
|
230
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("agent_spine_job must be one object");
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function channelEventInput(input) {
|
|
235
|
+
const gateway = gatewayEnvironmentContext();
|
|
236
|
+
const value = input.agent_spine_channel_event ?? (gateway?.eventId && gateway?.provider
|
|
237
|
+
? { event_id: gateway.eventId, provider: gateway.provider } : null);
|
|
238
|
+
if (value === undefined || value === null) return null;
|
|
239
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("agent_spine_channel_event must be one object");
|
|
240
|
+
const unknown = Object.keys(value).filter((key) => !["event_id", "eventId", "provider"].includes(key));
|
|
241
|
+
if (unknown.length) throw new Error(`agent_spine_channel_event contains unknown field: ${unknown.sort()[0]}`);
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function hookDeliveryId(input) {
|
|
246
|
+
return boundedId(input.tool_use_id ?? input.event_id ?? input.hook_event_id, "toolUseId");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function sessionId(input) {
|
|
250
|
+
return boundedId(input.session_id ?? input.sessionId, "sessionId");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function startSelfstarter(input, event, root, scope, sourceDiagnostics) {
|
|
254
|
+
if (!SELFSTART_EVENTS.has(event)) return null;
|
|
255
|
+
const requested = selfstarterInput(input);
|
|
256
|
+
if (["skipped-unmarked-home", "skipped-home-root"].includes(sourceDiagnostics?.projectTreeScan)) {
|
|
257
|
+
if (requested) throw new Error("self-starter cannot use a user home as its workspace root");
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const session = sessionId(input);
|
|
261
|
+
if (!scope.entityId || !scope.projectId || !session) {
|
|
262
|
+
if (requested) throw new Error("self-starter start requires an exact actor, project, and host session");
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
return startOrResumeJob({
|
|
266
|
+
root, actorId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
|
|
267
|
+
taskId: scope.currentTaskId, jobId: boundedId(requested?.job_id ?? requested?.jobId, "jobId"),
|
|
268
|
+
host: scope.host, sessionId: session, now: input.timestamp || new Date()
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function startChannelEvent(input, event, root, scope, catalog) {
|
|
273
|
+
const requested = channelEventInput(input);
|
|
274
|
+
if (!requested) return null;
|
|
275
|
+
if (event !== "SessionStart") throw new Error("channel event claims are accepted only at SessionStart");
|
|
276
|
+
const session = sessionId(input);
|
|
277
|
+
if (!scope.entityId || !scope.projectId || !session) {
|
|
278
|
+
throw new Error("channel event start requires an exact agent, project, and host session");
|
|
279
|
+
}
|
|
280
|
+
const workerId = `channel-worker:${createHash("sha256").update(`${scope.host}\0${session}`).digest("hex").slice(0, 24)}`;
|
|
281
|
+
const claim = await claimChannelEvent({
|
|
282
|
+
root, eventId: boundedId(requested.event_id ?? requested.eventId, "channelEventId"),
|
|
283
|
+
agentId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
|
|
284
|
+
provider: boundedId(requested.provider, "channelProvider"), workerId,
|
|
285
|
+
now: input.timestamp || new Date(), catalog
|
|
286
|
+
});
|
|
287
|
+
if (!claim.event) throw new Error("the exact channel event is unavailable in this agent lane");
|
|
288
|
+
return claim;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function selfstarterScope(input, scope, root, action) {
|
|
292
|
+
const requested = selfstarterInput(input);
|
|
293
|
+
const hostSession = sessionId(input);
|
|
294
|
+
const supplied = {
|
|
295
|
+
actorId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
|
|
296
|
+
taskId: scope.currentTaskId, host: scope.host, sessionId: hostSession
|
|
297
|
+
};
|
|
298
|
+
if (requested) return { ...supplied, jobId: boundedId(requested.job_id ?? requested.jobId, "jobId") };
|
|
299
|
+
if (!hostSession) return null;
|
|
300
|
+
return resolveSessionJob({ root, ...supplied, action, now: input.timestamp || new Date() });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function toolSucceeded(input) {
|
|
304
|
+
if (input.success === false || input.is_error === true || input.tool_error) return false;
|
|
305
|
+
if (input.tool_result && typeof input.tool_result === "object" && input.tool_result.isError === true) return false;
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function toolResult(input) {
|
|
310
|
+
return input.tool_result ?? input.tool_response ?? input.result ?? (input.tool_error ? { failed: true } : null);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function eventReceipt(input, event, scope, discriminator = "lifecycle") {
|
|
314
|
+
const supplied = input.event_id ?? input.hook_event_id ?? input.tool_use_id ?? input.session_id;
|
|
315
|
+
if (typeof supplied === "string" && ID_RE.test(supplied)) {
|
|
316
|
+
return `receipt:${createHash("sha256").update(`${event}\0${supplied}\0${discriminator}`).digest("hex").slice(0, 24)}`;
|
|
317
|
+
}
|
|
318
|
+
const material = [event, scope.host, scope.entityId, scope.projectId, scope.currentTaskId, discriminator].join("\0");
|
|
319
|
+
return `receipt:${createHash("sha256").update(material).digest("hex").slice(0, 24)}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function heartbeatReceipt(input, scope) {
|
|
323
|
+
const at = new Date(input.timestamp || Date.now());
|
|
324
|
+
if (!Number.isFinite(at.getTime())) throw new Error("heartbeat timestamp is invalid");
|
|
325
|
+
const minute = at.toISOString().slice(0, 16);
|
|
326
|
+
const material = [scope.host, scope.entityId, scope.groupId, scope.projectId, scope.currentTaskId, minute].join("\0");
|
|
327
|
+
return `receipt:heartbeat:${createHash("sha256").update(material).digest("hex").slice(0, 20)}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function minimalAttentionSignal(prompt) {
|
|
331
|
+
if (typeof prompt !== "string" || Buffer.byteLength(prompt) > 16384) return null;
|
|
332
|
+
const rules = [
|
|
333
|
+
{ 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: " },
|
|
334
|
+
{ 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: " }
|
|
335
|
+
];
|
|
336
|
+
for (const rule of rules) {
|
|
337
|
+
const match = prompt.trim().match(rule.re);
|
|
338
|
+
if (!match) continue;
|
|
339
|
+
const value = match[1].trim().replace(/\s+/g, " ").replace(/[.!?]+$/, "").slice(0, 220);
|
|
340
|
+
if (value) return { kind: rule.kind, summary: `${rule.prefix}${value}` };
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function captureAttentionLifecycle(input, event, root, scope, catalog) {
|
|
346
|
+
if (!ATTENTION_WRITE_EVENTS.has(event)) return null;
|
|
347
|
+
const explicit = input.agent_spine_attention && typeof input.agent_spine_attention === "object"
|
|
348
|
+
&& !Array.isArray(input.agent_spine_attention) ? input.agent_spine_attention : null;
|
|
349
|
+
let proposed = explicit;
|
|
350
|
+
let automaticHeartbeat = false;
|
|
351
|
+
if (!proposed && event === "UserPromptSubmit" && scope.config.enabled) {
|
|
352
|
+
proposed = minimalAttentionSignal(promptFromInput(input));
|
|
353
|
+
if (proposed && scope.groupId) return { event: null, duplicate: false, reason: "rejected:group conversation events are never learned automatically" };
|
|
354
|
+
}
|
|
355
|
+
if (!proposed && event === "PostToolUse" && scope.currentTaskId) {
|
|
356
|
+
proposed = { kind: "heartbeat", summary: "Work heartbeat recorded.", status: "active" };
|
|
357
|
+
automaticHeartbeat = true;
|
|
358
|
+
}
|
|
359
|
+
if (!proposed && ["Stop", "SubagentStop"].includes(event) && scope.currentTaskId) {
|
|
360
|
+
proposed = { kind: "heartbeat", summary: "Work heartbeat recorded.", status: "stopped" };
|
|
361
|
+
}
|
|
362
|
+
if (!proposed) return null;
|
|
363
|
+
if (event === "PostToolUse" && ![input.event_id, input.hook_event_id, input.tool_use_id].some((value) => typeof value === "string" && ID_RE.test(value))) {
|
|
364
|
+
throw new Error("PostToolUse attention requires a stable host delivery ID");
|
|
365
|
+
}
|
|
366
|
+
if (!scope.entityId || !scope.projectId || !scope.currentTaskId) {
|
|
367
|
+
if (explicit) throw new Error("lifecycle attention event is missing exact actor, project, or task scope");
|
|
368
|
+
return { event: null, duplicate: false, reason: "missing-exact-scope" };
|
|
369
|
+
}
|
|
370
|
+
const privacy = proposed.privacy || "private";
|
|
371
|
+
if (event === "UserPromptSubmit" && privacy === "group") {
|
|
372
|
+
return { event: null, duplicate: false, reason: "rejected:group conversation events are never learned automatically" };
|
|
373
|
+
}
|
|
374
|
+
const discriminator = proposed.id || `${proposed.kind}:${proposed.status || ""}:${proposed.summary}`;
|
|
375
|
+
return recordAttentionEvent({
|
|
376
|
+
root,
|
|
377
|
+
id: boundedId(proposed.id, "attentionEventId"),
|
|
378
|
+
kind: proposed.kind,
|
|
379
|
+
summary: proposed.summary,
|
|
380
|
+
status: proposed.status || null,
|
|
381
|
+
entityId: scope.entityId,
|
|
382
|
+
groupId: privacy === "group" ? scope.groupId : null,
|
|
383
|
+
projectId: scope.projectId,
|
|
384
|
+
taskId: scope.currentTaskId,
|
|
385
|
+
privacy,
|
|
386
|
+
dueAt: proposed.due_at ?? proposed.dueAt ?? null,
|
|
387
|
+
receiptId: automaticHeartbeat ? heartbeatReceipt(input, scope) : eventReceipt(input, event, scope, discriminator),
|
|
388
|
+
host: scope.host,
|
|
389
|
+
hookEvent: event,
|
|
390
|
+
observedAt: input.timestamp || new Date(), catalog
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function blunRuntimeContext(context) {
|
|
395
|
+
const detailed = JSON.parse(context);
|
|
396
|
+
const sourceResolution = detailed.sourceResolution ? {
|
|
397
|
+
status: detailed.sourceResolution.status || null,
|
|
398
|
+
reason: detailed.sourceResolution.reason || null
|
|
399
|
+
} : null;
|
|
400
|
+
const runtime = {
|
|
401
|
+
schema: "agentspine.blun-runtime-context/v1",
|
|
402
|
+
event: detailed.event,
|
|
403
|
+
loaded: Boolean(detailed.loaded),
|
|
404
|
+
failedClosed: detailed.failedClosed ? true : undefined,
|
|
405
|
+
indexedSources: detailed.indexedSources || 0,
|
|
406
|
+
sourceResolution,
|
|
407
|
+
instruction: detailed.loaded
|
|
408
|
+
? "Detailed AgentSpine context is available on demand through session_briefing. Load it only when the current request needs continuity."
|
|
409
|
+
: detailed.instruction,
|
|
410
|
+
authority: "context-only"
|
|
411
|
+
};
|
|
412
|
+
if (detailed.signal && (detailed.signal.captured || detailed.signal.accepted || detailed.signal.reason)) {
|
|
413
|
+
runtime.signal = detailed.signal;
|
|
414
|
+
}
|
|
415
|
+
if (detailed.attentionEvent
|
|
416
|
+
&& (detailed.attentionEvent.captured || detailed.attentionEvent.duplicate || detailed.attentionEvent.reason)) {
|
|
417
|
+
runtime.attentionEvent = detailed.attentionEvent;
|
|
418
|
+
}
|
|
419
|
+
if (detailed.selfstarter && (detailed.selfstarter.active || detailed.selfstarter.blocked)) {
|
|
420
|
+
runtime.selfstarter = detailed.selfstarter;
|
|
421
|
+
}
|
|
422
|
+
if (detailed.channelEvent?.active) runtime.channelEvent = detailed.channelEvent;
|
|
423
|
+
return JSON.stringify(runtime);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function blunRuntimeMessage(context) {
|
|
427
|
+
const runtime = JSON.parse(blunRuntimeContext(context));
|
|
428
|
+
const base = runtime.loaded
|
|
429
|
+
? `AgentSpine ready: ${runtime.indexedSources} sources indexed. Load detailed continuity only on demand through session_briefing.`
|
|
430
|
+
: `AgentSpine unavailable${runtime.sourceResolution?.reason ? `: ${runtime.sourceResolution.reason}` : ""}. ${runtime.instruction}`;
|
|
431
|
+
const active = {};
|
|
432
|
+
if (runtime.signal && (runtime.signal.captured || runtime.signal.accepted
|
|
433
|
+
|| String(runtime.signal.reason || "").startsWith("rejected:"))) {
|
|
434
|
+
active.signal = runtime.signal;
|
|
435
|
+
}
|
|
436
|
+
if (runtime.attentionEvent && (runtime.attentionEvent.captured || runtime.attentionEvent.duplicate
|
|
437
|
+
|| String(runtime.attentionEvent.reason || "").startsWith("rejected:"))) {
|
|
438
|
+
active.attentionEvent = runtime.attentionEvent;
|
|
439
|
+
}
|
|
440
|
+
if (runtime.selfstarter) active.selfstarter = runtime.selfstarter;
|
|
441
|
+
if (runtime.channelEvent) active.channelEvent = runtime.channelEvent;
|
|
442
|
+
return Object.keys(active).length === 0
|
|
443
|
+
? base
|
|
444
|
+
: `${base}\nActive AgentSpine runtime data: ${JSON.stringify(active)}`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function hookOutput(event, context) {
|
|
448
|
+
if (process.env.BLUN_PLUGIN_ROOT) {
|
|
449
|
+
return { hookSpecificOutput: { hookEventName: event, message: blunRuntimeMessage(context) } };
|
|
450
|
+
}
|
|
451
|
+
return { hookSpecificOutput: { hookEventName: event, additionalContext: context } };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function candidatePaths(value, output = []) {
|
|
455
|
+
if (!value) return output;
|
|
456
|
+
if (typeof value === "string") {
|
|
457
|
+
for (const match of value.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)) output.push(match[1].trim());
|
|
458
|
+
return output;
|
|
459
|
+
}
|
|
460
|
+
if (Array.isArray(value)) {
|
|
461
|
+
for (const item of value) candidatePaths(item, output);
|
|
462
|
+
return output;
|
|
463
|
+
}
|
|
464
|
+
for (const [key, item] of Object.entries(value)) {
|
|
465
|
+
if (["path", "file_path", "target_file", "filename"].includes(key) && typeof item === "string") output.push(item);
|
|
466
|
+
else candidatePaths(item, output);
|
|
467
|
+
}
|
|
468
|
+
return output;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function isMutationTool(name = "") {
|
|
472
|
+
return /(^|__)(apply_patch|edit|write|delete|move|rename|bash|exec_command|shell)(_|$)/i.test(name);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function isScanFailOpenTool(name = "") {
|
|
476
|
+
return /(^|__)(apply_patch|edit|write|bash|exec_command)(_|$)/i.test(name);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const RAW_SCAN_ERROR_CODES = new Set(["EPERM", "EACCES"]);
|
|
480
|
+
const RAW_SCAN_SYSCALLS = new Set(["opendir", "readdir", "scandir", "lstat", "stat", "realpath"]);
|
|
481
|
+
|
|
482
|
+
function filesystemScanError(error) {
|
|
483
|
+
if (!error) return false;
|
|
484
|
+
if (error.code === "AGENTSPINE_SCAN_INCOMPLETE" || error.agentSpineScan === true) return true;
|
|
485
|
+
return RAW_SCAN_ERROR_CODES.has(error.code)
|
|
486
|
+
&& RAW_SCAN_SYSCALLS.has(error.syscall)
|
|
487
|
+
&& typeof error.path === "string"
|
|
488
|
+
&& error.path.length > 0;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function hookScanFailureFailsOpen(_event, _toolName, error) {
|
|
492
|
+
return filesystemScanError(error);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function auditSkippedScans(input, phase, skipped = []) {
|
|
496
|
+
const item = skipped[0];
|
|
497
|
+
if (!item) return;
|
|
498
|
+
await recordHookScanAudit({
|
|
499
|
+
event: "PreToolUse", toolName: input.tool_name || null, phase,
|
|
500
|
+
error: { code: item.code, message: `${item.code}: ${item.operation} skipped ${item.path}` },
|
|
501
|
+
path: item.path, operation: item.operation, now: input.timestamp || new Date()
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function allowScanFailure(input, phase, error) {
|
|
506
|
+
await recordHookScanAudit({
|
|
507
|
+
event: input.hook_event_name || input.event_name || "unknown",
|
|
508
|
+
toolName: input.tool_name || null, phase, error,
|
|
509
|
+
path: error?.path || input.cwd || process.cwd(), now: input.timestamp || new Date()
|
|
510
|
+
});
|
|
511
|
+
return { blocked: false, scanFailedOpen: true, phase, error: error.message };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function stringValues(value, output = []) {
|
|
515
|
+
if (typeof value === "string") output.push(value);
|
|
516
|
+
else if (Array.isArray(value)) value.forEach((item) => stringValues(item, output));
|
|
517
|
+
else if (value && typeof value === "object") Object.values(value).forEach((item) => stringValues(item, output));
|
|
518
|
+
return output;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function shellTargetsProtected(input, documents, cwd, root) {
|
|
522
|
+
if (!/(bash|exec_command|shell)/i.test(input.tool_name || "")) return null;
|
|
523
|
+
const command = stringValues(input.tool_input || input.tool_args).join("\n").replaceAll("\\", "/");
|
|
524
|
+
if (!/(?:^|[;&|\s])(?:rm|mv|cp|truncate|tee|sed\s+-i|perl\s+-i)\b|(?:^|[^>])>{1,2}(?!>)/i.test(command)) return null;
|
|
525
|
+
for (const document of documents) {
|
|
526
|
+
const forms = new Set([document.path.replaceAll("\\", "/"), document.relativePath]);
|
|
527
|
+
for (const base of [cwd, root]) {
|
|
528
|
+
const candidate = relative(base, document.path);
|
|
529
|
+
if (candidate && !candidate.startsWith("..") && !isAbsolute(candidate)) forms.add(candidate.replaceAll("\\", "/"));
|
|
530
|
+
}
|
|
531
|
+
if ([...forms].some((form) => form && command.includes(form))) return document;
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function deny(reason) {
|
|
537
|
+
process.stdout.write(`${JSON.stringify({
|
|
538
|
+
decision: "block",
|
|
539
|
+
reason,
|
|
540
|
+
hookSpecificOutput: {
|
|
541
|
+
hookEventName: "PreToolUse",
|
|
542
|
+
permissionDecision: "deny",
|
|
543
|
+
permissionDecisionReason: reason
|
|
544
|
+
}
|
|
545
|
+
})}\n`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function blockPrompt(reason) {
|
|
549
|
+
process.stdout.write(`${JSON.stringify({
|
|
550
|
+
decision: "block",
|
|
551
|
+
reason,
|
|
552
|
+
hookSpecificOutput: { hookEventName: "UserPromptSubmit", decision: "block", reason }
|
|
553
|
+
})}\n`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function runHookWithInput(input, payload) {
|
|
557
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("hook input must be one JSON object");
|
|
558
|
+
const event = input.hook_event_name || input.event_name || "";
|
|
559
|
+
if (!KNOWN_EVENTS.has(event)) throw new Error(`unsupported hook event: ${event || "missing"}`);
|
|
560
|
+
if (event === "InstructionsLoaded") {
|
|
561
|
+
const file = input.file_path;
|
|
562
|
+
if (typeof file !== "string" || !file || !["User", "Project", "Local", "Managed"].includes(input.memory_type)
|
|
563
|
+
|| !["session_start", "nested_traversal", "path_glob_match", "include", "compact"].includes(input.load_reason)) {
|
|
564
|
+
throw new Error("InstructionsLoaded payload is invalid");
|
|
565
|
+
}
|
|
566
|
+
if (payload) return { blocked: false, observed: true };
|
|
567
|
+
process.stdout.write("{}\n");
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const cwd = await canonicalPath(input.cwd || process.cwd());
|
|
571
|
+
const host = hostFromInput(input);
|
|
572
|
+
const instructionHost = host === "generic" ? input.instruction_host : host;
|
|
573
|
+
if (host === "generic" && !["claude", "codex"].includes(instructionHost)) {
|
|
574
|
+
const reason = "AgentSpine generic hosts must bind instruction_host to claude or codex";
|
|
575
|
+
if (event === "UserPromptSubmit") {
|
|
576
|
+
if (payload) return { blocked: true, failedClosed: true, reason };
|
|
577
|
+
blockPrompt(reason);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
throw new Error(reason);
|
|
581
|
+
}
|
|
582
|
+
let resolvedSources;
|
|
583
|
+
try {
|
|
584
|
+
resolvedSources = await resolveHostSourceCatalog({ host: instructionHost, cwd, input });
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (hookScanFailureFailsOpen(event, input.tool_name, error)) {
|
|
587
|
+
const allowed = await allowScanFailure(input, "source-resolution", error);
|
|
588
|
+
if (payload) return allowed;
|
|
589
|
+
process.stdout.write("{}\n");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const reason = `AgentSpine source resolution failed closed: ${error.message}`;
|
|
593
|
+
if (event === "PreToolUse" && isMutationTool(input.tool_name)) {
|
|
594
|
+
if (payload) return { blocked: true, failedClosed: true, reason };
|
|
595
|
+
deny(reason);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (event === "UserPromptSubmit") {
|
|
599
|
+
if (payload) return { blocked: true, failedClosed: true, reason };
|
|
600
|
+
blockPrompt(reason);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const context = JSON.stringify({
|
|
604
|
+
schema: "agentspine.hook-context/v1", event, loaded: false, failedClosed: true,
|
|
605
|
+
indexedSources: 0, sourceResolution: { status: "failed-closed", reason: error.message },
|
|
606
|
+
instruction: "Do not claim AgentSpine recall succeeded. Continue under current native host rules; no remembered context or automatic effect was applied.",
|
|
607
|
+
authority: "context-only"
|
|
608
|
+
});
|
|
609
|
+
if (payload) return { blocked: false, failedClosed: true, context, error: error.message };
|
|
610
|
+
if (CONTEXT_EVENTS.has(event)) process.stdout.write(`${JSON.stringify(hookOutput(event, context))}\n`);
|
|
611
|
+
else process.stdout.write("{}\n");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const root = resolvedSources.projectRoot;
|
|
615
|
+
const catalog = resolvedSources.catalog;
|
|
616
|
+
const catalogPath = await saveCatalog(catalog);
|
|
617
|
+
let scope = null;
|
|
618
|
+
let selfstarter = null;
|
|
619
|
+
let channelEvent = null;
|
|
620
|
+
let learningDelivery = null;
|
|
621
|
+
|
|
622
|
+
if (event === "PreToolUse" && isScanFailOpenTool(input.tool_name) && resolvedSources.diagnostics.skipped?.length) {
|
|
623
|
+
await auditSkippedScans(input, "source-resolution", resolvedSources.diagnostics.skipped);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (event === "PreToolUse" && isMutationTool(input.tool_name)) {
|
|
627
|
+
const { graph } = await loadGraph(root, catalog);
|
|
628
|
+
const inferredProtected = new Set(graph.annotations
|
|
629
|
+
.filter((annotation) => ["constitution", "soul", "memory-index", "memory-fact"].includes(annotation.layer))
|
|
630
|
+
.map((annotation) => annotation.path));
|
|
631
|
+
const protectedRelative = new Set(catalog.documents
|
|
632
|
+
.filter((doc) => doc.protected || inferredProtected.has(doc.relativePath))
|
|
633
|
+
.map((doc) => doc.relativePath));
|
|
634
|
+
let expanded = true;
|
|
635
|
+
while (expanded) {
|
|
636
|
+
expanded = false;
|
|
637
|
+
for (const edge of graph.edges) {
|
|
638
|
+
if (protectedRelative.has(edge.from) && !protectedRelative.has(edge.to)) {
|
|
639
|
+
protectedRelative.add(edge.to);
|
|
640
|
+
expanded = true;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const protectedDocuments = catalog.documents.filter((doc) => protectedRelative.has(doc.relativePath));
|
|
645
|
+
const protectedPaths = new Set(protectedDocuments.map((doc) => resolve(doc.path)));
|
|
646
|
+
const targets = await Promise.all(candidatePaths(input.tool_input || input.tool_args).map(async (path) => {
|
|
647
|
+
const target = resolve(cwd, path);
|
|
648
|
+
try { return await canonicalPath(target); } catch (error) {
|
|
649
|
+
if (error.code !== "ENOENT") throw error;
|
|
650
|
+
return target;
|
|
651
|
+
}
|
|
652
|
+
}));
|
|
653
|
+
const hit = targets.find((path) => protectedPaths.has(path));
|
|
654
|
+
const shellHit = shellTargetsProtected(input, protectedDocuments, cwd, root);
|
|
655
|
+
if (hit || shellHit) {
|
|
656
|
+
const relativePath = shellHit?.relativePath || catalog.documents.find((doc) => resolve(doc.path) === hit)?.relativePath || hit;
|
|
657
|
+
const reason = `AgentSpine protected source: ${relativePath}. Existing identity, rule, soul, and memory documents are read-only to agents.`;
|
|
658
|
+
if (payload) return { blocked: true, reason };
|
|
659
|
+
deny(reason);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (event === "PreToolUse") {
|
|
665
|
+
try {
|
|
666
|
+
scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
|
|
667
|
+
const exact = await selfstarterScope(input, scope, root, "effect");
|
|
668
|
+
if (exact) {
|
|
669
|
+
selfstarter = await authorizeJobEffect({
|
|
670
|
+
root, ...exact, toolName: input.tool_name, toolUseId: hookDeliveryId(input),
|
|
671
|
+
now: input.timestamp || new Date()
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
} catch (error) {
|
|
675
|
+
if (isScanFailOpenTool(input.tool_name) && filesystemScanError(error)) {
|
|
676
|
+
const allowed = await allowScanFailure(input, "self-starter", error);
|
|
677
|
+
if (payload) return allowed;
|
|
678
|
+
process.stdout.write("{}\n");
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const reason = `AgentSpine self-starter denied this effect: ${error.message}`;
|
|
682
|
+
if (payload) return { blocked: true, reason, selfstarter: { allowed: false, reason: error.message } };
|
|
683
|
+
deny(reason);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
let attentionEvent = null;
|
|
689
|
+
if (ATTENTION_WRITE_EVENTS.has(event) && !CONTEXT_EVENTS.has(event)) {
|
|
690
|
+
scope = await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
|
|
691
|
+
attentionEvent = await captureAttentionLifecycle(input, event, root, scope, catalog);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (event === "PostToolUse") {
|
|
695
|
+
scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
|
|
696
|
+
const exact = await selfstarterScope(input, scope, root, "effect");
|
|
697
|
+
if (exact) {
|
|
698
|
+
selfstarter = await checkpointJobEffect({
|
|
699
|
+
root, ...exact, toolName: input.tool_name, toolUseId: hookDeliveryId(input),
|
|
700
|
+
success: toolSucceeded(input), result: toolResult(input), now: input.timestamp || new Date()
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (["Stop", "SubagentStop"].includes(event)) {
|
|
706
|
+
scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
|
|
707
|
+
const exact = await selfstarterScope(input, scope, root, "resume");
|
|
708
|
+
if (exact && !scope.currentTaskId) scope.currentTaskId = exact.taskId;
|
|
709
|
+
try {
|
|
710
|
+
learningDelivery = await recordLearningDeliveries({
|
|
711
|
+
root, sessionId: sessionId(input), hookEvent: event,
|
|
712
|
+
scope: {
|
|
713
|
+
personaId: scope.entityId, userId: scope.userId, tenantId: scope.tenantId,
|
|
714
|
+
projectId: scope.projectId, groupId: scope.groupId, taskId: scope.currentTaskId
|
|
715
|
+
},
|
|
716
|
+
completedAt: input.timestamp || new Date()
|
|
717
|
+
});
|
|
718
|
+
} catch (error) {
|
|
719
|
+
learningDelivery = { status: "degraded", receipts: [], reason: error.message, authority: "context-only" };
|
|
720
|
+
}
|
|
721
|
+
const requested = selfstarterInput(input);
|
|
722
|
+
if (exact) {
|
|
723
|
+
selfstarter = await closeJobLease({
|
|
724
|
+
root, ...exact, status: requested?.status || "waiting", now: input.timestamp || new Date()
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (CONTEXT_EVENTS.has(event)) {
|
|
730
|
+
let signal = null;
|
|
731
|
+
let preflight = null;
|
|
732
|
+
try {
|
|
733
|
+
await syncPersonaRosterFromEnvironment({ root, env: process.env, now: input.timestamp || new Date(), catalog });
|
|
734
|
+
scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
|
|
735
|
+
channelEvent = await startChannelEvent(input, event, root, scope, catalog);
|
|
736
|
+
selfstarter = await startSelfstarter(input, event, root, scope, resolvedSources.diagnostics);
|
|
737
|
+
if (selfstarter?.job && !scope.currentTaskId) scope.currentTaskId = selfstarter.job.taskId;
|
|
738
|
+
if (event === "UserPromptSubmit") {
|
|
739
|
+
const prompt = promptFromInput(input);
|
|
740
|
+
if (prompt === null) throw new Error("mandatory preflight requires the exact current prompt");
|
|
741
|
+
preflight = await runPreflight({
|
|
742
|
+
input, scope, resolvedSources, prompt, now: input.timestamp || new Date(), env: process.env
|
|
743
|
+
});
|
|
744
|
+
if (!await verifyPreflightReceipt({
|
|
745
|
+
receipt: preflight.receipt, input, scope, resolvedSources, prompt,
|
|
746
|
+
now: input.timestamp || new Date(), env: process.env
|
|
747
|
+
})) throw new Error("newly created preflight receipt failed exact turn verification");
|
|
748
|
+
preflight.pendingMustRemember = await captureMustRememberPrompt({ prompt, receipt: preflight.receipt, env: process.env });
|
|
749
|
+
try {
|
|
750
|
+
attentionEvent = await captureAttentionLifecycle(input, event, root, scope, catalog);
|
|
751
|
+
} catch (error) {
|
|
752
|
+
attentionEvent = { event: null, duplicate: false, reason: `rejected:${error.message}` };
|
|
753
|
+
}
|
|
754
|
+
if (prompt !== null) {
|
|
755
|
+
try {
|
|
756
|
+
signal = await captureContinuityPrompt({
|
|
757
|
+
root, prompt, entityId: scope.entityId, groupId: scope.groupId,
|
|
758
|
+
projectId: scope.projectId, userStateRoot: resolvedSources.userStateRoot,
|
|
759
|
+
eventId: boundedId(input.event_id ?? input.hook_event_id, "eventId"), catalog,
|
|
760
|
+
userCatalog: resolvedSources.userStateRoot && resolvedSources.userStateRoot !== root
|
|
761
|
+
? catalogForStateRoot(catalog, resolvedSources.userStateRoot) : catalog
|
|
762
|
+
});
|
|
763
|
+
} catch (error) {
|
|
764
|
+
signal = { captured: false, accepted: false, reason: `rejected:${error.message}` };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const briefing = await sessionBriefing({
|
|
769
|
+
root, cwd, host: scope.host, entityId: scope.entityId, groupId: scope.groupId,
|
|
770
|
+
userId: scope.userId, tenantId: scope.tenantId, projectId: scope.projectId, currentTaskId: scope.currentTaskId,
|
|
771
|
+
includePrivate: Boolean(scope.entityId && !scope.groupId),
|
|
772
|
+
focusActive: true, includeSourceContent: event === "UserPromptSubmit" ? false : !scope.groupId,
|
|
773
|
+
maxBytes: event === "UserPromptSubmit" ? 4096 : scope.config.maxBriefingBytes,
|
|
774
|
+
now: input.timestamp || new Date(),
|
|
775
|
+
catalog, userStateRoot: resolvedSources.userStateRoot, sourceDiagnostics: resolvedSources.diagnostics,
|
|
776
|
+
prompt: event === "UserPromptSubmit" ? promptFromInput(input) : null
|
|
777
|
+
});
|
|
778
|
+
let context = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent, resolvedSources.diagnostics, preflight);
|
|
779
|
+
if (event === "UserPromptSubmit" && Buffer.byteLength(context) > hostContextLimit(preflight)) {
|
|
780
|
+
throw new Error("mandatory preflight context exceeds the host hook injection limit");
|
|
781
|
+
}
|
|
782
|
+
if (event === "UserPromptSubmit" && !await verifyPreflightReceipt({
|
|
783
|
+
receipt: preflight.receipt, input, scope, resolvedSources, prompt: promptFromInput(input),
|
|
784
|
+
now: input.timestamp || new Date(), env: process.env, consume: true
|
|
785
|
+
})) throw new Error("preflight receipt could not be consumed atomically for this exact turn");
|
|
786
|
+
if (event === "UserPromptSubmit") {
|
|
787
|
+
const activeCanaries = briefing.learning.filter((item) => ["active", "revalidating"].includes(item.outcomeStatus));
|
|
788
|
+
if (!activeCanaries.length) {
|
|
789
|
+
preflight.learningApplications = {
|
|
790
|
+
status: "not-applicable", receipts: [], authority: "context-only"
|
|
791
|
+
};
|
|
792
|
+
} else try {
|
|
793
|
+
const application = await recordLearningApplications({
|
|
794
|
+
root, items: briefing.learning,
|
|
795
|
+
scope: {
|
|
796
|
+
personaId: scope.entityId, userId: scope.userId, tenantId: scope.tenantId,
|
|
797
|
+
projectId: scope.projectId, groupId: scope.groupId, taskId: scope.currentTaskId
|
|
798
|
+
},
|
|
799
|
+
preflightReceipt: preflight.receipt,
|
|
800
|
+
sessionBriefingDigest: createHash("sha256").update(JSON.stringify(briefing)).digest("hex"),
|
|
801
|
+
projectedAt: input.timestamp || new Date()
|
|
802
|
+
});
|
|
803
|
+
preflight.learningApplications = {
|
|
804
|
+
status: application.receipts.length ? "recorded" : "not-applicable",
|
|
805
|
+
receipts: application.receipts.map((item) => ({
|
|
806
|
+
id: item.id, learningId: item.learningId, projectedAt: item.projectedAt, expiresAt: item.expiresAt
|
|
807
|
+
})),
|
|
808
|
+
authority: "context-only"
|
|
809
|
+
};
|
|
810
|
+
} catch (error) {
|
|
811
|
+
briefing.learning = briefing.learning.filter((item) => !["active", "revalidating"].includes(item.outcomeStatus));
|
|
812
|
+
preflight.learningApplications = {
|
|
813
|
+
status: "degraded", receipts: [], reason: error.message, authority: "context-only"
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
const enriched = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent,
|
|
817
|
+
resolvedSources.diagnostics, preflight);
|
|
818
|
+
if (Buffer.byteLength(enriched) <= hostContextLimit(preflight)) context = enriched;
|
|
819
|
+
else if (preflight.learningApplications.status === "degraded") {
|
|
820
|
+
preflight.learningApplications = null;
|
|
821
|
+
context = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent,
|
|
822
|
+
resolvedSources.diagnostics, preflight);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (payload) return { blocked: false, context, briefing, preflight, signal, attentionEvent, channelEvent, catalogPath };
|
|
826
|
+
process.stdout.write(`${JSON.stringify(hookOutput(event, context))}\n`);
|
|
827
|
+
return;
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (event === "UserPromptSubmit") {
|
|
830
|
+
await recordPreflightFailure({ receiptId: preflight?.receipt?.id || null, input, host,
|
|
831
|
+
error, now: input.timestamp || new Date(), env: process.env }).catch(() => {});
|
|
832
|
+
if (hookScanFailureFailsOpen(event, input.tool_name, error)) {
|
|
833
|
+
const allowed = await allowScanFailure(input, "preflight", error);
|
|
834
|
+
if (payload) return { ...allowed, catalogPath };
|
|
835
|
+
process.stdout.write("{}\n");
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const reason = `AgentSpine pre-answer preflight blocked this turn: ${error.message}`;
|
|
839
|
+
if (payload) return { blocked: true, failedClosed: true, reason, error: error.message, catalogPath };
|
|
840
|
+
blockPrompt(reason);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const context = JSON.stringify({
|
|
844
|
+
schema: "agentspine.hook-context/v1", event, loaded: false, failedClosed: true,
|
|
845
|
+
indexedSources: catalog.summary.total,
|
|
846
|
+
sourceResolution: resolvedSources.diagnostics,
|
|
847
|
+
error: error.message,
|
|
848
|
+
instruction: "Do not claim AgentSpine recall succeeded. Continue with the current request under native host rules and run agentspine audit before using remembered context.",
|
|
849
|
+
authority: "context-only"
|
|
850
|
+
});
|
|
851
|
+
if (payload) return { blocked: false, failedClosed: true, context, error: error.message, catalogPath };
|
|
852
|
+
process.stdout.write(`${JSON.stringify(hookOutput(event, context))}\n`);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (payload) return { blocked: false, attentionEvent, selfstarter, learningDelivery };
|
|
858
|
+
process.stdout.write("{}\n");
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export async function runHook(payload = null, options = {}) {
|
|
862
|
+
const input = payload || await readStdin(options);
|
|
863
|
+
if (input === SILENT_OVERSIZE_POST_TOOL_USE) return;
|
|
864
|
+
try {
|
|
865
|
+
return await runHookWithInput(input, payload);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
const event = input?.hook_event_name || input?.event_name || "";
|
|
868
|
+
if (!hookScanFailureFailsOpen(event, input?.tool_name, error)) throw error;
|
|
869
|
+
const allowed = await allowScanFailure(input, "hook-runtime", error);
|
|
870
|
+
if (payload) return allowed;
|
|
871
|
+
process.stdout.write("{}\n");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (isMainModule(import.meta.url)) {
|
|
876
|
+
const args = process.argv.slice(2);
|
|
877
|
+
const silentOversizePostToolUse = args.length === 1 && args[0] === SILENT_OVERSIZE_POST_TOOL_USE_ARG;
|
|
878
|
+
const argumentError = args.length && !silentOversizePostToolUse
|
|
879
|
+
? new Error(`unsupported hook argument: ${args[0]}`) : null;
|
|
880
|
+
(argumentError ? Promise.reject(argumentError) : runHook(null, { silentOversizePostToolUse })).catch((error) => {
|
|
881
|
+
process.stderr.write(`AgentSpine hook: ${String(error.message).slice(0, 2048)}\n`);
|
|
882
|
+
// Claude command hooks treat exit 2 as a blocking failure. Exit 1 is
|
|
883
|
+
// fail-open, which is unsafe for malformed pre-answer payloads.
|
|
884
|
+
process.exitCode = 2;
|
|
885
|
+
});
|
|
886
|
+
}
|