engineering-behavior-observatory 0.2.4 → 0.2.5

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.
@@ -1,7 +1,7 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { createReadStream, createWriteStream } from "node:fs";
4
- import { cp, lstat, mkdir, mkdtemp, readdir, rm, rmdir, utimes, writeFile } from "node:fs/promises";
4
+ import { cp, lstat, mkdir, mkdtemp, readdir, realpath, rm, rmdir, utimes, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join, resolve, sep } from "node:path";
7
7
  import { promisify } from "node:util";
@@ -268,6 +268,7 @@ async function withWorkspaceOutcomeProjection(options, use) {
268
268
  try {
269
269
  await cp(finalPath, projectedPath, {
270
270
  recursive: true,
271
+ verbatimSymlinks: true,
271
272
  preserveTimestamps: true,
272
273
  force: false,
273
274
  filter: async (source) => source === finalPath
@@ -648,7 +649,7 @@ async function workspacePatchApplies(startingWorkspacePath, patch) {
648
649
  const applied = join(temporaryRoot, "workspace");
649
650
  const patchPath = join(temporaryRoot, "workspace.patch");
650
651
  try {
651
- await cp(resolve(startingWorkspacePath), applied, { recursive: true, preserveTimestamps: true, force: false });
652
+ await cp(resolve(startingWorkspacePath), applied, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
652
653
  if (patch.length > 0) {
653
654
  await writeFile(patchPath, patch, { flag: "wx", mode: 0o600 });
654
655
  await execFileAsync("git", ["apply", "--check", "--binary", patchPath], { cwd: applied });
@@ -842,7 +843,7 @@ async function removeIgnoredWorkspaceEntries(startPath, finalPath) {
842
843
  const globalExcludes = join(temporaryRoot, "global-excludes");
843
844
  try {
844
845
  await writeFile(globalExcludes, "");
845
- await cp(startPath, baseline, { recursive: true, preserveTimestamps: true, force: false });
846
+ await cp(startPath, baseline, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
846
847
  await execFileAsync("git", ["init", "--quiet"], { cwd: baseline });
847
848
  await execFileAsync("git", ["add", "--force", "--all"], { cwd: baseline });
848
849
  for (const relativePath of await ignoredWorkspacePaths(baseline, finalPath, globalExcludes)) {
@@ -910,12 +911,31 @@ async function isExcludedWorkspaceDirectory(root, source, exclusions) {
910
911
  }
911
912
  return false;
912
913
  }
913
- async function removeEmptyDirectories(directory, root = directory) {
914
+ async function removeEmptyDirectories(directory, root = directory, targets) {
915
+ if (targets === undefined) {
916
+ targets = new Set();
917
+ async function collect(path) {
918
+ for (const entry of await readdir(path, { withFileTypes: true })) {
919
+ const child = join(path, entry.name);
920
+ if (entry.isDirectory())
921
+ await collect(child);
922
+ else if (entry.isSymbolicLink()) {
923
+ // Omission must not turn a valid link into a dangling one. Invalid
924
+ // links are rejected later by workspace hashing; no content is read.
925
+ try {
926
+ targets.add(await realpath(child));
927
+ }
928
+ catch { /* validated by hashing */ }
929
+ }
930
+ }
931
+ }
932
+ await collect(directory);
933
+ }
914
934
  for (const entry of await readdir(directory, { withFileTypes: true })) {
915
935
  if (entry.isDirectory())
916
- await removeEmptyDirectories(join(directory, entry.name), root);
936
+ await removeEmptyDirectories(join(directory, entry.name), root, targets);
917
937
  }
918
- if (directory !== root && (await readdir(directory)).length === 0)
938
+ if (directory !== root && !targets.has(await realpath(directory)) && (await readdir(directory)).length === 0)
919
939
  await rmdir(directory);
920
940
  }
921
941
  async function restoreProjectedDirectoryTimestamps(source, projected) {
@@ -938,7 +958,7 @@ async function workspacePatch(startPath, finalPath, finalTreeDigest) {
938
958
  const applied = join(temporaryRoot, "applied");
939
959
  const patchPath = join(temporaryRoot, "workspace.patch");
940
960
  try {
941
- await cp(startPath, worktree, { recursive: true, preserveTimestamps: true, force: false });
961
+ await cp(startPath, worktree, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
942
962
  await execFileAsync("git", ["init", "--quiet"], { cwd: worktree });
943
963
  await execFileAsync("git", ["add", "--force", "--all"], { cwd: worktree });
944
964
  // A tree is enough for the diff. A commit can spawn detached maintenance
@@ -951,7 +971,7 @@ async function workspacePatch(startPath, finalPath, finalTreeDigest) {
951
971
  for (const entry of await readdir(finalPath)) {
952
972
  if (entry === ".git")
953
973
  throw new Error("Workspace patches cannot retain Git administrative state.");
954
- await cp(join(finalPath, entry), join(worktree, entry), { recursive: true, preserveTimestamps: true, force: false });
974
+ await cp(join(finalPath, entry), join(worktree, entry), { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
955
975
  }
956
976
  await execFileAsync("git", ["add", "--force", "--intent-to-add", "--all"], { cwd: worktree });
957
977
  let stdout;
@@ -966,12 +986,14 @@ async function workspacePatch(startPath, finalPath, finalTreeDigest) {
966
986
  throw error;
967
987
  }
968
988
  const patch = Buffer.from(stdout);
969
- await cp(startPath, applied, { recursive: true, preserveTimestamps: true, force: false });
989
+ await cp(startPath, applied, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
970
990
  if (patch.length > 0) {
971
991
  await writeFile(patchPath, patch, { flag: "wx", mode: 0o600 });
972
992
  await execFileAsync("git", ["apply", "--binary", patchPath], { cwd: applied });
973
993
  }
974
- const appliedTreeDigest = await digestWorkspaceTree(applied);
994
+ // Git omits empty directories, including legitimate link targets. An
995
+ // invalid reconstructed tree is a patch limitation, so use the snapshot.
996
+ const appliedTreeDigest = await digestWorkspaceTree(applied).catch(() => undefined);
975
997
  if (appliedTreeDigest !== finalTreeDigest) {
976
998
  return undefined;
977
999
  }
@@ -989,8 +1011,8 @@ async function workspaceSnapshot(finalPath, finalTreeDigest, use) {
989
1011
  const extracted = join(temporaryRoot, "extracted");
990
1012
  try {
991
1013
  await mkdir(snapshotParent);
992
- await cp(finalPath, snapshotRoot, { recursive: true, preserveTimestamps: true, force: false });
993
- const child = spawn(TAR_COMMAND, ["-czf", "-", "-C", snapshotParent, "workspace"], { stdio: ["ignore", "pipe", "pipe"] });
1014
+ await cp(finalPath, snapshotRoot, { recursive: true, verbatimSymlinks: true, preserveTimestamps: true, force: false });
1015
+ const child = spawn(TAR_COMMAND, [...(process.platform === "darwin" ? ["--no-mac-metadata"] : []), "-czf", "-", "-C", snapshotParent, "workspace"], { stdio: ["ignore", "pipe", "pipe"] });
994
1016
  let stderr = "";
995
1017
  child.stderr.on("data", (chunk) => { stderr = (stderr + chunk.toString("utf8")).slice(-8192); });
996
1018
  const exited = new Promise((resolveExit, reject) => {
@@ -1012,7 +1034,16 @@ async function workspaceSnapshot(finalPath, finalTreeDigest, use) {
1012
1034
  await Promise.allSettled([exited, streamed]);
1013
1035
  }
1014
1036
  await mkdir(extracted, { mode: 0o700 });
1015
- await execFileAsync(TAR_COMMAND, ["-xzpf", snapshotPath, "-C", extracted]);
1037
+ // macOS libarchive consumes genuine ._* AppleDouble files even when its
1038
+ // metadata flags are disabled. Native pax preserves them with copyfile off.
1039
+ if (process.platform === "darwin") {
1040
+ await execFileAsync("/bin/pax", ["-rz", "-p", "p", "-f", snapshotPath], {
1041
+ cwd: extracted, env: { ...process.env, COPYFILE_DISABLE: "1" },
1042
+ });
1043
+ }
1044
+ else {
1045
+ await execFileAsync(TAR_COMMAND, ["-xzpf", snapshotPath, "-C", extracted]);
1046
+ }
1016
1047
  if (await digestWorkspaceTree(join(extracted, "workspace")) !== finalTreeDigest) {
1017
1048
  throw new Error("Bounded workspace snapshot cannot reproduce the final workspace tree.");
1018
1049
  }
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { createReadStream, createWriteStream } from "node:fs";
4
- import { chmod, lstat, mkdir, mkdtemp, open, readdir, readFile, realpath, rm, utimes } from "node:fs/promises";
4
+ import { chmod, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rm, utimes } from "node:fs/promises";
5
5
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { performance } from "node:perf_hooks";
@@ -72,10 +72,10 @@ async function digestWorkspaceEntries(workspacePath, includeTimestamps, signal)
72
72
  if (!metadata.isDirectory() || metadata.isSymbolicLink())
73
73
  throw new Error("Workspace root is not a directory.");
74
74
  hash.update(`root\0${metadata.mode & 4095n}\0${includeTimestamps ? `${workspaceTimestamp(metadata)}\0` : ""}`);
75
- await hashWorkspaceDirectory(root, "", hash, includeTimestamps, signal);
75
+ await hashWorkspaceDirectory(root, "", hash, includeTimestamps, signal, root);
76
76
  return `sha256:${hash.digest("hex")}`;
77
77
  }
78
- async function hashWorkspaceDirectory(directory, relativeDirectory, hash, includeTimestamps, signal) {
78
+ async function hashWorkspaceDirectory(directory, relativeDirectory, hash, includeTimestamps, signal, root = directory) {
79
79
  assertNotAborted(signal);
80
80
  const entries = await readdir(directory, { withFileTypes: true });
81
81
  entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
@@ -83,11 +83,31 @@ async function hashWorkspaceDirectory(directory, relativeDirectory, hash, includ
83
83
  const path = join(directory, entry.name);
84
84
  const relativePath = posix.join(relativeDirectory, entry.name);
85
85
  const metadata = await lstat(path, { bigint: true });
86
- if (metadata.isSymbolicLink())
87
- throw new Error(`Workspace contains a symbolic link at "${relativePath}".`);
88
- if (metadata.isDirectory()) {
86
+ if (metadata.isSymbolicLink()) {
87
+ // Preserve link text, never hash the referent through the link. Both its
88
+ // lexical target and resolved chain must stay in this workspace.
89
+ const target = await readlink(path);
90
+ const contained = (candidate) => {
91
+ const rel = relative(root, candidate);
92
+ return !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
93
+ };
94
+ if (isAbsolute(target) || !contained(resolve(dirname(path), target))) {
95
+ throw new Error(`Workspace symbolic link escapes its root at "${relativePath}".`);
96
+ }
97
+ let resolved;
98
+ try {
99
+ resolved = await realpath(path);
100
+ }
101
+ catch {
102
+ throw new Error(`Workspace symbolic link is dangling or cyclic at "${relativePath}".`);
103
+ }
104
+ if (!contained(resolved))
105
+ throw new Error(`Workspace symbolic link escapes its root at "${relativePath}".`);
106
+ hash.update(`symlink\0${relativePath}\0${metadata.mode & 4095n}\0${includeTimestamps ? `${workspaceTimestamp(metadata)}\0` : ""}${Buffer.byteLength(target)}\0${target}`);
107
+ }
108
+ else if (metadata.isDirectory()) {
89
109
  hash.update(`directory\0${relativePath}\0${metadata.mode & 4095n}\0${includeTimestamps ? `${workspaceTimestamp(metadata)}\0` : ""}`);
90
- await hashWorkspaceDirectory(path, relativePath, hash, includeTimestamps, signal);
110
+ await hashWorkspaceDirectory(path, relativePath, hash, includeTimestamps, signal, root);
91
111
  }
92
112
  else if (metadata.isFile()) {
93
113
  if (metadata.nlink > 1n)
@@ -46,6 +46,34 @@ missing-evidence entries. Inspect the detail before retrying capture against a
46
46
  retained workspace. A later successful capture is recovery evidence; it does
47
47
  not change the original attempt's recorded failure.
48
48
 
49
+ When the native run completed but capture failed, recover capture without
50
+ executing the task again. Materialize the same frozen starting workspace, use
51
+ the same capture exclusions, and assemble a separate bundle with byte-identical
52
+ native evidence plus the recovered workspace outcome. Record the original
53
+ manifest digest, recovery time, capture-code identity and qualification beside
54
+ the recovered bundle. Preserve the original failed bundle. A later workspace
55
+ capture cannot prove the retained directory was unchanged since execution ended
56
+ unless a fingerprint was recorded then. Do not count the recovery as another
57
+ model trial or as an independent sample.
58
+
59
+ Workspace outcomes preserve relative symbolic links whose lexical targets and
60
+ resolved referents stay inside the workspace. Link text is hashed and copied
61
+ verbatim, not replaced by the referent's bytes. Empty-directory omission keeps
62
+ directories referenced by links. Absolute, escaping, dangling and
63
+ cyclic links fail capture. Patch application or snapshot extraction must still
64
+ reproduce the resulting tree digest. This does not relax task-archive admission
65
+ or verifier sandbox rules.
66
+
67
+ On macOS, snapshot creation disables generated AppleDouble metadata. Snapshot
68
+ verification uses the built-in `/bin/pax` with `COPYFILE_DISABLE=1` so genuine
69
+ `._*` files remain files. macOS tar may otherwise consume them as metadata,
70
+ changing the reconstructed tree. To extract a snapshot for inspection on macOS:
71
+
72
+ ```sh
73
+ mkdir restored
74
+ (cd restored && COPYFILE_DISABLE=1 /bin/pax -rz -p p -f /absolute/path/workspace.tar.gz)
75
+ ```
76
+
49
77
  Workspace capture tries a patch up to 64 MiB, then falls back to a snapshot
50
78
  when the patch is too large or cannot reproduce the tree. Snapshots stream to
51
79
  disk with a 1 GiB compressed limit and are extracted to verify the tree digest
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engineering-behavior-observatory",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Capture engineering-agent trajectories, evaluate behavior, and inspect cited evidence across harnesses.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -48,6 +48,7 @@
48
48
  "release/0.2.2/",
49
49
  "release/0.2.3/",
50
50
  "release/0.2.4/",
51
+ "release/0.2.5/",
51
52
  "schemas/",
52
53
  "scripts/atlas-grafana.sh"
53
54
  ],
@@ -0,0 +1,12 @@
1
+ # v0.2.5 known limitations
2
+
3
+ The [v0.2.4 limitations](../0.2.4/KNOWN_LIMITATIONS.md) continue to apply.
4
+
5
+ - Symlink support applies to workspace outcome capture, not task-archive
6
+ admission or permission to read outside the workspace.
7
+ - On macOS, use the documented native `pax` command when inspecting snapshots
8
+ containing genuine AppleDouble files. Default `tar` extraction can consume them.
9
+ - Capture-only recovery can use retained evidence and workspace files without
10
+ rerunning a model, but cannot reconstruct missing files or prove an earlier
11
+ workspace state without a fingerprint captured at that time.
12
+ - Workspace snapshots remain restricted evidence, not sanitized partner exports.
@@ -0,0 +1,25 @@
1
+ # v0.2.5: Preserve framework files in workspace capture
2
+
3
+ Workspace capture now preserves contained relative symbolic links, including
4
+ framework link chains and links to empty directories. Absolute, escaping,
5
+ dangling, and cyclic links remain rejected. Copies retain the original link
6
+ text, and patches that cannot reproduce the tree fall back to a snapshot.
7
+
8
+ On macOS, snapshot verification uses native `pax` to preserve genuine AppleDouble
9
+ files that macOS `tar` otherwise consumes as metadata. Snapshots remain tar.gz
10
+ archives. No runtime dependencies, pins, or serialized artifact formats change.
11
+
12
+ ## Verification
13
+
14
+ Regression tests cover link preservation, invalid targets, empty target
15
+ directories, AppleDouble bytes, and long paths. A completed model attempt's
16
+ retained workspace was recovered as a 695,459,840-byte snapshot with a verified
17
+ extracted tree digest, without another model call or rewriting original evidence.
18
+ The recovery qualified with an explicit missing-telemetry-receipt gap; it records
19
+ the later capture time rather than claiming an exact end-of-turn snapshot.
20
+
21
+ Run `npm ci` and `npm run acceptance` on Node 24.19.0 for tests, documentation
22
+ and package checks, and two byte-identical package builds. See
23
+ [known limitations](KNOWN_LIMITATIONS.md), the
24
+ [reproducibility manifest](reproducibility.json), and the
25
+ [workspace inspection guide](../../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.5"
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": "Contained relative symlinks, empty link targets, AppleDouble files and long paths; streamed snapshots with verified tree digests and unchanged 1 GiB bound"
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,7 @@ For downloads and published artifacts, see
5
5
 
6
6
  | Version | Changes and verification | Support boundary |
7
7
  | :--- | :--- | :--- |
8
+ | [0.2.5](0.2.5/README.md) | Preserve contained workspace symlinks and macOS AppleDouble files | [Known limitations](0.2.5/KNOWN_LIMITATIONS.md) |
8
9
  | [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
10
  | [0.2.3](0.2.3/README.md) | Retain workspace capture errors in partial-bundle reports | [Known limitations](0.2.3/KNOWN_LIMITATIONS.md) |
10
11
  | [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) |