@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
@@ -1,7 +1,31 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { AppError } from "../shared/errors.js";
4
- import { managedProcessRuntimeHome, managedProcessStatus } from "./managed-processes.js";
4
+ import { managedProcessRuntimeHome, managedProcessStatus, processTreeIsAlive } from "./managed-processes.js";
5
+ /** Native execution and AGY's potentially scheduled writers are separate facts. */
6
+ export function managedExecutionReusable(state, harness) {
7
+ const shutdown = state.shutdown, result = shutdown?.result;
8
+ if (state.active_tree !== false || state.unclaimed_activity === true || shutdown?.phases?.execution !== true
9
+ || result?.native_settled !== true || result.settled !== true || result.native_settlement?.confirmed === false
10
+ || state.native_execution_revision !== undefined && state.native_execution_revision !== result.native_settlement?.revision)
11
+ return false;
12
+ // These adapters do not yet publish a revision for every possible late
13
+ // child/tool event. A completed native observation alone cannot exclude a
14
+ // newly scheduled writer; the caller can still use physical absence.
15
+ if (["zcode-acp", "droid-cli"].includes(harness))
16
+ return false;
17
+ if (harness !== "antigravity-cli")
18
+ return true;
19
+ const daemon = state.daemon_id ?? state.config?.daemonId, proof = shutdown.native_resource_reconciliation, resources = result.native_resources;
20
+ return Boolean(daemon && shutdown.phases.native_resources_reconciled === true && proof?.daemon_id === daemon
21
+ && proof.dispatch_generation === shutdown.generation && Number.isSafeInteger(proof.dispatch_generation)
22
+ && proof.native_revision === state.native_execution_revision && Number.isSafeInteger(proof.native_revision)
23
+ && resources?.schema_id === "native-resources@1" && resources.owner_daemon_id === daemon
24
+ && resources.revision === proof.native_revision && Number.isSafeInteger(resources.generation)
25
+ && result.native_settlement?.owner_daemon_id === daemon && result.native_settlement.generation === resources.generation
26
+ && Array.isArray(resources.entries)
27
+ && resources.entries.every(item => item.owner_daemon_id === daemon && item.generation === resources.generation && item.state === "stopped"));
28
+ }
5
29
  export function readNativeDaemonState(stateDir) {
6
30
  if (!path.isAbsolute(stateDir))
7
31
  throw new AppError("daemon_owner_mismatch", "Native state directory must be absolute", 1);
@@ -36,3 +60,30 @@ export function requireManagedDaemonBinding(context, input) {
36
60
  }
37
61
  return { state, process: record };
38
62
  }
63
+ /** A dead bridge is not enough: its provider may own a detached process group. */
64
+ export function managedDaemonExecutionAbsent(context, input) {
65
+ const binding = requireManagedDaemonBinding(context, input);
66
+ const records = managedProcessStatus(context).filter(record => {
67
+ try {
68
+ return JSON.parse(record.metadata_json).state_dir === input.stateDir;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ });
74
+ if (!records.length || records.some(record => {
75
+ const group = JSON.parse(record.metadata_json).process_group_id;
76
+ // A dead leader without its retained tree boundary cannot exclude surviving
77
+ // children. Fall back to strict native reconciliation, not assumed absence.
78
+ return record.project_id !== input.projectId || record.run_id !== input.runId
79
+ || managedProcessRuntimeHome(record, context) !== context.ddFlowHome || !record.pid || !record.pid_started_at
80
+ || !Number.isSafeInteger(group) || group <= 0 || processTreeIsAlive(record);
81
+ }))
82
+ return false;
83
+ if (binding.state.provider_process || binding.state.provider_pid) {
84
+ const provider = records.find(record => record.id === binding.state.provider_process?.id);
85
+ if (!provider || !managedProviderBelongsToDaemon(provider, binding.process, binding.state))
86
+ return false;
87
+ }
88
+ return true;
89
+ }
@@ -2,11 +2,11 @@ import path from "node:path";
2
2
  import { AppError } from "../shared/errors.js";
3
3
  import { assertMemoryOrdinaryConfigurationCoverage, assertMemoryOrdinaryConfigurationOutputs } from "./memory-ordinary-defaults.js";
4
4
  import { hybridDigest } from "./hybrid-contracts.js";
5
- import { runtimeHarness } from "./execution-policy.js";
6
5
  import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
7
6
  import { requireProjectByRoot } from "./projects.js";
8
7
  import { resolveProjectRoot } from "../storage/paths.js";
9
8
  import { PreparedDescriptorStageCoordinator, readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
9
+ import { readDescriptorCoordinatorSession } from "./descriptor-coordinator-session.js";
10
10
  import { readDescriptorRootWork } from "./descriptor-root-work.js";
11
11
  import { stageGraphScope } from "./stage-work-graph.js";
12
12
  import { PreparedMemoryResultBinding } from "./memory-result-binding.js";
@@ -64,27 +64,7 @@ function compositionBaseline(context, bundle) {
64
64
  * separately. No new Session, hook claim or provider invocation is created.
65
65
  */
66
66
  function coordinatorSession(context, scope, bundle, workId) {
67
- const rows = context.db.all("SELECT ws.id,ws.session_id,ws.hook_event_id,ws.prompt_path,ws.result_path,s.harness,s.provider_session_id FROM work_sessions ws JOIN sessions s ON s.session_id=ws.session_id AND s.project_id=? WHERE ws.work_id=? AND ws.status='running' AND s.run_id=? AND s.current_stage=? AND s.status='active' LIMIT 2", [scope.projectId, workId, scope.runId, scope.stage]);
68
- if (rows.length !== 1)
69
- refuse("active_coordinator_session_required");
70
- const session = rows[0], route = bundle.execution.stage(scope.stage);
71
- const input = readDescriptorStageInput(context, scope, bundle), directory = bundle.descriptor.stage(scope.stage).directory;
72
- if (session.prompt_path !== path.join(input.runRoot, directory, "coordinator-prompt.md")
73
- || session.result_path !== path.join(input.runRoot, directory, "result-input.json"))
74
- refuse("coordinator_packet_link_changed");
75
- if (session.harness !== runtimeHarness(route.profile)
76
- || !context.db.get("SELECT 1 FROM hook_events WHERE project_id=? AND event_key=? AND session_id=? AND harness=? AND provider_session_id=? AND status='claimed' AND event_name='PreToolUse'", [scope.projectId, session.hook_event_id, session.session_id, session.harness, session.provider_session_id]))
77
- refuse("coordinator_start_custody_missing");
78
- const bindings = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='stage.coordinator_binding' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')=? AND json_extract(payload_json,'$.work_session_id')=? LIMIT 33", [scope.projectId, scope.runId, scope.stage, session.id]);
79
- if (!bindings.length || bindings.length > 32 || bindings.some(row => Buffer.byteLength(row.payload_json) > 32768))
80
- refuse("physical_binding_missing");
81
- const binding = JSON.parse(bindings[0].payload_json);
82
- if (bindings.some(row => hybridDigest(JSON.parse(row.payload_json)) !== hybridDigest(binding))
83
- || binding.work_id !== workId || binding.session_id !== session.session_id || binding.harness !== session.harness || binding.provider_session_id !== session.provider_session_id
84
- || binding.execution_sha256 !== bundle.execution.sha256 || binding.profile_id !== route.profile.id || binding.profile_sha256 !== hybridDigest(route.profile)
85
- || binding.session_mode !== route.session_mode || hybridDigest(binding.scope) !== hybridDigest({ ...stageGraphScope(context, scope.projectId, scope.runId, scope.stage), stage: scope.stage }))
86
- refuse("physical_binding_changed");
87
- return hybridDigest({ session, binding });
67
+ return readDescriptorCoordinatorSession(context, { scope, bundle, workId }, refuse);
88
68
  }
89
69
  /** Actual registration path for the composition handler, awaiting complete
90
70
  * memory-flow admission. Registers the immutable blueprint before workers run;
@@ -5,6 +5,7 @@ import { PreparedMemoryFinishReview } from "./memory-finish-handler.js";
5
5
  import { readMemoryInitBootstrapReadiness } from "./memory-init-bootstrap-readiness.js";
6
6
  import { readMemoryInitGitPolicy } from "./memory-init-git-policy.js";
7
7
  import { hybridDigest } from "./hybrid-contracts.js";
8
+ import { assertReviewedProjectGitInventory, observeReviewedProjectGitIntegration } from "./project-git-integration.js";
8
9
  import { observeReviewedMemoryGitIntegration, reviewedMemoryGitFiles, isReviewedMemoryGitFile, assertMemoryGitRemovalSources } from "./memory-git-integration.js";
9
10
  import { createHash } from "node:crypto";
10
11
  import { readBoundedReleaseFile } from "./release-source.js";
@@ -149,6 +150,20 @@ export function observeMemoryUpgradeFinishInventory(root, expected) {
149
150
  /** Physical Git operation only; callers must independently select accepted files
150
151
  * and retain each mutation's intent. No ambient Git redirection or whole-tree add. */
151
152
  export function integrateReviewedMemoryFiles(selection, environment, references, runId, mutation, policy, removals = []) {
153
+ if (!Array.isArray(references) || !Array.isArray(removals) || !references.length
154
+ || references.length + removals.length > 4096 || !references.some(ref => ref?.path === ".memory-bank/index.md")
155
+ || [...references, ...removals].some(ref => !isReviewedMemoryGitFile(ref))
156
+ || removals.some(ref => !ref.path.startsWith(".memory-bank/") || ref.path === ".memory-bank/index.md"))
157
+ refuse("invalid_owned_inventory");
158
+ return integrateReviewedFiles(selection, environment, references, mutation, policy, removals, () => observeReviewedMemoryGitIntegration(selection, environment, references, removals), `chore(${runId.toLowerCase()}): integrate reviewed Memory Bank`);
159
+ }
160
+ /** Physical project-file variant only. Accepted review and the original dirty
161
+ * baseline remain the caller's responsibility; this is not a RUN authority. */
162
+ export function integrateReviewedProjectFiles(selection, environment, references, runId, mutation, policy, removals = []) {
163
+ assertReviewedProjectGitInventory(references, removals);
164
+ return integrateReviewedFiles(selection, environment, references, mutation, policy, removals, () => observeReviewedProjectGitIntegration(selection, environment, references, removals), `chore(${runId.toLowerCase()}): integrate reviewed project changes`);
165
+ }
166
+ function integrateReviewedFiles(selection, environment, references, mutation, policy, removals, observe, message) {
152
167
  const env = { ...Object.fromEntries(Object.entries(environment).filter(([key]) => !key.startsWith("GIT_"))), GIT_LITERAL_PATHSPECS: "1" };
153
168
  const git = (directory, args, write = false, absent = false) => {
154
169
  const result = spawnSync(selection.gitExecutable, ["-c", "core.fsmonitor=false",
@@ -158,13 +173,8 @@ export function integrateReviewedMemoryFiles(selection, environment, references,
158
173
  if (result.error || result.signal || result.status !== 0)
159
174
  throw new AppError("memory_finish_integration_invalid", write ? "Git integration did not return success; inspect the original operation before retrying" : "Git integration preparation failed", 2, { reason: "git_operation_failed", effect: write ? "unknown" : "no_effect", operation: args[0], stderr: result.stderr,
160
175
  next_action: write ? "reconcile_original_integration" : "resolve_git_configuration_or_workspace" });
161
- return result.stdout.trim();
176
+ return args.includes("-z") ? result.stdout : result.stdout.trim();
162
177
  };
163
- if (!Array.isArray(references) || !Array.isArray(removals) || !references.length
164
- || references.length + removals.length > 4096 || !references.some(ref => ref?.path === ".memory-bank/index.md")
165
- || [...references, ...removals].some(ref => !isReviewedMemoryGitFile(ref))
166
- || removals.some(ref => !ref.path.startsWith(".memory-bank/") || ref.path === ".memory-bank/index.md"))
167
- refuse("invalid_owned_inventory");
168
178
  const paths = [...references, ...removals].map(ref => ref.path);
169
179
  if (new Set(paths).size !== paths.length)
170
180
  refuse("invalid_owned_inventory");
@@ -195,10 +205,10 @@ export function integrateReviewedMemoryFiles(selection, environment, references,
195
205
  if (hashes.get(memorySourceKey(ref.root, ref.path)) !== ref.sha256)
196
206
  refuse("reviewed_workspace_bytes_changed");
197
207
  try {
198
- return observeReviewedMemoryGitIntegration(selection, environment, references, removals);
208
+ return observe();
199
209
  }
200
210
  catch (error) {
201
- if (!(error instanceof AppError) || error.code !== "memory_git_integration_invalid")
211
+ if (!(error instanceof AppError) || !["memory_git_integration_invalid", "project_git_integration_invalid"].includes(error.code))
202
212
  throw error;
203
213
  }
204
214
  const visibility = git(source, ["ls-files", "-v", "--", ...paths]);
@@ -207,6 +217,12 @@ export function integrateReviewedMemoryFiles(selection, environment, references,
207
217
  if (git(source, ["diff", "--name-only", "--diff-filter=U"]))
208
218
  refuse("unmerged_workspace");
209
219
  const sourceHead = git(source, ["rev-parse", "--verify", "HEAD"], false, true);
220
+ let ownedHead = sourceHead;
221
+ const changedCommit = () => {
222
+ throw new AppError("memory_finish_integration_invalid", "The scoped commit changed before integration completed; reconcile its original effect", 2, { reason: "scoped_commit_changed", effect: "unknown", next_action: "reconcile_original_integration" });
223
+ };
224
+ const assertOwnedHead = () => { if (git(source, ["rev-parse", "HEAD"]) !== ownedHead)
225
+ changedCommit(); };
210
226
  const deleting = [];
211
227
  let removedBytes = 0;
212
228
  for (const ref of removals) {
@@ -237,26 +253,49 @@ export function integrateReviewedMemoryFiles(selection, environment, references,
237
253
  return bytes.status !== 0 || createHash("sha256").update(bytes.stdout).digest("hex") !== ref.sha256
238
254
  || !entry.startsWith(`${ref.git_mode ?? "100644"} blob `);
239
255
  });
256
+ const assertSourceBeforeMutation = () => {
257
+ observed.revalidate();
258
+ assertModes();
259
+ assertMemoryGitRemovalSources(source, removals);
260
+ if (git(source, ["rev-parse", "--verify", "HEAD"], false, true) !== sourceHead
261
+ || git(source, ["symbolic-ref", "--short", "HEAD"]) !== branch)
262
+ refuse("source_git_baseline_changed");
263
+ };
240
264
  if (needsCommit) {
241
265
  git(source, ["var", "GIT_AUTHOR_IDENT"]);
242
266
  git(source, ["var", "GIT_COMMITTER_IDENT"]);
243
267
  mutation("commit", sourceHead, () => {
244
- observed.revalidate();
245
- assertModes();
246
- assertMemoryGitRemovalSources(source, removals);
247
- if (git(source, ["rev-parse", "--verify", "HEAD"], false, true) !== sourceHead
248
- || git(source, ["symbolic-ref", "--short", "HEAD"]) !== branch)
249
- refuse("source_git_baseline_changed");
268
+ assertSourceBeforeMutation();
250
269
  const commitPaths = [...references.map(ref => ref.path), ...deleting];
251
270
  git(source, ["add", "-f", "--", ...commitPaths], true);
252
- git(source, ["commit", "--only", "-m", `chore(${runId.toLowerCase()}): integrate reviewed Memory Bank`, "--", ...commitPaths], true);
271
+ git(source, ["commit", "--only", "-m", message, "--", ...commitPaths], true);
272
+ });
273
+ // Hooks or a concurrent writer may have changed history/index after the
274
+ // original admission. Never carry an unowned commit into the merge.
275
+ ownedHead = git(source, ["rev-parse", "HEAD"]);
276
+ const parentAndSubject = git(source, ["show", "-s", "--format=%P%n%s", ownedHead]);
277
+ const committedPaths = git(source, ["diff-tree", "--root", "--no-commit-id", "--no-renames", "--name-only", "-r", "-z", ownedHead]).split("\0").filter(Boolean);
278
+ if (parentAndSubject !== `${sourceHead}\n${message}`.trim() || committedPaths.some(logical => !paths.includes(logical)))
279
+ changedCommit();
280
+ }
281
+ else if (git(source, ["diff", "--cached", "--name-only", "-z", "--", ...paths])) {
282
+ // The reviewed tree can already equal HEAD while a selected pre-existing
283
+ // staged version differs. Settle only its admitted index effect; do not
284
+ // manufacture an empty commit or disturb unrelated staged entries.
285
+ const indexedRemovals = removals.length ? git(source, ["ls-files", "--cached", "-z", "--", ...removals.map(ref => ref.path)]).split("\0").filter(Boolean) : [];
286
+ mutation("commit", sourceHead, () => {
287
+ assertSourceBeforeMutation();
288
+ git(source, ["add", "-f", "--", ...references.map(ref => ref.path), ...indexedRemovals], true);
253
289
  });
290
+ assertOwnedHead();
254
291
  }
255
292
  observed.revalidate();
293
+ assertOwnedHead();
256
294
  if (source !== target) {
257
- const head = git(source, ["rev-parse", "HEAD"]), targetHead = git(target, ["rev-parse", "HEAD"]);
295
+ const head = ownedHead, targetHead = git(target, ["rev-parse", "HEAD"]);
258
296
  if (head !== targetHead) {
259
297
  mutation("merge", targetHead, () => {
298
+ assertOwnedHead();
260
299
  if (git(target, ["rev-parse", "HEAD"]) !== targetHead
261
300
  || git(target, ["symbolic-ref", "--short", "HEAD"]) !== selection.integrationBranch
262
301
  || git(target, ["diff", "--cached", "--name-only"]))
@@ -265,7 +304,9 @@ export function integrateReviewedMemoryFiles(selection, environment, references,
265
304
  });
266
305
  }
267
306
  }
268
- return observeReviewedMemoryGitIntegration(selection, environment, references, removals);
307
+ const result = observe();
308
+ assertOwnedHead();
309
+ return result;
269
310
  }
270
311
  /** The admitted Memory finish operation. A retained intent never licenses a
271
312
  * second dispatch: replay first observes completed Git effects, or reports unknown. */
@@ -0,0 +1,129 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
6
+ import { observeMemoryGitPolicySelection } from "./memory-init-git-policy.js";
7
+ import { observeReviewedMemoryGitIntegration } from "./memory-git-integration.js";
8
+ import { readBoundedReleaseFile } from "./release-source.js";
9
+ import { readAcceptedStageData } from "./stage-report-renderer.js";
10
+ import { readAcceptedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
11
+ import { readMemoryFixPublication } from "./memory-fix-publication.js";
12
+ import { hybridDigest } from "./hybrid-contracts.js";
13
+ import { entityReferenceVariants } from "../shared/entity-references.js";
14
+ import { stageLifecycleMatchKey } from "./hooks.js";
15
+ const logical = "runtime/git-policy.json";
16
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
17
+ function refuse(reason) {
18
+ throw new AppError("memory_fix_finish_integration_invalid", "Repair finish requires accepted review and project-policy Git readback", 2, { reason, effect: "no_effect" });
19
+ }
20
+ /** Read-only explicit policy preparation before RUN creation. The factory must
21
+ * retain these bytes in the frozen bundle; this grants no commit/merge/push. */
22
+ export function prepareMemoryFixGitPolicy(environment, input) {
23
+ if (!path.isAbsolute(input.gitExecutable))
24
+ refuse("absolute_git_executable_required");
25
+ const gitExecutable = fs.realpathSync(input.gitExecutable);
26
+ const policy = observeMemoryGitPolicySelection(environment, { ...input, gitExecutable });
27
+ const value = { schema_id: "dd-flow/mb-fix-git-policy@1", scope: {
28
+ requestId: input.scope.requestId, projectRoot: input.scope.projectRoot, workspaceRoot: input.scope.workspaceRoot
29
+ },
30
+ git_executable: gitExecutable, git_sha256: hash(readBoundedReleaseFile(gitExecutable, 64 * 1024 * 1024)), policy };
31
+ const bytes = Buffer.from(JSON.stringify(value));
32
+ return { path: logical, bytes, sha256: hash(bytes) };
33
+ }
34
+ /** Frozen/SQL custody only, suitable for the acceptance writer. No ambient
35
+ * main-branch inference, tool discovery or filesystem access in this reader. */
36
+ export function readMemoryFixGitPolicy(context, scope, bundle) {
37
+ if (bundle.manifest.flow_id !== "mb-fix")
38
+ refuse("memory_fix_bundle_required");
39
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
40
+ const references = bundle.manifest.inventory.filter(ref => ref.path === logical), bytes = bundle.blob(logical);
41
+ if (references.length !== 1 || !bytes.length || bytes.length > 32768 || references[0].sha256 !== hash(bytes))
42
+ refuse("frozen_git_policy_required");
43
+ let value;
44
+ try {
45
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
46
+ }
47
+ catch {
48
+ return refuse("invalid_git_policy");
49
+ }
50
+ if (value?.schema_id !== "dd-flow/mb-fix-git-policy@1" || Object.keys(value).sort().join() !== "git_executable,git_sha256,policy,schema_id,scope"
51
+ || value.scope?.projectRoot !== run.project_root || value.scope?.workspaceRoot !== run.workspace_root
52
+ || !value.scope.requestId || value.scope.requestId !== bundle.execution.snapshot().scope.requestId
53
+ || typeof value.git_executable !== "string" || !/^[a-f0-9]{64}$/.test(value.git_sha256)
54
+ || value.policy?.source !== "explicit_request" || value.policy.push_authorized !== false
55
+ || !["feature_worktree", "integration_branch_direct"].includes(value.policy.route)
56
+ || !value.policy.repository || !value.policy.workspace_branch || !value.policy.integration_branch)
57
+ refuse("git_policy_scope_changed");
58
+ return structuredClone(value);
59
+ }
60
+ /** Selected repair changes plus the unchanged mandatory MB index anchor, not
61
+ * a whole-tree commit inventory. Only existing selected files may be removed. */
62
+ export function memoryFixGitInventory(publication) {
63
+ const before = new Map(publication.plan.state(0).entries().map(entry => [entry.path, entry]));
64
+ const after = new Map(publication.plan.state(publication.plan.actions().length).entries().map(entry => [entry.path, entry]));
65
+ const references = [], removals = [];
66
+ for (const logical of new Set([...publication.binding.write_scope.paths, ".memory-bank/index.md"])) {
67
+ const entry = after.get(logical);
68
+ if (entry) {
69
+ if (entry.kind !== "file" || !entry.sha256)
70
+ refuse("regular_reviewed_file_required");
71
+ references.push({ root: "workspace", path: logical, sha256: entry.sha256, ...((entry.mode & 0o111) ? { git_mode: "100755" } : {}) });
72
+ }
73
+ else {
74
+ const original = before.get(logical);
75
+ if (logical === ".memory-bank/index.md" || !original?.sha256 || original.kind !== "file")
76
+ refuse("retained_removal_required");
77
+ removals.push({ root: "workspace", path: logical, sha256: original.sha256, ...((original.mode & 0o111) ? { git_mode: "100755" } : {}) });
78
+ }
79
+ }
80
+ return { references, removals };
81
+ }
82
+ /** Accepted review/policy/publication inputs for the shared readback and scoped
83
+ * dispatcher. SQL custody only; this does not grant Git mutation by itself. */
84
+ export function prepareMemoryFixReviewedGitInputs(context, scope, bundle) {
85
+ if (context.db.isOwnedWriteTransaction?.())
86
+ refuse("preparation_outside_writer_required");
87
+ const selected = readMemoryFixGitPolicy(context, scope, bundle);
88
+ const keys = entityReferenceVariants(scope.runId).map(ref => stageLifecycleMatchKey("stage_finish", ref, "review", selected.scope.projectRoot));
89
+ const read = (writer) => {
90
+ const policy = readMemoryFixGitPolicy(writer, scope, bundle), review = readAcceptedStageData(writer, { ...scope, stage: "review" });
91
+ const settled = readAcceptedDescriptorStageSettlement(writer, { ...scope, stage: "review" }, bundle, keys);
92
+ const publication = readMemoryFixPublication(writer, { project_id: scope.projectId, run_id: scope.runId });
93
+ const result = JSON.parse(review.bytes.toString("utf8"));
94
+ if (review.data_schema_id !== "dd-flow/mb-fix-stage-result@1" || result.run_id !== scope.runId || result.stage !== "review"
95
+ || !publication?.completed || publication.binding.bundle_sha256 !== bundle.sha256
96
+ || hybridDigest(result.details.write_scope) !== hybridDigest(publication.binding.write_scope)
97
+ || result.details.verification.some(item => item.verdict !== "resolved")
98
+ || publication.binding.write_scope.finding_ids.some(id => !result.details.verification.some(item => item.finding_id === id && item.verdict === "resolved")
99
+ || !result.details.review.some(item => item.finding_id === id && item.disposition === "accept")))
100
+ refuse("accepted_repair_review_required");
101
+ return { policy, review, settled, binding: publication.binding, planSha256: publication.plan.sha256, inventory: memoryFixGitInventory(publication) };
102
+ };
103
+ const original = context.db.readSnapshot(() => read(context));
104
+ return { selected, original, assertCustody: (writer) => {
105
+ if (hybridDigest(read(writer)) !== hybridDigest(original))
106
+ refuse("accepted_integration_custody_changed");
107
+ } };
108
+ }
109
+ /** Current physical readback of independently admitted review/policy inputs. */
110
+ export function prepareMemoryFixFinishIntegration(context, scope, bundle) {
111
+ const inputs = prepareMemoryFixReviewedGitInputs(context, scope, bundle), { selected, original } = inputs;
112
+ const observe = () => {
113
+ if (hash(readBoundedReleaseFile(selected.git_executable, 64 * 1024 * 1024)) !== selected.git_sha256)
114
+ refuse("selected_git_changed");
115
+ const observation = observeReviewedMemoryGitIntegration({ projectRoot: selected.scope.projectRoot, workspaceRoot: selected.scope.workspaceRoot,
116
+ integrationBranch: selected.policy.integration_branch, gitExecutable: selected.git_executable }, context.env, original.inventory.references, original.inventory.removals);
117
+ if (observation.repository !== selected.policy.repository || observation.workspace_branch !== selected.policy.workspace_branch)
118
+ refuse("policy_repository_or_branch_changed");
119
+ return observation;
120
+ };
121
+ const observation = observe();
122
+ return { value: () => structuredClone(observation), assertCustody: inputs.assertCustody, revalidate: () => {
123
+ if (context.db.isOwnedWriteTransaction?.())
124
+ refuse("physical_revalidation_outside_writer_required");
125
+ inputs.assertCustody(context);
126
+ if (hybridDigest(observe()) !== hybridDigest(observation))
127
+ refuse("integration_readback_changed");
128
+ } };
129
+ }
@@ -0,0 +1,139 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { resolveProjectRoot } from "../storage/paths.js";
5
+ import { requireProjectByRoot } from "./projects.js";
6
+ import { readFlowRunExecutionBundle } from "./runs.js";
7
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
8
+ import { readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
9
+ import { prepareMemoryFixReviewedGitInputs, prepareMemoryFixFinishIntegration } from "./memory-fix-finish-integration.js";
10
+ import { integrateReviewedMemoryFiles } from "./memory-finish-integration.js";
11
+ import { readMemoryFixPublication } from "./memory-fix-publication.js";
12
+ import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
13
+ import { readBoundedReleaseFile } from "./release-source.js";
14
+ import { hybridDigest } from "./hybrid-contracts.js";
15
+ import { appendAudit } from "./audit.js";
16
+ const event = "memory_fix.integration_intent", anchorPath = '$.variables."memory_fix.integration"';
17
+ function refuse(reason) {
18
+ throw new AppError("memory_fix_git_dispatch_invalid", "Repair integration requires the original scoped reviewed operation", 2, { reason, effect: "no_effect" });
19
+ }
20
+ function pending(reason) {
21
+ throw new AppError("memory_fix_integration_pending", "Reconcile the original Git operation before redispatch", 1, { reason, effect: "unknown", next_action: "reconcile_original_integration" });
22
+ }
23
+ /** Explicit scoped local commit/merge only. Existing Git mechanics own writes;
24
+ * each phase has retained intent before effects. Never push, reset, autostash or
25
+ * repeat an unresolved phase. A proven commit can continue to its first merge.
26
+ */
27
+ export function integrateMemoryFixFinish(context, input) {
28
+ if (context.db.isOwnedWriteTransaction?.())
29
+ refuse("preparation_outside_writer_required");
30
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)), bundle = readFlowRunExecutionBundle(context, input);
31
+ if (!bundle || bundle.manifest.flow_id !== "mb-fix")
32
+ refuse("memory_fix_bundle_required");
33
+ const scope = { projectId: project.id, runId: input.runId, stage: "finish" };
34
+ const stage = readDescriptorStageInput(context, scope, bundle), actor = readActiveDescriptorStageCoordinator(context, scope, bundle);
35
+ const reviewed = prepareMemoryFixReviewedGitInputs(context, scope, bundle), { selected, original } = reviewed;
36
+ const delivered = readPreparedMemoryFlowUserInputs(context, scope, "mb-fix", bundle), userDigest = hybridDigest(delivered);
37
+ const agreement = input.agreement ?? { user_inputs_sha256: original.binding.user_inputs_sha256, approval_source: original.binding.write_scope.approval_source };
38
+ if (agreement.user_inputs_sha256 !== userDigest || agreement.approval_source?.root !== "run"
39
+ || !delivered.sources.some(source => ["request", "answer"].includes(source.role)
40
+ && source.path === agreement.approval_source.path && source.sha256 === agreement.approval_source.sha256))
41
+ refuse("current_delivered_agreement_required");
42
+ const selection = { projectRoot: selected.scope.projectRoot, workspaceRoot: selected.scope.workspaceRoot,
43
+ integrationBranch: selected.policy.integration_branch, gitExecutable: selected.git_executable };
44
+ const identity = hybridDigest({ scope, bundle_sha256: bundle.sha256, stage, actor, reviewed: original, agreement });
45
+ const intents = (writer) => {
46
+ const rows = writer.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? LIMIT 3", [project.id, event, input.runId]);
47
+ const anchor = writer.db.get("SELECT json_extract(index_json,?) identity FROM runs WHERE project_id=? AND id=?", [anchorPath, project.id, input.runId])?.identity;
48
+ if (rows.length > 2 || rows.some(row => Buffer.byteLength(row.payload_json) > 32768) || !!rows.length !== !!anchor || anchor && anchor !== identity)
49
+ pending("integration_intent_changed");
50
+ const values = rows.map(row => JSON.parse(row.payload_json));
51
+ if (new Set(values.map(value => value.phase)).size !== values.length || values.some(value => value.run_id !== input.runId || value.identity !== identity
52
+ || !["commit", "merge"].includes(value.phase) || typeof value.baseline !== "string" || value.baseline !== "" && !/^[a-f0-9]{40,64}$/.test(value.baseline)))
53
+ pending("integration_intent_changed");
54
+ return values;
55
+ };
56
+ const assertTool = () => {
57
+ if (createHash("sha256").update(readBoundedReleaseFile(selected.git_executable, 64 * 1024 * 1024)).digest("hex") !== selected.git_sha256)
58
+ refuse("selected_git_changed");
59
+ };
60
+ const assertCustody = (writer) => {
61
+ reviewed.assertCustody(writer);
62
+ if (hybridDigest(readDescriptorStageInput(writer, scope, bundle)) !== hybridDigest(stage)
63
+ || hybridDigest(readActiveDescriptorStageCoordinator(writer, scope, bundle)) !== hybridDigest(actor)
64
+ || hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, "mb-fix", bundle)) !== userDigest)
65
+ refuse("integration_authority_changed");
66
+ };
67
+ const assertPublication = () => {
68
+ assertTool();
69
+ assertCustody(context);
70
+ const publication = readMemoryFixPublication(context, { project_id: project.id, run_id: input.runId });
71
+ if (!publication?.completed || publication.plan.sha256 !== original.planSha256)
72
+ refuse("completed_publication_required");
73
+ const actual = publication.plan.classify(selection.workspaceRoot);
74
+ if (actual.completed !== actual.total)
75
+ refuse("publication_not_observed");
76
+ assertUnselectedIndexAnchor();
77
+ };
78
+ const env = Object.fromEntries(Object.entries(context.env).filter(([name]) => !name.startsWith("GIT_")));
79
+ const gitRead = (args) => {
80
+ const result = spawnSync(selected.git_executable, ["-c", "core.fsmonitor=false", "-C", selection.workspaceRoot, ...args], { env: { ...env, GIT_OPTIONAL_LOCKS: "0", GIT_LITERAL_PATHSPECS: "1" }, timeout: 10000, maxBuffer: 16 * 1024 * 1024, shell: false, windowsHide: true });
81
+ if (result.error || result.signal || result.status !== 0)
82
+ refuse("git_readback_failed");
83
+ return result.stdout;
84
+ };
85
+ const previous = intents(context);
86
+ assertPublication();
87
+ try {
88
+ const proof = prepareMemoryFixFinishIntegration(context, scope, bundle);
89
+ proof.revalidate();
90
+ return { integrated: true, replayed: true, integration: proof.value() };
91
+ }
92
+ catch (error) {
93
+ if (!(error instanceof AppError) || error.code !== "memory_git_integration_invalid")
94
+ throw error;
95
+ if (previous.length) {
96
+ const commit = previous.length === 1 && previous[0].phase === "commit" ? previous[0] : null;
97
+ if (!commit || selection.workspaceRoot === selection.projectRoot
98
+ || gitRead(["log", "-1", "--format=%P%n%s"]).toString("utf8").trim() !== `${commit.baseline}\nchore(${input.runId.toLowerCase()}): integrate reviewed Memory Bank`.trim())
99
+ pending("original_git_effect_not_proven");
100
+ }
101
+ }
102
+ // The required index anchor is observation coverage, not permission to add
103
+ // an unrelated dirty/untracked index document to this repair's commit.
104
+ function assertUnselectedIndexAnchor() {
105
+ if (original.binding.write_scope.paths.includes(".memory-bank/index.md"))
106
+ return;
107
+ const index = original.inventory.references.find(ref => ref.path === ".memory-bank/index.md");
108
+ if (createHash("sha256").update(gitRead(["cat-file", "blob", "HEAD:.memory-bank/index.md"])).digest("hex") !== index.sha256
109
+ || !gitRead(["ls-tree", "-z", "HEAD", "--", ".memory-bank/index.md"]).toString("utf8").startsWith(`${index.git_mode ?? "100644"} blob `))
110
+ refuse("unselected_index_anchor_not_committed");
111
+ if (gitRead(["diff", "--cached", "--name-only", "-z", "--", ".memory-bank/index.md"]).length)
112
+ refuse("unselected_index_anchor_staged");
113
+ }
114
+ let dispatched = false;
115
+ try {
116
+ integrateReviewedMemoryFiles(selection, context.env, original.inventory.references, input.runId, (phase, baseline, action) => {
117
+ assertPublication();
118
+ context.db.writeTransaction(() => {
119
+ assertCustody(context);
120
+ const current = intents(context);
121
+ if (current.some(value => value.phase === phase))
122
+ pending("original_git_phase_unresolved");
123
+ appendAudit(context, { projectId: project.id, eventType: event, payload: { run_id: input.runId, identity, phase, baseline } });
124
+ context.db.run("UPDATE runs SET index_json=json_set(index_json,?,?) WHERE project_id=? AND id=?", [anchorPath, identity, project.id, input.runId]);
125
+ });
126
+ dispatched = true;
127
+ assertUnselectedIndexAnchor();
128
+ action();
129
+ }, selected.policy, original.inventory.removals);
130
+ const proof = prepareMemoryFixFinishIntegration(context, scope, bundle);
131
+ proof.revalidate();
132
+ return { integrated: true, replayed: false, integration: proof.value() };
133
+ }
134
+ catch (error) {
135
+ if (!dispatched)
136
+ throw error;
137
+ return pending(error instanceof AppError ? error.code : "git_operation_failed");
138
+ }
139
+ }
@@ -0,0 +1,11 @@
1
+ import { assertDiagnosticRepairSourceIntakeCustody, prepareDiagnosticRepairSourceIntake, readDiagnosticRepairSourceIntake } from "./diagnostic-repair-source-intake.js";
2
+ // Preserve this flow's own capsule identity; shared transport grants no approval.
3
+ export function assertMemoryFixSourceIntakeCustody(context, scope, bundle, expectedSha256) {
4
+ return assertDiagnosticRepairSourceIntakeCustody(context, scope, bundle, expectedSha256, "mb-fix");
5
+ }
6
+ export function prepareMemoryFixSourceIntake(context, input) {
7
+ return prepareDiagnosticRepairSourceIntake(context, input, "mb-fix");
8
+ }
9
+ export function readMemoryFixSourceIntake(context, input, retainedBundle) {
10
+ return readDiagnosticRepairSourceIntake(context, input, "mb-fix", retainedBundle);
11
+ }