blun-king-cli 9.1.567 → 9.1.569

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
  2. package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
  3. package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
  4. package/agent-spine-plugin/CHANGELOG.md +1581 -0
  5. package/agent-spine-plugin/README.md +30 -4
  6. package/agent-spine-plugin/blun.plugin.json +3 -3
  7. package/agent-spine-plugin/docs/acceptance.md +61 -0
  8. package/agent-spine-plugin/docs/assignment-continuation.md +48 -0
  9. package/agent-spine-plugin/docs/host-integration.md +178 -0
  10. package/agent-spine-plugin/docs/preflight-recall.md +69 -0
  11. package/agent-spine-plugin/docs/preservation-contract.md +53 -0
  12. package/agent-spine-plugin/docs/quality-gates.md +50 -0
  13. package/agent-spine-plugin/docs/releasing.md +85 -0
  14. package/agent-spine-plugin/docs/session-timeline.md +251 -0
  15. package/agent-spine-plugin/docs/source-roots.md +113 -0
  16. package/agent-spine-plugin/docs/structured-completion.md +67 -0
  17. package/agent-spine-plugin/docs/world-model.md +94 -0
  18. package/agent-spine-plugin/hooks/codex.json +2 -2
  19. package/agent-spine-plugin/hooks/hooks.json +1 -1
  20. package/agent-spine-plugin/hooks/version.json +1 -1
  21. package/agent-spine-plugin/package.json +13 -3
  22. package/agent-spine-plugin/scripts/check-codex-install.js +226 -0
  23. package/agent-spine-plugin/scripts/check-hosts.js +14 -5
  24. package/agent-spine-plugin/scripts/check-install-hook.js +226 -0
  25. package/agent-spine-plugin/scripts/check-install-selfstarter.js +154 -0
  26. package/agent-spine-plugin/scripts/check-install.js +478 -0
  27. package/agent-spine-plugin/scripts/check-line-budget.js +58 -0
  28. package/agent-spine-plugin/scripts/check-syntax.js +29 -0
  29. package/agent-spine-plugin/scripts/github-actions.js +11 -0
  30. package/agent-spine-plugin/scripts/hermetic-process.js +183 -0
  31. package/agent-spine-plugin/scripts/release-check.js +145 -0
  32. package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
  33. package/agent-spine-plugin/scripts/run-checks.js +47 -0
  34. package/agent-spine-plugin/scripts/run-tests-hermetic.js +89 -0
  35. package/agent-spine-plugin/skills/agent-spine/SKILL.md +16 -2
  36. package/agent-spine-plugin/spine-example/1-identity.md +12 -0
  37. package/agent-spine-plugin/spine-example/2-voice.md +6 -0
  38. package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
  39. package/agent-spine-plugin/spine-example/4-history.md +4 -0
  40. package/agent-spine-plugin/src/cli-agent.js +296 -0
  41. package/agent-spine-plugin/src/cli-attention.js +95 -0
  42. package/agent-spine-plugin/src/cli-autonomy.js +36 -0
  43. package/agent-spine-plugin/src/cli-common.js +71 -0
  44. package/agent-spine-plugin/src/cli-continuity.js +116 -0
  45. package/agent-spine-plugin/src/cli-core.js +128 -0
  46. package/agent-spine-plugin/src/cli-diagnostics.js +291 -0
  47. package/agent-spine-plugin/src/cli-host.js +21 -0
  48. package/agent-spine-plugin/src/cli-learning.js +305 -0
  49. package/agent-spine-plugin/src/cli-premortem.js +16 -0
  50. package/agent-spine-plugin/src/cli-sharing.js +230 -0
  51. package/agent-spine-plugin/src/cli.js +40 -1350
  52. package/agent-spine-plugin/src/codex-reader-launcher.js +182 -0
  53. package/agent-spine-plugin/src/hook.js +185 -567
  54. package/agent-spine-plugin/src/index.js +16 -2
  55. package/agent-spine-plugin/src/lib/acceptance.js +113 -11
  56. package/agent-spine-plugin/src/lib/action-lesson-recall.js +53 -0
  57. package/agent-spine-plugin/src/lib/attention-context.js +167 -0
  58. package/agent-spine-plugin/src/lib/attention-events.js +113 -0
  59. package/agent-spine-plugin/src/lib/attention-privacy.js +72 -0
  60. package/agent-spine-plugin/src/lib/attention-schema.js +164 -0
  61. package/agent-spine-plugin/src/lib/attention-storage.js +93 -0
  62. package/agent-spine-plugin/src/lib/attention.js +9 -600
  63. package/agent-spine-plugin/src/lib/audit-premortem.js +223 -0
  64. package/agent-spine-plugin/src/lib/audit.js +56 -6
  65. package/agent-spine-plugin/src/lib/autonomy-policy.js +110 -0
  66. package/agent-spine-plugin/src/lib/autonomy-store.js +202 -0
  67. package/agent-spine-plugin/src/lib/autonomy.js +8 -0
  68. package/agent-spine-plugin/src/lib/briefing.js +67 -4
  69. package/agent-spine-plugin/src/lib/catalog-document-read.js +51 -0
  70. package/agent-spine-plugin/src/lib/catalog.js +1 -1
  71. package/agent-spine-plugin/src/lib/codex-installation.js +231 -0
  72. package/agent-spine-plugin/src/lib/codex-skill-installation.js +211 -0
  73. package/agent-spine-plugin/src/lib/context.js +3 -1
  74. package/agent-spine-plugin/src/lib/delivery-agent-usage.js +224 -0
  75. package/agent-spine-plugin/src/lib/delivery-assignment.js +220 -0
  76. package/agent-spine-plugin/src/lib/delivery-command-actions.js +453 -0
  77. package/agent-spine-plugin/src/lib/delivery-knowledge.js +78 -0
  78. package/agent-spine-plugin/src/lib/delivery-premortem-binding.js +107 -0
  79. package/agent-spine-plugin/src/lib/delivery-premortem-closure.js +171 -0
  80. package/agent-spine-plugin/src/lib/delivery-premortem-codec.js +45 -0
  81. package/agent-spine-plugin/src/lib/delivery-premortem-correction.js +101 -0
  82. package/agent-spine-plugin/src/lib/delivery-premortem-file.js +21 -0
  83. package/agent-spine-plugin/src/lib/delivery-premortem-index.js +493 -0
  84. package/agent-spine-plugin/src/lib/delivery-premortem-inspection.js +37 -0
  85. package/agent-spine-plugin/src/lib/delivery-premortem-recovery.js +120 -0
  86. package/agent-spine-plugin/src/lib/delivery-premortem-rejection.js +65 -0
  87. package/agent-spine-plugin/src/lib/delivery-premortem-results.js +28 -0
  88. package/agent-spine-plugin/src/lib/delivery-premortem-session-guard.js +46 -0
  89. package/agent-spine-plugin/src/lib/delivery-premortem-write-ledger.js +285 -0
  90. package/agent-spine-plugin/src/lib/delivery-premortem.js +499 -0
  91. package/agent-spine-plugin/src/lib/delivery-shell-heredoc.js +115 -0
  92. package/agent-spine-plugin/src/lib/delivery-shell-substitutions.js +111 -0
  93. package/agent-spine-plugin/src/lib/delivery-shell-wrapper.js +64 -0
  94. package/agent-spine-plugin/src/lib/delivery-target.js +73 -0
  95. package/agent-spine-plugin/src/lib/delivery-verification.js +445 -0
  96. package/agent-spine-plugin/src/lib/documents.js +27 -5
  97. package/agent-spine-plugin/src/lib/filesystem-retry.js +2 -0
  98. package/agent-spine-plugin/src/lib/gateway-common.js +68 -0
  99. package/agent-spine-plugin/src/lib/gateway-control.js +302 -0
  100. package/agent-spine-plugin/src/lib/gateway-delivery.js +103 -0
  101. package/agent-spine-plugin/src/lib/gateway-execution.js +350 -0
  102. package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +185 -0
  103. package/agent-spine-plugin/src/lib/gateway-inspection.js +85 -0
  104. package/agent-spine-plugin/src/lib/gateway-knowledge.js +129 -0
  105. package/agent-spine-plugin/src/lib/gateway-policy-provenance.js +197 -0
  106. package/agent-spine-plugin/src/lib/gateway-premortem-disposition.js +82 -0
  107. package/agent-spine-plugin/src/lib/gateway-premortem.js +363 -0
  108. package/agent-spine-plugin/src/lib/gateway-runs.js +300 -0
  109. package/agent-spine-plugin/src/lib/gateway-runtime-identity.js +31 -0
  110. package/agent-spine-plugin/src/lib/gateway-runtime-records.js +21 -0
  111. package/agent-spine-plugin/src/lib/gateway-runtime.js +18 -1623
  112. package/agent-spine-plugin/src/lib/gateway-state-transaction.js +356 -0
  113. package/agent-spine-plugin/src/lib/gateway-state.js +342 -0
  114. package/agent-spine-plugin/src/lib/hook-artifact-guards.js +356 -0
  115. package/agent-spine-plugin/src/lib/hook-audit.js +16 -2
  116. package/agent-spine-plugin/src/lib/hook-briefing-use.js +68 -0
  117. package/agent-spine-plugin/src/lib/hook-context.js +424 -0
  118. package/agent-spine-plugin/src/lib/hook-final-message.js +26 -0
  119. package/agent-spine-plugin/src/lib/hook-input.js +27 -0
  120. package/agent-spine-plugin/src/lib/hook-output.js +140 -0
  121. package/agent-spine-plugin/src/lib/hook-premortem.js +257 -0
  122. package/agent-spine-plugin/src/lib/hook-process-advisory.js +37 -0
  123. package/agent-spine-plugin/src/lib/hook-protection.js +95 -0
  124. package/agent-spine-plugin/src/lib/hook-stop-verification.js +84 -0
  125. package/agent-spine-plugin/src/lib/hook-timeline.js +91 -0
  126. package/agent-spine-plugin/src/lib/identifier-analysis.js +446 -0
  127. package/agent-spine-plugin/src/lib/indexed-memory.js +23 -6
  128. package/agent-spine-plugin/src/lib/knowledge-evidence.js +431 -0
  129. package/agent-spine-plugin/src/lib/learning-applications.js +441 -0
  130. package/agent-spine-plugin/src/lib/learning-candidates.js +274 -0
  131. package/agent-spine-plugin/src/lib/learning-context.js +130 -0
  132. package/agent-spine-plugin/src/lib/learning-delivery-contracts.js +310 -0
  133. package/agent-spine-plugin/src/lib/learning-evaluation-contracts.js +350 -0
  134. package/agent-spine-plugin/src/lib/learning-evaluation-registration.js +372 -0
  135. package/agent-spine-plugin/src/lib/learning-evaluation-revocation.js +292 -0
  136. package/agent-spine-plugin/src/lib/learning-evidence-contracts.js +321 -0
  137. package/agent-spine-plugin/src/lib/learning-findings.js +458 -0
  138. package/agent-spine-plugin/src/lib/learning-measurement-contracts.js +285 -0
  139. package/agent-spine-plugin/src/lib/learning-measurements.js +265 -0
  140. package/agent-spine-plugin/src/lib/learning-outcome-contracts.js +165 -0
  141. package/agent-spine-plugin/src/lib/learning-outcomes.js +343 -0
  142. package/agent-spine-plugin/src/lib/learning-reconciliation.js +290 -0
  143. package/agent-spine-plugin/src/lib/learning-retry-contracts.js +188 -0
  144. package/agent-spine-plugin/src/lib/learning-schema.js +229 -0
  145. package/agent-spine-plugin/src/lib/learning-scope-targets.js +424 -0
  146. package/agent-spine-plugin/src/lib/learning-state-upgrade.js +477 -0
  147. package/agent-spine-plugin/src/lib/learning-status-configuration.js +476 -0
  148. package/agent-spine-plugin/src/lib/learning-storage.js +203 -0
  149. package/agent-spine-plugin/src/lib/learning-trial-recovery.js +218 -0
  150. package/agent-spine-plugin/src/lib/learning-validation-contracts.js +375 -0
  151. package/agent-spine-plugin/src/lib/learning-validation-renewal.js +298 -0
  152. package/agent-spine-plugin/src/lib/learning-validation-runtime.js +234 -0
  153. package/agent-spine-plugin/src/lib/learning.js +36 -6923
  154. package/agent-spine-plugin/src/lib/lesson-recall-session.js +172 -0
  155. package/agent-spine-plugin/src/lib/mcp-autonomy-tools.js +37 -0
  156. package/agent-spine-plugin/src/lib/mcp-delivery-completion.js +124 -0
  157. package/agent-spine-plugin/src/lib/mcp-delivery-tools.js +45 -0
  158. package/agent-spine-plugin/src/lib/mcp-premortem.js +68 -0
  159. package/agent-spine-plugin/src/lib/mcp-runtime.js +269 -0
  160. package/agent-spine-plugin/src/lib/mcp-source-context.js +51 -0
  161. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +164 -0
  162. package/agent-spine-plugin/src/lib/mcp-world-tools.js +52 -0
  163. package/agent-spine-plugin/src/lib/owned-file-lock.js +30 -10
  164. package/agent-spine-plugin/src/lib/project-portfolio.js +176 -0
  165. package/agent-spine-plugin/src/lib/selfstarter-core.js +382 -0
  166. package/agent-spine-plugin/src/lib/selfstarter-jobs.js +149 -0
  167. package/agent-spine-plugin/src/lib/selfstarter-lease.js +204 -0
  168. package/agent-spine-plugin/src/lib/selfstarter-policy.js +90 -0
  169. package/agent-spine-plugin/src/lib/selfstarter-workspace.js +111 -0
  170. package/agent-spine-plugin/src/lib/selfstarter.js +11 -911
  171. package/agent-spine-plugin/src/lib/session-timeline-auth.js +316 -0
  172. package/agent-spine-plugin/src/lib/session-timeline-codex.js +58 -0
  173. package/agent-spine-plugin/src/lib/session-timeline-contract.js +48 -0
  174. package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +41 -0
  175. package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +132 -0
  176. package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +17 -0
  177. package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +500 -0
  178. package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +157 -0
  179. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +74 -0
  180. package/agent-spine-plugin/src/lib/session-timeline-host-receipt.js +117 -0
  181. package/agent-spine-plugin/src/lib/session-timeline-invocation.js +201 -0
  182. package/agent-spine-plugin/src/lib/session-timeline-king.js +79 -0
  183. package/agent-spine-plugin/src/lib/session-timeline-prior.js +59 -0
  184. package/agent-spine-plugin/src/lib/session-timeline-provider.js +34 -0
  185. package/agent-spine-plugin/src/lib/session-timeline-query.js +55 -0
  186. package/agent-spine-plugin/src/lib/session-timeline-results.js +31 -0
  187. package/agent-spine-plugin/src/lib/session-timeline-root.js +11 -0
  188. package/agent-spine-plugin/src/lib/session-timeline-search.js +82 -0
  189. package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +217 -0
  190. package/agent-spine-plugin/src/lib/session-timeline-source.js +83 -0
  191. package/agent-spine-plugin/src/lib/session-timeline-state.js +45 -0
  192. package/agent-spine-plugin/src/lib/session-timeline-transport.js +50 -0
  193. package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +148 -0
  194. package/agent-spine-plugin/src/lib/session-timeline.js +453 -0
  195. package/agent-spine-plugin/src/lib/source-roots.js +69 -136
  196. package/agent-spine-plugin/src/lib/source-tree-scan.js +178 -0
  197. package/agent-spine-plugin/src/lib/task-knowledge-context.js +78 -0
  198. package/agent-spine-plugin/src/lib/timeline-tool-guard.js +202 -0
  199. package/agent-spine-plugin/src/lib/world-knowledge.js +249 -0
  200. package/agent-spine-plugin/src/lib/world-model.js +278 -0
  201. package/agent-spine-plugin/src/mcp.js +20 -161
  202. package/agent-spine-plugin/src/version.js +1 -1
  203. package/agent-spine-plugin/src/worker.js +22 -5
  204. package/bin/agent-resume-snapshot.cjs +2 -2
  205. package/bin/agentspine-king-goal-inbox.mjs +111 -0
  206. package/bin/agentspine-king-goal-intake.mjs +106 -0
  207. package/bin/baseline-skill-performance-policy.cjs +1 -16
  208. package/bin/core-bootstrap.js +2 -0
  209. package/bin/curiosity-scout-policy.cjs +5 -1
  210. package/bin/input-draft-persistence.cjs +2 -2
  211. package/bin/king-tui-function-contract.json +33 -0
  212. package/bin/launcher-restart-policy.cjs +150 -0
  213. package/bin/launcher-runtime.js +56 -44
  214. package/bin/managed-context-startup-policy.cjs +27 -0
  215. package/bin/managed-plugin-selection.cjs +116 -0
  216. package/bin/mistake-relevance-policy.cjs +1 -1
  217. package/bin/observer-hooks.cjs +14 -0
  218. package/bin/oversized-context-offload-policy.cjs +86 -0
  219. package/bin/plugin-bootstrap.js +7 -40
  220. package/bin/proactive-compaction-policy.cjs +1 -1
  221. package/bin/provider-model-refresh-deadline.cjs +53 -0
  222. package/bin/provider-model-refresh-policy.cjs +107 -0
  223. package/bin/release-artifact-freeze-policy.cjs +30 -0
  224. package/bin/repeated-user-message-projection.cjs +3 -126
  225. package/bin/research-page-result.cjs +74 -0
  226. package/bin/runtime-exit-ledger.cjs +1 -0
  227. package/bin/session-compaction-policy.cjs +84 -0
  228. package/bin/skill-listing-performance-policy.cjs +2 -2
  229. package/bin/standard-tools-bootstrap.js +0 -37
  230. package/bin/subagent-skill-policy.cjs +3 -1
  231. package/bin/telegram-approval-relay.cjs +12 -7
  232. package/bin/telegram-private-conversation-policy.cjs +3 -2
  233. package/bin/telegram-queue-handoff-policy.cjs +24 -0
  234. package/bin/thinking-activity-status-policy.cjs +1 -1
  235. package/bin/thinking-only-guard.cjs +16 -12
  236. package/bin/tool-call-loop-policy.cjs +0 -2
  237. package/bin/tool-result-offload-policy.cjs +11 -2
  238. package/bin/tui-functional-contract.cjs +55 -0
  239. package/bin/update-notice.js +18 -14
  240. package/bin/user-message-offload-policy.cjs +1 -1
  241. package/bin/user-prompt-hook-origin-policy.cjs +34 -0
  242. package/bin/windows-node-crash-dump.cjs +110 -0
  243. package/blun.mjs +2517 -1048
  244. package/codebase-index/codebase_index.py +4 -3
  245. package/package.json +3 -17
  246. package/standard-skills/research-evidence/SKILL.md +39 -0
  247. package/standard-skills/research-evidence/references/evidence-format.md +82 -0
  248. package/standard-skills/research-evidence/scripts/evidence-collection.cjs +254 -0
  249. package/standard-skills/research-evidence/scripts/score-report.cjs +112 -0
  250. package/standard-skills/web-lesen/SKILL.md +37 -22
  251. package/standard-skills/web-lesen/scripts/crawl_public.py +376 -0
  252. package/telegram-plugin/DELIVERY.md +36 -0
  253. package/telegram-plugin/bin/telegram-approval-relay.cjs +13 -7
  254. package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +122 -0
  255. package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +3 -2
  256. package/telegram-plugin/bin/telegram-reply-parts.cjs +149 -0
  257. package/telegram-plugin/dist/bridge.mjs +7 -56
  258. package/telegram-plugin/dist/mcp-server.mjs +33 -4
  259. package/agent-spine-plugin/skill/SKILL.md +0 -76
  260. package/bin/mnemo-connect-heartbeat.cjs +0 -204
  261. package/bin/mnemo-tool-agent-policy.cjs +0 -22
  262. package/telegram-plugin/bin/telegram-mnemo-capture.cjs +0 -297
@@ -0,0 +1,202 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lstat, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
3
+ import { isAbsolute, join, resolve } from "node:path";
4
+ import { buildCatalog } from "./catalog.js";
5
+ import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
6
+ import { projectStateDir } from "./paths.js";
7
+
8
+ export const AUTONOMY_SCHEMA = "agentspine.autonomy-portfolio/v1";
9
+ export const AUTONOMY_MODES = Object.freeze(["observe", "advise", "execute", "publish"]);
10
+ export const AUTONOMY_ACTIONS = Object.freeze(["observe", "advise", "execute", "publish"]);
11
+ export const EVIDENCE_CLASSES = Object.freeze(["objective", "user-feedback", "model-suggestion"]);
12
+ export const AUTONOMY_CONFIRMATION = "local-owner-confirmed";
13
+ export const PUBLISH_CONFIRMATION = "local-owner-publish-confirmed";
14
+ export const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
15
+ export const DIGEST_RE = /^[a-f0-9]{64}$/;
16
+ export const CAPABILITY_RE = /^tool:[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
17
+ const MAX_STATE_BYTES = 5 * 1024 * 1024;
18
+ const SECRET_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|gh[opusu])_[A-Za-z0-9_-]{20,}|\bBearer\s+[A-Za-z0-9._~+/-]{20,}|\b(?:api[-_ ]?key|token|password|secret)\s*[:=]\s*\S{8,}/i;
19
+
20
+ export function digest(value) {
21
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
22
+ }
23
+
24
+ export function stableId(value, field, nullable = false) {
25
+ if ((value === null || value === undefined || value === "") && nullable) return null;
26
+ if (typeof value !== "string" || !ID_RE.test(value)) throw new Error(`${field} must be a stable whitespace-free ID`);
27
+ return value;
28
+ }
29
+
30
+ export function safeText(value, field, maximum, nullable = false) {
31
+ if ((value === null || value === undefined || value === "") && nullable) return null;
32
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${field} is required`);
33
+ const result = value.trim();
34
+ if (result.length > maximum) throw new Error(`${field} exceeds ${maximum} characters`);
35
+ if (SECRET_RE.test(result)) throw new Error(`${field} appears to contain a secret`);
36
+ return result;
37
+ }
38
+
39
+ export function timestamp(value, field = "date", nullable = false) {
40
+ if ((value === null || value === undefined || value === "") && nullable) return null;
41
+ const parsed = value instanceof Date ? value : new Date(value);
42
+ if (!Number.isFinite(parsed.getTime())) throw new Error(`${field} must be a valid date`);
43
+ return parsed.toISOString();
44
+ }
45
+
46
+ export function exactCapabilities(value = []) {
47
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !CAPABILITY_RE.test(item) || item.includes("*"))) {
48
+ throw new Error("capabilities must be exact tool:<name> values without wildcards");
49
+ }
50
+ return [...new Set(value)].sort();
51
+ }
52
+
53
+ export function modeAllows(mode, action) {
54
+ return AUTONOMY_MODES.indexOf(mode) >= AUTONOMY_ACTIONS.indexOf(action);
55
+ }
56
+
57
+ export function emptyAutonomy(root) {
58
+ return { schema: AUTONOMY_SCHEMA, root, revision: 0, projects: [], snapshots: [], observations: [], notices: [], history: [] };
59
+ }
60
+
61
+ function validSource(source) {
62
+ return source && ["local-root", "public-repository"].includes(source.kind)
63
+ && typeof source.value === "string" && source.value.length > 0
64
+ && (source.kind !== "local-root" || isAbsolute(source.value));
65
+ }
66
+
67
+ function validProject(item) {
68
+ return item && ID_RE.test(item.id || "") && ID_RE.test(item.projectId || "")
69
+ && ID_RE.test(item.tenantId || "") && (item.groupId === null || ID_RE.test(item.groupId || ""))
70
+ && AUTONOMY_MODES.includes(item.mode) && validSource(item.source)
71
+ && Array.isArray(item.capabilities) && JSON.stringify(item.capabilities) === JSON.stringify([...new Set(item.capabilities)].sort())
72
+ && item.capabilities.every((value) => CAPABILITY_RE.test(value) && !value.includes("*"))
73
+ && typeof item.active === "boolean" && Number.isInteger(item.revision) && item.revision > 0
74
+ && item.authority === "explicit-local-autonomy-policy" && item.sourceAuthority === "explicit-local-owner-policy"
75
+ && typeof item.goal === "string" && typeof item.updatedAt === "string"
76
+ && (item.publishConfirmedAt === null || typeof item.publishConfirmedAt === "string");
77
+ }
78
+
79
+ function ownDigest(item) {
80
+ const { recordDigest, ...body } = item;
81
+ return recordDigest === digest(body);
82
+ }
83
+
84
+ export function autonomyFindings(state) {
85
+ const findings = [];
86
+ if (!state || state.schema !== AUTONOMY_SCHEMA || typeof state.root !== "string" || !Number.isInteger(state.revision)
87
+ || state.revision < 0 || !["projects", "snapshots", "observations", "notices", "history"].every((key) => Array.isArray(state[key]))) {
88
+ return ["invalid-autonomy-state"];
89
+ }
90
+ if (new Set(state.projects.map((item) => item.id)).size !== state.projects.length) findings.push("duplicate-project-registration");
91
+ for (const item of state.projects) if (!validProject(item) || !ownDigest(item)) findings.push(`invalid-project:${item?.id || "unknown"}`);
92
+ for (const [key, limit] of [["snapshots", 256], ["observations", 1024], ["notices", 512], ["history", 2048]]) {
93
+ if (state[key].length > limit) findings.push(`${key}-limit-exceeded`);
94
+ if (new Set(state[key].map((item) => item.id)).size !== state[key].length) findings.push(`duplicate-${key}-id`);
95
+ for (const item of state[key]) if (!item || !ID_RE.test(item.id || "") || !ownDigest(item)) findings.push(`invalid-${key}:${item?.id || "unknown"}`);
96
+ }
97
+ return findings;
98
+ }
99
+
100
+ export function normalizeAutonomy(value, root) {
101
+ if (value?.root !== root) throw new Error("autonomy state root mismatch");
102
+ const findings = autonomyFindings(value);
103
+ if (findings.length) throw new Error(`autonomy state failed closed: ${findings.join(", ")}`);
104
+ return value;
105
+ }
106
+
107
+ export async function autonomyPaths(root = process.cwd(), providedCatalog = null) {
108
+ const catalog = providedCatalog || await buildCatalog(root);
109
+ const directory = await projectStateDir(catalog.root);
110
+ return { catalog, autonomyPath: join(directory, "autonomy-portfolio.json") };
111
+ }
112
+
113
+ export async function loadAutonomy(root = process.cwd(), providedCatalog = null) {
114
+ const paths = await autonomyPaths(root, providedCatalog);
115
+ try {
116
+ const metadata = await stat(paths.autonomyPath);
117
+ if (metadata.size > MAX_STATE_BYTES) throw new Error("autonomy state exceeds 5 MiB");
118
+ return { state: normalizeAutonomy(JSON.parse(await readFile(paths.autonomyPath, "utf8")), paths.catalog.root), ...paths };
119
+ } catch (error) {
120
+ if (error.code !== "ENOENT") throw error;
121
+ return { state: emptyAutonomy(paths.catalog.root), ...paths };
122
+ }
123
+ }
124
+
125
+ async function saveAutonomy(state, path) {
126
+ const findings = autonomyFindings(state);
127
+ if (findings.length) throw new Error(`autonomy state failed closed: ${findings.join(", ")}`);
128
+ const content = `${JSON.stringify(state, null, 2)}\n`;
129
+ if (Buffer.byteLength(content) > MAX_STATE_BYTES) throw new Error("autonomy state exceeds 5 MiB");
130
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
131
+ try {
132
+ await writeFile(temporary, content, { mode: 0o600 });
133
+ await replaceFileWithRetry(temporary, path);
134
+ } finally {
135
+ await unlink(temporary).catch((error) => { if (error.code !== "ENOENT") throw error; });
136
+ }
137
+ }
138
+
139
+ async function acquireLock(path) {
140
+ const lockPath = `${path}.lock`;
141
+ for (let attempt = 0; attempt < 120; attempt += 1) {
142
+ try {
143
+ const handle = await import("node:fs/promises").then(({ open }) => open(lockPath, "wx", 0o600));
144
+ return { handle, lockPath };
145
+ } catch (error) {
146
+ if (!isFileLockContention(error)) throw error;
147
+ try {
148
+ const metadata = await stat(lockPath);
149
+ if (Date.now() - metadata.mtimeMs > 90000) await unlink(lockPath);
150
+ } catch (lockError) {
151
+ if (lockError.code !== "ENOENT") throw lockError;
152
+ }
153
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 25));
154
+ }
155
+ }
156
+ throw new Error("autonomy state is busy; retry later");
157
+ }
158
+
159
+ export async function mutateAutonomy(root, operation) {
160
+ const paths = await autonomyPaths(root);
161
+ const { handle, lockPath } = await acquireLock(paths.autonomyPath);
162
+ try {
163
+ const { state } = await loadAutonomy(paths.catalog.root, paths.catalog);
164
+ const result = await operation(state, paths);
165
+ await saveAutonomy(state, paths.autonomyPath);
166
+ return result;
167
+ } finally {
168
+ await handle.close();
169
+ await unlink(lockPath).catch((error) => { if (error.code !== "ENOENT") throw error; });
170
+ }
171
+ }
172
+
173
+ export function signedRecord(body) {
174
+ return { ...body, recordDigest: digest(body) };
175
+ }
176
+
177
+ export function appendHistory(state, body) {
178
+ const entry = signedRecord({ id: `history:${randomUUID()}`, ...body, authority: "context-only" });
179
+ state.history.push(entry);
180
+ if (state.history.length > 2048) state.history.splice(0, state.history.length - 2048);
181
+ return entry;
182
+ }
183
+
184
+ export async function canonicalLocalRoot(value) {
185
+ const input = resolve(safeText(value, "localRoot", 4096));
186
+ const metadata = await lstat(input);
187
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error("local project root must be a regular directory, not a symlink");
188
+ const canonical = await realpath(input);
189
+ const same = process.platform === "win32"
190
+ ? input.toLowerCase() === canonical.toLowerCase()
191
+ : input === canonical;
192
+ if (!same) throw new Error("local project root cannot traverse a symlink");
193
+ return canonical;
194
+ }
195
+
196
+ export function publicRepository(value) {
197
+ const url = new URL(safeText(value, "publicRepository", 2048));
198
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
199
+ throw new Error("public repository must be a credential-free HTTPS URL without query or fragment");
200
+ }
201
+ return url.toString().replace(/\/$/, "");
202
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ AUTONOMY_ACTIONS, AUTONOMY_CONFIRMATION, AUTONOMY_MODES, AUTONOMY_SCHEMA,
3
+ EVIDENCE_CLASSES, PUBLISH_CONFIRMATION, autonomyFindings, loadAutonomy, modeAllows
4
+ } from "./autonomy-store.js";
5
+ export {
6
+ autonomyStatus, configureAutonomyProject, evaluateAutonomyAction, revokeAutonomyProject
7
+ } from "./autonomy-policy.js";
8
+ export { projectPortfolioContext, recordProjectObservation, scanProjectPortfolio } from "./project-portfolio.js";
@@ -8,6 +8,8 @@ import { sharedContext } from "./sharing.js";
8
8
  import { loadPersonaRuntime, personaRuntimeFindings } from "./persona-runtime.js";
9
9
  import { gatewayRuntimeFindings, loadGatewayRuntime } from "./gateway-runtime.js";
10
10
  import { voiceCue } from "./voice-runtime.js";
11
+ import { worldContext } from "./world-model.js";
12
+ import { projectPortfolioContext } from "./project-portfolio.js";
11
13
 
12
14
  const MIN_BYTES = 4096;
13
15
  const MAX_BYTES = 262144;
@@ -133,7 +135,7 @@ export async function sessionBriefing({
133
135
  ? { root: userStateRoot, entityId, includePrivate, groupId, catalog: userCatalog }
134
136
  : { root: catalog.root, entityId, includePrivate, groupId, catalog })
135
137
  : null;
136
- const [learned, attention, tasks, shared, userLearned, personas, gateway] = await settleReads([
138
+ const [learned, attention, tasks, shared, userLearned, personas, gateway, world, portfolio] = await settleReads([
137
139
  learningContext({ root: catalog.root, includePrivate, groupId, scope: learningScope, maxItems: 50, catalog, now }),
138
140
  attentionContext({
139
141
  root: catalog.root, includePrivate, entityId, groupId, projectId, currentTaskId,
@@ -146,7 +148,11 @@ export async function sessionBriefing({
146
148
  catalog: userCatalog })
147
149
  : Promise.resolve({ items: [] }),
148
150
  loadPersonaRuntime(catalog.root, catalog),
149
- loadGatewayRuntime(catalog.root, catalog)
151
+ loadGatewayRuntime(catalog.root, catalog),
152
+ worldContext({ root: catalog.root, projectId, groupId, includePrivate,
153
+ continuationTaskId: currentTaskId, maxItems: 50, now }),
154
+ tenantId ? projectPortfolioContext({ root: catalog.root, tenantId, groupId, markPresented: false, now })
155
+ : Promise.resolve({ projects: [], observations: [], notice: null, rateLimited: false })
150
156
  ]);
151
157
  const personaFindings = personaRuntimeFindings(personas.policy, personas.runtime);
152
158
  if (personaFindings.length) throw new Error(`persona runtime failed closed: ${personaFindings.join(", ")}`);
@@ -199,13 +205,26 @@ export async function sessionBriefing({
199
205
  },
200
206
  learning: [],
201
207
  shared: [],
208
+ world: {
209
+ schema: world.schema,
210
+ facts: [], conflicts: [], proposals: [], stale: [],
211
+ ...(world.knowledge.current.length ? { knowledge: {
212
+ schema: world.knowledge.schema, current: [], counts: world.knowledge.counts,
213
+ continuation: { ...world.knowledge.continuation, tasks: [], terminal: [] },
214
+ taskContext: { ...world.knowledge.taskContext, items: [] },
215
+ omitted: { ...world.knowledge.omitted }, authority: "context-only"
216
+ } } : {}),
217
+ uncertainty: world.uncertainty,
218
+ authority: "context-only"
219
+ },
202
220
  attention: { suppressed: attention.suppressed, items: [] },
221
+ portfolio: { projects: [], observations: [], notice: portfolio.notice, rateLimited: portfolio.rateLimited, authority: "context-only" },
203
222
  budget: {
204
223
  maxBytes: limit,
205
224
  usedBytes: 0,
206
225
  remainingBytes: 0,
207
226
  measurement: "compact-json-utf8",
208
- omitted: { sources: 0, tasks: 0, relationships: 0, voice: 0, learning: 0, shared: 0, attention: 0 }
227
+ omitted: { sources: 0, tasks: 0, relationships: 0, voice: 0, learning: 0, shared: 0, world: 0, attention: 0, portfolio: 0 }
209
228
  },
210
229
  authority: "context-only",
211
230
  note: "This packet is descriptive context only. It grants no delegation, host, tool, file, network, production, spending, or policy rights. Native host rules and explicit local policy remain authoritative."
@@ -213,8 +232,42 @@ export async function sessionBriefing({
213
232
  recalculateBudget(result);
214
233
  if (result.budget.usedBytes > limit) throw new Error(`maxBytes is too small for the briefing envelope; use at least ${MIN_BYTES}`);
215
234
 
235
+ const continuationIds = new Set();
236
+ for (const item of world.knowledge.continuation.tasks) {
237
+ if (tryAdd(result, result.world.knowledge.continuation.tasks, item)) continuationIds.add(item.assertionId);
238
+ else countOmitted(result, "world");
239
+ }
240
+ for (const item of world.knowledge.continuation.terminal) {
241
+ if (tryAdd(result, result.world.knowledge.continuation.terminal, item)) continuationIds.add(item.assertionId);
242
+ else countOmitted(result, "world");
243
+ }
244
+ const contextualIds = new Set(continuationIds);
245
+ for (const item of world.knowledge.taskContext.items) {
246
+ if (tryAdd(result, result.world.knowledge.taskContext.items, item)) contextualIds.add(item.id);
247
+ else {
248
+ result.world.knowledge.taskContext.omitted += 1;
249
+ countOmitted(result, "world");
250
+ }
251
+ }
252
+ for (const section of ["facts", "conflicts", "proposals", "stale"]) {
253
+ const items = section === "facts"
254
+ ? world[section].filter((item) => !item.assertionIds?.some((id) => contextualIds.has(id)))
255
+ : world[section];
256
+ for (const item of items) {
257
+ if (!tryAdd(result, result.world[section], item)) countOmitted(result, "world");
258
+ }
259
+ }
260
+ for (const item of world.knowledge.current.filter((entry) => !contextualIds.has(entry.id))) {
261
+ if (!tryAdd(result, result.world.knowledge.current, item)) {
262
+ result.world.knowledge.omitted.current += 1;
263
+ countOmitted(result, "world");
264
+ }
265
+ }
266
+
216
267
  const scopedTasks = tasks.items.filter((task) => taskMatchesScope(task, entityId));
217
- if (currentTaskId && !scopedTasks.some((task) => task.id === currentTaskId)) {
268
+ const continuedTask = [...world.knowledge.continuation.tasks, ...world.knowledge.continuation.terminal]
269
+ .find((task) => task.taskId === currentTaskId) || null;
270
+ if (currentTaskId && !scopedTasks.some((task) => task.id === currentTaskId) && !continuedTask) {
218
271
  throw new Error(`current task is not visible in this briefing scope: ${currentTaskId}`);
219
272
  }
220
273
  const orderedTasks = [...scopedTasks].sort((left, right) => {
@@ -229,6 +282,10 @@ export async function sessionBriefing({
229
282
  if (focusedTask && !trySet(result, result.voiceBrief, "currentTask", {
230
283
  id: focusedTask.id, title: focusedTask.title, status: focusedTask.status
231
284
  })) countOmitted(result, "voice");
285
+ if (!focusedTask && continuedTask && !trySet(result, result.voiceBrief, "currentTask", {
286
+ id: continuedTask.taskId, title: continuedTask.objective, status: continuedTask.status,
287
+ nextStep: continuedTask.nextStep
288
+ })) countOmitted(result, "voice");
232
289
  const focusedGoal = entityId ? gateway.policy.goals.find((goal) => goal.agentId === entityId && goal.status === "active"
233
290
  && (projectId === null || goal.projectId === projectId) && goal.groupId === groupId) : null;
234
291
  if (focusedGoal && !trySet(result, result.voiceBrief, "currentGoal", {
@@ -310,6 +367,12 @@ export async function sessionBriefing({
310
367
  for (const item of attentionItems) {
311
368
  if (!tryAdd(result, result.attention.items, item)) countOmitted(result, "attention");
312
369
  }
370
+ for (const item of portfolio.projects) {
371
+ if (!tryAdd(result, result.portfolio.projects, item)) countOmitted(result, "portfolio");
372
+ }
373
+ for (const item of portfolio.observations) {
374
+ if (!tryAdd(result, result.portfolio.observations, item)) countOmitted(result, "portfolio");
375
+ }
313
376
 
314
377
  recalculateBudget(result);
315
378
  if (result.budget.usedBytes > limit) throw new Error("briefing budget invariant failed");
@@ -0,0 +1,51 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, open, realpath } from "node:fs/promises";
4
+ import { relative, resolve } from "node:path";
5
+ import { canonicalPath, isInside } from "./paths.js";
6
+
7
+ function sameFile(a, b) {
8
+ return a.dev === b.dev && a.ino === b.ino && a.size === b.size
9
+ && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
10
+ }
11
+
12
+ export async function readCatalogDocument({ root, catalog, path, offset = 0, length = 65536 }) {
13
+ if (catalog.root !== await canonicalPath(root)) throw new Error("catalog belongs to a different project root");
14
+ const absolute = resolve(catalog.root, path);
15
+ const document = catalog.documents.find(item => item.relativePath === path
16
+ || (isInside(catalog.root, absolute) && item.path === absolute));
17
+ if (!document) throw new Error("path is not an indexed Markdown document in the bounded host sources");
18
+ const before = await lstat(document.path);
19
+ if (!before.isFile() || before.isSymbolicLink() || before.size !== document.bytes
20
+ || before.size > 4 * 1024 * 1024 || await realpath(document.path) !== document.path) {
21
+ throw new Error("indexed contract source changed before reading");
22
+ }
23
+ const handle = await open(document.path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
24
+ let buffer;
25
+ try {
26
+ if (!sameFile(before, await handle.stat())) throw new Error("indexed contract source was replaced");
27
+ // A concurrent append must not turn this bounded read into an unbounded one.
28
+ buffer = Buffer.alloc(before.size + 1);
29
+ let bytesRead = 0;
30
+ while (bytesRead < buffer.length) {
31
+ const result = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
32
+ if (!result.bytesRead) break;
33
+ bytesRead += result.bytesRead;
34
+ }
35
+ buffer = buffer.subarray(0, bytesRead);
36
+ if (!sameFile(before, await handle.stat()) || !sameFile(before, await lstat(document.path))
37
+ || await realpath(document.path) !== document.path
38
+ || createHash("sha256").update(buffer).digest("hex") !== document.sha256) {
39
+ throw new Error("indexed contract source changed during reading");
40
+ }
41
+ } finally {
42
+ await handle.close();
43
+ }
44
+ const start = Math.min(buffer.length, Math.max(0, Number(offset) || 0));
45
+ const end = Math.min(buffer.length, start + Math.max(0, Number(length) || 0));
46
+ return { path: isInside(catalog.root, document.path)
47
+ ? relative(catalog.root, document.path).replaceAll("\\", "/") : document.relativePath,
48
+ sha256: document.sha256, offset: start, bytes: end - start, totalBytes: buffer.length,
49
+ eof: end >= buffer.length, encoding: "utf8", content: buffer.subarray(start, end).toString("utf8"),
50
+ contentBase64: buffer.subarray(start, end).toString("base64") };
51
+ }
@@ -67,7 +67,7 @@ function catalogConflicts(documents) {
67
67
 
68
68
  export async function buildCatalog(inputRoot = process.cwd(), { env = process.env } = {}) {
69
69
  const root = await canonicalPath(inputRoot);
70
- const documents = await discoverDocuments(root, { excludeRoots: [stateRoot(env)] });
70
+ const documents = await discoverDocuments(root, { excludeRoots: [stateRoot(env)], env });
71
71
  const conflicts = catalogConflicts(documents);
72
72
  const byLayer = Object.fromEntries(
73
73
  [...new Set(documents.map((document) => document.layer))]
@@ -0,0 +1,231 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, open, readFile, realpath, rm } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { withOwnedFileLock } from "./owned-file-lock.js";
7
+ import { replaceFileWithRetry } from "./filesystem-retry.js";
8
+ import { defaultCodexSkillsRoot, installCodexSkill } from "./codex-skill-installation.js";
9
+
10
+ const REGISTRATION_SCHEMA = "agentspine.codex-reader-registration/v1";
11
+ const AUTHORITY = "host-registration-only";
12
+ const START = "# BEGIN AGENTSPINE MANAGED MCP";
13
+ const END = "# END AGENTSPINE MANAGED MCP";
14
+ const MAX_CONFIG_BYTES = 1024 * 1024;
15
+ const EXPECTED_TOOLS = [
16
+ "session_briefing", "delivery_knowledge_query",
17
+ "record_delivery_premortem", "read_document"
18
+ ];
19
+
20
+ function sha256(value) {
21
+ return createHash("sha256").update(value).digest("hex");
22
+ }
23
+
24
+ function canonical(value) {
25
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
26
+ if (value && typeof value === "object") {
27
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
28
+ }
29
+ return JSON.stringify(value);
30
+ }
31
+
32
+ function seal(value) {
33
+ return { ...value, digest: sha256(canonical(value)) };
34
+ }
35
+
36
+ async function regularFile(path, label) {
37
+ const metadata = await lstat(path);
38
+ if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(`${label} must be a regular non-symlink file`);
39
+ return path;
40
+ }
41
+
42
+ async function exactDirectory(path, label, { create = false } = {}) {
43
+ const target = resolve(path);
44
+ if (!isAbsolute(path)) throw new Error(`${label} must be absolute`);
45
+ if (create) await mkdir(target, { recursive: true, mode: 0o700 });
46
+ const metadata = await lstat(target);
47
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error(`${label} must be a regular non-symlink directory`);
48
+ return realpath(target);
49
+ }
50
+
51
+ async function optionalText(path) {
52
+ try {
53
+ await regularFile(path, "Codex config.toml");
54
+ const value = await readFile(path, "utf8");
55
+ if (Buffer.byteLength(value) > MAX_CONFIG_BYTES) throw new Error("Codex config.toml exceeds 1 MiB");
56
+ return value;
57
+ } catch (error) {
58
+ if (error.code === "ENOENT") return "";
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ async function regularOrMissing(path, label) {
64
+ try {
65
+ await regularFile(path, label);
66
+ } catch (error) {
67
+ if (error.code !== "ENOENT") throw error;
68
+ }
69
+ }
70
+
71
+ function managedRange(text) {
72
+ const starts = [...text.matchAll(new RegExp(`^${START}$`, "gm"))];
73
+ const ends = [...text.matchAll(new RegExp(`^${END}$`, "gm"))];
74
+ if (!starts.length && !ends.length) return null;
75
+ if (starts.length !== 1 || ends.length !== 1 || ends[0].index < starts[0].index) {
76
+ throw new Error("Codex config contains a malformed AgentSpine managed block");
77
+ }
78
+ let end = ends[0].index + END.length;
79
+ if (text.slice(end, end + 2) === "\r\n") end += 2;
80
+ else if (text[end] === "\n") end += 1;
81
+ const start = starts[0].index > 0 ? starts[0].index - 1 : starts[0].index;
82
+ return { start, end };
83
+ }
84
+
85
+ function hasRegistration(text) {
86
+ return /^\s*\[\s*mcp_servers\s*\.\s*(?:agent-spine|["']agent-spine["'])\s*\]\s*(?:#.*)?$/m.test(text);
87
+ }
88
+
89
+ function tomlString(value) {
90
+ return JSON.stringify(value);
91
+ }
92
+
93
+ function managedBlock(nodePath, launcherPath) {
94
+ return [
95
+ START,
96
+ "[mcp_servers.agent-spine]",
97
+ `command = ${tomlString(nodePath)}`,
98
+ `args = [${tomlString(launcherPath)}]`,
99
+ "enabled = true",
100
+ END,
101
+ ""
102
+ ].join("\n");
103
+ }
104
+
105
+ function updateConfig(text, block) {
106
+ const range = managedRange(text);
107
+ if (range) {
108
+ const prefix = text.slice(0, range.start);
109
+ const suffix = text.slice(range.end);
110
+ const outside = prefix + suffix;
111
+ if (hasRegistration(outside)) throw new Error("Codex config has an unmanaged AgentSpine MCP registration");
112
+ return { text: prefix + (prefix.length ? "\n" : "") + block + suffix,
113
+ outsideBefore: outside, outsideAfter: outside, status: "updated" };
114
+ }
115
+ if (hasRegistration(text)) throw new Error("Codex config has an unmanaged AgentSpine MCP registration");
116
+ const separator = text.length ? "\n" : "";
117
+ return { text: text + separator + block,
118
+ outsideBefore: text, outsideAfter: text, status: "installed" };
119
+ }
120
+
121
+ async function atomicWrite(path, content, assertOwned) {
122
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
123
+ let handle;
124
+ try {
125
+ handle = await open(temporary, "wx", 0o600);
126
+ await handle.writeFile(content);
127
+ await handle.sync();
128
+ await handle.close();
129
+ handle = null;
130
+ await replaceFileWithRetry(temporary, path, { beforeAttempt: assertOwned });
131
+ } finally {
132
+ await handle?.close();
133
+ await rm(temporary, { force: true }).catch(() => {});
134
+ }
135
+ }
136
+
137
+ async function packageRegistration(packageRoot, nodePath) {
138
+ const root = await exactDirectory(packageRoot, "AgentSpine package root");
139
+ const entrypoint = await regularFile(join(root, "src", "mcp.js"), "AgentSpine MCP entrypoint");
140
+ const versionSource = await regularFile(join(root, "src", "version.js"), "AgentSpine version source");
141
+ const packageFile = await regularFile(join(root, "package.json"), "AgentSpine package manifest");
142
+ const packageJson = await readFile(packageFile, "utf8");
143
+ const pkg = JSON.parse(packageJson);
144
+ const source = await readFile(versionSource, "utf8");
145
+ const sourceVersion = source.match(/^export const VERSION = "([^"]+)";\s*$/)?.[1];
146
+ if (!pkg.version || sourceVersion !== pkg.version) throw new Error("AgentSpine package and runtime versions differ");
147
+ const canonicalNode = await realpath(await regularFile(nodePath, "Node.js runtime"));
148
+ return seal({
149
+ schema: REGISTRATION_SCHEMA,
150
+ expectedName: "agent-spine",
151
+ expectedVersion: pkg.version,
152
+ expectedTools: EXPECTED_TOOLS,
153
+ packageRoot: root,
154
+ nodePath: canonicalNode,
155
+ entrypoint,
156
+ entrypointDigest: sha256(await readFile(entrypoint)),
157
+ packageDigest: sha256(packageJson),
158
+ versionSourceDigest: sha256(source),
159
+ authority: AUTHORITY
160
+ });
161
+ }
162
+
163
+ export function defaultCodexHome(env = process.env) {
164
+ return resolve(env.CODEX_HOME || join(homedir(), ".codex"));
165
+ }
166
+
167
+ export async function installCodexMcp({
168
+ codexHome = defaultCodexHome(),
169
+ skillsRoot = defaultCodexSkillsRoot(),
170
+ packageRoot = fileURLToPath(new URL("../..", import.meta.url)),
171
+ nodePath = process.execPath,
172
+ faultAfter = null
173
+ } = {}) {
174
+ const home = await exactDirectory(codexHome, "Codex home", { create: true });
175
+ const managed = join(home, "agentspine");
176
+ await exactDirectory(managed, "Codex AgentSpine directory", { create: true });
177
+ const configPath = join(home, "config.toml");
178
+ const launcherPath = join(managed, "reader-launcher.mjs");
179
+ const registrationPath = join(managed, "registration.json");
180
+ const lockPath = join(managed, "install.lock");
181
+ try {
182
+ const lock = await lstat(lockPath);
183
+ if (lock.isSymbolicLink()) throw new Error("Codex AgentSpine install lock must not be a symlink");
184
+ } catch (error) {
185
+ if (error.code !== "ENOENT") throw error;
186
+ }
187
+ return withOwnedFileLock(lockPath, async ({ assertOwned }) => {
188
+ await Promise.all([
189
+ regularOrMissing(launcherPath, "Codex AgentSpine launcher"),
190
+ regularOrMissing(registrationPath, "Codex AgentSpine registration")
191
+ ]);
192
+ const [before, registration, launcher] = await Promise.all([
193
+ optionalText(configPath), packageRegistration(packageRoot, nodePath),
194
+ readFile(fileURLToPath(new URL("../codex-reader-launcher.js", import.meta.url)))
195
+ ]);
196
+ const update = updateConfig(before, managedBlock(registration.nodePath, launcherPath));
197
+ if (update.outsideBefore !== update.outsideAfter) throw new Error("unmanaged Codex configuration bytes changed");
198
+ const skill = await installCodexSkill({
199
+ skillsRoot, packageRoot,
200
+ faultAfter: faultAfter === "skill-marker" ? "marker" : faultAfter === "skill" ? "skill" : null
201
+ });
202
+ if (skill.version !== registration.expectedVersion || skill.packageRoot !== registration.packageRoot) {
203
+ throw new Error("Codex skill and MCP reader package identity differ");
204
+ }
205
+ await atomicWrite(launcherPath, launcher, assertOwned);
206
+ if (faultAfter === "launcher") throw new Error("synthetic crash after launcher publication");
207
+ await atomicWrite(registrationPath, `${JSON.stringify(registration, null, 2)}\n`, assertOwned);
208
+ if (faultAfter === "registration") throw new Error("synthetic crash after registration publication");
209
+ await atomicWrite(configPath, update.text, assertOwned);
210
+ return {
211
+ schema: "agentspine.codex-installation/v1",
212
+ status: update.status,
213
+ version: registration.expectedVersion,
214
+ configPath,
215
+ launcherPath,
216
+ registrationPath,
217
+ registrationDigest: registration.digest,
218
+ unmanagedBeforeDigest: sha256(update.outsideBefore),
219
+ unmanagedAfterDigest: sha256(update.outsideAfter),
220
+ configDigest: sha256(update.text),
221
+ skill,
222
+ restartRequired: true,
223
+ authority: AUTHORITY
224
+ };
225
+ });
226
+ }
227
+
228
+ export function stripManagedCodexBlock(text) {
229
+ const range = managedRange(text);
230
+ return range ? text.slice(0, range.start) + text.slice(range.end) : text;
231
+ }