@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.155

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 (226) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +273 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-execution.js +4 -1
  84. package/dist/services/controller-fanout.js +9 -4
  85. package/dist/services/controller-memory-worker.js +6 -4
  86. package/dist/services/controller-result-authority.js +122 -0
  87. package/dist/services/dashboard.js +4 -2
  88. package/dist/services/delivery-access.js +123 -0
  89. package/dist/services/delivery-approval.js +68 -0
  90. package/dist/services/delivery-artifact.js +86 -0
  91. package/dist/services/delivery-checks.js +178 -0
  92. package/dist/services/delivery-execution.js +181 -0
  93. package/dist/services/delivery-intents.js +212 -0
  94. package/dist/services/delivery-operation.js +81 -0
  95. package/dist/services/delivery-policy.js +91 -0
  96. package/dist/services/delivery-procedures.js +43 -0
  97. package/dist/services/delivery-request.js +99 -0
  98. package/dist/services/delivery-result.js +144 -0
  99. package/dist/services/delivery-stage-commands.js +26 -0
  100. package/dist/services/delivery-stage-entry.js +56 -0
  101. package/dist/services/delivery-stage-handlers.js +135 -0
  102. package/dist/services/delivery-stage-results.js +94 -0
  103. package/dist/services/descriptor-coordinator-session.js +3 -1
  104. package/dist/services/descriptor-run-start.js +24 -4
  105. package/dist/services/descriptor-stage-coordinator.js +78 -10
  106. package/dist/services/descriptor-stage-settlement.js +59 -14
  107. package/dist/services/diagnostic-repair-intake.js +123 -0
  108. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  109. package/dist/services/diagnostic-report.js +30 -0
  110. package/dist/services/diagnostic-run-bundle.js +48 -9
  111. package/dist/services/diagnostic-snapshot.js +2 -2
  112. package/dist/services/diagnostic-stage-handlers.js +3 -2
  113. package/dist/services/diagnostic-stage-results.js +26 -0
  114. package/dist/services/diagnostic-start.js +56 -6
  115. package/dist/services/diagnostic-work-collection.js +3 -3
  116. package/dist/services/distill-document-publication.js +280 -0
  117. package/dist/services/distill-promotion.js +197 -0
  118. package/dist/services/distill-publication-custody.js +162 -0
  119. package/dist/services/distill-stage-handlers.js +249 -0
  120. package/dist/services/distill-stage-results.js +105 -0
  121. package/dist/services/distill-user-inputs.js +4 -0
  122. package/dist/services/distill-work-batch.js +42 -0
  123. package/dist/services/distill-work-collection.js +74 -0
  124. package/dist/services/distill-work-registration.js +93 -0
  125. package/dist/services/distill-work-results.js +98 -0
  126. package/dist/services/eval-snapshots.js +17 -7
  127. package/dist/services/external-work-launch.js +8 -2
  128. package/dist/services/flow-catalog.js +24 -0
  129. package/dist/services/frozen-engine.js +19 -2
  130. package/dist/services/generic-execution.js +4 -3
  131. package/dist/services/harness-adapter.js +2 -14
  132. package/dist/services/hook-memory-routing.js +1 -1
  133. package/dist/services/hooks.js +31 -6
  134. package/dist/services/hybrid-contracts.js +6 -1
  135. package/dist/services/installed-flow-source.js +37 -2
  136. package/dist/services/interactive-attached-session.js +215 -0
  137. package/dist/services/interactive-baseline-custody.js +54 -0
  138. package/dist/services/interactive-before-versions.js +148 -0
  139. package/dist/services/interactive-bundle-data.js +85 -0
  140. package/dist/services/interactive-checks.js +179 -0
  141. package/dist/services/interactive-execution.js +33 -0
  142. package/dist/services/interactive-finish-integration.js +236 -0
  143. package/dist/services/interactive-git-baseline.js +230 -0
  144. package/dist/services/interactive-git-dispatch.js +278 -0
  145. package/dist/services/interactive-input-delivery.js +82 -0
  146. package/dist/services/interactive-mutation-scope.js +231 -0
  147. package/dist/services/interactive-request.js +287 -0
  148. package/dist/services/interactive-run-bundle.js +109 -0
  149. package/dist/services/interactive-stage-entry.js +41 -0
  150. package/dist/services/interactive-stage-handlers.js +289 -0
  151. package/dist/services/interactive-stage-observation.js +136 -0
  152. package/dist/services/interactive-stage-results.js +111 -0
  153. package/dist/services/interactive-user-inputs.js +142 -0
  154. package/dist/services/interactive-work-batch.js +43 -0
  155. package/dist/services/interactive-work-collection.js +119 -0
  156. package/dist/services/interactive-work-completion.js +180 -0
  157. package/dist/services/interactive-work-context.js +249 -0
  158. package/dist/services/interactive-work-history.js +112 -0
  159. package/dist/services/interactive-work-registration.js +124 -0
  160. package/dist/services/interactive-work-results.js +115 -0
  161. package/dist/services/interactive-work-start.js +155 -0
  162. package/dist/services/lifecycle-invocations.js +214 -24
  163. package/dist/services/managed-daemon-binding.js +52 -1
  164. package/dist/services/memory-compose-handler.js +2 -22
  165. package/dist/services/memory-finish-integration.js +58 -17
  166. package/dist/services/memory-fix-finish-integration.js +129 -0
  167. package/dist/services/memory-fix-git-dispatch.js +139 -0
  168. package/dist/services/memory-fix-intake-custody.js +11 -0
  169. package/dist/services/memory-fix-intake-settlement.js +127 -0
  170. package/dist/services/memory-fix-publication.js +196 -0
  171. package/dist/services/memory-fix-repair-admission.js +116 -0
  172. package/dist/services/memory-fix-request.js +141 -0
  173. package/dist/services/memory-fix-run-bundle.js +83 -0
  174. package/dist/services/memory-fix-scope.js +78 -0
  175. package/dist/services/memory-fix-sources.js +85 -0
  176. package/dist/services/memory-fix-stage-entry.js +24 -0
  177. package/dist/services/memory-fix-stage-handlers.js +282 -0
  178. package/dist/services/memory-fix-stage-results.js +106 -0
  179. package/dist/services/memory-fix-workspace.js +127 -0
  180. package/dist/services/memory-flow-user-inputs.js +88 -0
  181. package/dist/services/memory-init-contexts.js +55 -5
  182. package/dist/services/memory-init-controller-start.js +20 -4
  183. package/dist/services/memory-layer.js +7 -3
  184. package/dist/services/memory-native-review-copy.js +27 -3
  185. package/dist/services/memory-repair-document-delta.js +1 -0
  186. package/dist/services/project-flow-source.js +195 -0
  187. package/dist/services/project-git-integration.js +92 -0
  188. package/dist/services/repair-intents.js +2 -7
  189. package/dist/services/retained-native-hook.js +245 -0
  190. package/dist/services/review-fix-handoff.js +82 -0
  191. package/dist/services/review-fix-intake-custody.js +17 -0
  192. package/dist/services/review-fix-stage-entry.js +43 -0
  193. package/dist/services/review-fix-stage-handlers.js +176 -0
  194. package/dist/services/review-fix-stage-results.js +86 -0
  195. package/dist/services/run-control-worker.js +42 -1
  196. package/dist/services/run-control.js +40 -5
  197. package/dist/services/run-controller-adapter.js +37 -3
  198. package/dist/services/run-controller-process.js +12 -4
  199. package/dist/services/run-controller-recovery.js +46 -6
  200. package/dist/services/run-controller-state.js +3 -2
  201. package/dist/services/run-controller.js +209 -43
  202. package/dist/services/run-recovery.js +31 -2
  203. package/dist/services/run-timeline.js +59 -0
  204. package/dist/services/runs.js +18 -31
  205. package/dist/services/sessions.js +2 -1
  206. package/dist/services/stage-handlers.js +55 -3
  207. package/dist/services/stage-lifecycle.js +4 -3
  208. package/dist/services/stage-pause.js +26 -9
  209. package/dist/services/stage-report-renderer.js +2 -6
  210. package/dist/services/technical-notes.js +31 -0
  211. package/dist/services/verification-matrix.js +1 -1
  212. package/dist/services/version-status.js +1 -1
  213. package/dist/services/vnext-code-review.js +74 -5
  214. package/dist/services/vnext-code.js +8 -32
  215. package/dist/services/vnext-fanout.js +53 -8
  216. package/dist/services/vnext-merge.js +10 -5
  217. package/dist/services/vnext-plan-review.js +1 -1
  218. package/dist/services/vnext-plan.js +1 -1
  219. package/dist/services/vnext-protocolize.js +1 -1
  220. package/dist/services/vnext-specify.js +10 -5
  221. package/dist/services/work-registry.js +252 -66
  222. package/dist/services/worktrees.js +7 -4
  223. package/dist/storage/database.js +2 -1
  224. package/dist/verification-components.json +82 -0
  225. package/package.json +3 -2
  226. package/tools/verification-jobs.mjs +35 -10
@@ -0,0 +1,196 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { createHash } from "node:crypto";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { hybridDigest } from "./hybrid-contracts.js";
6
+ import { appendAudit } from "./audit.js";
7
+ import { projectCheckoutRoot } from "../storage/paths.js";
8
+ import { prepareMemoryFixRepairAdmission } from "./memory-fix-repair-admission.js";
9
+ import { RetainedMemoryRepairPublicationPlan } from "./memory-repair-publication-plan.js";
10
+ import { prepareMemoryFixDocumentPlan, applyMemoryFixDocumentStep, captureDistillBeforeDocuments, validateDistillBeforeDocuments, publishMemoryFixBeforeDocuments, memoryFixBeforeDocumentReferences } from "./distill-document-publication.js";
11
+ import { readFlowRunExecutionBundle } from "./runs.js";
12
+ import { prepareStagePacket, acceptStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
13
+ const intent = "memory_fix.publication_intent", complete = "memory_fix.publication_complete";
14
+ const key = (scope) => `memory_fix_publication:${hybridDigest({ project_id: scope.project_id, run_id: scope.run_id })}`;
15
+ function refuse(reason) {
16
+ throw new AppError("memory_fix_publication_invalid", "Repair publication requires its original admitted output and retained plan", 2, { reason, effect: "no_effect" });
17
+ }
18
+ const receipt = (binding, plan, completed = false) => ({
19
+ schema_id: completed ? "dd-flow/mb-fix-publication-complete@1" : "dd-flow/mb-fix-publication-intent@1",
20
+ run_id: binding.run_id, binding_sha256: hybridDigest(binding), plan_sha256: plan.sha256
21
+ });
22
+ /** SQL custody only, never current write admission or proof of independent
23
+ * verification. Consumers must match this binding to their own frozen RUN.
24
+ * Missing either the retained value or independent receipt is not a new intent.
25
+ */
26
+ export function readMemoryFixPublication(context, scope) {
27
+ return context.db.readSnapshot(() => {
28
+ const row = context.db.get("SELECT value_json FROM runtime_config WHERE key=?", [key(scope)]);
29
+ const anchor = context.db.get("SELECT json_extract(index_json,'$.variables.\"memory_fix.publication\"') anchor FROM runs WHERE project_id=? AND id=?", [scope.project_id, scope.run_id])?.anchor;
30
+ const events = (type) => context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? LIMIT 2", [scope.project_id, type, scope.run_id]);
31
+ const starts = events(intent), ends = events(complete);
32
+ if (!row && !starts.length && !ends.length && !anchor)
33
+ return null;
34
+ if (!row || !anchor || starts.length !== 1 || ends.length > 1 || Buffer.byteLength(row.value_json) > 180 * 1024 * 1024
35
+ || [...starts, ...ends].some(event => Buffer.byteLength(event.payload_json) > 32768))
36
+ refuse("publication_custody_missing");
37
+ try {
38
+ const value = JSON.parse(row.value_json), binding = value.binding;
39
+ if (Object.keys(value).sort().join() !== "before_documents,binding,plan_json,plan_sha256" || !binding
40
+ || binding.project_id !== scope.project_id || binding.run_id !== scope.run_id || typeof value.plan_json !== "string")
41
+ refuse("publication_binding_changed");
42
+ const plan = RetainedMemoryRepairPublicationPlan.restore(Buffer.from(value.plan_json), value.plan_sha256);
43
+ const beforeDocuments = validateDistillBeforeDocuments(plan, value.before_documents);
44
+ const paths = JSON.parse(plan.state(0).bytes().toString("utf8")).write_paths;
45
+ if (hybridDigest([...paths].sort()) !== hybridDigest([...binding.write_scope.paths].sort())
46
+ || anchor !== hybridDigest(receipt(binding, plan))
47
+ || hybridDigest(JSON.parse(starts[0].payload_json)) !== hybridDigest(receipt(binding, plan))
48
+ || ends.length && hybridDigest(JSON.parse(ends[0].payload_json)) !== hybridDigest(receipt(binding, plan, true)))
49
+ refuse("publication_receipt_changed");
50
+ return { binding: structuredClone(binding), plan, beforeDocuments, completed: ends.length === 1 };
51
+ }
52
+ catch (error) {
53
+ if (error instanceof AppError)
54
+ throw error;
55
+ return refuse("invalid_publication_custody");
56
+ }
57
+ });
58
+ }
59
+ /** Exact handoff bytes include removed paths and original retained text refs.
60
+ * Serialization alone is not publication, Stage acceptance or finding closure. */
61
+ export function memoryFixPublicationArtifact(binding, plan) {
62
+ const entries = new Map(plan.state(plan.actions().length).entries().map(entry => [entry.path, entry]));
63
+ return Buffer.from(JSON.stringify({ schema_id: "dd-flow/mb-fix-publication@1", binding, plan_sha256: plan.sha256,
64
+ documents: binding.write_scope.paths.map(logical => ({ path: logical, after: entries.get(logical) ?? null })),
65
+ before_documents: memoryFixBeforeDocumentReferences(plan), receipt: receipt(binding, plan, true) }));
66
+ }
67
+ export function readMemoryFixPublicationReference(context, retained, logical) {
68
+ if (!retained.completed)
69
+ refuse("publication_not_complete");
70
+ if (!logical || path.isAbsolute(logical) || logical.includes("\\") || logical.split("/").some(part => !part || part === "." || part === ".."))
71
+ refuse("invalid_publication_locator");
72
+ const { binding, plan } = retained;
73
+ const run = context.db.get("SELECT run_root FROM runs WHERE project_id=? AND id=?", [binding.project_id, binding.run_id]);
74
+ if (!run?.run_root)
75
+ refuse("run_home_required");
76
+ const file = path.join(run.run_root, logical), bytes = memoryFixPublicationArtifact(binding, plan);
77
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_packet' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='repair' AND json_extract(payload_json,'$.attempt')=? LIMIT 33", [binding.project_id, binding.run_id, binding.attempt]);
78
+ if (rows.length > 32 || rows.some(row => Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024))
79
+ refuse("unbounded_publication_packets");
80
+ const packets = rows.map(row => JSON.parse(row.payload_json)).filter(packet => typeof packet.file === "string"
81
+ && resolveStagePublicationPath(context, binding.project_id, binding.run_id, packet.file) === file);
82
+ if (!packets.length)
83
+ return null;
84
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
85
+ if (packets.some(packet => packet.sha256 !== sha256 || packet.base64 !== bytes.toString("base64")))
86
+ refuse("publication_packet_changed");
87
+ return { root: "run", path: logical, sha256 };
88
+ }
89
+ /** Internal publisher: actual active author + accepted intake, SQL intent before
90
+ * effects, exact retained prefix and effect-aware retry. No public registration,
91
+ * new Session allocation, independent verification or finding closure here. */
92
+ export function prepareMemoryFixPublication(context, input) {
93
+ const admission = prepareMemoryFixRepairAdmission(context, input), selected = admission.snapshot();
94
+ const proposal = structuredClone(input.proposal), scope = selected.intake.write_scope;
95
+ if (!proposal || Object.keys(proposal).join() !== "outputs" || !Array.isArray(proposal.outputs) || !proposal.outputs.length || proposal.outputs.length > 512
96
+ || proposal.outputs.some(output => !output || Object.keys(output).sort().join() !== "content,finding_ids,path"
97
+ || typeof output.path !== "string" || output.content !== null && typeof output.content !== "string"
98
+ || !Array.isArray(output.finding_ids) || !output.finding_ids.length || new Set(output.finding_ids).size !== output.finding_ids.length
99
+ || output.finding_ids.some(id => !scope.finding_ids.includes(id) || !selected.intake.routing.some(decision => decision.finding_id === id && decision.route === "memory_repair" && decision.write_paths.includes(output.path))))
100
+ || hybridDigest(proposal.outputs.map(output => output.path).sort()) !== hybridDigest([...scope.paths].sort())
101
+ || scope.finding_ids.some(id => !proposal.outputs.some(output => output.finding_ids.includes(id))))
102
+ refuse("output_scope_changed");
103
+ if (proposal.outputs.some(output => output.content !== null && Buffer.byteLength(output.content) > 16 * 1024 * 1024)
104
+ || proposal.outputs.reduce((total, output) => total + (output.content === null ? 0 : Buffer.byteLength(output.content)), 0) > 64 * 1024 * 1024)
105
+ refuse("unbounded_output");
106
+ const { intake, run_root: runRoot, ...identity } = selected;
107
+ const checkoutRelative = path.relative(projectCheckoutRoot(context.ddFlowHome, selected.project_id), selected.workspace_root);
108
+ const managedCheckout = !!checkoutRelative && !path.isAbsolute(checkoutRelative)
109
+ && checkoutRelative !== ".." && !checkoutRelative.startsWith(`..${path.sep}`);
110
+ // The bound project checkout is productive workspace, even when its owner
111
+ // stores it in Home. Other Home areas and every RUN root remain protected.
112
+ for (const protectedRoot of [...(managedCheckout ? [] : [context.ddFlowHome]), runRoot]) {
113
+ const root = fs.realpathSync(protectedRoot);
114
+ for (const output of proposal.outputs) {
115
+ const destination = path.join(selected.workspace_root, output.path), relative = path.relative(root, destination);
116
+ if (!relative || !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`))
117
+ refuse("output_overlaps_runtime");
118
+ }
119
+ }
120
+ const binding = { ...identity, intake_sha256: intake.intake_sha256, source_observation_sha256: intake.source_observation_sha256,
121
+ user_inputs_sha256: intake.user_inputs_sha256, write_scope: scope, proposal_sha256: hybridDigest(proposal),
122
+ assignments: proposal.outputs.map(output => ({ path: output.path, finding_ids: [...output.finding_ids] })) };
123
+ const selection = { project_id: binding.project_id, run_id: binding.run_id };
124
+ const prior = readMemoryFixPublication(context, selection);
125
+ if (prior && hybridDigest(prior.binding) !== hybridDigest(binding))
126
+ refuse("original_publication_required");
127
+ if (!prior)
128
+ admission.assertOriginalSources();
129
+ const plan = prior?.plan ?? prepareMemoryFixDocumentPlan(selected.workspace_root, proposal.outputs.map(output => ({ path: output.path, bytes: output.content === null ? null : Buffer.from(output.content) })));
130
+ const beforeDocuments = prior?.beforeDocuments ?? captureDistillBeforeDocuments(selected.workspace_root, plan);
131
+ const bundle = readFlowRunExecutionBundle(context, input);
132
+ if (!bundle || bundle.sha256 !== selected.bundle_sha256)
133
+ refuse("bundle_changed");
134
+ const logical = `${bundle.descriptor.stage("repair").directory}/publication.json`, file = path.join(runRoot, logical);
135
+ const assertIntent = () => {
136
+ const current = readMemoryFixPublication(context, selection);
137
+ if (!current || hybridDigest(current.binding) !== hybridDigest(binding) || current.plan.sha256 !== plan.sha256)
138
+ refuse("publication_intent_changed");
139
+ return current;
140
+ };
141
+ const fence = () => { admission.assertCustody(context); admission.assertPlanSources(plan); assertIntent(); };
142
+ const applyStep = () => {
143
+ admission.assertPhysical();
144
+ admission.assertPlanSources(plan);
145
+ context.db.writeTransaction(() => {
146
+ admission.assertCustody(context);
147
+ const current = readMemoryFixPublication(context, selection);
148
+ if (current) {
149
+ assertIntent();
150
+ return;
151
+ }
152
+ if (prior)
153
+ refuse("publication_custody_missing");
154
+ const value = JSON.stringify({ binding, plan_json: plan.bytes().toString("utf8"), plan_sha256: plan.sha256, before_documents: beforeDocuments });
155
+ if (Buffer.byteLength(value) > 180 * 1024 * 1024)
156
+ refuse("unbounded_publication_custody");
157
+ context.db.run("INSERT INTO runtime_config(key,value_json,created_at,updated_at) VALUES (?,?,?,?)", [key(selection), value, context.now(), context.now()]);
158
+ appendAudit(context, { projectId: binding.project_id, eventType: intent, payload: receipt(binding, plan) });
159
+ context.db.run("UPDATE runs SET index_json=json_set(index_json,'$.variables.\"memory_fix.publication\"',?) WHERE project_id=? AND id=?", [hybridDigest(receipt(binding, plan)), binding.project_id, binding.run_id]);
160
+ assertIntent();
161
+ });
162
+ publishMemoryFixBeforeDocuments(runRoot, plan, beforeDocuments);
163
+ const result = applyMemoryFixDocumentStep(selected.workspace_root, plan, fence);
164
+ if (result.status !== "applied")
165
+ return result;
166
+ try {
167
+ fence();
168
+ const packet = prepareStagePacket(file, memoryFixPublicationArtifact(binding, plan));
169
+ context.db.writeTransaction(() => {
170
+ admission.assertCustody(context);
171
+ const current = assertIntent();
172
+ if (!current.completed)
173
+ appendAudit(context, { projectId: binding.project_id, eventType: complete, payload: receipt(binding, plan, true) });
174
+ const completed = assertIntent();
175
+ if (!readMemoryFixPublicationReference(context, completed, logical))
176
+ acceptStagePacket(context, { projectId: binding.project_id, runId: binding.run_id, stage: "repair", prepared: packet });
177
+ });
178
+ fence();
179
+ const reference = readMemoryFixPublicationReference(context, assertIntent(), logical);
180
+ if (!reference)
181
+ refuse("publication_reference_missing");
182
+ return { ...result, publication: reference };
183
+ }
184
+ catch (error) {
185
+ throw new AppError("memory_fix_publication_pending", "Reconcile this retained publication before claiming repair completion", 1, { effect: "publication_pending", plan_sha256: plan.sha256, cause: error instanceof Error ? error.message : String(error) });
186
+ }
187
+ };
188
+ return { binding: () => structuredClone(binding), applyStep, apply: () => {
189
+ for (let step = 0; step <= plan.actions().length; step++) {
190
+ const result = applyStep();
191
+ if (result.status === "applied")
192
+ return result;
193
+ }
194
+ return refuse("publication_did_not_progress");
195
+ } };
196
+ }
@@ -0,0 +1,116 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { entityReferenceVariants } from "../shared/entity-references.js";
5
+ import { resolveProjectRoot } from "../storage/paths.js";
6
+ import { requireProjectByRoot } from "./projects.js";
7
+ import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
8
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
9
+ import { readAcceptedDescriptorStageSettlement, readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
10
+ import { readAcceptedStageData } from "./stage-report-renderer.js";
11
+ import { stageLifecycleMatchKey } from "./hooks.js";
12
+ import { readMemoryFixSourceIntake, assertMemoryFixSourceIntakeCustody } from "./memory-fix-intake-custody.js";
13
+ import { readAcceptedMemoryFixIntakeEvidence } from "./memory-fix-intake-settlement.js";
14
+ import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
15
+ import { observeRetainedMemoryFixSources, observeMemoryFixWriteTargets, memoryFixWriteTargetsFromBaseline } from "./memory-fix-sources.js";
16
+ import { RetainedMemoryRepairPublicationPlan } from "./memory-repair-publication-plan.js";
17
+ import { hybridDigest } from "./hybrid-contracts.js";
18
+ function refuse(reason) {
19
+ throw new AppError("memory_fix_repair_admission_invalid", "Repair requires its accepted intake and current delivered scope", 2, { reason, effect: "no_effect" });
20
+ }
21
+ /** Intake/Stage/user and active author Work custody, not publication permission.
22
+ * The publisher must also admit the author's exact output and retain its original
23
+ * plan before effects. Fresh plans require assertOriginalSources; resumed plans
24
+ * require assertPlanSources on that same SQL-retained plan, never a new baseline.
25
+ */
26
+ export function prepareMemoryFixRepairAdmission(context, input) {
27
+ if (context.db.isOwnedWriteTransaction?.())
28
+ refuse("preparation_outside_writer_required");
29
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
30
+ const bundle = readFlowRunExecutionBundle(context, input);
31
+ if (!bundle || bundle.manifest.flow_id !== "mb-fix")
32
+ refuse("memory_fix_bundle_required");
33
+ const scope = { projectId: project.id, runId: input.runId, stage: "repair" };
34
+ const source = readMemoryFixSourceIntake(context, input, bundle);
35
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
36
+ const stage = readDescriptorStageInput(context, scope, bundle);
37
+ const accepted = readAcceptedStageData(context, { ...scope, stage: "intake" });
38
+ if (stage.input.predecessor?.stage !== "intake" || hybridDigest(stage.input.predecessor.result) !== hybridDigest(accepted.result))
39
+ refuse("accepted_intake_predecessor_required");
40
+ const keys = entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, "intake", input.projectRoot));
41
+ const settlement = readAcceptedDescriptorStageSettlement(context, { ...scope, stage: "intake" }, bundle, keys);
42
+ const author = readActiveDescriptorStageCoordinator(context, scope, bundle);
43
+ const file = path.join(stage.runRoot, bundle.descriptor.stage("intake").directory, "intake-evidence.json");
44
+ const evidence = readAcceptedMemoryFixIntakeEvidence(context, scope, file);
45
+ if (evidence.intake_sha256 !== source.sha256 || !evidence.write_scope)
46
+ refuse("accepted_repair_scope_required");
47
+ const workspace = resolveProjectRoot(run.workspace_root);
48
+ const physical = () => {
49
+ if (fs.realpathSync(workspace) !== workspace)
50
+ refuse("workspace_identity_changed");
51
+ const stat = fs.lstatSync(workspace);
52
+ if (!stat.isDirectory())
53
+ refuse("workspace_identity_changed");
54
+ return { dev: stat.dev, ino: stat.ino };
55
+ };
56
+ const identity = physical();
57
+ const assertPhysical = () => {
58
+ if (context.db.isOwnedWriteTransaction?.())
59
+ refuse("physical_check_outside_writer_required");
60
+ if (hybridDigest(physical()) !== hybridDigest(identity))
61
+ refuse("workspace_identity_changed");
62
+ };
63
+ const assertCustody = (writer) => {
64
+ assertMemoryFixSourceIntakeCustody(writer, scope, bundle, source.sha256);
65
+ const current = assertPreparedRunBundleSqlAuthority(writer, scope, bundle);
66
+ if (current.workspace_root !== workspace || current.run_root !== stage.runRoot
67
+ || hybridDigest(readDescriptorStageInput(writer, scope, bundle)) !== hybridDigest(stage)
68
+ || hybridDigest(readAcceptedStageData(writer, { ...scope, stage: "intake" })) !== hybridDigest(accepted)
69
+ || hybridDigest(readAcceptedDescriptorStageSettlement(writer, { ...scope, stage: "intake" }, bundle, keys)) !== hybridDigest(settlement)
70
+ || hybridDigest(readActiveDescriptorStageCoordinator(writer, scope, bundle)) !== hybridDigest(author)
71
+ || hybridDigest(readAcceptedMemoryFixIntakeEvidence(writer, scope, file)) !== hybridDigest(evidence)
72
+ || hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, "mb-fix", bundle)) !== evidence.user_inputs_sha256)
73
+ refuse("accepted_scope_changed");
74
+ };
75
+ assertCustody(context);
76
+ const observe = () => { assertPhysical(); return observeRetainedMemoryFixSources(workspace, source.intake).snapshot(); };
77
+ return {
78
+ snapshot: () => structuredClone({ project_id: project.id, run_id: input.runId, workspace_root: workspace,
79
+ workspace_identity: identity, run_root: stage.runRoot, intake: evidence,
80
+ intake_result: accepted.result, bundle_sha256: bundle.sha256, author, attempt: stage.attempt, input_sha256: stage.inputSha256 }),
81
+ assertCustody, assertPhysical,
82
+ assertOriginalSources: () => {
83
+ const current = { ...observe(), write_targets: observeMemoryFixWriteTargets(workspace, evidence.write_scope.paths) };
84
+ if (hybridDigest(current) !== hybridDigest(evidence.source_observation))
85
+ refuse("sources_changed_since_intake");
86
+ },
87
+ assertPlanSources: (plan) => {
88
+ assertPhysical();
89
+ if (!(plan instanceof RetainedMemoryRepairPublicationPlan))
90
+ refuse("retained_plan_required");
91
+ const before = JSON.parse(plan.state(0).bytes().toString("utf8"));
92
+ if (hybridDigest([...before.write_paths].sort()) !== hybridDigest([...evidence.write_scope.paths].sort())
93
+ || plan.state(0).canonicalWritePaths().size)
94
+ refuse("plan_scope_changed");
95
+ if (hybridDigest(memoryFixWriteTargetsFromBaseline(plan.state(0))) !== hybridDigest(evidence.source_observation.write_targets))
96
+ refuse("write_targets_changed_since_intake");
97
+ const current = plan.classify(workspace), entries = plan.state(current.completed).entries();
98
+ const actual = observe();
99
+ for (const original of evidence.source_observation.observations) {
100
+ const observed = actual.observations.find(item => item.path === original.path);
101
+ if (!observed)
102
+ refuse("source_inventory_changed");
103
+ if (before.write_paths.includes(original.path)) {
104
+ const baseline = before.entries.find((entry) => entry.path === original.path);
105
+ if ((baseline?.sha256 ?? null) !== original.current_sha256)
106
+ refuse("plan_rebased_after_intake");
107
+ const expected = entries.find(entry => entry.path === original.path);
108
+ if ((expected?.sha256 ?? null) !== observed.current_sha256)
109
+ refuse("source_differs_from_retained_prefix");
110
+ }
111
+ else if (hybridDigest(observed) !== hybridDigest(original))
112
+ refuse("sources_changed_since_intake");
113
+ }
114
+ }
115
+ };
116
+ }
@@ -0,0 +1,141 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { requireProjectByRoot } from "./projects.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { appendAudit } from "./audit.js";
5
+ import { prepareMemoryFixRunBundle } from "./memory-fix-run-bundle.js";
6
+ import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
7
+ import { FrozenGenericExecution } from "./generic-execution.js";
8
+ import { AdmittedFrozenEngine } from "./frozen-engine.js";
9
+ import { PreparedDescriptorRunStart } from "./descriptor-run-start.js";
10
+ import { readRunStartRequest } from "./run-start-custody.js";
11
+ import { prepareMemoryFixGitPolicy } from "./memory-fix-finish-integration.js";
12
+ const event = "memory_fix.request";
13
+ function refuse(reason) {
14
+ throw new AppError("memory_fix_request_invalid", "Repair launch requires its original retained request", 2, { reason, effect: "no_effect" });
15
+ }
16
+ function selection(input) {
17
+ return hybridDigest({ source_sha256: input.source.sha256, execution_sha256: input.execution.sha256,
18
+ engine_manifest_sha256: input.engine.manifestSha256, request_text: input.requestText,
19
+ source_run_id: input.sourceRunId, finding_ids: input.findingIds, git_executable: input.gitExecutable, git_policy: input.gitPolicy,
20
+ receipts: [...input.receipts ?? []], receipt_bytes: [...input.receiptBytes ?? []].map(([key, bytes]) => [key, Buffer.from(bytes).toString("base64")]) });
21
+ }
22
+ /** SQL-only lookup, also used by the allocation writer. No neighboring audit,
23
+ * current canon/profile settings or filesystem observation is consulted. */
24
+ export function readMemoryFixRequest(context, { projectRoot, requestId }) {
25
+ const scope = { projectRoot, requestId };
26
+ const project = context.db.get("SELECT id FROM projects WHERE root=? AND status='active'", [scope.projectRoot]);
27
+ if (!project)
28
+ refuse("project_not_registered");
29
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.scope.requestId')=? LIMIT 2", [project.id, event, scope.requestId]);
30
+ if (!rows.length)
31
+ return null;
32
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 48 * 1024 * 1024)
33
+ refuse("ambiguous_request");
34
+ let value;
35
+ try {
36
+ value = JSON.parse(rows[0].payload_json);
37
+ }
38
+ catch {
39
+ return refuse("invalid_request");
40
+ }
41
+ if (value.schema_id !== "dd-flow/memory-fix-request@1" || value.project_id !== project.id
42
+ || hybridDigest(value.scope) !== hybridDigest(scope) || !/^[a-f0-9]{64}$/.test(value.selection_sha256)
43
+ || !Number.isFinite(Date.parse(value.bound_at)) || typeof value.request_text !== "string" || !value.request_text.trim()
44
+ || Buffer.byteLength(value.request_text) > 64 * 1024 || !Array.isArray(value.artifacts) || value.artifacts.length !== 2
45
+ || new Set(value.artifacts.map(item => item.path)).size !== 2
46
+ || value.artifacts.some(item => !["runtime/repair-intake.json", "runtime/git-policy.json"].includes(item.path)
47
+ || typeof item.base64 !== "string" || !item.base64 || item.base64.length > 2 * 1024 * 1024
48
+ || Buffer.from(item.base64, "base64").toString("base64") !== item.base64)
49
+ || !Array.isArray(value.receipts) || !Array.isArray(value.receipt_bytes)
50
+ || value.launch_selection !== null && (!value.launch_selection || typeof value.launch_selection !== "object" || Array.isArray(value.launch_selection))
51
+ || value.execution?.scope?.projectRoot !== scope.projectRoot || value.execution.scope.requestId !== scope.requestId
52
+ || value.execution.settings.flow_id !== "mb-fix")
53
+ refuse("request_scope_changed");
54
+ return value;
55
+ }
56
+ export function readMemoryFixRequestArtifacts(context, input) {
57
+ const frozen = input.execution.snapshot(), request = readMemoryFixRequest(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId });
58
+ if (request && request.selection_sha256 !== selection(input))
59
+ refuse("request_changed");
60
+ return request;
61
+ }
62
+ /** Allocation fence: metadata and retained bytes only. Historical RUN replay
63
+ * owns its bundle independently and does not need the neighboring source RUN. */
64
+ export function assertMemoryFixRequestBundle(context, projectId, bundle) {
65
+ const frozen = bundle.execution.snapshot(), request = readMemoryFixRequest(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId });
66
+ if (!request || request.project_id !== projectId || bundle.manifest.flow_id !== "mb-fix" || bundle.manifest.bootstrap_receipts.length
67
+ || bundle.execution.sha256 !== request.execution_sha256 || bundle.manifest.descriptor.sha256 !== request.source.descriptor.sha256
68
+ || bundle.blob("runtime/source-request.md").toString("utf8") !== request.request_text
69
+ || bundle.manifest.engine_manifest.sha256 !== request.engine.manifest_sha256
70
+ || request.artifacts.some(item => bundle.blob(item.path).toString("base64") !== item.base64)
71
+ || request.source.blobs.some(item => bundle.blob(item.path).toString("base64") !== item.base64))
72
+ refuse("request_bundle_changed");
73
+ }
74
+ /** Preserve prepared input before allocating a RUN. IDs are selected only by
75
+ * common RUN-start, so another request may allocate between retain and start. */
76
+ export function retainMemoryFixRequest(context, input, launchSelection) {
77
+ if (context.db.isOwnedWriteTransaction?.())
78
+ refuse("preparation_outside_writer_required");
79
+ const frozen = input.execution.snapshot(), scope = { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId };
80
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(scope.requestId))
81
+ refuse("invalid_request_id");
82
+ const project = requireProjectByRoot(context, scope.projectRoot), digest = selection(input), prior = readMemoryFixRequest(context, scope);
83
+ const raw = structuredClone(launchSelection ?? null);
84
+ const assertSame = (request) => {
85
+ if (request.selection_sha256 !== digest || launchSelection !== undefined && hybridDigest(request.launch_selection) !== hybridDigest(raw))
86
+ refuse("request_changed");
87
+ };
88
+ if (prior) {
89
+ assertSame(prior);
90
+ return prior;
91
+ }
92
+ if (readRunStartRequest(context, project.id, scope.requestId))
93
+ refuse("foreign_run_request");
94
+ const bundle = prepareMemoryFixRunBundle(context, input), manifest = input.engine.manifest();
95
+ const value = { schema_id: "dd-flow/memory-fix-request@1", project_id: project.id, scope, selection_sha256: digest,
96
+ bound_at: input.boundAt, request_text: input.requestText, source_run_id: input.sourceRunId, finding_ids: [...input.findingIds],
97
+ source: input.source.snapshot(), source_sha256: input.source.sha256, execution: frozen, execution_sha256: input.execution.sha256,
98
+ engine: { identity: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
99
+ integrity_checksum: manifest.integrity.checksum, snapshot_root: manifest.snapshot_root }, manifest_sha256: input.engine.manifestSha256 },
100
+ git_executable: input.gitExecutable, git_policy: structuredClone(input.gitPolicy),
101
+ launch_selection: raw,
102
+ artifacts: ["runtime/repair-intake.json", "runtime/git-policy.json"].map(path => ({ path, base64: bundle.blob(path).toString("base64") })),
103
+ receipts: [...input.receipts ?? []], receipt_bytes: [...input.receiptBytes ?? []].map(([key, bytes]) => [key, Buffer.from(bytes).toString("base64")]) };
104
+ if (Buffer.byteLength(JSON.stringify(value)) > 48 * 1024 * 1024)
105
+ refuse("request_exceeds_bound");
106
+ return context.db.writeTransaction(() => {
107
+ const current = readMemoryFixRequest(context, scope);
108
+ if (current) {
109
+ assertSame(current);
110
+ return current;
111
+ }
112
+ if (readRunStartRequest(context, project.id, scope.requestId))
113
+ refuse("foreign_run_request");
114
+ appendAudit(context, { projectId: project.id, eventType: event, payload: value });
115
+ return value;
116
+ });
117
+ }
118
+ /** Existing allocations resume the common publisher. Before allocation only
119
+ * retained source/execution and input artifacts are restored, never refreshed. */
120
+ export function startMemoryFixRun(context, scope) {
121
+ const request = readMemoryFixRequest(context, scope);
122
+ if (!request)
123
+ refuse("original_request_required");
124
+ const allocation = readRunStartRequest(context, request.project_id, scope.requestId);
125
+ if (allocation) {
126
+ if (allocation.payload.initial_index.flow_kind !== "mb-fix")
127
+ refuse("foreign_run_request");
128
+ return PreparedDescriptorRunStart.restore(context, scope).apply(context);
129
+ }
130
+ const source = PreparedInstalledFlowSource.restoreRetained(request.source, request.source_sha256), receipts = new Map(request.receipts);
131
+ const execution = FrozenGenericExecution.restore(request.execution, request.execution_sha256, source.resolveRegisteredFlow(), request.execution.scope, receipts);
132
+ const engine = AdmittedFrozenEngine.admitSelection(request.engine.identity, request.engine.manifest_sha256);
133
+ const actualGit = prepareMemoryFixGitPolicy(context.env, { scope: { ...scope, workspaceRoot: request.execution.scope.workspace },
134
+ gitExecutable: request.git_executable, selection: request.git_policy });
135
+ if (actualGit.bytes.toString("base64") !== request.artifacts.find(item => item.path === actualGit.path)?.base64)
136
+ refuse("git_selection_changed_before_start");
137
+ const bundle = prepareMemoryFixRunBundle(context, { source, execution, engine, boundAt: request.bound_at, requestText: request.request_text,
138
+ sourceRunId: request.source_run_id, findingIds: request.finding_ids, gitExecutable: request.git_executable, gitPolicy: request.git_policy,
139
+ receipts, receiptBytes: new Map(request.receipt_bytes.map(([key, bytes]) => [key, Buffer.from(bytes, "base64")])) });
140
+ return PreparedDescriptorRunStart.prepare(context, { bundle }).apply(context);
141
+ }
@@ -0,0 +1,83 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
4
+ import { FrozenGenericExecution } from "./generic-execution.js";
5
+ import { AdmittedFrozenEngine } from "./frozen-engine.js";
6
+ import { RetainedExecutionBundle } from "./execution-bundles.js";
7
+ import { nextDescriptorRunId } from "./descriptor-run-start.js";
8
+ import { bundledHandlerContext } from "./stage-handlers.js";
9
+ import { hybridDigest } from "./hybrid-contracts.js";
10
+ import { prepareMemoryFixSourceIntake } from "./memory-fix-intake-custody.js";
11
+ import { prepareMemoryFixGitPolicy } from "./memory-fix-finish-integration.js";
12
+ import { readMemoryFixRequestArtifacts } from "./memory-fix-request.js";
13
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
14
+ const encode = (value) => Buffer.from(JSON.stringify(value));
15
+ function refuse(reason) {
16
+ throw new AppError("memory_fix_run_bundle_invalid", "Repair RUN requires its original source selection, request and Git policy", 2, { reason, effect: "no_effect" });
17
+ }
18
+ /** Initial construction outside the writer only; no RUN or workspace is created.
19
+ * The request owner must retain this selection before allocation and use the
20
+ * retained bundle on retry, never recapture a later diagnostic or Git policy.
21
+ */
22
+ export function prepareMemoryFixRunBundle(context, input) {
23
+ if (context.db.isOwnedWriteTransaction?.())
24
+ refuse("preparation_outside_writer_required");
25
+ if (!(input.source instanceof PreparedInstalledFlowSource) || !(input.execution instanceof FrozenGenericExecution)
26
+ || !(input.engine instanceof AdmittedFrozenEngine) || !Number.isFinite(Date.parse(input.boundAt)))
27
+ refuse("unprepared_selection");
28
+ if (typeof input.requestText !== "string" || !input.requestText.trim() || Buffer.byteLength(input.requestText) > 64 * 1024)
29
+ refuse("original_request_required");
30
+ const frozen = input.execution.snapshot(), flow = input.source.resolveRegisteredFlow();
31
+ if (frozen.settings.flow_id !== "mb-fix" || flow.descriptor.id !== "mb-fix"
32
+ || flow.sha256 !== frozen.settings.descriptor_sha256)
33
+ refuse("flow_selection_changed");
34
+ input.source.revalidate();
35
+ input.engine.admitMemoryFixFlow(input.source);
36
+ const retained = readMemoryFixRequestArtifacts(context, input);
37
+ const restored = (path) => ({ path, bytes: Buffer.from(retained.artifacts.find(item => item.path === path).base64, "base64") });
38
+ const intake = retained ? restored("runtime/repair-intake.json") : prepareMemoryFixSourceIntake(context, { projectRoot: frozen.scope.projectRoot,
39
+ sourceRunId: input.sourceRunId, findingIds: input.findingIds });
40
+ const git = retained ? restored("runtime/git-policy.json") : prepareMemoryFixGitPolicy(context.env, { scope: { requestId: frozen.scope.requestId,
41
+ projectRoot: frozen.scope.projectRoot, workspaceRoot: frozen.scope.workspace }, gitExecutable: input.gitExecutable, selection: input.gitPolicy });
42
+ const manifest = input.engine.manifest(), checksum = manifest.integrity.checksum;
43
+ if (typeof checksum !== "string")
44
+ refuse("engine_identity_missing");
45
+ const runId = nextDescriptorRunId(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, slug: "mb-fix" });
46
+ const binding = { schema_id: "dd-flow/run-engine-binding@1", run_id: runId,
47
+ project_root: frozen.scope.projectRoot, bound_at: retained?.bound_at ?? input.boundAt, source: "run_creation", reason: "Request-bound selected Memory Bank repair",
48
+ engine: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
49
+ integrity_checksum: checksum, snapshot_root: manifest.snapshot_root }, probe: { status: "not_required", command: null } };
50
+ const blobs = input.source.blobs();
51
+ const put = (logical, content) => {
52
+ if (blobs.has(logical) || !content.length || content.length > 1024 * 1024)
53
+ refuse("bundle_path_collision_or_size");
54
+ blobs.set(logical, content);
55
+ return { path: logical, sha256: hash(content) };
56
+ };
57
+ put("runtime/source-request.md", Buffer.from(input.requestText));
58
+ put(intake.path, intake.bytes);
59
+ put(git.path, git.bytes);
60
+ for (const reference of [frozen.settings.attached_session_receipt, frozen.settings.delegation.native_capacity_receipt]) {
61
+ if (!reference)
62
+ continue;
63
+ const content = input.receiptBytes?.get(reference.sha256);
64
+ if (!content || hash(content) !== reference.sha256 || !input.receipts?.has(reference.sha256))
65
+ refuse("execution_receipt_missing");
66
+ put(reference.path, Buffer.from(content));
67
+ }
68
+ const profiles = { schema_id: "dd-flow/profile-settings@1", profiles: frozen.profiles, aliases: {}, default_profile: null };
69
+ const profile = put("runtime/profiles.json", encode({ ...profiles, revision: hybridDigest(profiles) }));
70
+ const execution = put("runtime/execution.json", encode(frozen.settings));
71
+ const engineBinding = put("runtime/engine-binding.json", encode(binding));
72
+ const engineManifest = put("runtime/engine-manifest.json", input.engine.manifestBytes());
73
+ const provenance = { canon: input.source.canonProvenance(), project_pack: null };
74
+ const bundle = { schema_id: "dd-flow/execution-bundle@1", run_id: runId, flow_id: "mb-fix",
75
+ descriptor: input.source.descriptorReference(), execution_settings: execution, profile_settings: profile,
76
+ engine_binding: engineBinding, engine_manifest: engineManifest, bootstrap_receipts: [], ...provenance,
77
+ inventory: [...blobs].map(([logical, content]) => ({ path: logical, sha256: hash(content) })) };
78
+ const bytes = encode(bundle);
79
+ return RetainedExecutionBundle.admit(bytes, hash(bytes), blobs, { runId, executionScope: frozen.scope,
80
+ bundleProjectRoot: frozen.scope.projectRoot, descriptorSha256: flow.sha256, engineChecksum: checksum,
81
+ engineBindingSha256: engineBinding.sha256, engineManifestSha256: engineManifest.sha256,
82
+ descriptorContext: bundledHandlerContext(new Map()), bundleProvenance: provenance, receipts: input.receipts ?? new Map() });
83
+ }
@@ -0,0 +1,78 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { resolveProjectRoot } from "../storage/paths.js";
3
+ import { requireProjectByRoot } from "./projects.js";
4
+ import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
5
+ import { hybridDigest } from "./hybrid-contracts.js";
6
+ import { validateMemoryFixRouting } from "./diagnostic-repair-intake.js";
7
+ import { readMemoryFixSourceIntake, assertMemoryFixSourceIntakeCustody } from "./memory-fix-intake-custody.js";
8
+ import { observeRetainedMemoryFixSources, observeMemoryFixWriteTargets } from "./memory-fix-sources.js";
9
+ import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
10
+ import { assertMemoryRepairWritePaths } from "./memory-review-correction-plan.js";
11
+ function refuse(reason) {
12
+ throw new AppError("memory_fix_scope_invalid", "Memory repair scope requires the selected findings and actual delivered user agreement", 2, { reason, effect: "no_effect" });
13
+ }
14
+ /** Prepares scope evidence, not semantic consent, Stage settlement or permission
15
+ * to apply files. The active intake coordinator must read the full delivered
16
+ * conversation, assess drift and bind its decision through the ordinary hook.
17
+ * Later publication must fence this same input set and original baseline again.
18
+ */
19
+ export function prepareMemoryFixScopeEvidence(context, input) {
20
+ if (context.db.isOwnedWriteTransaction?.())
21
+ refuse("preparation_outside_writer_required");
22
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
23
+ const bundle = readFlowRunExecutionBundle(context, input);
24
+ if (!bundle || bundle.manifest.flow_id !== "mb-fix")
25
+ refuse("memory_fix_bundle_required");
26
+ const scope = { projectId: project.id, runId: input.runId };
27
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
28
+ const source = readMemoryFixSourceIntake(context, input, bundle);
29
+ const routing = validateMemoryFixRouting(source.intake.selected_findings.map(finding => finding.id), input.routing);
30
+ const users = readPreparedMemoryFlowUserInputs(context, scope, "mb-fix", bundle), userInputsSha256 = hybridDigest(users);
31
+ const writeScope = structuredClone(input.writeScope);
32
+ const repairs = routing.filter(decision => decision.route === "memory_repair").map(decision => decision.finding_id);
33
+ if (!repairs.length) {
34
+ if (writeScope !== null)
35
+ refuse("scope_without_repair_findings");
36
+ }
37
+ else {
38
+ if (!writeScope || typeof writeScope !== "object" || Array.isArray(writeScope)
39
+ || Object.keys(writeScope).sort().join() !== "approval_source,finding_ids,paths,user_inputs_sha256"
40
+ || !Array.isArray(writeScope.finding_ids) || !Array.isArray(writeScope.paths)
41
+ || hybridDigest(writeScope.finding_ids) !== hybridDigest(repairs))
42
+ refuse("exact_selected_scope_required");
43
+ assertMemoryRepairWritePaths(writeScope.paths);
44
+ const paths = [...new Set(routing.flatMap(decision => decision.write_paths))].sort();
45
+ if (hybridDigest([...writeScope.paths].sort()) !== hybridDigest(paths))
46
+ refuse("routing_write_scope_changed");
47
+ const agreement = writeScope.approval_source;
48
+ if (!agreement || Object.keys(agreement).sort().join() !== "path,root,sha256" || agreement.root !== "run"
49
+ || !users.sources.some(packet => ["request", "answer"].includes(packet.role)
50
+ && packet.path === agreement.path && packet.sha256 === agreement.sha256))
51
+ refuse("delivered_agreement_required");
52
+ if (writeScope.user_inputs_sha256 !== userInputsSha256)
53
+ refuse("user_inputs_changed");
54
+ }
55
+ const workspaceRoot = resolveProjectRoot(run.workspace_root), runRoot = run.run_root;
56
+ if (workspaceRoot !== run.workspace_root || !runRoot)
57
+ refuse("workspace_identity_required");
58
+ const observation = observeRetainedMemoryFixSources(workspaceRoot, source.intake);
59
+ const writeTargets = observeMemoryFixWriteTargets(workspaceRoot, writeScope?.paths ?? []);
60
+ const sourceObservation = { ...observation.snapshot(), write_targets: writeTargets };
61
+ const evidence = { intake_sha256: source.sha256, user_inputs_sha256: userInputsSha256,
62
+ workspace_root: workspaceRoot, routing, write_scope: writeScope, source_observation: sourceObservation,
63
+ source_observation_sha256: hybridDigest(sourceObservation) };
64
+ return { snapshot: () => structuredClone(evidence), assertSourcesCurrent: () => {
65
+ if (context.db.isOwnedWriteTransaction?.())
66
+ refuse("physical_observation_outside_writer_required");
67
+ observation.revalidate();
68
+ if (hybridDigest(observeMemoryFixWriteTargets(workspaceRoot, writeScope?.paths ?? [])) !== hybridDigest(writeTargets))
69
+ refuse("write_targets_changed");
70
+ }, assertCustody: (writer) => {
71
+ assertMemoryFixSourceIntakeCustody(writer, scope, bundle, source.sha256);
72
+ const current = assertPreparedRunBundleSqlAuthority(writer, scope, bundle);
73
+ if (current.workspace_root !== workspaceRoot || current.run_root !== runRoot)
74
+ refuse("workspace_identity_changed");
75
+ if (hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, "mb-fix", bundle)) !== userInputsSha256)
76
+ refuse("user_inputs_changed");
77
+ } };
78
+ }