engineering-behavior-observatory 0.2.2 → 0.2.4

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.
@@ -20,14 +20,14 @@ export declare function validateArtifact(artifact: string, document: unknown): A
20
20
  export declare function validateRunManifestEvidence(artifact: string, manifest: unknown, bundleRoot: string): ArtifactValidationError[];
21
21
  export declare function validateExportManifest(artifact: string, exportManifest: unknown, containingManifest: unknown | undefined, bundleRoot?: string): ArtifactValidationError[];
22
22
  export declare function readVerifiedArtifact(artifactRoot: string, relativePath: string, expectedDigest: Digest, maxBytes?: number): Promise<Buffer>;
23
- export declare function inspectRetainedArtifact(artifactRoot: string, relativePath: string): Promise<{
23
+ export declare function inspectRetainedArtifact(artifactRoot: string, relativePath: string, maxBytes?: number): Promise<{
24
24
  digest: Digest;
25
25
  sizeBytes: number;
26
26
  }>;
27
27
  export declare function writeMetadataAtomically(artifactRoot: string, relativePath: string, metadata: unknown, signal?: AbortSignal, options?: {
28
28
  overwrite?: boolean;
29
29
  }): Promise<Digest>;
30
- export declare function writeArtifactAtomically(artifactRoot: string, relativePath: string, content: Uint8Array, signal?: AbortSignal, options?: {
30
+ export declare function writeArtifactAtomically(artifactRoot: string, relativePath: string, content: Uint8Array | AsyncIterable<Uint8Array>, signal?: AbortSignal, options?: {
31
31
  overwrite?: boolean;
32
32
  }): Promise<Digest>;
33
33
  export declare function writeMetadataAtomicallyIfAbsentSync(artifactRoot: string, relativePath: string, metadata: unknown, rootHandle?: BundleRootHandle, beforeDestinationPublish?: () => void, afterDestinationPublish?: () => void): {
@@ -852,22 +852,35 @@ export async function readVerifiedArtifact(artifactRoot, relativePath, expectedD
852
852
  await handle.close();
853
853
  }
854
854
  }
855
- export async function inspectRetainedArtifact(artifactRoot, relativePath) {
855
+ export async function inspectRetainedArtifact(artifactRoot, relativePath, maxBytes) {
856
+ if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes < 0)) {
857
+ throw new Error("Artifact byte limit must be a nonnegative safe integer.");
858
+ }
856
859
  const { path } = await resolveExistingArtifactPath(artifactRoot, relativePath);
857
- return inspectExistingPath(path, relativePath);
860
+ return inspectExistingPath(path, relativePath, maxBytes);
858
861
  }
859
862
  export async function writeMetadataAtomically(artifactRoot, relativePath, metadata, signal, options = {}) {
860
863
  const bytes = Buffer.from(canonicalizeMetadata(metadata));
861
864
  return writeArtifactAtomically(artifactRoot, relativePath, bytes, signal, options);
862
865
  }
863
866
  export async function writeArtifactAtomically(artifactRoot, relativePath, content, signal, options = {}) {
864
- const bytes = Buffer.from(content);
867
+ const source = content instanceof Uint8Array ? [Buffer.from(content)] : content;
868
+ const hash = createHash("sha256");
869
+ async function* chunks() {
870
+ for await (const chunk of source) {
871
+ if (signal?.aborted)
872
+ throw new Error("Artifact metadata write interrupted.");
873
+ const bytes = Buffer.from(chunk);
874
+ hash.update(bytes);
875
+ yield bytes;
876
+ }
877
+ }
865
878
  const { parent, path } = await prepareArtifactPath(artifactRoot, relativePath);
866
879
  const temporaryPath = resolve(parent, `.${randomUUID()}.tmp`);
867
880
  let handle;
868
881
  try {
869
882
  handle = await open(temporaryPath, "wx", 0o600);
870
- await handle.writeFile(bytes);
883
+ await handle.writeFile(chunks());
871
884
  await handle.sync();
872
885
  await handle.close();
873
886
  handle = undefined;
@@ -894,8 +907,10 @@ export async function writeArtifactAtomically(artifactRoot, relativePath, conten
894
907
  await handle?.close();
895
908
  await rm(temporaryPath, { force: true });
896
909
  }
897
- const digest = digestBytes(bytes);
898
- await readVerifiedArtifact(artifactRoot, relativePath, digest);
910
+ const digest = { algorithm: "sha256", value: hash.digest("hex") };
911
+ const inspected = await inspectRetainedArtifact(artifactRoot, relativePath);
912
+ if (inspected.digest.value !== digest.value)
913
+ throw new Error(`Artifact "${relativePath}" digest does not match its source reference.`);
899
914
  return digest;
900
915
  }
901
916
  export function writeMetadataAtomicallyIfAbsentSync(artifactRoot, relativePath, metadata, rootHandle, beforeDestinationPublish, afterDestinationPublish) {
@@ -1270,7 +1285,7 @@ function prepareArtifactPathSync(artifactRoot, relativePath) {
1270
1285
  function digestExistingPath(path, relativePath) {
1271
1286
  return inspectExistingPath(path, relativePath).digest;
1272
1287
  }
1273
- function inspectExistingPath(path, relativePath) {
1288
+ function inspectExistingPath(path, relativePath, maxBytes) {
1274
1289
  const descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
1275
1290
  try {
1276
1291
  let opened = fstatSync(descriptor);
@@ -1283,6 +1298,9 @@ function inspectExistingPath(path, relativePath) {
1283
1298
  || !Number.isSafeInteger(opened.size) || opened.size < 0) {
1284
1299
  throw new Error(`Artifact path "${relativePath}" is not an isolated regular file.`);
1285
1300
  }
1301
+ if (maxBytes !== undefined && opened.size > maxBytes) {
1302
+ throw new Error(`Artifact "${relativePath}" exceeds the qualification byte limit of ${maxBytes}.`);
1303
+ }
1286
1304
  const hash = createHash("sha256");
1287
1305
  const chunk = Buffer.allocUnsafe(64 * 1024);
1288
1306
  for (let offset = 0; offset < opened.size;) {
@@ -1,9 +1,12 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { createReadStream, createWriteStream } from "node:fs";
3
4
  import { cp, lstat, mkdir, mkdtemp, readdir, rm, rmdir, utimes, writeFile } from "node:fs/promises";
4
5
  import { tmpdir } from "node:os";
5
6
  import { join, resolve, sep } from "node:path";
6
7
  import { promisify } from "node:util";
8
+ import { Transform } from "node:stream";
9
+ import { pipeline } from "node:stream/promises";
7
10
  import { HOOK_EVENTS } from "@anthropic-ai/claude-agent-sdk";
8
11
  import { assertNoDuplicateJsonKeys, assertUniqueArtifactIdentities, canonicalizeMetadata, inspectRetainedArtifact, readVerifiedArtifact, validateArtifact, validateExportManifest, validateRunManifestEvidence, writeArtifactAtomically, writeMetadataAtomically, } from "./artifacts.js";
9
12
  import { isSafeArtifactRelativePath } from "./contracts.js";
@@ -11,7 +14,7 @@ import { digestWorkspace, digestWorkspaceTree } from "./verifiers.js";
11
14
  import { readBoundedFile } from "./scheduler.js";
12
15
  const execFileAsync = promisify(execFile);
13
16
  const MAX_WORKSPACE_PATCH_BYTES = 64 * 1024 * 1024;
14
- const MAX_WORKSPACE_SNAPSHOT_BYTES = 128 * 1024 * 1024;
17
+ const MAX_WORKSPACE_SNAPSHOT_BYTES = 1024 * 1024 * 1024;
15
18
  const MAX_QUALIFICATION_ARTIFACT_BYTES = 64 * 1024 * 1024;
16
19
  const QUALIFICATION_DIMENSION_RANK = { qualified: 0, unsupported: 1, gap: 2, unqualified: 3 };
17
20
  const PINNED_HOOK_EVENTS = new Set(HOOK_EVENTS);
@@ -151,11 +154,17 @@ export class RunBundleAssembler {
151
154
  throw new Error("Final workspace metadata changed while its outcome was being captured.");
152
155
  }
153
156
  const format = patch === undefined ? "snapshot" : "patch";
154
- const content = patch ?? await workspaceSnapshot(capturedPath, treeDigest);
155
157
  const relativePath = format === "patch"
156
158
  ? options.relativePath ?? "workspace.patch"
157
159
  : options.snapshotRelativePath ?? "workspace.tar.gz";
158
- await writeArtifactAtomically(this.bundleRoot, relativePath, content, undefined, { overwrite: false });
160
+ if (patch !== undefined) {
161
+ await writeArtifactAtomically(this.bundleRoot, relativePath, patch, undefined, { overwrite: false });
162
+ }
163
+ else {
164
+ await workspaceSnapshot(capturedPath, treeDigest, async (snapshotPath) => {
165
+ await writeArtifactAtomically(this.bundleRoot, relativePath, createReadStream(snapshotPath), undefined, { overwrite: false });
166
+ });
167
+ }
159
168
  const descriptor = await this.registerArtifact({
160
169
  id: options.id ?? "workspace",
161
170
  source: options.source ?? `workspace-${format}`,
@@ -168,6 +177,18 @@ export class RunBundleAssembler {
168
177
  const outcome = { descriptor, fingerprint, treeDigest, format };
169
178
  await whileProjected?.(capturedPath, outcome);
170
179
  return outcome;
180
+ }).then((outcome) => {
181
+ this.captureMissing = this.captureMissing.filter(({ kind }) => kind !== "workspace-capture-error");
182
+ return outcome;
183
+ }, (error) => {
184
+ // Keep the cause when a harness retains a partial bundle after packaging fails.
185
+ // Otherwise its terminal classification alone loses the actionable exception.
186
+ this.captureMissing = this.captureMissing.filter(({ kind }) => kind !== "workspace-capture-error");
187
+ this.captureMissing.push({
188
+ kind: "workspace-capture-error", reason: "not-collected", affects: ["outcome"],
189
+ detail: errorMessage(error),
190
+ });
191
+ throw error;
171
192
  });
172
193
  }
173
194
  async finalize(input) {
@@ -300,9 +321,15 @@ export async function qualifyRunBundle(bundleRoot, options = {}) {
300
321
  const state = { descriptor, valid: true };
301
322
  states.set(descriptor.id, state);
302
323
  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);
324
+ if (descriptor.kind === "workspace" && descriptor.mediaType === "application/gzip") {
325
+ // Opaque snapshots need streaming integrity checks, not a whole-archive buffer.
326
+ const inspected = await inspectRetainedArtifact(bundleRoot, descriptor.relativePath, MAX_WORKSPACE_SNAPSHOT_BYTES);
327
+ if (inspected.sizeBytes !== descriptor.sizeBytes || digestString(inspected.digest.value) !== descriptor.digest) {
328
+ throw new Error("Workspace snapshot size or digest does not match its descriptor.");
329
+ }
330
+ continue;
331
+ }
332
+ const bytes = await readVerifiedArtifact(bundleRoot, descriptor.relativePath, digestValue(descriptor.digest), MAX_QUALIFICATION_ARTIFACT_BYTES);
306
333
  if (descriptor.mediaType === "application/x-ndjson") {
307
334
  const summary = parseNativeJsonl(bytes);
308
335
  state.hookNames = summary.hookNames;
@@ -954,7 +981,7 @@ async function workspacePatch(startPath, finalPath, finalTreeDigest) {
954
981
  await rm(temporaryRoot, { recursive: true, force: true });
955
982
  }
956
983
  }
957
- async function workspaceSnapshot(finalPath, finalTreeDigest) {
984
+ async function workspaceSnapshot(finalPath, finalTreeDigest, use) {
958
985
  const temporaryRoot = await mkdtemp(join(tmpdir(), "ebo-workspace-snapshot-"));
959
986
  const snapshotPath = join(temporaryRoot, "workspace.tar.gz");
960
987
  const snapshotParent = join(temporaryRoot, "source");
@@ -963,18 +990,33 @@ async function workspaceSnapshot(finalPath, finalTreeDigest) {
963
990
  try {
964
991
  await mkdir(snapshotParent);
965
992
  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,
993
+ const child = spawn(TAR_COMMAND, ["-czf", "-", "-C", snapshotParent, "workspace"], { stdio: ["ignore", "pipe", "pipe"] });
994
+ let stderr = "";
995
+ child.stderr.on("data", (chunk) => { stderr = (stderr + chunk.toString("utf8")).slice(-8192); });
996
+ const exited = new Promise((resolveExit, reject) => {
997
+ child.once("error", reject);
998
+ child.once("close", (code) => code === 0 ? resolveExit() : reject(new Error(`Workspace snapshot tar failed (${code}): ${stderr.trim()}`)));
969
999
  });
970
- const snapshot = Buffer.from(stdout);
971
- await writeFile(snapshotPath, snapshot, { flag: "wx", mode: 0o600 });
1000
+ let size = 0;
1001
+ const limit = new Transform({ transform(chunk, _encoding, callback) {
1002
+ size += chunk.length;
1003
+ callback(size > MAX_WORKSPACE_SNAPSHOT_BYTES ? new Error("Workspace snapshot exceeds the 1 GiB compressed byte limit.") : null, chunk);
1004
+ } });
1005
+ const streamed = pipeline(child.stdout, limit, createWriteStream(snapshotPath, { flags: "wx", mode: 0o600 }));
1006
+ try {
1007
+ await Promise.all([exited, streamed]);
1008
+ }
1009
+ finally {
1010
+ if (child.exitCode === null)
1011
+ child.kill("SIGKILL");
1012
+ await Promise.allSettled([exited, streamed]);
1013
+ }
972
1014
  await mkdir(extracted, { mode: 0o700 });
973
1015
  await execFileAsync(TAR_COMMAND, ["-xzpf", snapshotPath, "-C", extracted]);
974
1016
  if (await digestWorkspaceTree(join(extracted, "workspace")) !== finalTreeDigest) {
975
1017
  throw new Error("Bounded workspace snapshot cannot reproduce the final workspace tree.");
976
1018
  }
977
- return snapshot;
1019
+ await use(snapshotPath);
978
1020
  }
979
1021
  finally {
980
1022
  await rm(temporaryRoot, { recursive: true, force: true });
@@ -41,6 +41,20 @@ events can still explain a stop. Keep terminal state, infrastructure failure,
41
41
  and capture quality separate. Preserve `retainedWorkspacePath` when outcome
42
42
  packaging failed; that path is a recovery location, not qualified evidence.
43
43
 
44
+ Capture reports retain packaging exceptions as `workspace-capture-error`
45
+ missing-evidence entries. Inspect the detail before retrying capture against a
46
+ retained workspace. A later successful capture is recovery evidence; it does
47
+ not change the original attempt's recorded failure.
48
+
49
+ Workspace capture tries a patch up to 64 MiB, then falls back to a snapshot
50
+ when the patch is too large or cannot reproduce the tree. Snapshots stream to
51
+ disk with a 1 GiB compressed limit and are extracted to verify the tree digest
52
+ before publication. Snapshot qualification also checks integrity without loading
53
+ the archive into memory. Exceeding the limit leaves explicit missing evidence
54
+ and retains the workspace; it never truncates the archive. These capture limits
55
+ are separate from portable-export limits. Configure known cache-directory
56
+ exclusions before a study rather than dropping files from an existing attempt.
57
+
44
58
  Use a new output destination for derived records and reruns. Do not edit a
45
59
  native bundle to make a validator accept it.
46
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engineering-behavior-observatory",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Capture engineering-agent trajectories, evaluate behavior, and inspect cited evidence across harnesses.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -46,6 +46,8 @@
46
46
  "release/0.2.0/",
47
47
  "release/0.2.1/",
48
48
  "release/0.2.2/",
49
+ "release/0.2.3/",
50
+ "release/0.2.4/",
49
51
  "schemas/",
50
52
  "scripts/atlas-grafana.sh"
51
53
  ],
@@ -0,0 +1,10 @@
1
+ # v0.2.3 known limitations
2
+
3
+ The [v0.2.2 limitations](../0.2.2/KNOWN_LIMITATIONS.md) continue to apply.
4
+
5
+ - Error details are retained for future captures. Exceptions discarded by older
6
+ versions cannot be reconstructed from a generic missing-workspace report.
7
+ - A later successful workspace capture is recovery evidence, not proof that the
8
+ original capture succeeded or that its transient failure cannot recur.
9
+ - The existing OTLP size bounds, missing-signal reporting, and bounded native
10
+ normalization projection are unchanged.
@@ -0,0 +1,25 @@
1
+ # v0.2.3: Workspace capture error diagnostics
2
+
3
+ The shared run-bundle assembler now preserves workspace packaging exceptions
4
+ in capture reports as `workspace-capture-error` missing-evidence entries.
5
+ Previously, a harness could finalize a partial bundle with only a generic
6
+ missing-workspace report, losing the exception needed to diagnose the failure.
7
+
8
+ Capture still rejects failures and preserves the source workspace through the
9
+ existing harness retention paths. A successful explicit capture retry clears
10
+ the prior diagnostic. No automatic retry, weaker validation, dependency update,
11
+ runtime-pin change, or artifact-schema change is introduced.
12
+
13
+ This release fixes diagnostic loss. It does not claim to eliminate every
14
+ workspace packaging failure. Existing immutable bundles are not rewritten.
15
+
16
+ ## Verification
17
+
18
+ Run `npm ci` and `npm run acceptance` on Node 24.19.0. Regression coverage checks
19
+ retention of the original capture error, successful retry clearing, and valid
20
+ partial manifests. Acceptance also runs the full suite, checks documentation
21
+ and package contents, and compares two independently built package archives.
22
+
23
+ See [known limitations](KNOWN_LIMITATIONS.md), the
24
+ [reproducibility manifest](reproducibility.json), and
25
+ [recovery guidance](../../docs/guides/evidence-and-sharing.md).
@@ -0,0 +1,74 @@
1
+ {
2
+ "schemaVersion": "ebo.release-reproducibility/v1",
3
+ "release": {
4
+ "name": "engineering-behavior-observatory",
5
+ "version": "0.2.3"
6
+ },
7
+ "runtime": {
8
+ "node": "24.19.0",
9
+ "claudeAgentSdk": "0.3.258",
10
+ "openhandsAgentServer": "1.46.0",
11
+ "deepseekClient": "0.1.1-rc.2",
12
+ "deepseekProtocol": "0.1.1-rc.2",
13
+ "deepseekRuntime": "0.1.1-rc.2",
14
+ "codexAppServer": "0.153.4",
15
+ "grafana": "13.2.0",
16
+ "grafanaInfinity": "4.0.0",
17
+ "piSdk": "0.85.1",
18
+ "cursorSdk": "1.0.31"
19
+ },
20
+ "commands": [
21
+ "npm ci",
22
+ "npm run acceptance"
23
+ ],
24
+ "fixtureCoverage": {
25
+ "agent-sdk": "frozen queue entry through qualified capture, approved export, normalization, configurable judging, review, aggregation, and Atlas",
26
+ "openhands-agent-server": "pinned REST/WebSocket stream-final reconciliation, workspace/outcome evidence, partial capture, and retained evaluation",
27
+ "deepseek-harness": "official-client JSON-RPC composition, receipt-to-idle completion, stderr, interruption, shutdown, swaps, and retained evaluation",
28
+ "codex-app-server": "owned stdio lifecycle, full history, OTLP, interruption/failure, export, and retained evaluation; configurable workspace-write networking with explicit offline queue propagation",
29
+ "acceptance-cases": "seeded ordering, secret scanning, partial attempts, native references, abstention, disputes, denominators, and unsupported comparisons",
30
+ "pi-sdk": "frozen queue, passive hooks, durable session, partial cleanup, export, retained normalization and evaluation",
31
+ "cursor-sdk": "frozen queue, official store, callbacks/history, bounded cancellation, export, retained normalization and evaluation",
32
+ "workspace-capture": "shared packaging error retained in partial reports; explicit successful retry clears prior error"
33
+ },
34
+ "determinism": {
35
+ "stable": [
36
+ "task, run, attempt, event, assertion, aggregate, and Atlas case identities",
37
+ "fixture, native-reference, normalized-dataset, source/cohort, archive, and package digests",
38
+ "seeded queue and review-sample ordering"
39
+ ],
40
+ "excluded": [
41
+ "native and EBO observation timestamps",
42
+ "lifecycle start/finish timestamps",
43
+ "provider timing and usage",
44
+ "temporary workspace and output paths"
45
+ ]
46
+ },
47
+ "fixtures": {
48
+ "tests/fixtures/task-packet.valid.v1.json": "ac2ef1043c0cc16bf4c21d05c1dc880ca1b4b8cc2f9ea7653328e91f0b0e2681",
49
+ "test/fixtures/agent-sdk-normalizer/complete.input.json": "7285dee088517e5de38ceda84fc7c58d9303949c7448ab3b5e10a5a2d2ebdfe3",
50
+ "test/fixtures/agent-sdk-normalizer/complete.expected.jsonl": "d32fa3a013bc179f1673f7e1a89b8a721bb8ddaf63eba5bc8a3f14f1cc148ed8",
51
+ "contracts/openhands-agent-server-v1.44.1.json": "e7a07977688a0703b15751a8a1ab17f29be45f9fdf438017e0dbcbc49cca37b0",
52
+ "contracts/openhands-agent-server-v1.46.0.json": "455cdddd2c206cb2f20245c2189b92e773657ec14d651236baa532b2dbadeb28",
53
+ "test/fixtures/openhands/v1.44.1/streamed-events.json": "d751083ba2ef94c9865117db481cc2f4933772bbbbb6b9fcf60571563715c1ea",
54
+ "test/fixtures/openhands/v1.44.1/final-events.json": "551db9904e860004d604c923692b5c5d3aaee966220492214c60003d2df415b6",
55
+ "test/fixtures/deepseek/golden-success.json": "f3867d38169bf0e28f64aba1dd60553f21cf07f41f81b4bad48261e49fa3ca88",
56
+ "test/fixtures/deepseek/golden-interrupted.json": "79e1f8404df1ee3b562d0bdbc0b2ee712ffc6d5774992b92970814ea815677b7",
57
+ "test/fixtures/deepseek/compositions/minimal/composition.json": "72fe4368e105f1bad82b7fdf8380e92c9fa689d8985f6ab91f534f0baf3eda02",
58
+ "contracts/codex-app-server-0.153.4/manifest.json": "e62281ff5e5d1cc71d07763b85e997ccc9ddbad9aa82a469da4181ee4b1890a8",
59
+ "test/fixtures/codex/legacy-0.150.1.dataset.json": "f71906b009e546afbf3b4e79396f223181382f3b3d8795e9cfcd032398a9d6bf",
60
+ "test/fixtures/behavior-assertions/abstained.json": "7f1962864f664c794925a67d93074e7b150af0f57c58041a81e062990d641197",
61
+ "test/fixtures/behavior-assertions/disputed.review.json": "450d3d9efe7feefaf7500b3ad20ab2803b412c7e53b91763125e15a83be87981",
62
+ "test/fixtures/comparison/exact.json": "cafa68e94b8d50dc308ddcfc2e73aa139c828b4ecd1e1c762bbc51bf1b44501e",
63
+ "test/fixtures/structural-observations/golden.json": "bad71a3a93524b76c0a2fdbb58e98e1e65be46cae14acf18ac89d9ba76e03e8a",
64
+ "test/atlas-fixture.ts": "9f03ce9cbcd0e5d3e3a5916866554ceb1c21f1c8712e58529499c1ea499f048d",
65
+ "test/pi.test.ts": "a00997c5617ca178e19b25b0d483cbdce8ce7c306d4c871557945b31a2e657cf",
66
+ "test/cursor-sdk.test.ts": "753094f127b11dc392ec2244c9edddabec27aa94347bb6159a292129ed2a2822",
67
+ "examples/cursor-sdk/README.md": "a2cf481123e7f3deb1c5a682ba208a775c3f91c35e1a6c1a428c517c79692618",
68
+ "examples/cursor-sdk/capture-profile.json": "d1a5787875daf5523b3783f259b891c256a904cbb10279d5be214af846305a2d",
69
+ "examples/cursor-sdk/harness.json": "2f1da6f2b17258d0ee59431f687696901ce3692589175d182f3a4bbe8dd4d689",
70
+ "examples/cursor-sdk/model.json": "ad8f31ff5d2387c02d79226679022358afafc61ba5dda73040e3f754530a230a",
71
+ "examples/cursor-sdk/native-limits.json": "40fe451f56b0863e79b42f5c85defbce678e1dc90dd248d15c430db176d71511",
72
+ "examples/cursor-sdk/native-tool-policy.json": "0fef98b36cd39de82914c5b068022c1335dadda24b7d6e7df398b2430e6a16e0"
73
+ }
74
+ }
@@ -0,0 +1,16 @@
1
+ # v0.2.4 known limitations
2
+
3
+ The [v0.2.3 limitations](../0.2.3/KNOWN_LIMITATIONS.md) continue to apply.
4
+
5
+ - Snapshots remain bounded to 1 GiB compressed. Temporary source and extracted
6
+ copies also require free disk space; large cache trees increase capture time.
7
+ - Known cache exclusions must be declared before a study. Capture does not
8
+ silently omit additional files to fit a limit.
9
+ - Opaque snapshots remain restricted evidence; larger capture support does
10
+ not make them portable sanitized exports.
11
+ - Full-parallel testing exposed intermittent interruption/cleanup failures.
12
+ Release acceptance runs tests serially, as in earlier releases.
13
+ - npm audit reports three dependency findings (two moderate, one high) through
14
+ the pinned Cursor SDK's connect-node/undici dependency chain. npm reports no
15
+ available automatic fix for this dependency graph. This patch leaves those
16
+ dependencies unchanged; passing acceptance is not a clean security audit.
@@ -0,0 +1,23 @@
1
+ # v0.2.4: Stream large workspace snapshots
2
+
3
+ Workspace snapshot capture no longer buffers the compressed archive in child
4
+ process stdout. Archives stream to disk, pass extraction and tree-digest checks,
5
+ and publish atomically. Qualification verifies snapshot integrity in chunks.
6
+
7
+ The compressed snapshot limit increases from 128 MiB to 1 GiB. Output above
8
+ that limit fails explicitly and retains partial evidence. The 64 MiB patch
9
+ limit, capture exclusions, archive format, and portable-export policy are
10
+ unchanged. No runtime pins or dependencies change.
11
+
12
+ ## Verification
13
+
14
+ Regression tests cover a 129 MiB incompressible snapshot, streamed-write failure
15
+ cleanup, no-clobber publication, and bounded integrity checks. A retained real
16
+ workspace that failed on v0.2.3 was captured separately as a 605,143,040-byte
17
+ snapshot, with its extracted tree digest verified. Original evidence was not
18
+ rewritten.
19
+
20
+ Run `npm ci` and `npm run acceptance` on Node 24.19.0 for the full suite,
21
+ documentation and package checks, and two byte-identical package builds.
22
+ See [known limitations](KNOWN_LIMITATIONS.md) and the
23
+ [reproducibility manifest](reproducibility.json).
@@ -0,0 +1,74 @@
1
+ {
2
+ "schemaVersion": "ebo.release-reproducibility/v1",
3
+ "release": {
4
+ "name": "engineering-behavior-observatory",
5
+ "version": "0.2.4"
6
+ },
7
+ "runtime": {
8
+ "node": "24.19.0",
9
+ "claudeAgentSdk": "0.3.258",
10
+ "openhandsAgentServer": "1.46.0",
11
+ "deepseekClient": "0.1.1-rc.2",
12
+ "deepseekProtocol": "0.1.1-rc.2",
13
+ "deepseekRuntime": "0.1.1-rc.2",
14
+ "codexAppServer": "0.153.4",
15
+ "grafana": "13.2.0",
16
+ "grafanaInfinity": "4.0.0",
17
+ "piSdk": "0.85.1",
18
+ "cursorSdk": "1.0.31"
19
+ },
20
+ "commands": [
21
+ "npm ci",
22
+ "npm run acceptance"
23
+ ],
24
+ "fixtureCoverage": {
25
+ "agent-sdk": "frozen queue entry through qualified capture, approved export, normalization, configurable judging, review, aggregation, and Atlas",
26
+ "openhands-agent-server": "pinned REST/WebSocket stream-final reconciliation, workspace/outcome evidence, partial capture, and retained evaluation",
27
+ "deepseek-harness": "official-client JSON-RPC composition, receipt-to-idle completion, stderr, interruption, shutdown, swaps, and retained evaluation",
28
+ "codex-app-server": "owned stdio lifecycle, full history, OTLP, interruption/failure, export, and retained evaluation; configurable workspace-write networking with explicit offline queue propagation",
29
+ "acceptance-cases": "seeded ordering, secret scanning, partial attempts, native references, abstention, disputes, denominators, and unsupported comparisons",
30
+ "pi-sdk": "frozen queue, passive hooks, durable session, partial cleanup, export, retained normalization and evaluation",
31
+ "cursor-sdk": "frozen queue, official store, callbacks/history, bounded cancellation, export, retained normalization and evaluation",
32
+ "workspace-capture": "streamed snapshot capture, atomic publication and qualification beyond 128 MiB; bounded 1 GiB compressed output; retained error and partial evidence"
33
+ },
34
+ "determinism": {
35
+ "stable": [
36
+ "task, run, attempt, event, assertion, aggregate, and Atlas case identities",
37
+ "fixture, native-reference, normalized-dataset, source/cohort, archive, and package digests",
38
+ "seeded queue and review-sample ordering"
39
+ ],
40
+ "excluded": [
41
+ "native and EBO observation timestamps",
42
+ "lifecycle start/finish timestamps",
43
+ "provider timing and usage",
44
+ "temporary workspace and output paths"
45
+ ]
46
+ },
47
+ "fixtures": {
48
+ "tests/fixtures/task-packet.valid.v1.json": "ac2ef1043c0cc16bf4c21d05c1dc880ca1b4b8cc2f9ea7653328e91f0b0e2681",
49
+ "test/fixtures/agent-sdk-normalizer/complete.input.json": "7285dee088517e5de38ceda84fc7c58d9303949c7448ab3b5e10a5a2d2ebdfe3",
50
+ "test/fixtures/agent-sdk-normalizer/complete.expected.jsonl": "d32fa3a013bc179f1673f7e1a89b8a721bb8ddaf63eba5bc8a3f14f1cc148ed8",
51
+ "contracts/openhands-agent-server-v1.44.1.json": "e7a07977688a0703b15751a8a1ab17f29be45f9fdf438017e0dbcbc49cca37b0",
52
+ "contracts/openhands-agent-server-v1.46.0.json": "455cdddd2c206cb2f20245c2189b92e773657ec14d651236baa532b2dbadeb28",
53
+ "test/fixtures/openhands/v1.44.1/streamed-events.json": "d751083ba2ef94c9865117db481cc2f4933772bbbbb6b9fcf60571563715c1ea",
54
+ "test/fixtures/openhands/v1.44.1/final-events.json": "551db9904e860004d604c923692b5c5d3aaee966220492214c60003d2df415b6",
55
+ "test/fixtures/deepseek/golden-success.json": "f3867d38169bf0e28f64aba1dd60553f21cf07f41f81b4bad48261e49fa3ca88",
56
+ "test/fixtures/deepseek/golden-interrupted.json": "79e1f8404df1ee3b562d0bdbc0b2ee712ffc6d5774992b92970814ea815677b7",
57
+ "test/fixtures/deepseek/compositions/minimal/composition.json": "72fe4368e105f1bad82b7fdf8380e92c9fa689d8985f6ab91f534f0baf3eda02",
58
+ "contracts/codex-app-server-0.153.4/manifest.json": "e62281ff5e5d1cc71d07763b85e997ccc9ddbad9aa82a469da4181ee4b1890a8",
59
+ "test/fixtures/codex/legacy-0.150.1.dataset.json": "f71906b009e546afbf3b4e79396f223181382f3b3d8795e9cfcd032398a9d6bf",
60
+ "test/fixtures/behavior-assertions/abstained.json": "7f1962864f664c794925a67d93074e7b150af0f57c58041a81e062990d641197",
61
+ "test/fixtures/behavior-assertions/disputed.review.json": "450d3d9efe7feefaf7500b3ad20ab2803b412c7e53b91763125e15a83be87981",
62
+ "test/fixtures/comparison/exact.json": "cafa68e94b8d50dc308ddcfc2e73aa139c828b4ecd1e1c762bbc51bf1b44501e",
63
+ "test/fixtures/structural-observations/golden.json": "bad71a3a93524b76c0a2fdbb58e98e1e65be46cae14acf18ac89d9ba76e03e8a",
64
+ "test/atlas-fixture.ts": "9f03ce9cbcd0e5d3e3a5916866554ceb1c21f1c8712e58529499c1ea499f048d",
65
+ "test/pi.test.ts": "a00997c5617ca178e19b25b0d483cbdce8ce7c306d4c871557945b31a2e657cf",
66
+ "test/cursor-sdk.test.ts": "753094f127b11dc392ec2244c9edddabec27aa94347bb6159a292129ed2a2822",
67
+ "examples/cursor-sdk/README.md": "a2cf481123e7f3deb1c5a682ba208a775c3f91c35e1a6c1a428c517c79692618",
68
+ "examples/cursor-sdk/capture-profile.json": "d1a5787875daf5523b3783f259b891c256a904cbb10279d5be214af846305a2d",
69
+ "examples/cursor-sdk/harness.json": "2f1da6f2b17258d0ee59431f687696901ce3692589175d182f3a4bbe8dd4d689",
70
+ "examples/cursor-sdk/model.json": "ad8f31ff5d2387c02d79226679022358afafc61ba5dda73040e3f754530a230a",
71
+ "examples/cursor-sdk/native-limits.json": "40fe451f56b0863e79b42f5c85defbce678e1dc90dd248d15c430db176d71511",
72
+ "examples/cursor-sdk/native-tool-policy.json": "0fef98b36cd39de82914c5b068022c1335dadda24b7d6e7df398b2430e6a16e0"
73
+ }
74
+ }
package/release/README.md CHANGED
@@ -5,6 +5,8 @@ For downloads and published artifacts, see
5
5
 
6
6
  | Version | Changes and verification | Support boundary |
7
7
  | :--- | :--- | :--- |
8
+ | [0.2.4](0.2.4/README.md) | Stream workspace snapshots and integrity checks; support archives up to 1 GiB | [Known limitations](0.2.4/KNOWN_LIMITATIONS.md) |
9
+ | [0.2.3](0.2.3/README.md) | Retain workspace capture errors in partial-bundle reports | [Known limitations](0.2.3/KNOWN_LIMITATIONS.md) |
8
10
  | [0.2.2](0.2.2/README.md) | Configurable Codex tool networking, enabled by default for workspace-write | [Known limitations](0.2.2/KNOWN_LIMITATIONS.md) |
9
11
  | [0.2.1](0.2.1/README.md) | Documentation redesign, Apache-2.0, and npm distribution setup | [Known limitations](0.2.1/KNOWN_LIMITATIONS.md) |
10
12
  | [0.2.0](0.2.0/README.md) | Pi and Cursor SDK integrations; reproducible package | [Known limitations](0.2.0/KNOWN_LIMITATIONS.md) |