@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149

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 (254) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +13 -3
  5. package/dist/cli/command-metadata.js +51 -16
  6. package/dist/cli/diagnostic-invocation.js +46 -0
  7. package/dist/cli/diagnostic.js +120 -0
  8. package/dist/cli/help.js +67 -1
  9. package/dist/cli/hook-ingress.js +27 -3
  10. package/dist/cli/memory-flow-admission.js +9 -3
  11. package/dist/cli/memory-init-invocation.js +10 -3
  12. package/dist/cli/memory-init.js +77 -68
  13. package/dist/cli/memory-storage-admission.js +3 -2
  14. package/dist/cli/memory-storage-qualification.js +105 -0
  15. package/dist/cli/memory-storage-transition.js +123 -0
  16. package/dist/cli/memory-upgrade.js +189 -0
  17. package/dist/cli/native-hook-ingress.js +14 -4
  18. package/dist/cli/native-hook-preparation.js +55 -0
  19. package/dist/cli/response-file.js +2 -1
  20. package/dist/cli/run-cli.js +79 -40
  21. package/dist/cli/setup.js +2 -1
  22. package/dist/cli.js +18 -0
  23. package/dist/domain/diagnostic-aspects.js +103 -0
  24. package/dist/domain/diagnostic-flows.js +8 -0
  25. package/dist/domain/stage-handler-identities.js +8 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  31. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  32. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  33. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  34. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
  35. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  37. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  38. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  39. package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
  40. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
  41. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  42. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  43. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  44. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  45. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  46. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  47. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  48. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  49. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  50. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  51. package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
  52. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  53. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  54. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  55. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  56. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  57. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  58. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  59. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  60. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
  61. package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
  62. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  63. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  64. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  65. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  66. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  67. package/dist/schemas/execution-bundle.schema.json +7 -1
  68. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  69. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  70. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  71. package/dist/schemas/release-impact.schema.json +3 -3
  72. package/dist/schemas/stage-start-response.schema.json +4 -0
  73. package/dist/services/accepted-intent.js +13 -1
  74. package/dist/services/canon.js +43 -7
  75. package/dist/services/check-environment.js +21 -0
  76. package/dist/services/check-runner-evidence.js +267 -15
  77. package/dist/services/cli-operation-classifier.js +1 -1
  78. package/dist/services/cli-upgrade-allowlist.js +1 -1
  79. package/dist/services/code-checks.js +34 -19
  80. package/dist/services/compatibility-preflight.js +5 -2
  81. package/dist/services/controller-execution.js +10 -4
  82. package/dist/services/controller-fanout.js +34 -10
  83. package/dist/services/controller-memory-task.js +45 -0
  84. package/dist/services/controller-memory-worker.js +49 -0
  85. package/dist/services/dashboard.js +2 -2
  86. package/dist/services/descriptor-run-settlement.js +5 -3
  87. package/dist/services/descriptor-run-start.js +46 -20
  88. package/dist/services/descriptor-stage-coordinator.js +66 -13
  89. package/dist/services/descriptor-stage-settlement.js +2 -1
  90. package/dist/services/descriptor-stage-start.js +40 -4
  91. package/dist/services/diagnostic-evidence.js +73 -0
  92. package/dist/services/diagnostic-report-publication.js +72 -0
  93. package/dist/services/diagnostic-report.js +67 -0
  94. package/dist/services/diagnostic-run-bundle.js +74 -0
  95. package/dist/services/diagnostic-snapshot.js +159 -0
  96. package/dist/services/diagnostic-stage-handlers.js +108 -0
  97. package/dist/services/diagnostic-stage-results.js +132 -0
  98. package/dist/services/diagnostic-start.js +84 -0
  99. package/dist/services/diagnostic-work-batch.js +34 -0
  100. package/dist/services/diagnostic-work-collection.js +97 -0
  101. package/dist/services/diagnostic-work-registration.js +101 -0
  102. package/dist/services/diagnostic-work-results.js +93 -0
  103. package/dist/services/engines.js +64 -14
  104. package/dist/services/eval-snapshots.js +186 -119
  105. package/dist/services/execution-bundles.js +19 -7
  106. package/dist/services/external-work-launch.js +5 -3
  107. package/dist/services/flow-catalog.js +13 -4
  108. package/dist/services/frozen-engine.js +148 -11
  109. package/dist/services/hook-memory-routing.js +188 -0
  110. package/dist/services/hook-recovery-guard.js +14 -0
  111. package/dist/services/hook-session-observation.js +24 -0
  112. package/dist/services/hooks.js +27 -8
  113. package/dist/services/hybrid-contracts.js +4 -13
  114. package/dist/services/hybrid-digest.js +14 -0
  115. package/dist/services/installed-flow-source.js +47 -4
  116. package/dist/services/lifecycle-invocations.js +179 -38
  117. package/dist/services/memory-accepted-composition.js +5 -4
  118. package/dist/services/memory-completed-correction-wave.js +23 -15
  119. package/dist/services/memory-compose-handler.js +163 -21
  120. package/dist/services/memory-composition-acceptance.js +186 -10
  121. package/dist/services/memory-composition-graph.js +42 -11
  122. package/dist/services/memory-composition-registration.js +23 -8
  123. package/dist/services/memory-composition-work-custody.js +14 -9
  124. package/dist/services/memory-composition-work-results.js +15 -3
  125. package/dist/services/memory-corrected-review-batch.js +10 -3
  126. package/dist/services/memory-corrected-review-findings.js +4 -2
  127. package/dist/services/memory-corrected-review-inputs.js +10 -5
  128. package/dist/services/memory-corrected-review-receipt.js +17 -9
  129. package/dist/services/memory-corrected-review-registration.js +3 -2
  130. package/dist/services/memory-correction-check-receipt.js +20 -4
  131. package/dist/services/memory-custody-snapshot.js +36 -0
  132. package/dist/services/memory-finish-evidence.js +44 -22
  133. package/dist/services/memory-finish-handler.js +37 -23
  134. package/dist/services/memory-finish-integration.js +218 -48
  135. package/dist/services/memory-finish-readiness.js +66 -19
  136. package/dist/services/memory-git-integration.js +87 -18
  137. package/dist/services/memory-historical-corrected-review.js +21 -15
  138. package/dist/services/memory-historical-original-review.js +22 -9
  139. package/dist/services/memory-historical-repair-registration.js +14 -8
  140. package/dist/services/memory-historical-second-review.js +30 -11
  141. package/dist/services/memory-init-contexts.js +44 -2
  142. package/dist/services/memory-init-controller-start.js +27 -8
  143. package/dist/services/memory-init-git-policy.js +50 -21
  144. package/dist/services/memory-init-lint-capability.js +24 -6
  145. package/dist/services/memory-init-native-capacity.js +17 -4
  146. package/dist/services/memory-init-preflight.js +21 -17
  147. package/dist/services/memory-init-request-candidate.js +73 -17
  148. package/dist/services/memory-init-request-input.js +23 -7
  149. package/dist/services/memory-init-run-bundle.js +49 -12
  150. package/dist/services/memory-init-storage.js +17 -7
  151. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  152. package/dist/services/memory-layer.js +71 -6
  153. package/dist/services/memory-lint-config.js +111 -2
  154. package/dist/services/memory-native-review-copy.js +5 -67
  155. package/dist/services/memory-normal-flow-entry.js +12 -0
  156. package/dist/services/memory-ordinary-defaults.js +22 -5
  157. package/dist/services/memory-preflight-handler.js +15 -0
  158. package/dist/services/memory-repair-current-view.js +1 -1
  159. package/dist/services/memory-repair-document-delta.js +70 -33
  160. package/dist/services/memory-repair-history.js +12 -7
  161. package/dist/services/memory-repair-physical-binding.js +1 -119
  162. package/dist/services/memory-repair-publication-plan.js +5 -3
  163. package/dist/services/memory-repair-publication-source.js +1 -1
  164. package/dist/services/memory-repair-publication.js +3 -2
  165. package/dist/services/memory-repair-registration.js +2 -1
  166. package/dist/services/memory-repair-work-baseline.js +4 -2
  167. package/dist/services/memory-repair-work-completion.js +32 -12
  168. package/dist/services/memory-repair-work-results.js +21 -2
  169. package/dist/services/memory-repair-workspace.js +11 -2
  170. package/dist/services/memory-repair-writer-copy.js +2 -1
  171. package/dist/services/memory-result-binding.js +78 -12
  172. package/dist/services/memory-retained-review-authority.js +17 -8
  173. package/dist/services/memory-retained-second-baseline.js +1 -1
  174. package/dist/services/memory-retained-second-repairs.js +12 -6
  175. package/dist/services/memory-review-correction-plan.js +47 -12
  176. package/dist/services/memory-review-handler.js +17 -17
  177. package/dist/services/memory-review-inputs.js +55 -9
  178. package/dist/services/memory-review-limits.js +10 -0
  179. package/dist/services/memory-review-receipt.js +2 -2
  180. package/dist/services/memory-review-registration.js +13 -4
  181. package/dist/services/memory-review-work-custody.js +3 -1
  182. package/dist/services/memory-review-work-input.js +26 -7
  183. package/dist/services/memory-review-work-results.js +26 -4
  184. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  185. package/dist/services/memory-second-correction-plan.js +2 -1
  186. package/dist/services/memory-second-repair-launch.js +4 -4
  187. package/dist/services/memory-second-repair-source.js +1 -1
  188. package/dist/services/memory-stage-dispatch.js +38 -1
  189. package/dist/services/memory-stage-results.js +113 -22
  190. package/dist/services/memory-tool-probe.js +25 -0
  191. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  192. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  193. package/dist/services/memory-upgrade-evidence.js +100 -0
  194. package/dist/services/memory-upgrade-inventory.js +442 -0
  195. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  196. package/dist/services/memory-upgrade-publication.js +413 -0
  197. package/dist/services/memory-upgrade-selection.js +430 -0
  198. package/dist/services/memory-upgrade-start.js +60 -0
  199. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  200. package/dist/services/memory-verification-acceptance.js +38 -17
  201. package/dist/services/memory-verification-checks.js +92 -27
  202. package/dist/services/memory-verification-receipt.js +6 -5
  203. package/dist/services/memory-verify-handler.js +15 -13
  204. package/dist/services/memory-work-result-json.js +12 -0
  205. package/dist/services/memory-work-result-preparation.js +5 -4
  206. package/dist/services/merge-server.js +4 -2
  207. package/dist/services/migrations.js +127 -98
  208. package/dist/services/native-daemon-history.js +2 -1
  209. package/dist/services/physical-startup.js +45 -0
  210. package/dist/services/project-registry.js +97 -0
  211. package/dist/services/project-summary.js +1 -1
  212. package/dist/services/projects.js +3 -93
  213. package/dist/services/prompt-delivery.js +71 -0
  214. package/dist/services/provider-capacity.js +112 -46
  215. package/dist/services/recovery-snapshot-database.js +32 -4
  216. package/dist/services/release-cache.js +8 -0
  217. package/dist/services/repair-intents.js +1 -12
  218. package/dist/services/repair-receipt.js +13 -0
  219. package/dist/services/run-control-worker.js +35 -18
  220. package/dist/services/run-control.js +2 -1
  221. package/dist/services/run-controller-adapter.js +2 -1
  222. package/dist/services/run-controller-process.js +4 -7
  223. package/dist/services/run-controller-recovery.js +8 -2
  224. package/dist/services/run-controller.js +215 -94
  225. package/dist/services/run-fork.js +1 -1
  226. package/dist/services/run-recovery-guard.js +30 -0
  227. package/dist/services/run-recovery.js +11 -44
  228. package/dist/services/run-start-custody.js +17 -5
  229. package/dist/services/runs.js +6 -1
  230. package/dist/services/runtime-scope-resume.js +12 -6
  231. package/dist/services/runtime-scope-stop.js +3 -2
  232. package/dist/services/runtime-scope-worker.js +46 -19
  233. package/dist/services/runtime-service.js +2 -1
  234. package/dist/services/sessions.js +3 -24
  235. package/dist/services/setup.js +63 -13
  236. package/dist/services/stage-handlers.js +22 -7
  237. package/dist/services/stage-pause.js +111 -44
  238. package/dist/services/version-status.js +1 -1
  239. package/dist/services/vnext-code.js +2 -2
  240. package/dist/services/vnext-fanout.js +51 -9
  241. package/dist/services/vnext-plan-review.js +3 -5
  242. package/dist/services/work-registry.js +117 -39
  243. package/dist/services/workspace-bootstrap.js +2 -1
  244. package/dist/services/workspace-files.js +1 -1
  245. package/dist/shared/entity-references.js +1 -8
  246. package/dist/storage/data-preservation.js +65 -0
  247. package/dist/storage/database.js +53 -19
  248. package/dist/storage/import-guard.js +84 -27
  249. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  250. package/dist/storage/paired-writer-migration.js +424 -0
  251. package/dist/storage/writer-contract.js +11 -4
  252. package/dist/storage/writer-migration.js +313 -52
  253. package/dist/storage/writer-restore.js +217 -0
  254. package/package.json +13 -6
@@ -12,19 +12,8 @@ const hybridContractRegistry = new Set(hybridContractNames);
12
12
  const record = (input) => input;
13
13
  const list = (input) => input;
14
14
  const string = (input) => input;
15
- /** Deterministic JSON content identity, not a signature or secret-derived hash. */
16
- export function hybridDigest(input) {
17
- const canonical = (value) => {
18
- if (Array.isArray(value))
19
- return value.map(canonical);
20
- if (value !== null && typeof value === "object") {
21
- return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b, "en"))
22
- .map(([key, child]) => [key, canonical(child)]));
23
- }
24
- return value;
25
- };
26
- return createHash("sha256").update(JSON.stringify(canonical(input))).digest("hex");
27
- }
15
+ import { hybridDigest } from "./hybrid-digest.js";
16
+ export { hybridDigest };
28
17
  /** E1 standalone validation. Runtime ingress must explicitly integrate it in E2/E3. */
29
18
  export function validateHybridContract(name, input, context = {}) {
30
19
  if (name === "flow-descriptor") {
@@ -260,6 +249,8 @@ export function validateHybridContract(name, input, context = {}) {
260
249
  fail("foreign_bundle_canon", "/canon");
261
250
  if (hybridDigest(data.project_pack) !== hybridDigest(context.bundleProvenance.project_pack))
262
251
  fail("foreign_bundle_project_pack", "/project_pack");
252
+ if ((data.flow_id === "mb-audit" || data.flow_id === "project-review") !== (data.project_pack === null))
253
+ fail("invalid_bundle_project_pack_role", "/project_pack");
263
254
  const memoryFlow = data.flow_id === "mb-init" || data.flow_id === "mb-upgrade";
264
255
  if (memoryFlow && context.bootstrapIdentity?.projectRoot !== context.bundleProjectRoot)
265
256
  fail("foreign_bootstrap_identity", "/bootstrap_receipts");
@@ -0,0 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Deterministic JSON content identity, not a signature or secret-derived hash. */
3
+ export function hybridDigest(input) {
4
+ const canonical = (value) => {
5
+ if (Array.isArray(value))
6
+ return value.map(canonical);
7
+ if (value !== null && typeof value === "object") {
8
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b, "en"))
9
+ .map(([key, child]) => [key, canonical(child)]));
10
+ }
11
+ return value;
12
+ };
13
+ return createHash("sha256").update(JSON.stringify(canonical(input))).digest("hex");
14
+ }
@@ -6,7 +6,9 @@ import { readSetupStatus } from "./setup.js";
6
6
  import { readBoundedReleaseFile } from "./release-source.js";
7
7
  import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
8
8
  import { admitBundledFlow } from "./stage-handlers.js";
9
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
9
10
  const limit = 1024 * 1024;
11
+ const flowIds = ["mb-init", "mb-upgrade", "mb-audit", "project-review"];
10
12
  const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
11
13
  function refuse(reason) {
12
14
  throw new AppError("installed_flow_source_invalid", "Installed flow source is not the selected verified canon", 1, { reason, phase: "prepare", effect: "no_effect" });
@@ -50,7 +52,7 @@ export class PreparedInstalledFlowSource {
50
52
  }
51
53
  static fromInstalledRelease(home, expectedInstallationSha256, flowId) {
52
54
  try {
53
- if (!path.isAbsolute(home) || !/^[a-f0-9]{64}$/.test(expectedInstallationSha256) || flowId !== "mb-init")
55
+ if (!path.isAbsolute(home) || !/^[a-f0-9]{64}$/.test(expectedInstallationSha256) || !flowIds.includes(flowId))
54
56
  refuse("invalid_selection");
55
57
  const selected = readSetupStatus(home);
56
58
  if (!selected.installation || selected.installation_readiness !== "installed")
@@ -95,6 +97,26 @@ export class PreparedInstalledFlowSource {
95
97
  folded.set(reference.path.toLowerCase(), reference.path);
96
98
  blobs.set(reference.path, Buffer.from(content));
97
99
  }
100
+ if (isDiagnosticFlow(flowId)) {
101
+ const support = ["mbb/index.md", "mbb/memory-bank-structure.md", "mbb/frontmatter-standards.md", "mbb/cross-references.md", "mbb/named-deferrals-guide.md",
102
+ ...fs.readdirSync(path.join(memoryRoot, "mbb/aspects")).filter(name => name.endsWith(".md")).map(name => `mbb/aspects/${name}`),
103
+ "dd-flow/mb-sdlc/review/aspects.md", "dd-flow/mb-sdlc/review/critics/universal.md", "dd-flow/mb-sdlc/review/critics/architectural-harmonization.md",
104
+ ...fs.readdirSync(path.join(flowRoot, "mb-audit/aspects")).filter(name => name.endsWith(".md")).map(name => `dd-flow/mb-audit/aspects/${name}`)];
105
+ for (const logical of support) {
106
+ const content = readBoundedReleaseFile(containedFile(memoryRoot, logical), limit), name = `support/${logical}`;
107
+ if (!content.length || (total += content.length) > 32 * limit || folded.has(name.toLowerCase()))
108
+ refuse("invalid_support_inventory");
109
+ new TextDecoder("utf-8", { fatal: true }).decode(content);
110
+ folded.set(name.toLowerCase(), name);
111
+ blobs.set(name, Buffer.from(content));
112
+ }
113
+ const workSchema = "schemas/diagnostic-work-result.schema.json";
114
+ const content = readBoundedReleaseFile(containedFile(flowRoot, workSchema), limit);
115
+ if (!content.length || (total += content.length) > 32 * limit || folded.has(workSchema.toLowerCase()))
116
+ refuse("invalid_support_inventory");
117
+ JSON.parse(content.toString("utf8"));
118
+ blobs.set(workSchema, Buffer.from(content));
119
+ }
98
120
  // Verify compressed/expanded archive identity again after collecting bytes.
99
121
  // A coherent descriptor+prompt edit cannot become a new trusted selection.
100
122
  const final = readSetupStatus(home);
@@ -120,6 +142,18 @@ export class PreparedInstalledFlowSource {
120
142
  * or admit its engine/storage; those remain the bootstrap owner's duties.
121
143
  */
122
144
  static restoreRetained(input, expectedSha256) {
145
+ const result = this.restoreRetainedData(input, expectedSha256);
146
+ try {
147
+ result.resolveRegisteredFlow();
148
+ return result;
149
+ }
150
+ catch {
151
+ return refuse("invalid_retained_source");
152
+ }
153
+ }
154
+ /** Data recovery before storage/engine admission. This cannot register a
155
+ * handler; productive restoration still uses restoreRetained above. */
156
+ static restoreRetainedData(input, expectedSha256) {
123
157
  try {
124
158
  if (!input || typeof input !== "object" || Array.isArray(input) || typeof expectedSha256 !== "string"
125
159
  || !/^[a-f0-9]{64}$/.test(expectedSha256))
@@ -132,7 +166,7 @@ export class PreparedInstalledFlowSource {
132
166
  || typeof value.canon.archive_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.canon.archive_sha256)
133
167
  || typeof value.canon.version !== "string" || !/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value.canon.version)
134
168
  || !value.descriptor || Object.keys(value.descriptor).sort().join() !== "path,sha256"
135
- || value.descriptor.path !== "mb-init/flow-descriptor.json" || typeof value.descriptor.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.descriptor.sha256)
169
+ || !flowIds.some(id => value.descriptor.path === `${id}/flow-descriptor.json`) || typeof value.descriptor.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.descriptor.sha256)
136
170
  || !Array.isArray(value.blobs) || value.blobs.length < 1 || value.blobs.length > 129)
137
171
  refuse("invalid_retained_source");
138
172
  const blobs = new Map(), folded = new Set();
@@ -154,10 +188,19 @@ export class PreparedInstalledFlowSource {
154
188
  const descriptor = blobs.get(value.descriptor.path);
155
189
  if (!descriptor || hash(descriptor) !== value.descriptor.sha256)
156
190
  refuse("retained_source_changed");
157
- const result = new PreparedInstalledFlowSource(null, value.installation_sha256, "mb-init", { ...value.descriptor }, { ...value.canon }, blobs);
191
+ const flowId = value.descriptor.path.slice(0, -"/flow-descriptor.json".length);
192
+ const declared = JSON.parse(descriptor.toString("utf8"));
193
+ if (validateHybridShape("flow-descriptor", declared).length || declared.id !== flowId || declared.origin !== "canon")
194
+ refuse("invalid_retained_source");
195
+ for (const stage of declared.stages)
196
+ for (const kind of ["instruction", "result_schema"]) {
197
+ const reference = stage[kind], content = blobs.get(reference.path);
198
+ if (!content || hash(content) !== reference.sha256)
199
+ refuse("retained_source_changed");
200
+ }
201
+ const result = new PreparedInstalledFlowSource(null, value.installation_sha256, flowId, { ...value.descriptor }, { ...value.canon }, blobs);
158
202
  if (result.sha256 !== expectedSha256)
159
203
  refuse("retained_source_changed");
160
- result.resolveRegisteredFlow();
161
204
  return result;
162
205
  }
163
206
  catch (error) {
@@ -8,16 +8,15 @@ import { AppError, errorRecord } from "../shared/errors.js";
8
8
  import { shortEntityReference } from "../shared/entity-references.js";
9
9
  import { resolveWorkReference } from "../storage/work-references.js";
10
10
  import { commandOption, commandPosition, continuationCommand, lifecycleCommandFromPayload, parseLifecycleCommand, shellSuffixOffset } from "./lifecycle-command.js";
11
- import { requireProjectByRoot } from "./projects.js";
12
- import { assertRunMutationAllowed, assertStageSettlementAllowed, assertWorkSettlementAllowed, recoveryGuard, recoveryPacketReceipt } from "./run-recovery.js";
11
+ import { requireProjectByRoot } from "./project-registry.js";
12
+ import { assertRunMutationAllowed, assertStageSettlementAllowed, assertWorkSettlementAllowed, recoveryGuard, recoveryPacketReceipt } from "./run-recovery-guard.js";
13
13
  import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
14
14
  import { managedProcessStatus, processIsAlive, processStartedAt } from "./managed-processes.js";
15
15
  import { appendEvent } from "./run-controller-state.js";
16
- import { recoverCommittedWorkStart } from "./work-registry.js";
17
16
  import { observeCommandHook } from "./hooks.js";
18
17
  import { runtimeHarness } from "./execution-policy.js";
19
18
  import { normalizedPathAlias, resolveContextualPaths } from "./command-context.js";
20
- import { isRegisteredRepair } from "./repair-intents.js";
19
+ import { isRegisteredRepair } from "./repair-receipt.js";
21
20
  import { stageAttempt } from "./stage-work-graph.js";
22
21
  import { contextualPathOptionsForLifecycle } from "../cli/command-inputs.js";
23
22
  import { canonicalPath } from "../storage/paths.js";
@@ -25,6 +24,9 @@ import { runtimeCommand, shellArgument } from "./runtime-command.js";
25
24
  // Process-local custody: an executing SQL row or ambient variable alone cannot
26
25
  // recreate admission after a CLI restart, or in another resource database.
27
26
  const currentAdmissions = new WeakMap();
27
+ // A same-stage finish continuation is not authority until its predecessor's
28
+ // outcome commits. Process death before that commit leaves no issued successor.
29
+ const stagedContinuations = new WeakMap();
28
30
  export function currentLifecycleAdmission(context, id) {
29
31
  const owned = currentAdmissions.get(context.db)?.get(id);
30
32
  if (!owned || context.env.DD_FLOW_CURRENT_INVOCATION !== id || owned.pid !== process.pid
@@ -380,8 +382,9 @@ export function assertLifecycleOutcomes(context, scope) {
380
382
  AND NOT EXISTS (SELECT 1 FROM lifecycle_invocations newer WHERE newer.scope_json = a.scope_json AND newer.fingerprint = a.fingerprint AND newer.rowid > a.rowid)
381
383
  ORDER BY a.rowid`, [scope.projectRoot, scope.runId, scope.generation]);
382
384
  for (const row of rows) {
383
- const error = (row.outcome_json ? JSON.parse(row.outcome_json) : null)?.error;
384
- if (typeof error?.code === "string" && !isCorrectableLifecycleRejection(error.code, error.details) && !isRegisteredRepair(context, error.details?.continuation, { projectId: project.id, runId: scope.runId }))
385
+ const outcome = row.outcome_json ? JSON.parse(row.outcome_json) : null;
386
+ const error = outcome?.error;
387
+ if (typeof error?.code === "string" && !committedSuccessor(context, row, outcome) && !isCorrectableLifecycleRejection(error.code, error.details) && !isRegisteredRepair(context, error.details?.continuation, { projectId: project.id, runId: scope.runId }))
385
388
  throw new AppError(error.code, typeof error.message === "string" ? error.message : "Mandatory lifecycle failed", 1, { ...(error.details ?? {}), invocation_id: row.id });
386
389
  }
387
390
  }
@@ -490,14 +493,27 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
490
493
  }
491
494
  }
492
495
  const registeredContinuation = error && registeredLifecycleContinuation(context, error, anchor.project_id, anchor.run_id);
493
- const retainedSuccessor = error && typeof error.details.retry_command === "string"
494
- && typeof error.details.rejection_signature === "string"
496
+ const retainedRejections = error && typeof error.details.retry_command === "string"
495
497
  && context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'lifecycle_invocations'")
496
- && context.db.get(`SELECT 1 FROM lifecycle_invocations a JOIN hook_events h ON h.event_key = a.event_key
497
- JOIN projects p ON p.id = h.project_id WHERE h.id = ? AND h.project_id = ? AND a.status = 'settled'
498
- AND json_extract(a.scope_json,'$.projectRoot') = p.root AND json_extract(a.scope_json,'$.runId') = ?
499
- AND json_extract(a.scope_json,'$.generation') = ? AND json_extract(a.outcome_json,'$.error.details.retry_command') = ?
500
- AND json_extract(a.outcome_json,'$.error.details.rejection_signature') = ?`, [anchor.hook_event_id, anchor.project_id, anchor.run_id, anchor.generation, error.details.retry_command, error.details.rejection_signature]);
498
+ ? context.db.all(`SELECT a.* FROM lifecycle_invocations a JOIN hook_events h ON h.event_key = a.event_key
499
+ WHERE h.id = ? AND h.project_id = ? AND a.status = 'settled'
500
+ AND json_extract(a.scope_json,'$.runId') = ?
501
+ AND json_extract(a.scope_json,'$.generation') = ? AND json_extract(a.outcome_json,'$.error.code') = ?
502
+ AND json_extract(a.outcome_json,'$.error.details.retry_command') = ?`, [anchor.hook_event_id, anchor.project_id, anchor.run_id, anchor.generation, error.code, error.details.retry_command]) : [];
503
+ const projectRoot = context.db.get("SELECT root FROM projects WHERE id = ?", [anchor.project_id])?.root;
504
+ const ownedRejections = retainedRejections.filter(row => {
505
+ try {
506
+ return projectRoot !== undefined && canonicalPath(JSON.parse(row.scope_json).projectRoot, false) === projectRoot;
507
+ }
508
+ catch {
509
+ return false;
510
+ }
511
+ });
512
+ const retainedRejection = ownedRejections.length === 1 ? ownedRejections[0] : undefined;
513
+ // A committed exact successor is authority; reclassifying its sanitized
514
+ // error would discard the original validated check capability again.
515
+ const retainedSuccessor = retainedRejection && (committedSuccessor(context, retainedRejection, JSON.parse(retainedRejection.outcome_json ?? "null"))
516
+ ?? (error && currentIssuedCommand(context, retainedRejection.scope_json, String(error.details.retry_command))));
501
517
  const hookOnlyArgumentDefect = error && !invocationId && !context.env.DD_FLOW_INVOCATION_SCOPE
502
518
  && !retainedSuccessor
503
519
  && ["missing_required_option", "missing_option_value", "empty_option_value", "invalid_number", "invalid_choice", "duplicate_option"].includes(String(error.details.reason));
@@ -536,7 +552,7 @@ export function observeLifecycleCommand(context, command, nativeEvent) {
536
552
  }
537
553
  }
538
554
  const correctable = error && !hookOnlyArgumentDefect && error.details.recoverable !== false && error.details.effect !== "unknown"
539
- && (registeredContinuation || error.details.effect === "no_effect" && isCorrectableLifecycleRejection(error.code, error.details)
555
+ && (registeredContinuation || retainedSuccessor || error.details.effect === "no_effect" && isCorrectableLifecycleRejection(error.code, error.details)
540
556
  && typeof error.details.retry_command === "string");
541
557
  const correlationKey = parsed.kind === "standalone" ? fingerprint(parsed.invocation)
542
558
  : JSON.stringify([auxiliary.operation, auxiliary.operation === "stage_block" ? auxiliary.args.positional[2] ?? null : auxiliary.args.options.get("run")?.[0] ?? null, auxiliary.args.options.get("stage")?.[0] ?? null]);
@@ -646,18 +662,60 @@ export function lifecycleRetryCommands(context, stage) {
646
662
  .flatMap((row) => {
647
663
  try {
648
664
  const outcome = row.outcome_json ? JSON.parse(row.outcome_json) : null;
649
- const retry = outcome?.error?.details?.retry_command;
665
+ const retry = outcome?.error?.details?.retry_command ?? outcome?.result?.next?.finish_command;
650
666
  const parsed = invocation(row.command);
651
667
  if (typeof retry !== "string" || parsed.operation !== "stage_finish" || commandOption(parsed, "stage") !== stage)
652
668
  return [];
653
- const latest = context.db.get("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? ORDER BY rowid DESC LIMIT 1", [row.scope_json, row.fingerprint]);
654
- return latest?.status === "issued" && (latest.deadline === null || latest.deadline > Date.now()) ? [retry] : [];
669
+ const successor = outcome?.successor_invocation_id
670
+ ? context.db.get("SELECT * FROM lifecycle_invocations WHERE id = ? AND scope_json = ?", [outcome.successor_invocation_id, row.scope_json])
671
+ : currentIssuedCommand(context, row.scope_json, retry);
672
+ return successor?.status === "issued" && successor.fingerprint === scopedCommandFingerprint(context, row.scope_json, retry)
673
+ && (successor.deadline === null || successor.deadline > Date.now()) ? [publicInvocationCommand(context, successor, successor.command)] : [];
655
674
  }
656
675
  catch {
657
676
  return [];
658
677
  }
659
678
  }))];
660
679
  }
680
+ function currentIssuedCommand(context, scope, command) {
681
+ const rows = context.db.all("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? AND status = 'issued'", [scope, scopedCommandFingerprint(context, scope, command)])
682
+ .filter(row => row.deadline === null || row.deadline > Date.now());
683
+ if (rows.length > 1)
684
+ throw new AppError("invocation_ambiguous", "Multiple current lifecycle successors match this command", 1);
685
+ return rows[0] ?? null;
686
+ }
687
+ function scopedCommandFingerprint(context, scope, command) {
688
+ return fingerprint(invocation(canonicalManagedCommand(context, command, JSON.parse(scope))));
689
+ }
690
+ function committedSuccessor(context, prior, outcome) {
691
+ if (typeof outcome?.successor_invocation_id !== "string" || outcome.successor_invocation_id === prior.id)
692
+ return null;
693
+ const command = outcome.error?.details?.retry_command ?? outcome.result?.next?.finish_command;
694
+ if (typeof command !== "string")
695
+ return null;
696
+ const next = context.db.get("SELECT * FROM lifecycle_invocations WHERE id = ? AND scope_json = ?", [outcome.successor_invocation_id, prior.scope_json]);
697
+ return next && next.status !== "expired" && (next.status !== "issued" || next.deadline === null || next.deadline > Date.now())
698
+ && next.fingerprint === scopedCommandFingerprint(context, prior.scope_json, command) ? next : null;
699
+ }
700
+ function invocationRecoverySnapshot(context, scope) {
701
+ return JSON.stringify(context.db.get(`SELECT g.generation,g.status,g.recovery_id FROM run_recovery_guards g
702
+ JOIN projects p ON p.id=g.project_id WHERE p.root=? AND g.run_id=? ORDER BY g.generation DESC LIMIT 1`, [scope.projectRoot, scope.runId]) ?? null);
703
+ }
704
+ /** Resolve authority, never reconstruct it from an old prompt or issue on read. */
705
+ export function currentStageFinishCommand(context, stage) {
706
+ const scope = context.env.DD_FLOW_INVOCATION_SCOPE;
707
+ if (!scope || !context.db.get("SELECT 1 FROM sqlite_master WHERE name='lifecycle_invocations'"))
708
+ return null;
709
+ const rows = context.db.all("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND status = 'issued'", [scope])
710
+ .filter(row => {
711
+ const parsed = invocation(row.command);
712
+ return parsed.operation === "stage_finish" && commandOption(parsed, "stage") === stage
713
+ && (row.deadline === null || row.deadline > Date.now());
714
+ });
715
+ if (rows.length > 1)
716
+ throw new AppError("invocation_ambiguous", "Stage has multiple current finish authorities", 1, { stage });
717
+ return rows[0] ? publicInvocationCommand(context, rows[0], rows[0].command) : null;
718
+ }
661
719
  export function managedInvocationContext(context, input) {
662
720
  const env = { ...context.env, DD_FLOW_CURRENT_INVOCATION: undefined, DD_FLOW_INVOCATION_SCOPE: undefined };
663
721
  if (!["zcode-acp", "codex-desktop", "grok-acp", "antigravity-cli", "droid-cli", "opencode-server"].includes(input.harness))
@@ -713,6 +771,21 @@ export function managedLifecycleCommand(context, command, options = {}) {
713
771
  return publicInvocationCommand(context, retained, command);
714
772
  }
715
773
  storage(context);
774
+ const predecessorId = context.env.DD_FLOW_CURRENT_INVOCATION;
775
+ const predecessor = predecessorId ? load(context, predecessorId) : null;
776
+ if (predecessor?.status === "executing" && predecessor.scope_json === configured
777
+ && parsed.operation === "stage_finish" && invocation(predecessor.command).operation === "stage_finish"
778
+ && commandOption(parsed, "stage") === commandOption(invocation(predecessor.command), "stage")) {
779
+ let staged = stagedContinuations.get(context.db);
780
+ if (!staged)
781
+ stagedContinuations.set(context.db, staged = new Map());
782
+ const prior = staged.get(predecessor.id);
783
+ if (prior && prior.fingerprint !== fingerprint(parsed))
784
+ throw new AppError("invocation_continuation_conflict", "One finish may publish only one exact successor", 1);
785
+ const next = prior ?? prepareIssuedInvocation(command, scope);
786
+ staged.set(predecessor.id, next);
787
+ return publicInvocationCommand(context, next, command);
788
+ }
716
789
  return context.db.writeTransaction(() => {
717
790
  const prior = options.freshWorkAttempt ? undefined : context.db.get("SELECT * FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? ORDER BY rowid DESC LIMIT 1", [JSON.stringify(scope), fingerprint(parsed)]);
718
791
  if (prior && prior.id !== context.env.DD_FLOW_CURRENT_INVOCATION) {
@@ -793,17 +866,35 @@ export function settleLifecycleRejection(context, id, error) {
793
866
  // Only this settlement may publish a successor; inherited help is not authority.
794
867
  delete error.details.retry_command;
795
868
  delete error.details.retry_instruction;
796
- const details = { ...error.details };
869
+ // A producer may have rendered help before throwing. Only the decision below
870
+ // may publish replacement authority for a rejected operation.
871
+ stagedContinuations.get(context.db)?.delete(id);
872
+ // Validate process-local receipt capabilities BEFORE diagnostic cloning.
873
+ // Only this local preparation crosses into the SQL commit below; public
874
+ // category fields/serialized receipts cannot recreate the decision.
875
+ const prepared = {
876
+ invocation: JSON.stringify(load(context, id)),
877
+ recovery: invocationRecoverySnapshot(context, JSON.parse(load(context, id).scope_json)),
878
+ retryable: isRetryableLifecycleRejection(error.code, error.details),
879
+ receipts: ["work_checks_failed", "code_gate_failed", "code_review_gate_failed", "merge_gate_failed"].includes(error.code) && Array.isArray(error.details.failures)
880
+ ? error.details.failures.map(receipt => ({ receipt, row: context.db.get("SELECT * FROM check_receipts WHERE id = ?", [receipt.id]) })) : []
881
+ };
882
+ if (prepared.receipts.some(({ receipt, row }) => !row || row.status !== receipt.status || row.verification_epoch !== receipt.verification_epoch || row.command !== receipt.command || row.input_hash !== receipt.input_hash))
883
+ prepared.retryable = false;
884
+ const serialized = errorRecord(error);
885
+ const details = { ...serialized.details };
797
886
  try {
798
887
  context.db.writeTransaction(() => {
799
888
  const prior = load(context, id);
800
- if (prior.status !== "executing")
801
- throw new AppError("invocation_settlement_conflict", "Attempt is no longer executing", 1, { invocation_id: id });
889
+ if (prior.status !== "executing" || JSON.stringify(prior) !== prepared.invocation
890
+ || invocationRecoverySnapshot(context, JSON.parse(prior.scope_json)) !== prepared.recovery
891
+ || prepared.receipts.some(({ receipt, row }) => JSON.stringify(context.db.get("SELECT * FROM check_receipts WHERE id = ?", [receipt.id])) !== JSON.stringify(row)))
892
+ throw new AppError("invocation_settlement_conflict", "Attempt or bound check receipts changed before settlement", 1, { invocation_id: id });
802
893
  const storageRetry = error.code === "storage_write_failed" && error.details.effect === "no_effect"
803
894
  && !context.db.get("SELECT 1 FROM lifecycle_invocations WHERE scope_json = ? AND fingerprint = ? AND json_extract(outcome_json, '$.error.details.storage_retry') = 1 LIMIT 1", [prior.scope_json, prior.fingerprint]);
804
- if (isRetryableLifecycleRejection(error.code, error.details) || storageRetry) {
805
- const failures = ["work_checks_failed", "code_gate_failed", "code_review_gate_failed", "merge_gate_failed"].includes(error.code) ? error.details.failures : undefined;
806
- const retry = retryLifecycleInvocationCommand(context, id, failures?.[0]?.id);
895
+ if (prepared.retryable || storageRetry) {
896
+ const failures = prepared.receipts.map(value => value.receipt);
897
+ const retry = retryLifecycleInvocationCommand(context, id, failures[0]?.id);
807
898
  details.retry_command = retry;
808
899
  details.effect = "no_effect";
809
900
  details.recoverable = true;
@@ -825,7 +916,9 @@ export function settleLifecycleRejection(context, id, error) {
825
916
  else {
826
917
  details.recoverable = false;
827
918
  }
828
- settleLifecycleInvocation(context, id, { error: { ...error, details } });
919
+ const successor = stagedContinuations.get(context.db)?.get(id)
920
+ ?? (typeof details.retry_command === "string" ? currentIssuedCommand(context, prior.scope_json, details.retry_command) : null);
921
+ settleLifecycleInvocation(context, id, { error: { ...serialized, details }, ...(successor ? { successor_invocation_id: successor.id } : {}) });
829
922
  });
830
923
  }
831
924
  catch (persistence) {
@@ -964,15 +1057,22 @@ export function prepareLifecycleIssuance(command, value) {
964
1057
  return { command, scope, parsed };
965
1058
  }
966
1059
  export function issueLifecycleInvocation(context, command, scope) {
967
- const { parsed } = prepareLifecycleIssuance(command, scope);
1060
+ const row = prepareIssuedInvocation(command, scope);
968
1061
  storage(context);
1062
+ insertIssuedInvocation(context, row);
1063
+ return { id: row.id, command: row.command };
1064
+ }
1065
+ function prepareIssuedInvocation(command, scope) {
1066
+ const { parsed } = prepareLifecycleIssuance(command, scope);
969
1067
  const id = crypto.randomUUID();
970
1068
  const prepared = commandWithInvocationId(command, id);
971
1069
  // Validate the actual inserted argv, including quoted heredoc boundaries.
972
- const row = { id, command: prepared, scope_json: JSON.stringify(scope), fingerprint: fingerprint(parsed) };
1070
+ const row = { id, command: prepared, scope_json: JSON.stringify(scope), fingerprint: fingerprint(parsed), status: "issued", event_key: null, identity_json: null, deadline: null, outcome_json: null };
973
1071
  checkCommand(row, prepared);
974
- context.db.run("INSERT INTO lifecycle_invocations (id, scope_json, command, fingerprint, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'issued', ?, ?)", [id, row.scope_json, prepared, row.fingerprint, context.now(), context.now()]);
975
- return { id, command: prepared };
1072
+ return row;
1073
+ }
1074
+ function insertIssuedInvocation(context, row) {
1075
+ context.db.run("INSERT INTO lifecycle_invocations (id, scope_json, command, fingerprint, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'issued', ?, ?)", [row.id, row.scope_json, row.command, row.fingerprint, context.now(), context.now()]);
976
1076
  }
977
1077
  /** Prepare outside a write transaction; the Work retry CAS owns issuance. */
978
1078
  export function prepareWorkStartAuthority(context, command, previousId) {
@@ -1930,9 +2030,40 @@ export async function awaitLifecycleInvocation(context, input) {
1930
2030
  return { outcome: JSON.parse(row.outcome_json), replay: true };
1931
2031
  if (row.status === "executing") {
1932
2032
  const scope = JSON.parse(row.scope_json);
2033
+ const parsed = invocation(row.command);
2034
+ if (parsed.operation === "stage_resume" && scope.runId) {
2035
+ input.assertCurrent(scope);
2036
+ const { recoverCommittedStageResume } = await import("./stage-pause.js");
2037
+ const result = recoverCommittedStageResume(context, { id: row.id, projectRoot: scope.projectRoot, runId: scope.runId, generation: scope.generation, stage: commandOption(parsed, "stage") ?? "", workId: commandOption(parsed, "work") ?? "" });
2038
+ if (result) {
2039
+ const project = requireProjectByRoot(context, scope.projectRoot);
2040
+ const { publishStagePacket } = await import("./stage-report-renderer.js");
2041
+ publishStagePacket(context, project.id, scope.runId, commandOption(parsed, "stage") ?? "", String(result.resume_source_path));
2042
+ const { preparePromptDelivery } = await import("./prompt-delivery.js");
2043
+ const delivered = preparePromptDelivery(result);
2044
+ const previousScope = context.env.DD_FLOW_INVOCATION_SCOPE;
2045
+ context.env.DD_FLOW_INVOCATION_SCOPE = row.scope_json;
2046
+ try {
2047
+ bindLifecyclePublishedPacket(context, String(result.worker_prompt_markdown));
2048
+ }
2049
+ finally {
2050
+ if (previousScope === undefined)
2051
+ delete context.env.DD_FLOW_INVOCATION_SCOPE;
2052
+ else
2053
+ context.env.DD_FLOW_INVOCATION_SCOPE = previousScope;
2054
+ }
2055
+ const event = row.event_key ? context.db.get("SELECT id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, row.event_key]) : null;
2056
+ if (event)
2057
+ observeLifecycleCommand(context, row.command, { project_id: project.id, id: event.id })({ result: delivered });
2058
+ const outcome = { result: delivered, effect: "committed" };
2059
+ settleLifecycleInvocation(context, row.id, outcome);
2060
+ return { outcome, replay: true };
2061
+ }
2062
+ }
1933
2063
  if (invocation(row.command).operation === "work_start" && row.event_key) {
1934
2064
  input.assertCurrent(scope);
1935
- const result = recoverCommittedWorkStart(context, row.id, row.event_key, scope.generation);
2065
+ const { recoverCommittedWorkStart } = await import("./work-registry.js");
2066
+ const result = recoverCommittedWorkStart(context, row.id, row.event_key, scope.generation, undefined, input.progress);
1936
2067
  if (result) {
1937
2068
  const outcome = { result, effect: "committed" };
1938
2069
  settleLifecycleInvocation(context, row.id, outcome);
@@ -1994,14 +2125,24 @@ export async function awaitLifecycleInvocation(context, input) {
1994
2125
  }
1995
2126
  }
1996
2127
  export function settleLifecycleInvocation(context, id, outcome) {
1997
- currentAdmissions.get(context.db)?.delete(id);
1998
- const encoded = JSON.stringify(outcome);
1999
- if (encoded === undefined)
2000
- throw new AppError("invocation_outcome_invalid", "Invocation outcome must be JSON", 1);
2001
- const changed = context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ? AND status = 'executing'", [encoded, context.now(), id]);
2002
- if (changed.changes !== 1) {
2128
+ const successor = stagedContinuations.get(context.db)?.get(id);
2129
+ context.db.writeTransaction(() => {
2003
2130
  const row = load(context, id);
2004
- if (row.status !== "settled" || row.outcome_json !== encoded)
2005
- throw new AppError("invocation_settlement_conflict", "Attempt cannot be settled with a different outcome", 1, { invocation_id: id });
2006
- }
2131
+ const retained = successor && outcome && typeof outcome === "object" ? { ...outcome, successor_invocation_id: successor.id } : outcome;
2132
+ const encoded = JSON.stringify(retained);
2133
+ if (encoded === undefined)
2134
+ throw new AppError("invocation_outcome_invalid", "Invocation outcome must be JSON", 1);
2135
+ if (row.status === "settled") {
2136
+ if (row.outcome_json !== encoded)
2137
+ throw new AppError("invocation_settlement_conflict", "Attempt cannot be settled with a different outcome", 1, { invocation_id: id });
2138
+ return;
2139
+ }
2140
+ if (row.status !== "executing")
2141
+ throw new AppError("invocation_settlement_conflict", "Attempt is no longer executing", 1, { invocation_id: id });
2142
+ if (successor)
2143
+ insertIssuedInvocation(context, successor);
2144
+ if (context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ? AND status = 'executing'", [encoded, context.now(), id]).changes !== 1)
2145
+ throw new AppError("invocation_settlement_conflict", "Lifecycle outcome changed before commit", 1, { invocation_id: id });
2146
+ });
2147
+ currentAdmissions.get(context.db)?.delete(id);
2007
2148
  }
@@ -35,15 +35,16 @@ export class PreparedAcceptedMemoryComposition {
35
35
  }
36
36
  static prepare(context, input) {
37
37
  const { scope, bundle } = input;
38
- if (!(bundle instanceof RetainedExecutionBundle) || bundle.descriptor.descriptor.id !== "mb-init")
38
+ if (!(bundle instanceof RetainedExecutionBundle) || !["mb-init", "mb-upgrade"].includes(bundle.descriptor.descriptor.id))
39
39
  refuse("registered_memory_bundle_required");
40
+ const stage = bundle.descriptor.descriptor.id === "mb-init" ? "compose" : "upgrade";
40
41
  const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), root = readDescriptorRootWork(context, scope, bundle);
41
- const registration = selectMemoryCompositionRegistration(context, { ...stageGraphScope(context, scope.projectId, scope.runId, "compose"), stage: "compose", parent_work_id: root.workId });
42
+ const registration = selectMemoryCompositionRegistration(context, { ...stageGraphScope(context, scope.projectId, scope.runId, stage), stage, parent_work_id: root.workId });
42
43
  const blueprint = PreparedMemoryResultBinding.prepareRegisteredComposition(context, { ...scope, flow: bundle.descriptor, execution: bundle.execution, bundle, acceptedReplay: true,
43
44
  registrationScope: registration.scope });
44
45
  const outputs = PreparedMemoryCompositionAcceptance.prepare(context, { scope: registration.scope, flow: bundle.descriptor, engine: bundle.engine(), binding: blueprint.binding() });
45
- const composed = readAcceptedStageData(context, { ...scope, stage: "compose" }), runRoot = run.run_root ?? run.run_dir;
46
- const binding = PreparedMemoryResultBinding.prepare(context, { ...scope, stage: "compose", flow: bundle.descriptor, execution: bundle.execution, bundle, acceptedReplay: true,
46
+ const composed = readAcceptedStageData(context, { ...scope, stage }), runRoot = run.run_root ?? run.run_dir;
47
+ const binding = PreparedMemoryResultBinding.prepare(context, { ...scope, stage, flow: bundle.descriptor, execution: bundle.execution, bundle, acceptedReplay: true,
47
48
  sources: captureMemoryCompositionSources(context, bundle, runRoot, composed.bytes, outputs), historicalEvidence: outputs.historicalEvidence() });
48
49
  if (!binding.acceptedBytes()?.equals(composed.bytes))
49
50
  refuse("accepted_composition_changed");
@@ -1,9 +1,11 @@
1
+ import { assertMemorySqlCustody } from "./memory-custody-snapshot.js";
1
2
  import { AppError } from "../shared/errors.js";
2
3
  import { hybridDigest } from "./hybrid-contracts.js";
3
4
  import { PreparedRegisteredMemoryCorrections } from "./memory-repair-registration.js";
4
5
  import { PreparedRetainedSecondMemoryRepairs } from "./memory-retained-second-repairs.js";
5
6
  import { PreparedSettledMemoryRepair } from "./memory-repair-history.js";
6
7
  import { PreparedMemoryRepairWorkspace } from "./memory-repair-workspace.js";
8
+ import { isCompletedMemoryRepairClaim } from "./memory-repair-work-results.js";
7
9
  import { reviewSourcePaths } from "./review-copy.js";
8
10
  import { readAcceptedStageData } from "./stage-report-renderer.js";
9
11
  const token = Symbol("completed-memory-correction-wave");
@@ -41,8 +43,7 @@ export function memoryCorrectionAffectedUnits(units, repairs, composition = [])
41
43
  const affected = new Set(), changed = new Set();
42
44
  for (const repair of repairs) {
43
45
  if (!repair || !Array.isArray(repair.unit_ids) || !repair.unit_ids.length || new Set(repair.unit_ids).size !== repair.unit_ids.length
44
- || repair.unit_ids.some((id) => !applicable.has(id)) || repair.result?.outcome !== "accepted"
45
- || !Array.isArray(repair.result.changes) || !repair.result.changes.length)
46
+ || repair.unit_ids.some((id) => !applicable.has(id)) || !isCompletedMemoryRepairClaim(repair.result))
46
47
  refuse("invalid_settled_repair_projection");
47
48
  for (const id of repair.unit_ids)
48
49
  affected.add(id);
@@ -138,6 +139,7 @@ const nonMemory = (entry) => entry.path !== ".memory-bank" && !entry.path.starts
138
139
  * review. Reconstruction starts at independently retained first-wave state;
139
140
  * neither current files nor a second writer's claimed before-view are anchors. */
140
141
  export class PreparedHistoricalSecondMemoryCorrectionWave {
142
+ #custodyKey = {};
141
143
  static isPrepared(value) {
142
144
  return typeof value === "object" && value !== null && #registration in value;
143
145
  }
@@ -167,11 +169,11 @@ export class PreparedHistoricalSecondMemoryCorrectionWave {
167
169
  for (const history of histories) {
168
170
  if (!prefixes.has(hybridDigest(history.before().entries())))
169
171
  refuse("unexplained_historical_input_view");
170
- baseline = baseline.composeSettledDelta(history.before(), history.after());
172
+ baseline = baseline.composeSettledDelta(history.before(), history.after(), history.result().outcome);
171
173
  prefixes.add(hybridDigest(baseline.entries()));
172
174
  }
173
175
  const batch = registration.batch(), receipt = registration.receipt(), first = registration.initialInputView();
174
- const compose = readAcceptedStageData(context, { ...input.scope, stage: "compose" });
176
+ const compose = readAcceptedStageData(context, { ...input.scope, stage: input.bundle.descriptor.descriptor.id === "mb-upgrade" ? "upgrade" : "compose" });
175
177
  const affected = memoryCorrectionAffectedUnits(memoryCorrectionCurrentUnits(first.unit_inventory, first.assignments), histories.map(history => ({ result: history.result(),
176
178
  unit_ids: batch.works.find(item => receipt.work_ids[item.key] === history.workId).payload.memory_repair.unit_ids })), JSON.parse(compose.bytes.toString("utf8")).details.works);
177
179
  const prepared = new PreparedHistoricalSecondMemoryCorrectionWave(historicalToken, registration, selected, histories, baseline, affected);
@@ -189,10 +191,13 @@ export class PreparedHistoricalSecondMemoryCorrectionWave {
189
191
  unit_ids: batch.works.find(item => receipt.work_ids[item.key] === history.workId).payload.memory_repair.unit_ids }));
190
192
  }
191
193
  assertCustody(context) {
192
- if (hybridDigest(selection(context, this.#registration)) !== hybridDigest(this.#selected))
193
- refuse("historical_second_graph_changed");
194
- for (const history of this.#histories)
195
- history.assertCustody(context);
194
+ const registration = this.#registration;
195
+ assertMemorySqlCustody(context, this.#custodyKey, () => {
196
+ if (hybridDigest(selection(context, registration)) !== hybridDigest(this.#selected))
197
+ refuse("historical_second_graph_changed");
198
+ for (const history of this.#histories)
199
+ history.assertCustody(context);
200
+ });
196
201
  }
197
202
  revalidate(context) {
198
203
  this.#registration.revalidate(context);
@@ -207,6 +212,7 @@ export class PreparedHistoricalSecondMemoryCorrectionWave {
207
212
  * from today's filesystem. This owner neither allocates reviewers nor marks
208
213
  * repair findings resolved; independent re-review must consume it separately. */
209
214
  export class PreparedCompletedMemoryCorrectionWave {
215
+ #custodyKey = {};
210
216
  static isPrepared(value) {
211
217
  return typeof value === "object" && value !== null && #selected in value;
212
218
  }
@@ -252,7 +258,7 @@ export class PreparedCompletedMemoryCorrectionWave {
252
258
  for (const history of histories) {
253
259
  if (!known.has(hybridDigest(history.before().entries())))
254
260
  refuse("unexplained_historical_input_view");
255
- baseline = baseline.composeSettledDelta(history.before(), history.after());
261
+ baseline = baseline.composeSettledDelta(history.before(), history.after(), history.result().outcome);
256
262
  known.add(hybridDigest(baseline.entries()));
257
263
  }
258
264
  const view = PreparedRetainedSecondMemoryRepairs.isPrepared(registration) ? registration.initialInputView()
@@ -272,7 +278,7 @@ export class PreparedCompletedMemoryCorrectionWave {
272
278
  if (histories.some(history => hybridDigest(history.workspaceManifest().entries().filter(nonMemory)) !== sourceSha256))
273
279
  refuse("non_memory_source_changed");
274
280
  const batch = registration.batch(), receipt = registration.receipt();
275
- const compose = readAcceptedStageData(context, { projectId: receipt.scope.project_id, runId: receipt.run_id, stage: "compose" });
281
+ const compose = readAcceptedStageData(context, { projectId: receipt.scope.project_id, runId: receipt.run_id, stage: bundle.descriptor.descriptor.id === "mb-upgrade" ? "upgrade" : "compose" });
276
282
  const dependencies = JSON.parse(compose.bytes.toString("utf8")).details.works;
277
283
  const currentUnits = PreparedRetainedSecondMemoryRepairs.isPrepared(registration)
278
284
  ? memoryCorrectionCurrentUnits(registration.initialInputView().unit_inventory, registration.initialInputView().assignments)
@@ -292,11 +298,13 @@ export class PreparedCompletedMemoryCorrectionWave {
292
298
  repairResults() { return this.#histories.map(history => ({ work_id: history.workId, result: history.result(), reference: history.reference(), authority_sha256: history.sha256 })); }
293
299
  /** Only SQL custody is inspected inside the future review-allocation writer. */
294
300
  assertCustody(context) {
295
- const selected = this.#selected;
296
- if (hybridDigest(selection(context, this.#registration)) !== hybridDigest(selected))
297
- refuse("completed_wave_custody_changed");
298
- for (const history of this.#histories)
299
- history.assertCustody(context);
301
+ assertMemorySqlCustody(context, this.#custodyKey, () => {
302
+ const selected = this.#selected;
303
+ if (hybridDigest(selection(context, this.#registration)) !== hybridDigest(selected))
304
+ refuse("completed_wave_custody_changed");
305
+ for (const history of this.#histories)
306
+ history.assertCustody(context);
307
+ });
300
308
  }
301
309
  /** Fresh allocation fence, before adding successor reviewers. General
302
310
  * custody deliberately stays valid when those admitted children exist. */