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,997 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ realpathSync,
9
+ } from "node:fs";
10
+ import path from "node:path";
11
+
12
+ import { createDeliveryFromLeaseWithinControlTransaction } from "./delivery-create.mjs";
13
+ import { collapseByteIdenticalDeliveryCandidates } from "./delivery-selection.mjs";
14
+ import { runFinalize } from "./finalize.mjs";
15
+ import {
16
+ listLeaseArtifacts,
17
+ withControlTransaction,
18
+ } from "./lease-lifecycle.mjs";
19
+ import { derivedVerificationContext } from "./lifecycle-orchestration.mjs";
20
+ import { resolveProfileImpactDetailed } from "./profile-impact.mjs";
21
+ import {
22
+ loadActiveExecution,
23
+ readJson,
24
+ relativeToWorkspace,
25
+ safeIdentifier,
26
+ safeRelativePath,
27
+ sha256,
28
+ writeJsonAtomically,
29
+ writeJsonExclusiveAtomically,
30
+ } from "./provenance-common.mjs";
31
+ import { validateReceiptLineage } from "./receipt-lineage.mjs";
32
+ import { runSnapshot } from "./snapshot.mjs";
33
+ import { verifyDeliveryPacket } from "./source-fingerprint.mjs";
34
+ import { verificationContextFingerprint } from "./verification-context.mjs";
35
+
36
+ const STATUS_MODE = "porcelain-v1-z-untracked-all-runkit-excluded";
37
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
38
+ const RISK_CATEGORIES = new Set(["migration", "backtest", "release", "funds", "production"]);
39
+
40
+ function regularFile(filePath, label) {
41
+ const stat = lstatSync(filePath);
42
+ if (stat.isSymbolicLink() || !stat.isFile()) {
43
+ throw new Error(`${label} must be a regular file, not a symlink.`);
44
+ }
45
+ return realpathSync(filePath);
46
+ }
47
+
48
+ function matchesRule(filePath, rule) {
49
+ return rule.endsWith("/**")
50
+ ? filePath.startsWith(`${rule.slice(0, -3)}/`)
51
+ : filePath === rule;
52
+ }
53
+
54
+ function isOwned(filePath, ownedPaths) {
55
+ return ownedPaths.some(ownedPath => matchesRule(filePath, ownedPath));
56
+ }
57
+
58
+ function gitStatusRecords(workspaceRoot) {
59
+ const completed = spawnSync("git", [
60
+ "-C",
61
+ workspaceRoot,
62
+ "status",
63
+ "--porcelain=v1",
64
+ "-z",
65
+ "--untracked-files=all",
66
+ "--",
67
+ ".",
68
+ ":(exclude).owlcoda/runkit/**",
69
+ ], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
70
+ if (completed.error) throw completed.error;
71
+ if (completed.status !== 0) throw new Error(`Git status failed: ${completed.stderr.trim()}`);
72
+ const entries = completed.stdout.split("\0");
73
+ const records = [];
74
+ for (let index = 0; index < entries.length; index += 1) {
75
+ const entry = entries[index];
76
+ if (!entry) continue;
77
+ if (entry.length < 4 || entry[2] !== " ") throw new Error("Git status emitted malformed porcelain.");
78
+ const status = entry.slice(0, 2);
79
+ const currentPath = safeRelativePath(entry.slice(3), "Git status path");
80
+ const paths = [currentPath];
81
+ if (status.includes("R") || status.includes("C")) {
82
+ const priorPath = entries[index + 1];
83
+ if (!priorPath) throw new Error("Git status rename entry is incomplete.");
84
+ paths.push(safeRelativePath(priorPath, "Git status prior path"));
85
+ index += 1;
86
+ }
87
+ records.push({ status, currentPath, paths });
88
+ }
89
+ return records;
90
+ }
91
+
92
+ function currentOwnedFiles(workspaceRoot, ownedPaths) {
93
+ const files = {};
94
+ const blockers = [];
95
+ for (const record of gitStatusRecords(workspaceRoot)) {
96
+ const owned = record.paths.filter(filePath => isOwned(filePath, ownedPaths));
97
+ if (owned.length === 0) continue;
98
+ if (record.status.includes("R") || record.status.includes("C") || record.status.includes("D")) {
99
+ blockers.push(...owned);
100
+ continue;
101
+ }
102
+ if (!isOwned(record.currentPath, ownedPaths)) continue;
103
+ const absolutePath = regularFile(
104
+ path.join(workspaceRoot, record.currentPath),
105
+ `Owned repair source ${record.currentPath}`,
106
+ );
107
+ files[record.currentPath] = sha256(readFileSync(absolutePath));
108
+ }
109
+ return {
110
+ files: Object.fromEntries(Object.entries(files).sort(([left], [right]) => left.localeCompare(right))),
111
+ blockers: [...new Set(blockers)].sort(),
112
+ };
113
+ }
114
+
115
+ function packetFiles(packet) {
116
+ return packet?.changedFiles?.files ?? packet?.changedFiles?.wholeFileSha256 ?? null;
117
+ }
118
+
119
+ function matchingCurrentPackets({ workspaceRoot, executionRoot, workItemId, files }) {
120
+ const root = path.join(executionRoot, "delivery-packets");
121
+ const matches = [];
122
+ for (const entry of readdirSync(root, { withFileTypes: true })
123
+ .filter(candidate => candidate.name.endsWith(".json"))
124
+ .sort((left, right) => left.name.localeCompare(right.name))) {
125
+ if (entry.isSymbolicLink() || !entry.isFile()) {
126
+ throw new Error(`Delivery packet must be a regular file: ${entry.name}`);
127
+ }
128
+ const packetPath = regularFile(path.join(root, entry.name), `Delivery packet ${entry.name}`);
129
+ const packet = readJson(packetPath);
130
+ if (packet.discovery?.fromLease !== workItemId) continue;
131
+ if (JSON.stringify(packetFiles(packet)) !== JSON.stringify(files)) continue;
132
+ const gate = verifyDeliveryPacket({ workspaceRoot, packet });
133
+ if (gate.status !== "valid") continue;
134
+ matches.push({
135
+ packet,
136
+ packetPath,
137
+ sourceFingerprint: gate.recomputedFingerprint,
138
+ });
139
+ }
140
+ return collapseByteIdenticalDeliveryCandidates(matches);
141
+ }
142
+
143
+ function nextRepairSequence(executionRoot) {
144
+ const plansRoot = path.join(executionRoot, "repair-plans");
145
+ if (!existsSync(plansRoot)) return 1;
146
+ const values = readdirSync(plansRoot)
147
+ .map(name => /^repair-(\d+)-plan\.json$/.exec(name)?.[1])
148
+ .filter(Boolean)
149
+ .map(Number);
150
+ return values.length === 0 ? 1 : Math.max(...values) + 1;
151
+ }
152
+
153
+ function repairId(sequence) {
154
+ return `repair-${String(sequence).padStart(3, "0")}`;
155
+ }
156
+
157
+ function selectPacket({
158
+ workspaceRoot,
159
+ executionRoot,
160
+ runId,
161
+ lease,
162
+ sequence,
163
+ }) {
164
+ const current = currentOwnedFiles(workspaceRoot, lease.ownedPaths);
165
+ if (current.blockers.length > 0) {
166
+ throw new Error(`Repair cannot infer deleted or renamed owned paths: ${current.blockers.join(", ")}`);
167
+ }
168
+ if (Object.keys(current.files).length === 0) {
169
+ throw new Error("Repair requires at least one changed file owned by the active lease.");
170
+ }
171
+ const matching = matchingCurrentPackets({
172
+ workspaceRoot,
173
+ executionRoot,
174
+ workItemId: lease.workItemId,
175
+ files: current.files,
176
+ });
177
+ if (matching.length > 1) {
178
+ return { ...matching.at(-1), ambiguous: true };
179
+ }
180
+ if (matching.length === 1) return { ...matching[0], ambiguous: false };
181
+ const created = createDeliveryFromLeaseWithinControlTransaction({
182
+ workspaceRoot,
183
+ runId,
184
+ workItemId: lease.workItemId,
185
+ packetId: `${repairId(sequence)}-delivery`,
186
+ });
187
+ if (created.status !== "delivery_packet_created") {
188
+ throw new Error(`Repair could not create a current DeliveryPacket: ${created.status}`);
189
+ }
190
+ const packetPath = path.join(workspaceRoot, created.deliveryPacketPath);
191
+ return {
192
+ packet: readJson(packetPath),
193
+ packetPath,
194
+ sourceFingerprint: created.sourceFingerprint,
195
+ ambiguous: false,
196
+ };
197
+ }
198
+
199
+ function activeReceipt(executionRoot) {
200
+ const lineagePath = path.join(executionRoot, "verification-receipts", "receipt-lineage.json");
201
+ if (!existsSync(lineagePath)) throw new Error("Repair requires an existing verification receipt lineage.");
202
+ const lineage = readJson(regularFile(lineagePath, "Receipt lineage"));
203
+ const validation = validateReceiptLineage(lineage);
204
+ if (!validation.valid || !validation.active) {
205
+ throw new Error("Repair requires one unambiguous active verification receipt.");
206
+ }
207
+ return { lineagePath, lineage, active: validation.active };
208
+ }
209
+
210
+ function normalizeRisk(goal) {
211
+ const riskMode = goal.riskMode ?? "standard";
212
+ if (!new Set(["lightweight", "standard", "full"]).has(riskMode)) {
213
+ return {
214
+ riskMode: "full",
215
+ riskCategories: [],
216
+ invalid: true,
217
+ };
218
+ }
219
+ const categories = Array.isArray(goal.riskCategories)
220
+ ? [...new Set(goal.riskCategories)].sort()
221
+ : [];
222
+ const invalid = categories.some(category => !RISK_CATEGORIES.has(category));
223
+ return { riskMode, riskCategories: invalid ? [] : categories, invalid };
224
+ }
225
+
226
+ function normalizeProfileCommand(command, profileId) {
227
+ if (typeof command?.id !== "string"
228
+ || command.id.length === 0
229
+ || typeof command.cwd !== "string"
230
+ || typeof command.executable !== "string"
231
+ || command.executable.length === 0
232
+ || !Array.isArray(command.argv)
233
+ || command.argv.some(value => typeof value !== "string")) {
234
+ throw new Error(`Profile command is invalid: ${profileId}`);
235
+ }
236
+ safeIdentifier(command.id, "profile command id");
237
+ safeRelativePath(command.cwd, "profile command cwd", { allowDot: true });
238
+ return {
239
+ id: command.id,
240
+ cwd: command.cwd,
241
+ executable: command.executable,
242
+ argv: [...command.argv],
243
+ };
244
+ }
245
+
246
+ function legacyReceiptCommandDefinitions({ parentReceipt, selectedProfileIds, files }) {
247
+ const parentProfiles = new Set(parentReceipt.selectedProfileIds ?? []);
248
+ if (selectedProfileIds.some(profileId => !parentProfiles.has(profileId))) return [];
249
+ return (parentReceipt.commandReceipts ?? []).map((commandReceipt, index) => {
250
+ const evidence = commandReceipt?.evidence;
251
+ const executable = evidence?.launcher?.executable;
252
+ if (typeof executable !== "string"
253
+ || !path.isAbsolute(executable)
254
+ || !Array.isArray(evidence.argv)
255
+ || evidence.argv[0] !== executable
256
+ || typeof evidence.cwd !== "string") {
257
+ return {
258
+ id: `legacy-${index + 1}`,
259
+ profileIds: [...selectedProfileIds],
260
+ sourcePaths: Object.keys(files).sort(),
261
+ missing: true,
262
+ };
263
+ }
264
+ return {
265
+ id: `legacy-${index + 1}`,
266
+ cwd: evidence.cwd,
267
+ executable,
268
+ argv: [...evidence.argv.slice(1)],
269
+ profileIds: [...selectedProfileIds],
270
+ sourcePaths: Object.keys(files).sort(),
271
+ missing: false,
272
+ };
273
+ });
274
+ }
275
+
276
+ function commandDefinitions(profiles, selectedProfileIds, files, parentReceipt) {
277
+ const byId = new Map();
278
+ const selectedProfiles = profiles.filter(item => selectedProfileIds.includes(item.id));
279
+ const profilesWithCommands = selectedProfiles.filter(profile =>
280
+ Array.isArray(profile.commands) && profile.commands.length > 0);
281
+ if (profilesWithCommands.length === 0) {
282
+ return legacyReceiptCommandDefinitions({
283
+ parentReceipt,
284
+ selectedProfileIds,
285
+ files,
286
+ });
287
+ }
288
+ for (const profile of selectedProfiles) {
289
+ if (!Array.isArray(profile.commands) || profile.commands.length === 0) {
290
+ byId.set(`__missing__${profile.id}`, {
291
+ id: `__missing__${profile.id}`,
292
+ profileIds: [profile.id],
293
+ sourcePaths: Object.keys(files).filter(filePath =>
294
+ profile.paths.some(rule => matchesRule(filePath, rule))),
295
+ missing: true,
296
+ });
297
+ continue;
298
+ }
299
+ for (const rawCommand of profile.commands) {
300
+ const command = normalizeProfileCommand(rawCommand, profile.id);
301
+ const current = byId.get(command.id);
302
+ if (current
303
+ && JSON.stringify({
304
+ cwd: current.cwd,
305
+ executable: current.executable,
306
+ argv: current.argv,
307
+ }) !== JSON.stringify({
308
+ cwd: command.cwd,
309
+ executable: command.executable,
310
+ argv: command.argv,
311
+ })) {
312
+ throw new Error(`Profile command id has conflicting definitions: ${command.id}`);
313
+ }
314
+ const sourcePaths = Object.keys(files).filter(filePath =>
315
+ profile.paths.some(rule => matchesRule(filePath, rule)));
316
+ byId.set(command.id, {
317
+ ...command,
318
+ profileIds: [...new Set([...(current?.profileIds ?? []), profile.id])].sort(),
319
+ sourcePaths: [...new Set([...(current?.sourcePaths ?? []), ...sourcePaths])].sort(),
320
+ missing: false,
321
+ });
322
+ }
323
+ }
324
+ return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
325
+ }
326
+
327
+ function exactCommandMatches(command, commandReceipt) {
328
+ const evidence = commandReceipt?.evidence;
329
+ const executable = evidence?.launcher?.executable;
330
+ if (typeof executable !== "string"
331
+ || evidence.cwd !== command.cwd
332
+ || !Array.isArray(evidence.argv)
333
+ || evidence.argv[0] !== executable
334
+ || JSON.stringify(evidence.argv.slice(1)) !== JSON.stringify(command.argv)) {
335
+ return false;
336
+ }
337
+ return command.executable.includes("/")
338
+ ? command.executable === executable
339
+ : path.basename(executable) === command.executable
340
+ || path.basename(executable) === path.basename(command.executable);
341
+ }
342
+
343
+ function materialInputsReusable({ workspaceRoot, commandReceipt, sourcePaths, files }) {
344
+ const materials = new Map(
345
+ (commandReceipt.evidence?.materialInputs ?? []).map(item => [item.id, item.sha256]),
346
+ );
347
+ for (const sourcePath of sourcePaths) {
348
+ if (materials.get(sourcePath) !== files[sourcePath]) return false;
349
+ }
350
+ for (const [materialPath, expected] of materials) {
351
+ try {
352
+ const actual = sha256(readFileSync(regularFile(
353
+ path.join(workspaceRoot, materialPath),
354
+ `Reusable command material ${materialPath}`,
355
+ )));
356
+ if (actual !== expected) return false;
357
+ } catch {
358
+ return false;
359
+ }
360
+ }
361
+ return true;
362
+ }
363
+
364
+ function resolveExecutable(value) {
365
+ let candidate = value;
366
+ if (!path.isAbsolute(candidate)) {
367
+ candidate = execFileSync("/usr/bin/which", [candidate], { encoding: "utf8" }).trim();
368
+ }
369
+ return regularFile(candidate, `Repair executable ${value}`);
370
+ }
371
+
372
+ function executableBinding(commandId, executable) {
373
+ return {
374
+ commandId,
375
+ executable,
376
+ sha256: sha256(readFileSync(executable)),
377
+ };
378
+ }
379
+
380
+ function classifyCommands({
381
+ workspaceRoot,
382
+ profiles,
383
+ selectedProfileIds,
384
+ files,
385
+ parentReceipt,
386
+ currentContextFingerprint,
387
+ }) {
388
+ const definitions = commandDefinitions(profiles, selectedProfileIds, files, parentReceipt);
389
+ const reusable = [];
390
+ const pending = [];
391
+ const blocked = [];
392
+ if (definitions.length === 0) {
393
+ return {
394
+ reusable,
395
+ pending,
396
+ blocked: [{ commandId: "__command_metadata__", issueCode: "repair_plan_incomplete" }],
397
+ };
398
+ }
399
+ const parentSelected = new Set(parentReceipt.selectedProfileIds ?? []);
400
+ for (const command of definitions) {
401
+ if (command.missing) {
402
+ blocked.push({ commandId: command.id, issueCode: "repair_plan_incomplete" });
403
+ continue;
404
+ }
405
+ const selectedPaths = command.sourcePaths.length === 0
406
+ ? Object.keys(files).sort()
407
+ : [...command.sourcePaths];
408
+ let binding;
409
+ try {
410
+ const executable = resolveExecutable(command.executable);
411
+ binding = executableBinding(command.id, executable);
412
+ } catch {
413
+ blocked.push({ commandId: command.id, issueCode: "repair_plan_incomplete" });
414
+ continue;
415
+ }
416
+ const matches = (parentReceipt.commandReceipts ?? [])
417
+ .filter(commandReceipt => exactCommandMatches(command, commandReceipt));
418
+ if (matches.length > 1) {
419
+ blocked.push({ commandId: command.id, issueCode: "repair_plan_incomplete" });
420
+ continue;
421
+ }
422
+ if (matches.length === 1
423
+ && parentReceipt.verificationContextFingerprint === currentContextFingerprint
424
+ && materialInputsReusable({
425
+ workspaceRoot,
426
+ commandReceipt: matches[0],
427
+ sourcePaths: selectedPaths,
428
+ files,
429
+ })) {
430
+ reusable.push({
431
+ commandId: command.id,
432
+ commandReceipt: matches[0],
433
+ ...binding,
434
+ });
435
+ continue;
436
+ }
437
+ if (matches.length === 0
438
+ && command.profileIds.some(profileId => parentSelected.has(profileId))) {
439
+ blocked.push({ commandId: command.id, issueCode: "repair_plan_incomplete" });
440
+ continue;
441
+ }
442
+ pending.push({
443
+ ...binding,
444
+ argv: [...command.argv],
445
+ cwd: command.cwd,
446
+ selectedPaths,
447
+ });
448
+ }
449
+ return { reusable, pending, blocked };
450
+ }
451
+
452
+ function repairPlan({
453
+ workspaceRoot,
454
+ runId,
455
+ selectedPacket,
456
+ selectedPacketPath,
457
+ selectedPacketSha256,
458
+ active,
459
+ invalidReason,
460
+ classification,
461
+ risk,
462
+ packetAmbiguous,
463
+ }) {
464
+ const trustBlocked = risk.invalid || risk.riskMode === "full";
465
+ const blockedCommands = [
466
+ ...classification.blocked,
467
+ ...(packetAmbiguous
468
+ ? [{ commandId: "__delivery_packet__", issueCode: "repair_plan_incomplete" }]
469
+ : []),
470
+ ...(trustBlocked
471
+ ? [...classification.reusable, ...classification.pending].map(command => ({
472
+ commandId: command.commandId,
473
+ issueCode: "repair_plan_incomplete",
474
+ }))
475
+ : []),
476
+ ].filter((entry, index, values) =>
477
+ values.findIndex(candidate => candidate.commandId === entry.commandId) === index);
478
+ return {
479
+ schemaVersion: "OwlCodaRepairPlanV1",
480
+ runId,
481
+ selectedPacket: {
482
+ packetId: path.basename(selectedPacketPath, ".json"),
483
+ path: relativeToWorkspace(workspaceRoot, selectedPacketPath),
484
+ sha256: `sha256:${selectedPacketSha256}`,
485
+ sourceFingerprint: `sha256:${selectedPacket.sourceFingerprint.sha256}`,
486
+ },
487
+ currentSourceFingerprint: `sha256:${selectedPacket.sourceFingerprint.sha256}`,
488
+ invalidReceiptRef: {
489
+ receiptId: active.receipt.receiptId,
490
+ sha256: `sha256:${active.receiptSha256}`,
491
+ },
492
+ invalidReason,
493
+ reusableCommandIds: classification.reusable.map(item => item.commandId).sort(),
494
+ pendingReplayCommands: classification.pending.map(command => ({
495
+ commandId: command.commandId,
496
+ executable: command.executable,
497
+ argv: [...command.argv],
498
+ cwd: command.cwd,
499
+ })),
500
+ blockedCommands: blockedCommands.sort((left, right) => left.commandId.localeCompare(right.commandId)),
501
+ requiredTrust: {
502
+ riskMode: risk.riskMode,
503
+ riskCategories: [...risk.riskCategories],
504
+ implementationActor: risk.riskMode === "full",
505
+ independentReviewer: risk.riskMode === "full",
506
+ },
507
+ expectedSupersedes: `sha256:${active.receiptSha256}`,
508
+ sourceStabilityGate: "fail_on_any_source_drift_during_repair",
509
+ planStatus: blockedCommands.length === 0 ? "ready" : "repair_plan_incomplete",
510
+ authorizationGranted: false,
511
+ };
512
+ }
513
+
514
+ function attemptDocument({
515
+ runId,
516
+ id,
517
+ planPath,
518
+ replacementReceiptId,
519
+ repairControl,
520
+ }) {
521
+ return {
522
+ schemaVersion: "OwlCodaRepairAttemptV1",
523
+ runId,
524
+ repairId: id,
525
+ repairPlanPath: planPath,
526
+ replacementReceiptId,
527
+ repairControl,
528
+ status: "running",
529
+ commandAttempts: [],
530
+ issueCodes: [],
531
+ authorizationGranted: false,
532
+ };
533
+ }
534
+
535
+ function readControlState(workspaceRoot, executionRoot) {
536
+ const executionPlanPath = regularFile(
537
+ path.join(executionRoot, "execution-plan.json"),
538
+ "Execution plan",
539
+ );
540
+ const goalPath = regularFile(path.join(executionRoot, "goal-contract.json"), "Goal contract");
541
+ const profilesPath = regularFile(
542
+ path.join(workspaceRoot, ".owlcoda/runkit/profiles.json"),
543
+ "RunKit profiles",
544
+ );
545
+ const executionPlan = readJson(executionPlanPath);
546
+ const goalBytes = readFileSync(goalPath);
547
+ const profileBytes = readFileSync(profilesPath);
548
+ const goalContractSha256 = sha256(goalBytes);
549
+ return {
550
+ executionPlan,
551
+ goal: JSON.parse(goalBytes.toString("utf8")),
552
+ profilesDocument: JSON.parse(profileBytes.toString("utf8")),
553
+ goalContractSha256,
554
+ profilesSha256: sha256(profileBytes),
555
+ goalBindingValid: SHA256_PATTERN.test(executionPlan.goalContractSha256 ?? "")
556
+ && executionPlan.goalContractSha256 === goalContractSha256,
557
+ };
558
+ }
559
+
560
+ function repairControlBinding({
561
+ planPath,
562
+ planSha256,
563
+ controlState,
564
+ classification,
565
+ }) {
566
+ return {
567
+ repairPlanPath: planPath,
568
+ repairPlanSha256: planSha256,
569
+ parentBindingMode: "receipt",
570
+ goalContractSha256: controlState.goalContractSha256,
571
+ risk: {
572
+ riskMode: classification.risk.riskMode,
573
+ riskCategories: [...classification.risk.riskCategories],
574
+ },
575
+ profilesSha256: controlState.profilesSha256,
576
+ executableBindings: [...classification.reusable, ...classification.pending]
577
+ .map(({ commandId, executable, sha256: executableSha256 }) => ({
578
+ commandId,
579
+ executable,
580
+ sha256: executableSha256,
581
+ }))
582
+ .sort((left, right) => left.commandId.localeCompare(right.commandId)),
583
+ };
584
+ }
585
+
586
+ function repairBindingsValid({
587
+ workspaceRoot,
588
+ executionRoot,
589
+ packet,
590
+ repairControl,
591
+ }) {
592
+ if (verifyDeliveryPacket({ workspaceRoot, packet }).status !== "valid") return false;
593
+ try {
594
+ const current = readControlState(workspaceRoot, executionRoot);
595
+ const currentRisk = normalizeRisk(current.goal);
596
+ if (repairControl.parentBindingMode !== "receipt"
597
+ || !current.goalBindingValid
598
+ || current.goalContractSha256 !== repairControl.goalContractSha256
599
+ || currentRisk.invalid
600
+ || currentRisk.riskMode !== repairControl.risk.riskMode
601
+ || JSON.stringify(currentRisk.riskCategories)
602
+ !== JSON.stringify(repairControl.risk.riskCategories)
603
+ || current.profilesSha256 !== repairControl.profilesSha256) {
604
+ return false;
605
+ }
606
+ return repairControl.executableBindings.every(binding => {
607
+ const executable = regularFile(binding.executable, `Repair executable ${binding.commandId}`);
608
+ return executable === binding.executable
609
+ && sha256(readFileSync(executable)) === binding.sha256;
610
+ });
611
+ } catch {
612
+ return false;
613
+ }
614
+ }
615
+
616
+ function resultForAttempt({
617
+ status,
618
+ exitCode,
619
+ runId,
620
+ planPath,
621
+ planSha256,
622
+ attemptPath,
623
+ replacementReceiptId,
624
+ reusable,
625
+ replayed,
626
+ sourceFingerprint,
627
+ issueCodes = [],
628
+ extra = {},
629
+ }) {
630
+ return {
631
+ status,
632
+ exitCode,
633
+ runId,
634
+ repairPlanPath: planPath,
635
+ repairPlanSha256: planSha256,
636
+ repairAttemptPath: attemptPath,
637
+ replacementReceiptId,
638
+ reusableCommandIds: reusable.map(item => item.commandId).sort(),
639
+ replayedCommandIds: replayed.map(item => item.commandId),
640
+ sourceFingerprint,
641
+ issueCodes,
642
+ authorizationGranted: false,
643
+ ...extra,
644
+ };
645
+ }
646
+
647
+ function runRepairWithinControlTransaction({
648
+ workspaceRoot,
649
+ runId,
650
+ onPlan,
651
+ onBeforeReceiptPersist,
652
+ onBeforeLineageActivate,
653
+ }) {
654
+ safeIdentifier(runId, "run-id");
655
+ const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
656
+ if (pinGate.status !== "valid") return { ...pinGate, authorizationGranted: false };
657
+ const activeLeases = listLeaseArtifacts({ workspaceRoot, executionRoot })
658
+ .filter(lease => lease.state === "active");
659
+ if (activeLeases.length !== 1) {
660
+ throw new Error("Repair requires exactly one active writer lease.");
661
+ }
662
+ const sequence = nextRepairSequence(executionRoot);
663
+ const id = repairId(sequence);
664
+ const selected = selectPacket({
665
+ workspaceRoot,
666
+ executionRoot,
667
+ runId,
668
+ lease: activeLeases[0],
669
+ sequence,
670
+ });
671
+ const selectedPacketBytes = readFileSync(selected.packetPath);
672
+ const files = packetFiles(selected.packet);
673
+ const receipts = activeReceipt(executionRoot);
674
+ const parent = receipts.active.receipt;
675
+ const currentContext = derivedVerificationContext(workspaceRoot);
676
+ const currentContextFingerprint = verificationContextFingerprint(currentContext);
677
+ const controlState = readControlState(workspaceRoot, executionRoot);
678
+ const profilesDocument = controlState.profilesDocument;
679
+ const detailedProfileImpact = resolveProfileImpactDetailed({
680
+ changedPaths: Object.keys(files),
681
+ profiles: profilesDocument.profiles,
682
+ });
683
+ const profileImpact = {
684
+ decision: detailedProfileImpact.decision,
685
+ profileIds: detailedProfileImpact.selectedProfileIds,
686
+ uncoveredPaths: detailedProfileImpact.uncoveredPaths,
687
+ };
688
+ const risk = normalizeRisk(controlState.goal);
689
+ const parentHasGoalBinding = SHA256_PATTERN.test(parent.goalContractSha256 ?? "")
690
+ && parent.risk !== undefined;
691
+ const parentGoalBindingValid = parentHasGoalBinding
692
+ && parent.goalContractSha256 === controlState.goalContractSha256
693
+ && parent.risk?.riskMode === risk.riskMode
694
+ && JSON.stringify(parent.risk?.riskCategories) === JSON.stringify(risk.riskCategories);
695
+ const legacyParentBindingMissing = parent.goalContractSha256 === undefined
696
+ && parent.risk === undefined;
697
+ const invalidReason = parent.sourceFingerprint !== selected.sourceFingerprint
698
+ ? "receipt_source_mismatch"
699
+ : parent.status === "invalidated_by_concurrent_write"
700
+ ? "source_mutated_during_verification"
701
+ : "repair_plan_incomplete";
702
+ const classification = profileImpact.decision === "targeted_profiles"
703
+ ? classifyCommands({
704
+ workspaceRoot,
705
+ profiles: profilesDocument.profiles,
706
+ selectedProfileIds: profileImpact.profileIds,
707
+ files,
708
+ parentReceipt: parent,
709
+ currentContextFingerprint,
710
+ })
711
+ : {
712
+ reusable: [],
713
+ pending: [],
714
+ blocked: [{ commandId: "__profile_impact__", issueCode: "repair_plan_incomplete" }],
715
+ };
716
+ if (invalidReason === "repair_plan_incomplete") {
717
+ classification.blocked.push({
718
+ commandId: "__active_receipt__",
719
+ issueCode: "repair_plan_incomplete",
720
+ });
721
+ }
722
+ if (!controlState.goalBindingValid || !parentGoalBindingValid) {
723
+ classification.blocked.push({
724
+ commandId: "__goal_contract__",
725
+ issueCode: "repair_plan_incomplete",
726
+ });
727
+ }
728
+ const plan = repairPlan({
729
+ workspaceRoot,
730
+ runId,
731
+ selectedPacket: selected.packet,
732
+ selectedPacketPath: selected.packetPath,
733
+ selectedPacketSha256: sha256(selectedPacketBytes),
734
+ active: receipts.active,
735
+ invalidReason,
736
+ classification,
737
+ risk,
738
+ packetAmbiguous: selected.ambiguous,
739
+ });
740
+ const planPath = path.join(executionRoot, "repair-plans", `${id}-plan.json`);
741
+ writeJsonExclusiveAtomically(planPath, plan);
742
+ const planRelative = relativeToWorkspace(workspaceRoot, planPath);
743
+ const planSha256 = sha256(readFileSync(planPath));
744
+ if (typeof onPlan === "function") {
745
+ onPlan({
746
+ plan,
747
+ planPath: planRelative,
748
+ planSha256,
749
+ });
750
+ }
751
+ if (plan.planStatus !== "ready") {
752
+ return {
753
+ status: "repair_plan_incomplete",
754
+ exitCode: 3,
755
+ runId,
756
+ repairPlanPath: planRelative,
757
+ repairPlanSha256: planSha256,
758
+ sourceFingerprint: selected.sourceFingerprint,
759
+ reusableCommandIds: plan.reusableCommandIds,
760
+ replayedCommandIds: [],
761
+ issueCodes: ["repair_plan_incomplete"],
762
+ nextAllowedAction: legacyParentBindingMissing
763
+ ? "run_full_verification_to_bind_goal_and_risk"
764
+ : "repair_plan_inputs_or_trusted_provenance",
765
+ authorizationGranted: false,
766
+ };
767
+ }
768
+
769
+ const replacementReceiptId = `${id}-receipt`;
770
+ const repairControl = repairControlBinding({
771
+ planPath: planRelative,
772
+ planSha256,
773
+ controlState,
774
+ classification: { ...classification, risk },
775
+ });
776
+ if (!repairBindingsValid({
777
+ workspaceRoot,
778
+ executionRoot,
779
+ packet: selected.packet,
780
+ repairControl,
781
+ })) {
782
+ return {
783
+ status: "repair_plan_incomplete",
784
+ exitCode: 3,
785
+ runId,
786
+ repairPlanPath: planRelative,
787
+ repairPlanSha256: planSha256,
788
+ sourceFingerprint: selected.sourceFingerprint,
789
+ reusableCommandIds: plan.reusableCommandIds,
790
+ replayedCommandIds: [],
791
+ issueCodes: ["repair_plan_incomplete"],
792
+ nextAllowedAction: "repair_plan_inputs_or_trusted_provenance",
793
+ authorizationGranted: false,
794
+ };
795
+ }
796
+ const attemptPath = path.join(executionRoot, "repair-attempts", `${id}-attempt.json`);
797
+ const attempt = attemptDocument({
798
+ runId,
799
+ id,
800
+ planPath: planRelative,
801
+ replacementReceiptId,
802
+ repairControl,
803
+ });
804
+ writeJsonExclusiveAtomically(attemptPath, attempt);
805
+ const attemptRelative = relativeToWorkspace(workspaceRoot, attemptPath);
806
+ const replayed = [];
807
+ for (const command of classification.pending) {
808
+ if (!repairBindingsValid({
809
+ workspaceRoot,
810
+ executionRoot,
811
+ packet: selected.packet,
812
+ repairControl,
813
+ })) {
814
+ attempt.status = "repair_plan_incomplete";
815
+ attempt.issueCodes = ["repair_plan_incomplete"];
816
+ writeJsonAtomically(attemptPath, attempt);
817
+ return resultForAttempt({
818
+ status: attempt.status,
819
+ exitCode: 3,
820
+ runId,
821
+ planPath: planRelative,
822
+ planSha256,
823
+ attemptPath: attemptRelative,
824
+ replacementReceiptId,
825
+ reusable: classification.reusable,
826
+ replayed,
827
+ sourceFingerprint: selected.sourceFingerprint,
828
+ issueCodes: attempt.issueCodes,
829
+ extra: { nextAllowedAction: "repair_plan_inputs_or_trusted_provenance" },
830
+ });
831
+ }
832
+ const snapshotRequest = {
833
+ schemaVersion: "OwlCodaRunKitSnapshotRequestV1",
834
+ snapshotId: `${id}-${command.commandId}`,
835
+ mode: "project",
836
+ targetRoot: workspaceRoot,
837
+ cwd: command.cwd,
838
+ executable: command.executable,
839
+ argv: [...command.argv],
840
+ launcherVersion: "runkit-repair-v1",
841
+ toolVersions: structuredClone(currentContext.toolchains),
842
+ selectedPaths: [...command.selectedPaths],
843
+ statusMode: STATUS_MODE,
844
+ verificationContext: currentContext,
845
+ };
846
+ const snapshot = runSnapshot({ workspaceRoot, runId, request: snapshotRequest });
847
+ const snapshotDocument = readJson(path.join(workspaceRoot, snapshot.snapshotPath));
848
+ const commandAttempt = {
849
+ commandId: command.commandId,
850
+ snapshotPath: snapshot.snapshotPath,
851
+ status: snapshotDocument.status,
852
+ exitCode: snapshotDocument.command.exitCode,
853
+ };
854
+ attempt.commandAttempts.push(commandAttempt);
855
+ replayed.push(commandAttempt);
856
+ writeJsonAtomically(attemptPath, attempt);
857
+ if (snapshotDocument.status === "snapshot_failed") {
858
+ attempt.status = "repair_replay_failed";
859
+ attempt.issueCodes = ["repair_replay_failed"];
860
+ writeJsonAtomically(attemptPath, attempt);
861
+ return resultForAttempt({
862
+ status: attempt.status,
863
+ exitCode: 1,
864
+ runId,
865
+ planPath: planRelative,
866
+ planSha256,
867
+ attemptPath: attemptRelative,
868
+ replacementReceiptId,
869
+ reusable: classification.reusable,
870
+ replayed,
871
+ sourceFingerprint: selected.sourceFingerprint,
872
+ issueCodes: attempt.issueCodes,
873
+ extra: { nextAllowedAction: "repair_failed_command_then_retry" },
874
+ });
875
+ }
876
+ if (snapshotDocument.status !== "snapshot_passed"
877
+ || verifyDeliveryPacket({ workspaceRoot, packet: selected.packet }).status !== "valid") {
878
+ attempt.status = "repair_source_drift";
879
+ attempt.issueCodes = ["source_mutated_during_verification"];
880
+ writeJsonAtomically(attemptPath, attempt);
881
+ return resultForAttempt({
882
+ status: attempt.status,
883
+ exitCode: 2,
884
+ runId,
885
+ planPath: planRelative,
886
+ planSha256,
887
+ attemptPath: attemptRelative,
888
+ replacementReceiptId,
889
+ reusable: classification.reusable,
890
+ replayed,
891
+ sourceFingerprint: selected.sourceFingerprint,
892
+ issueCodes: attempt.issueCodes,
893
+ extra: { nextAllowedAction: "replace_delivery_packet" },
894
+ });
895
+ }
896
+ }
897
+
898
+ const finalizeRequest = {
899
+ schemaVersion: "OwlCodaRunKitFinalizeRequestV1",
900
+ receiptId: replacementReceiptId,
901
+ deliveryPacketPath: relativeToWorkspace(workspaceRoot, selected.packetPath),
902
+ verificationContext: currentContext,
903
+ snapshotPaths: replayed.map(item => item.snapshotPath),
904
+ supersedesReceiptSha256: receipts.active.receiptSha256,
905
+ reusedCommandReceipts: classification.reusable.map(item =>
906
+ structuredClone(item.commandReceipt)),
907
+ repairControl,
908
+ };
909
+ const finalizeRequestPath = path.join(
910
+ executionRoot,
911
+ "repair-finalize-requests",
912
+ `${id}-finalize-request.json`,
913
+ );
914
+ writeJsonExclusiveAtomically(finalizeRequestPath, finalizeRequest);
915
+ let finalized;
916
+ try {
917
+ finalized = runFinalize({
918
+ workspaceRoot,
919
+ runId,
920
+ request: finalizeRequest,
921
+ hooks: {
922
+ beforeReceiptPersist: onBeforeReceiptPersist,
923
+ beforeLineageActivate: onBeforeLineageActivate,
924
+ },
925
+ });
926
+ } catch (error) {
927
+ attempt.status = "repair_finalize_failed";
928
+ attempt.issueCodes = ["repair_plan_incomplete"];
929
+ attempt.finalizeError = error instanceof Error ? error.message : String(error);
930
+ writeJsonAtomically(attemptPath, attempt);
931
+ return resultForAttempt({
932
+ status: attempt.status,
933
+ exitCode: 3,
934
+ runId,
935
+ planPath: planRelative,
936
+ planSha256,
937
+ attemptPath: attemptRelative,
938
+ replacementReceiptId,
939
+ reusable: classification.reusable,
940
+ replayed,
941
+ sourceFingerprint: selected.sourceFingerprint,
942
+ issueCodes: attempt.issueCodes,
943
+ extra: {
944
+ nextAllowedAction: "inspect_repair_finalize_failure",
945
+ finalizeError: attempt.finalizeError,
946
+ },
947
+ });
948
+ }
949
+ if (finalized.status !== "accepted_passed") {
950
+ attempt.status = "repair_finalize_failed";
951
+ attempt.issueCodes = ["repair_plan_incomplete"];
952
+ writeJsonAtomically(attemptPath, attempt);
953
+ return resultForAttempt({
954
+ status: attempt.status,
955
+ exitCode: 3,
956
+ runId,
957
+ planPath: planRelative,
958
+ planSha256,
959
+ attemptPath: attemptRelative,
960
+ replacementReceiptId,
961
+ reusable: classification.reusable,
962
+ replayed,
963
+ sourceFingerprint: selected.sourceFingerprint,
964
+ issueCodes: attempt.issueCodes,
965
+ extra: { nextAllowedAction: "inspect_repair_finalize_failure" },
966
+ });
967
+ }
968
+ attempt.status = "repair_passed";
969
+ attempt.activeReceiptSha256 = finalized.activeReceiptSha256;
970
+ attempt.receiptPath = finalized.receiptPath;
971
+ attempt.issueCodes = [];
972
+ writeJsonAtomically(attemptPath, attempt);
973
+ return resultForAttempt({
974
+ status: "repaired",
975
+ exitCode: 0,
976
+ runId,
977
+ planPath: planRelative,
978
+ planSha256,
979
+ attemptPath: attemptRelative,
980
+ replacementReceiptId,
981
+ reusable: classification.reusable,
982
+ replayed,
983
+ sourceFingerprint: selected.sourceFingerprint,
984
+ extra: {
985
+ receiptPath: finalized.receiptPath,
986
+ activeReceiptSha256: finalized.activeReceiptSha256,
987
+ nextAllowedAction: "independent_review_required",
988
+ },
989
+ });
990
+ }
991
+
992
+ export function runRepairExecution(options) {
993
+ return withControlTransaction(
994
+ options.workspaceRoot,
995
+ () => runRepairWithinControlTransaction(options),
996
+ );
997
+ }