engineering-behavior-observatory 0.2.1

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 (178) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +151 -0
  3. package/contracts/codex-app-server-0.150.1/manifest.json +25 -0
  4. package/contracts/codex-app-server-0.150.1/schema/InitializeParams.json +84 -0
  5. package/contracts/codex-app-server-0.150.1/schema/JSONRPCMessage.json +137 -0
  6. package/contracts/codex-app-server-0.150.1/schema/v2/ThreadReadParams.json +17 -0
  7. package/contracts/codex-app-server-0.150.1/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
  8. package/contracts/codex-app-server-0.150.1/schema/v2/TurnInterruptParams.json +17 -0
  9. package/contracts/codex-app-server-0.150.1/types/AskForApproval.ts +5 -0
  10. package/contracts/codex-app-server-0.150.1/types/ClientNotification.ts +5 -0
  11. package/contracts/codex-app-server-0.150.1/types/SandboxMode.ts +5 -0
  12. package/contracts/codex-app-server-0.150.1/types/ThreadReadParams.ts +9 -0
  13. package/contracts/codex-app-server-0.150.1/types/TokenUsageBreakdown.ts +5 -0
  14. package/contracts/codex-app-server-0.150.1/types/TurnInterruptParams.ts +5 -0
  15. package/contracts/codex-app-server-0.153.4/manifest.json +27 -0
  16. package/contracts/codex-app-server-0.153.4/schema/JSONRPCMessage.json +137 -0
  17. package/contracts/codex-app-server-0.153.4/schema/v1/InitializeParams.json +84 -0
  18. package/contracts/codex-app-server-0.153.4/schema/v2/ThreadReadParams.json +17 -0
  19. package/contracts/codex-app-server-0.153.4/schema/v2/ThreadStartParams.json +515 -0
  20. package/contracts/codex-app-server-0.153.4/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
  21. package/contracts/codex-app-server-0.153.4/schema/v2/TurnInterruptParams.json +17 -0
  22. package/contracts/codex-app-server-0.153.4/schema/v2/TurnStartParams.json +921 -0
  23. package/contracts/codex-app-server-0.153.4/types/AskForApproval.ts +5 -0
  24. package/contracts/codex-app-server-0.153.4/types/ClientNotification.ts +5 -0
  25. package/contracts/codex-app-server-0.153.4/types/SandboxMode.ts +5 -0
  26. package/contracts/codex-app-server-0.153.4/types/ThreadReadParams.ts +12 -0
  27. package/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.ts +5 -0
  28. package/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.ts +5 -0
  29. package/contracts/openhands-agent-server-v1.44.1.json +65 -0
  30. package/contracts/openhands-agent-server-v1.46.0.json +46 -0
  31. package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.d.ts +9 -0
  32. package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.js +2 -0
  33. package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.d.ts +3 -0
  34. package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.js +2 -0
  35. package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.d.ts +1 -0
  36. package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.js +2 -0
  37. package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.d.ts +10 -0
  38. package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.js +2 -0
  39. package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.d.ts +8 -0
  40. package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.js +2 -0
  41. package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.d.ts +4 -0
  42. package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.js +2 -0
  43. package/dist/src/agent-sdk-normalizer.d.ts +22 -0
  44. package/dist/src/agent-sdk-normalizer.js +995 -0
  45. package/dist/src/agent-sdk-run.d.ts +31 -0
  46. package/dist/src/agent-sdk-run.js +242 -0
  47. package/dist/src/agent-sdk-runner.d.ts +96 -0
  48. package/dist/src/agent-sdk-runner.js +407 -0
  49. package/dist/src/agent-sdk.d.ts +294 -0
  50. package/dist/src/agent-sdk.js +644 -0
  51. package/dist/src/aggregation.d.ts +183 -0
  52. package/dist/src/aggregation.js +513 -0
  53. package/dist/src/artifacts.d.ts +36 -0
  54. package/dist/src/artifacts.js +1372 -0
  55. package/dist/src/atlas-grafana.d.ts +377 -0
  56. package/dist/src/atlas-grafana.js +47 -0
  57. package/dist/src/atlas-html.d.ts +2 -0
  58. package/dist/src/atlas-html.js +34 -0
  59. package/dist/src/atlas.d.ts +130 -0
  60. package/dist/src/atlas.js +235 -0
  61. package/dist/src/behavior-assertions.d.ts +95 -0
  62. package/dist/src/behavior-assertions.js +154 -0
  63. package/dist/src/cli.d.ts +5 -0
  64. package/dist/src/cli.js +957 -0
  65. package/dist/src/codex-judge.d.ts +44 -0
  66. package/dist/src/codex-judge.js +214 -0
  67. package/dist/src/codex-run.d.ts +114 -0
  68. package/dist/src/codex-run.js +519 -0
  69. package/dist/src/codex.d.ts +187 -0
  70. package/dist/src/codex.js +1028 -0
  71. package/dist/src/contracts.d.ts +155 -0
  72. package/dist/src/contracts.js +866 -0
  73. package/dist/src/corpus.d.ts +67 -0
  74. package/dist/src/corpus.js +540 -0
  75. package/dist/src/cursor-sdk-runner.d.ts +81 -0
  76. package/dist/src/cursor-sdk-runner.js +362 -0
  77. package/dist/src/cursor-sdk.d.ts +81 -0
  78. package/dist/src/cursor-sdk.js +1078 -0
  79. package/dist/src/deepseek-adapter.d.ts +152 -0
  80. package/dist/src/deepseek-adapter.js +777 -0
  81. package/dist/src/exports.d.ts +69 -0
  82. package/dist/src/exports.js +966 -0
  83. package/dist/src/human-calibration.d.ts +167 -0
  84. package/dist/src/human-calibration.js +618 -0
  85. package/dist/src/index.d.ts +71 -0
  86. package/dist/src/index.js +38 -0
  87. package/dist/src/lifecycle.d.ts +196 -0
  88. package/dist/src/lifecycle.js +1889 -0
  89. package/dist/src/normalization-integrity.d.ts +128 -0
  90. package/dist/src/normalization-integrity.js +429 -0
  91. package/dist/src/openhands-run.d.ts +36 -0
  92. package/dist/src/openhands-run.js +415 -0
  93. package/dist/src/openhands.d.ts +130 -0
  94. package/dist/src/openhands.js +859 -0
  95. package/dist/src/pi.d.ts +203 -0
  96. package/dist/src/pi.js +1345 -0
  97. package/dist/src/process-protocol.d.ts +245 -0
  98. package/dist/src/process-protocol.js +1261 -0
  99. package/dist/src/retained-evidence.d.ts +12 -0
  100. package/dist/src/retained-evidence.js +201 -0
  101. package/dist/src/run-bundles.d.ts +193 -0
  102. package/dist/src/run-bundles.js +993 -0
  103. package/dist/src/scheduler.d.ts +109 -0
  104. package/dist/src/scheduler.js +1080 -0
  105. package/dist/src/semantic-judge.d.ts +194 -0
  106. package/dist/src/semantic-judge.js +875 -0
  107. package/dist/src/structural-observations.d.ts +124 -0
  108. package/dist/src/structural-observations.js +671 -0
  109. package/dist/src/task-packets.d.ts +115 -0
  110. package/dist/src/task-packets.js +683 -0
  111. package/dist/src/uniform-events.d.ts +120 -0
  112. package/dist/src/uniform-events.js +158 -0
  113. package/dist/src/verifiers.d.ts +116 -0
  114. package/dist/src/verifiers.js +819 -0
  115. package/dist/src/workspaces.d.ts +56 -0
  116. package/dist/src/workspaces.js +1314 -0
  117. package/docs/README.md +60 -0
  118. package/docs/development/README.md +71 -0
  119. package/docs/development/documentation-sync.md +58 -0
  120. package/docs/development/extension-contracts.md +307 -0
  121. package/docs/evaluation/README.md +48 -0
  122. package/docs/evaluation/aggregation.md +112 -0
  123. package/docs/evaluation/behavior-assertions.md +45 -0
  124. package/docs/evaluation/human-calibration.md +170 -0
  125. package/docs/evaluation/normalization-integrity.md +80 -0
  126. package/docs/evaluation/semantic-judge.md +188 -0
  127. package/docs/evaluation/structural-observations.md +84 -0
  128. package/docs/evaluation/uniform-events.md +131 -0
  129. package/docs/guides/atlas.md +144 -0
  130. package/docs/guides/evidence-and-sharing.md +100 -0
  131. package/docs/guides/operator-guide.md +424 -0
  132. package/docs/guides/quickstart.md +152 -0
  133. package/docs/guides/telemetry.md +66 -0
  134. package/docs/harnesses/README.md +31 -0
  135. package/docs/harnesses/claude-agent-sdk.md +61 -0
  136. package/docs/harnesses/codex-harness.md +174 -0
  137. package/docs/harnesses/cursor-sdk.md +149 -0
  138. package/docs/harnesses/deepseek-harness.md +134 -0
  139. package/docs/harnesses/openhands-agent-server.md +153 -0
  140. package/docs/harnesses/pi-sdk.md +66 -0
  141. package/docs/reference/README.md +19 -0
  142. package/docs/reference/agent-sdk-operational-runner.md +118 -0
  143. package/docs/reference/cli.md +114 -0
  144. package/docs/reference/contracts.md +222 -0
  145. package/docs/reference/run-bundle-contract.md +354 -0
  146. package/docs/reference/run-lifecycle.md +54 -0
  147. package/examples/cursor-sdk/README.md +11 -0
  148. package/examples/cursor-sdk/capture-profile.json +1 -0
  149. package/examples/cursor-sdk/harness.json +1 -0
  150. package/examples/cursor-sdk/model.json +1 -0
  151. package/examples/cursor-sdk/native-limits.json +1 -0
  152. package/examples/cursor-sdk/native-tool-policy.json +1 -0
  153. package/ontology/behavior-categories.v1.json +46 -0
  154. package/package.json +66 -0
  155. package/release/0.1.0/KNOWN_LIMITATIONS.md +35 -0
  156. package/release/0.1.0/README.md +46 -0
  157. package/release/0.1.0/reproducibility.json +61 -0
  158. package/release/0.2.0/KNOWN_LIMITATIONS.md +40 -0
  159. package/release/0.2.0/README.md +42 -0
  160. package/release/0.2.0/reproducibility.json +73 -0
  161. package/release/0.2.1/KNOWN_LIMITATIONS.md +18 -0
  162. package/release/0.2.1/README.md +63 -0
  163. package/release/0.2.1/reproducibility.json +73 -0
  164. package/release/README.md +13 -0
  165. package/schemas/aggregation.v1.json +293 -0
  166. package/schemas/behavior-assertions.v1.json +220 -0
  167. package/schemas/deepseek-runtime-composition.v1.schema.json +122 -0
  168. package/schemas/experiment.v1.schema.json +188 -0
  169. package/schemas/human-calibration.v1.json +336 -0
  170. package/schemas/normalization-integrity.v1.json +302 -0
  171. package/schemas/run-bundles/v1.json +1040 -0
  172. package/schemas/run-queue.v1.schema.json +206 -0
  173. package/schemas/semantic-judge.v1.json +380 -0
  174. package/schemas/structural-observations.v1.json +131 -0
  175. package/schemas/task-packet-freeze.v1.schema.json +106 -0
  176. package/schemas/task-packet.v1.schema.json +234 -0
  177. package/schemas/uniform-events/v1.json +289 -0
  178. package/scripts/atlas-grafana.sh +18 -0
@@ -0,0 +1,993 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { cp, lstat, mkdir, mkdtemp, readdir, rm, rmdir, utimes, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join, resolve, sep } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { HOOK_EVENTS } from "@anthropic-ai/claude-agent-sdk";
8
+ import { assertNoDuplicateJsonKeys, assertUniqueArtifactIdentities, canonicalizeMetadata, inspectRetainedArtifact, readVerifiedArtifact, validateArtifact, validateExportManifest, validateRunManifestEvidence, writeArtifactAtomically, writeMetadataAtomically, } from "./artifacts.js";
9
+ import { isSafeArtifactRelativePath } from "./contracts.js";
10
+ import { digestWorkspace, digestWorkspaceTree } from "./verifiers.js";
11
+ import { readBoundedFile } from "./scheduler.js";
12
+ const execFileAsync = promisify(execFile);
13
+ const MAX_WORKSPACE_PATCH_BYTES = 64 * 1024 * 1024;
14
+ const MAX_WORKSPACE_SNAPSHOT_BYTES = 128 * 1024 * 1024;
15
+ const MAX_QUALIFICATION_ARTIFACT_BYTES = 64 * 1024 * 1024;
16
+ const QUALIFICATION_DIMENSION_RANK = { qualified: 0, unsupported: 1, gap: 2, unqualified: 3 };
17
+ const PINNED_HOOK_EVENTS = new Set(HOOK_EVENTS);
18
+ const TAR_COMMAND = process.platform === "win32" ? "tar" : "/usr/bin/tar";
19
+ const PROVISIONAL_TERMINAL = {
20
+ state: "interrupted",
21
+ failureClass: "infrastructure",
22
+ stopReason: "none",
23
+ };
24
+ const AUTHORITIES = {
25
+ session: "semantic",
26
+ hook: "semantic",
27
+ telemetry: "timing-resource",
28
+ workspace: "outcome",
29
+ verifier: "outcome",
30
+ };
31
+ export async function createRunBundleAssembler(definition) {
32
+ const assembler = new RunBundleAssembler(definition);
33
+ await assembler.initialize();
34
+ return assembler;
35
+ }
36
+ export class RunBundleAssembler {
37
+ bundleRoot;
38
+ bundleId;
39
+ attempt;
40
+ run;
41
+ configuration;
42
+ evidence = [];
43
+ manifest;
44
+ revision = 0;
45
+ finalized = false;
46
+ captureMissing = [];
47
+ constructor(definition) {
48
+ this.bundleRoot = resolve(definition.bundleRoot);
49
+ this.bundleId = definition.bundleId;
50
+ this.run = structuredClone(definition.run);
51
+ this.attempt = structuredClone(definition.attempt);
52
+ this.configuration = structuredClone(definition.configuration);
53
+ }
54
+ async initialize() {
55
+ if (this.manifest !== undefined)
56
+ throw new Error("Run-bundle assembler is already initialized.");
57
+ await mkdir(this.bundleRoot, { recursive: true, mode: 0o700 });
58
+ try {
59
+ await lstat(join(this.bundleRoot, "manifest.json"));
60
+ throw new Error("Run bundle already has a manifest.");
61
+ }
62
+ catch (error) {
63
+ if (error.code !== "ENOENT")
64
+ throw error;
65
+ }
66
+ await this.checkpoint([], this.run, PROVISIONAL_TERMINAL);
67
+ }
68
+ currentManifest() {
69
+ if (this.manifest === undefined)
70
+ throw new Error("Run-bundle assembler is not initialized.");
71
+ return structuredClone(this.manifest);
72
+ }
73
+ async registerArtifact(input) {
74
+ this.assertOpen();
75
+ if (!isSafeArtifactRelativePath(input.relativePath) || input.relativePath.toLowerCase() === "manifest.json") {
76
+ throw new Error(`Artifact path "${input.relativePath}" is unsafe or reserved.`);
77
+ }
78
+ const inspected = await inspectRetainedArtifact(this.bundleRoot, input.relativePath);
79
+ const descriptor = {
80
+ id: input.id,
81
+ source: input.source,
82
+ kind: input.kind,
83
+ authority: AUTHORITIES[input.kind],
84
+ mediaType: input.mediaType,
85
+ digest: digestString(inspected.digest.value),
86
+ sizeBytes: inspected.sizeBytes,
87
+ sharingClass: input.sharingClass,
88
+ relativePath: input.relativePath,
89
+ ...(input.fingerprint === undefined ? {} : { fingerprint: input.fingerprint }),
90
+ ...(input.nativeReference === undefined ? {} : { nativeReference: structuredClone(input.nativeReference) }),
91
+ };
92
+ const candidateEvidence = [...this.evidence, descriptor];
93
+ assertUniqueArtifactIdentities(candidateEvidence);
94
+ const candidateRun = runWithNativeReference(this.run, descriptor);
95
+ await this.checkpoint(candidateEvidence, candidateRun, PROVISIONAL_TERMINAL);
96
+ return structuredClone(descriptor);
97
+ }
98
+ async writeJsonArtifact(input, document) {
99
+ this.assertOpen();
100
+ await writeMetadataAtomically(this.bundleRoot, input.relativePath, document, undefined, { overwrite: false });
101
+ return this.registerArtifact(input);
102
+ }
103
+ async writeAgentSdkTelemetry(input) {
104
+ if (input.evidence.telemetry === undefined && input.evidence.usage === undefined) {
105
+ throw new Error("Agent SDK telemetry evidence requires telemetry or SDK-reported usage.");
106
+ }
107
+ const previousMissing = this.captureMissing;
108
+ const receipt = input.evidence.telemetry?.receipt;
109
+ this.captureMissing = previousMissing.filter((entry) => !["telemetry", "telemetry-receipt"].includes(entry.kind));
110
+ if (input.evidence.telemetry === undefined || receipt?.status !== "received") {
111
+ const reason = receipt?.status === "not-checked"
112
+ ? "not-checked"
113
+ : receipt?.status === "missing" && receipt.reason === "process-interrupted"
114
+ ? "process-interrupted"
115
+ : "not-collected";
116
+ this.captureMissing = deduplicateMissing([
117
+ ...this.captureMissing,
118
+ { kind: receipt === undefined ? "telemetry" : "telemetry-receipt", reason, affects: ["timing-resource"] },
119
+ ]);
120
+ }
121
+ try {
122
+ return await this.writeJsonArtifact({
123
+ id: input.id ?? "telemetry",
124
+ source: "anthropic-agent-sdk",
125
+ kind: "telemetry",
126
+ mediaType: "application/json",
127
+ sharingClass: "restricted",
128
+ relativePath: input.relativePath ?? "telemetry/agent-sdk.json",
129
+ ...(input.traceId === undefined ? {} : { nativeReference: { type: "trace", id: input.traceId } }),
130
+ }, {
131
+ schemaVersion: "ebo.agent-sdk-telemetry/v1",
132
+ attemptId: this.attempt.id,
133
+ ...(input.evidence.telemetry === undefined ? {} : { telemetry: structuredClone(input.evidence.telemetry) }),
134
+ ...(input.evidence.usage === undefined ? {} : { usage: structuredClone(input.evidence.usage) }),
135
+ });
136
+ }
137
+ catch (error) {
138
+ this.captureMissing = previousMissing;
139
+ throw error;
140
+ }
141
+ }
142
+ async captureWorkspaceOutcome(options, whileProjected) {
143
+ this.assertOpen();
144
+ return withWorkspaceOutcomeProjection(options, async (startPath, capturedPath) => {
145
+ const [fingerprint, treeDigest] = await Promise.all([
146
+ digestWorkspace(capturedPath),
147
+ digestWorkspaceTree(capturedPath),
148
+ ]);
149
+ const patch = await workspacePatch(startPath, capturedPath, treeDigest);
150
+ if (await digestWorkspace(capturedPath) !== fingerprint) {
151
+ throw new Error("Final workspace metadata changed while its outcome was being captured.");
152
+ }
153
+ const format = patch === undefined ? "snapshot" : "patch";
154
+ const content = patch ?? await workspaceSnapshot(capturedPath, treeDigest);
155
+ const relativePath = format === "patch"
156
+ ? options.relativePath ?? "workspace.patch"
157
+ : options.snapshotRelativePath ?? "workspace.tar.gz";
158
+ await writeArtifactAtomically(this.bundleRoot, relativePath, content, undefined, { overwrite: false });
159
+ const descriptor = await this.registerArtifact({
160
+ id: options.id ?? "workspace",
161
+ source: options.source ?? `workspace-${format}`,
162
+ kind: "workspace",
163
+ mediaType: format === "patch" ? "text/x-diff" : "application/gzip",
164
+ sharingClass: "restricted",
165
+ relativePath,
166
+ fingerprint,
167
+ });
168
+ const outcome = { descriptor, fingerprint, treeDigest, format };
169
+ await whileProjected?.(capturedPath, outcome);
170
+ return outcome;
171
+ });
172
+ }
173
+ async finalize(input) {
174
+ this.assertOpen();
175
+ const terminal = structuredClone(input.terminal);
176
+ const missingEvidence = input.missingEvidence ?? [];
177
+ await this.checkpoint(this.evidence, this.run, terminal, missingEvidence, input.qualification);
178
+ if (input.qualification !== undefined) {
179
+ const structuralQualification = await qualifyRunBundle(this.bundleRoot, input.qualification);
180
+ await this.checkpoint(this.evidence, this.run, terminal, missingEvidence, input.qualification, structuralQualification);
181
+ }
182
+ this.finalized = true;
183
+ return this.currentManifest();
184
+ }
185
+ assertOpen() {
186
+ if (this.manifest === undefined)
187
+ throw new Error("Run-bundle assembler is not initialized.");
188
+ if (this.finalized)
189
+ throw new Error("Run bundle is already finalized.");
190
+ }
191
+ async checkpoint(evidence, run, terminal, missingEvidence = [], qualification, structuralQualification) {
192
+ const report = captureReport(this.bundleId, run.assessmentMode, evidence, [
193
+ ...this.captureMissing,
194
+ ...missingEvidence,
195
+ ], qualification, structuralQualification);
196
+ assertValid("capture-report", report);
197
+ const reportPath = `capture/report-${String(this.revision).padStart(4, "0")}-${randomUUID()}.json`;
198
+ const reportDigest = await writeMetadataAtomically(this.bundleRoot, reportPath, report, undefined, { overwrite: false });
199
+ const reportDescriptor = {
200
+ id: "capture-report",
201
+ source: "ebo-capture",
202
+ kind: "capture-report",
203
+ authority: "capture",
204
+ mediaType: "application/json",
205
+ digest: digestString(reportDigest.value),
206
+ sizeBytes: Buffer.byteLength(canonicalizeMetadata(report)),
207
+ sharingClass: "internal",
208
+ relativePath: reportPath,
209
+ };
210
+ const manifest = {
211
+ schemaVersion: "run-manifest/v1",
212
+ bundleId: this.bundleId,
213
+ run: structuredClone(run),
214
+ attempt: structuredClone(this.attempt),
215
+ configuration: structuredClone(this.configuration),
216
+ terminal: structuredClone(terminal),
217
+ evidence: [...structuredClone(evidence), reportDescriptor],
218
+ };
219
+ assertValid("manifest.json", manifest);
220
+ const evidenceErrors = validateRunManifestEvidence("manifest.json", manifest, this.bundleRoot);
221
+ if (evidenceErrors.length > 0)
222
+ throw new Error(formatValidationErrors(evidenceErrors));
223
+ await writeMetadataAtomically(this.bundleRoot, "manifest.json", manifest, undefined, {
224
+ overwrite: this.manifest !== undefined,
225
+ });
226
+ this.evidence = structuredClone(evidence);
227
+ this.run = structuredClone(run);
228
+ this.manifest = structuredClone(manifest);
229
+ this.revision += 1;
230
+ }
231
+ }
232
+ async function withWorkspaceOutcomeProjection(options, use) {
233
+ const startPath = resolve(options.startPath);
234
+ const finalPath = resolve(options.finalPath);
235
+ const exclusions = [...new Set(options.excludeDirectoryNames ?? [])];
236
+ for (const name of exclusions) {
237
+ if (name.includes("/") || name !== ".git" && !isSafeArtifactRelativePath(name)) {
238
+ throw new Error(`Workspace outcome exclusion "${name}" is invalid.`);
239
+ }
240
+ }
241
+ if (exclusions.length === 0 && options.respectGitignore !== true && options.omitEmptyDirectories !== true) {
242
+ return use(startPath, finalPath);
243
+ }
244
+ await assertNoWorkspaceHardLinks(finalPath, new Set(exclusions));
245
+ const filteredRoot = await mkdtemp(join(tmpdir(), "ebo-workspace-filter-"));
246
+ const projectedPath = join(filteredRoot, "workspace");
247
+ try {
248
+ await cp(finalPath, projectedPath, {
249
+ recursive: true,
250
+ preserveTimestamps: true,
251
+ force: false,
252
+ filter: async (source) => source === finalPath
253
+ || !await isExcludedWorkspaceDirectory(finalPath, source, exclusions),
254
+ });
255
+ if (options.respectGitignore === true)
256
+ await removeIgnoredWorkspaceEntries(startPath, projectedPath);
257
+ if (options.omitEmptyDirectories === true)
258
+ await removeEmptyDirectories(projectedPath);
259
+ await restoreProjectedDirectoryTimestamps(finalPath, projectedPath);
260
+ return await use(startPath, projectedPath);
261
+ }
262
+ finally {
263
+ await rm(filteredRoot, { recursive: true, force: true });
264
+ }
265
+ }
266
+ /** Structurally qualify one retained run bundle without normalizing native evidence. */
267
+ export async function qualifyRunBundle(bundleRoot, options = {}) {
268
+ const report = emptyQualificationReport();
269
+ let document;
270
+ try {
271
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(readBoundedFile(join(resolve(bundleRoot), "manifest.json"), "Run manifest"));
272
+ assertNoDuplicateJsonKeys(text);
273
+ document = JSON.parse(text);
274
+ }
275
+ catch (error) {
276
+ addQualificationReason(report, "attemptIdentity", "unqualified", "MANIFEST_INVALID", undefined, errorMessage(error));
277
+ return finishQualificationReport(report);
278
+ }
279
+ if (!isRecord(document) || !Array.isArray(document.evidence)) {
280
+ addQualificationReason(report, "attemptIdentity", "unqualified", "MANIFEST_INVALID", undefined, "Run manifest is not a record with evidence.");
281
+ return finishQualificationReport(report);
282
+ }
283
+ const manifest = document;
284
+ const assessmentMode = manifest.run?.assessmentMode ?? "verified";
285
+ report.assessmentMode = assessmentMode;
286
+ report.attempt = structuredClone(manifest.attempt);
287
+ report.terminal = structuredClone(manifest.terminal);
288
+ const manifestErrors = validateArtifact("manifest.json", manifest);
289
+ for (const error of manifestErrors) {
290
+ const dimension = error.field.startsWith("/attempt") ? "attemptIdentity"
291
+ : error.field.startsWith("/terminal") ? "terminal"
292
+ : "semanticEvidence";
293
+ addQualificationReason(report, dimension, "unqualified", dimension === "attemptIdentity" ? "ATTEMPT_IDENTITY_INVALID"
294
+ : dimension === "terminal" ? "TERMINAL_CLASSIFICATION_INVALID" : "MANIFEST_INVALID", undefined, `${error.field}: ${error.message}`);
295
+ }
296
+ if (manifestErrors.length > 0)
297
+ return finishQualificationReport(report);
298
+ const states = new Map();
299
+ for (const descriptor of manifest.evidence) {
300
+ const state = { descriptor, valid: true };
301
+ states.set(descriptor.id, state);
302
+ try {
303
+ const bytes = await readVerifiedArtifact(bundleRoot, descriptor.relativePath, digestValue(descriptor.digest), descriptor.kind === "workspace" && descriptor.mediaType === "application/gzip"
304
+ ? MAX_WORKSPACE_SNAPSHOT_BYTES
305
+ : MAX_QUALIFICATION_ARTIFACT_BYTES);
306
+ if (descriptor.mediaType === "application/x-ndjson") {
307
+ const summary = parseNativeJsonl(bytes);
308
+ state.hookNames = summary.hookNames;
309
+ state.sessionIds = summary.sessionIds;
310
+ }
311
+ else if (descriptor.mediaType === "application/json") {
312
+ const parsed = parseNativeJson(bytes);
313
+ if (descriptor.kind === "session" || descriptor.kind === "hook") {
314
+ const summary = nativeRecordSummary([parsed]);
315
+ state.hookNames = summary.hookNames;
316
+ state.sessionIds = summary.sessionIds;
317
+ }
318
+ else {
319
+ state.document = descriptor.kind === "telemetry" ? telemetrySummary(parsed) : parsed;
320
+ }
321
+ }
322
+ if (descriptor.kind === "workspace" && descriptor.mediaType === "text/x-diff")
323
+ state.bytes = bytes;
324
+ }
325
+ catch (error) {
326
+ state.valid = false;
327
+ const { dimension, code } = artifactFailure(descriptor, error);
328
+ addQualificationReason(report, dimension, "unqualified", code, descriptor.id, errorMessage(error));
329
+ }
330
+ }
331
+ if (!report.reasons.some((reason) => reason.code === "ARTIFACT_TOO_LARGE")) {
332
+ for (const error of validateRunManifestEvidence("manifest.json", manifest, bundleRoot)) {
333
+ const descriptor = descriptorFromValidationField(manifest, error.field);
334
+ const dimension = descriptor === undefined
335
+ ? error.field.startsWith("/terminal") ? "terminal" : "semanticEvidence"
336
+ : artifactFailure(descriptor, new Error(error.message)).dimension;
337
+ const code = dimension === "terminal" ? "TERMINAL_CLASSIFICATION_INVALID" : "ARTIFACT_INTEGRITY_INVALID";
338
+ addQualificationReason(report, dimension, "unqualified", code, descriptor?.id, `${error.field}: ${error.message}`);
339
+ }
340
+ }
341
+ const valid = (kind) => [...states.values()].filter((state) => state.descriptor.kind === kind && state.descriptor.sanitizedFrom === undefined && state.valid);
342
+ const sessions = valid("session");
343
+ const hookArtifacts = valid("hook");
344
+ const hooks = hookArtifacts.filter(({ hookNames }) => (hookNames?.length ?? 0) > 0);
345
+ const telemetry = valid("telemetry");
346
+ const workspaces = valid("workspace");
347
+ const verifiers = valid("verifier");
348
+ const captureReports = valid("capture-report");
349
+ const captureReport = captureReports[0]?.document;
350
+ const retainedSemanticEvidenceKinds = captureReportSemanticEvidenceKinds(captureReport);
351
+ const semanticEvidenceKinds = retainedSemanticEvidenceKinds
352
+ ?? options.semanticEvidenceKinds
353
+ ?? ["session", "hook"];
354
+ if (retainedSemanticEvidenceKinds !== undefined && options.semanticEvidenceKinds !== undefined
355
+ && !sameStringSet(retainedSemanticEvidenceKinds, options.semanticEvidenceKinds)) {
356
+ addQualificationReason(report, "semanticEvidence", "unqualified", "CAPTURE_REPORT_CONTRADICTS_SOURCE", captureReports[0]?.descriptor.id, "Caller semantic-evidence requirements contradict the retained capture report.");
357
+ }
358
+ const requiresSession = semanticEvidenceKinds.includes("session");
359
+ const requiresHooks = semanticEvidenceKinds.includes("hook");
360
+ if (requiresSession && sessions.length === 0) {
361
+ addQualificationReason(report, "semanticEvidence", "unqualified", "SESSION_EVIDENCE_MISSING", undefined, "No valid native session evidence is retained.");
362
+ }
363
+ const sessionId = manifest.run?.native?.sessionId;
364
+ if (typeof sessionId !== "string" || !sessions.some(({ descriptor }) => descriptor.nativeReference?.type === "session" && descriptor.nativeReference.id === sessionId)) {
365
+ addQualificationReason(report, "semanticEvidence", "unqualified", "SESSION_IDENTITY_MISSING", undefined, "Run session identity is not bound to retained session evidence.");
366
+ }
367
+ const nativeSessionIds = new Set(sessions.flatMap(({ sessionIds }) => sessionIds ?? []));
368
+ const retainedRelatedSessionIds = captureReportRelatedSessionIds(captureReport);
369
+ const relatedSessionIds = retainedRelatedSessionIds ?? options.relatedSessionIds ?? [];
370
+ if (retainedRelatedSessionIds !== undefined && options.relatedSessionIds !== undefined
371
+ && !sameStringSet(retainedRelatedSessionIds, options.relatedSessionIds)) {
372
+ addQualificationReason(report, "semanticEvidence", "unqualified", "CAPTURE_REPORT_CONTRADICTS_SOURCE", captureReports[0]?.descriptor.id, "Caller related-session identities contradict the retained capture report.");
373
+ }
374
+ const allowedSessionIds = new Set([sessionId, ...relatedSessionIds]);
375
+ if (typeof sessionId === "string" && (!nativeSessionIds.has(sessionId)
376
+ || [...nativeSessionIds].some((id) => !allowedSessionIds.has(id)))) {
377
+ addQualificationReason(report, "semanticEvidence", "unqualified", "SESSION_RECORD_IDENTITY_MISMATCH", undefined, "Retained native session records do not bind exclusively to the run session ID.");
378
+ }
379
+ for (const hook of hookArtifacts.filter(({ hookNames }) => hookNames?.length === 0)) {
380
+ addQualificationReason(report, "hooks", "unqualified", "HOOK_RECORDS_MISSING", hook.descriptor.id, "Hook evidence contains no recognized callback records.");
381
+ }
382
+ if (requiresHooks && hooks.length === 0) {
383
+ addQualificationReason(report, "semanticEvidence", "unqualified", "HOOK_EVIDENCE_MISSING", undefined, "No valid native hook evidence is retained.");
384
+ }
385
+ if (requiresHooks)
386
+ qualifyHooks(report, manifest, hooks, options);
387
+ if (captureMissingEvidence(captureReport).some((entry) => entry.kind === "event-log-completeness" && entry.reason === "not-checked")) {
388
+ addQualificationReason(report, "semanticEvidence", "gap", "EVENT_LOG_COMPLETENESS_UNPROVEN", captureReports[0]?.descriptor.id, "The pinned Agent Server API reconciliation cannot prove complete in-process EventLog delivery.");
389
+ }
390
+ qualifyTelemetry(report, telemetry, captureMissingEvidence(captureReport));
391
+ const captureWarnings = isRecord(captureReport) && isRecord(captureReport.agentSdk)
392
+ && isRecord(captureReport.agentSdk.captureWarnings)
393
+ ? captureReport.agentSdk.captureWarnings
394
+ : undefined;
395
+ if (typeof captureWarnings?.count === "number" && captureWarnings.count > 0) {
396
+ addQualificationReason(report, "hooks", "gap", "HOOK_CAPTURE_WARNING", captureReports[0]?.descriptor.id, `${String(captureWarnings.count)} hook callback${captureWarnings.count === 1 ? "" : "s"} could not be retained: ${String(captureWarnings.diagnostic ?? "no diagnostic")}`);
397
+ }
398
+ if (workspaces.length === 0) {
399
+ addQualificationReason(report, "workspace", "unqualified", "WORKSPACE_EVIDENCE_MISSING", undefined, "No valid workspace outcome is retained.");
400
+ }
401
+ for (const workspace of workspaces.filter(({ descriptor }) => descriptor.mediaType === "text/x-diff")) {
402
+ if (options.startingWorkspacePath === undefined) {
403
+ addQualificationReason(report, "workspace", "gap", "WORKSPACE_PATCH_NOT_CHECKED", workspace.descriptor.id, "Workspace patch applicability was not checked against its starting fixture.");
404
+ }
405
+ else if (!await workspacePatchApplies(options.startingWorkspacePath, workspace.bytes)) {
406
+ addQualificationReason(report, "workspace", "unqualified", "WORKSPACE_PATCH_UNUSABLE", workspace.descriptor.id, "Workspace patch does not reproduce the retained outcome.");
407
+ }
408
+ }
409
+ if (assessmentMode === "verified" && verifiers.length === 0) {
410
+ addQualificationReason(report, "verifier", "unqualified", "VERIFIER_EVIDENCE_MISSING", undefined, "No valid verifier result is retained.");
411
+ }
412
+ for (const verifier of verifiers) {
413
+ const status = isRecord(verifier.document) ? verifier.document.status : undefined;
414
+ if (status === "error") {
415
+ addQualificationReason(report, "verifier", "unqualified", "VERIFIER_RESULT_ERROR", verifier.descriptor.id, "Verifier infrastructure returned an error result.");
416
+ if (manifest.terminal?.failureClass === "task") {
417
+ addQualificationReason(report, "terminal", "unqualified", "INFRASTRUCTURE_FAILURE_MISCLASSIFIED_AS_TASK", verifier.descriptor.id, "Verifier infrastructure error cannot be a model task failure.");
418
+ }
419
+ }
420
+ else if (status === "not-run") {
421
+ addQualificationReason(report, "verifier", "unqualified", "VERIFIER_RESULT_NOT_RUN", verifier.descriptor.id, "Verifier was not run.");
422
+ }
423
+ }
424
+ if (manifest.evidence.some((descriptor) => descriptor.sharingClass === "unknown")) {
425
+ addQualificationReason(report, "sharing", "gap", "SHARING_CLASSIFICATION_UNKNOWN", undefined, "At least one retained artifact has unknown sharing classification.");
426
+ }
427
+ for (const exportState of valid("export-manifest")) {
428
+ const errors = validateExportManifest(exportState.descriptor.relativePath, exportState.document, manifest, bundleRoot);
429
+ for (const error of errors) {
430
+ addQualificationReason(report, "sharing", "unqualified", "EXPORT_MANIFEST_INVALID", exportState.descriptor.id, `${error.field}: ${error.message}`);
431
+ }
432
+ }
433
+ if (captureReports.length === 0) {
434
+ addQualificationReason(report, "semanticEvidence", "unqualified", "CAPTURE_REPORT_MISSING", undefined, "No valid capture report is retained.");
435
+ }
436
+ else {
437
+ for (const error of validateArtifact(captureReports[0].descriptor.relativePath, captureReport)) {
438
+ addQualificationReason(report, "semanticEvidence", "unqualified", "CAPTURE_REPORT_INVALID", captureReports[0].descriptor.id, `${error.field}: ${error.message}`);
439
+ }
440
+ crossCheckCaptureReport(report, captureReports[0].descriptor.id, captureReport, {
441
+ semantic: (!requiresSession || sessions.length > 0) && (!requiresHooks || hooks.length > 0),
442
+ timingResource: telemetry.length > 0 && telemetry.every(({ document }) => {
443
+ const receipt = isRecord(document) && isRecord(document.telemetry) ? document.telemetry.receipt : undefined;
444
+ return isRecord(receipt) && receipt.status === "received";
445
+ }),
446
+ outcome: workspaces.length > 0 && (assessmentMode === "observational" || verifiers.length > 0),
447
+ }, assessmentMode, semanticEvidenceKinds);
448
+ }
449
+ return finishQualificationReport(report);
450
+ }
451
+ function emptyQualificationReport() {
452
+ const dimension = () => ({ status: "qualified", reasonCodes: [] });
453
+ return {
454
+ status: "qualified",
455
+ semanticAnalysisUsable: true,
456
+ dimensions: {
457
+ attemptIdentity: dimension(), semanticEvidence: dimension(), hooks: dimension(), telemetry: dimension(),
458
+ workspace: dimension(), verifier: dimension(), terminal: dimension(), sharing: dimension(),
459
+ },
460
+ reasons: [],
461
+ };
462
+ }
463
+ function addQualificationReason(report, dimension, status, code, evidenceId, detail) {
464
+ if (report.reasons.some((reason) => reason.code === code && reason.dimension === dimension
465
+ && reason.evidenceId === evidenceId && reason.detail === detail))
466
+ return;
467
+ report.reasons.push({ code, dimension, ...(evidenceId === undefined ? {} : { evidenceId }), detail });
468
+ const target = report.dimensions[dimension];
469
+ if (!target.reasonCodes.includes(code))
470
+ target.reasonCodes.push(code);
471
+ if (QUALIFICATION_DIMENSION_RANK[status] > QUALIFICATION_DIMENSION_RANK[target.status])
472
+ target.status = status;
473
+ }
474
+ function finishQualificationReport(report) {
475
+ const statuses = Object.values(report.dimensions).map((dimension) => dimension.status);
476
+ report.status = statuses.includes("unqualified") ? "unqualified" : statuses.includes("gap") ? "qualified-with-gaps" : "qualified";
477
+ report.semanticAnalysisUsable = report.dimensions.attemptIdentity.status !== "unqualified"
478
+ && report.dimensions.semanticEvidence.status !== "unqualified"
479
+ && report.dimensions.hooks.status !== "unqualified";
480
+ return report;
481
+ }
482
+ function artifactFailure(descriptor, error) {
483
+ const dimension = descriptor.kind === "session" ? "semanticEvidence"
484
+ : descriptor.kind === "hook" ? "hooks"
485
+ : descriptor.kind === "telemetry" ? "telemetry"
486
+ : descriptor.kind === "workspace" ? "workspace"
487
+ : descriptor.kind === "verifier" || descriptor.kind === "diagnostic" ? "verifier"
488
+ : descriptor.kind === "export-manifest" ? "sharing" : "semanticEvidence";
489
+ const message = errorMessage(error);
490
+ const code = message.includes("qualification byte limit") ? "ARTIFACT_TOO_LARGE"
491
+ : descriptor.kind === "capture-report" ? "CAPTURE_REPORT_INVALID"
492
+ : descriptor.kind === "export-manifest" ? "EXPORT_MANIFEST_INVALID"
493
+ : descriptor.mediaType === "application/x-ndjson" && message.includes("empty") ? "NATIVE_JSONL_EMPTY"
494
+ : descriptor.mediaType === "application/x-ndjson" ? "NATIVE_JSONL_MALFORMED"
495
+ : descriptor.mediaType === "application/json" && ["session", "hook", "telemetry"].includes(descriptor.kind) && message.includes("empty") ? "NATIVE_JSON_EMPTY"
496
+ : descriptor.mediaType === "application/json" && ["session", "hook", "telemetry"].includes(descriptor.kind) ? "NATIVE_JSON_MALFORMED"
497
+ : "ARTIFACT_INTEGRITY_INVALID";
498
+ return { dimension, code };
499
+ }
500
+ function parseNativeJson(bytes) {
501
+ if (bytes.length === 0)
502
+ throw new Error("Native JSON evidence is empty.");
503
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
504
+ assertNoDuplicateJsonKeys(text);
505
+ let value;
506
+ try {
507
+ value = JSON.parse(text);
508
+ }
509
+ catch (error) {
510
+ throw new Error(`Native JSON evidence is malformed: ${errorMessage(error)}`);
511
+ }
512
+ if (!isRecord(value) || Object.keys(value).length === 0)
513
+ throw new Error("Native JSON evidence is empty.");
514
+ return value;
515
+ }
516
+ function parseNativeJsonl(bytes) {
517
+ if (bytes.length === 0)
518
+ throw new Error("Native JSONL evidence is empty.");
519
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
520
+ const lines = text.split(/\r?\n/u).filter((line) => line.trim() !== "");
521
+ if (lines.length === 0)
522
+ throw new Error("Native JSONL evidence is empty.");
523
+ return nativeRecordSummary(lines.map((line, index) => {
524
+ try {
525
+ assertNoDuplicateJsonKeys(line);
526
+ const value = JSON.parse(line);
527
+ if (!isRecord(value) || Object.keys(value).length === 0)
528
+ throw new Error("record is empty");
529
+ return value;
530
+ }
531
+ catch (error) {
532
+ throw new Error(`Native JSONL evidence line ${index + 1} is malformed: ${errorMessage(error)}`);
533
+ }
534
+ }));
535
+ }
536
+ function nativeRecordSummary(records) {
537
+ const hookNames = [];
538
+ const sessionIds = [];
539
+ for (const record of records) {
540
+ if (!isRecord(record))
541
+ continue;
542
+ const hook = typeof record.hook === "string" ? record.hook
543
+ : typeof record.hook_event_name === "string" ? record.hook_event_name
544
+ : typeof record.type === "string" ? record.type : undefined;
545
+ if (hook !== undefined && PINNED_HOOK_EVENTS.has(hook))
546
+ hookNames.push(hook);
547
+ for (const sessionId of [
548
+ record.sessionId,
549
+ record.session_id,
550
+ record.schemaVersion === "ebo.protocol-observation/v1" && record.source === "codex-app-server"
551
+ ? record.sourceIdentity : undefined,
552
+ isRecord(record.message) ? record.message.session_id : undefined,
553
+ ]) {
554
+ if (typeof sessionId === "string")
555
+ sessionIds.push(sessionId);
556
+ }
557
+ }
558
+ return { hookNames, sessionIds };
559
+ }
560
+ function qualifyHooks(report, manifest, hooks, options) {
561
+ const capabilities = options.hookCapabilities ?? options.agentSdkEvidence?.capabilities;
562
+ if (capabilities === undefined) {
563
+ addQualificationReason(report, "hooks", "gap", "HOOK_CAPABILITY_NOT_CHECKED", undefined, "Pinned TypeScript hook capabilities were not supplied.");
564
+ return;
565
+ }
566
+ const runtimeVersion = manifest.run.runtime.find((component) => component.name === "agent-sdk")?.version;
567
+ if (runtimeVersion !== capabilities.sdkVersion) {
568
+ addQualificationReason(report, "hooks", "unqualified", "HOOK_CAPABILITY_VERSION_MISMATCH", undefined, `Retained Agent SDK ${String(runtimeVersion)} does not match capability profile ${capabilities.sdkVersion}.`);
569
+ }
570
+ const observed = new Set(hooks.flatMap(({ hookNames }) => hookNames ?? []));
571
+ const supported = new Set(Object.keys(capabilities.hooks));
572
+ const unsupported = new Set(capabilities.unsupportedHooks);
573
+ for (const hook of options.expectedHooks ?? []) {
574
+ if (supported.has(hook) && !observed.has(hook)) {
575
+ addQualificationReason(report, "hooks", "unqualified", "HOOK_SUPPORTED_BUT_MISSING", undefined, `Pinned SDK supports expected hook ${hook}, but no callback was retained.`);
576
+ }
577
+ else if (unsupported.has(hook)) {
578
+ addQualificationReason(report, "hooks", "unsupported", "HOOK_UNSUPPORTED_BY_PINNED_SDK", undefined, `Pinned SDK does not support expected hook ${hook}.`);
579
+ }
580
+ else if (!supported.has(hook)) {
581
+ addQualificationReason(report, "hooks", "unqualified", "HOOK_CAPABILITY_CONTRADICTION", undefined, `Expected hook ${hook} is absent from the pinned capability profile.`);
582
+ }
583
+ }
584
+ for (const hook of observed) {
585
+ if (unsupported.has(hook)) {
586
+ addQualificationReason(report, "hooks", "unqualified", "HOOK_CAPABILITY_CONTRADICTION", undefined, `Hook ${hook} was observed although the pinned profile declares it unsupported.`);
587
+ }
588
+ }
589
+ }
590
+ function qualifyTelemetry(report, telemetry, missing) {
591
+ const optionalBeta = missing.some((entry) => entry.reason === "optional-beta-unavailable"
592
+ && Array.isArray(entry.affects) && entry.affects.includes("timing-resource"));
593
+ const unsupported = missing.some((entry) => entry.reason === "unsupported"
594
+ && Array.isArray(entry.affects) && entry.affects.includes("timing-resource"));
595
+ if (optionalBeta) {
596
+ addQualificationReason(report, "telemetry", "gap", "OPTIONAL_BETA_TIMING_UNAVAILABLE", undefined, "Optional detailed-beta timing is unavailable; semantic hook evidence remains usable.");
597
+ }
598
+ if (telemetry.length === 0) {
599
+ if (unsupported) {
600
+ addQualificationReason(report, "telemetry", "unsupported", "TELEMETRY_UNSUPPORTED_BY_PINNED_RUNTIME", undefined, "Pinned runtime explicitly does not support telemetry evidence.");
601
+ }
602
+ else if (!optionalBeta) {
603
+ addQualificationReason(report, "telemetry", "unqualified", "TELEMETRY_EVIDENCE_MISSING", undefined, "No valid telemetry evidence is retained.");
604
+ }
605
+ return;
606
+ }
607
+ for (const entry of telemetry) {
608
+ const receipt = isRecord(entry.document) && isRecord(entry.document.telemetry)
609
+ ? entry.document.telemetry.receipt : undefined;
610
+ if (!isRecord(receipt) || receipt.status !== "received") {
611
+ addQualificationReason(report, "telemetry", "gap", "TELEMETRY_RECEIPT_MISSING", undefined, `Collector receipt status is ${String(isRecord(receipt) ? receipt.status : "absent")}.`);
612
+ }
613
+ }
614
+ }
615
+ function telemetrySummary(value) {
616
+ const receipt = isRecord(value) && isRecord(value.telemetry) ? value.telemetry.receipt : undefined;
617
+ return { telemetry: { ...(isRecord(receipt) ? { receipt: structuredClone(receipt) } : {}) } };
618
+ }
619
+ async function workspacePatchApplies(startingWorkspacePath, patch) {
620
+ const temporaryRoot = await mkdtemp(join(tmpdir(), "ebo-qualification-patch-"));
621
+ const applied = join(temporaryRoot, "workspace");
622
+ const patchPath = join(temporaryRoot, "workspace.patch");
623
+ try {
624
+ await cp(resolve(startingWorkspacePath), applied, { recursive: true, preserveTimestamps: true, force: false });
625
+ if (patch.length > 0) {
626
+ await writeFile(patchPath, patch, { flag: "wx", mode: 0o600 });
627
+ await execFileAsync("git", ["apply", "--check", "--binary", patchPath], { cwd: applied });
628
+ await execFileAsync("git", ["apply", "--binary", patchPath], { cwd: applied });
629
+ }
630
+ return true;
631
+ }
632
+ catch {
633
+ return false;
634
+ }
635
+ finally {
636
+ await rm(temporaryRoot, { recursive: true, force: true });
637
+ }
638
+ }
639
+ function crossCheckCaptureReport(report, evidenceId, value, actual, assessmentMode = "verified", semanticEvidenceKinds = ["session", "hook"]) {
640
+ if (!isRecord(value) || !isRecord(value.capabilities))
641
+ return;
642
+ if ((value.assessmentMode ?? "verified") !== assessmentMode) {
643
+ addQualificationReason(report, "terminal", "unqualified", "CAPTURE_REPORT_CONTRADICTS_SOURCE", evidenceId, "Capture report assessment mode contradicts the run manifest.");
644
+ }
645
+ for (const [area, available] of Object.entries(actual)) {
646
+ const capability = value.capabilities[area];
647
+ const claimsAvailable = isRecord(capability) && capability.status === "available";
648
+ const claimsUnsupported = isRecord(capability) && capability.status === "unsupported";
649
+ if (!available && claimsAvailable || available && claimsUnsupported) {
650
+ const dimension = area === "semantic" ? "semanticEvidence" : area === "timingResource" ? "telemetry" : "workspace";
651
+ addQualificationReason(report, dimension, "unqualified", "CAPTURE_REPORT_CONTRADICTS_SOURCE", evidenceId, `Capture report ${area}=${String(isRecord(capability) ? capability.status : undefined)} contradicts retained source evidence.`);
652
+ }
653
+ }
654
+ const retainedKinds = new Set();
655
+ if (actual.semantic)
656
+ for (const kind of semanticEvidenceKinds)
657
+ retainedKinds.add(kind);
658
+ if (actual.timingResource)
659
+ retainedKinds.add("telemetry");
660
+ if (actual.outcome) {
661
+ retainedKinds.add("workspace");
662
+ if (assessmentMode === "verified")
663
+ retainedKinds.add("verifier");
664
+ }
665
+ for (const entry of captureMissingEvidence(value)) {
666
+ if (retainedKinds.has(entry.kind) && entry.reason !== "optional-beta-unavailable") {
667
+ const dimension = entry.kind === "telemetry" ? "telemetry" : ["workspace", "verifier"].includes(String(entry.kind)) ? "workspace" : "semanticEvidence";
668
+ addQualificationReason(report, dimension, "unqualified", "CAPTURE_REPORT_CONTRADICTS_SOURCE", evidenceId, `Capture report marks retained ${String(entry.kind)} evidence as missing.`);
669
+ }
670
+ }
671
+ }
672
+ function captureMissingEvidence(value) {
673
+ return isRecord(value) && Array.isArray(value.missingEvidence)
674
+ ? value.missingEvidence.filter(isRecord)
675
+ : [];
676
+ }
677
+ function captureReportSemanticEvidenceKinds(value) {
678
+ if (!isRecord(value) || !Array.isArray(value.semanticEvidenceKinds)
679
+ || value.semanticEvidenceKinds.length === 0
680
+ || new Set(value.semanticEvidenceKinds).size !== value.semanticEvidenceKinds.length
681
+ || !value.semanticEvidenceKinds.includes("session")
682
+ || value.semanticEvidenceKinds.some((kind) => kind !== "session" && kind !== "hook"))
683
+ return undefined;
684
+ return value.semanticEvidenceKinds;
685
+ }
686
+ function captureReportRelatedSessionIds(value) {
687
+ if (!isRecord(value) || !Array.isArray(value.relatedSessionIds)
688
+ || value.relatedSessionIds.some((id) => typeof id !== "string" || id.trim() === "")
689
+ || new Set(value.relatedSessionIds).size !== value.relatedSessionIds.length)
690
+ return undefined;
691
+ return value.relatedSessionIds;
692
+ }
693
+ function sameStringSet(left, right) {
694
+ return left.length === right.length && left.every((value) => right.includes(value));
695
+ }
696
+ function descriptorFromValidationField(manifest, field) {
697
+ if (!field.startsWith("/evidence/"))
698
+ return undefined;
699
+ const id = field.slice("/evidence/".length).split("/")[0]?.replaceAll("~1", "/").replaceAll("~0", "~");
700
+ return manifest.evidence.find((descriptor) => descriptor.id === id);
701
+ }
702
+ function digestValue(value) {
703
+ return { algorithm: "sha256", value: value.slice("sha256:".length) };
704
+ }
705
+ function errorMessage(error) {
706
+ return error instanceof Error ? error.message : String(error);
707
+ }
708
+ function isRecord(value) {
709
+ return typeof value === "object" && value !== null && !Array.isArray(value);
710
+ }
711
+ function captureReport(bundleId, assessmentMode, evidence, suppliedMissing, qualification, structuralQualification) {
712
+ const kinds = new Set(evidence.map((descriptor) => descriptor.kind));
713
+ const semanticEvidenceKinds = qualification?.semanticEvidenceKinds ?? ["session", "hook"];
714
+ const missing = deduplicateMissing([
715
+ ...structuredClone(suppliedMissing),
716
+ ...defaultMissing(kinds, suppliedMissing, assessmentMode, semanticEvidenceKinds),
717
+ ]);
718
+ const semantic = capability(semanticEvidenceKinds.every((kind) => kinds.has(kind)), "semantic", missing);
719
+ const timingResource = capability(kinds.has("telemetry"), "timing-resource", missing);
720
+ const outcome = capability(kinds.has("workspace") && (assessmentMode === "observational" || kinds.has("verifier")), "outcome", missing);
721
+ const qualified = semantic === "available"
722
+ && outcome === "available"
723
+ && (timingResource === "available" || timingResource === "unsupported")
724
+ && !missing.some((entry) => entry.affects.some((area) => area !== "timing-resource")
725
+ || entry.affects.includes("timing-resource")
726
+ && !["unsupported", "optional-beta-unavailable"].includes(entry.reason));
727
+ return {
728
+ schemaVersion: "capture-report/v1",
729
+ bundleId,
730
+ assessmentMode,
731
+ qualification: qualified ? "qualified" : "incomplete",
732
+ capabilities: {
733
+ semantic: { status: semantic },
734
+ timingResource: { status: timingResource },
735
+ outcome: { status: outcome },
736
+ },
737
+ missingEvidence: missing,
738
+ semanticEvidenceKinds: [...semanticEvidenceKinds],
739
+ ...(qualification?.relatedSessionIds === undefined ? {} : {
740
+ relatedSessionIds: [...new Set(qualification.relatedSessionIds)],
741
+ }),
742
+ ...(qualification?.workspaceOutcomeExcludedDirectoryNames === undefined ? {} : {
743
+ workspaceOutcomeExcludedDirectoryNames: [...qualification.workspaceOutcomeExcludedDirectoryNames],
744
+ }),
745
+ ...(qualification?.workspaceOutcomeRespectsGitignore === undefined ? {} : {
746
+ workspaceOutcomeRespectsGitignore: qualification.workspaceOutcomeRespectsGitignore,
747
+ }),
748
+ ...(qualification?.workspaceOutcomeOmitsEmptyDirectories === undefined ? {} : {
749
+ workspaceOutcomeOmitsEmptyDirectories: qualification.workspaceOutcomeOmitsEmptyDirectories,
750
+ }),
751
+ ...(qualification?.agentSdkEvidence === undefined ? {} : {
752
+ agentSdk: {
753
+ capabilities: structuredClone(qualification.agentSdkEvidence.capabilities),
754
+ effectiveConfiguration: structuredClone(qualification.agentSdkEvidence.effectiveConfiguration),
755
+ expectedHooks: [...(qualification.expectedHooks ?? [])],
756
+ captureWarnings: structuredClone(qualification.agentSdkEvidence.captureWarnings),
757
+ },
758
+ }),
759
+ ...(structuralQualification === undefined ? {} : {
760
+ structuralQualification: {
761
+ status: structuralQualification.status,
762
+ semanticAnalysisUsable: structuralQualification.semanticAnalysisUsable,
763
+ dimensions: structuredClone(structuralQualification.dimensions),
764
+ reasons: structuredClone(structuralQualification.reasons),
765
+ },
766
+ }),
767
+ };
768
+ }
769
+ function defaultMissing(kinds, supplied, assessmentMode, semanticEvidenceKinds = ["session", "hook"]) {
770
+ const expected = [
771
+ ...semanticEvidenceKinds.map((kind) => [kind, "semantic"]),
772
+ ["telemetry", "timing-resource"],
773
+ ["workspace", "outcome"],
774
+ ...(assessmentMode === "verified" ? [["verifier", "outcome"]] : []),
775
+ ];
776
+ return expected.flatMap(([kind, affects]) => kinds.has(kind)
777
+ || supplied.some((entry) => entry.kind === kind && entry.affects.includes(affects))
778
+ ? []
779
+ : [{ kind, reason: "not-collected", affects: [affects] }]);
780
+ }
781
+ function deduplicateMissing(entries) {
782
+ const unique = new Map(entries.map((entry) => [JSON.stringify(entry), entry]));
783
+ return [...unique.values()];
784
+ }
785
+ function capability(available, area, missing) {
786
+ const relevant = missing.filter((entry) => entry.affects.includes(area)
787
+ && entry.reason !== "optional-beta-unavailable");
788
+ if (available && relevant.length === 0)
789
+ return "available";
790
+ if (relevant.length > 0 && relevant.every((entry) => entry.reason === "unsupported"))
791
+ return "unsupported";
792
+ if (relevant.some((entry) => entry.reason === "not-checked"))
793
+ return "not-checked";
794
+ return "missing";
795
+ }
796
+ function runWithNativeReference(run, descriptor) {
797
+ const native = structuredClone(run.native ?? {});
798
+ if (descriptor.kind === "session" && descriptor.nativeReference?.type === "session") {
799
+ if (native.sessionId !== undefined && native.sessionId !== descriptor.nativeReference.id) {
800
+ throw new Error("Session evidence does not match the run's native session ID.");
801
+ }
802
+ native.sessionId = descriptor.nativeReference.id;
803
+ }
804
+ if (descriptor.kind === "telemetry" && descriptor.nativeReference?.type === "trace") {
805
+ if (native.traceId !== undefined && native.traceId !== descriptor.nativeReference.id) {
806
+ throw new Error("Telemetry evidence does not match the run's native trace ID.");
807
+ }
808
+ native.traceId = descriptor.nativeReference.id;
809
+ }
810
+ return { ...structuredClone(run), ...(Object.keys(native).length === 0 ? {} : { native }) };
811
+ }
812
+ async function removeIgnoredWorkspaceEntries(startPath, finalPath) {
813
+ const temporaryRoot = await mkdtemp(join(tmpdir(), "ebo-workspace-ignore-index-"));
814
+ const baseline = join(temporaryRoot, "baseline");
815
+ const globalExcludes = join(temporaryRoot, "global-excludes");
816
+ try {
817
+ await writeFile(globalExcludes, "");
818
+ await cp(startPath, baseline, { recursive: true, preserveTimestamps: true, force: false });
819
+ await execFileAsync("git", ["init", "--quiet"], { cwd: baseline });
820
+ await execFileAsync("git", ["add", "--force", "--all"], { cwd: baseline });
821
+ for (const relativePath of await ignoredWorkspacePaths(baseline, finalPath, globalExcludes)) {
822
+ await rm(join(finalPath, relativePath), { recursive: true, force: true });
823
+ }
824
+ }
825
+ finally {
826
+ await rm(temporaryRoot, { recursive: true, force: true });
827
+ }
828
+ }
829
+ async function assertNoWorkspaceHardLinks(directory, exclusions, prefix = "") {
830
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
831
+ const path = join(directory, entry.name);
832
+ const relativePath = `${prefix}${entry.name}`;
833
+ const metadata = await lstat(path, { bigint: true });
834
+ if (exclusions.has(entry.name) && metadata.isDirectory() && !metadata.isSymbolicLink())
835
+ continue;
836
+ if (metadata.isDirectory() && !metadata.isSymbolicLink()) {
837
+ await assertNoWorkspaceHardLinks(path, exclusions, `${relativePath}/`);
838
+ }
839
+ else if (metadata.isFile() && metadata.nlink > 1n) {
840
+ throw new Error(`Workspace contains a hard-linked file at "${relativePath}".`);
841
+ }
842
+ }
843
+ }
844
+ async function ignoredWorkspacePaths(baseline, finalPath, globalExcludes) {
845
+ const paths = [];
846
+ async function visit(directory, prefix = "") {
847
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
848
+ const relativePath = `${prefix}${entry.name}`;
849
+ paths.push(entry.isDirectory() ? `${relativePath}/` : relativePath);
850
+ if (entry.isDirectory())
851
+ await visit(join(directory, entry.name), `${relativePath}/`);
852
+ }
853
+ }
854
+ await visit(finalPath);
855
+ if (paths.length === 0)
856
+ return [];
857
+ return new Promise((resolvePaths, reject) => {
858
+ const child = spawn("git", ["-c", `core.excludesFile=${globalExcludes}`, "check-ignore", "--no-index", "-z", "--stdin"], {
859
+ cwd: baseline,
860
+ stdio: ["pipe", "pipe", "pipe"],
861
+ });
862
+ const stdout = [];
863
+ const stderr = [];
864
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
865
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
866
+ child.once("error", reject);
867
+ child.once("close", (code) => {
868
+ if (code !== 0 && code !== 1) {
869
+ reject(new Error(`Unable to apply starting workspace ignore rules: ${Buffer.concat(stderr).toString("utf8").trim()}`));
870
+ return;
871
+ }
872
+ resolvePaths(Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean));
873
+ });
874
+ child.stdin.end(Buffer.from(`${paths.join("\0")}\0`));
875
+ });
876
+ }
877
+ async function isExcludedWorkspaceDirectory(root, source, exclusions) {
878
+ let candidate = root;
879
+ for (const segment of source.slice(root.length + 1).split(sep)) {
880
+ candidate = join(candidate, segment);
881
+ if (exclusions.includes(segment) && (await lstat(candidate)).isDirectory())
882
+ return true;
883
+ }
884
+ return false;
885
+ }
886
+ async function removeEmptyDirectories(directory, root = directory) {
887
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
888
+ if (entry.isDirectory())
889
+ await removeEmptyDirectories(join(directory, entry.name), root);
890
+ }
891
+ if (directory !== root && (await readdir(directory)).length === 0)
892
+ await rmdir(directory);
893
+ }
894
+ async function restoreProjectedDirectoryTimestamps(source, projected) {
895
+ for (const entry of await readdir(projected, { withFileTypes: true })) {
896
+ if (entry.isDirectory()) {
897
+ await restoreProjectedDirectoryTimestamps(join(source, entry.name), join(projected, entry.name));
898
+ }
899
+ }
900
+ if (process.platform === "win32") {
901
+ const metadata = await lstat(source);
902
+ await utimes(projected, metadata.atime, metadata.mtime);
903
+ }
904
+ else {
905
+ await execFileAsync("/usr/bin/touch", ["-r", source, projected]);
906
+ }
907
+ }
908
+ async function workspacePatch(startPath, finalPath, finalTreeDigest) {
909
+ const temporaryRoot = await mkdtemp(join(tmpdir(), "ebo-workspace-patch-"));
910
+ const worktree = join(temporaryRoot, "worktree");
911
+ const applied = join(temporaryRoot, "applied");
912
+ const patchPath = join(temporaryRoot, "workspace.patch");
913
+ try {
914
+ await cp(startPath, worktree, { recursive: true, preserveTimestamps: true, force: false });
915
+ await execFileAsync("git", ["init", "--quiet"], { cwd: worktree });
916
+ await execFileAsync("git", ["add", "--force", "--all"], { cwd: worktree });
917
+ // A tree is enough for the diff. A commit can spawn detached maintenance
918
+ // that keeps writing .git/objects while the temporary directory is removed.
919
+ const { stdout: startingTree } = await execFileAsync("git", ["write-tree"], { cwd: worktree });
920
+ for (const entry of await readdir(worktree)) {
921
+ if (entry !== ".git")
922
+ await rm(join(worktree, entry), { recursive: true, force: true });
923
+ }
924
+ for (const entry of await readdir(finalPath)) {
925
+ if (entry === ".git")
926
+ throw new Error("Workspace patches cannot retain Git administrative state.");
927
+ await cp(join(finalPath, entry), join(worktree, entry), { recursive: true, preserveTimestamps: true, force: false });
928
+ }
929
+ await execFileAsync("git", ["add", "--force", "--intent-to-add", "--all"], { cwd: worktree });
930
+ let stdout;
931
+ try {
932
+ ({ stdout } = await execFileAsync("git", [
933
+ "diff", "--binary", "--full-index", "--no-ext-diff", "--no-renames", startingTree.trim(), "--", ".",
934
+ ], { cwd: worktree, encoding: "utf8", maxBuffer: MAX_WORKSPACE_PATCH_BYTES }));
935
+ }
936
+ catch (error) {
937
+ if (error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
938
+ return undefined;
939
+ throw error;
940
+ }
941
+ const patch = Buffer.from(stdout);
942
+ await cp(startPath, applied, { recursive: true, preserveTimestamps: true, force: false });
943
+ if (patch.length > 0) {
944
+ await writeFile(patchPath, patch, { flag: "wx", mode: 0o600 });
945
+ await execFileAsync("git", ["apply", "--binary", patchPath], { cwd: applied });
946
+ }
947
+ const appliedTreeDigest = await digestWorkspaceTree(applied);
948
+ if (appliedTreeDigest !== finalTreeDigest) {
949
+ return undefined;
950
+ }
951
+ return patch;
952
+ }
953
+ finally {
954
+ await rm(temporaryRoot, { recursive: true, force: true });
955
+ }
956
+ }
957
+ async function workspaceSnapshot(finalPath, finalTreeDigest) {
958
+ const temporaryRoot = await mkdtemp(join(tmpdir(), "ebo-workspace-snapshot-"));
959
+ const snapshotPath = join(temporaryRoot, "workspace.tar.gz");
960
+ const snapshotParent = join(temporaryRoot, "source");
961
+ const snapshotRoot = join(snapshotParent, "workspace");
962
+ const extracted = join(temporaryRoot, "extracted");
963
+ try {
964
+ await mkdir(snapshotParent);
965
+ await cp(finalPath, snapshotRoot, { recursive: true, preserveTimestamps: true, force: false });
966
+ const { stdout } = await execFileAsync(TAR_COMMAND, ["-czf", "-", "-C", snapshotParent, "workspace"], {
967
+ encoding: "buffer",
968
+ maxBuffer: MAX_WORKSPACE_SNAPSHOT_BYTES,
969
+ });
970
+ const snapshot = Buffer.from(stdout);
971
+ await writeFile(snapshotPath, snapshot, { flag: "wx", mode: 0o600 });
972
+ await mkdir(extracted, { mode: 0o700 });
973
+ await execFileAsync(TAR_COMMAND, ["-xzpf", snapshotPath, "-C", extracted]);
974
+ if (await digestWorkspaceTree(join(extracted, "workspace")) !== finalTreeDigest) {
975
+ throw new Error("Bounded workspace snapshot cannot reproduce the final workspace tree.");
976
+ }
977
+ return snapshot;
978
+ }
979
+ finally {
980
+ await rm(temporaryRoot, { recursive: true, force: true });
981
+ }
982
+ }
983
+ function assertValid(artifact, document) {
984
+ const errors = validateArtifact(artifact, document);
985
+ if (errors.length > 0)
986
+ throw new Error(formatValidationErrors(errors));
987
+ }
988
+ function formatValidationErrors(errors) {
989
+ return errors.map((error) => `${error.artifact} ${error.field}: ${error.message}`).join("\n");
990
+ }
991
+ function digestString(value) {
992
+ return `sha256:${value}`;
993
+ }