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

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 +31 -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 +274 -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 +115 -17
  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 +2 -1
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
  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-child-repair.js +95 -0
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +19 -9
  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 +250 -33
  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 -7
  190. package/dist/services/retained-native-hook.js +274 -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 +25 -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 +257 -44
  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 +8 -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 +255 -69
  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 +3 -2
  227. package/tools/verification-jobs.mjs +35 -10
@@ -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
+ }
@@ -0,0 +1,88 @@
1
+ import path from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
5
+ import { requireProjectByRoot } from "./projects.js";
6
+ import { resolveProjectRoot } from "../storage/paths.js";
7
+ import { resolveStagePublicationPath } from "./stage-report-renderer.js";
8
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
9
+ const inputOwners = {
10
+ "mb-distill": { code: "distill_user_input_invalid", message: "Distillation needs its original delivered request and answers", bundle: "distill_bundle_required", schema: "dd-flow/distill-user-inputs@1" },
11
+ "mb-fix": { code: "memory_fix_user_input_invalid", message: "Memory repair needs its original delivered request and answers", bundle: "memory_fix_bundle_required", schema: "dd-flow/mb-fix-user-inputs@1" },
12
+ "review-fix": { code: "review_fix_user_input_invalid", message: "Review-fix needs its original delivered request and answers", bundle: "review_fix_bundle_required", schema: "dd-flow/review-fix-user-inputs@1" },
13
+ "interactive": { code: "interactive_user_input_invalid", message: "Interactive needs its original delivered request and answers", bundle: "interactive_bundle_required", schema: "dd-flow/interactive-user-answers@1" },
14
+ "release": { code: "delivery_user_input_invalid", message: "Release needs its original delivered request and answers", bundle: "delivery_bundle_required", schema: "dd-flow/release-user-inputs@1" },
15
+ "deploy": { code: "delivery_user_input_invalid", message: "Deploy needs its original delivered request and answers", bundle: "delivery_bundle_required", schema: "dd-flow/deploy-user-inputs@1" },
16
+ "publish": { code: "delivery_user_input_invalid", message: "Publish needs its original delivered request and answers", bundle: "delivery_bundle_required", schema: "dd-flow/publish-user-inputs@1" }
17
+ };
18
+ /** Read task evidence, not approval semantics. A Stage owner must consider the
19
+ * full ordered input set, select the actual agreement and fence this same set
20
+ * again before publication. Agent files and result claims are not user input.
21
+ * SQL custody survives projection loss and supported RUN relocation. */
22
+ export function readMemoryFlowUserInputs(context, input, flowId, retainedBundle) {
23
+ const owner = Object.hasOwn(inputOwners, flowId) ? inputOwners[flowId] : inputOwners["mb-fix"];
24
+ function refuse(reason) {
25
+ throw new AppError(owner.code, owner.message, 1, { reason, effect: "no_effect" });
26
+ }
27
+ if (!Object.hasOwn(inputOwners, flowId))
28
+ refuse("unsupported_flow");
29
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
30
+ const bundle = retainedBundle ?? readFlowRunExecutionBundle(context, input);
31
+ return readPreparedMemoryFlowUserInputs(context, { projectId: project.id, runId: input.runId }, flowId, bundle);
32
+ }
33
+ /** SQL/in-memory read for an already selected RUN. No root discovery or
34
+ * filesystem reads occur inside a Stage/publication settlement fence. */
35
+ export function readPreparedMemoryFlowUserInputs(context, scope, flowId, bundle) {
36
+ const owner = Object.hasOwn(inputOwners, flowId) ? inputOwners[flowId] : inputOwners["mb-fix"];
37
+ function refuse(reason) {
38
+ throw new AppError(owner.code, owner.message, 1, { reason, effect: "no_effect" });
39
+ }
40
+ if (!Object.hasOwn(inputOwners, flowId))
41
+ refuse("unsupported_flow");
42
+ if (!bundle || bundle.manifest.flow_id !== flowId)
43
+ refuse(owner.bundle);
44
+ return context.db.readSnapshot(() => {
45
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
46
+ if (!run.run_root)
47
+ refuse("run_home_required");
48
+ const request = bundle.blob("runtime/source-request.md"), reference = bundle.manifest.inventory.find(ref => ref.path === "runtime/source-request.md");
49
+ if (!request.length || request.length > 64 * 1024 || !reference || hash(request) !== reference.sha256)
50
+ refuse("original_request_missing");
51
+ const sources = [{ role: "request", event_id: 0, stage: null, pause_id: null,
52
+ root: "run", path: "execution-bundle/runtime/source-request.md", sha256: reference.sha256, base64: request.toString("base64") }];
53
+ const rows = context.db.all("SELECT id,payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.stage_resumed' AND json_extract(payload_json,'$.run_id')=? ORDER BY id LIMIT 129", [scope.projectId, scope.runId]);
54
+ if (rows.length > 128)
55
+ refuse("too_many_answers");
56
+ const pauses = new Set();
57
+ let total = request.length;
58
+ for (const row of rows) {
59
+ if (Buffer.byteLength(row.payload_json) > 64 * 1024)
60
+ refuse("unbounded_answer_event");
61
+ const answer = JSON.parse(row.payload_json);
62
+ if (typeof answer.stage !== "string" || typeof answer.pause_id !== "string" || !answer.pause_id || pauses.has(answer.pause_id)
63
+ || typeof answer.answer_path !== "string")
64
+ refuse("ambiguous_answer");
65
+ pauses.add(answer.pause_id);
66
+ for (const [role, locator] of [["question", path.join(path.dirname(answer.answer_path), "question.md")], ["answer", answer.answer_path]]) {
67
+ const file = resolveStagePublicationPath(context, scope.projectId, scope.runId, locator);
68
+ const relative = path.relative(run.run_root, file).split(path.sep).join("/");
69
+ if (!relative || path.isAbsolute(relative) || relative.split("/").some(part => !part || part === ".." || part === "."))
70
+ refuse("input_outside_run");
71
+ const packets = 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')=? AND json_extract(payload_json,'$.file')=? LIMIT 2", [scope.projectId, scope.runId, answer.stage, locator]);
72
+ if (packets.length !== 1 || Buffer.byteLength(packets[0].payload_json) > 2 * 1024 * 1024)
73
+ refuse("answer_custody_missing");
74
+ const packet = JSON.parse(packets[0].payload_json);
75
+ if (typeof packet.base64 !== "string")
76
+ refuse("answer_custody_changed");
77
+ const bytes = Buffer.from(packet.base64, "base64");
78
+ total += bytes.length;
79
+ if (!bytes.length || bytes.length > 1024 * 1024 || total > 16 * 1024 * 1024 || bytes.toString("base64") !== packet.base64
80
+ || hash(bytes) !== packet.sha256)
81
+ refuse("answer_custody_changed");
82
+ sources.push({ role: role, event_id: row.id, stage: answer.stage, pause_id: answer.pause_id,
83
+ root: "run", path: relative, sha256: packet.sha256, base64: packet.base64 });
84
+ }
85
+ }
86
+ return { schema_id: owner.schema, run_id: scope.runId, bundle_sha256: bundle.sha256, sources };
87
+ });
88
+ }
@@ -7,20 +7,70 @@ import { publishImmutableInput } from "../storage/immutable-input.js";
7
7
  import { readBoundedReleaseFile } from "./release-source.js";
8
8
  import { readMemoryInitStartupInput } from "./memory-init-startup-custody.js";
9
9
  import { readUpgradeStorageReadiness } from "./memory-upgrade-storage-readiness.js";
10
- import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
10
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
11
11
  import { readDiagnosticRequest } from "./diagnostic-start.js";
12
+ import { isDeliveryFlow } from "../domain/delivery-flows.js";
13
+ /** Repair's objective is already part of its own retained bundle. Continuation
14
+ * does not need the source diagnostic or mutable operator/request files. */
15
+ export function prepareMemoryFixContexts(bundle, directory) {
16
+ return prepareRequestContexts(bundle, directory, "mb-fix");
17
+ }
18
+ /** Static controller candidates carry only the original request. Current turns,
19
+ * scope and checks are read from their retained owners at each Stage boundary. */
20
+ export function prepareInteractiveContexts(bundle, directory) {
21
+ return prepareRequestContexts(bundle, directory, "interactive");
22
+ }
23
+ export function prepareDeliveryContexts(bundle, directory) {
24
+ if (!isDeliveryFlow(bundle.manifest.flow_id))
25
+ throw new AppError("delivery_context_invalid", "Delivery bundle required", 2, { effect: "no_effect" });
26
+ return prepareRequestContexts(bundle, directory, bundle.manifest.flow_id);
27
+ }
28
+ export function publishDeliveryContexts(bundle, objective, directory, expectedSha256, expectedOwner) {
29
+ const original = prepareDeliveryContexts(bundle, directory);
30
+ if (objective !== original.objective)
31
+ throw new AppError("delivery_context_invalid", "Delivery objective cannot be replaced", 2, { effect: "no_effect" });
32
+ return publishMemoryContexts(bundle, objective, directory, expectedSha256, expectedOwner, bundle.manifest.flow_id);
33
+ }
34
+ function prepareRequestContexts(bundle, directory, flowId) {
35
+ const code = isDeliveryFlow(flowId) ? "delivery_context_invalid" : flowId === "interactive" ? "interactive_context_invalid" : "memory_fix_context_invalid";
36
+ if (!(bundle instanceof RetainedExecutionBundle) || bundle.manifest.flow_id !== flowId)
37
+ throw new AppError(code, "Retained request-owning bundle required", 2, { effect: "no_effect" });
38
+ const bytes = bundle.blob("runtime/source-request.md"), refs = bundle.manifest.inventory.filter(ref => ref.path === "runtime/source-request.md");
39
+ if (!bytes.length || bytes.length > 64 * 1024 || refs.length !== 1 || refs[0].sha256 !== createHash("sha256").update(bytes).digest("hex"))
40
+ throw new AppError(code, "Original retained request required", 2, { effect: "no_effect" });
41
+ let objective;
42
+ try {
43
+ objective = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
44
+ }
45
+ catch {
46
+ throw new AppError(code, "Original request must be UTF-8", 2, { effect: "no_effect" });
47
+ }
48
+ return { ...prepareMemoryContexts(bundle, objective, directory, flowId), objective };
49
+ }
50
+ export function publishInteractiveContexts(bundle, objective, directory, expectedSha256, expectedOwner) {
51
+ const original = prepareInteractiveContexts(bundle, directory);
52
+ if (objective !== original.objective)
53
+ throw new AppError("interactive_context_invalid", "Interactive objective cannot be replaced", 2, { effect: "no_effect" });
54
+ return publishMemoryContexts(bundle, objective, directory, expectedSha256, expectedOwner, "interactive");
55
+ }
56
+ export function publishMemoryFixContexts(bundle, objective, directory, expectedSha256, expectedOwner) {
57
+ const original = prepareMemoryFixContexts(bundle, directory);
58
+ if (objective !== original.objective)
59
+ throw new AppError("memory_fix_context_invalid", "Repair objective cannot be replaced", 2, { effect: "no_effect" });
60
+ return publishMemoryContexts(bundle, objective, directory, expectedSha256, expectedOwner, "mb-fix");
61
+ }
12
62
  export function prepareDiagnosticContextsFromRequest(context, bundle, directory) {
13
63
  const scope = bundle.execution.snapshot().scope, flowId = bundle.manifest.flow_id;
14
- if (!isDiagnosticFlow(flowId))
15
- throw new AppError("diagnostic_context_invalid", "Diagnostic bundle required", 2, { effect: "no_effect" });
64
+ if (!isSourceSnapshotFlow(flowId))
65
+ throw new AppError("diagnostic_context_invalid", "Source-snapshot bundle required", 2, { effect: "no_effect" });
16
66
  const request = readDiagnosticRequest(context, { projectRoot: scope.projectRoot, requestId: scope.requestId, flowId });
17
67
  if (!request || request.execution_sha256 !== bundle.execution.sha256)
18
68
  throw new AppError("diagnostic_context_invalid", "Original diagnostic objective required", 2, { effect: "no_effect" });
19
69
  return { ...prepareMemoryContexts(bundle, request.objective, directory, flowId), objective: request.objective };
20
70
  }
21
71
  export function publishDiagnosticContexts(bundle, objective, directory, expectedSha256, expectedOwner) {
22
- if (!isDiagnosticFlow(bundle.manifest.flow_id))
23
- throw new AppError("diagnostic_context_invalid", "Diagnostic bundle required", 2, { effect: "no_effect" });
72
+ if (!isSourceSnapshotFlow(bundle.manifest.flow_id))
73
+ throw new AppError("diagnostic_context_invalid", "Source-snapshot bundle required", 2, { effect: "no_effect" });
24
74
  return publishMemoryContexts(bundle, objective, directory, expectedSha256, expectedOwner, bundle.manifest.flow_id);
25
75
  }
26
76
  /** Original upgrade intake comes from the independently retained storage
@@ -11,9 +11,16 @@ import { requireRun } from "./run-controller-state.js";
11
11
  import { prepareRunControllerLaunch } from "./run-controller.js";
12
12
  import { prepareMemoryInitContextsFromStartup, publishMemoryInitContexts, prepareMemoryUpgradeContextsFromStartup, publishMemoryUpgradeContexts } from "./memory-init-contexts.js";
13
13
  import { parseRuntimeBudget } from "./runtime-budget.js";
14
- import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
14
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
15
15
  import { startDiagnosticRun } from "./diagnostic-start.js";
16
16
  import { prepareDiagnosticContextsFromRequest, publishDiagnosticContexts } from "./memory-init-contexts.js";
17
+ import { prepareMemoryFixContexts, publishMemoryFixContexts } from "./memory-init-contexts.js";
18
+ import { startMemoryFixRun } from "./memory-fix-request.js";
19
+ import { startInteractiveRun } from "./interactive-request.js";
20
+ import { prepareInteractiveContexts, publishInteractiveContexts } from "./memory-init-contexts.js";
21
+ import { prepareDeliveryContexts, publishDeliveryContexts } from "./memory-init-contexts.js";
22
+ import { isDeliveryFlow } from "../domain/delivery-flows.js";
23
+ import { startDeliveryRun } from "./delivery-request.js";
17
24
  function refuse(reason) {
18
25
  throw new AppError("memory_init_controller_start_invalid", "Init controller requires its exact published startup request", 1, { reason, effect: "no_effect", next_action: "reconcile_exact_request" });
19
26
  }
@@ -30,6 +37,15 @@ export function prepareMemoryUpgradeControllerStart(context, scope) {
30
37
  export function prepareDiagnosticControllerStart(context, scope, flowId) {
31
38
  return prepareMemoryControllerStart(context, scope, flowId);
32
39
  }
40
+ export function prepareMemoryFixControllerStart(context, scope) {
41
+ return prepareMemoryControllerStart(context, scope, "mb-fix");
42
+ }
43
+ export function prepareInteractiveControllerStart(context, scope) {
44
+ return prepareMemoryControllerStart(context, scope, "interactive");
45
+ }
46
+ export function prepareDeliveryControllerStart(context, scope, flowId) {
47
+ return prepareMemoryControllerStart(context, scope, flowId);
48
+ }
33
49
  function prepareMemoryControllerStart(context, scope, flowId) {
34
50
  validateMemoryInitStartupScope(scope);
35
51
  if (context.db.isOwnedWriteTransaction?.())
@@ -47,7 +63,7 @@ function prepareMemoryControllerStart(context, scope, flowId) {
47
63
  refuse("published_run_required");
48
64
  // Restores the exact request and verifies independent SQL/physical root
49
65
  // ownership without consulting current canon/profiles or creating a RUN.
50
- const started = isDiagnosticFlow(flowId) ? startDiagnosticRun(context, { projectRoot: scope.projectRoot, requestId: scope.requestId, flowId })
66
+ const started = isDeliveryFlow(flowId) ? startDeliveryRun(context, { projectRoot: scope.projectRoot, requestId: scope.requestId, flowId }) : flowId === "interactive" ? startInteractiveRun(context, scope) : flowId === "mb-fix" ? startMemoryFixRun(context, scope) : isSourceSnapshotFlow(flowId) ? startDiagnosticRun(context, { projectRoot: scope.projectRoot, requestId: scope.requestId, flowId })
51
67
  : flowId === "mb-init" ? startMemoryInitRun(context, scope) : startMemoryUpgradeRun(context, scope);
52
68
  const run = requireRun(context, project.id, started.runId);
53
69
  const bundle = readFlowRunExecutionBundle(context, { projectRoot: scope.projectRoot, runId: run.id });
@@ -55,7 +71,7 @@ function prepareMemoryControllerStart(context, scope, flowId) {
55
71
  refuse("retained_bundle_required");
56
72
  if (bundle.execution.settings.execution !== "managed")
57
73
  refuse("managed_execution_required");
58
- const contexts = isDiagnosticFlow(flowId) ? prepareDiagnosticContextsFromRequest(context, bundle, run.run_root)
74
+ const contexts = isDeliveryFlow(flowId) ? prepareDeliveryContexts(bundle, run.run_root) : flowId === "interactive" ? prepareInteractiveContexts(bundle, run.run_root) : flowId === "mb-fix" ? prepareMemoryFixContexts(bundle, run.run_root) : isSourceSnapshotFlow(flowId) ? prepareDiagnosticContextsFromRequest(context, bundle, run.run_root)
59
75
  : flowId === "mb-init" ? prepareMemoryInitContextsFromStartup(context, bundle, run.run_root)
60
76
  : prepareMemoryUpgradeContextsFromStartup(context, bundle, run.run_root);
61
77
  const input = { projectRoot: scope.projectRoot, runId: run.id, requestId: scope.requestId,
@@ -91,7 +107,7 @@ function prepareMemoryControllerStart(context, scope, flowId) {
91
107
  });
92
108
  if (!rootOwner || !Number.isSafeInteger(rootOwner.dev) || !Number.isSafeInteger(rootOwner.ino))
93
109
  refuse("root_owner_changed");
94
- const publish = isDiagnosticFlow(flowId) ? publishDiagnosticContexts : flowId === "mb-init" ? publishMemoryInitContexts : publishMemoryUpgradeContexts;
110
+ const publish = isDeliveryFlow(flowId) ? publishDeliveryContexts : flowId === "interactive" ? publishInteractiveContexts : flowId === "mb-fix" ? publishMemoryFixContexts : isSourceSnapshotFlow(flowId) ? publishDiagnosticContexts : flowId === "mb-init" ? publishMemoryInitContexts : publishMemoryUpgradeContexts;
95
111
  publish(bundle, contexts.objective, run.run_root, contexts.sha256, rootOwner);
96
112
  try {
97
113
  return { input, prepared: prepareRunControllerLaunch(context, input) };
@@ -12,12 +12,14 @@ import { inspectReleaseArchive } from "./release-archive.js";
12
12
  import { canonicalOnlyFlowNames } from "./version-status.js";
13
13
  export function isCanonicalOnlyMemoryPath(logical) {
14
14
  return logical.startsWith("dd-flow/project-review/")
15
+ || logical.startsWith("dd-flow/delivery/")
16
+ || logical === "dd-flow/mb-sdlc/review-fix.md"
15
17
  || canonicalOnlyFlowNames.some(name => logical === `dd-flow/${name}` || logical.startsWith(`dd-flow/${name.slice(0, -3)}/`));
16
18
  }
17
19
  const rootFiles = ["README.md", "index.md", "compatibility.json", "flow-contract.json", "go.md", "f.md", "prime.md", "protocol.md",
18
20
  "protocol-implement.md", "plan.md", "code.md", "merge.md", "merge-start.md", "merge-stop.md", "release.md", "deploy.md", "publish.md",
19
- "review-fix.md", "mb-fix.md", "mb-lint.md", "interactive.md", "finish.md", "fix-def.md"];
20
- const supportDirectories = ["common", "prime", "mb-sdlc", "aspects", "def", "workers", "schemas", "vnext", "specify", "stage-reports", "dashboard", "plan", "code", "merge"];
21
+ "mb-lint.md", "interactive.md", "finish.md", "fix-def.md"];
22
+ const supportDirectories = ["common", "prime", "mb-sdlc", "aspects", "def", "workers", "schemas", "vnext", "specify", "stage-reports", "dashboard", "plan", "code", "merge", "interactive"];
21
23
  const required = ["README.md", "index.md", "flow-contract.json", "compatibility.json", "prime.md", "go.md",
22
24
  "common/style.md", "common/runtime-contract.md", "common/lifecycle-guards.md", "common/workspace-bootstrap.md", "common/worker-session.md",
23
25
  "vnext/start.md", "vnext/specify.md", "vnext/protocolize.md", "vnext/plan.md", "vnext/plan-review.md", "vnext/code.md", "vnext/code-review.md", "vnext/merge.md",
@@ -77,6 +79,8 @@ export class PreparedMemoryLayer {
77
79
  directory(source);
78
80
  for (const name of fs.readdirSync(source).sort()) {
79
81
  const file = path.join(source, name), target = `${logical}/${name}`, stat = fs.lstatSync(file);
82
+ if (isCanonicalOnlyMemoryPath(target))
83
+ continue;
80
84
  if (stat.isDirectory())
81
85
  collect(file, target);
82
86
  else if (stat.isFile())
@@ -91,7 +95,7 @@ export class PreparedMemoryLayer {
91
95
  const flowRoot = path.join(memoryRoot, "dd-flow");
92
96
  directory(flowRoot);
93
97
  for (const name of rootFiles)
94
- if (fs.existsSync(path.join(flowRoot, name)))
98
+ if (!isCanonicalOnlyMemoryPath(`dd-flow/${name}`) && fs.existsSync(path.join(flowRoot, name)))
95
99
  put(`dd-flow/${name}`, readBoundedReleaseFile(path.join(flowRoot, name), 1024 * 1024));
96
100
  for (const name of supportDirectories)
97
101
  if (fs.existsSync(path.join(flowRoot, name)))
@@ -8,10 +8,29 @@ import { assertRunMutationAllowed } from "./run-recovery-guard.js";
8
8
  import { reviewSourceFilter, reviewSourceHash, reviewExecutionCopyHash } from "./review-copy.js";
9
9
  import { writeReviewGitEvidence } from "./external-work-launch.js";
10
10
  import { memoryReviewPhysicalDestination } from "./hook-memory-routing.js";
11
+ import { assertSharedReviewInput } from "./work-registry.js";
12
+ import { readFlowRunExecutionBundle } from "./runs.js";
13
+ import { PreparedInteractiveWorkStart } from "./interactive-work-start.js";
11
14
  function refuse(reason) { throw new AppError("memory_native_review_copy_invalid", "Native Memory review requires an independently owned immutable execution copy", 1, { reason, effect: "no_effect" }); }
12
15
  export { memoryNativeReviewAssignedHookCwd } from "./hook-memory-routing.js";
16
+ function prepareInteractiveReviewOwner(context, work, run, generation) {
17
+ const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: work.run_id });
18
+ if (bundle?.manifest.flow_id !== "interactive" || JSON.parse(work.payload_json ?? "null")?.kind !== "interactive-review")
19
+ refuse("registered_interactive_review_required");
20
+ const input = assertSharedReviewInput(context, work, { ...run, id: work.run_id }, generation);
21
+ if (!input)
22
+ refuse("registered_interactive_review_required");
23
+ const start = PreparedInteractiveWorkStart.prepare(context, { projectId: work.project_id, runId: work.run_id, workId: work.work_id, projectRoot: run.project_root });
24
+ return { bundle, input, assertCustody: (writer) => start.assertCustody(writer),
25
+ revalidate: (reader) => {
26
+ start.revalidate();
27
+ if (hybridDigest(assertSharedReviewInput(reader, work, { ...run, id: work.run_id }, generation)) !== hybridDigest(input))
28
+ refuse("review_input_changed");
29
+ } };
30
+ }
13
31
  /** Existing source/copy policy, separate native physical custody: no external
14
- * launch row is invented. An interrupted allocation is never overwritten. */
32
+ * launch row is invented. An interrupted allocation is never overwritten.
33
+ * Interactive review reuses this owner and its historical copy event format. */
15
34
  export function prepareMemoryNativeReviewCopy(context, workId, allocate = false, preparedOwner) {
16
35
  const work = context.db.get("SELECT * FROM works WHERE work_id=?", [workId]);
17
36
  if (!work)
@@ -20,8 +39,13 @@ export function prepareMemoryNativeReviewCopy(context, workId, allocate = false,
20
39
  if (!run)
21
40
  refuse("owned_run_required");
22
41
  const generation = assertRunMutationAllowed(context, work.project_id, work.run_id);
23
- const owner = preparedOwner ?? prepareMemoryReviewWorkInput(context, work, run, generation);
24
- assertPreparedMemoryReviewWorkInput(context, owner, work, run, generation);
42
+ const interactive = work.result_schema === "dd-flow/interactive-work-result@1";
43
+ if (interactive && preparedOwner)
44
+ refuse("foreign_prepared_review_input");
45
+ const memoryOwner = interactive ? null : preparedOwner ?? prepareMemoryReviewWorkInput(context, work, run, generation);
46
+ if (memoryOwner)
47
+ assertPreparedMemoryReviewWorkInput(context, memoryOwner, work, run, generation);
48
+ const owner = memoryOwner ?? prepareInteractiveReviewOwner(context, work, run, generation);
25
49
  const route = owner.bundle.execution.work("review");
26
50
  if (route.delegation.mode !== "native")
27
51
  refuse("native_route_required");
@@ -20,6 +20,7 @@ function ordered(entries) {
20
20
  return left.length - right.length;
21
21
  });
22
22
  }
23
+ export { ordered as orderMemoryDocumentEntries };
23
24
  function refuse(reason) {
24
25
  throw new AppError("memory_repair_document_delta_invalid", "Physical repair documents differ from their bounded baseline", 2, { reason, effect: "no_effect" });
25
26
  }