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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +273 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-execution.js +4 -1
  84. package/dist/services/controller-fanout.js +9 -4
  85. package/dist/services/controller-memory-worker.js +6 -4
  86. package/dist/services/controller-result-authority.js +122 -0
  87. package/dist/services/dashboard.js +4 -2
  88. package/dist/services/delivery-access.js +123 -0
  89. package/dist/services/delivery-approval.js +68 -0
  90. package/dist/services/delivery-artifact.js +86 -0
  91. package/dist/services/delivery-checks.js +178 -0
  92. package/dist/services/delivery-execution.js +181 -0
  93. package/dist/services/delivery-intents.js +212 -0
  94. package/dist/services/delivery-operation.js +81 -0
  95. package/dist/services/delivery-policy.js +91 -0
  96. package/dist/services/delivery-procedures.js +43 -0
  97. package/dist/services/delivery-request.js +99 -0
  98. package/dist/services/delivery-result.js +144 -0
  99. package/dist/services/delivery-stage-commands.js +26 -0
  100. package/dist/services/delivery-stage-entry.js +56 -0
  101. package/dist/services/delivery-stage-handlers.js +135 -0
  102. package/dist/services/delivery-stage-results.js +94 -0
  103. package/dist/services/descriptor-coordinator-session.js +3 -1
  104. package/dist/services/descriptor-run-start.js +24 -4
  105. package/dist/services/descriptor-stage-coordinator.js +78 -10
  106. package/dist/services/descriptor-stage-settlement.js +59 -14
  107. package/dist/services/diagnostic-repair-intake.js +123 -0
  108. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  109. package/dist/services/diagnostic-report.js +30 -0
  110. package/dist/services/diagnostic-run-bundle.js +48 -9
  111. package/dist/services/diagnostic-snapshot.js +2 -2
  112. package/dist/services/diagnostic-stage-handlers.js +3 -2
  113. package/dist/services/diagnostic-stage-results.js +26 -0
  114. package/dist/services/diagnostic-start.js +56 -6
  115. package/dist/services/diagnostic-work-collection.js +3 -3
  116. package/dist/services/distill-document-publication.js +280 -0
  117. package/dist/services/distill-promotion.js +197 -0
  118. package/dist/services/distill-publication-custody.js +162 -0
  119. package/dist/services/distill-stage-handlers.js +249 -0
  120. package/dist/services/distill-stage-results.js +105 -0
  121. package/dist/services/distill-user-inputs.js +4 -0
  122. package/dist/services/distill-work-batch.js +42 -0
  123. package/dist/services/distill-work-collection.js +74 -0
  124. package/dist/services/distill-work-registration.js +93 -0
  125. package/dist/services/distill-work-results.js +98 -0
  126. package/dist/services/eval-snapshots.js +17 -7
  127. package/dist/services/external-work-launch.js +8 -2
  128. package/dist/services/flow-catalog.js +24 -0
  129. package/dist/services/frozen-engine.js +19 -2
  130. package/dist/services/generic-execution.js +4 -3
  131. package/dist/services/harness-adapter.js +2 -14
  132. package/dist/services/hook-memory-routing.js +1 -1
  133. package/dist/services/hooks.js +31 -6
  134. package/dist/services/hybrid-contracts.js +6 -1
  135. package/dist/services/installed-flow-source.js +37 -2
  136. package/dist/services/interactive-attached-session.js +215 -0
  137. package/dist/services/interactive-baseline-custody.js +54 -0
  138. package/dist/services/interactive-before-versions.js +148 -0
  139. package/dist/services/interactive-bundle-data.js +85 -0
  140. package/dist/services/interactive-checks.js +179 -0
  141. package/dist/services/interactive-execution.js +33 -0
  142. package/dist/services/interactive-finish-integration.js +236 -0
  143. package/dist/services/interactive-git-baseline.js +230 -0
  144. package/dist/services/interactive-git-dispatch.js +278 -0
  145. package/dist/services/interactive-input-delivery.js +82 -0
  146. package/dist/services/interactive-mutation-scope.js +231 -0
  147. package/dist/services/interactive-request.js +287 -0
  148. package/dist/services/interactive-run-bundle.js +109 -0
  149. package/dist/services/interactive-stage-entry.js +41 -0
  150. package/dist/services/interactive-stage-handlers.js +289 -0
  151. package/dist/services/interactive-stage-observation.js +136 -0
  152. package/dist/services/interactive-stage-results.js +111 -0
  153. package/dist/services/interactive-user-inputs.js +142 -0
  154. package/dist/services/interactive-work-batch.js +43 -0
  155. package/dist/services/interactive-work-collection.js +119 -0
  156. package/dist/services/interactive-work-completion.js +180 -0
  157. package/dist/services/interactive-work-context.js +249 -0
  158. package/dist/services/interactive-work-history.js +112 -0
  159. package/dist/services/interactive-work-registration.js +124 -0
  160. package/dist/services/interactive-work-results.js +115 -0
  161. package/dist/services/interactive-work-start.js +155 -0
  162. package/dist/services/lifecycle-invocations.js +214 -24
  163. package/dist/services/managed-daemon-binding.js +52 -1
  164. package/dist/services/memory-compose-handler.js +2 -22
  165. package/dist/services/memory-finish-integration.js +58 -17
  166. package/dist/services/memory-fix-finish-integration.js +129 -0
  167. package/dist/services/memory-fix-git-dispatch.js +139 -0
  168. package/dist/services/memory-fix-intake-custody.js +11 -0
  169. package/dist/services/memory-fix-intake-settlement.js +127 -0
  170. package/dist/services/memory-fix-publication.js +196 -0
  171. package/dist/services/memory-fix-repair-admission.js +116 -0
  172. package/dist/services/memory-fix-request.js +141 -0
  173. package/dist/services/memory-fix-run-bundle.js +83 -0
  174. package/dist/services/memory-fix-scope.js +78 -0
  175. package/dist/services/memory-fix-sources.js +85 -0
  176. package/dist/services/memory-fix-stage-entry.js +24 -0
  177. package/dist/services/memory-fix-stage-handlers.js +282 -0
  178. package/dist/services/memory-fix-stage-results.js +106 -0
  179. package/dist/services/memory-fix-workspace.js +127 -0
  180. package/dist/services/memory-flow-user-inputs.js +88 -0
  181. package/dist/services/memory-init-contexts.js +55 -5
  182. package/dist/services/memory-init-controller-start.js +20 -4
  183. package/dist/services/memory-layer.js +7 -3
  184. package/dist/services/memory-native-review-copy.js +27 -3
  185. package/dist/services/memory-repair-document-delta.js +1 -0
  186. package/dist/services/project-flow-source.js +195 -0
  187. package/dist/services/project-git-integration.js +92 -0
  188. package/dist/services/repair-intents.js +2 -7
  189. package/dist/services/retained-native-hook.js +245 -0
  190. package/dist/services/review-fix-handoff.js +82 -0
  191. package/dist/services/review-fix-intake-custody.js +17 -0
  192. package/dist/services/review-fix-stage-entry.js +43 -0
  193. package/dist/services/review-fix-stage-handlers.js +176 -0
  194. package/dist/services/review-fix-stage-results.js +86 -0
  195. package/dist/services/run-control-worker.js +42 -1
  196. package/dist/services/run-control.js +40 -5
  197. package/dist/services/run-controller-adapter.js +37 -3
  198. package/dist/services/run-controller-process.js +12 -4
  199. package/dist/services/run-controller-recovery.js +46 -6
  200. package/dist/services/run-controller-state.js +3 -2
  201. package/dist/services/run-controller.js +209 -43
  202. package/dist/services/run-recovery.js +31 -2
  203. package/dist/services/run-timeline.js +59 -0
  204. package/dist/services/runs.js +18 -31
  205. package/dist/services/sessions.js +2 -1
  206. package/dist/services/stage-handlers.js +55 -3
  207. package/dist/services/stage-lifecycle.js +4 -3
  208. package/dist/services/stage-pause.js +26 -9
  209. package/dist/services/stage-report-renderer.js +2 -6
  210. package/dist/services/technical-notes.js +31 -0
  211. package/dist/services/verification-matrix.js +1 -1
  212. package/dist/services/version-status.js +1 -1
  213. package/dist/services/vnext-code-review.js +74 -5
  214. package/dist/services/vnext-code.js +8 -32
  215. package/dist/services/vnext-fanout.js +53 -8
  216. package/dist/services/vnext-merge.js +10 -5
  217. package/dist/services/vnext-plan-review.js +1 -1
  218. package/dist/services/vnext-plan.js +1 -1
  219. package/dist/services/vnext-protocolize.js +1 -1
  220. package/dist/services/vnext-specify.js +10 -5
  221. package/dist/services/work-registry.js +252 -66
  222. package/dist/services/worktrees.js +7 -4
  223. package/dist/storage/database.js +2 -1
  224. package/dist/verification-components.json +82 -0
  225. package/package.json +3 -2
  226. package/tools/verification-jobs.mjs +35 -10
@@ -0,0 +1,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
  }
@@ -0,0 +1,195 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { Ajv } from "ajv/dist/ajv.js";
5
+ import semver from "semver";
6
+ import { AppError } from "../shared/errors.js";
7
+ import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
8
+ import { readBoundedReleaseFile } from "./release-source.js";
9
+ import { isCanonicalOnlyMemoryPath } from "./memory-layer.js";
10
+ import { localEnvironmentAllowed } from "./workspace-files.js";
11
+ import { admitBundledFlow } from "./stage-handlers.js";
12
+ import { normalizeFlowContract } from "../domain/flow-contract.js";
13
+ const limit = 1024 * 1024, manifestPath = "project-pack/manifest.json";
14
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
15
+ function refuse(reason) {
16
+ throw new AppError("project_flow_source_invalid", "Project-local flow requires its own valid curated pack; use the supported pack upgrade/repair route", 2, { reason, phase: "prepare", effect: "no_effect", next_action: "inspect_project_pack_and_upgrade_or_repair" });
17
+ }
18
+ function safe(name) {
19
+ return typeof name === "string" && /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(name)
20
+ && name.split("/").every(part => ![".", "..", ".git", "node_modules", ".dd-flow"].includes(part.toLowerCase()) && localEnvironmentAllowed(part));
21
+ }
22
+ function schema(name) { return JSON.parse(fs.readFileSync(new URL(`../schemas/${name}.schema.json`, import.meta.url), "utf8")); }
23
+ function decoded(bytes) { try {
24
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
25
+ }
26
+ catch {
27
+ return refuse("invalid_pack_json");
28
+ } }
29
+ function validate(scope, blobs) {
30
+ if (!scope || Object.keys(scope).sort().join() !== "flowId,projectRoot,workspace" || scope.flowId !== "interactive"
31
+ || [scope.projectRoot, scope.workspace].some(value => typeof value !== "string" || !path.isAbsolute(value)))
32
+ refuse("invalid_project_scope");
33
+ const manifest = decoded(blobs.get(manifestPath) ?? Buffer.alloc(0));
34
+ if (!new Ajv({ strict: false, validateFormats: false }).compile(schema("project-flow-pack-manifest"))(manifest)
35
+ || manifest.schema_id !== "dd-flow/project-flow-pack-manifest@2" || !/^[a-f0-9]{40}$/.test(manifest.source_commit)
36
+ || !semver.valid(manifest.canon_version_at_source_commit))
37
+ refuse("invalid_or_legacy_project_manifest");
38
+ if (manifest.included_files.length > 20_000 || manifest.included_files.some(name => !safe(name) || name === manifestPath
39
+ || name.startsWith("project-pack/") || isCanonicalOnlyMemoryPath(`dd-flow/${name}`)))
40
+ refuse("invalid_curated_inventory");
41
+ const names = [manifestPath, ...manifest.included_files], folded = new Set();
42
+ let total = 0;
43
+ if (blobs.size !== names.length)
44
+ refuse("pack_inventory_changed");
45
+ for (const name of names) {
46
+ if (folded.has(name.toLowerCase()))
47
+ refuse("aliased_pack_path");
48
+ folded.add(name.toLowerCase());
49
+ const bytes = blobs.get(name);
50
+ if (!bytes?.length || bytes.length > limit || (total += bytes.length) > 32 * limit)
51
+ refuse("missing_or_unbounded_pack_file");
52
+ if (name !== manifestPath && manifest.file_hashes?.[name] !== undefined && manifest.file_hashes[name] !== hash(bytes))
53
+ refuse("project_pack_file_changed");
54
+ }
55
+ const descriptorPath = `${scope.flowId}/flow-descriptor.json`, descriptor = decoded(blobs.get(descriptorPath) ?? Buffer.alloc(0));
56
+ if (validateHybridShape("flow-descriptor", descriptor).length || descriptor.id !== scope.flowId || descriptor.origin !== "project")
57
+ refuse("project_descriptor_required");
58
+ for (const stage of descriptor.stages)
59
+ for (const kind of ["instruction", "result_schema"]) {
60
+ const ref = stage[kind], bytes = blobs.get(ref.path);
61
+ if (!bytes || ref.path === manifestPath || !manifest.included_files.includes(ref.path) || hash(bytes) !== ref.sha256)
62
+ refuse("stage_source_not_in_project_pack");
63
+ try {
64
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
65
+ }
66
+ catch {
67
+ refuse("invalid_stage_source_encoding");
68
+ }
69
+ if (kind === "result_schema")
70
+ decoded(bytes);
71
+ }
72
+ const compatibility = decoded(blobs.get("compatibility.json") ?? Buffer.alloc(0));
73
+ if (!new Ajv({ strict: false, validateFormats: false }).compile(schema("compatibility"))(compatibility))
74
+ refuse("invalid_project_compatibility");
75
+ const contract = normalizeFlowContract(decoded(blobs.get("flow-contract.json") ?? Buffer.alloc(0)));
76
+ if (contract.id !== manifest.flow_contract || compatibility.dd_flow_cli.flow_contract !== contract.id
77
+ || compatibility.memory_bank_version !== manifest.canon_version_at_source_commit)
78
+ refuse("project_contract_mismatch");
79
+ return { manifest, descriptorPath, compatibility };
80
+ }
81
+ /** Project pack data, not engine/handler admission. Manifest canon paths are
82
+ * provenance hints only: never follow them or fall back to global instructions. */
83
+ export class PreparedProjectFlowSource {
84
+ sha256;
85
+ #scope;
86
+ #blobs;
87
+ #facts;
88
+ #fresh;
89
+ constructor(scope, blobs, fresh) {
90
+ this.#facts = validate(scope, blobs);
91
+ this.#scope = structuredClone(scope);
92
+ this.#blobs = new Map([...blobs].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, bytes]) => [name, Buffer.from(bytes)]));
93
+ this.sha256 = hybridDigest({ scope, inventory: [...this.#blobs].map(([name, bytes]) => ({ path: name, sha256: hash(bytes) })) });
94
+ this.#fresh = fresh;
95
+ Object.freeze(this);
96
+ }
97
+ static fromProject(scope) {
98
+ try {
99
+ for (const root of [scope.projectRoot, scope.workspace])
100
+ if (fs.realpathSync(root) !== root || !fs.lstatSync(root).isDirectory())
101
+ refuse("noncanonical_project_root");
102
+ let root = scope.workspace;
103
+ for (const name of [".memory-bank", "dd-flow"]) {
104
+ root = path.join(root, name);
105
+ if (!fs.lstatSync(root).isDirectory())
106
+ refuse("linked_project_pack");
107
+ }
108
+ const read = (logical) => {
109
+ if (!safe(logical))
110
+ refuse("unsafe_pack_path");
111
+ let parent = root;
112
+ for (const part of logical.split("/").slice(0, -1)) {
113
+ parent = path.join(parent, part);
114
+ if (!fs.lstatSync(parent).isDirectory())
115
+ refuse("linked_project_pack");
116
+ }
117
+ return readBoundedReleaseFile(path.join(root, logical), limit);
118
+ };
119
+ const raw = read("manifest.json"), manifest = decoded(raw);
120
+ if (!new Ajv({ strict: false, validateFormats: false }).compile(schema("project-flow-pack-manifest"))(manifest)
121
+ || manifest.schema_id !== "dd-flow/project-flow-pack-manifest@2")
122
+ refuse("invalid_or_legacy_project_manifest");
123
+ const blobs = new Map([[manifestPath, raw]]);
124
+ if (manifest.included_files.length > 20_000)
125
+ refuse("unbounded_project_pack");
126
+ let total = raw.length;
127
+ for (const name of manifest.included_files) {
128
+ if (!safe(name) || isCanonicalOnlyMemoryPath(`dd-flow/${name}`) || name.startsWith("project-pack/"))
129
+ refuse("invalid_curated_inventory");
130
+ const bytes = read(name);
131
+ total += bytes.length;
132
+ if (total > 32 * limit)
133
+ refuse("unbounded_project_pack");
134
+ blobs.set(name, bytes);
135
+ }
136
+ if (!read("manifest.json").equals(raw))
137
+ refuse("manifest_changed_during_capture");
138
+ return new PreparedProjectFlowSource(scope, blobs, true);
139
+ }
140
+ catch (error) {
141
+ if (error instanceof AppError && error.code === "project_flow_source_invalid")
142
+ throw error;
143
+ return refuse("project_pack_unavailable_or_changed");
144
+ }
145
+ }
146
+ snapshot() { return { schema_id: "dd-flow/project-flow-source@1", scope: structuredClone(this.#scope), blobs: [...this.#blobs].map(([name, bytes]) => ({ path: name, base64: bytes.toString("base64") })) }; }
147
+ /** Data restoration only. expectedSha256 must come from independent request/
148
+ * RUN custody. Engine/handler admission is separate and never inferred here. */
149
+ static restoreRetained(value, expectedSha256) {
150
+ if (!value || Object.keys(value).sort().join() !== "blobs,schema_id,scope" || value.schema_id !== "dd-flow/project-flow-source@1"
151
+ || !Array.isArray(value.blobs) || value.blobs.length > 20_001)
152
+ refuse("invalid_retained_project_source");
153
+ const blobs = new Map();
154
+ let total = 0;
155
+ for (const item of value.blobs) {
156
+ if (!item || Object.keys(item).sort().join() !== "base64,path" || !safe(item.path) || typeof item.base64 !== "string" || item.base64.length > 2 * limit || blobs.has(item.path))
157
+ refuse("invalid_retained_project_source");
158
+ const bytes = Buffer.from(item.base64, "base64");
159
+ total += bytes.length;
160
+ if (!bytes.length || bytes.length > limit || total > 32 * limit || bytes.toString("base64") !== item.base64)
161
+ refuse("invalid_retained_project_source");
162
+ blobs.set(item.path, bytes);
163
+ }
164
+ const result = new PreparedProjectFlowSource(value.scope, blobs, false);
165
+ if (result.sha256 !== expectedSha256)
166
+ refuse("retained_project_source_changed");
167
+ return result;
168
+ }
169
+ blobs() { return new Map([...this.#blobs].map(([name, bytes]) => [name, Buffer.from(bytes)])); }
170
+ scope() { return structuredClone(this.#scope); }
171
+ descriptorReference() { const name = this.#facts.descriptorPath; return { path: name, sha256: hash(this.#blobs.get(name)) }; }
172
+ projectPackProvenance() { return { sha256: hybridDigest([...this.#blobs].map(([name, bytes]) => ({ path: name === manifestPath ? "dd-flow/manifest.json" : `dd-flow/${name}`, sha256: hash(bytes) })).sort((a, b) => a.path.localeCompare(b.path, "en"))), source_commit: this.#facts.manifest.source_commit }; }
173
+ canonSource() { return { version: this.#facts.manifest.canon_version_at_source_commit, commit: this.#facts.manifest.source_commit }; }
174
+ /** Declared project base, not a claim that customized instructions are bytes
175
+ * from an installed canonical archive. Actual data identity is the pack hash. */
176
+ canonProvenance() { return { ...this.canonSource(), archive_sha256: null }; }
177
+ resolveRegisteredFlow() { const ref = this.descriptorReference(); return admitBundledFlow(this.#blobs.get(ref.path), ref.sha256, this.#blobs); }
178
+ assertEngineCompatibility(engine) {
179
+ const value = this.#facts.compatibility;
180
+ if (engine.package_name !== value.engine.package_name || engine.package_name !== value.dd_flow_cli.package_name
181
+ || !semver.valid(engine.package_version) || !semver.valid(engine.engine_version)
182
+ || !semver.validRange(value.engine.version_range) || !semver.valid(value.dd_flow_cli.min_version)
183
+ || !semver.satisfies(engine.engine_version, value.engine.version_range, { includePrerelease: true })
184
+ || !semver.gte(engine.package_version, value.dd_flow_cli.min_version))
185
+ refuse("project_pack_engine_incompatible");
186
+ }
187
+ revalidate() {
188
+ if (!this.#fresh) {
189
+ validate(this.#scope, this.#blobs);
190
+ return;
191
+ }
192
+ if (PreparedProjectFlowSource.fromProject(this.#scope).sha256 !== this.sha256)
193
+ refuse("project_source_changed_before_retention");
194
+ }
195
+ }
@@ -0,0 +1,92 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { hybridDigest } from "./hybrid-contracts.js";
7
+ import { interactiveWorkPathAllowed } from "./interactive-work-batch.js";
8
+ import { ObservedMemorySources } from "./memory-source-observations.js";
9
+ import { memorySourceKey } from "./memory-stage-results.js";
10
+ import { assertMemoryGitRemovalSources } from "./memory-git-integration.js";
11
+ function refuse(reason) {
12
+ throw new AppError("project_git_integration_invalid", "Reviewed project files lack exact local Git integration", 2, { reason, effect: "no_effect" });
13
+ }
14
+ export function assertReviewedProjectGitInventory(references, removals) {
15
+ const valid = (ref) => ref?.root === "workspace" && interactiveWorkPathAllowed(ref.path)
16
+ && /^[a-f0-9]{64}$/.test(ref.sha256) && (Object.keys(ref).sort().join() === "path,root,sha256"
17
+ || Object.keys(ref).sort().join() === "git_mode,path,root,sha256" && ["100644", "100755"].includes(ref.git_mode));
18
+ if (!Array.isArray(references) || !Array.isArray(removals) || !references.length && !removals.length
19
+ || references.length + removals.length > 4096 || [...references, ...removals].some(ref => !valid(ref))
20
+ || new Set([...references, ...removals].map(ref => ref.path)).size !== references.length + removals.length)
21
+ refuse("invalid_reviewed_project_inventory");
22
+ }
23
+ /** Physical selected-file readback only, not review, policy or commit authority.
24
+ * Unrelated dirty files/index entries are deliberately neither committed nor
25
+ * treated as failures. The terminal owner must admit the original dirty input. */
26
+ export function observeReviewedProjectGitIntegration(input, environment, references, removals = []) {
27
+ assertReviewedProjectGitInventory(references, removals);
28
+ for (const root of [input.projectRoot, input.workspaceRoot]) {
29
+ if (!path.isAbsolute(root) || fs.realpathSync(root) !== root || !fs.lstatSync(root).isDirectory())
30
+ refuse("canonical_repository_root_required");
31
+ }
32
+ if (!path.isAbsolute(input.gitExecutable) || typeof input.integrationBranch !== "string" || !input.integrationBranch)
33
+ refuse("explicit_git_selection_required");
34
+ const env = { ...Object.fromEntries(Object.entries(environment).filter(([key]) => !key.startsWith("GIT_"))), GIT_OPTIONAL_LOCKS: "0", GIT_LITERAL_PATHSPECS: "1" };
35
+ const git = (root, args) => {
36
+ const result = spawnSync(input.gitExecutable, ["-c", "core.fsmonitor=false", "-C", root, ...args], { env, timeout: 10000, maxBuffer: 16 * 1024 * 1024, shell: false, windowsHide: true });
37
+ if (result.error || result.signal || result.status !== 0)
38
+ refuse("git_readback_failed");
39
+ return result.stdout;
40
+ };
41
+ const text = (root, args) => git(root, args).toString("utf8").trim();
42
+ const source = input.workspaceRoot, target = input.projectRoot, integrationRef = `refs/heads/${input.integrationBranch}`;
43
+ git(target, ["check-ref-format", integrationRef]);
44
+ const common = (root) => fs.realpathSync(path.resolve(root, text(root, ["rev-parse", "--git-common-dir"])));
45
+ const repository = common(source);
46
+ if (common(target) !== repository || [source, target].some(root => text(root, ["rev-parse", "--show-toplevel"]) !== root))
47
+ refuse("foreign_repository");
48
+ const workspaceBranch = text(source, ["symbolic-ref", "--short", "HEAD"]);
49
+ if (text(target, ["symbolic-ref", "--short", "HEAD"]) !== input.integrationBranch)
50
+ refuse("integration_branch_changed");
51
+ const head = text(source, ["rev-parse", "--verify", "HEAD^{commit}"]), integrated = text(target, ["rev-parse", "--verify", `${integrationRef}^{commit}`]);
52
+ git(target, ["merge-base", "--is-ancestor", head, integrated]);
53
+ const paths = [...references, ...removals].map(ref => ref.path);
54
+ const observations = [...new Set([source, target])].map(root => {
55
+ const visibility = git(root, ["ls-files", "-v", "-z", "--", ...paths]).toString("utf8");
56
+ if (visibility.split("\0").filter(Boolean).some(entry => !entry.startsWith("H ")))
57
+ refuse("selected_index_visibility_override");
58
+ git(root, ["diff", "--cached", "--exit-code", "--", ...paths]);
59
+ assertMemoryGitRemovalSources(root, removals);
60
+ const observed = ObservedMemorySources.capture({ workspace: root }, references.map(({ root, path }) => ({ root, path })));
61
+ const hashes = observed.hashes();
62
+ for (const ref of references) {
63
+ if (hashes.get(memorySourceKey("workspace", ref.path)) !== ref.sha256)
64
+ refuse("reviewed_workspace_bytes_changed");
65
+ if (process.platform !== "win32" && ((fs.lstatSync(path.join(root, ref.path)).mode & 0o111) !== 0) !== (ref.git_mode === "100755"))
66
+ refuse("reviewed_workspace_mode_changed");
67
+ }
68
+ return observed;
69
+ });
70
+ // ponytail: at most 4096 selected paths; batch Git reads if real interactive
71
+ // inventories make per-file subprocess latency material.
72
+ for (const [root, revision] of [[source, head], [target, integrated]]) {
73
+ for (const ref of references) {
74
+ const entry = git(root, ["ls-tree", "-z", revision, "--", ref.path]).toString("utf8");
75
+ if (!entry.startsWith(`${ref.git_mode ?? "100644"} blob `) || !entry.endsWith(`\t${ref.path}\0`))
76
+ refuse("reviewed_regular_git_blob_required");
77
+ if (createHash("sha256").update(git(root, ["cat-file", "blob", `${revision}:${ref.path}`])).digest("hex") !== ref.sha256)
78
+ refuse("reviewed_git_bytes_changed");
79
+ }
80
+ for (const ref of removals)
81
+ if (git(root, ["ls-tree", "-z", revision, "--", ref.path]).length)
82
+ refuse("removed_file_still_committed");
83
+ }
84
+ for (const observation of observations)
85
+ observation.revalidate();
86
+ if (text(source, ["rev-parse", "HEAD"]) !== head || text(target, ["rev-parse", integrationRef]) !== integrated)
87
+ refuse("integration_changed_during_observation");
88
+ return { schema_id: "dd-flow/project-git-integration-observation@1", project_root: target, workspace_root: source,
89
+ repository, workspace_branch: workspaceBranch, integration_ref: integrationRef, workspace_head: head, integration_head: integrated,
90
+ reviewed_files_sha256: hybridDigest([...references].sort((a, b) => a.path.localeCompare(b.path, "en"))),
91
+ removed_files_sha256: hybridDigest([...removals].sort((a, b) => a.path.localeCompare(b.path, "en"))) };
92
+ }
@@ -2,15 +2,10 @@ import crypto from "node:crypto";
2
2
  import { stageAttempt, stageCycle, bindStageWork } from "./stage-work-graph.js";
3
3
  import { assertRunMutationAllowed } from "./run-recovery.js";
4
4
  import { AppError } from "../shared/errors.js";
5
- import { assertSourceRepairEligible } from "./code-checks.js";
5
+ import { assertCheckDiagnosisEligible } from "./code-checks.js";
6
6
  /** Checks finish before entering this synchronous registration transaction. */
7
7
  export function ensureAggregateRepair(context, input, create) {
8
- if (!input.failures.length)
9
- throw new AppError("repair_intent_invalid", "Repair requires candidate-source-backed failed receipts", 1);
10
- const workspace = context.db.get("SELECT workspace_root FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]);
11
- if (!workspace?.workspace_root && input.failures.some(receipt => receipt.candidate_library_inputs?.length))
12
- throw new AppError("repair_intent_invalid", "Library-backed repair requires the current RUN workspace", 1);
13
- assertSourceRepairEligible(input.failures, workspace?.workspace_root);
8
+ assertCheckDiagnosisEligible(input.failures);
14
9
  const causes = input.failures.map(receipt => ({ id: receipt.id, epoch: receipt.verification_epoch, fingerprint: receipt.fingerprint_scope === "check_inputs" ? receipt.before_fingerprint : receipt.workspace_fingerprint })).sort((a, b) => a.id.localeCompare(b.id));
15
10
  return ensureRepairIntent(context, { ...input, causes: { receipts: causes, origins: [...(input.origins ?? [])].sort() } }, create);
16
11
  }