owlrunkit 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +674 -0
  3. package/NOTICE.md +137 -0
  4. package/README.md +44 -0
  5. package/SOURCE.md +10 -0
  6. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_1.md +160 -0
  7. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +459 -0
  8. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +408 -0
  9. package/integrations/codex/skills/owlcoda-runkit/agents/openai.yaml +4 -0
  10. package/integrations/codex/skills/owlcoda-runkit/assets/templates/agents-snippet.md +7 -0
  11. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +10 -0
  12. package/integrations/codex/skills/owlcoda-runkit/assets/templates/coverage-adopt-request.json +26 -0
  13. package/integrations/codex/skills/owlcoda-runkit/assets/templates/execution-delivery-packet.json +13 -0
  14. package/integrations/codex/skills/owlcoda-runkit/assets/templates/finalize-request.json +18 -0
  15. package/integrations/codex/skills/owlcoda-runkit/assets/templates/goal-contract.json +11 -0
  16. package/integrations/codex/skills/owlcoda-runkit/assets/templates/profiles.json +9 -0
  17. package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +11 -0
  18. package/integrations/codex/skills/owlcoda-runkit/assets/templates/resource-preflight-request.json +56 -0
  19. package/integrations/codex/skills/owlcoda-runkit/assets/templates/resume-request.json +10 -0
  20. package/integrations/codex/skills/owlcoda-runkit/assets/templates/snapshot-request.json +23 -0
  21. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-context.json +15 -0
  22. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-receipt.json +11 -0
  23. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verify-plan-request.json +31 -0
  24. package/integrations/codex/skills/owlcoda-runkit/assets/templates/visual-smoke-request.json +32 -0
  25. package/integrations/codex/skills/owlcoda-runkit/assets/templates/worker-lease.json +7 -0
  26. package/package.json +24 -0
  27. package/packages/attest/LICENSE +674 -0
  28. package/packages/attest/README.md +63 -0
  29. package/packages/attest/cli/owlcoda-attest.mjs +155 -0
  30. package/packages/attest/consumer-surface-v1.json +255 -0
  31. package/packages/attest/distribution-manifest-v1.json +104 -0
  32. package/packages/attest/fixtures/bad/accepted-authority-leak.json +7 -0
  33. package/packages/attest/fixtures/bad/accepted-failed-receipt.json +7 -0
  34. package/packages/attest/fixtures/bad/active-context-binding-mismatch.json +7 -0
  35. package/packages/attest/fixtures/bad/active-source-binding-mismatch.json +7 -0
  36. package/packages/attest/fixtures/bad/context-fingerprint-mismatch.json +7 -0
  37. package/packages/attest/fixtures/bad/duplicate-object-key.json +8 -0
  38. package/packages/attest/fixtures/bad/multiple-active-leaves.json +10 -0
  39. package/packages/attest/fixtures/bad/orphan-replacement.json +7 -0
  40. package/packages/attest/fixtures/bad/root-shape-extra-property.json +8 -0
  41. package/packages/attest/fixtures/bad/source-fingerprint-mismatch.json +7 -0
  42. package/packages/attest/fixtures/good/accepted-replacement-chain.json +59 -0
  43. package/packages/attest/fixtures/good/blocked-single-leaf.json +9 -0
  44. package/packages/attest/fixtures/index.json +90 -0
  45. package/packages/attest/sbom.spdx.json +33 -0
  46. package/packages/attest/schemas/attestation-ref-v1.schema.json +44 -0
  47. package/packages/attest/schemas/attestation-result-v1.schema.json +163 -0
  48. package/packages/attest/schemas/offline-attestation-bundle-v1.schema.json +30 -0
  49. package/packages/attest/schemas/public-verification-bundle-v1.schema.json +74 -0
  50. package/packages/attest/schemas/quick-verification-receipt-v1.schema.json +219 -0
  51. package/packages/attest/schemas/workspace-snapshot-v1.schema.json +135 -0
  52. package/packages/attest/src/canonical.mjs +12 -0
  53. package/packages/attest/src/formal.mjs +762 -0
  54. package/packages/attest/src/index.mjs +113 -0
  55. package/packages/attest/src/offline-bundle.mjs +108 -0
  56. package/packages/attest/src/quick-receipt-contract.mjs +156 -0
  57. package/packages/attest/src/quick.mjs +274 -0
  58. package/packages/attest/src/reference-contract.mjs +64 -0
  59. package/packages/attest/src/reference.mjs +131 -0
  60. package/packages/attest/src/workspace-snapshot.mjs +171 -0
  61. package/registry-release-policy-v1.json +34 -0
  62. package/sbom.spdx.json +39 -0
  63. package/scripts/runkit-contract/acceptance-evidence.mjs +103 -0
  64. package/scripts/runkit-contract/core-contract.mjs +392 -0
  65. package/scripts/runkit-contract/coverage-adoption.mjs +414 -0
  66. package/scripts/runkit-contract/delivery-create.mjs +252 -0
  67. package/scripts/runkit-contract/delivery-selection.mjs +31 -0
  68. package/scripts/runkit-contract/finalize.mjs +790 -0
  69. package/scripts/runkit-contract/inspect-presentation.mjs +183 -0
  70. package/scripts/runkit-contract/install-codex-skill.mjs +239 -0
  71. package/scripts/runkit-contract/lease-lifecycle.mjs +324 -0
  72. package/scripts/runkit-contract/lifecycle-orchestration.mjs +415 -0
  73. package/scripts/runkit-contract/offline-store.mjs +248 -0
  74. package/scripts/runkit-contract/profile-impact.mjs +223 -0
  75. package/scripts/runkit-contract/provenance-common.mjs +153 -0
  76. package/scripts/runkit-contract/quick-attest.mjs +4 -0
  77. package/scripts/runkit-contract/quick-canonical.mjs +41 -0
  78. package/scripts/runkit-contract/quick-metrics.mjs +86 -0
  79. package/scripts/runkit-contract/quick-receipt.mjs +139 -0
  80. package/scripts/runkit-contract/quick-verify.mjs +201 -0
  81. package/scripts/runkit-contract/quick-workspace-snapshot.mjs +3 -0
  82. package/scripts/runkit-contract/ready-for-commit.mjs +188 -0
  83. package/scripts/runkit-contract/receipt-lineage.mjs +332 -0
  84. package/scripts/runkit-contract/registry-adoption-gate.mjs +117 -0
  85. package/scripts/runkit-contract/repair-execution.mjs +997 -0
  86. package/scripts/runkit-contract/resource-preflight.mjs +829 -0
  87. package/scripts/runkit-contract/resume-execution.mjs +569 -0
  88. package/scripts/runkit-contract/runkit-cli.mjs +1214 -0
  89. package/scripts/runkit-contract/schemas/closeout-receipt-v2.schema.json +42 -0
  90. package/scripts/runkit-contract/schemas/core-artifact-v2.schema.json +23 -0
  91. package/scripts/runkit-contract/schemas/core-artifact.schema.json +23 -0
  92. package/scripts/runkit-contract/schemas/coverage-adopt-request-v1.schema.json +66 -0
  93. package/scripts/runkit-contract/schemas/engine-pin-v2.schema.json +14 -0
  94. package/scripts/runkit-contract/schemas/engine-pin.schema.json +14 -0
  95. package/scripts/runkit-contract/schemas/evidence-coverage-index-v1.schema.json +131 -0
  96. package/scripts/runkit-contract/schemas/finalize-request-v1.schema.json +131 -0
  97. package/scripts/runkit-contract/schemas/profiles.schema.json +47 -0
  98. package/scripts/runkit-contract/schemas/project-config-v2.schema.json +13 -0
  99. package/scripts/runkit-contract/schemas/project-config.schema.json +13 -0
  100. package/scripts/runkit-contract/schemas/ready-for-commit-v1.schema.json +26 -0
  101. package/scripts/runkit-contract/schemas/replayable-evidence-v1.schema.json +82 -0
  102. package/scripts/runkit-contract/schemas/resource-preflight-request-v1.schema.json +178 -0
  103. package/scripts/runkit-contract/schemas/resource-preflight-v1.schema.json +153 -0
  104. package/scripts/runkit-contract/schemas/resume-attempt-v1.schema.json +80 -0
  105. package/scripts/runkit-contract/schemas/resume-request-v1.schema.json +77 -0
  106. package/scripts/runkit-contract/schemas/snapshot-v1.schema.json +71 -0
  107. package/scripts/runkit-contract/schemas/verification-context-v1.schema.json +92 -0
  108. package/scripts/runkit-contract/schemas/verification-plan-v1.schema.json +150 -0
  109. package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +118 -0
  110. package/scripts/runkit-contract/schemas/verify-plan-request-v1.schema.json +56 -0
  111. package/scripts/runkit-contract/schemas/visual-smoke-request-v1.schema.json +64 -0
  112. package/scripts/runkit-contract/schemas/visual-smoke-result-v1.schema.json +68 -0
  113. package/scripts/runkit-contract/snapshot.mjs +340 -0
  114. package/scripts/runkit-contract/source-fingerprint.mjs +404 -0
  115. package/scripts/runkit-contract/templates/coverage-adopt-request.json +26 -0
  116. package/scripts/runkit-contract/templates/finalize-request.json +18 -0
  117. package/scripts/runkit-contract/templates/goal-contract.json +11 -0
  118. package/scripts/runkit-contract/templates/ready-for-commit-request.json +11 -0
  119. package/scripts/runkit-contract/templates/resource-preflight-request.json +56 -0
  120. package/scripts/runkit-contract/templates/resume-request.json +10 -0
  121. package/scripts/runkit-contract/templates/snapshot-request.json +23 -0
  122. package/scripts/runkit-contract/templates/verify-plan-request.json +31 -0
  123. package/scripts/runkit-contract/templates/visual-smoke-request.json +32 -0
  124. package/scripts/runkit-contract/templates/worker-lease.json +7 -0
  125. package/scripts/runkit-contract/verification-context.mjs +141 -0
  126. package/scripts/runkit-contract/verification-plan.mjs +608 -0
  127. package/scripts/runkit-contract/verification-receipt-gate.mjs +566 -0
  128. package/scripts/runkit-contract/visual-smoke.mjs +410 -0
@@ -0,0 +1,340 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import {
3
+ lstatSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ realpathSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import path from "node:path";
10
+
11
+ import { canonicalSourceFingerprint } from "./source-fingerprint.mjs";
12
+ import {
13
+ assertAllowedKeys,
14
+ isRecord,
15
+ loadActiveExecution,
16
+ relativeToWorkspace,
17
+ resolveWithinRoot,
18
+ safeIdentifier,
19
+ safeRelativePath,
20
+ sha256,
21
+ writeJsonExclusive,
22
+ } from "./provenance-common.mjs";
23
+ import {
24
+ validateVerificationContext,
25
+ verificationContextFingerprint,
26
+ } from "./verification-context.mjs";
27
+
28
+ const STATUS_MODE = "porcelain-v1-z-untracked-all-runkit-excluded";
29
+ const REQUEST_KEYS = [
30
+ "schemaVersion",
31
+ "snapshotId",
32
+ "mode",
33
+ "targetRoot",
34
+ "cwd",
35
+ "executable",
36
+ "argv",
37
+ "launcherVersion",
38
+ "toolVersions",
39
+ "selectedPaths",
40
+ "statusMode",
41
+ "verificationContext",
42
+ ];
43
+
44
+ function lineCount(bytes) {
45
+ if (bytes.length === 0) return 0;
46
+ const text = bytes.toString("utf8");
47
+ return text.endsWith("\n") ? text.slice(0, -1).split("\n").length : text.split("\n").length;
48
+ }
49
+
50
+ function git(targetRoot, args) {
51
+ const completed = spawnSync("git", ["-C", targetRoot, ...args], {
52
+ encoding: null,
53
+ maxBuffer: 64 * 1024 * 1024,
54
+ });
55
+ if (completed.error) throw completed.error;
56
+ if (completed.status !== 0) {
57
+ throw new Error(`Git snapshot command failed: ${completed.stderr.toString("utf8").trim()}`);
58
+ }
59
+ return completed.stdout;
60
+ }
61
+
62
+ function selectedFileMap(targetRoot, selectedPaths) {
63
+ const files = {};
64
+ for (const selectedPath of selectedPaths) {
65
+ const safePath = safeRelativePath(selectedPath, "selectedPaths entry");
66
+ if (safePath === ".owlcoda/runkit" || safePath.startsWith(".owlcoda/runkit/")) {
67
+ throw new Error("selectedPaths must exclude .owlcoda/runkit/**.");
68
+ }
69
+ const absolutePath = resolveWithinRoot(targetRoot, safePath, "selectedPaths entry");
70
+ const stat = lstatSync(absolutePath);
71
+ if (stat.isSymbolicLink() || !stat.isFile()) {
72
+ throw new Error(`selectedPaths entry must be a regular non-symlink file: ${safePath}`);
73
+ }
74
+ files[safePath] = sha256(readFileSync(absolutePath));
75
+ }
76
+ return Object.fromEntries(Object.entries(files).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
77
+ }
78
+
79
+ export function captureTargetState({ targetRoot, selectedPaths }) {
80
+ const head = git(targetRoot, ["rev-parse", "HEAD"]).toString("utf8").trim();
81
+ const status = git(targetRoot, [
82
+ "status",
83
+ "--porcelain=v1",
84
+ "-z",
85
+ "--untracked-files=all",
86
+ "--",
87
+ ".",
88
+ ":(exclude).owlcoda/runkit/**",
89
+ ]);
90
+ const selectedFiles = selectedFileMap(targetRoot, selectedPaths);
91
+ return {
92
+ head,
93
+ statusMode: STATUS_MODE,
94
+ statusSha256: sha256(status),
95
+ statusByteCount: status.length,
96
+ statusEntryCount: status.length === 0 ? 0 : status.toString("utf8").split("\0").filter(Boolean).length,
97
+ selectedFiles,
98
+ manifestFingerprint: canonicalSourceFingerprint(selectedFiles),
99
+ };
100
+ }
101
+
102
+ function stableTarget(before, after) {
103
+ return before.head === after.head
104
+ && before.statusSha256 === after.statusSha256
105
+ && before.manifestFingerprint === after.manifestFingerprint;
106
+ }
107
+
108
+ function normalizedFileMap(value) {
109
+ if (!isRecord(value)
110
+ || Object.values(value).some(hash => typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash))) {
111
+ return null;
112
+ }
113
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
114
+ }
115
+
116
+ export function verifySnapshotSourceBinding({ snapshot, expectedFiles, expectedFingerprint }) {
117
+ const issues = [];
118
+ const expected = normalizedFileMap(expectedFiles);
119
+ const before = normalizedFileMap(snapshot?.repositoryBefore?.selectedFiles);
120
+ const after = normalizedFileMap(snapshot?.repositoryAfter?.selectedFiles);
121
+ if (!expected) issues.push("Delivery packet changed-file map is invalid.");
122
+ if (!before || !after) issues.push("Snapshot selected-file maps are invalid.");
123
+ if (expected && canonicalSourceFingerprint(expected) !== expectedFingerprint) {
124
+ issues.push("Delivery packet files do not match its source fingerprint.");
125
+ }
126
+ if (snapshot?.repositoryBefore?.manifestFingerprint !== expectedFingerprint
127
+ || snapshot?.repositoryAfter?.manifestFingerprint !== expectedFingerprint) {
128
+ issues.push("Snapshot manifest does not match the delivery source fingerprint.");
129
+ }
130
+ if (expected && before && JSON.stringify(before) !== JSON.stringify(expected)) {
131
+ issues.push("Snapshot repositoryBefore files do not match the DeliveryPacket.");
132
+ }
133
+ if (expected && after && JSON.stringify(after) !== JSON.stringify(expected)) {
134
+ issues.push("Snapshot repositoryAfter files do not match the DeliveryPacket.");
135
+ }
136
+ const expectedInputs = expected
137
+ ? Object.entries(expected).map(([id, hash]) => ({ id, sha256: hash }))
138
+ : [];
139
+ const actualInputs = Array.isArray(snapshot?.command?.evidence?.materialInputs)
140
+ ? [...snapshot.command.evidence.materialInputs]
141
+ .sort((left, right) => String(left?.id).localeCompare(String(right?.id)))
142
+ : null;
143
+ if (!actualInputs || JSON.stringify(actualInputs) !== JSON.stringify(expectedInputs)) {
144
+ issues.push("Snapshot material inputs do not match the DeliveryPacket.");
145
+ }
146
+ return { valid: issues.length === 0, issues };
147
+ }
148
+
149
+ function validateRequest(request) {
150
+ assertAllowedKeys(request, "Snapshot request", REQUEST_KEYS);
151
+ if (request.schemaVersion !== "OwlCodaRunKitSnapshotRequestV1") {
152
+ throw new Error("Unsupported snapshot request schemaVersion.");
153
+ }
154
+ safeIdentifier(request.snapshotId, "snapshotId");
155
+ if (!new Set(["project", "foreign_readonly"]).has(request.mode)) {
156
+ throw new Error("Snapshot mode must be project or foreign_readonly.");
157
+ }
158
+ if (typeof request.targetRoot !== "string" || request.targetRoot.length === 0) {
159
+ throw new Error("Snapshot request requires targetRoot.");
160
+ }
161
+ safeRelativePath(request.cwd, "Snapshot cwd", { allowDot: true });
162
+ if (typeof request.executable !== "string" || !path.isAbsolute(request.executable)) {
163
+ throw new Error("Snapshot requires an absolute executable path.");
164
+ }
165
+ if (!Array.isArray(request.argv)
166
+ || request.argv.some((value) => typeof value !== "string")) {
167
+ throw new Error("Snapshot requires an exact string argv array.");
168
+ }
169
+ if (typeof request.launcherVersion !== "string" || request.launcherVersion.length === 0) {
170
+ throw new Error("Snapshot requires launcherVersion.");
171
+ }
172
+ if (!Array.isArray(request.toolVersions)
173
+ || request.toolVersions.some((entry) => !isRecord(entry)
174
+ || typeof entry.name !== "string"
175
+ || typeof entry.version !== "string")) {
176
+ throw new Error("Snapshot toolVersions must contain name and version.");
177
+ }
178
+ if (!Array.isArray(request.selectedPaths) || request.selectedPaths.length === 0) {
179
+ throw new Error("Snapshot requires selectedPaths.");
180
+ }
181
+ if (request.statusMode !== STATUS_MODE) {
182
+ throw new Error(`Snapshot statusMode must be ${STATUS_MODE}.`);
183
+ }
184
+ const context = validateVerificationContext(request.verificationContext);
185
+ if (!context.valid) throw new Error(`Snapshot verification context is invalid: ${context.issues.join("; ")}`);
186
+ }
187
+
188
+ export function verifySnapshotEvidence({ workspaceRoot, runId, snapshot }) {
189
+ const issues = [];
190
+ if (!isRecord(snapshot) || snapshot.schemaVersion !== "OwlCodaRunKitSnapshotV1") {
191
+ return { valid: false, issues: ["Unsupported snapshot schemaVersion."] };
192
+ }
193
+ if (snapshot.runId !== runId) issues.push("Snapshot runId does not match the execution.");
194
+ if (snapshot.status !== "snapshot_passed") issues.push("Snapshot did not pass.");
195
+ for (const [label, relativePath, expected] of [
196
+ ["stdout", snapshot.command?.stdoutPath, snapshot.command?.stdoutSha256],
197
+ ["stderr", snapshot.command?.stderrPath, snapshot.command?.stderrSha256],
198
+ ]) {
199
+ try {
200
+ const absolutePath = resolveWithinRoot(workspaceRoot, relativePath, `${label} evidence path`);
201
+ const actual = sha256(readFileSync(absolutePath));
202
+ if (actual !== expected) issues.push(`${label} evidence hash mismatch.`);
203
+ } catch (error) {
204
+ issues.push(error instanceof Error ? error.message : String(error));
205
+ }
206
+ }
207
+ for (const artifact of snapshot.command?.evidence?.outputArtifacts ?? []) {
208
+ try {
209
+ const absolutePath = resolveWithinRoot(workspaceRoot, artifact.path, "output artifact path");
210
+ const actual = sha256(readFileSync(absolutePath));
211
+ if (actual !== artifact.sha256) issues.push(`output artifact hash mismatch: ${artifact.path}`);
212
+ } catch (error) {
213
+ issues.push(error instanceof Error ? error.message : String(error));
214
+ }
215
+ }
216
+ if (snapshot.command?.evidence?.kind === "automation") {
217
+ const manifest = snapshot.command.evidence.outputArtifacts?.find(
218
+ (artifact) => artifact.path === snapshot.visual?.automationManifestPath,
219
+ );
220
+ if (!manifest || manifest.sha256 !== snapshot.command.evidence.automationManifestSha256) {
221
+ issues.push("Automation manifest is not bound to the replayable evidence hash.");
222
+ }
223
+ if (!snapshot.command.evidence.outputArtifacts?.some(
224
+ (artifact) => artifact.path === snapshot.visual?.resultPath,
225
+ )) {
226
+ issues.push("Automation result is not bound as an output artifact.");
227
+ }
228
+ }
229
+ if (!stableTarget(snapshot.repositoryBefore, snapshot.repositoryAfter)) {
230
+ issues.push("Snapshot target changed while the command ran.");
231
+ }
232
+ return { valid: issues.length === 0, issues };
233
+ }
234
+
235
+ export function verifySnapshotFreshness(snapshot) {
236
+ try {
237
+ const current = captureTargetState({
238
+ targetRoot: realpathSync(snapshot.targetRoot),
239
+ selectedPaths: Object.keys(snapshot.repositoryAfter.selectedFiles),
240
+ });
241
+ const valid = current.head === snapshot.repositoryAfter.head
242
+ && current.manifestFingerprint === snapshot.repositoryAfter.manifestFingerprint;
243
+ return {
244
+ valid,
245
+ current,
246
+ issues: valid ? [] : ["Snapshot target HEAD or selected manifest changed."],
247
+ };
248
+ } catch (error) {
249
+ return { valid: false, current: null, issues: [error instanceof Error ? error.message : String(error)] };
250
+ }
251
+ }
252
+
253
+ export function runSnapshot({ workspaceRoot, runId, request }) {
254
+ validateRequest(request);
255
+ const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
256
+ if (pinGate.status !== "valid") return pinGate;
257
+ const targetRoot = realpathSync(request.targetRoot);
258
+ if (request.mode === "project" && targetRoot !== workspaceRoot) {
259
+ throw new Error("Project snapshot targetRoot must equal the RunKit workspace.");
260
+ }
261
+ const cwd = request.cwd === "."
262
+ ? targetRoot
263
+ : realpathSync(resolveWithinRoot(targetRoot, request.cwd, "Snapshot cwd"));
264
+ const snapshotRoot = path.join(executionRoot, "snapshots");
265
+ const evidenceRoot = path.join(executionRoot, "snapshot-evidence");
266
+ const snapshotPath = path.join(snapshotRoot, `${request.snapshotId}.json`);
267
+ const stdoutPath = path.join(evidenceRoot, `${request.snapshotId}.stdout`);
268
+ const stderrPath = path.join(evidenceRoot, `${request.snapshotId}.stderr`);
269
+ const before = captureTargetState({ targetRoot, selectedPaths: request.selectedPaths });
270
+ const startedAt = new Date().toISOString();
271
+ const startedMs = Date.now();
272
+ const completed = spawnSync(request.executable, request.argv, {
273
+ cwd,
274
+ encoding: null,
275
+ env: process.env,
276
+ maxBuffer: 64 * 1024 * 1024,
277
+ });
278
+ const endedAt = new Date().toISOString();
279
+ const stdout = completed.stdout ?? Buffer.alloc(0);
280
+ const stderr = completed.stderr ?? Buffer.from(completed.error?.message ?? "", "utf8");
281
+ const after = captureTargetState({ targetRoot, selectedPaths: request.selectedPaths });
282
+ const targetStable = stableTarget(before, after);
283
+ const exitCode = Number.isInteger(completed.status) ? completed.status : 1;
284
+ const status = !targetStable
285
+ ? "invalidated_by_target_write"
286
+ : exitCode === 0
287
+ ? "snapshot_passed"
288
+ : "snapshot_failed";
289
+ const contextFingerprint = verificationContextFingerprint(request.verificationContext);
290
+ const stdoutRelative = relativeToWorkspace(workspaceRoot, stdoutPath);
291
+ const stderrRelative = relativeToWorkspace(workspaceRoot, stderrPath);
292
+ mkdirSync(evidenceRoot, { recursive: true });
293
+ writeFileSync(stdoutPath, stdout, { flag: "wx" });
294
+ writeFileSync(stderrPath, stderr, { flag: "wx" });
295
+ const snapshot = {
296
+ schemaVersion: "OwlCodaRunKitSnapshotV1",
297
+ runId,
298
+ snapshotId: request.snapshotId,
299
+ status,
300
+ mode: request.mode,
301
+ targetRoot,
302
+ verificationContextFingerprint: contextFingerprint,
303
+ repositoryBefore: before,
304
+ repositoryAfter: after,
305
+ command: {
306
+ startedAt,
307
+ endedAt,
308
+ durationMs: Date.now() - startedMs,
309
+ exitCode,
310
+ stdoutPath: stdoutRelative,
311
+ stdoutSha256: sha256(stdout),
312
+ stdoutLineCount: lineCount(stdout),
313
+ stderrPath: stderrRelative,
314
+ stderrSha256: sha256(stderr),
315
+ stderrLineCount: lineCount(stderr),
316
+ evidence: {
317
+ schemaVersion: "OwlCodaRunKitReplayableEvidenceV1",
318
+ kind: "shell",
319
+ cwd: request.cwd,
320
+ launcher: { executable: request.executable, version: request.launcherVersion },
321
+ argv: [request.executable, ...request.argv],
322
+ toolVersions: structuredClone(request.toolVersions),
323
+ materialInputs: Object.entries(before.selectedFiles).map(([id, hash]) => ({ id, sha256: hash })),
324
+ outputArtifacts: [
325
+ { path: stdoutRelative, sha256: sha256(stdout) },
326
+ { path: stderrRelative, sha256: sha256(stderr) },
327
+ ],
328
+ },
329
+ },
330
+ authorizationGranted: false,
331
+ };
332
+ writeJsonExclusive(snapshotPath, snapshot);
333
+ return {
334
+ status,
335
+ exitCode: status === "snapshot_passed" ? 0 : 2,
336
+ runId,
337
+ snapshotPath: relativeToWorkspace(workspaceRoot, snapshotPath),
338
+ authorizationGranted: false,
339
+ };
340
+ }