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,176 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, readFile, readdir, stat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import {
5
+ EVIDENCE_CLASSES, appendHistory, digest, loadAutonomy, modeAllows, mutateAutonomy,
6
+ safeText, signedRecord, stableId, timestamp
7
+ } from "./autonomy-store.js";
8
+
9
+ const MAX_ENTRIES = 256;
10
+ const MAX_FILE_BYTES = 64 * 1024;
11
+ const KINDS = new Set(["ci", "error", "idea", "goal", "state"]);
12
+ const STATUSES = new Set(["open", "passed", "failed", "resolved", "unknown"]);
13
+
14
+ async function smallRegular(path) {
15
+ try {
16
+ const before = await lstat(path, { bigint: true });
17
+ if (!before.isFile() || before.isSymbolicLink() || before.size > BigInt(MAX_FILE_BYTES)) return null;
18
+ const content = await readFile(path);
19
+ const after = await stat(path, { bigint: true });
20
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs) {
21
+ throw new Error("project source changed during bounded read");
22
+ }
23
+ return { content, bytes: Number(after.size), sha256: digest([...content]) };
24
+ } catch (error) {
25
+ if (error.code === "ENOENT" || error.code === "EACCES" || error.code === "EPERM") return null;
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ function activeProject(state, registrationId, tenantId, groupId) {
31
+ return state.projects.find((item) => item.id === registrationId && item.active
32
+ && item.tenantId === tenantId && item.groupId === groupId);
33
+ }
34
+
35
+ function shouldNotify(observation) {
36
+ return (observation.kind === "error" && ["open", "failed"].includes(observation.status))
37
+ || (observation.kind === "ci" && observation.status === "failed")
38
+ || (observation.kind === "idea" && observation.status === "open");
39
+ }
40
+
41
+ function addNotice(state, project, observation, at) {
42
+ if (!modeAllows(project.mode, "advise") || !shouldNotify(observation)) return null;
43
+ const identity = digest({ projectRegistrationId: project.id, observationId: observation.id });
44
+ const existing = state.notices.find((item) => item.identityDigest === identity);
45
+ if (existing) return existing;
46
+ const notice = signedRecord({
47
+ id: `notice:${identity.slice(0, 32)}`, identityDigest: identity, projectRegistrationId: project.id,
48
+ observationId: observation.id, tenantId: project.tenantId, groupId: project.groupId,
49
+ kind: observation.kind, summary: observation.summary, createdAt: at, presentedAt: null,
50
+ authority: "context-only"
51
+ });
52
+ state.notices.push(notice);
53
+ return notice;
54
+ }
55
+
56
+ export async function recordProjectObservation({
57
+ root = process.cwd(), registrationId, tenantId, groupId = null, kind, status = "open",
58
+ evidenceClass, summary, sourceDigest, observedAt = new Date(), expiresAt = null
59
+ }) {
60
+ stableId(registrationId, "registrationId");
61
+ stableId(tenantId, "tenantId");
62
+ stableId(groupId, "groupId", true);
63
+ if (!KINDS.has(kind)) throw new Error("unsupported observation kind");
64
+ if (!STATUSES.has(status)) throw new Error("unsupported observation status");
65
+ if (!EVIDENCE_CLASSES.includes(evidenceClass)) throw new Error("unsupported evidence class");
66
+ if (["ci", "error"].includes(kind) && evidenceClass !== "objective") throw new Error("CI and error findings require objective evidence");
67
+ if (!/^[a-f0-9]{64}$/.test(sourceDigest || "")) throw new Error("sourceDigest must be SHA-256");
68
+ const at = timestamp(observedAt, "observedAt");
69
+ const expiry = timestamp(expiresAt, "expiresAt", true);
70
+ if (expiry && new Date(expiry) <= new Date(at)) throw new Error("expiresAt must follow observedAt");
71
+ const cleanSummary = safeText(summary, "summary", 500);
72
+ const identity = digest({ registrationId, kind, status, evidenceClass, summary: cleanSummary, sourceDigest });
73
+ return mutateAutonomy(root, (state, paths) => {
74
+ const project = activeProject(state, registrationId, tenantId, groupId);
75
+ if (!project) throw new Error("project registration is unavailable in this exact scope");
76
+ const previous = state.observations.find((item) => item.identityDigest === identity);
77
+ if (previous) return { observation: previous, notice: state.notices.find((item) => item.observationId === previous.id) || null,
78
+ duplicate: true, autonomyPath: paths.autonomyPath };
79
+ const observation = signedRecord({
80
+ id: `observation:${identity.slice(0, 32)}`, identityDigest: identity, projectRegistrationId: project.id,
81
+ tenantId, groupId, kind, status, evidenceClass, summary: cleanSummary, sourceDigest,
82
+ observedAt: at, expiresAt: expiry, authority: "context-only"
83
+ });
84
+ state.observations.push(observation);
85
+ const notice = addNotice(state, project, observation, at);
86
+ appendHistory(state, { kind: "observation-recorded", projectRegistrationId: project.id, observationId: observation.id, at });
87
+ return { observation, notice, duplicate: false, autonomyPath: paths.autonomyPath };
88
+ });
89
+ }
90
+
91
+ export async function scanProjectPortfolio({ root = process.cwd(), tenantId, groupId = null, now = new Date() } = {}) {
92
+ stableId(tenantId, "tenantId");
93
+ stableId(groupId, "groupId", true);
94
+ const { state } = await loadAutonomy(root);
95
+ const projects = state.projects.filter((item) => item.active && item.tenantId === tenantId && item.groupId === groupId);
96
+ const results = [];
97
+ for (const project of projects) {
98
+ if (project.source.kind !== "local-root") {
99
+ results.push({ registrationId: project.id, source: project.source, status: "registered-unfetched", authority: "context-only" });
100
+ continue;
101
+ }
102
+ const entries = (await readdir(project.source.value, { withFileTypes: true }))
103
+ .slice(0, MAX_ENTRIES).map((item) => ({ name: item.name.slice(0, 200), kind: item.isDirectory() ? "directory" : item.isFile() ? "file" : "other" }));
104
+ const [pkg, agents, head] = await Promise.all([
105
+ smallRegular(join(project.source.value, "package.json")), smallRegular(join(project.source.value, "AGENTS.md")),
106
+ smallRegular(join(project.source.value, ".git", "HEAD"))
107
+ ]);
108
+ let packageInfo = null;
109
+ let packageError = null;
110
+ if (pkg) {
111
+ try {
112
+ const parsed = JSON.parse(pkg.content.toString("utf8"));
113
+ packageInfo = { name: typeof parsed.name === "string" ? parsed.name.slice(0, 200) : null,
114
+ version: typeof parsed.version === "string" ? parsed.version.slice(0, 80) : null, sha256: pkg.sha256 };
115
+ } catch { packageError = "package.json is not valid JSON"; }
116
+ }
117
+ const snapshotBody = {
118
+ id: `snapshot:${randomUUID()}`, projectRegistrationId: project.id, tenantId, groupId,
119
+ observedAt: timestamp(now, "now"), entries, truncated: entries.length === MAX_ENTRIES,
120
+ package: packageInfo, instructions: agents ? { bytes: agents.bytes, sha256: agents.sha256 } : null,
121
+ gitHead: head ? { bytes: head.bytes, sha256: head.sha256 } : null,
122
+ sourceDigest: digest({ entries, packageInfo, agents: agents?.sha256 || null, head: head?.sha256 || null }),
123
+ authority: "context-only"
124
+ };
125
+ const saved = await mutateAutonomy(root, (current, paths) => {
126
+ const currentProject = activeProject(current, project.id, tenantId, groupId);
127
+ if (!currentProject || currentProject.source.value !== project.source.value) throw new Error("project registration changed during scan");
128
+ const prior = current.snapshots.find((item) => item.projectRegistrationId === project.id && item.sourceDigest === snapshotBody.sourceDigest);
129
+ if (prior) return { snapshot: prior, duplicate: true, autonomyPath: paths.autonomyPath };
130
+ const snapshot = signedRecord(snapshotBody);
131
+ current.snapshots.push(snapshot);
132
+ if (current.snapshots.length > 256) current.snapshots.splice(0, current.snapshots.length - 256);
133
+ appendHistory(current, { kind: "project-scanned", projectRegistrationId: project.id, snapshotId: snapshot.id, at: snapshot.observedAt });
134
+ return { snapshot, duplicate: false, autonomyPath: paths.autonomyPath };
135
+ });
136
+ if (packageError) await recordProjectObservation({ root, registrationId: project.id, tenantId, groupId,
137
+ kind: "error", status: "failed", evidenceClass: "objective", summary: packageError,
138
+ sourceDigest: pkg.sha256, observedAt: now });
139
+ results.push({ registrationId: project.id, source: project.source, status: "scanned", ...saved });
140
+ }
141
+ return { schema: "agentspine.project-portfolio-scan/v1", projects: results, boundedEntries: MAX_ENTRIES, authority: "context-only" };
142
+ }
143
+
144
+ function contextFromState(state, tenantId, groupId, now) {
145
+ const projects = state.projects.filter((item) => item.active && item.tenantId === tenantId && item.groupId === groupId);
146
+ const projectIds = new Set(projects.map((item) => item.id));
147
+ const observations = state.observations.filter((item) => projectIds.has(item.projectRegistrationId)
148
+ && (!item.expiresAt || new Date(item.expiresAt) > now)).slice(-50);
149
+ const observationIds = new Set(observations.map((item) => item.id));
150
+ const lastPresented = state.notices.filter((item) => item.tenantId === tenantId && item.groupId === groupId && item.presentedAt)
151
+ .sort((a, b) => b.presentedAt.localeCompare(a.presentedAt))[0];
152
+ const rateLimited = lastPresented && now - new Date(lastPresented.presentedAt) < 60 * 60 * 1000;
153
+ const notice = rateLimited ? null : state.notices.find((item) => item.tenantId === tenantId && item.groupId === groupId
154
+ && item.presentedAt === null && projectIds.has(item.projectRegistrationId) && observationIds.has(item.observationId)) || null;
155
+ return { projects, observations, notice, rateLimited: Boolean(rateLimited) };
156
+ }
157
+
158
+ export async function projectPortfolioContext({ root = process.cwd(), tenantId, groupId = null, markPresented = false, now = new Date() } = {}) {
159
+ stableId(tenantId, "tenantId");
160
+ stableId(groupId, "groupId", true);
161
+ const at = timestamp(now, "now");
162
+ if (!markPresented) {
163
+ const { state } = await loadAutonomy(root);
164
+ return { schema: "agentspine.project-portfolio-context/v1", ...contextFromState(state, tenantId, groupId, new Date(at)), authority: "context-only" };
165
+ }
166
+ return mutateAutonomy(root, (state) => {
167
+ const context = contextFromState(state, tenantId, groupId, new Date(at));
168
+ if (context.notice) {
169
+ const updated = signedRecord({ ...Object.fromEntries(Object.entries(context.notice).filter(([key]) => key !== "recordDigest")), presentedAt: at });
170
+ state.notices = state.notices.map((item) => item.id === updated.id ? updated : item);
171
+ appendHistory(state, { kind: "notice-presented", projectRegistrationId: updated.projectRegistrationId, noticeId: updated.id, at });
172
+ context.notice = updated;
173
+ }
174
+ return { schema: "agentspine.project-portfolio-context/v1", ...context, authority: "context-only" };
175
+ });
176
+ }
@@ -0,0 +1,382 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { open, readFile, stat, unlink, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { buildCatalog } from "./catalog.js";
6
+ import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
7
+ import { loadCoordination } from "./coordination.js";
8
+ import { loadGraph } from "./graph.js";
9
+ import { comparablePath, projectStateDir } from "./paths.js";
10
+
11
+ export const POLICY_SCHEMA = "agentspine.execution-policy/v1";
12
+ export const JOB_SCHEMA = "agentspine.selfstarter/v1";
13
+ export const ACTIONS = new Set(["start", "resume", "effect"]);
14
+ export const JOB_STATUSES = new Set(["waiting", "running", "blocked", "completed", "cancelled", "exhausted"]);
15
+ export const HOSTS = new Set(["claude", "codex"]);
16
+ export const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
17
+ export const CAPABILITY_RE = /^tool:[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
18
+ export const DIGEST_RE = /^[a-f0-9]{64}$/;
19
+ const MAX_STATE_BYTES = 5 * 1024 * 1024;
20
+ const CONFIRMATION = "local-owner-confirmed";
21
+ 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;
22
+
23
+ export function workspaceScanError(error) { error.agentSpineScan = true; return error; }
24
+
25
+ export function configuredHostProfileRoots(env = process.env) {
26
+ return [env.CODEX_HOME, env.BLUN_HOME, env.CLAUDE_CONFIG_DIR, join(homedir(), ".codex"), join(homedir(), ".claude")]
27
+ .filter((value) => typeof value === "string" && value)
28
+ .map((value) => comparablePath(value));
29
+ }
30
+
31
+ export function isConfiguredHostProfileRoot(root, env = process.env) {
32
+ const target = comparablePath(root);
33
+ return configuredHostProfileRoots(env).some((profile) => process.platform === "win32"
34
+ ? profile.toLowerCase() === target.toLowerCase() : profile === target);
35
+ }
36
+
37
+ export function emptyPolicy(root) {
38
+ return { schema: POLICY_SCHEMA, root, revision: 0, grants: [], history: [] };
39
+ }
40
+
41
+ export function emptyJobs(root) {
42
+ return { schema: JOB_SCHEMA, root, jobs: [], history: [], receipts: [] };
43
+ }
44
+
45
+ export function timestamp(value, field = "date", nullable = false) {
46
+ if ((value === null || value === undefined || value === "") && nullable) return null;
47
+ const parsed = value instanceof Date ? value : new Date(value);
48
+ if (!Number.isFinite(parsed.getTime())) throw new Error(`${field} must be a valid date`);
49
+ return parsed.toISOString();
50
+ }
51
+
52
+ export function stableId(value, field, nullable = false) {
53
+ if ((value === null || value === undefined || value === "") && nullable) return null;
54
+ if (typeof value !== "string" || !ID_RE.test(value)) throw new Error(`${field} must be a stable whitespace-free ID`);
55
+ return value;
56
+ }
57
+
58
+ export function safeText(value, field, maximum, nullable = false) {
59
+ if ((value === null || value === undefined || value === "") && nullable) return null;
60
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${field} is required`);
61
+ const result = value.trim().slice(0, maximum);
62
+ if (SECRET_RE.test(result)) throw new Error(`${field} appears to contain a secret and cannot enter self-starter state`);
63
+ return result;
64
+ }
65
+
66
+ export function integer(value, field, minimum, maximum) {
67
+ const parsed = Number(value);
68
+ if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) throw new Error(`${field} must be an integer between ${minimum} and ${maximum}`);
69
+ return parsed;
70
+ }
71
+
72
+ export function capabilities(value) {
73
+ if (!Array.isArray(value) || !value.length || value.some((item) => typeof item !== "string" || !CAPABILITY_RE.test(item))) {
74
+ throw new Error("capabilities must contain exact tool:<name> values and cannot use wildcards");
75
+ }
76
+ const unique = [...new Set(value)].sort();
77
+ if (unique.some((item) => item.includes("*"))) throw new Error("capability wildcards are forbidden");
78
+ return unique;
79
+ }
80
+
81
+ export function normalizePolicy(value, root) {
82
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.schema !== POLICY_SCHEMA || value.root !== root
83
+ || !Number.isInteger(value.revision) || value.revision < 0 || !Array.isArray(value.grants) || !Array.isArray(value.history)) {
84
+ throw new Error("execution policy structure is invalid; self-starter is disabled until repaired");
85
+ }
86
+ return value;
87
+ }
88
+
89
+ export function normalizeJobs(value, root) {
90
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.schema !== JOB_SCHEMA || value.root !== root
91
+ || !Array.isArray(value.jobs) || !Array.isArray(value.history) || !Array.isArray(value.receipts)) {
92
+ throw new Error("self-starter checkpoint structure is invalid; automatic execution is disabled until repaired");
93
+ }
94
+ return value;
95
+ }
96
+
97
+ export async function readJson(path, root, normalize, empty) {
98
+ try {
99
+ const metadata = await stat(path);
100
+ if (metadata.size > MAX_STATE_BYTES) throw new Error("self-starter state exceeds the 5 MiB read limit");
101
+ return normalize(JSON.parse(await readFile(path, "utf8")), root);
102
+ } catch (error) {
103
+ if (error.code !== "ENOENT") throw error;
104
+ return empty(root);
105
+ }
106
+ }
107
+
108
+ export async function saveJson(value, path) {
109
+ const content = `${JSON.stringify(value, null, 2)}\n`;
110
+ if (Buffer.byteLength(content) > MAX_STATE_BYTES) throw new Error("self-starter state exceeds 5 MiB; purge completed jobs first");
111
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
112
+ try {
113
+ await writeFile(temporary, content, { mode: 0o600 });
114
+ await replaceFileWithRetry(temporary, path);
115
+ } finally {
116
+ await unlink(temporary).catch((error) => { if (error.code !== "ENOENT") throw error; });
117
+ }
118
+ }
119
+
120
+ export async function acquireLock(path) {
121
+ const lockPath = `${path}.lock`;
122
+ for (let attempt = 0; attempt < 120; attempt += 1) {
123
+ try {
124
+ return { handle: await open(lockPath, "wx", 0o600), lockPath };
125
+ } catch (error) {
126
+ if (!isFileLockContention(error)) throw error;
127
+ try {
128
+ const metadata = await stat(lockPath);
129
+ if (Date.now() - metadata.mtimeMs > 90000) await unlink(lockPath);
130
+ } catch (lockError) {
131
+ if (lockError.code !== "ENOENT") throw lockError;
132
+ }
133
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 25));
134
+ }
135
+ }
136
+ throw new Error("self-starter state is busy; retry later");
137
+ }
138
+
139
+ export async function withLock(path, read, operation, save = true) {
140
+ const { handle, lockPath } = await acquireLock(path);
141
+ try {
142
+ const state = await read();
143
+ const result = await operation(state);
144
+ if (save) await saveJson(state, path);
145
+ return result;
146
+ } finally {
147
+ await handle.close();
148
+ await unlink(lockPath).catch((error) => { if (error.code !== "ENOENT") throw error; });
149
+ }
150
+ }
151
+
152
+ export async function pathsFor(root, providedCatalog = null) {
153
+ if (isConfiguredHostProfileRoot(root)) {
154
+ throw new Error("self-starter cannot use a host profile as its workspace root");
155
+ }
156
+ let catalog = providedCatalog;
157
+ if (!catalog) {
158
+ try {
159
+ catalog = await buildCatalog(root);
160
+ } catch (error) {
161
+ if (typeof error?.code === "string" && (error.path || error.syscall)) throw workspaceScanError(error);
162
+ throw error;
163
+ }
164
+ }
165
+ const directory = await projectStateDir(catalog.root);
166
+ return {
167
+ catalog,
168
+ executionPolicyPath: join(directory, "execution-policy.json"),
169
+ selfstarterPath: join(directory, "selfstarter.json")
170
+ };
171
+ }
172
+
173
+ export function knownActor(graph, id) {
174
+ return graph.entities.find((entity) => entity.id === id && ["person", "agent"].includes(entity.kind));
175
+ }
176
+
177
+ export function currentTask(coordination, id) {
178
+ return coordination.tasks.find((task) => task.id === id);
179
+ }
180
+
181
+ export async function referenceState(catalog) {
182
+ const [{ graph }, { coordination }] = await Promise.all([
183
+ loadGraph(catalog.root, catalog), loadCoordination(catalog.root, catalog)
184
+ ]);
185
+ return { graph, coordination };
186
+ }
187
+
188
+ export function validateGrantReferences(grant, graph, coordination) {
189
+ const task = currentTask(coordination, grant.taskId);
190
+ return Boolean(knownActor(graph, grant.actorId) && knownActor(graph, grant.targetId) && task
191
+ && task.assigneeId === grant.actorId && task.projectId === grant.projectId && task.groupId === grant.groupId
192
+ && ["open", "in-progress"].includes(task.status));
193
+ }
194
+
195
+ export function executionPolicyFindingsInternal(policy, graph, coordination, { checkCurrentReferences = true } = {}) {
196
+ const findings = [];
197
+ if (policy.schema !== POLICY_SCHEMA || !Number.isInteger(policy.revision) || policy.revision < 0) findings.push("invalid-execution-policy");
198
+ if (new Set(policy.grants.map((grant) => grant.id)).size !== policy.grants.length) findings.push("duplicate-execution-grant-id");
199
+ const records = [
200
+ ...policy.grants.map((grant) => ({ grant, current: true })),
201
+ ...policy.history.map((entry) => entry.value).filter(Boolean).map((grant) => ({ grant, current: false }))
202
+ ];
203
+ for (const { grant, current } of records) {
204
+ const valid = ID_RE.test(grant.id || "") && ID_RE.test(grant.jobId || "") && ID_RE.test(grant.taskId || "")
205
+ && ID_RE.test(grant.actorId || "") && ID_RE.test(grant.targetId || "") && ID_RE.test(grant.projectId || "")
206
+ && (grant.groupId === null || ID_RE.test(grant.groupId || "")) && HOSTS.has(grant.host)
207
+ && JSON.stringify(grant.actions) === JSON.stringify(["effect", "resume", "start"])
208
+ && Array.isArray(grant.capabilities) && grant.capabilities.length > 0
209
+ && JSON.stringify(grant.capabilities) === JSON.stringify([...new Set(grant.capabilities)].sort())
210
+ && grant.capabilities.every((item) => CAPABILITY_RE.test(item) && !item.includes("*"))
211
+ && typeof grant.active === "boolean" && typeof grant.reason === "string" && !SECRET_RE.test(grant.reason)
212
+ && Number.isInteger(grant.revision) && grant.revision > 0 && grant.revision <= policy.revision
213
+ && grant.source === "explicit-local-owner-policy" && grant.authority === "explicit-local-execution-policy"
214
+ && typeof grant.createdAt === "string" && typeof grant.updatedAt === "string"
215
+ && (grant.expiresAt === null || Number.isFinite(new Date(grant.expiresAt).getTime()));
216
+ if (!valid) findings.push(`invalid-execution-grant:${grant.id || "unknown"}`);
217
+ if (checkCurrentReferences && current && grant.active && !validateGrantReferences(grant, graph, coordination)) {
218
+ findings.push(`stale-execution-grant:${grant.id || "unknown"}`);
219
+ }
220
+ }
221
+ if (policy.history.some((entry) => entry.kind !== "execution-grant" || !ID_RE.test(entry.recordId || "")
222
+ || !Number.isFinite(new Date(entry.supersededAt).getTime())
223
+ || entry.authority !== "explicit-local-execution-policy" || !entry.value)) findings.push("invalid-execution-policy-history");
224
+ return findings;
225
+ }
226
+
227
+ export function receiptDigest(value) {
228
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
229
+ }
230
+
231
+ export function validLease(value) {
232
+ return value === null || (value && ID_RE.test(value.sessionId || "") && HOSTS.has(value.host)
233
+ && Number.isFinite(new Date(value.acquiredAt).getTime()) && Number.isFinite(new Date(value.expiresAt).getTime())
234
+ && new Date(value.expiresAt) > new Date(value.acquiredAt) && DIGEST_RE.test(value.workspaceDigest || ""));
235
+ }
236
+
237
+ export function validPending(value) {
238
+ return value === null || (value && ID_RE.test(value.toolUseId || "") && CAPABILITY_RE.test(value.capability || "")
239
+ && DIGEST_RE.test(value.beforeDigest || "") && Number.isFinite(new Date(value.authorizedAt).getTime())
240
+ && ID_RE.test(value.sessionId || ""));
241
+ }
242
+
243
+ export function validBlocker(value) {
244
+ return value === null || (value && typeof value.code === "string" && value.code.length > 0 && value.code.length <= 128
245
+ && Number.isFinite(new Date(value.at).getTime()) && value.authority === "execution-state-only");
246
+ }
247
+
248
+ export function validJobRecord(job) {
249
+ return job && ID_RE.test(job.id || "") && ID_RE.test(job.taskId || "") && ID_RE.test(job.actorId || "")
250
+ && ID_RE.test(job.targetId || "") && ID_RE.test(job.projectId || "")
251
+ && (job.groupId === null || ID_RE.test(job.groupId || "")) && HOSTS.has(job.host) && JOB_STATUSES.has(job.status)
252
+ && Array.isArray(job.capabilities) && job.capabilities.length > 0
253
+ && JSON.stringify(job.capabilities) === JSON.stringify([...new Set(job.capabilities)].sort())
254
+ && job.capabilities.every((item) => CAPABILITY_RE.test(item) && !item.includes("*"))
255
+ && ID_RE.test(job.grantId || "") && DIGEST_RE.test(job.checkpoint?.workspaceDigest || "")
256
+ && Number.isInteger(job.checkpoint?.sequence) && job.checkpoint.sequence >= 0
257
+ && Number.isInteger(job.checkpoint?.workspaceFiles) && job.checkpoint.workspaceFiles >= 0
258
+ && Number.isInteger(job.checkpoint?.workspaceBytes) && job.checkpoint.workspaceBytes >= 0
259
+ && (job.checkpoint?.lastToolUseId === null || ID_RE.test(job.checkpoint?.lastToolUseId || ""))
260
+ && (job.checkpoint?.resultDigest === null || DIGEST_RE.test(job.checkpoint?.resultDigest || ""))
261
+ && Number.isInteger(job.checkpoint?.retryCount) && job.checkpoint.retryCount >= 0
262
+ && (job.checkpoint?.nextRetryAt === null || Number.isFinite(new Date(job.checkpoint?.nextRetryAt).getTime()))
263
+ && Number.isFinite(new Date(job.checkpoint?.updatedAt).getTime())
264
+ && Number.isInteger(job.maxRetries) && job.maxRetries >= 0 && job.maxRetries <= 10
265
+ && Number.isInteger(job.leaseSeconds) && job.leaseSeconds >= 15 && job.leaseSeconds <= 900
266
+ && Number.isInteger(job.baseRetrySeconds) && job.baseRetrySeconds >= 1 && job.baseRetrySeconds <= 3600
267
+ && validLease(job.lease) && validPending(job.pendingEffect) && validBlocker(job.lastBlocker)
268
+ && job.authority === "execution-state-only" && Number.isFinite(new Date(job.createdAt).getTime())
269
+ && Number.isFinite(new Date(job.updatedAt).getTime());
270
+ }
271
+
272
+ export function validReceipt(receipt) {
273
+ if (!receipt || !ID_RE.test(receipt.id || "") || !ID_RE.test(receipt.jobId || "") || typeof receipt.event !== "string"
274
+ || !Number.isFinite(new Date(receipt.at).getTime()) || !receipt.details || typeof receipt.details !== "object"
275
+ || Array.isArray(receipt.details) || !DIGEST_RE.test(receipt.digest || "") || receipt.authority !== "execution-state-only") return false;
276
+ const { digest, ...material } = receipt;
277
+ return receiptDigest(material) === digest;
278
+ }
279
+
280
+ export function selfstarterFindingsInternal(state, policy, graph, coordination) {
281
+ const findings = [];
282
+ const grants = new Map(policy.grants.map((grant) => [grant.id, grant]));
283
+ if (state.schema !== JOB_SCHEMA) findings.push("invalid-selfstarter-state");
284
+ if (new Set(state.jobs.map((job) => job.id)).size !== state.jobs.length) findings.push("duplicate-job-id");
285
+ for (const job of state.jobs) {
286
+ const grant = grants.get(job.grantId);
287
+ const task = currentTask(coordination, job.taskId);
288
+ if (!validJobRecord(job)) findings.push(`invalid-job:${job.id || "unknown"}`);
289
+ if (!grant || grant.jobId !== job.id || grant.taskId !== job.taskId || grant.actorId !== job.actorId
290
+ || grant.targetId !== job.targetId || grant.projectId !== job.projectId || grant.groupId !== job.groupId
291
+ || grant.host !== job.host || JSON.stringify(grant.capabilities) !== JSON.stringify(job.capabilities)) findings.push(`job-grant-mismatch:${job.id || "unknown"}`);
292
+ if (!task || task.assigneeId !== job.actorId || task.projectId !== job.projectId || task.groupId !== job.groupId) findings.push(`job-task-mismatch:${job.id || "unknown"}`);
293
+ }
294
+ if (state.history.some((entry) => entry.kind !== "selfstarter-job" || !ID_RE.test(entry.recordId || "")
295
+ || typeof entry.event !== "string" || !Number.isFinite(new Date(entry.supersededAt).getTime())
296
+ || entry.authority !== "execution-state-only" || !validJobRecord(entry.value))) findings.push("invalid-job-history");
297
+ if (state.receipts.some((receipt) => !validReceipt(receipt))) findings.push("invalid-job-receipt");
298
+ return findings;
299
+ }
300
+
301
+ export function requireOwnerConfirmation(confirmation) {
302
+ if (confirmation !== CONFIRMATION) throw new Error("self-starter policy changes require explicit local owner confirmation");
303
+ }
304
+
305
+ export function preservePolicy(policy, grant, at) {
306
+ policy.history.push({ kind: "execution-grant", recordId: grant.id, supersededAt: at, value: { ...grant }, authority: "explicit-local-execution-policy" });
307
+ }
308
+
309
+ export function preserveJob(state, job, event, at) {
310
+ state.history.push({ kind: "selfstarter-job", event, recordId: job.id, supersededAt: at, value: structuredClone(job), authority: "execution-state-only" });
311
+ }
312
+
313
+ export function appendReceipt(state, job, id, event, at, details = {}) {
314
+ const existing = state.receipts.find((receipt) => receipt.id === id);
315
+ const material = { id, jobId: job.id, event, at, details, authority: "execution-state-only" };
316
+ const digest = receiptDigest(material);
317
+ if (existing) {
318
+ if (existing.digest !== digest) throw new Error("self-starter receipt collision; execution stopped");
319
+ return { receipt: existing, duplicate: true };
320
+ }
321
+ const receipt = { ...material, digest };
322
+ state.receipts.push(receipt);
323
+ return { receipt, duplicate: false };
324
+ }
325
+
326
+ export function exactGrant(policy, job, action, at) {
327
+ const grant = policy.grants.find((item) => item.id === job.grantId);
328
+ if (!grant || !grant.active || !grant.actions.includes(action)) throw new Error(`default deny: no current exact ${action} grant for ${job.id}`);
329
+ if (grant.expiresAt && new Date(grant.expiresAt) <= new Date(at)) throw new Error(`execution grant expired for ${job.id}`);
330
+ const matches = grant.actorId === job.actorId && grant.jobId === job.id && grant.taskId === job.taskId
331
+ && grant.targetId === job.targetId && grant.projectId === job.projectId && grant.groupId === job.groupId
332
+ && grant.host === job.host && JSON.stringify(grant.capabilities) === JSON.stringify(job.capabilities);
333
+ if (!matches) throw new Error(`execution grant scope mismatch for ${job.id}`);
334
+ return grant;
335
+ }
336
+
337
+ export function ensureScope(job, scope) {
338
+ const matches = job.actorId === scope.actorId && job.projectId === scope.projectId
339
+ && job.groupId === (scope.groupId ?? null) && job.host === scope.host
340
+ && (!scope.taskId || job.taskId === scope.taskId) && (!scope.jobId || job.id === scope.jobId);
341
+ if (!matches) throw new Error(`self-starter scope mismatch for ${job.id}`);
342
+ }
343
+
344
+ export function currentTaskValid(job, coordination) {
345
+ const task = currentTask(coordination, job.taskId);
346
+ return task && task.assigneeId === job.actorId && task.projectId === job.projectId && task.groupId === job.groupId
347
+ && ["open", "in-progress"].includes(task.status);
348
+ }
349
+
350
+ export function leaseExpired(job, at) {
351
+ return job.lease && new Date(job.lease.expiresAt) <= new Date(at);
352
+ }
353
+
354
+ export function blocker(job, code, at) {
355
+ job.status = code === "retry-budget-exhausted" ? "exhausted" : "blocked";
356
+ job.lastBlocker = { code, at, authority: "execution-state-only" };
357
+ job.lease = null;
358
+ job.updatedAt = at;
359
+ }
360
+
361
+ export async function lockedStates(root, operation) {
362
+ const paths = await pathsFor(root);
363
+ const { graph, coordination } = await referenceState(paths.catalog);
364
+ return withLock(paths.executionPolicyPath, () => readJson(paths.executionPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy), (policy) => withLock(
365
+ paths.selfstarterPath,
366
+ () => readJson(paths.selfstarterPath, paths.catalog.root, normalizeJobs, emptyJobs),
367
+ (state) => {
368
+ const findings = [
369
+ ...executionPolicyFindingsInternal(policy, graph, coordination, { checkCurrentReferences: false }),
370
+ ...selfstarterFindingsInternal(state, policy, graph, coordination)
371
+ ];
372
+ if (findings.length) throw new Error(`self-starter failed closed: ${findings.join(", ")}`);
373
+ return operation({ paths, policy, state, graph, coordination });
374
+ }
375
+ ), false);
376
+ }
377
+
378
+ export function resultHash(value) {
379
+ let encoded;
380
+ try { encoded = JSON.stringify(value ?? null); } catch { encoded = "unserializable-result"; }
381
+ return createHash("sha256").update(encoded.slice(0, 65536)).digest("hex");
382
+ }