faberun 0.3.0

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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/bin/faberun.mjs +25 -0
  4. package/integrations/claude-code/statusline-bench.sh +42 -0
  5. package/integrations/claude-code/statusline.sh +80 -0
  6. package/package.json +33 -0
  7. package/skills/faberun/SKILL.md +24 -0
  8. package/skills/faberun/references/contract.md +380 -0
  9. package/skills/faberun/references/engineering.md +29 -0
  10. package/skills/faberun/references/handoffs.md +26 -0
  11. package/skills/faberun/references/operations.md +184 -0
  12. package/skills/faberun/references/rules.md +35 -0
  13. package/skills/faberun/references/workflow.md +23 -0
  14. package/skills/init-agentkit/SKILL.md +108 -0
  15. package/skills/init-agentkit/scripts/install-agentkit.sh +127 -0
  16. package/skills/init-agentkit/templates/.claude/commands/create-adr.md +44 -0
  17. package/skills/init-agentkit/templates/.github/workflows/quality.yml +43 -0
  18. package/skills/init-agentkit/templates/.sentrux/baseline.json +9 -0
  19. package/skills/init-agentkit/templates/.sentrux/rules.toml +21 -0
  20. package/skills/init-agentkit/templates/AGENTS.md +110 -0
  21. package/skills/init-agentkit/templates/docs/ABSTRACTIONS.md +30 -0
  22. package/skills/init-agentkit/templates/docs/ARCHITECTURE.md +31 -0
  23. package/skills/init-agentkit/templates/docs/GETTING-STARTED.md +44 -0
  24. package/skills/init-agentkit/templates/docs/VISION.md +33 -0
  25. package/skills/init-agentkit/templates/docs/adr/0001-record-architecture-decisions.md +36 -0
  26. package/skills/init-agentkit/templates/docs/adr/0002-root-managed-ai-guidance.md +37 -0
  27. package/skills/init-agentkit/templates/docs/adr/0003-sentrux-structural-quality-gates.md +49 -0
  28. package/skills/init-agentkit/templates/docs/adr/README.md +52 -0
  29. package/skills/init-agentkit/templates/docs/sentrux.md +66 -0
  30. package/skills/init-agentkit/templates/githooks/commit-msg +22 -0
  31. package/skills/init-agentkit/templates/githooks/pre-commit +32 -0
  32. package/src/campaign/brief.mjs +394 -0
  33. package/src/campaign/chain.mjs +555 -0
  34. package/src/campaign/handoff.mjs +516 -0
  35. package/src/campaign/index.mjs +300 -0
  36. package/src/campaign/journal.mjs +347 -0
  37. package/src/campaign/layout.mjs +51 -0
  38. package/src/campaign/metrics-evals.mjs +25 -0
  39. package/src/campaign/metrics.mjs +517 -0
  40. package/src/campaign/projection.mjs +250 -0
  41. package/src/campaign/record.mjs +102 -0
  42. package/src/campaign/unpark.mjs +56 -0
  43. package/src/cli/brand.mjs +205 -0
  44. package/src/cli/campaign.mjs +730 -0
  45. package/src/cli/contract.mjs +67 -0
  46. package/src/cli/init.mjs +170 -0
  47. package/src/cli/launch.mjs +239 -0
  48. package/src/cli/seat.mjs +139 -0
  49. package/src/cli/setup.mjs +294 -0
  50. package/src/cli/skills.mjs +105 -0
  51. package/src/cli/update.mjs +216 -0
  52. package/src/cli.mjs +525 -0
  53. package/src/contract/articles.mjs +12 -0
  54. package/src/contract/assert.mjs +162 -0
  55. package/src/contract/definition-of-done.mjs +97 -0
  56. package/src/contract/final-verification.mjs +96 -0
  57. package/src/contract/index.mjs +641 -0
  58. package/src/contract/judge-envelope.mjs +25 -0
  59. package/src/contract/review-modes.mjs +151 -0
  60. package/src/contract/runtime.mjs +204 -0
  61. package/src/contract/schema-version.mjs +25 -0
  62. package/src/contract/scope-findings.mjs +77 -0
  63. package/src/contract/snapshot.mjs +639 -0
  64. package/src/contract/task-packet.mjs +495 -0
  65. package/src/contract/untrusted.mjs +75 -0
  66. package/src/contract/verification.mjs +185 -0
  67. package/src/contract/worker-result.mjs +138 -0
  68. package/src/engine/assignment.mjs +63 -0
  69. package/src/engine/backoff.mjs +492 -0
  70. package/src/engine/bulk-read.mjs +361 -0
  71. package/src/engine/cancel.mjs +177 -0
  72. package/src/engine/detach.mjs +101 -0
  73. package/src/engine/dispatch.mjs +752 -0
  74. package/src/engine/failover.mjs +192 -0
  75. package/src/engine/gate.mjs +183 -0
  76. package/src/engine/judge-gate.mjs +517 -0
  77. package/src/engine/lifecycle.mjs +772 -0
  78. package/src/engine/live-preflight.mjs +299 -0
  79. package/src/engine/mutation.mjs +146 -0
  80. package/src/engine/notify-queue.mjs +327 -0
  81. package/src/engine/process-identity.mjs +72 -0
  82. package/src/engine/process.mjs +774 -0
  83. package/src/engine/prompts.mjs +289 -0
  84. package/src/engine/recover.mjs +300 -0
  85. package/src/engine/result-file.mjs +222 -0
  86. package/src/engine/resume.mjs +635 -0
  87. package/src/engine/retry.mjs +334 -0
  88. package/src/engine/review.mjs +228 -0
  89. package/src/engine/run-command.mjs +287 -0
  90. package/src/engine/run-identity.mjs +411 -0
  91. package/src/engine/runtime-discovery.mjs +235 -0
  92. package/src/engine/scheduler.mjs +526 -0
  93. package/src/engine/scope.mjs +378 -0
  94. package/src/engine/settle.mjs +207 -0
  95. package/src/engine/state.mjs +148 -0
  96. package/src/engine/supervise.mjs +713 -0
  97. package/src/engine/verify.mjs +167 -0
  98. package/src/harnesses/agy/index.mjs +62 -0
  99. package/src/harnesses/catalogue.mjs +509 -0
  100. package/src/harnesses/claude/index.mjs +90 -0
  101. package/src/harnesses/codex/index.mjs +87 -0
  102. package/src/harnesses/dsh/closed-packet.patch.yml +42 -0
  103. package/src/harnesses/dsh/index.mjs +210 -0
  104. package/src/harnesses/dsh/runner.mjs +259 -0
  105. package/src/harnesses/exec-jsonl/index.mjs +788 -0
  106. package/src/harnesses/index.mjs +508 -0
  107. package/src/harnesses/protocol.mjs +531 -0
  108. package/src/harnesses/replay/bin.mjs +386 -0
  109. package/src/harnesses/replay/index.mjs +238 -0
  110. package/src/harnesses/zcode/index.mjs +276 -0
  111. package/src/host/config.mjs +87 -0
  112. package/src/host/home.mjs +149 -0
  113. package/src/host/package.mjs +23 -0
  114. package/src/host/preflight.mjs +520 -0
  115. package/src/host/tool-policy-decisions.mjs +341 -0
  116. package/src/host/tool-policy-hook.mjs +270 -0
  117. package/src/notify/index.mjs +359 -0
  118. package/src/notify/os-macos.mjs +81 -0
  119. package/src/repo/declared-paths.mjs +220 -0
  120. package/src/repo/integrate.mjs +546 -0
  121. package/src/repo/scope-closure.mjs +665 -0
  122. package/src/repo/signal-block.mjs +16 -0
  123. package/src/repo/signal.mjs +222 -0
  124. package/src/repo/source-identity.mjs +295 -0
  125. package/src/repo/workspace.mjs +557 -0
  126. package/src/repo/worktree.mjs +352 -0
  127. package/src/report/final.mjs +200 -0
  128. package/src/report/metrics-report.mjs +99 -0
  129. package/src/report/next.mjs +383 -0
  130. package/src/report/render.mjs +716 -0
  131. package/src/run/disk-gc.mjs +251 -0
  132. package/src/run/lock.mjs +329 -0
  133. package/src/run/node-store.mjs +62 -0
  134. package/src/run/operations.mjs +286 -0
  135. package/src/run/store.mjs +187 -0
  136. package/src/run/usage.mjs +337 -0
  137. package/src/seat/harnesses.mjs +83 -0
  138. package/src/seat/index.mjs +239 -0
  139. package/src/seat/tmux.mjs +208 -0
  140. package/src/util.mjs +0 -0
  141. package/src/web/api.mjs +371 -0
  142. package/src/web/boundary.mjs +88 -0
  143. package/src/web/index.html +299 -0
  144. package/src/web/server.mjs +552 -0
@@ -0,0 +1,639 @@
1
+ /**
2
+ * Everything the controller persists and reads back: run metadata, a node
3
+ * snapshot, a transition event, and the nested records inside them (invocations,
4
+ * gate results, routing, worktree, scope, usage).
5
+ *
6
+ * Validated on the way in *and* on the way out. A snapshot written by an earlier
7
+ * controller version, or hand-edited between runs, is refused by name rather
8
+ * than half-understood -- the whole point of a resumable run is that what is on
9
+ * disk is trustworthy or loudly not.
10
+ */
11
+ import { Buffer } from "node:buffer";
12
+ import { MAX_SCOPE_FINDING_PATHS } from "./scope-findings.mjs";
13
+ import { REVIEW_MODES } from "./review-modes.mjs";
14
+ import { assertObject, boundedString, nonNegativeInteger, nonNegativeNumber, positiveInteger, positiveNumber, rejectUnknown, requireId, requireInteger, requirePacketHash, requireString, requireTimestamp } from "./assert.mjs";
15
+ import { stableJson } from "../util.mjs";
16
+ import { isAbsolute } from "node:path";
17
+ import { validateCompleteSourceIdentity, validateSourceIdentity } from "../repo/source-identity.mjs";
18
+ import { validateMetadata } from "./schema-version.mjs";
19
+ import { validateSnapshotRuntime } from "./runtime.mjs";
20
+ import { validateVerificationSnapshot } from "./final-verification.mjs";
21
+ import { validateWorkerResult } from "./worker-result.mjs";
22
+
23
+ /** @typedef {import("./index.mjs").EventRecord} EventRecord */
24
+ /** @typedef {import("../notify/index.mjs").JsonObject} JsonObject */
25
+ /** @typedef {import("./index.mjs").NodeSnapshot} NodeSnapshot */
26
+ /** @typedef {import("./index.mjs").RunMetadata} RunMetadata */
27
+ /** @typedef {import("./index.mjs").ValidatedNode} ValidatedNode */
28
+
29
+ /**
30
+ * Hard byte ceiling for an operator's answer text, matching the bounded
31
+ * `Previous attempt` section a retry in place already enforces (retry.mjs).
32
+ */
33
+ export const OPERATOR_ANSWER_MAX_BYTES = 8 * 1024;
34
+
35
+ const GATE_VERDICTS = new Set(["pass", "fail", "invalid_judge_output"]);
36
+ const NODE_STATUSES = new Set(["pending", "running", "done", "no-op", "blocked", "failed", "exhausted", "stalled", "canceled"]);
37
+ const NODE_PHASES = new Set(["waiting", "worker", "judge", "complete", "dependency", "canceled"]);
38
+ const GATE_RESULT_FIELDS = new Set(["verdict", "maxSeverity", "summary", "findings"]);
39
+ const FINDING_FIELDS = new Set(["severity", "description", "evidence"]);
40
+ const ERROR_FIELDS = new Set(["code", "message", "exhaustedUntil"]);
41
+ const USAGE_FIELDS = new Set(["inputTokens", "outputTokens", "cacheReadInputTokens"]);
42
+ const MAX_ROUTING_HISTORY = 64;
43
+ /**
44
+ * @param {JsonObject} value
45
+ * @param {{requireSourceIdentity?: boolean}} options
46
+ * @returns {RunMetadata}
47
+ */
48
+ export function validateRunMetadata(value, options = {}) {
49
+ assertObject(value, "run metadata");
50
+ rejectUnknown(value, new Set([
51
+ "schemaVersion", "contractVersion", "pid", "processStartToken", "startedAt", "sourceIdentity",
52
+ "controllerIdentity", "identityWarnings", "integrationRef", "relaunchCount", "lastRelaunchProgressAt", "attention",
53
+ "contractDigest", "scopeDecision", "autoRetries",
54
+ ]), "run metadata");
55
+ validateMetadata(value, "run metadata");
56
+ requireInteger(value.pid, "run metadata.pid");
57
+ if (value.processStartToken !== undefined && value.processStartToken !== null) requireString(value.processStartToken, "run metadata.processStartToken");
58
+ requireString(value.startedAt, "run metadata.startedAt");
59
+ if (value.integrationRef !== undefined) requireString(value.integrationRef, "run metadata.integrationRef");
60
+ // The controller snapshot a run was launched from: a path plus the sha of
61
+ // its executable bytes, verified before the next chain node launches N+1.
62
+ if (value.controllerIdentity !== undefined) {
63
+ assertObject(value.controllerIdentity, "run metadata.controllerIdentity");
64
+ rejectUnknown(/** @type {JsonObject} */ (value.controllerIdentity), new Set(["path", "sha"]), "run metadata.controllerIdentity");
65
+ const controllerIdentity = /** @type {JsonObject} */ (value.controllerIdentity);
66
+ requireString(controllerIdentity.path, "run metadata.controllerIdentity.path");
67
+ requirePacketHash(controllerIdentity.sha, "run metadata.controllerIdentity.sha");
68
+ }
69
+ // The supervisor's no-progress relaunch guard is durable in run.json: an
70
+ // in-memory counter resets whenever the supervisor restarts, which is an
71
+ // endless dispatch storm rather than a guard.
72
+ if (value.relaunchCount !== undefined) nonNegativeInteger(value.relaunchCount, "run metadata.relaunchCount");
73
+ if (value.lastRelaunchProgressAt !== undefined && value.lastRelaunchProgressAt !== null) requireTimestamp(value.lastRelaunchProgressAt, "run metadata.lastRelaunchProgressAt");
74
+ if (value.attention !== undefined && value.attention !== null) {
75
+ assertObject(value.attention, "run metadata.attention");
76
+ rejectUnknown(/** @type {JsonObject} */ (value.attention), new Set(["code", "message", "at"]), "run metadata.attention");
77
+ const attention = /** @type {JsonObject} */ (value.attention);
78
+ requireString(attention.code, "run metadata.attention.code");
79
+ requireString(attention.message, "run metadata.attention.message");
80
+ requireTimestamp(attention.at, "run metadata.attention.at");
81
+ }
82
+ validateSourceIdentity(value.sourceIdentity, "run metadata.sourceIdentity", { kind: "run" });
83
+ if (value.identityWarnings !== undefined) {
84
+ if (!Array.isArray(value.identityWarnings) || value.identityWarnings.length > 8) {
85
+ throw new TypeError("run metadata.identityWarnings must be an array of at most 8 strings");
86
+ }
87
+ for (const [index, warning] of value.identityWarnings.entries()) {
88
+ if (typeof warning !== "string" || !warning.trim() || Buffer.byteLength(warning, "utf8") > 1024) {
89
+ throw new TypeError(`run metadata.identityWarnings[${index}] must be a string of at most 1024 bytes`);
90
+ }
91
+ }
92
+ }
93
+ // The contract the run was launched with, frozen as a digest, and the scope
94
+ // decision that authoring made against the then-current tree. A persisted
95
+ // load compares its recomputed digest against the first; both survive every
96
+ // metadata rewrite because they live on the fixed field list.
97
+ if (value.contractDigest !== undefined) requirePacketHash(value.contractDigest, "run metadata.contractDigest");
98
+ if (value.scopeDecision !== undefined) {
99
+ assertObject(value.scopeDecision, "run metadata.scopeDecision");
100
+ rejectUnknown(/** @type {JsonObject} */ (value.scopeDecision), new Set(["at", "base", "dirtyTreeFingerprint"]), "run metadata.scopeDecision");
101
+ const scopeDecision = /** @type {JsonObject} */ (value.scopeDecision);
102
+ requireTimestamp(scopeDecision.at, "run metadata.scopeDecision.at");
103
+ if (scopeDecision.base !== null) requireString(scopeDecision.base, "run metadata.scopeDecision.base");
104
+ if (scopeDecision.dirtyTreeFingerprint !== null) requireString(scopeDecision.dirtyTreeFingerprint, "run metadata.scopeDecision.dirtyTreeFingerprint");
105
+ }
106
+ // The one-shot automatic-retry ledger: node id to the code it retried and
107
+ // when. It lives in run.json so a controller restart cannot grant a second.
108
+ if (value.autoRetries !== undefined) {
109
+ assertObject(value.autoRetries, "run metadata.autoRetries");
110
+ for (const [nodeId, entry] of Object.entries(/** @type {JsonObject} */ (value.autoRetries))) {
111
+ requireId(nodeId, "run metadata.autoRetries node id");
112
+ assertObject(entry, `run metadata.autoRetries.${nodeId}`);
113
+ rejectUnknown(/** @type {JsonObject} */ (entry), new Set(["code", "at"]), `run metadata.autoRetries.${nodeId}`);
114
+ requireString(/** @type {JsonObject} */ (entry).code, `run metadata.autoRetries.${nodeId}.code`);
115
+ requireTimestamp(/** @type {JsonObject} */ (entry).at, `run metadata.autoRetries.${nodeId}.at`);
116
+ }
117
+ }
118
+ if (options.requireSourceIdentity) validateCompleteSourceIdentity(/** @type {JsonObject} */ (value.sourceIdentity));
119
+ return /** @type {RunMetadata} */ (value);
120
+ }
121
+ /**
122
+ * @param {JsonObject} value
123
+ * @param {ValidatedNode|null} expectedNode
124
+ * @returns {NodeSnapshot}
125
+ */
126
+ export function validateNodeSnapshot(value, expectedNode = null) {
127
+ assertObject(value, "node snapshot");
128
+ rejectUnknown(value, new Set([
129
+ "schemaVersion", "contractVersion", "id", "type", "sourceIdentity", "packetHash", "status", "phase",
130
+ "attempt", "revisions", "judgeFailures", "runtime", "blockedBy", "startedAt", "updatedAt", "result", "gate", "error", "usage",
131
+ "costUsd", "routing", "progress", "worktree", "invocations", "executionOverrides", "verification", "scope",
132
+ "scopeFindings", "review", "previousAttempt", "sessionPolicy", "integratedHead",
133
+ ]), "node snapshot");
134
+ validateMetadata(value, "node snapshot");
135
+ requireId(value.id, "node snapshot.id");
136
+ requireString(value.type, "node snapshot.type");
137
+ if (!NODE_STATUSES.has(/** @type {string} */ (value.status))) throw new TypeError("node snapshot.status is invalid");
138
+ if (!NODE_PHASES.has(/** @type {string} */ (value.phase))) throw new TypeError("node snapshot.phase is invalid");
139
+ nonNegativeInteger(value.attempt, "node snapshot.attempt");
140
+ nonNegativeInteger(value.revisions, "node snapshot.revisions");
141
+ if (value.judgeFailures !== undefined) nonNegativeInteger(value.judgeFailures, "node snapshot.judgeFailures");
142
+ // The review mode that governed the attempt's gate, recorded so a status
143
+ // surface can tell an advisory finding from a below-threshold blocking one.
144
+ if (value.review !== undefined && !REVIEW_MODES.has(/** @type {string} */ (value.review))) {
145
+ throw new TypeError("node snapshot.review is invalid");
146
+ }
147
+ requirePacketHash(value.packetHash, "node snapshot.packetHash");
148
+ validateSourceIdentity(value.sourceIdentity, "node snapshot.sourceIdentity", { kind: "node" });
149
+ if (value.integratedHead !== undefined && value.integratedHead !== null) boundedString(value.integratedHead, "node snapshot.integratedHead", 256);
150
+ const sourceIdentity = /** @type {JsonObject} */ (value.sourceIdentity);
151
+ requireId(sourceIdentity.contractId, "node snapshot.sourceIdentity.contractId");
152
+ requireId(sourceIdentity.nodeId, "node snapshot.sourceIdentity.nodeId");
153
+ validateSnapshotRuntime(value.runtime, "node snapshot.runtime");
154
+ if (!Array.isArray(value.blockedBy) || (/** @type {unknown[]} */ (value.blockedBy)).some((id) => {
155
+ requireId(id, "node snapshot.blockedBy item");
156
+ return false;
157
+ })) {
158
+ throw new TypeError("node snapshot.blockedBy must be an array of ids");
159
+ }
160
+ if (value.startedAt !== null) requireTimestamp(value.startedAt, "node snapshot.startedAt");
161
+ requireTimestamp(value.updatedAt, "node snapshot.updatedAt");
162
+ if (value.result !== null) {
163
+ validateWorkerResult(/** @type {Record<string, unknown>} */ (value.result));
164
+ }
165
+ validateGateResult(value.gate, "node snapshot.gate");
166
+ validateSnapshotError(value.error, "node snapshot.error");
167
+ if (value.usage !== undefined) validateUsage(value.usage, "node snapshot.usage");
168
+ if (value.costUsd !== undefined) nonNegativeNumber(value.costUsd, "node snapshot.costUsd");
169
+ if (value.routing !== undefined && value.routing !== null) validateRoutingState(value.routing, "node snapshot.routing");
170
+ if (value.progress !== undefined && value.progress !== null) validateProgressState(value.progress, "node snapshot.progress");
171
+ if (value.worktree !== undefined && value.worktree !== null) validateWorktreeState(value.worktree, "node snapshot.worktree");
172
+ if (value.invocations !== undefined) validateInvocations(value.invocations, "node snapshot.invocations");
173
+ if (value.executionOverrides !== undefined) validateExecutionOverrides(value.executionOverrides, "node snapshot.executionOverrides");
174
+ if (value.verification !== undefined && value.verification !== null) validateVerificationSnapshot(value.verification);
175
+ if (value.scope !== undefined && value.scope !== null) validateScopeSnapshot(value.scope);
176
+ if (value.scopeFindings !== undefined && value.scopeFindings !== null) validateScopeFindings(value.scopeFindings);
177
+ // The session policy a rejection decision leaves for the dispatch that will
178
+ // run the retry. It is persisted because the decision can hand the node back
179
+ // to the scheduler, whose own `startWorker` call carries no argument; without
180
+ // it `phaseInvocationPlan` would rediscover the prior compatible continuation
181
+ // from the ledger and re-read the failed transcript (phase 6).
182
+ if (value.sessionPolicy !== undefined && value.sessionPolicy !== null) validateSessionPolicy(value.sessionPolicy, "node snapshot.sessionPolicy");
183
+ // The bounded `Previous attempt` section a retry in place attaches to the
184
+ // re-dispatched attempt's worker and judge prompts (retry.mjs renders it).
185
+ if (value.previousAttempt !== undefined) {
186
+ if (typeof value.previousAttempt !== "string" || !value.previousAttempt.trim()) {
187
+ throw new TypeError("node snapshot.previousAttempt must be a non-empty string");
188
+ }
189
+ if (Buffer.byteLength(value.previousAttempt, "utf8") > 8 * 1024) throw new TypeError("node snapshot.previousAttempt exceeds 8192 bytes");
190
+ }
191
+ if (expectedNode) validateSnapshotBinding(value, expectedNode);
192
+ if (Buffer.byteLength(JSON.stringify(value), "utf8") > 128 * 1024) throw new TypeError("node snapshot exceeds 131072 bytes");
193
+ return /** @type {NodeSnapshot} */ (value);
194
+ }
195
+ /**
196
+ * @param {JsonObject} value
197
+ * @returns {EventRecord}
198
+ */
199
+ export function validateEvent(value) {
200
+ assertObject(value, "event");
201
+ rejectUnknown(value, new Set([
202
+ "schemaVersion", "contractVersion", "at", "node", "from", "to", "type", "phase", "attempt", "runtime",
203
+ "role", "status", "currentRuntime", "errorCode", "error", "verdict", "summary", "revisions", "sourceIdentity", "packetHash", "override", "recovery", "invocationId", "unexpectedPaths", "unexpectedPathCount",
204
+ ]), "event");
205
+ validateMetadata(value, "event");
206
+ requireString(value.at, "event.at");
207
+ requireId(value.node, "event.node");
208
+ requireString(value.to, "event.to");
209
+ if (value.type !== undefined) boundedString(value.type, "event.type", 128);
210
+ if (value.role !== undefined && value.role !== "worker" && value.role !== "judge") throw new TypeError("event.role is invalid");
211
+ if (value.status !== undefined && !NODE_STATUSES.has(/** @type {string} */ (value.status))) throw new TypeError("event.status is invalid");
212
+ if (value.runtime !== undefined) requireId(value.runtime, "event.runtime");
213
+ if (value.currentRuntime !== undefined) requireId(value.currentRuntime, "event.currentRuntime");
214
+ if (value.errorCode !== undefined) boundedString(value.errorCode, "event.errorCode", 256);
215
+ requireString(value.packetHash, "event.packetHash");
216
+ validateSourceIdentity(value.sourceIdentity, "event.sourceIdentity");
217
+ if (value.summary !== undefined && Buffer.byteLength(/** @type {string} */ (value.summary), "utf8") > 4 * 1024) {
218
+ throw new TypeError("event.summary exceeds 4096 bytes");
219
+ }
220
+ if (value.unexpectedPaths !== undefined) {
221
+ const unexpectedPaths = /** @type {unknown[]} */ (value.unexpectedPaths);
222
+ if (!Array.isArray(value.unexpectedPaths) || unexpectedPaths.length > 64 || unexpectedPaths.some((path) => typeof path !== "string")) {
223
+ throw new TypeError("event unexpected paths are invalid");
224
+ }
225
+ }
226
+ if (value.unexpectedPathCount !== undefined) nonNegativeInteger(value.unexpectedPathCount, "event.unexpectedPathCount");
227
+ return /** @type {EventRecord} */ (value);
228
+ }
229
+ /**
230
+ * @param {unknown} value
231
+ * @param {string} label
232
+ */
233
+ function validateInvocations(value, label) {
234
+ if (!Array.isArray(value)) throw new TypeError(`${label} must be an array`);
235
+ const invocations = /** @type {JsonObject[]} */ (value);
236
+ for (const [index, invocation] of invocations.entries()) {
237
+ assertObject(invocation, `${label}[${index}]`);
238
+ const allowed = new Set([
239
+ "id", "pid", "processGroupId", "processStartToken", "harness", "runtimeId", "phase",
240
+ "promptPath", "stdoutPath", "stderrPath", "startedAt", "updatedAt", "closedAt", "deadlineAt",
241
+ "exitCode", "signal", "status", "executable", "usage", "usageEstimated", "costUsd", "costProvenance", "snapshotPath", "revision", "cycle",
242
+ "runId", "campaignId", "planPhase", "role", "runtimeFingerprint", "model", "reasoning", "sandbox", "continuationId", "continuationMode",
243
+ "nodeId", "attempt", "workspace", "worktreeBranch", "worktreeBaseSha",
244
+ ]);
245
+ rejectUnknown(invocation, allowed, `${label}[${index}]`);
246
+ requireString(invocation.id, `${label}[${index}].id`);
247
+ requireInteger(invocation.pid, `${label}[${index}].pid`);
248
+ if (invocation.processGroupId !== null) requireInteger(invocation.processGroupId, `${label}[${index}].processGroupId`);
249
+ if (invocation.processStartToken !== null) requireString(invocation.processStartToken, `${label}[${index}].processStartToken`);
250
+ requireString(invocation.harness, `${label}[${index}].harness`);
251
+ requireString(invocation.phase, `${label}[${index}].phase`);
252
+ requireId(invocation.runId, `${label}[${index}].runId`);
253
+ requireId(invocation.campaignId, `${label}[${index}].campaignId`);
254
+ if (invocation.nodeId !== undefined) requireId(invocation.nodeId, `${label}[${index}].nodeId`);
255
+ if (invocation.attempt !== undefined) nonNegativeInteger(invocation.attempt, `${label}[${index}].attempt`);
256
+ if (invocation.workspace !== undefined) boundedString(invocation.workspace, `${label}[${index}].workspace`, 4096);
257
+ if (invocation.worktreeBranch !== undefined && invocation.worktreeBranch !== null) boundedString(invocation.worktreeBranch, `${label}[${index}].worktreeBranch`, 512);
258
+ if (invocation.worktreeBaseSha !== undefined && invocation.worktreeBaseSha !== null) boundedString(invocation.worktreeBaseSha, `${label}[${index}].worktreeBaseSha`, 256);
259
+ boundedString(invocation.planPhase, `${label}[${index}].planPhase`, 128);
260
+ if (invocation.role !== "worker" && invocation.role !== "judge") throw new TypeError(`${label}[${index}].role is invalid`);
261
+ boundedString(invocation.runtimeFingerprint, `${label}[${index}].runtimeFingerprint`, 128);
262
+ requireString(invocation.model, `${label}[${index}].model`);
263
+ if (invocation.reasoning !== null) requireString(invocation.reasoning, `${label}[${index}].reasoning`);
264
+ if (invocation.sandbox !== null) requireString(invocation.sandbox, `${label}[${index}].sandbox`);
265
+ if (invocation.continuationId !== null) boundedString(invocation.continuationId, `${label}[${index}].continuationId`, 512);
266
+ if (!['fresh', 'reuse', 'rotate'].includes(/** @type {string} */ (invocation.continuationMode))) {
267
+ throw new TypeError(`${label}[${index}].continuationMode is invalid`);
268
+ }
269
+ if (invocation.revision !== undefined) nonNegativeInteger(invocation.revision, `${label}[${index}].revision`);
270
+ if (invocation.cycle !== undefined) nonNegativeInteger(invocation.cycle, `${label}[${index}].cycle`);
271
+ for (const key of ["promptPath", "stdoutPath", "stderrPath", "executable"]) {
272
+ if (invocation[key] !== null) requireString(invocation[key], `${label}[${index}].${key}`);
273
+ }
274
+ if (invocation.snapshotPath !== undefined) requireString(invocation.snapshotPath, `${label}[${index}].snapshotPath`);
275
+ for (const key of ["startedAt", "updatedAt", "deadlineAt"]) requireTimestamp(invocation[key], `${label}[${index}].${key}`);
276
+ if (invocation.closedAt !== null) requireTimestamp(invocation.closedAt, `${label}[${index}].closedAt`);
277
+ if (!Number.isInteger(invocation.exitCode) && invocation.exitCode !== null) throw new TypeError(`${label}[${index}].exitCode must be an integer or null`);
278
+ if (invocation.signal !== null) requireString(invocation.signal, `${label}[${index}].signal`);
279
+ if (! ["active", "closed", "terminated"].includes(/** @type {string} */ (invocation.status))) throw new TypeError(`${label}[${index}].status is invalid`);
280
+ if (invocation.usage !== undefined) validateInvocationUsage(invocation.usage, `${label}[${index}].usage`);
281
+ if (invocation.usageEstimated !== undefined && typeof invocation.usageEstimated !== "boolean") {
282
+ throw new TypeError(`${label}[${index}].usageEstimated must be a boolean`);
283
+ }
284
+ if (invocation.costUsd !== undefined && invocation.costUsd !== null) nonNegativeNumber(invocation.costUsd, `${label}[${index}].costUsd`);
285
+ // Provenance is only ever explicit when it is `priced`; a number costUsd
286
+ // with no field is a harness-reported `provider`, and no costUsd is
287
+ // `unknown`. Encoding either of those in the field would let a persisted
288
+ // record claim a provenance it never earned.
289
+ if (invocation.costProvenance !== undefined && invocation.costProvenance !== "priced") {
290
+ throw new TypeError(`${label}[${index}].costProvenance must be "priced" when present`);
291
+ }
292
+ }
293
+ }
294
+ /**
295
+ * @param {unknown} value
296
+ * @param {string} label
297
+ */
298
+ function validateInvocationUsage(value, label) {
299
+ assertObject(value, label);
300
+ rejectUnknown(value, USAGE_FIELDS, label);
301
+ for (const key of USAGE_FIELDS) {
302
+ if (value[key] !== null) nonNegativeInteger(value[key], `${label}.${key}`);
303
+ }
304
+ }
305
+ /**
306
+ * @param {unknown} value
307
+ * @param {string} label
308
+ */
309
+ function validateExecutionOverrides(value, label) {
310
+ if (!Array.isArray(value)) throw new TypeError(`${label} must be an array`);
311
+ const overrides = /** @type {JsonObject[]} */ (value);
312
+ for (const [index, override] of overrides.entries()) {
313
+ assertObject(override, `${label}[${index}]`);
314
+ rejectUnknown(override, new Set(["kind", "at", "reason", "timeoutSec", "decision", "invocationId", "phase", "result", "usage", "costUsd", "text"]), `${label}[${index}]`);
315
+ requireString(override.kind, `${label}[${index}].kind`);
316
+ requireTimestamp(override.at, `${label}[${index}].at`);
317
+ requireString(override.reason, `${label}[${index}].reason`);
318
+ if (override.timeoutSec !== undefined) positiveNumber(override.timeoutSec, `${label}[${index}].timeoutSec`);
319
+ if (override.decision !== undefined) requireString(override.decision, `${label}[${index}].decision`);
320
+ if (override.invocationId !== undefined) requireString(override.invocationId, `${label}[${index}].invocationId`);
321
+ if (override.phase !== undefined) requireString(override.phase, `${label}[${index}].phase`);
322
+ if (override.result !== undefined && override.result !== null) requireString(override.result, `${label}[${index}].result`);
323
+ if (override.usage !== undefined) validateInvocationUsage(override.usage, `${label}[${index}].usage`);
324
+ if (override.costUsd !== undefined && override.costUsd !== null) nonNegativeNumber(override.costUsd, `${label}[${index}].costUsd`);
325
+ if (override.text !== undefined) boundedString(override.text, `${label}[${index}].text`, OPERATOR_ANSWER_MAX_BYTES);
326
+ }
327
+ }
328
+ /**
329
+ * @param {unknown} value
330
+ * @param {string} label
331
+ */
332
+ function validateGateResult(value, label) {
333
+ if (value === null) return;
334
+ assertObject(value, label);
335
+ rejectUnknown(value, GATE_RESULT_FIELDS, label);
336
+ const verdict = value.verdict;
337
+ const maxSeverity = value.maxSeverity;
338
+ if (!GATE_VERDICTS.has(/** @type {string} */ (verdict))) throw new TypeError(`${label}.verdict is invalid`);
339
+ if (!["none", "minor", "major", "critical"].includes(/** @type {string} */ (maxSeverity))) {
340
+ throw new TypeError(`${label}.maxSeverity is invalid`);
341
+ }
342
+ // An invalid verdict is the record that the judge produced nothing usable:
343
+ // it carries no finding the node could be rejected with.
344
+ if (verdict === "invalid_judge_output" && (maxSeverity !== "none" || /** @type {unknown[]} */ (value.findings).length > 0)) {
345
+ throw new TypeError(`${label} with verdict invalid_judge_output records no findings`);
346
+ }
347
+ if (typeof value.summary !== "string") throw new TypeError(`${label}.summary must be a string`);
348
+ if (Buffer.byteLength(value.summary, "utf8") > 4 * 1024) throw new TypeError(`${label}.summary exceeds 4096 bytes`);
349
+ if (!Array.isArray(value.findings)) throw new TypeError(`${label}.findings must be an array`);
350
+ if (value.findings.length > 32) throw new TypeError(`${label}.findings must have at most 32 items`);
351
+ const rank = { none: 0, minor: 1, major: 2, critical: 3 };
352
+ let actualMax = "none";
353
+ const findings = /** @type {JsonObject[]} */ (value.findings);
354
+ for (const [index, finding] of findings.entries()) {
355
+ assertObject(finding, `${label}.findings[${index}]`);
356
+ rejectUnknown(finding, FINDING_FIELDS, `${label}.findings[${index}]`);
357
+ const severity = finding.severity;
358
+ if (typeof severity !== "string" || !["minor", "major", "critical"].includes(severity)) {
359
+ throw new TypeError(`${label}.findings[${index}].severity is invalid`);
360
+ }
361
+ if (typeof finding.description !== "string") {
362
+ throw new TypeError(`${label}.findings[${index}].description must be a string`);
363
+ }
364
+ if (typeof finding.evidence !== "string") {
365
+ throw new TypeError(`${label}.findings[${index}].evidence must be a string`);
366
+ }
367
+ if (Buffer.byteLength(finding.description, "utf8") > 2 * 1024 || Buffer.byteLength(finding.evidence, "utf8") > 4 * 1024) {
368
+ throw new TypeError(`${label}.findings[${index}] exceeds evidence limits`);
369
+ }
370
+ const severityKey = /** @type {"minor"|"major"|"critical"} */ (severity);
371
+ if (rank[severityKey] > rank[/** @type {keyof typeof rank} */ (actualMax)]) actualMax = severityKey;
372
+ }
373
+ if (actualMax !== maxSeverity) throw new TypeError(`${label}.maxSeverity does not match findings`);
374
+ // An invalid verdict is not a pass, yet it records no severity either: only a
375
+ // real arbitrated verdict is held to the pass/none pairing.
376
+ if (verdict !== "invalid_judge_output" && (verdict === "pass") !== (maxSeverity === "none")) {
377
+ throw new TypeError(`${label}.verdict and maxSeverity are inconsistent`);
378
+ }
379
+ }
380
+ /**
381
+ * @param {unknown} value
382
+ * @param {string} label
383
+ */
384
+ function validateSnapshotError(value, label) {
385
+ if (value === null) return;
386
+ assertObject(value, label);
387
+ rejectUnknown(value, ERROR_FIELDS, label);
388
+ requireString(value.code, `${label}.code`);
389
+ requireString(value.message, `${label}.message`);
390
+ if (value.exhaustedUntil !== undefined && value.exhaustedUntil !== null) requireTimestamp(value.exhaustedUntil, `${label}.exhaustedUntil`);
391
+ }
392
+ /**
393
+ * The one persisted session policy: `forceFresh` starts the next dispatch in a
394
+ * provider session of its own instead of a rediscovered continuation. Only
395
+ * `true` is meaningful; a false or absent flag is the ordinary reuse path.
396
+ *
397
+ * @param {unknown} value
398
+ * @param {string} label
399
+ */
400
+ function validateSessionPolicy(value, label) {
401
+ assertObject(value, label);
402
+ rejectUnknown(value, new Set(["forceFresh"]), label);
403
+ if (value.forceFresh !== undefined && typeof value.forceFresh !== "boolean") {
404
+ throw new TypeError(`${label}.forceFresh must be a boolean`);
405
+ }
406
+ }
407
+ /**
408
+ * @param {unknown} value
409
+ * @param {string} label
410
+ */
411
+ function validateUsage(value, label) {
412
+ assertObject(value, label);
413
+ rejectUnknown(value, USAGE_FIELDS, label);
414
+ for (const key of USAGE_FIELDS) nonNegativeInteger(value[key], `${label}.${key}`);
415
+ }
416
+ /**
417
+ * The durable evidence of one tier-exhaustion generation: which role it
418
+ * belongs to and which candidates were tried, each with the reset instant the
419
+ * provider announced (null when it announced none).
420
+ *
421
+ * @param {unknown} value
422
+ * @param {string} label
423
+ */
424
+ function validateTierExhaustion(value, label) {
425
+ assertObject(value, label);
426
+ rejectUnknown(value, new Set(["role", "candidates"]), label);
427
+ if (value.role !== "worker" && value.role !== "judge") throw new TypeError(`${label}.role is invalid`);
428
+ if (!Array.isArray(value.candidates)) throw new TypeError(`${label}.candidates must be an array`);
429
+ for (const [index, candidate] of value.candidates.entries()) {
430
+ assertObject(candidate, `${label}.candidates[${index}]`);
431
+ rejectUnknown(candidate, new Set(["runtimeId", "exhaustedUntil"]), `${label}.candidates[${index}]`);
432
+ requireId(candidate.runtimeId, `${label}.candidates[${index}].runtimeId`);
433
+ if (candidate.exhaustedUntil !== null) requireTimestamp(candidate.exhaustedUntil, `${label}.candidates[${index}].exhaustedUntil`);
434
+ }
435
+ }
436
+ /**
437
+ * @param {unknown} value
438
+ * @param {string} label
439
+ */
440
+ function validateRoutingState(value, label) {
441
+ assertObject(value, label);
442
+ rejectUnknown(value, new Set(["history", "currentOverride", "assignments", "availability", "tierExhaustion", "tierExhaustionCycle"]), label);
443
+ if (!Array.isArray(value.history) || value.history.length > MAX_ROUTING_HISTORY) {
444
+ throw new TypeError(`${label}.history must be an array with at most ${MAX_ROUTING_HISTORY} items`);
445
+ }
446
+ for (const [index, entry] of value.history.entries()) {
447
+ validateRoutingEntry(entry, `${label}.history[${index}]`, false);
448
+ }
449
+ if (value.currentOverride !== null) validateRoutingEntry(value.currentOverride, `${label}.currentOverride`, true);
450
+ if (value.tierExhaustion !== undefined) validateTierExhaustion(value.tierExhaustion, `${label}.tierExhaustion`);
451
+ if (value.tierExhaustionCycle !== undefined) nonNegativeInteger(value.tierExhaustionCycle, `${label}.tierExhaustionCycle`);
452
+ if (value.assignments !== undefined) {
453
+ assertObject(value.assignments, `${label}.assignments`);
454
+ rejectUnknown(value.assignments, new Set(["worker", "judge", "composedWorker", "composedJudge"]), `${label}.assignments`);
455
+ requireId(value.assignments.worker, `${label}.assignments.worker`);
456
+ requireId(value.assignments.judge, `${label}.assignments.judge`);
457
+ for (const key of ["composedWorker", "composedJudge"]) {
458
+ if (value.assignments[key] !== undefined && typeof value.assignments[key] !== "boolean") throw new TypeError(`${label}.assignments.${key} must be boolean`);
459
+ }
460
+ }
461
+ if (value.availability !== undefined) {
462
+ assertObject(value.availability, `${label}.availability`);
463
+ for (const [id, availability] of Object.entries(value.availability)) {
464
+ requireId(id, `${label}.availability runtime`);
465
+ assertObject(availability, `${label}.availability.${id}`);
466
+ rejectUnknown(availability, new Set(["available", "exhaustedUntil", "reason"]), `${label}.availability.${id}`);
467
+ if (typeof availability.available !== "boolean") throw new TypeError(`${label}.availability.${id}.available must be boolean`);
468
+ if (availability.exhaustedUntil !== undefined && availability.exhaustedUntil !== null) requireTimestamp(availability.exhaustedUntil, `${label}.availability.${id}.exhaustedUntil`);
469
+ requireString(availability.reason, `${label}.availability.${id}.reason`);
470
+ }
471
+ }
472
+ }
473
+ /**
474
+ * @param {unknown} value
475
+ * @param {string} label
476
+ * @param {boolean} override
477
+ */
478
+ function validateRoutingEntry(value, label, override) {
479
+ assertObject(value, label);
480
+ const fields = override
481
+ ? new Set(["at", "role", "runtime", "nextRuntime", "rule", "ruleIndex", "revision", "hop", "reason", "backoffSec", "backoffUntil", "usage", "costUsd", "costProvenance"])
482
+ : new Set(["at", "role", "runtime", "nextRuntime", "rule", "ruleIndex", "revision", "hop", "status", "errorCode", "backoffSec", "backoffUntil", "usage", "costUsd", "costProvenance"]);
483
+ rejectUnknown(value, fields, label);
484
+ requireTimestamp(value.at, `${label}.at`);
485
+ if (value.role !== "worker" && value.role !== "judge") throw new TypeError(`${label}.role is invalid`);
486
+ requireId(value.runtime, `${label}.runtime`);
487
+ if (value.nextRuntime !== undefined) requireId(value.nextRuntime, `${label}.nextRuntime`);
488
+ if (value.rule !== undefined) nonNegativeInteger(value.rule, `${label}.rule`);
489
+ if (value.ruleIndex !== undefined) nonNegativeInteger(value.ruleIndex, `${label}.ruleIndex`);
490
+ if (value.revision !== undefined) nonNegativeInteger(value.revision, `${label}.revision`);
491
+ if (value.hop !== undefined) nonNegativeInteger(value.hop, `${label}.hop`);
492
+ if (override) {
493
+ boundedString(value.reason, `${label}.reason`, 2 * 1024);
494
+ } else {
495
+ if (value.status !== undefined && !NODE_STATUSES.has(/** @type {string} */ (value.status))) {
496
+ throw new TypeError(`${label}.status is invalid`);
497
+ }
498
+ if (value.errorCode !== undefined) boundedString(value.errorCode, `${label}.errorCode`, 256);
499
+ }
500
+ if (value.backoffSec !== undefined) nonNegativeNumber(value.backoffSec, `${label}.backoffSec`);
501
+ if (value.backoffUntil !== undefined) requireTimestamp(value.backoffUntil, `${label}.backoffUntil`);
502
+ if (value.usage !== undefined) validateInvocationUsage(value.usage, `${label}.usage`);
503
+ if (value.costUsd !== undefined && value.costUsd !== null) nonNegativeNumber(value.costUsd, `${label}.costUsd`);
504
+ // Provenance is explicit only when it is `priced`; a provider-reported
505
+ // number leaves the field absent in a routing entry, exactly as it does on
506
+ // an invocation.
507
+ if (value.costProvenance !== undefined && value.costProvenance !== "priced") {
508
+ throw new TypeError(`${label}.costProvenance must be "priced" when present`);
509
+ }
510
+ }
511
+ /**
512
+ * @param {unknown} value
513
+ * @param {string} label
514
+ */
515
+ function validateProgressState(value, label) {
516
+ assertObject(value, label);
517
+ rejectUnknown(value, new Set(["revision", "heartbeatCount", "dryHeartbeatCount", "progressSignature", "lastHeartbeatAt", "lastProgressAt", "nextCheckAt"]), label);
518
+ if (value.revision !== undefined) nonNegativeInteger(value.revision, `${label}.revision`);
519
+ const heartbeatCount = nonNegativeInteger(value.heartbeatCount, `${label}.heartbeatCount`);
520
+ const dryHeartbeatCount = nonNegativeInteger(value.dryHeartbeatCount, `${label}.dryHeartbeatCount`);
521
+ if (dryHeartbeatCount > heartbeatCount) throw new TypeError(`${label}.dryHeartbeatCount cannot exceed heartbeatCount`);
522
+ if (heartbeatCount > 1_000_000 || dryHeartbeatCount > 1_000_000) throw new TypeError(`${label} heartbeat counts are out of bounds`);
523
+ if (value.lastHeartbeatAt !== null) requireTimestamp(value.lastHeartbeatAt, `${label}.lastHeartbeatAt`);
524
+ if (value.lastProgressAt !== null) requireTimestamp(value.lastProgressAt, `${label}.lastProgressAt`);
525
+ if (value.progressSignature !== undefined && value.progressSignature !== null) boundedString(value.progressSignature, `${label}.progressSignature`, 256);
526
+ if (value.nextCheckAt !== undefined && value.nextCheckAt !== null) requireTimestamp(value.nextCheckAt, `${label}.nextCheckAt`);
527
+ }
528
+ /**
529
+ * @param {unknown} value
530
+ * @param {string} label
531
+ */
532
+ function validateWorktreeState(value, label) {
533
+ assertObject(value, label);
534
+ rejectUnknown(value, new Set(["status", "path", "branch", "commit", "baseSha", "sealedSha", "sealError", "previousAttempt"]), label);
535
+ if (!["unassigned", "provisioning", "ready", "failed", "removed"].includes(/** @type {string} */ (value.status))) {
536
+ throw new TypeError(`${label}.status is invalid`);
537
+ }
538
+ for (const [key, maxBytes] of /** @type {[string, number][]} */ ([['path', 4096], ['branch', 512], ['commit', 256], ['baseSha', 256], ['sealedSha', 256], ['sealError', 1024]])) {
539
+ if (value[key] !== undefined && value[key] !== null) boundedString(value[key], `${label}.${key}`, maxBytes);
540
+ }
541
+ if (value.previousAttempt !== undefined && value.previousAttempt !== null) positiveInteger(value.previousAttempt, `${label}.previousAttempt`);
542
+ }
543
+ /**
544
+ * @param {JsonObject} value
545
+ * @param {ValidatedNode} node
546
+ */
547
+ function validateSnapshotBinding(value, node) {
548
+ if (value.id !== node.id) throw new TypeError(`node snapshot.id does not match contract node ${node.id}`);
549
+ if (value.type !== node.type) throw new TypeError(`node snapshot.type does not match contract node ${node.id}`);
550
+ if (value.packetHash !== node.packetHash) throw new TypeError(`node snapshot.packetHash does not match contract node ${node.id}`);
551
+ if (stableJson(value.sourceIdentity) !== stableJson(node.sourceIdentity)) {
552
+ throw new TypeError(`node snapshot.sourceIdentity does not match contract node ${node.id}`);
553
+ }
554
+ }
555
+ /**
556
+ * @param {unknown} value
557
+ */
558
+ function validateScopeSnapshot(value) {
559
+ assertObject(value, "node snapshot.scope");
560
+ rejectUnknown(value, new Set([
561
+ "changedPaths", "unexpectedPaths", "changedPathCount", "unexpectedPathCount", "truncated", "boundary",
562
+ ]), "node snapshot.scope");
563
+ for (const key of ["changedPaths", "unexpectedPaths"]) {
564
+ const paths = /** @type {unknown[]} */ (value[key]);
565
+ if (!Array.isArray(value[key]) || paths.length > 64 || paths.some((path) => typeof path !== "string")) {
566
+ throw new TypeError(`node snapshot.scope.${key} is invalid`);
567
+ }
568
+ if (paths.some((path) => Buffer.byteLength(/** @type {string} */ (path), "utf8") > 1024)) {
569
+ throw new TypeError(`node snapshot.scope.${key} contains an oversized path`);
570
+ }
571
+ }
572
+ for (const key of ["changedPathCount", "unexpectedPathCount"]) {
573
+ if (value[key] !== undefined) nonNegativeInteger(value[key], `node snapshot.scope.${key}`);
574
+ }
575
+ if (value.truncated !== undefined && typeof value.truncated !== "boolean") throw new TypeError("node snapshot.scope.truncated is invalid");
576
+ if (value.boundary !== undefined && value.boundary !== null) validateScopeBoundarySnapshot(value.boundary, "node snapshot.scope.boundary");
577
+ }
578
+ /**
579
+ * An advisory finding recorded when a completed attempt's controller
580
+ * verification passed despite unexpected workspace writes (TECH-SPEC lean,
581
+ * rule 1). Never a terminal state.
582
+ *
583
+ * @param {unknown} value
584
+ */
585
+ function validateScopeFindings(value) {
586
+ assertObject(value, "node snapshot.scopeFindings");
587
+ rejectUnknown(value, new Set(["unexpectedPaths"]), "node snapshot.scopeFindings");
588
+ const paths = /** @type {unknown[]} */ (value.unexpectedPaths);
589
+ if (!Array.isArray(value.unexpectedPaths) || paths.length > MAX_SCOPE_FINDING_PATHS || paths.some((path) => typeof path !== "string")) {
590
+ throw new TypeError("node snapshot.scopeFindings.unexpectedPaths is invalid");
591
+ }
592
+ if (paths.some((path) => Buffer.byteLength(/** @type {string} */ (path), "utf8") > 1024)) {
593
+ throw new TypeError("node snapshot.scopeFindings.unexpectedPaths contains an oversized path");
594
+ }
595
+ }
596
+ /**
597
+ * @param {unknown} value
598
+ * @param {string} label
599
+ */
600
+ function validateScopeBoundarySnapshot(value, label) {
601
+ assertObject(value, label);
602
+ rejectUnknown(value, new Set(["schemaVersion", "files", "roots", "fileRoots", "fileOrigins", "rootOrigins"]), label);
603
+ if (value.schemaVersion !== 1 || !Array.isArray(value.files) || !Array.isArray(value.roots) || !Array.isArray(value.fileOrigins) || !Array.isArray(value.rootOrigins)) {
604
+ throw new TypeError(`${label} is malformed`);
605
+ }
606
+ // Declared roots that named a regular file at capture time: they authorize
607
+ // exactly that path, never the paths beneath it.
608
+ const fileRoots = value.fileRoots ?? [];
609
+ if (!Array.isArray(fileRoots)) throw new TypeError(`${label}.fileRoots is invalid`);
610
+ const total = value.files.length + value.roots.length + fileRoots.length + value.fileOrigins.length + value.rootOrigins.length;
611
+ if (total > 4096) throw new TypeError(`${label} is too large`);
612
+ /** @param {unknown} path @param {string} pathLabel */
613
+ const validPath = (path, pathLabel) => {
614
+ if (typeof path !== "string" || path.length === 0 || isAbsolute(path) || /^[A-Za-z]:[\\/]/u.test(path) || /(?:^|[\\/])\.\.(?:[\\/]|$)/u.test(path)) {
615
+ throw new TypeError(`${pathLabel} contains an invalid path`);
616
+ }
617
+ if (path === "." || /[\\/]$/u.test(path) || Buffer.byteLength(path, "utf8") > 1024) {
618
+ throw new TypeError(`${pathLabel} contains an invalid path`);
619
+ }
620
+ };
621
+ for (const [kind, paths] of [["files", value.files], ["roots", value.roots], ["fileRoots", fileRoots]]) {
622
+ for (const path of /** @type {unknown[]} */ (paths)) validPath(path, `${label}.${kind}`);
623
+ }
624
+ for (const path of /** @type {unknown[]} */ (fileRoots)) {
625
+ if (!/** @type {unknown[]} */ (value.roots).includes(path)) throw new TypeError(`${label}.fileRoots must be declared roots`);
626
+ }
627
+ for (const [kind, origins] of [["fileOrigins", value.fileOrigins], ["rootOrigins", value.rootOrigins]]) {
628
+ for (const [index, origin] of /** @type {unknown[]} */ (origins).entries()) {
629
+ assertObject(origin, `${label}.${kind}[${index}]`);
630
+ rejectUnknown(origin, new Set(["literal", "paths"]), `${label}.${kind}[${index}]`);
631
+ validPath(origin.literal, `${label}.${kind}[${index}].literal`);
632
+ if (!Array.isArray(origin.paths) || origin.paths.length === 0 || origin.paths.length > 4096) {
633
+ throw new TypeError(`${label}.${kind}[${index}].paths is invalid`);
634
+ }
635
+ for (const path of /** @type {unknown[]} */ (origin.paths)) validPath(path, `${label}.${kind}[${index}].paths`);
636
+ if (!origin.paths.includes(origin.literal)) throw new TypeError(`${label}.${kind}[${index}] must include its literal path`);
637
+ }
638
+ }
639
+ }