@deksden-com/dd-flow-cli 0.9.0-beta.151 → 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 (227) 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 +170 -25
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
  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 +77 -10
  80. package/dist/services/check-runner-proof.js +69 -1
  81. package/dist/services/cli-operation-classifier.js +6 -1
  82. package/dist/services/code-checks.js +56 -23
  83. package/dist/services/codex-hook-delivery.js +11 -2
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +9 -4
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +214 -24
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -4
  190. package/dist/services/retained-native-hook.js +245 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +12 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +209 -43
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +9 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +252 -66
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +4 -3
  227. package/tools/verification-jobs.mjs +342 -0
@@ -0,0 +1,85 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { resolveProjectRoot } from "../storage/paths.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { readDiagnosticRepairIntake } from "./diagnostic-repair-intake.js";
5
+ import { ObservedMemorySources } from "./memory-source-observations.js";
6
+ import { memorySourceKey } from "./memory-stage-results.js";
7
+ import { MemoryRepairDocumentBaseline } from "./memory-repair-document-delta.js";
8
+ function refuse(reason) {
9
+ throw new AppError("memory_fix_source_invalid", "Memory repair source observations changed or exceed their bounds", 2, { reason, effect: "no_effect" });
10
+ }
11
+ /** Only selected target metadata, including absence, becomes intake authority.
12
+ * Neighboring current documents remain protected by the later publication plan
13
+ * but unrelated edits do not invalidate target approval by themselves. */
14
+ export function observeMemoryFixWriteTargets(projectRoot, paths) {
15
+ if (!paths.length)
16
+ return [];
17
+ const baseline = MemoryRepairDocumentBaseline.capture(projectRoot, paths);
18
+ return memoryFixWriteTargetsFromBaseline(baseline);
19
+ }
20
+ export function memoryFixWriteTargetsFromBaseline(baseline) {
21
+ const value = JSON.parse(baseline.bytes().toString("utf8"));
22
+ const entries = new Map(baseline.entries().map(entry => [entry.path, entry]));
23
+ return value.write_paths.slice().sort().map(logical => ({ path: logical, before: entries.get(logical) ?? null }));
24
+ }
25
+ /** Read-only drift evidence, never a replacement for the original finding or
26
+ * permission to repair/close it. The Stage owner must assess drift and revalidate
27
+ * this same observation before admitting any writer. Unavailable is not unchanged.
28
+ */
29
+ export function observeMemoryFixSources(context, input) {
30
+ return observeSources(resolveProjectRoot(input.projectRoot), () => readDiagnosticRepairIntake(context, input));
31
+ }
32
+ /** The handler must first admit this intake from its own frozen RUN bundle.
33
+ * A scoped restore need not contain the neighboring source audit's SQL rows.
34
+ * This helper grants no intake/approval authority and never searches another Home.
35
+ */
36
+ export function observeRetainedMemoryFixSources(projectRoot, intake) {
37
+ const retained = structuredClone(intake);
38
+ return observeSources(resolveProjectRoot(projectRoot), () => retained);
39
+ }
40
+ function observeSources(projectRoot, readIntake) {
41
+ const capture = () => {
42
+ const intake = readIntake(), references = new Map();
43
+ for (const finding of intake.selected_findings) {
44
+ for (const reference of [...finding.evidence, ...finding.resolution.evidence,
45
+ ...finding.critic_decisions.flatMap(critic => critic.decision.evidence)]) {
46
+ if (typeof reference.path !== "string" || reference.path.length > 4096 || reference.path.split("/").includes(".git")
47
+ || !/^[a-f0-9]{64}$/.test(reference.sha256))
48
+ refuse("invalid_original_source");
49
+ if (references.has(reference.path) && references.get(reference.path) !== reference.sha256)
50
+ refuse("conflicting_original_source");
51
+ references.set(reference.path, reference.sha256);
52
+ }
53
+ }
54
+ if (references.size > 20_000)
55
+ refuse("source_inventory_exceeds_bound");
56
+ let total = 0;
57
+ const observations = [...references].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([path, original_sha256]) => {
58
+ try {
59
+ const location = { root: "project", path };
60
+ const observed = ObservedMemorySources.capture({ project: projectRoot }, [location]);
61
+ total += observed.bytes(location).length;
62
+ if (total > 64 * 1024 * 1024)
63
+ refuse("source_bytes_exceed_bound");
64
+ const current_sha256 = observed.hashes().get(memorySourceKey("project", path));
65
+ return { path, original_sha256, current_sha256,
66
+ state: current_sha256 === original_sha256 ? "unchanged" : "changed", reason: null };
67
+ }
68
+ catch (error) {
69
+ if (!(error instanceof AppError) || error.code !== "memory_source_observation_failed")
70
+ throw error;
71
+ return { path, original_sha256, current_sha256: null, state: "unavailable",
72
+ reason: String(error.details?.reason ?? "source_unavailable") };
73
+ }
74
+ });
75
+ return { schema_id: "dd-flow/mb-fix-source-observation@1", source: intake.source,
76
+ finding_ids: intake.selected_findings.map(finding => finding.id), observations,
77
+ requires_reassessment: !observations.length || observations.some(source => source.state !== "unchanged"),
78
+ authorization: "not_granted" };
79
+ };
80
+ const original = capture();
81
+ return { snapshot: () => structuredClone(original), revalidate: () => {
82
+ if (hybridDigest(capture()) !== hybridDigest(original))
83
+ refuse("sources_changed_after_observation");
84
+ } };
85
+ }
@@ -0,0 +1,24 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { memoryFixStages } from "../domain/memory-fix-flow.js";
3
+ import { readFlowRunExecutionBundle } from "./runs.js";
4
+ import { requireProjectByRoot } from "./projects.js";
5
+ import { resolveProjectRoot } from "../storage/paths.js";
6
+ import { readMemoryFixSourceIntake, assertMemoryFixSourceIntakeCustody } from "./memory-fix-intake-custody.js";
7
+ import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
8
+ import { PreparedDescriptorStageCoordinator } from "./descriptor-stage-coordinator.js";
9
+ /** Registered entry implementation, not standalone RUN-start permission.
10
+ * Generic runtime alone owns attempts, root Work, coordinator and replay.
11
+ */
12
+ export function startMemoryFixStage(context, input) {
13
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
14
+ const bundle = readFlowRunExecutionBundle(context, input);
15
+ if (!bundle || bundle.manifest.flow_id !== "mb-fix" || !memoryFixStages.includes(input.stage))
16
+ throw new AppError("memory_fix_stage_invalid", "Memory repair requires its registered five-stage flow and retained intake", 2, { reason: "memory_fix_stage_required", effect: "no_effect" });
17
+ bundle.descriptor.stage(input.stage);
18
+ const source = readMemoryFixSourceIntake(context, input, bundle);
19
+ const scope = { projectId: project.id, runId: input.runId };
20
+ const fence = (writer) => assertMemoryFixSourceIntakeCustody(writer, scope, bundle, source.sha256);
21
+ const start = PreparedDescriptorStageStart.prepare(context, input).apply(context, fence);
22
+ const coordinator = PreparedDescriptorStageCoordinator.prepare(context, input).apply(context, fence);
23
+ return { ...start, ...coordinator, intake_sha256: source.sha256, source_intake: source.reference };
24
+ }
@@ -0,0 +1,282 @@
1
+ import path from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { resolveProjectRoot } from "../storage/paths.js";
5
+ import { entityReferenceVariants } from "../shared/entity-references.js";
6
+ import { requireProjectByRoot } from "./projects.js";
7
+ import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
8
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
9
+ import { PreparedDescriptorStageSettlement, readAcceptedDescriptorStageSettlement, readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
10
+ import { readAcceptedStageData, prepareStagePacket, acceptStagePacket, resolveStagePublicationPath, prepareStageReport, acceptStageReport, publishStageReport } from "./stage-report-renderer.js";
11
+ import { readMemoryFixSourceIntake, assertMemoryFixSourceIntakeCustody } from "./memory-fix-intake-custody.js";
12
+ import { finishMemoryFixIntake, readAcceptedMemoryFixIntakeEvidence } from "./memory-fix-intake-settlement.js";
13
+ import { readMemoryFixPublication, readMemoryFixPublicationReference } from "./memory-fix-publication.js";
14
+ import { memoryFixBeforeDocumentReferences, publishMemoryFixBeforeDocuments } from "./distill-document-publication.js";
15
+ import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
16
+ import { validateMemoryFixStageResult, assertMemoryFixStageScope, memoryFixClosureProjection } from "./memory-fix-stage-results.js";
17
+ import { PreparedDescriptorRunSettlement } from "./descriptor-run-settlement.js";
18
+ import { prepareMemoryFixFinishIntegration } from "./memory-fix-finish-integration.js";
19
+ import { stageLifecycleMatchKey } from "./hooks.js";
20
+ import { assertDiagnosticSessionIndependence } from "./diagnostic-work-collection.js";
21
+ import { hybridDigest } from "./hybrid-contracts.js";
22
+ import { memoryFixStages } from "../domain/memory-fix-flow.js";
23
+ import { startMemoryFixStage } from "./memory-fix-stage-entry.js";
24
+ /** Engine-owned handlers only; source admission does not allocate or start a RUN. */
25
+ export const memoryFixStageHandlers = Object.freeze(memoryFixStages.map(stage => Object.freeze({
26
+ flowId: "mb-fix", stage, entryId: "memory-fix-stage-entry", finishId: "memory-fix-stage-finish",
27
+ entry: startMemoryFixStage, finish: finishMemoryFixStage
28
+ })));
29
+ function refuse(reason) {
30
+ const correctable = reason.startsWith("changed_") || ["invalid_result_json", "invalid_result_details", "result_exceeds_bound",
31
+ "preservation_evidence_required", "output_evidence_required"].includes(reason);
32
+ throw new AppError("memory_fix_stage_invalid", "Repair Stage requires its admitted publication and independent observations", 2, { reason, effect: "no_effect", ...(correctable ? { phase: "prepare", recoverable: true } : {}) });
33
+ }
34
+ function readTerminalReport(context, scope, attempt, root) {
35
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_report' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')='finish' AND json_extract(payload_json,'$.attempt')=? LIMIT 2", [scope.projectId, scope.runId, attempt]);
36
+ if (!rows.length)
37
+ return null;
38
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 8 * 1024 * 1024)
39
+ refuse("terminal_report_custody_invalid");
40
+ try {
41
+ const retained = JSON.parse(rows[0].payload_json);
42
+ const report = prepareStageReport(JSON.parse(retained.bytes.json));
43
+ if (retained.sha256 !== report.sha256 || hybridDigest(retained.bytes) !== hybridDigest(report.bytes)
44
+ || resolveStagePublicationPath(context, scope.projectId, scope.runId, retained.root) !== root)
45
+ refuse("terminal_report_changed");
46
+ return report;
47
+ }
48
+ catch (error) {
49
+ if (error instanceof AppError)
50
+ throw error;
51
+ return refuse("terminal_report_custody_invalid");
52
+ }
53
+ }
54
+ /** Common Stage settlement; public launch still requires the real RUN factory,
55
+ * command dispatch and installed family acceptance. */
56
+ export function finishMemoryFixStage(context, input) {
57
+ if (input.stage === "intake")
58
+ return finishMemoryFixIntake(context, input);
59
+ if (!["repair", "verify", "review", "finish"].includes(input.stage))
60
+ refuse("repair_stage_required");
61
+ const stage = input.stage;
62
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
63
+ const bundle = readFlowRunExecutionBundle(context, input);
64
+ if (!bundle || bundle.manifest.flow_id !== "mb-fix")
65
+ refuse("memory_fix_bundle_required");
66
+ const scope = { projectId: project.id, runId: input.runId, stage };
67
+ const source = readMemoryFixSourceIntake(context, input, bundle), run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
68
+ const packet = readDescriptorStageInput(context, scope, bundle, true, true);
69
+ const previousStage = stage === "repair" ? "intake" : stage === "verify" ? "repair" : stage === "review" ? "verify" : "review";
70
+ const previous = readAcceptedStageData(context, { ...scope, stage: previousStage });
71
+ if (packet.input.predecessor?.stage !== previousStage || hybridDigest(packet.input.predecessor.result) !== hybridDigest(previous.result))
72
+ refuse("predecessor_changed");
73
+ const prior = JSON.parse(previous.bytes.toString("utf8"));
74
+ const intakeFile = path.join(packet.runRoot, bundle.descriptor.stage("intake").directory, "intake-evidence.json");
75
+ const intake = readAcceptedMemoryFixIntakeEvidence(context, scope, intakeFile);
76
+ if (intake.intake_sha256 !== source.sha256)
77
+ refuse("source_intake_changed");
78
+ const publication = readMemoryFixPublication(context, { project_id: project.id, run_id: input.runId });
79
+ const repairInput = stage === "repair" ? packet : readDescriptorStageInput(context, { ...scope, stage: "repair" }, bundle, true, true);
80
+ const intakeResult = readAcceptedStageData(context, { ...scope, stage: "intake" });
81
+ if (intake.write_scope) {
82
+ if (!publication?.completed || publication.binding.bundle_sha256 !== bundle.sha256
83
+ || publication.binding.attempt !== repairInput.attempt || publication.binding.input_sha256 !== repairInput.inputSha256
84
+ || publication.binding.intake_sha256 !== source.sha256 || publication.binding.source_observation_sha256 !== intake.source_observation_sha256
85
+ || hybridDigest(publication.binding.intake_result) !== hybridDigest(intakeResult.result)
86
+ || hybridDigest(publication.binding.write_scope) !== hybridDigest(intake.write_scope))
87
+ refuse("completed_publication_required");
88
+ }
89
+ else if (publication)
90
+ refuse("publication_without_accepted_scope");
91
+ const reference = publication ? readMemoryFixPublicationReference(context, publication, `${bundle.descriptor.stage("repair").directory}/publication.json`) : null;
92
+ if (publication && !reference)
93
+ refuse("publication_reference_required");
94
+ if (!input.bytes.length || input.bytes.length > 1024 * 1024)
95
+ refuse("result_exceeds_bound");
96
+ let result;
97
+ try {
98
+ result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes));
99
+ }
100
+ catch {
101
+ return refuse("invalid_result_json");
102
+ }
103
+ if (!result?.details || typeof result.details !== "object" || !Array.isArray(result.details.limitations))
104
+ refuse("invalid_result_details");
105
+ const insert = (object, field, expected) => {
106
+ const proposed = object[field];
107
+ if (proposed != null && !(Array.isArray(proposed) && !proposed.length) && hybridDigest(proposed) !== hybridDigest(expected))
108
+ refuse(`changed_${field}`);
109
+ Object.assign(object, { [field]: structuredClone(expected) });
110
+ };
111
+ for (const [field, expected] of Object.entries({ schema_id: "dd-flow/mb-fix-stage-result@1", flow_id: "mb-fix", run_id: input.runId, stage,
112
+ input_sha256: packet.inputSha256, intake_sha256: source.sha256, source_observation_sha256: intake.source_observation_sha256,
113
+ predecessors: [{ stage: previousStage, ...previous.result }] }))
114
+ insert(result, field, expected);
115
+ insert(result.details, "routing", intake.routing);
116
+ insert(result.details, "write_scope", intake.write_scope);
117
+ insert(result.details, "repair_receipt", reference);
118
+ if (stage === "review" || stage === "finish")
119
+ insert(result.details, "verification", prior.details.verification);
120
+ if (stage === "finish")
121
+ insert(result.details, "review", prior.details.review);
122
+ result.details.limitations = [...new Set([...prior.details.limitations, ...source.intake.limitations, ...result.details.limitations])];
123
+ const bytes = Buffer.from(JSON.stringify(result));
124
+ const index = JSON.parse(run.index_json), replay = index.stage_runs.some((entry) => entry.stage === stage && entry.status === "done");
125
+ const logicalReadback = `${bundle.descriptor.stage(stage).directory}/publication-readback.json`;
126
+ const readback = prepareStagePacket(path.join(packet.runRoot, logicalReadback), Buffer.from(JSON.stringify({
127
+ schema_id: "dd-flow/mb-fix-publication-readback@1", run_id: input.runId, stage, input_sha256: packet.inputSha256,
128
+ result_sha256: createHash("sha256").update(bytes).digest("hex"), publication: reference,
129
+ documents: publication ? publication.binding.write_scope.paths.map(logical => ({ path: logical,
130
+ after: publication.plan.state(publication.plan.actions().length).entries().find(entry => entry.path === logical) ?? null })) : []
131
+ })));
132
+ if (replay) {
133
+ if (!readAcceptedStageData(context, scope).bytes.equals(bytes))
134
+ refuse("accepted_result_changed");
135
+ }
136
+ else {
137
+ if (publication) {
138
+ const actual = publication.plan.classify(run.workspace_root);
139
+ if (actual.completed !== actual.total)
140
+ refuse("publication_not_observed");
141
+ publishMemoryFixBeforeDocuments(packet.runRoot, publication.plan, publication.beforeDocuments);
142
+ }
143
+ result = validateMemoryFixStageResult(bytes, bundle.descriptor.stageBlob(stage, "result_schema"), {
144
+ runId: input.runId, stage, inputSha256: packet.inputSha256, intakeSha256: source.sha256, sourceObservationSha256: intake.source_observation_sha256,
145
+ findingIds: source.intake.selected_findings.map(finding => finding.id), projectRoot: run.workspace_root, runRoot: packet.runRoot,
146
+ predecessors: [{ stage: previousStage, ...previous.result }], writeScope: intake.write_scope, repairReceipt: reference
147
+ });
148
+ if (publication && stage !== "repair") {
149
+ const evidence = result.details.verification.flatMap(item => item.evidence);
150
+ if (memoryFixBeforeDocumentReferences(publication.plan).some(document => !evidence.some(ref => ref.root === "run"
151
+ && ref.path === document.source.path && ref.sha256 === document.source.sha256)))
152
+ refuse("preservation_evidence_required");
153
+ const after = publication.plan.state(publication.plan.actions().length).entries();
154
+ if (publication.binding.write_scope.paths.some(logical => {
155
+ const document = after.find(entry => entry.path === logical);
156
+ return document?.kind === "file" && !evidence.some(ref => ref.root === "project" && ref.path === logical && ref.sha256 === document.sha256);
157
+ }))
158
+ refuse("output_evidence_required");
159
+ }
160
+ assertMemoryFixStageScope(result, prior);
161
+ }
162
+ const settlement = PreparedDescriptorStageSettlement.prepare(context, { ...input, bytes });
163
+ const keys = (name) => entityReferenceVariants(input.runId).map(ref => stageLifecycleMatchKey("stage_finish", ref, name, input.projectRoot));
164
+ const repairKeys = stage !== "repair" || replay ? keys("repair") : [];
165
+ const verifyKeys = stage === "review" || stage === "finish" ? keys("verify") : [];
166
+ const reviewKeys = stage === "finish" ? keys("review") : [];
167
+ const readOwner = (writer) => {
168
+ const physical = (sessionId) => {
169
+ const session = writer.db.get("SELECT harness,provider_session_id FROM sessions WHERE project_id=? AND run_id=? AND session_id=?", [project.id, input.runId, sessionId]);
170
+ if (!session?.harness || !session.provider_session_id)
171
+ refuse("physical_owner_missing");
172
+ return session;
173
+ };
174
+ const repair = stage !== "repair" || replay ? readAcceptedDescriptorStageSettlement(writer, { ...scope, stage: "repair" }, bundle, repairKeys) : null;
175
+ const verify = stage === "review" || stage === "finish" ? readAcceptedDescriptorStageSettlement(writer, { ...scope, stage: "verify" }, bundle, verifyKeys) : null;
176
+ const review = stage === "finish" ? readAcceptedDescriptorStageSettlement(writer, { ...scope, stage: "review" }, bundle, reviewKeys) : null;
177
+ const authorId = repair?.binding.sessionId ?? settlement.coordinatorSessionId(), author = physical(authorId);
178
+ const authorProof = repair ? { work_id: repair.work_id, work_session_id: repair.work_session_id, session_id: authorId,
179
+ ...author, start_hook: repair.binding.startHook, association_sha256: repair.binding.associationSha256,
180
+ physical_binding_sha256: repair.binding.physicalBindingSha256, prompt_path: repair.binding.promptPath }
181
+ : readActiveDescriptorStageCoordinator(writer, scope, bundle);
182
+ if (publication && hybridDigest(publication.binding.author) !== hybridDigest(authorProof))
183
+ refuse("publication_author_changed");
184
+ if (stage === "finish") {
185
+ assertDiagnosticSessionIndependence([physical(verify.binding.sessionId)], [author]);
186
+ assertDiagnosticSessionIndependence([physical(review.binding.sessionId)], [author, physical(verify.binding.sessionId)]);
187
+ }
188
+ else if (stage !== "repair")
189
+ assertDiagnosticSessionIndependence([physical(settlement.coordinatorSessionId())], [author, ...(verify ? [physical(verify.binding.sessionId)] : [])]);
190
+ return { repair, verify, review, author };
191
+ };
192
+ const owner = context.db.readSnapshot(() => readOwner(context));
193
+ const closure = stage === "finish" ? memoryFixClosureProjection(result) : null;
194
+ const reportRoot = path.join(packet.runRoot, bundle.descriptor.stage(stage).directory);
195
+ const retainedReport = stage === "finish" ? readTerminalReport(context, scope, packet.attempt, reportRoot) : null;
196
+ if (stage === "finish" && replay && !retainedReport)
197
+ refuse("accepted_terminal_report_missing");
198
+ const acceptedRepair = !!closure && !!publication && !closure.some(item => item.status === "open");
199
+ const integration = stage === "finish" && acceptedRepair && (!replay || run.status !== "done")
200
+ ? prepareMemoryFixFinishIntegration(context, scope, bundle) : null;
201
+ const integrationOutcome = replay && retainedReport ? retainedReport.normalized.integration
202
+ : integration ? { state: "integrated", observation: integration.value() }
203
+ : { state: publication ? "not_integrated_open_findings" : "not_applicable" };
204
+ const report = closure ? prepareStageReport({ run_id: input.runId, stage: "finish",
205
+ verdict: closure.some(item => item.status === "open") ? "completed_with_open_findings" : "completed", summary: result.summary,
206
+ memory_fix: { result, closure, source: source.intake.source, source_intake: source.reference,
207
+ unselected_ids: source.intake.unselected_ids, limitations: source.intake.limitations }, integration: integrationOutcome,
208
+ semantic: { acceptance: closure.map(item => `${item.finding_id}: ${item.status}; route=${item.route}`),
209
+ checks: result.details.verification.map(item => `${item.finding_id}: ${item.verdict}; ${item.summary}`),
210
+ evidence: [...result.details.verification, ...result.details.review].flatMap(item => item.evidence.map(ref => `${ref.root}:${ref.path}@${ref.sha256}`)),
211
+ next_action: closure.some(item => item.status === "open") ? "Open findings remain open; retain unintegrated work for explicit follow-up. Do not silently retry or close DEF records."
212
+ : "Only this accepted scope is complete; routed, deferred and unselected findings are not repaired by this RUN." } }) : null;
213
+ if (report && Buffer.byteLength(JSON.stringify(report.bytes)) > 7 * 1024 * 1024)
214
+ refuse("terminal_report_exceeds_bound");
215
+ if (retainedReport && report && retainedReport.sha256 !== report.sha256)
216
+ refuse("accepted_terminal_report_changed");
217
+ integration?.revalidate();
218
+ if (!replay && publication) {
219
+ const observed = publication.plan.classify(run.workspace_root);
220
+ if (observed.completed !== observed.total)
221
+ refuse("publication_not_observed");
222
+ }
223
+ const fence = (writer) => {
224
+ assertMemoryFixSourceIntakeCustody(writer, scope, bundle, source.sha256);
225
+ if (hybridDigest(readAcceptedStageData(writer, { ...scope, stage: previousStage })) !== hybridDigest(previous)
226
+ || hybridDigest(readAcceptedMemoryFixIntakeEvidence(writer, scope, intakeFile)) !== hybridDigest(intake)
227
+ || hybridDigest(readOwner(writer)) !== hybridDigest(owner))
228
+ refuse("stage_authority_changed");
229
+ const current = readMemoryFixPublication(writer, { project_id: project.id, run_id: input.runId });
230
+ if (publication && (!current?.completed || current.plan.sha256 !== publication.plan.sha256 || hybridDigest(current.binding) !== hybridDigest(publication.binding)
231
+ || hybridDigest(readMemoryFixPublicationReference(writer, current, reference.path)) !== hybridDigest(reference)))
232
+ refuse("publication_changed");
233
+ if (!publication && current)
234
+ refuse("publication_without_accepted_scope");
235
+ if (stage === "repair" && !replay && intake.user_inputs_sha256 !== hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, "mb-fix", bundle)))
236
+ refuse("user_inputs_changed");
237
+ const rows = writer.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')=? AND json_extract(payload_json,'$.attempt')=? LIMIT 33", [project.id, input.runId, stage, packet.attempt]);
238
+ if (rows.length > 32 || rows.some(row => Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024))
239
+ refuse("readback_custody_invalid");
240
+ const packets = rows.map(row => JSON.parse(row.payload_json)).filter(value => typeof value.file === "string"
241
+ && resolveStagePublicationPath(writer, project.id, input.runId, value.file) === readback.file);
242
+ if (packets.length > 1 || packets.some(value => value.sha256 !== readback.sha256 || value.base64 !== readback.bytes.toString("base64")))
243
+ refuse("readback_custody_changed");
244
+ if (!packets.length) {
245
+ if (replay)
246
+ refuse("accepted_readback_missing");
247
+ acceptStagePacket(writer, { ...scope, prepared: readback });
248
+ }
249
+ integration?.assertCustody(writer);
250
+ if (report) {
251
+ const currentReport = readTerminalReport(writer, scope, packet.attempt, reportRoot);
252
+ if (currentReport && currentReport.sha256 !== report.sha256)
253
+ refuse("terminal_report_changed");
254
+ if (!currentReport) {
255
+ if (replay)
256
+ refuse("accepted_terminal_report_missing");
257
+ acceptStageReport(writer, { ...scope, root: reportRoot, prepared: report });
258
+ }
259
+ }
260
+ };
261
+ try {
262
+ const accepted = settlement.apply(context, fence);
263
+ if (stage !== "finish")
264
+ return accepted;
265
+ integration?.revalidate();
266
+ const complete = PreparedDescriptorRunSettlement.prepare(context, input).apply(context, fence);
267
+ publishStageReport(context, project.id, input.runId, "finish", reportRoot);
268
+ return { ...accepted, ...complete, closure, reports: ["json", "md", "html"].map(extension => path.join(reportRoot, `stage-report.${extension}`)) };
269
+ }
270
+ catch (error) {
271
+ if (stage !== "finish")
272
+ throw error;
273
+ let committed = false;
274
+ try {
275
+ committed = readAcceptedStageData(context, scope).bytes.equals(bytes);
276
+ }
277
+ catch { /* No accepted terminal data. */ }
278
+ if (!committed)
279
+ throw error;
280
+ throw new AppError("memory_fix_finish_pending", "Accepted finish requires projection or RUN settlement reconciliation", 1, { effect: "committed", run_id: input.runId, next_action: "reconcile_accepted_finish", cause: error instanceof AppError ? error.code : "finish_settlement_failed" });
281
+ }
282
+ }
@@ -0,0 +1,106 @@
1
+ import { Ajv2020 } from "ajv/dist/2020.js";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { validateMemoryFixRouting } from "./diagnostic-repair-intake.js";
5
+ import { assertMemoryRepairWritePaths } from "./memory-review-correction-plan.js";
6
+ import { verifyDiagnosticSources } from "./diagnostic-evidence.js";
7
+ function refuse(reason) {
8
+ throw new AppError("memory_fix_stage_result_invalid", "Memory repair result requires its selected input, bounded publication and independent observations", 2, { reason, effect: "no_effect", ...(reason === "invalid_schema" ? { phase: "prepare", recoverable: true } : {}) });
9
+ }
10
+ /** Contract/evidence validation only. Registered handlers must prove actual
11
+ * delivered consent, source-drift assessment, publication and distinct physical
12
+ * writer/verifier/reviewer ownership before settlement or finding closure.
13
+ */
14
+ export function validateMemoryFixStageResult(bytes, schemaBytes, binding) {
15
+ if (!bytes.length || bytes.length > 1024 * 1024 || !schemaBytes.length || schemaBytes.length > 1024 * 1024)
16
+ refuse("result_exceeds_bound");
17
+ let result;
18
+ try {
19
+ result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
20
+ }
21
+ catch {
22
+ return refuse("invalid_schema");
23
+ }
24
+ const validate = (() => {
25
+ try {
26
+ const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(schemaBytes));
27
+ if (schema.$id !== "dd-flow/mb-fix-stage-result@1")
28
+ refuse("frozen_schema_invalid");
29
+ return new Ajv2020({ strict: true, validateFormats: false }).compile(schema);
30
+ }
31
+ catch {
32
+ return refuse("frozen_schema_invalid");
33
+ }
34
+ })();
35
+ if (!validate(result) || result?.schema_id !== "dd-flow/mb-fix-stage-result@1" || result.flow_id !== "mb-fix"
36
+ || typeof result.summary !== "string" || !result.summary.trim())
37
+ refuse("invalid_schema");
38
+ if (result.run_id !== binding.runId || result.stage !== binding.stage || result.input_sha256 !== binding.inputSha256
39
+ || result.intake_sha256 !== binding.intakeSha256 || result.source_observation_sha256 !== binding.sourceObservationSha256
40
+ || hybridDigest(result.predecessors) !== hybridDigest(binding.predecessors))
41
+ refuse("foreign_stage_binding");
42
+ const details = result.details, routing = validateMemoryFixRouting(binding.findingIds, details.routing);
43
+ if (hybridDigest(routing) !== hybridDigest(details.routing))
44
+ refuse("routing_order_changed");
45
+ const repairs = routing.filter(decision => decision.route === "memory_repair").map(decision => decision.finding_id);
46
+ if (hybridDigest(details.write_scope) !== hybridDigest(binding.writeScope)
47
+ || hybridDigest(details.repair_receipt) !== hybridDigest(binding.repairReceipt))
48
+ refuse("unowned_repair_claim");
49
+ if (details.write_scope) {
50
+ const scope = details.write_scope, paths = [...new Set(routing.flatMap(decision => decision.write_paths))].sort();
51
+ assertMemoryRepairWritePaths(scope.paths);
52
+ if (hybridDigest(scope.finding_ids) !== hybridDigest(repairs)
53
+ || hybridDigest([...scope.paths].sort()) !== hybridDigest(paths))
54
+ refuse("repair_scope_changed");
55
+ }
56
+ if (!repairs.length && (details.write_scope || details.repair_receipt))
57
+ refuse("publication_without_repair_findings");
58
+ if (repairs.length && !details.write_scope)
59
+ refuse("intake_requires_approved_scope");
60
+ if (binding.stage === "intake" && details.repair_receipt)
61
+ refuse("repair_before_intake");
62
+ if (binding.stage !== "intake" && repairs.length && (!details.write_scope || !details.repair_receipt))
63
+ refuse("repair_requires_scope_and_receipt");
64
+ // Approval/publication authority is independently selected from SQL by the
65
+ // handler. Re-reading mutable projections here neither proves user consent
66
+ // nor strengthens that custody, and would break supported projection restore.
67
+ const verify = binding.stage === "verify" || binding.stage === "review" || binding.stage === "finish";
68
+ const review = binding.stage === "review" || binding.stage === "finish";
69
+ const observations = (items, expected) => {
70
+ const ids = items.map(item => item.finding_id);
71
+ if (new Set(ids).size !== ids.length || ids.length !== expected.length || expected.some(id => !ids.includes(id)))
72
+ refuse("incomplete_finding_observations");
73
+ for (const item of items) {
74
+ if (!item.summary.trim())
75
+ refuse("invalid_schema");
76
+ for (const evidence of item.evidence)
77
+ verifyDiagnosticSources(evidence.root === "run" ? binding.runRoot : binding.projectRoot, [evidence]);
78
+ }
79
+ };
80
+ observations(details.verification, verify ? repairs : []);
81
+ observations(details.review, review ? binding.findingIds : []);
82
+ if (details.verification.some(item => item.verdict === "resolved" && !item.evidence.length))
83
+ refuse("closure_without_evidence");
84
+ if (details.review.some(item => item.disposition === "accept" && !item.evidence.length))
85
+ refuse("closure_without_evidence");
86
+ return result;
87
+ }
88
+ export function assertMemoryFixStageScope(result, prior) {
89
+ if (result.intake_sha256 !== prior.intake_sha256 || result.source_observation_sha256 !== prior.source_observation_sha256
90
+ || hybridDigest(result.details.routing) !== hybridDigest(prior.details.routing)
91
+ || hybridDigest(result.details.write_scope) !== hybridDigest(prior.details.write_scope)
92
+ || prior.details.limitations.some(value => !result.details.limitations.includes(value))
93
+ || prior.stage !== "intake" && hybridDigest(result.details.repair_receipt) !== hybridDigest(prior.details.repair_receipt)
94
+ || ["verify", "review"].includes(prior.stage) && hybridDigest(result.details.verification) !== hybridDigest(prior.details.verification)
95
+ || prior.stage === "review" && hybridDigest(result.details.review) !== hybridDigest(prior.details.review))
96
+ refuse("accepted_scope_changed");
97
+ }
98
+ /** Proposed report projection, never a DEF mutation or independent ownership proof. */
99
+ export function memoryFixClosureProjection(result) {
100
+ if (result.stage !== "finish")
101
+ refuse("finish_required");
102
+ return result.details.routing.map(decision => ({ finding_id: decision.finding_id, route: decision.route,
103
+ status: decision.route === "defer" ? "deferred" : decision.route !== "memory_repair" ? "routed"
104
+ : result.details.verification.some(item => item.finding_id === decision.finding_id && item.verdict === "resolved")
105
+ && result.details.review.some(item => item.finding_id === decision.finding_id && item.disposition === "accept") ? "closed" : "open" }));
106
+ }
@@ -0,0 +1,127 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { projectCheckoutRoot, resolveProjectRoot } from "../storage/paths.js";
6
+ import { requireProjectByRoot } from "./projects.js";
7
+ import { loadVnextWorkspacePolicy } from "./vnext-workspace-policy.js";
8
+ import { prepareManagedWorktree, createManagedWorktree } from "./worktrees.js";
9
+ import { inspectMemoryGitExecutable } from "./memory-init-preflight.js";
10
+ import { prepareMemoryFixGitPolicy } from "./memory-fix-finish-integration.js";
11
+ import { hybridDigest } from "./hybrid-contracts.js";
12
+ import { appendAudit } from "./audit.js";
13
+ const intentEvent = "memory_fix.workspace_intent", readyEvent = "memory_fix.workspace_ready";
14
+ function refuse(reason) { throw new AppError("memory_fix_workspace_invalid", "Repair workspace requires its original project-scoped selection", 2, { reason, effect: "no_effect" }); }
15
+ function pending(reason, details = {}) {
16
+ throw new AppError("memory_fix_workspace_pending", "Reconcile the original workspace creation before redispatch", 1, { reason, effect: "unknown", next_action: "reconcile_original_workspace", ...details });
17
+ }
18
+ /** Worktrunk owns physical creation. This wrapper retains one intent before
19
+ * dispatch and never reruns an unresolved creation or adopts a foreign tree. */
20
+ export async function prepareMemoryFixWorkspace(context, input) {
21
+ if (context.db.isOwnedWriteTransaction?.())
22
+ refuse("preparation_outside_writer_required");
23
+ const projectRoot = resolveProjectRoot(input.projectRoot), project = requireProjectByRoot(context, projectRoot);
24
+ if (projectRoot !== input.projectRoot || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(input.requestId))
25
+ refuse("invalid_scope");
26
+ if (input.selectionSha256 !== undefined && !/^[a-f0-9]{64}$/.test(input.selectionSha256))
27
+ refuse("invalid_selection_identity");
28
+ const scope = { projectRoot, requestId: input.requestId };
29
+ const read = (event) => {
30
+ 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, input.requestId]);
31
+ if (!rows.length)
32
+ return null;
33
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 32768)
34
+ pending("ambiguous_workspace_custody");
35
+ try {
36
+ const value = JSON.parse(rows[0].payload_json);
37
+ if (hybridDigest(value.scope) !== hybridDigest(scope))
38
+ pending("workspace_scope_changed");
39
+ return value;
40
+ }
41
+ catch {
42
+ return pending("invalid_workspace_custody");
43
+ }
44
+ };
45
+ const observe = (intent) => {
46
+ const policy = prepareMemoryFixGitPolicy(context.env, { scope: { ...scope, workspaceRoot: intent.workspace_root }, gitExecutable: intent.git_executable, selection: intent.git_policy });
47
+ const git = JSON.parse(policy.bytes.toString("utf8")).policy;
48
+ if (intent.creation && (git.workspace_branch !== intent.creation.branch || git.initial_head !== intent.creation.base))
49
+ refuse("created_workspace_baseline_changed");
50
+ const stat = fs.lstatSync(intent.workspace_root);
51
+ if (!stat.isDirectory() || fs.realpathSync(intent.workspace_root) !== intent.workspace_root)
52
+ refuse("workspace_identity_changed");
53
+ return { policy_sha256: policy.sha256, owner: { dev: stat.dev, ino: stat.ino } };
54
+ };
55
+ const retainReady = (intent, actual) => context.db.writeTransaction(() => {
56
+ const intentSha256 = hybridDigest(intent), value = { scope, intent_sha256: intentSha256, ...actual };
57
+ if (hybridDigest(read(intentEvent)) !== intentSha256)
58
+ pending("workspace_custody_changed");
59
+ const current = read(readyEvent);
60
+ if (current) {
61
+ if (hybridDigest(current) !== hybridDigest(value))
62
+ pending("workspace_custody_changed");
63
+ return;
64
+ }
65
+ appendAudit(context, { projectId: project.id, eventType: readyEvent, payload: value });
66
+ });
67
+ const prior = read(intentEvent);
68
+ let ready = read(readyEvent);
69
+ if (prior) {
70
+ if (input.workspaceRoot !== undefined && input.workspaceRoot !== prior.workspace_root
71
+ || input.selectionSha256 !== undefined && input.selectionSha256 !== prior.selection_sha256
72
+ || input.gitPolicy !== undefined && hybridDigest(input.gitPolicy) !== hybridDigest(prior.git_policy))
73
+ refuse("request_changed");
74
+ if (!ready && prior.creation && fs.existsSync(prior.workspace_root)) {
75
+ // Read-only reconciliation of an original creation with a lost reply.
76
+ // Exact private path/repository/branch/base and a clean tree are required;
77
+ // no second Worktrunk call, reset or adoption of changed user work occurs.
78
+ const actual = observe(prior), result = spawnSync(prior.git_executable, ["-c", "core.fsmonitor=false", "-C", prior.workspace_root, "status", "--porcelain=v1", "-z", "--untracked-files=all"], { env: { ...Object.fromEntries(Object.entries(context.env).filter(([key]) => !key.startsWith("GIT_"))), GIT_OPTIONAL_LOCKS: "0" }, timeout: 5000, maxBuffer: 1024 * 1024 });
79
+ if (result.error || result.signal || result.status !== 0 || result.stdout.length)
80
+ pending("created_workspace_not_pristine");
81
+ retainReady(prior, actual);
82
+ ready = read(readyEvent);
83
+ }
84
+ if (!ready || ready.intent_sha256 !== hybridDigest(prior))
85
+ pending("workspace_creation_unresolved");
86
+ const actual = observe(prior);
87
+ if (hybridDigest(actual) !== hybridDigest({ policy_sha256: ready.policy_sha256, owner: ready.owner }))
88
+ refuse("workspace_changed_before_request_retention");
89
+ return { workspaceRoot: prior.workspace_root, gitPolicy: prior.git_policy, gitExecutable: prior.git_executable };
90
+ }
91
+ if (ready)
92
+ pending("workspace_intent_missing");
93
+ const defaults = input.gitPolicy ? null : loadVnextWorkspacePolicy(projectRoot).workspace;
94
+ const gitPolicy = input.gitPolicy ?? { route: defaults.route, integrationBranch: defaults.integration_branch };
95
+ const gitExecutable = inspectMemoryGitExecutable(context.env).executable;
96
+ const key = hybridDigest({ project_id: project.id, scope }).slice(0, 16);
97
+ const workspaceRoot = input.workspaceRoot ?? (gitPolicy.route === "integration_branch_direct" ? projectRoot
98
+ : path.join(projectCheckoutRoot(context.ddFlowHome, project.id), "worktrees", `mb-fix-${key}`, path.basename(projectRoot)));
99
+ const creation = input.workspaceRoot === undefined && gitPolicy.route === "feature_worktree"
100
+ ? prepareManagedWorktree({ projectRoot, branch: `codex/mb-fix-${key}`, base: gitPolicy.integrationBranch, path: workspaceRoot }) : null;
101
+ const intent = { scope, workspace_root: workspaceRoot, git_policy: structuredClone(gitPolicy), git_executable: gitExecutable,
102
+ selection_sha256: input.selectionSha256 ?? null, creation };
103
+ if (!creation)
104
+ observe(intent);
105
+ const claimed = context.db.writeTransaction(() => {
106
+ if (read(intentEvent) || read(readyEvent))
107
+ return false;
108
+ appendAudit(context, { projectId: project.id, eventType: intentEvent, payload: intent });
109
+ return true;
110
+ });
111
+ if (!claimed)
112
+ pending("workspace_creation_already_owned");
113
+ try {
114
+ if (creation) {
115
+ const created = await createManagedWorktree(context, creation, creation);
116
+ if (created.worktreePath !== workspaceRoot)
117
+ pending("unexpected_worktrunk_workspace", { expected_workspace_root: workspaceRoot, observed_workspace_root: created.worktreePath });
118
+ }
119
+ retainReady(intent, observe(intent));
120
+ return { workspaceRoot, gitPolicy, gitExecutable };
121
+ }
122
+ catch (error) {
123
+ if (error instanceof AppError && error.code === "memory_fix_workspace_pending")
124
+ throw error;
125
+ return pending(error instanceof AppError ? error.code : "workspace_creation_failed");
126
+ }
127
+ }