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,482 +1,529 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
-
import { lstat, mkdir, open, opendir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
-
import { ancestorsBetween, canonicalPath, isInside, stateRoot } from "./paths.js";
|
|
6
|
-
import { indexExplicitDocuments } from "./documents.js";
|
|
7
|
-
import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
|
|
8
|
-
import { purgeIndexedMemoryCache, resolveIndexedMemory } from "./indexed-memory.js";
|
|
9
|
-
import { catalogScanPolicy } from "./catalog.js";
|
|
10
|
-
|
|
11
|
-
export const SOURCE_REGISTRY_SCHEMA = "agentspine.source-roots/v1";
|
|
12
|
-
const MAX_REGISTRY_BYTES = 1024 * 1024;
|
|
13
|
-
const MAX_SOURCES = 256;
|
|
14
|
-
const MAX_RULE_FILES = 128;
|
|
15
|
-
const MAX_PROJECT_FILES = 240;
|
|
16
|
-
const MAX_TOTAL_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
17
|
-
const MAX_DIRECTORY_ENTRIES = 4096;
|
|
18
|
-
const MAX_PROJECT_DIRECTORY_ENTRIES = 8192;
|
|
19
|
-
const SOURCE_RESOLUTION_MS = 2000;
|
|
20
|
-
const SAFE_NAME = /^[A-Za-z0-9._-]{1,128}$/;
|
|
21
|
-
const SKIP_EXTRA_DIRS = new Set([".git", ".hg", ".svn", ".claude", ".codex", "node_modules", "vendor", "dist", "build", "coverage"]);
|
|
22
|
-
|
|
23
|
-
function digest(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
24
|
-
function
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
||
|
|
36
|
-
|| typeof binding.
|
|
37
|
-
|| binding.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
await
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (value
|
|
110
|
-
return value;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
cursor
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { lstat, mkdir, open, opendir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { ancestorsBetween, canonicalPath, isInside, stateRoot } from "./paths.js";
|
|
6
|
+
import { indexExplicitDocuments } from "./documents.js";
|
|
7
|
+
import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
|
|
8
|
+
import { purgeIndexedMemoryCache, resolveIndexedMemory } from "./indexed-memory.js";
|
|
9
|
+
import { catalogScanPolicy } from "./catalog.js";
|
|
10
|
+
|
|
11
|
+
export const SOURCE_REGISTRY_SCHEMA = "agentspine.source-roots/v1";
|
|
12
|
+
const MAX_REGISTRY_BYTES = 1024 * 1024;
|
|
13
|
+
const MAX_SOURCES = 256;
|
|
14
|
+
const MAX_RULE_FILES = 128;
|
|
15
|
+
const MAX_PROJECT_FILES = 240;
|
|
16
|
+
const MAX_TOTAL_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
17
|
+
const MAX_DIRECTORY_ENTRIES = 4096;
|
|
18
|
+
const MAX_PROJECT_DIRECTORY_ENTRIES = 8192;
|
|
19
|
+
const SOURCE_RESOLUTION_MS = 2000;
|
|
20
|
+
const SAFE_NAME = /^[A-Za-z0-9._-]{1,128}$/;
|
|
21
|
+
const SKIP_EXTRA_DIRS = new Set([".git", ".hg", ".svn", ".claude", ".codex", "node_modules", "vendor", "dist", "build", "coverage"]);
|
|
22
|
+
|
|
23
|
+
function digest(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
24
|
+
function sourceScanError(error) { error.agentSpineScan = true; return error; }
|
|
25
|
+
function registryPath(env = process.env) { return join(stateRoot(env), "source-roots.json"); }
|
|
26
|
+
function emptyRegistry() { return { schema: SOURCE_REGISTRY_SCHEMA, revision: 0, bindings: [], history: [] }; }
|
|
27
|
+
|
|
28
|
+
function normalizeRegistry(value) {
|
|
29
|
+
if (!value || value.schema !== SOURCE_REGISTRY_SCHEMA || !Number.isInteger(value.revision)
|
|
30
|
+
|| !Array.isArray(value.bindings) || !Array.isArray(value.history)) {
|
|
31
|
+
throw new Error("source-root registry is corrupt; host-native recall is disabled until repaired");
|
|
32
|
+
}
|
|
33
|
+
for (const binding of value.bindings) {
|
|
34
|
+
if (!binding || typeof binding.id !== "string" || !["all", "claude", "codex"].includes(binding.host)
|
|
35
|
+
|| !["project-memory", "state-user"].includes(binding.scope) || typeof binding.profileKey !== "string"
|
|
36
|
+
|| typeof binding.projectRoot !== "string" || typeof binding.sourceRoot !== "string"
|
|
37
|
+
|| typeof binding.provenance !== "string" || typeof binding.active !== "boolean"
|
|
38
|
+
|| binding.authority !== "context-only") throw new Error("source-root registry contains an unsafe binding");
|
|
39
|
+
}
|
|
40
|
+
if (value.history.some((item) => !item || item.authority !== "context-only")) {
|
|
41
|
+
throw new Error("source-root registry history contains an authority violation");
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readRegistry(env = process.env) {
|
|
47
|
+
const path = registryPath(env);
|
|
48
|
+
try {
|
|
49
|
+
const metadata = await stat(path);
|
|
50
|
+
if (metadata.size > MAX_REGISTRY_BYTES) throw new Error("source-root registry exceeds 1 MiB");
|
|
51
|
+
return { registry: normalizeRegistry(JSON.parse(await readFile(path, "utf8"))), path };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.code !== "ENOENT") throw error;
|
|
54
|
+
return { registry: emptyRegistry(), path };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function mutateRegistry(task, env = process.env) {
|
|
59
|
+
const path = registryPath(env);
|
|
60
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
61
|
+
const lock = `${path}.lock`;
|
|
62
|
+
let handle;
|
|
63
|
+
for (let attempt = 0; attempt < 80; attempt += 1) {
|
|
64
|
+
try { handle = await open(lock, "wx", 0o600); break; } catch (error) {
|
|
65
|
+
if (!isFileLockContention(error)) throw error;
|
|
66
|
+
await new Promise((done) => setTimeout(done, 25));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!handle) throw new Error("source-root registry is busy; retry shortly");
|
|
70
|
+
try {
|
|
71
|
+
const { registry } = await readRegistry(env);
|
|
72
|
+
const result = await task(registry);
|
|
73
|
+
registry.revision += 1;
|
|
74
|
+
const content = `${JSON.stringify(registry, null, 2)}\n`;
|
|
75
|
+
if (Buffer.byteLength(content) > MAX_REGISTRY_BYTES) throw new Error("source-root registry exceeds 1 MiB");
|
|
76
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
77
|
+
await writeFile(temporary, content, { mode: 0o600 });
|
|
78
|
+
await replaceFileWithRetry(temporary, path);
|
|
79
|
+
return { ...result, registryPath: path, revision: registry.revision };
|
|
80
|
+
} finally {
|
|
81
|
+
await handle.close();
|
|
82
|
+
await unlink(lock).catch((error) => { if (error.code !== "ENOENT") throw error; });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function existingRegular(path) {
|
|
87
|
+
try {
|
|
88
|
+
const metadata = await lstat(path);
|
|
89
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) return null;
|
|
90
|
+
return realpath(path);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error.code === "ENOENT") return null;
|
|
93
|
+
throw sourceScanError(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function existingDirectory(path) {
|
|
98
|
+
try {
|
|
99
|
+
const metadata = await lstat(path);
|
|
100
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) return null;
|
|
101
|
+
return realpath(path);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error.code === "ENOENT") return null;
|
|
104
|
+
throw sourceScanError(error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function expandHome(value, home) {
|
|
109
|
+
if (value === "~") return home;
|
|
110
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) return join(home, value.slice(2));
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function samePath(left, right) {
|
|
115
|
+
const normalize = (value) => resolve(value).replace(/[\\/]+$/, "");
|
|
116
|
+
const a = normalize(left);
|
|
117
|
+
const b = normalize(right);
|
|
118
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function homeRoots(env) {
|
|
122
|
+
const candidates = [homedir(), env.HOME, env.USERPROFILE,
|
|
123
|
+
env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : null]
|
|
124
|
+
.filter((value) => typeof value === "string" && value && isAbsolute(value));
|
|
125
|
+
return [...new Set(await Promise.all(candidates.map(async (value) =>
|
|
126
|
+
await existingDirectory(value) || resolve(value))))];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readJsonObject(path) {
|
|
130
|
+
const file = await existingRegular(path);
|
|
131
|
+
if (!file) return null;
|
|
132
|
+
const metadata = await stat(file);
|
|
133
|
+
if (metadata.size > 1024 * 1024) throw new Error(`host settings exceed 1 MiB: ${basename(path)}`);
|
|
134
|
+
const value = JSON.parse(await readFile(file, "utf8"));
|
|
135
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`host settings are not an object: ${basename(path)}`);
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseTomlArray(text, key) {
|
|
140
|
+
const match = text.match(new RegExp(`^\\s*${key}\\s*=\\s*\\[([^\\]]*)\\]`, "m"));
|
|
141
|
+
if (!match) return null;
|
|
142
|
+
const values = [];
|
|
143
|
+
for (const item of match[1].matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g)) {
|
|
144
|
+
const value = item[1] === undefined ? item[2] : JSON.parse(`"${item[1]}"`);
|
|
145
|
+
if (!SAFE_NAME.test(value) || value.includes("/") || value.includes("\\")) throw new Error(`unsafe ${key} entry`);
|
|
146
|
+
values.push(value);
|
|
147
|
+
}
|
|
148
|
+
return values;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function codexConfig(home) {
|
|
152
|
+
const path = join(home, "config.toml");
|
|
153
|
+
const file = await existingRegular(path);
|
|
154
|
+
if (!file) return { fallbackNames: [], rootMarkers: [".git"], maxBytes: 32768 };
|
|
155
|
+
const metadata = await stat(file);
|
|
156
|
+
if (metadata.size > 1024 * 1024) throw new Error("Codex config exceeds 1 MiB");
|
|
157
|
+
const text = await readFile(file, "utf8");
|
|
158
|
+
const bytes = Number(text.match(/^\s*project_doc_max_bytes\s*=\s*(\d+)/m)?.[1] || 32768);
|
|
159
|
+
return {
|
|
160
|
+
fallbackNames: parseTomlArray(text, "project_doc_fallback_filenames") || [],
|
|
161
|
+
rootMarkers: parseTomlArray(text, "project_root_markers") || [".git"],
|
|
162
|
+
maxBytes: Number.isInteger(bytes) && bytes >= 1024 && bytes <= 4 * 1024 * 1024 ? bytes : 32768
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function findRoot(cwd, markers) {
|
|
167
|
+
let cursor = cwd;
|
|
168
|
+
while (true) {
|
|
169
|
+
for (const marker of markers) {
|
|
170
|
+
try { await lstat(join(cursor, marker)); return { root: cursor, resolution: "project-marker" }; } catch (error) {
|
|
171
|
+
if (error.code !== "ENOENT") throw sourceScanError(error);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const parent = dirname(cursor);
|
|
175
|
+
if (parent === cursor) return { root: cwd, resolution: "cwd-fallback" };
|
|
176
|
+
cursor = parent;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function skippedExtraDirectory(name) {
|
|
181
|
+
const lower = name.toLowerCase();
|
|
182
|
+
return SKIP_EXTRA_DIRS.has(name) || SKIP_EXTRA_DIRS.has(lower)
|
|
183
|
+
|| lower.includes("dropbox") || lower === "onedrive" || lower.startsWith("onedrive - ");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function skippableTraversalError(error) {
|
|
187
|
+
return ["EPERM", "EACCES", "ENOENT"].includes(error?.code);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function recordSkippedPath(skipped, path, error, operation) {
|
|
191
|
+
skipped.push({ path, code: error.code, operation });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function containsProjectMarker(directory) {
|
|
195
|
+
try {
|
|
196
|
+
await lstat(join(directory, ".git"));
|
|
197
|
+
return true;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error.code === "ENOENT") return false;
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function containsEmbeddedHostProfile(directory) {
|
|
205
|
+
return Boolean(
|
|
206
|
+
await existingDirectory(join(directory, ".runtime-private"))
|
|
207
|
+
&& await existingRegular(join(directory, "config.toml"))
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function boundedMarkdownTree(directory, prefix, host, scope, precedenceStart, deadline, {
|
|
212
|
+
projectBoundary = false,
|
|
213
|
+
maxFiles = MAX_RULE_FILES,
|
|
214
|
+
maxDirectoryEntries = MAX_DIRECTORY_ENTRIES,
|
|
215
|
+
skipped = []
|
|
216
|
+
} = {}) {
|
|
217
|
+
let root;
|
|
218
|
+
try {
|
|
219
|
+
root = await existingDirectory(directory);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (!skippableTraversalError(error)) throw sourceScanError(error);
|
|
222
|
+
recordSkippedPath(skipped, directory, error, "inspect-directory");
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
if (!root) return [];
|
|
226
|
+
const output = [];
|
|
227
|
+
let visitedEntries = 0;
|
|
228
|
+
async function walk(current) {
|
|
229
|
+
if (Date.now() > deadline) throw new Error(`host-native source resolution exceeded ${SOURCE_RESOLUTION_MS} ms`);
|
|
230
|
+
try {
|
|
231
|
+
if (projectBoundary && current !== root
|
|
232
|
+
&& (await containsProjectMarker(current) || await containsEmbeddedHostProfile(current))) return;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (!skippableTraversalError(error)) throw sourceScanError(error);
|
|
235
|
+
recordSkippedPath(skipped, current, error, "inspect-directory-boundary");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const entries = [];
|
|
239
|
+
let stream;
|
|
240
|
+
try {
|
|
241
|
+
stream = await opendir(current);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (!skippableTraversalError(error)) throw sourceScanError(error);
|
|
244
|
+
recordSkippedPath(skipped, error.path || current, error, "opendir");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
for await (const entry of stream) {
|
|
249
|
+
visitedEntries += 1;
|
|
250
|
+
if (visitedEntries > maxDirectoryEntries) throw new Error(`host-native source tree exceeds ${maxDirectoryEntries} entries`);
|
|
251
|
+
entries.push(entry);
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (!skippableTraversalError(error)) throw sourceScanError(error);
|
|
255
|
+
recordSkippedPath(skipped, current, error, "readdir");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
if (output.length >= maxFiles) throw new Error(`host-native rule tree exceeds ${maxFiles} files`);
|
|
261
|
+
if (entry.isSymbolicLink()) continue;
|
|
262
|
+
const path = join(current, entry.name);
|
|
263
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && !skippedExtraDirectory(entry.name)) await walk(path);
|
|
264
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
265
|
+
output.push({ path, id: `${prefix}/${relative(root, path).replaceAll("\\", "/")}`, host, scope,
|
|
266
|
+
binding: "host-native-rule-tree", precedence: precedenceStart + output.length });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
await walk(root);
|
|
271
|
+
return output;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function addFile(output, path, metadata) {
|
|
275
|
+
const file = await existingRegular(path);
|
|
276
|
+
if (file) output.push({ path: file, ...metadata });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function profileKey(host, hostHome) { return `${host}:${digest(hostHome).slice(0, 24)}`; }
|
|
280
|
+
|
|
281
|
+
function activeBinding(registry, host, hostHome, projectRoot, scope) {
|
|
282
|
+
const targetRoot = scope === "state-user" ? "*" : projectRoot;
|
|
283
|
+
return registry.bindings.find((item) => item.active && (item.host === host || item.host === "all")
|
|
284
|
+
&& (item.host === "all" || item.profileKey === profileKey(host, hostHome))
|
|
285
|
+
&& item.projectRoot === targetRoot && item.scope === scope) || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function rememberRuntimeBinding({ host, hostHome, projectRoot, sourceRoot, scope, provenance, env }) {
|
|
289
|
+
const canonicalSource = await canonicalPath(sourceRoot);
|
|
290
|
+
return mutateRegistry((registry) => {
|
|
291
|
+
const key = host === "all" ? "all:local-user" : profileKey(host, hostHome);
|
|
292
|
+
const targetRoot = scope === "state-user" ? "*" : projectRoot;
|
|
293
|
+
const current = registry.bindings.find((item) => item.active && item.host === host
|
|
294
|
+
&& item.profileKey === key && item.projectRoot === targetRoot && item.scope === scope);
|
|
295
|
+
if (current?.sourceRoot === canonicalSource) return { binding: current, unchanged: true };
|
|
296
|
+
if (current && current.sourceRoot !== canonicalSource) throw new Error(`conflicting ${host} ${scope} source binding; inspect or roll back the registry`);
|
|
297
|
+
const binding = {
|
|
298
|
+
id: `binding:${randomUUID()}`, host, profileKey: key, projectRoot: targetRoot, sourceRoot: canonicalSource,
|
|
299
|
+
scope, provenance, active: true, createdAt: new Date().toISOString(), authority: "context-only"
|
|
300
|
+
};
|
|
301
|
+
registry.bindings.push(binding);
|
|
302
|
+
registry.history.push({ kind: "bound", bindingId: binding.id, at: binding.createdAt, provenance, authority: "context-only" });
|
|
303
|
+
return { binding, unchanged: false };
|
|
304
|
+
}, env);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function bindSourceRoot({ host, hostHome, projectRoot, sourceRoot, scope = "state-user", confirmation, env = process.env }) {
|
|
308
|
+
if (confirmation !== "local-user-confirmed") throw new Error("source binding requires explicit local user confirmation");
|
|
309
|
+
if (!["claude", "codex", "all"].includes(host) || !["project-memory", "state-user"].includes(scope)
|
|
310
|
+
|| (host === "all" && scope !== "state-user")) throw new Error("invalid source binding scope");
|
|
311
|
+
return rememberRuntimeBinding({ host, hostHome: host === "all" ? "local-user" : await canonicalPath(hostHome), projectRoot: await canonicalPath(projectRoot),
|
|
312
|
+
sourceRoot, scope, provenance: "local-user-confirmed", env });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function rollbackSourceBinding({ id, confirmation, env = process.env }) {
|
|
316
|
+
if (confirmation !== "local-user-confirmed") throw new Error("source binding rollback requires explicit local user confirmation");
|
|
317
|
+
const result = await mutateRegistry((registry) => {
|
|
318
|
+
const binding = registry.bindings.find((item) => item.id === id && item.active);
|
|
319
|
+
if (!binding) throw new Error(`active source binding not found: ${id}`);
|
|
320
|
+
binding.active = false;
|
|
321
|
+
binding.rolledBackAt = new Date().toISOString();
|
|
322
|
+
registry.history.push({ kind: "rolled-back", bindingId: id, at: binding.rolledBackAt, authority: "context-only" });
|
|
323
|
+
return { binding };
|
|
324
|
+
}, env);
|
|
325
|
+
if (result.binding.scope === "project-memory") await purgeIndexedMemoryCache(result.binding.sourceRoot, env);
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function purgeSourceBinding({ id, confirmation, env = process.env }) {
|
|
330
|
+
if (confirmation !== "local-user-confirmed") throw new Error("source binding purge requires explicit local user confirmation");
|
|
331
|
+
const result = await mutateRegistry((registry) => {
|
|
332
|
+
const removed = registry.bindings.find((item) => item.id === id);
|
|
333
|
+
const before = registry.bindings.length;
|
|
334
|
+
registry.bindings = registry.bindings.filter((item) => item.id !== id);
|
|
335
|
+
if (registry.bindings.length === before) throw new Error(`source binding not found: ${id}`);
|
|
336
|
+
registry.history.push({ kind: "purged", bindingDigest: digest(id), at: new Date().toISOString(), authority: "context-only" });
|
|
337
|
+
return { purged: true, removed };
|
|
338
|
+
}, env);
|
|
339
|
+
if (result.removed?.scope === "project-memory") await purgeIndexedMemoryCache(result.removed.sourceRoot, env);
|
|
340
|
+
return { ...result, removed: undefined };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function inspectSourceRegistry(env = process.env) {
|
|
344
|
+
const { registry, path } = await readRegistry(env);
|
|
345
|
+
return { registry, registryPath: path };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function claudeSources({ cwd, projectRoot, configDir, input, env, registry, deadline, memoryHooks }) {
|
|
349
|
+
const sources = [];
|
|
350
|
+
const skipped = [];
|
|
351
|
+
await addFile(sources, join(configDir, "CLAUDE.md"), { id: "claude:user/CLAUDE.md", host: "claude", scope: "user", binding: "CLAUDE_CONFIG_DIR", precedence: 100 });
|
|
352
|
+
sources.push(...await boundedMarkdownTree(join(configDir, "rules"), "claude:user/rules", "claude", "user", 110, deadline, { skipped }));
|
|
353
|
+
let precedence = 1000;
|
|
354
|
+
for (const directory of ancestorsBetween(projectRoot, cwd)) {
|
|
355
|
+
for (const name of ["CLAUDE.md", "CLAUDE.local.md"]) {
|
|
356
|
+
await addFile(sources, join(directory, name), { id: `claude:project/${relative(projectRoot, join(directory, name)).replaceAll("\\", "/")}`,
|
|
357
|
+
host: "claude", scope: "project", binding: "native-project-chain", precedence: precedence++ });
|
|
358
|
+
}
|
|
359
|
+
await addFile(sources, join(directory, ".claude", "CLAUDE.md"), { id: `claude:project/${relative(projectRoot, join(directory, ".claude", "CLAUDE.md")).replaceAll("\\", "/")}`,
|
|
360
|
+
host: "claude", scope: "project", binding: "native-project-chain", precedence: precedence++ });
|
|
361
|
+
}
|
|
362
|
+
sources.push(...await boundedMarkdownTree(join(projectRoot, ".claude", "rules"), "claude:project/.claude/rules", "claude", "project", precedence, deadline, { skipped }));
|
|
363
|
+
|
|
364
|
+
let memoryRoot = null;
|
|
365
|
+
let memoryProvenance = null;
|
|
366
|
+
for (const path of [join(configDir, "settings.json"), join(projectRoot, ".claude", "settings.json"), join(projectRoot, ".claude", "settings.local.json")]) {
|
|
367
|
+
const value = await readJsonObject(path);
|
|
368
|
+
if (typeof value?.autoMemoryDirectory === "string") {
|
|
369
|
+
const expanded = expandHome(value.autoMemoryDirectory, homedir());
|
|
370
|
+
if (!isAbsolute(expanded)) throw new Error("Claude autoMemoryDirectory must be absolute or home-relative");
|
|
371
|
+
memoryRoot = await existingDirectory(expanded);
|
|
372
|
+
memoryProvenance = "autoMemoryDirectory";
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (!memoryRoot && typeof env.CLAUDE_CODE_PROJECT_DIR_NAME === "string" && SAFE_NAME.test(env.CLAUDE_CODE_PROJECT_DIR_NAME)) {
|
|
376
|
+
memoryRoot = await existingDirectory(join(configDir, "projects", env.CLAUDE_CODE_PROJECT_DIR_NAME, "memory"));
|
|
377
|
+
memoryProvenance = "CLAUDE_CODE_PROJECT_DIR_NAME";
|
|
378
|
+
}
|
|
379
|
+
const transcript = input.transcript_path || input.transcriptPath;
|
|
380
|
+
if (!memoryRoot && typeof transcript === "string" && isAbsolute(transcript)) {
|
|
381
|
+
const transcriptFile = await existingRegular(transcript);
|
|
382
|
+
const projectsRoot = await existingDirectory(join(configDir, "projects"));
|
|
383
|
+
if (transcriptFile && projectsRoot && isInside(projectsRoot, transcriptFile)) {
|
|
384
|
+
memoryRoot = await existingDirectory(join(dirname(transcriptFile), "memory"));
|
|
385
|
+
memoryProvenance = "host-hook-transcript";
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (memoryRoot) {
|
|
389
|
+
await rememberRuntimeBinding({ host: "claude", hostHome: configDir, projectRoot, sourceRoot: memoryRoot,
|
|
390
|
+
scope: "project-memory", provenance: memoryProvenance, env });
|
|
391
|
+
} else {
|
|
392
|
+
memoryRoot = activeBinding(registry, "claude", configDir, projectRoot, "project-memory")?.sourceRoot || null;
|
|
393
|
+
memoryProvenance = memoryRoot ? "source-root-registry" : null;
|
|
394
|
+
}
|
|
395
|
+
let memoryDiagnostics = null;
|
|
396
|
+
if (memoryRoot) {
|
|
397
|
+
const supplied = input.agent_spine_scope && typeof input.agent_spine_scope === "object"
|
|
398
|
+
? input.agent_spine_scope : input;
|
|
399
|
+
const resolved = await resolveIndexedMemory({
|
|
400
|
+
root: memoryRoot, env, hooks: memoryHooks, deadline,
|
|
401
|
+
scope: {
|
|
402
|
+
entityId: supplied.entity_id ?? supplied.entityId ?? null,
|
|
403
|
+
groupId: supplied.group_id ?? supplied.groupId ?? null,
|
|
404
|
+
projectId: supplied.project_id ?? supplied.projectId ?? null,
|
|
405
|
+
currentTaskId: supplied.task_id ?? supplied.currentTaskId ?? null,
|
|
406
|
+
prompt: input.prompt ?? input.user_prompt ?? input.message ?? input.input ?? null
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
memoryDiagnostics = resolved.diagnostics;
|
|
410
|
+
for (const item of resolved.sources) {
|
|
411
|
+
sources.push({
|
|
412
|
+
path: item.path, id: `claude:memory/${item.relativePath}`, host: "claude", scope: "project-memory",
|
|
413
|
+
binding: "host-native-memory-index", precedence: 2000 + sources.length,
|
|
414
|
+
snapshot: item.snapshot, relevance: item.relevance
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return { sources, memoryRoot, memoryProvenance, memoryDiagnostics, skipped };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function codexSources({ cwd, projectRoot, codexHome, config }) {
|
|
422
|
+
const sources = [];
|
|
423
|
+
for (const name of ["AGENTS.override.md", "AGENTS.md"]) {
|
|
424
|
+
const file = await existingRegular(join(codexHome, name));
|
|
425
|
+
if (file && (await stat(file)).size > 0) {
|
|
426
|
+
sources.push({ path: file, id: `codex:user/${name}`, host: "codex", scope: "user", binding: "CODEX_HOME", precedence: 100, maxBytes: config.maxBytes });
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
let precedence = 1000;
|
|
431
|
+
for (const directory of ancestorsBetween(projectRoot, cwd)) {
|
|
432
|
+
for (const name of ["AGENTS.override.md", "AGENTS.md", ...config.fallbackNames]) {
|
|
433
|
+
const file = await existingRegular(join(directory, name));
|
|
434
|
+
if (file && (await stat(file)).size > 0) {
|
|
435
|
+
sources.push({ path: file, id: `codex:project/${relative(projectRoot, file).replaceAll("\\", "/")}`,
|
|
436
|
+
host: "codex", scope: "project", binding: "native-project-chain", precedence: precedence++, maxBytes: config.maxBytes });
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return sources;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export async function resolveHostSourceCatalog({ host, cwd = process.cwd(), input = {}, env = process.env, memoryHooks = {} } = {}) {
|
|
445
|
+
if (!["claude", "codex"].includes(host)) throw new Error(`host-native source resolution requires claude or codex, received ${host}`);
|
|
446
|
+
const canonicalCwd = await canonicalPath(cwd);
|
|
447
|
+
const deadline = Date.now() + SOURCE_RESOLUTION_MS;
|
|
448
|
+
const { registry } = await readRegistry(env);
|
|
449
|
+
let hostHome;
|
|
450
|
+
let projectRoot;
|
|
451
|
+
let sources;
|
|
452
|
+
let skipped = [];
|
|
453
|
+
let hostDetails = {};
|
|
454
|
+
let rootResolution = "explicit-root";
|
|
455
|
+
if (host === "codex") {
|
|
456
|
+
const codexHome = env.CODEX_HOME || env.BLUN_HOME || join(homedir(), ".codex");
|
|
457
|
+
hostHome = await existingDirectory(resolve(codexHome)) || resolve(codexHome);
|
|
458
|
+
const config = await codexConfig(hostHome);
|
|
459
|
+
if (env.AGENTSPINE_ROOT) projectRoot = await canonicalPath(env.AGENTSPINE_ROOT);
|
|
460
|
+
else ({ root: projectRoot, resolution: rootResolution } = await findRoot(canonicalCwd, config.rootMarkers));
|
|
461
|
+
sources = await codexSources({ cwd: canonicalCwd, projectRoot, codexHome: hostHome, config });
|
|
462
|
+
hostDetails = { rootMarkers: config.rootMarkers, fallbackNames: config.fallbackNames };
|
|
463
|
+
} else {
|
|
464
|
+
hostHome = await existingDirectory(resolve(env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude")))
|
|
465
|
+
|| resolve(env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"));
|
|
466
|
+
if (env.AGENTSPINE_ROOT) projectRoot = await canonicalPath(env.AGENTSPINE_ROOT);
|
|
467
|
+
else ({ root: projectRoot, resolution: rootResolution } = await findRoot(canonicalCwd, [".git"]));
|
|
468
|
+
const result = await claudeSources({ cwd: canonicalCwd, projectRoot, configDir: hostHome, input, env, registry, deadline, memoryHooks });
|
|
469
|
+
sources = result.sources;
|
|
470
|
+
skipped = result.skipped;
|
|
471
|
+
hostDetails = { memoryRoot: result.memoryRoot, memoryProvenance: result.memoryProvenance,
|
|
472
|
+
memoryDiagnostics: result.memoryDiagnostics };
|
|
473
|
+
}
|
|
474
|
+
const knownHomeRoots = await homeRoots(env);
|
|
475
|
+
const skippedHomeTree = knownHomeRoots.some((root) => samePath(root, projectRoot));
|
|
476
|
+
const skippedFallbackHomeTree = skippedHomeTree && rootResolution === "cwd-fallback";
|
|
477
|
+
if (!skippedHomeTree) {
|
|
478
|
+
sources.push(...await boundedMarkdownTree(projectRoot, "agentspine:project", host, "project", 3000, deadline,
|
|
479
|
+
{ projectBoundary: true, maxFiles: MAX_PROJECT_FILES, maxDirectoryEntries: MAX_PROJECT_DIRECTORY_ENTRIES, skipped }));
|
|
480
|
+
}
|
|
481
|
+
const nativeNames = new Set(host === "codex"
|
|
482
|
+
? ["AGENTS.override.md", "AGENTS.md", ...(hostDetails.fallbackNames || [])]
|
|
483
|
+
: ["CLAUDE.md", "CLAUDE.local.md"]);
|
|
484
|
+
sources = sources.filter((item) => item.binding !== "host-native-rule-tree"
|
|
485
|
+
|| !item.id.startsWith("agentspine:project/") || !nativeNames.has(basename(item.path)));
|
|
486
|
+
sources = [...new Map(sources.map((item) => [item.path, item])).values()];
|
|
487
|
+
if (sources.length > MAX_SOURCES) throw new Error(`host-native source set exceeds ${MAX_SOURCES} files`);
|
|
488
|
+
if (Date.now() > deadline) throw new Error(`host-native source resolution exceeded ${SOURCE_RESOLUTION_MS} ms`);
|
|
489
|
+
let documents;
|
|
490
|
+
try {
|
|
491
|
+
documents = await indexExplicitDocuments(sources);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (typeof error?.code === "string" && (error.path || error.syscall)) throw sourceScanError(error);
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
if (Date.now() > deadline) throw new Error(`host-native source resolution exceeded ${SOURCE_RESOLUTION_MS} ms`);
|
|
497
|
+
const totalBytes = documents.reduce((sum, document) => sum + document.bytes, 0);
|
|
498
|
+
if (totalBytes > MAX_TOTAL_SOURCE_BYTES) throw new Error("host-native source set exceeds 8 MiB");
|
|
499
|
+
const activeUserState = activeBinding(registry, host, hostHome, projectRoot, "state-user");
|
|
500
|
+
const diagnostics = {
|
|
501
|
+
schema: SOURCE_REGISTRY_SCHEMA, host, status: documents.length ? "loaded" : "empty", projectRoot,
|
|
502
|
+
hostHomeDigest: digest(hostHome).slice(0, 16), checked: ["host-profile", "project-chain", ...(host === "claude" ? ["project-memory"] : [])],
|
|
503
|
+
scopes: Object.fromEntries(["user", "project", "project-memory"].map((scope) => [scope, documents.filter((item) => item.sourceScope === scope).length])),
|
|
504
|
+
reason: documents.length ? null : "No regular, non-symlink host-native Markdown source exists in the checked scope.",
|
|
505
|
+
personalContinuityLoaded: documents.some((item) => item.sourceScope === "user") || Boolean(activeUserState),
|
|
506
|
+
broadHomeScan: false, projectTreeScan: skippedFallbackHomeTree ? "skipped-unmarked-home"
|
|
507
|
+
: skippedHomeTree ? "skipped-home-root" : "bounded",
|
|
508
|
+
skipped: skipped.sort((a, b) => a.path.localeCompare(b.path) || a.operation.localeCompare(b.operation)),
|
|
509
|
+
rootResolution, registryRevision: registry.revision,
|
|
510
|
+
...(host === "claude" ? {
|
|
511
|
+
memoryBound: Boolean(hostDetails.memoryRoot),
|
|
512
|
+
memoryRootDigest: hostDetails.memoryRoot ? digest(hostDetails.memoryRoot).slice(0, 16) : null,
|
|
513
|
+
memoryProvenance: hostDetails.memoryProvenance,
|
|
514
|
+
memory: hostDetails.memoryDiagnostics || {
|
|
515
|
+
indexed: 0, relevant: 0, loaded: 0, cacheHits: 0, cacheMisses: 0, missing: 0,
|
|
516
|
+
rejected: { scope: 0, path: 0, symlink: 0, race: 0, size: 0 }, directoryEnumeration: 0
|
|
517
|
+
}
|
|
518
|
+
} : hostDetails)
|
|
519
|
+
};
|
|
520
|
+
const catalog = {
|
|
521
|
+
schema: "agentspine.catalog/v1", generatedAt: new Date().toISOString(), root: projectRoot,
|
|
522
|
+
scanPolicy: catalogScanPolicy(projectRoot, env),
|
|
523
|
+
preservation: "source-files-are-read-only", documents, conflicts: [], sourceRegistry: diagnostics,
|
|
524
|
+
summary: { total: documents.length, protected: documents.filter((item) => item.protected).length, conflicts: 0,
|
|
525
|
+
byLayer: Object.fromEntries([...new Set(documents.map((item) => item.layer))].sort().map((layer) => [layer, documents.filter((item) => item.layer === layer).length])) }
|
|
526
|
+
};
|
|
527
|
+
return { host, hostHome, projectRoot, cwd: canonicalCwd, catalog, diagnostics,
|
|
528
|
+
userStateRoot: activeUserState?.sourceRoot || null, memoryRoot: hostDetails.memoryRoot || null };
|
|
529
|
+
}
|