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

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 (197) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +9 -3
  5. package/dist/cli/command-metadata.js +33 -14
  6. package/dist/cli/help.js +32 -1
  7. package/dist/cli/hook-ingress.js +27 -3
  8. package/dist/cli/memory-flow-admission.js +7 -3
  9. package/dist/cli/memory-init-invocation.js +10 -3
  10. package/dist/cli/memory-init.js +74 -68
  11. package/dist/cli/memory-storage-admission.js +3 -2
  12. package/dist/cli/memory-storage-qualification.js +105 -0
  13. package/dist/cli/memory-storage-transition.js +123 -0
  14. package/dist/cli/memory-upgrade.js +189 -0
  15. package/dist/cli/native-hook-ingress.js +14 -4
  16. package/dist/cli/native-hook-preparation.js +55 -0
  17. package/dist/cli/response-file.js +2 -1
  18. package/dist/cli/run-cli.js +70 -32
  19. package/dist/cli/setup.js +2 -1
  20. package/dist/cli.js +12 -0
  21. package/dist/domain/stage-handler-identities.js +8 -0
  22. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  23. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  24. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  25. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +62 -20
  27. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  28. package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
  29. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +70 -13
  30. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  31. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  33. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  34. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +13 -6
  36. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  37. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  38. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  39. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +4 -2
  40. package/dist/harness-runtime/lib/provider-overload-policy.mjs +19 -6
  41. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  42. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  43. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  44. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  45. package/dist/schemas/release-impact.schema.json +3 -3
  46. package/dist/schemas/stage-start-response.schema.json +4 -0
  47. package/dist/services/accepted-intent.js +13 -1
  48. package/dist/services/canon.js +43 -7
  49. package/dist/services/cli-operation-classifier.js +1 -1
  50. package/dist/services/cli-upgrade-allowlist.js +1 -1
  51. package/dist/services/code-checks.js +11 -4
  52. package/dist/services/compatibility-preflight.js +5 -2
  53. package/dist/services/controller-fanout.js +21 -4
  54. package/dist/services/controller-memory-task.js +45 -0
  55. package/dist/services/controller-memory-worker.js +48 -0
  56. package/dist/services/dashboard.js +2 -2
  57. package/dist/services/descriptor-run-start.js +26 -19
  58. package/dist/services/descriptor-stage-coordinator.js +30 -10
  59. package/dist/services/descriptor-stage-start.js +40 -4
  60. package/dist/services/engines.js +64 -14
  61. package/dist/services/eval-snapshots.js +104 -93
  62. package/dist/services/external-work-launch.js +5 -3
  63. package/dist/services/flow-catalog.js +8 -4
  64. package/dist/services/frozen-engine.js +113 -11
  65. package/dist/services/hook-memory-routing.js +188 -0
  66. package/dist/services/hook-recovery-guard.js +14 -0
  67. package/dist/services/hook-session-observation.js +24 -0
  68. package/dist/services/hooks.js +27 -8
  69. package/dist/services/hybrid-contracts.js +2 -13
  70. package/dist/services/hybrid-digest.js +14 -0
  71. package/dist/services/installed-flow-source.js +25 -4
  72. package/dist/services/lifecycle-invocations.js +33 -5
  73. package/dist/services/memory-accepted-composition.js +5 -4
  74. package/dist/services/memory-completed-correction-wave.js +23 -15
  75. package/dist/services/memory-compose-handler.js +163 -21
  76. package/dist/services/memory-composition-acceptance.js +186 -10
  77. package/dist/services/memory-composition-graph.js +42 -11
  78. package/dist/services/memory-composition-registration.js +23 -8
  79. package/dist/services/memory-composition-work-custody.js +14 -9
  80. package/dist/services/memory-composition-work-results.js +15 -3
  81. package/dist/services/memory-corrected-review-batch.js +10 -3
  82. package/dist/services/memory-corrected-review-findings.js +4 -2
  83. package/dist/services/memory-corrected-review-inputs.js +10 -5
  84. package/dist/services/memory-corrected-review-receipt.js +17 -9
  85. package/dist/services/memory-corrected-review-registration.js +3 -2
  86. package/dist/services/memory-correction-check-receipt.js +20 -4
  87. package/dist/services/memory-custody-snapshot.js +36 -0
  88. package/dist/services/memory-finish-evidence.js +44 -22
  89. package/dist/services/memory-finish-handler.js +37 -23
  90. package/dist/services/memory-finish-integration.js +218 -48
  91. package/dist/services/memory-finish-readiness.js +66 -19
  92. package/dist/services/memory-git-integration.js +87 -18
  93. package/dist/services/memory-historical-corrected-review.js +21 -15
  94. package/dist/services/memory-historical-original-review.js +22 -9
  95. package/dist/services/memory-historical-repair-registration.js +14 -8
  96. package/dist/services/memory-historical-second-review.js +30 -11
  97. package/dist/services/memory-init-contexts.js +28 -2
  98. package/dist/services/memory-init-controller-start.js +19 -8
  99. package/dist/services/memory-init-git-policy.js +50 -21
  100. package/dist/services/memory-init-lint-capability.js +21 -3
  101. package/dist/services/memory-init-native-capacity.js +13 -0
  102. package/dist/services/memory-init-preflight.js +11 -1
  103. package/dist/services/memory-init-request-candidate.js +73 -17
  104. package/dist/services/memory-init-request-input.js +23 -7
  105. package/dist/services/memory-init-run-bundle.js +49 -12
  106. package/dist/services/memory-init-storage.js +17 -7
  107. package/dist/services/memory-layer.js +68 -4
  108. package/dist/services/memory-lint-config.js +111 -2
  109. package/dist/services/memory-native-review-copy.js +5 -67
  110. package/dist/services/memory-normal-flow-entry.js +12 -0
  111. package/dist/services/memory-ordinary-defaults.js +22 -5
  112. package/dist/services/memory-preflight-handler.js +15 -0
  113. package/dist/services/memory-repair-current-view.js +1 -1
  114. package/dist/services/memory-repair-document-delta.js +70 -33
  115. package/dist/services/memory-repair-history.js +12 -7
  116. package/dist/services/memory-repair-physical-binding.js +1 -119
  117. package/dist/services/memory-repair-publication-plan.js +5 -3
  118. package/dist/services/memory-repair-publication-source.js +1 -1
  119. package/dist/services/memory-repair-publication.js +3 -2
  120. package/dist/services/memory-repair-registration.js +2 -1
  121. package/dist/services/memory-repair-work-baseline.js +4 -2
  122. package/dist/services/memory-repair-work-completion.js +32 -12
  123. package/dist/services/memory-repair-work-results.js +21 -2
  124. package/dist/services/memory-repair-workspace.js +11 -2
  125. package/dist/services/memory-repair-writer-copy.js +2 -1
  126. package/dist/services/memory-result-binding.js +78 -12
  127. package/dist/services/memory-retained-review-authority.js +17 -8
  128. package/dist/services/memory-retained-second-baseline.js +1 -1
  129. package/dist/services/memory-retained-second-repairs.js +12 -6
  130. package/dist/services/memory-review-correction-plan.js +47 -12
  131. package/dist/services/memory-review-handler.js +17 -17
  132. package/dist/services/memory-review-inputs.js +55 -9
  133. package/dist/services/memory-review-limits.js +10 -0
  134. package/dist/services/memory-review-receipt.js +2 -2
  135. package/dist/services/memory-review-registration.js +13 -4
  136. package/dist/services/memory-review-work-custody.js +3 -1
  137. package/dist/services/memory-review-work-input.js +26 -7
  138. package/dist/services/memory-review-work-results.js +26 -4
  139. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  140. package/dist/services/memory-second-correction-plan.js +2 -1
  141. package/dist/services/memory-second-repair-launch.js +4 -4
  142. package/dist/services/memory-second-repair-source.js +1 -1
  143. package/dist/services/memory-stage-dispatch.js +38 -1
  144. package/dist/services/memory-stage-results.js +113 -22
  145. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  146. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  147. package/dist/services/memory-upgrade-evidence.js +100 -0
  148. package/dist/services/memory-upgrade-inventory.js +442 -0
  149. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  150. package/dist/services/memory-upgrade-publication.js +413 -0
  151. package/dist/services/memory-upgrade-selection.js +430 -0
  152. package/dist/services/memory-upgrade-start.js +60 -0
  153. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  154. package/dist/services/memory-verification-acceptance.js +38 -17
  155. package/dist/services/memory-verification-checks.js +77 -23
  156. package/dist/services/memory-verification-receipt.js +6 -5
  157. package/dist/services/memory-verify-handler.js +15 -13
  158. package/dist/services/memory-work-result-json.js +12 -0
  159. package/dist/services/memory-work-result-preparation.js +5 -4
  160. package/dist/services/merge-server.js +4 -2
  161. package/dist/services/migrations.js +127 -98
  162. package/dist/services/physical-startup.js +45 -0
  163. package/dist/services/project-registry.js +97 -0
  164. package/dist/services/project-summary.js +1 -1
  165. package/dist/services/projects.js +3 -93
  166. package/dist/services/prompt-delivery.js +71 -0
  167. package/dist/services/provider-capacity.js +69 -8
  168. package/dist/services/recovery-snapshot-database.js +32 -4
  169. package/dist/services/repair-intents.js +1 -12
  170. package/dist/services/repair-receipt.js +13 -0
  171. package/dist/services/run-control-worker.js +35 -18
  172. package/dist/services/run-controller-process.js +2 -6
  173. package/dist/services/run-controller-recovery.js +6 -1
  174. package/dist/services/run-controller.js +131 -80
  175. package/dist/services/run-recovery-guard.js +30 -0
  176. package/dist/services/run-recovery.js +11 -44
  177. package/dist/services/run-start-custody.js +17 -5
  178. package/dist/services/runtime-scope-worker.js +46 -19
  179. package/dist/services/sessions.js +3 -24
  180. package/dist/services/setup.js +63 -13
  181. package/dist/services/stage-handlers.js +9 -7
  182. package/dist/services/stage-pause.js +108 -44
  183. package/dist/services/version-status.js +1 -1
  184. package/dist/services/vnext-fanout.js +21 -9
  185. package/dist/services/vnext-plan-review.js +3 -5
  186. package/dist/services/work-registry.js +80 -33
  187. package/dist/services/workspace-files.js +1 -1
  188. package/dist/shared/entity-references.js +1 -8
  189. package/dist/storage/data-preservation.js +65 -0
  190. package/dist/storage/database.js +53 -19
  191. package/dist/storage/import-guard.js +84 -27
  192. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  193. package/dist/storage/paired-writer-migration.js +424 -0
  194. package/dist/storage/writer-contract.js +11 -4
  195. package/dist/storage/writer-migration.js +313 -52
  196. package/dist/storage/writer-restore.js +217 -0
  197. package/package.json +8 -4
@@ -8,6 +8,11 @@ import { ObservedMemorySources } from "./memory-source-observations.js";
8
8
  import { readWorkBatchCustody } from "./work-registry.js";
9
9
  import { readMemoryCompositionRegistration, readRegisteredMemoryComposition } from "./memory-composition-registration.js";
10
10
  import { validateMemoryCompositionWorkResult } from "./memory-composition-work-results.js";
11
+ import { readMemoryUpgradePublication } from "./memory-upgrade-publication-custody.js";
12
+ import { readMemoryUpgradeBootstrapReadiness } from "./memory-upgrade-bootstrap-readiness.js";
13
+ import { readUpgradeStorageReadiness } from "./memory-upgrade-storage-readiness.js";
14
+ import { readAcceptedStageData } from "./stage-report-renderer.js";
15
+ import { getProjectVersionStatus } from "./version-status.js";
11
16
  const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
12
17
  function refuse(reason) {
13
18
  throw new AppError("memory_composition_acceptance_invalid", "Composition lacks registered, settled and current output custody", 2, { reason, effect: "no_effect" });
@@ -77,17 +82,34 @@ export class PreparedMemoryCompositionAcceptance {
77
82
  #registrationSha256;
78
83
  #workers;
79
84
  #artifacts;
85
+ #semanticArtifacts;
86
+ #publication = null;
87
+ #migrationRecord = null;
88
+ #historicalEvidence = new Set();
80
89
  #observed;
81
90
  constructor(scope, original, registrationSha256, workers, artifacts, observed) {
82
91
  this.#scope = structuredClone(scope);
83
92
  this.#original = original;
84
93
  this.#registrationSha256 = registrationSha256;
85
94
  this.#workers = workers;
86
- this.#artifacts = artifacts;
95
+ this.#artifacts = structuredClone(artifacts);
96
+ this.#semanticArtifacts = structuredClone(artifacts);
87
97
  this.#observed = observed;
88
98
  Object.freeze(this);
89
99
  }
90
100
  static prepare(context, input) {
101
+ return this.#prepare(context, input, true);
102
+ }
103
+ /** Restore settled semantic Work independently of today's possibly partly
104
+ * published marker bytes. The publication owner still checks the before view
105
+ * before retaining its intent; this method does not accept current artifacts.
106
+ */
107
+ static prepareForUpgradePublication(context, input) {
108
+ if (input.scope.stage !== "upgrade")
109
+ refuse("upgrade_publication_scope_required");
110
+ return this.#prepare(context, input, false);
111
+ }
112
+ static #prepare(context, input, observe) {
91
113
  const registration = readMemoryCompositionRegistration(context, input.scope);
92
114
  const original = readRegisteredMemoryComposition(context, input), value = original.value();
93
115
  const works = value.details.works;
@@ -137,21 +159,127 @@ export class PreparedMemoryCompositionAcceptance {
137
159
  for (const record of current.values())
138
160
  for (const id of record.units)
139
161
  artifacts.set(id, [...(artifacts.get(id) ?? []), record.artifact]);
140
- const observed = ObservedMemorySources.capture(assigned, [...current.values()].map(record => ({ root: record.artifact.root, path: record.artifact.path })));
141
- const hashes = observed.hashes();
142
- if ([...current.values()].some(record => hashes.get(memorySourceKey(record.artifact.root, record.artifact.path)) !== record.artifact.sha256))
143
- refuse("current_output_bytes_changed");
144
- const prepared = new PreparedMemoryCompositionAcceptance(input.scope, original, registrationSha256, selected.map(item => ({ id: item.id, harness: item.harness, authority: item.authority, evidence: item.evidence })), artifacts, observed);
162
+ const prepared = new PreparedMemoryCompositionAcceptance(input.scope, original, registrationSha256, selected.map(item => ({ id: item.id, harness: item.harness, authority: item.authority, evidence: item.evidence })), artifacts, null);
163
+ if (input.scope.stage === "upgrade")
164
+ prepared.#historicalEvidence = new Set(input.binding.historicalEvidence);
165
+ if (observe) {
166
+ if (input.scope.stage === "upgrade")
167
+ prepared.#includePublication(context, input.binding);
168
+ prepared.#observed = ObservedMemorySources.capture(assigned, prepared.outputLocations());
169
+ const hashes = prepared.#observed.hashes();
170
+ if ([...prepared.#artifacts.values()].flat().some(artifact => hashes.get(memorySourceKey(artifact.root, artifact.path)) !== artifact.sha256))
171
+ refuse("current_output_bytes_changed");
172
+ }
145
173
  prepared.assertCustody(context);
146
174
  return prepared;
147
175
  }
176
+ #includePublication(context, binding) {
177
+ const frozen = binding.execution?.snapshot().scope;
178
+ if (!frozen)
179
+ refuse("upgrade_execution_required");
180
+ const scope = { requestId: frozen.requestId, projectRoot: frozen.projectRoot, workspaceRoot: frozen.workspace };
181
+ const ready = readMemoryUpgradeBootstrapReadiness(context, scope), storage = readUpgradeStorageReadiness(context, scope);
182
+ if (!ready || !storage?.selection || ready.authority.binding.storage_sha256 !== storage.sha256)
183
+ refuse("upgrade_bootstrap_required");
184
+ const identity = { scope: this.#scope, selection_sha256: storage.selection.sha256, execution_sha256: binding.executionSha256,
185
+ registration_sha256: this.#registrationSha256, bootstrap_sha256: hybridDigest(ready.authority), work_custody_sha256: this.custodySha256() };
186
+ const publication = readMemoryUpgradePublication(context, identity);
187
+ if (!publication?.completed)
188
+ refuse("completed_upgrade_publication_required");
189
+ const state = publication.plan.classify();
190
+ if (state.completed !== state.total)
191
+ refuse("upgrade_publication_not_current");
192
+ const actions = new Map(publication.plan.actions().map(action => [`.memory-bank/${action.path}`, action]));
193
+ for (const artifacts of this.#artifacts.values())
194
+ for (const artifact of artifacts) {
195
+ const action = artifact.root === "workspace" ? actions.get(artifact.path) : undefined;
196
+ if (!action)
197
+ continue;
198
+ // Only the exact CLI transition can advance a settled worker hash. No
199
+ // caller-provided allowlist may excuse an arbitrary changed document.
200
+ if (action.before?.sha256 !== artifact.sha256 || action.after?.kind !== "file")
201
+ refuse("unbound_semantic_output_transition");
202
+ artifact.sha256 = action.after.sha256;
203
+ }
204
+ const outputs = new Map(publication.plan.outputs().map(output => [`.memory-bank/${output.path}`, output]));
205
+ const recordPath = `.memory-bank/archive/mb-upgrade/${identity.selection_sha256}/migration.json`, record = outputs.get(recordPath);
206
+ if (!record)
207
+ refuse("upgrade_migration_record_missing");
208
+ this.#migrationRecord = { path: recordPath, sha256: record.sha256 };
209
+ const add = (unit, path, sha256) => {
210
+ const values = this.#artifacts.get(unit) ?? [], artifact = { root: "workspace", path, sha256 };
211
+ const prior = values.find(value => value.root === artifact.root && value.path === path);
212
+ if (prior && prior.sha256 !== sha256)
213
+ refuse("conflicting_mechanical_artifact");
214
+ if (!prior)
215
+ values.push(artifact);
216
+ this.#artifacts.set(unit, values);
217
+ };
218
+ const diff = readAcceptedStageData(context, { projectId: this.#scope.project_id, runId: this.#scope.run_id, stage: "diff-analysis" });
219
+ const changes = JSON.parse(diff.bytes.toString("utf8")).details.path_map;
220
+ const old = new Map(storage.selection.payload.current.inventory.inventory.entries.map(entry => [`.memory-bank/${entry.path}`, entry]));
221
+ const final = new Map(publication.plan.beforeFiles().map(entry => [entry.path, entry]));
222
+ for (const action of publication.plan.actions()) {
223
+ if (action.after)
224
+ final.set(action.path, action.after);
225
+ else
226
+ final.delete(action.path);
227
+ }
228
+ for (const entry of storage.selection.payload.current.inventory.inventory.entries) {
229
+ const name = `.memory-bank/${entry.path}`, actual = final.get(entry.path);
230
+ const semantic = changes.some(change => change.to === name && change.action === "semantic-work");
231
+ if (entry.target && (entry.ownership === "generated" || entry.ownership === "canonical" && !semantic)
232
+ && (actual?.kind !== "file" || actual.sha256 !== entry.target.sha256))
233
+ refuse("incomplete_canonical_publication");
234
+ if (entry.ownership === "canonical" && entry.action === "archive" && actual)
235
+ refuse("obsolete_canonical_path_active");
236
+ }
237
+ const status = getProjectVersionStatus({ projectRoot: frozen.workspace, rootSource: "explicit", canon: null });
238
+ if (status?.memory_bank.version !== storage.selection.release.canon.version
239
+ || status.flow_pack.canon_version_at_source_commit !== storage.selection.release.canon.version
240
+ || status.flow_pack.source_commit !== storage.selection.release.canon.commit)
241
+ refuse("upgrade_versions_inconsistent");
242
+ for (const change of changes) {
243
+ if (change.action === "retain" && change.from) {
244
+ const source = old.get(change.from)?.current;
245
+ if (source?.kind === "file")
246
+ add(change.unit_id, change.from, outputs.get(change.from)?.sha256 ?? source.sha256);
247
+ }
248
+ else if (["canonical", "generated"].includes(change.ownership) && change.action !== "semantic-work") {
249
+ if (change.to) {
250
+ const output = outputs.get(change.to) ?? final.get(change.to.slice(13));
251
+ if (!output || output.kind !== "file")
252
+ refuse("missing_mechanical_artifact");
253
+ add(change.unit_id, change.to, output.sha256);
254
+ }
255
+ if (change.from) {
256
+ const archive = `.memory-bank/archive/mb-upgrade/${identity.selection_sha256}/files/${change.from.slice(13)}.bin`, output = outputs.get(archive);
257
+ if (!output)
258
+ refuse("missing_recovery_artifact");
259
+ add(change.unit_id, archive, output.sha256);
260
+ }
261
+ }
262
+ }
263
+ for (const unit of this.#original.value().units)
264
+ if (unit.applicability === "applicable")
265
+ add(unit.id, recordPath, record.sha256);
266
+ this.#publication = { binding: identity, sha256: publication.plan.sha256 };
267
+ }
148
268
  /** Only these independently settled output locations may supplement baseline
149
269
  * evidence in the final result binding. The returned map is a defensive copy.
150
270
  */
151
- outputHashes() { return this.#observed.hashes(); }
271
+ outputHashes() { if (!this.#observed)
272
+ refuse("current_output_observation_required"); return this.#observed.hashes(); }
273
+ /** SQL-settled before-publication facts, not current filesystem witnesses. */
274
+ settledArtifacts() {
275
+ return [...this.#semanticArtifacts].flatMap(([unit_id, artifacts]) => artifacts.map(artifact => ({ unit_id, artifact: structuredClone(artifact) })));
276
+ }
277
+ custodySha256() {
278
+ return hybridDigest({ scope: this.#scope, registration_sha256: this.#registrationSha256, workers: this.#workers, artifacts: [...this.#semanticArtifacts] });
279
+ }
152
280
  historicalEvidence() {
153
281
  const units = this.#original.value().units;
154
- return new Set(units.flatMap(unit => unit.evidence.map(ref => `${memorySourceKey(ref.root, ref.path)}\0${ref.sha256}`)));
282
+ return new Set([...this.#historicalEvidence, ...units.flatMap(unit => unit.evidence.map(ref => `${memorySourceKey(ref.root, ref.path)}\0${ref.sha256}`))]);
155
283
  }
156
284
  resultEvidence() { return this.#workers.map(worker => structuredClone(worker.evidence)); }
157
285
  outputLocations() {
@@ -161,14 +289,48 @@ export class PreparedMemoryCompositionAcceptance {
161
289
  selected.set(memorySourceKey(artifact.root, artifact.path), { root: artifact.root, path: artifact.path });
162
290
  return [...selected.values()];
163
291
  }
292
+ /** Runtime projection from completed SQL publication, never an agent map.
293
+ * Keep substantive fields and immutable graph intact. Existing invented
294
+ * references are rejected rather than silently overwritten by this helper.
295
+ */
296
+ projectResult(bytes) {
297
+ if (!this.#migrationRecord)
298
+ return Buffer.from(bytes);
299
+ const value = object(Buffer.from(bytes).toString("utf8"), "invalid_result_json");
300
+ const reject = (pointer) => {
301
+ throw new AppError("memory_stage_result_invalid", "Use the selected upgrade outputs; do not invent or rerun publication", 2, { reason: "unselected_upgrade_output", phase: "prepare", effect: "no_effect", recoverable: true, result_sha256: hash(bytes), pointer });
302
+ };
303
+ if (Array.isArray(value.units))
304
+ for (const [index, unit] of value.units.entries()) {
305
+ if (!unit || typeof unit !== "object" || typeof unit.id !== "string" || !this.#original.mechanicalUnitIds().has(unit.id) && !this.#artifacts.has(unit.id))
306
+ continue;
307
+ const expected = this.#artifacts.get(unit.id) ?? [], before = this.#semanticArtifacts.get(unit.id) ?? [];
308
+ if (unit.artifacts !== undefined && !Array.isArray(unit.artifacts))
309
+ continue;
310
+ for (const reference of unit.artifacts ?? [])
311
+ if (![...expected, ...before].some(artifact => hybridDigest(artifact) === hybridDigest(reference)))
312
+ reject(`/units/${index}/artifacts`);
313
+ unit.artifacts = structuredClone(expected);
314
+ }
315
+ if (value.details && typeof value.details === "object" && !Array.isArray(value.details)) {
316
+ const details = value.details, original = this.#original.value().details;
317
+ if (details.migration_record !== undefined && ![original.migration_record, this.#migrationRecord].some(reference => hybridDigest(reference) === hybridDigest(details.migration_record)))
318
+ reject("/details/migration_record");
319
+ details.migration_record = { ...this.#migrationRecord };
320
+ }
321
+ return Buffer.from(JSON.stringify(value));
322
+ }
164
323
  assertResult(result) {
165
324
  if (!(result instanceof PreparedMemoryStageResult))
166
325
  refuse("prepared_result_required");
326
+ if (this.#scope.stage === "upgrade" && !this.#publication)
327
+ refuse("completed_upgrade_publication_required");
167
328
  const original = this.#original.value(), final = result.value(), value = final;
168
329
  const identity = ["schema_id", "run_id", "input_sha256", "predecessors"];
169
330
  if (hybridDigest(identity.map(field => original[field])) !== hybridDigest(identity.map(field => final[field])))
170
331
  refuse("foreign_result_identity");
171
- if (value.stage !== "compose" || value.outcome !== "accepted" || hybridDigest(value.details) !== hybridDigest(original.details)
332
+ const details = this.#migrationRecord ? { ...original.details, migration_record: this.#migrationRecord } : original.details;
333
+ if (value.stage !== this.#original.authoringStage() || value.outcome !== "accepted" || hybridDigest(value.details) !== hybridDigest(details)
172
334
  || value.findings.some(finding => finding.severity !== "minor" && finding.status !== "resolved"))
173
335
  refuse("composition_not_accepted_or_plan_changed");
174
336
  const inventory = new Map(original.units.map(unit => [unit.id, unit]));
@@ -193,7 +355,21 @@ export class PreparedMemoryCompositionAcceptance {
193
355
  for (const worker of this.#workers)
194
356
  if (settled(context, this.#scope, worker.id, worker.harness, this.#registrationSha256, assigned).authority !== worker.authority)
195
357
  refuse("work_settlement_changed");
358
+ if (this.#publication) {
359
+ const publication = readMemoryUpgradePublication(context, this.#publication.binding);
360
+ if (!publication?.completed || publication.plan.sha256 !== this.#publication.sha256)
361
+ refuse("publication_custody_changed");
362
+ }
196
363
  });
197
364
  }
198
- revalidate(context) { this.#observed.revalidate(); this.assertCustody(context); }
365
+ revalidate(context) {
366
+ if (this.#observed)
367
+ this.#observed.revalidate();
368
+ else {
369
+ const hashes = ObservedMemorySources.capture(roots(context, this.#scope), this.outputLocations()).hashes();
370
+ if (this.settledArtifacts().some(({ artifact }) => hashes.get(memorySourceKey(artifact.root, artifact.path)) !== artifact.sha256))
371
+ refuse("current_output_bytes_changed");
372
+ }
373
+ this.assertCustody(context);
374
+ }
199
375
  }
@@ -1,7 +1,7 @@
1
1
  import { AppError } from "../shared/errors.js";
2
2
  import { localEnvironmentAllowed } from "./workspace-files.js";
3
- function refuse(reason) {
4
- throw new AppError("memory_stage_result_invalid", "Memory composition graph cannot be dispatched", 2, { reason, phase: "prepare", effect: "no_effect" });
3
+ function refuse(reason, details = {}) {
4
+ throw new AppError("memory_stage_result_invalid", "Memory composition graph cannot be dispatched", 2, { ...details, reason, phase: "prepare", effect: "no_effect" });
5
5
  }
6
6
  // Portable scopes must remain safe on case-insensitive/normalizing filesystems.
7
7
  const physicalPath = (value) => value.normalize("NFC").toLowerCase();
@@ -9,8 +9,10 @@ const projectConfigurationPaths = [".memory-bank/dd-flow/project-execution.json"
9
9
  /** Exact project-owned defaults, never directory or case aliases of canon. */
10
10
  export function memoryProjectConfigurationPath(value) { return projectConfigurationPaths.includes(value); }
11
11
  /** Check both assigned directories and their concrete output descendants. */
12
- export function memoryCompositionOutputAllowed(value) {
13
- if (overlaps(value, ".memory-bank/dd-flow") && !memoryProjectConfigurationPath(value))
12
+ export function memoryCompositionOutputAllowed(value, upgradeWritePaths) {
13
+ if (upgradeWritePaths && !upgradeWritePaths.has(value))
14
+ return false;
15
+ if (!upgradeWritePaths && overlaps(value, ".memory-bank/dd-flow") && !memoryProjectConfigurationPath(value))
14
16
  return false;
15
17
  return value.split("/").every(part => localEnvironmentAllowed(part)
16
18
  && ![".git", "node_modules", ".dd-flow"].includes(physicalPath(part)));
@@ -23,7 +25,9 @@ function overlaps(left, right) {
23
25
  * semantic graph checks precede Work allocation, not merely worker dispatch.
24
26
  * They do not grant filesystem access or replace frozen profile/Work custody.
25
27
  */
26
- export function validateMemoryCompositionGraph(value) {
28
+ export function validateMemoryCompositionGraph(value, upgradePaths, mechanicalUnits) {
29
+ if (mechanicalUnits && !upgradePaths)
30
+ refuse("missing_upgrade_authority");
27
31
  const result = value;
28
32
  const unresolved = result.units.findIndex(unit => unit.applicability === "unknown");
29
33
  if (unresolved !== -1)
@@ -49,17 +53,24 @@ export function validateMemoryCompositionGraph(value) {
49
53
  // entry, not selected canon. Never grant their enclosing directory or
50
54
  // case/normalization aliases; the installed prompt/schema pack is sealed.
51
55
  const projectConfig = memoryProjectConfigurationPath(scope.path);
52
- if (scope.root === "workspace" && !projectConfig && [".memory-bank/mbb", ".memory-bank/dd-flow"].some(canonical => overlaps(scope.path, canonical)))
56
+ if (!upgradePaths && scope.root === "workspace" && !projectConfig && [".memory-bank/mbb", ".memory-bank/dd-flow"].some(canonical => overlaps(scope.path, canonical)))
53
57
  refuse("canonical_composition_write");
58
+ const selectedPaths = upgradePaths && scope.root === "workspace" ? new Set(work.unit_ids.flatMap(id => [...upgradePaths.get(id) ?? []])) : undefined;
54
59
  // A contained semantic scope is not permission to alter credentials,
55
60
  // repository metadata, installed dependencies or runtime state.
56
- if (!memoryCompositionOutputAllowed(scope.path))
57
- refuse("private_composition_write");
61
+ if (!memoryCompositionOutputAllowed(scope.path, selectedPaths))
62
+ refuse("private_composition_write", {
63
+ work_key: work.key, scope: { ...scope },
64
+ hint: selectedPaths && work.unit_ids.every(id => mechanicalUnits?.has(id))
65
+ ? "These units are mechanical: remove their agent Work and use memory upgrade publish after graph registration. Do not change accepted predecessor or ownership files."
66
+ : selectedPaths ? "This path is outside this Work's accepted upgrade authoring paths. Mechanical canonical/generated changes use memory upgrade publish, not agent workspace writes. Correct only the proposed scopes; keep accepted predecessor and ownership files unchanged."
67
+ : "Remove the private filesystem scope from the proposed Work; credentials, repository metadata and runtime state are not semantic outputs."
68
+ });
58
69
  if (scope.root === "run" && ["execution-bundle", "run.json"].some(authority => overlaps(scope.path, authority)))
59
70
  refuse("authority_composition_write");
60
71
  // Mechanical RUN files and another Work's evidence are never semantic outputs.
61
72
  if (scope.root === "run") {
62
- const namespace = `memory-artifacts/compose/${work.key}`;
73
+ const namespace = `memory-artifacts/${upgradePaths ? "upgrade" : "compose"}/${work.key}`;
63
74
  if (scope.path !== namespace && !scope.path.startsWith(`${namespace}/`))
64
75
  refuse("foreign_composition_run_write");
65
76
  }
@@ -107,15 +118,35 @@ export function validateMemoryCompositionGraph(value) {
107
118
  || assemblies.some(work => !declared.has(work.key)))
108
119
  refuse("invalid_assembly_membership");
109
120
  const applicable = new Set(result.units.filter(unit => unit.applicability !== "not-applicable").map(unit => unit.id));
121
+ for (const unit of mechanicalUnits ?? []) {
122
+ if (!applicable.has(unit) || upgradePaths?.has(unit))
123
+ refuse("invalid_mechanical_unit");
124
+ const assigned = works.filter(work => work.unit_ids.includes(unit));
125
+ if (assigned.length)
126
+ refuse("mechanical_unit_cannot_allocate_agent_work", {
127
+ unit_id: unit, work_keys: assigned.map(work => work.key).slice(0, 8),
128
+ hint: "Remove this unit from agent Work assignments, but keep the unit in the result inventory. CLI owns its mechanical publication through memory upgrade publish."
129
+ });
130
+ }
110
131
  for (const work of works)
111
132
  if (work.unit_ids.some(id => !applicable.has(id)))
112
133
  refuse("inapplicable_composition_work");
113
134
  for (const unit of applicable) {
135
+ if (mechanicalUnits?.has(unit))
136
+ continue;
114
137
  const authors = works.filter(work => work.unit_ids.includes(unit));
115
138
  const writers = authors.filter(work => work.role === "writer");
116
139
  const collectors = assemblies.filter(work => work.unit_ids.includes(unit));
117
- if (!authors.length || !collectors.length)
118
- refuse("incomplete_composition_coverage");
140
+ // Init assembles a new knowledge shelf. Upgrade may reconcile one existing
141
+ // document without changing its navigation; do not invent a collector Work.
142
+ // Explicit collectors still depend on every writer, in either flow.
143
+ if (!authors.length || !upgradePaths && !collectors.length)
144
+ refuse("incomplete_composition_coverage", {
145
+ unit_id: unit,
146
+ hint: upgradePaths
147
+ ? "Assign this applicable non-mechanical unit to Work. For preservation evidence only, group it into an existing evidence Work with its RUN-local output; keep accepted unit identities and workspace write scopes unchanged."
148
+ : "Assign this applicable unit to composition Work and its index-assembly collector. Keep accepted unit identities unchanged."
149
+ });
119
150
  if (collectors.some(collector => writers.some(writer => !dependsOn(collector.key, writer.key))))
120
151
  refuse("assembly_precedes_unit_writers");
121
152
  }
@@ -23,7 +23,7 @@ function readReceipt(context, scope) {
23
23
  || receipt.run_id !== scope.run_id || hybridDigest(receipt.scope) !== requestSha256 || receipt.result_schema !== workerSchema
24
24
  || [receipt.batch_sha256, receipt.compose_result_sha256, receipt.input_sha256, receipt.execution_sha256].some(hash => typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash))
25
25
  || typeof receipt.result_base64 !== "string" || !receipt.work_ids || Array.isArray(receipt.work_ids)
26
- || typeof receipt.work_ids !== "object" || !Object.keys(receipt.work_ids).length || Object.keys(receipt.work_ids).length > 4096
26
+ || typeof receipt.work_ids !== "object" || !Object.keys(receipt.work_ids).length && scope.stage !== "upgrade" || Object.keys(receipt.work_ids).length > 4096
27
27
  || Object.values(receipt.work_ids).some(id => typeof id !== "string")
28
28
  || new Set(Object.values(receipt.work_ids)).size !== Object.keys(receipt.work_ids).length)
29
29
  refuse("foreign_receipt_binding");
@@ -37,6 +37,15 @@ function readReceipt(context, scope) {
37
37
  || hybridDigest(custody.work_ids) !== hybridDigest(receipt.work_ids) || hybridDigest(custody.scope) !== requestSha256)
38
38
  refuse("changed_work_custody");
39
39
  }
40
+ if (!Object.keys(receipt.work_ids).length) {
41
+ // No invented worker for deterministic-only upgrade. Its empty batch
42
+ // still has the same independent allocator receipt and active scope.
43
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='work.batch_registration' AND json_extract(payload_json,'$.request_sha256')=? LIMIT 2", [scope.project_id, requestSha256]);
44
+ if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 32768 || hybridDigest(JSON.parse(rows[0].payload_json)) !== hybridDigest({
45
+ run_id: scope.run_id, scope, request_sha256: requestSha256, batch_sha256: hybridDigest([]), work_ids: {}, work_sha256: {}
46
+ }) || receipt.batch_sha256 !== hybridDigest([]))
47
+ refuse("changed_empty_batch_custody");
48
+ }
40
49
  return receipt;
41
50
  }
42
51
  catch (error) {
@@ -85,24 +94,28 @@ export function selectMemoryCompositionRegistration(context, scope) {
85
94
  export function registerMemoryComposition(context, input, assertHandlerCustody) {
86
95
  if (!(input.composition instanceof PreparedMemoryStageResult))
87
96
  refuse("foreign_prepared_result");
97
+ const stage = input.composition.authoringStage();
88
98
  const value = input.composition.compositionBatch({ ...input.binding, resultSchema: workerSchema });
89
99
  const bytes = input.composition.bytes();
90
- const batchInput = { parentWorkId: input.parentWorkId, requestKey: input.requestKey, stage: "compose", value };
91
- const prepared = prepareWorkBatch(context, batchInput);
100
+ const batchInput = { parentWorkId: input.parentWorkId, requestKey: input.requestKey, stage, value };
101
+ // Generic CLI batches remain non-empty. The admitted upgrade graph alone
102
+ // may register zero agent tasks; addWorkBatch still checks parent/run/stage
103
+ // and records exact batch custody in the existing writer transaction.
104
+ const prepared = stage === "upgrade" && !value.works.length ? [] : prepareWorkBatch(context, batchInput);
92
105
  return context.db.writeTransaction(() => {
93
106
  assertHandlerCustody?.(context);
94
107
  const parent = context.db.get("SELECT project_id, run_id FROM works WHERE work_id = ?", [input.parentWorkId]);
95
108
  if (!parent || parent.run_id !== input.binding.runId)
96
109
  refuse("foreign_coordinator");
97
- const scope = { ...stageGraphScope(context, parent.project_id, parent.run_id, "compose"),
98
- stage: "compose", parent_work_id: input.parentWorkId, request_key: input.requestKey };
110
+ const scope = { ...stageGraphScope(context, parent.project_id, parent.run_id, stage),
111
+ stage, parent_work_id: input.parentWorkId, request_key: input.requestKey };
99
112
  // One immutable blueprint per coordinator/attempt. A different request key
100
113
  // cannot create another productive graph or bypass a lost original receipt.
101
- const originals = context.db.all("SELECT json_extract(payload_json,'$.request_sha256') AS request_sha256 FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')='compose' AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? AND json_extract(payload_json,'$.scope.parent_work_id')=? LIMIT 2", [scope.project_id, scope.run_id, scope.attempt, scope.cycle, scope.parent_work_id]);
114
+ const originals = context.db.all("SELECT json_extract(payload_json,'$.request_sha256') AS request_sha256 FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')=? AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? AND json_extract(payload_json,'$.scope.parent_work_id')=? LIMIT 2", [scope.project_id, scope.run_id, stage, scope.attempt, scope.cycle, scope.parent_work_id]);
102
115
  if (originals.length > 1 || originals.length === 1 && originals[0].request_sha256 !== hybridDigest(scope))
103
116
  refuse("original_composition_already_registered");
104
117
  const prior = readReceipt(context, scope);
105
- if (!prior && context.db.get("SELECT 1 FROM works w JOIN stage_work_memberships m ON m.work_id=w.work_id AND m.project_id=w.project_id AND m.run_id=w.run_id WHERE w.project_id=? AND w.run_id=? AND w.parent_work_id=? AND w.result_schema=? AND m.stage='compose' AND m.attempt=? AND m.cycle=? LIMIT 1", [scope.project_id, scope.run_id, scope.parent_work_id, workerSchema, scope.attempt, scope.cycle]))
118
+ if (!prior && context.db.get("SELECT 1 FROM works w JOIN stage_work_memberships m ON m.work_id=w.work_id AND m.project_id=w.project_id AND m.run_id=w.run_id WHERE w.project_id=? AND w.run_id=? AND w.parent_work_id=? AND w.result_schema=? AND m.stage=? AND m.attempt=? AND m.cycle=? LIMIT 1", [scope.project_id, scope.run_id, scope.parent_work_id, workerSchema, stage, scope.attempt, scope.cycle]))
106
119
  refuse("composition_receipt_missing");
107
120
  if (prior && (prior.compose_result_sha256 !== input.composition.sha256 || prior.batch_sha256 !== hybridDigest(value.works)
108
121
  || prior.input_sha256 !== input.binding.inputSha256 || prior.execution_sha256 !== input.binding.executionSha256))
@@ -133,10 +146,12 @@ export function readRegisteredMemoryComposition(context, input) {
133
146
  const receipt = readReceipt(context, input.scope);
134
147
  if (!receipt)
135
148
  refuse("composition_receipt_missing");
136
- if (input.binding.runId !== input.scope.run_id || input.binding.stage !== "compose"
149
+ if (input.binding.runId !== input.scope.run_id || input.binding.stage !== input.scope.stage
137
150
  || input.binding.inputSha256 !== receipt.input_sha256 || input.binding.executionSha256 !== receipt.execution_sha256)
138
151
  refuse("foreign_restore_binding");
139
152
  const result = PreparedMemoryStageResult.prepare(Buffer.from(receipt.result_base64, "base64"), input.flow, input.binding);
153
+ if (result.authoringStage() !== input.scope.stage)
154
+ refuse("foreign_restore_binding");
140
155
  const batch = result.compositionBatch({ runId: input.binding.runId, inputSha256: receipt.input_sha256,
141
156
  executionSha256: receipt.execution_sha256, resultSchema: workerSchema });
142
157
  if (result.sha256 !== receipt.compose_result_sha256 || hybridDigest(batch.works) !== receipt.batch_sha256
@@ -46,10 +46,11 @@ export class PreparedRegisteredMemoryCompositionWorkResult {
46
46
  */
47
47
  static prepareFromStageCustody(context, input, engine, binding) {
48
48
  const custody = readWorkBatchCustody(context, binding.workId);
49
+ const stage = binding.flow.descriptor.id === "mb-init" ? "compose" : binding.flow.descriptor.id === "mb-upgrade" ? "upgrade" : refuse();
49
50
  const selected = binding.bundle ? PreparedMemoryResultBinding.prepareRegisteredComposition(context, { ...binding, bundle: binding.bundle,
50
- registrationScope: { project_id: binding.projectId, run_id: binding.runId, stage: "compose", attempt: custody.scope.attempt, cycle: custody.scope.cycle,
51
+ registrationScope: { project_id: binding.projectId, run_id: binding.runId, stage, attempt: custody.scope.attempt, cycle: custody.scope.cycle,
51
52
  parent_work_id: binding.coordinatorWorkId, request_key: binding.requestKey } })
52
- : binding.sources ? PreparedMemoryResultBinding.prepare(context, { ...binding, sources: binding.sources, stage: "compose" }) : refuse();
53
+ : binding.sources ? PreparedMemoryResultBinding.prepare(context, { ...binding, sources: binding.sources, stage }) : refuse();
53
54
  const prepared = this.prepareFromRegistration(context, input, engine, { ...binding, resultBinding: selected.binding() });
54
55
  prepared.#stageBinding = selected;
55
56
  prepared.assertCustody(context);
@@ -57,11 +58,12 @@ export class PreparedRegisteredMemoryCompositionWorkResult {
57
58
  }
58
59
  static prepareFromRegistration(context, input, engine, binding) {
59
60
  const custody = readWorkBatchCustody(context, binding.workId);
60
- if (custody.scope.stage !== "compose" || custody.scope.parent_work_id !== binding.coordinatorWorkId
61
+ const stage = binding.flow.descriptor.id === "mb-init" ? "compose" : binding.flow.descriptor.id === "mb-upgrade" ? "upgrade" : refuse();
62
+ if (custody.scope.stage !== stage || custody.scope.parent_work_id !== binding.coordinatorWorkId
61
63
  || custody.scope.request_key !== binding.requestKey || !binding.resultBinding.executionSha256)
62
64
  refuse();
63
65
  const scope = { project_id: custody.scope.project_id, run_id: custody.scope.run_id,
64
- stage: "compose", attempt: custody.scope.attempt, cycle: custody.scope.cycle,
66
+ stage, attempt: custody.scope.attempt, cycle: custody.scope.cycle,
65
67
  parent_work_id: binding.coordinatorWorkId, request_key: binding.requestKey };
66
68
  const registrationSha256 = memoryCompositionRegistrationDigest(context, scope);
67
69
  const composition = readRegisteredMemoryComposition(context, { scope, flow: binding.flow, binding: binding.resultBinding });
@@ -75,7 +77,7 @@ export class PreparedRegisteredMemoryCompositionWorkResult {
75
77
  static prepare(context, input, engine, binding) {
76
78
  const custody = readWorkBatchCustody(context, binding.workId);
77
79
  if (!(binding.composition instanceof PreparedMemoryStageResult) || binding.composition.sha256 !== binding.compositionSha256
78
- || custody.scope.run_id !== binding.runId || custody.scope.stage !== "compose"
80
+ || custody.scope.run_id !== binding.runId || custody.scope.stage !== binding.composition.authoringStage()
79
81
  || custody.scope.parent_work_id !== binding.coordinatorWorkId || custody.scope.request_key !== binding.requestKey)
80
82
  refuse();
81
83
  const batch = binding.composition.compositionBatch({ ...binding, resultSchema: "dd-flow/memory-composition-work-result@1" });
@@ -116,11 +118,14 @@ export class PreparedRegisteredMemoryCompositionWorkResult {
116
118
  const scope = this.#custody.scope;
117
119
  const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [scope.project_id, scope.run_id]);
118
120
  const index = run ? JSON.parse(run.index_json) : null;
121
+ const authoringStage = scope.stage;
122
+ if (!["compose", "upgrade"].includes(authoringStage))
123
+ refuse();
119
124
  assertRunBundleSqlAuthority(context, { projectId: scope.project_id, runId: scope.run_id }, index?.execution_bundle);
120
- const stage = index?.stage_runs?.findLast((item) => item.stage === "compose" && item.status === "running");
121
- if (!this.#registration || !this.#stageBinding || index?.current_stage !== "compose" || !stage || (stage.attempt ?? "try-001") !== scope.attempt
122
- || !stageWorkIds(context, scope.project_id, scope.run_id, "compose").has(this.workId)
123
- || !stageWorkIds(context, scope.project_id, scope.run_id, "compose").has(scope.parent_work_id)
125
+ const stage = index?.stage_runs?.findLast((item) => item.stage === authoringStage && item.status === "running");
126
+ if (!this.#registration || !this.#stageBinding || index?.current_stage !== authoringStage || !stage || (stage.attempt ?? "try-001") !== scope.attempt
127
+ || !stageWorkIds(context, scope.project_id, scope.run_id, authoringStage).has(this.workId)
128
+ || !stageWorkIds(context, scope.project_id, scope.run_id, authoringStage).has(scope.parent_work_id)
124
129
  || !context.db.get("SELECT 1 FROM work_sessions WHERE id = ? AND work_id = ? AND session_id = ? AND status = 'running'", [workSessionId, this.workId, sessionId])
125
130
  || !context.db.get("SELECT 1 FROM works WHERE work_id = ? AND project_id = ? AND run_id = ? AND status = 'running'", [scope.parent_work_id, scope.project_id, scope.run_id])
126
131
  || !context.db.get("SELECT 1 FROM sessions s JOIN runs r ON r.project_id = s.project_id AND r.id = s.run_id WHERE s.project_id = ? AND s.session_id = ? AND s.run_id = ? AND s.worker_id = ? AND s.harness = ? AND s.status = 'active' AND s.current_stage = 'work' AND s.project_root = r.project_root AND s.workspace_path = r.workspace_root", [scope.project_id, sessionId, scope.run_id, this.workId, this.#harness])) {
@@ -5,7 +5,7 @@ import { AdmittedFrozenEngine } from "./frozen-engine.js";
5
5
  import { hybridDigest } from "./hybrid-contracts.js";
6
6
  import { PreparedMemoryStageResult, memorySourceKey } from "./memory-stage-results.js";
7
7
  import { ObservedMemorySources } from "./memory-source-observations.js";
8
- import { memoryCompositionOutputAllowed } from "./memory-composition-graph.js";
8
+ import { parseMemoryWorkResultProposal } from "./memory-work-result-json.js";
9
9
  const schemaId = "dd-flow/memory-composition-work-result@1";
10
10
  function refuse(reason) {
11
11
  throw new AppError("memory_composition_work_result_invalid", "Composition Work result lacks assigned output proof", 2, { reason, phase: "prepare", effect: "no_effect" });
@@ -20,7 +20,7 @@ export function validateMemoryCompositionWorkResult(input, engine, binding) {
20
20
  || !(engine instanceof AdmittedFrozenEngine) || !(binding.composition instanceof PreparedMemoryStageResult)
21
21
  || binding.composition.sha256 !== binding.compositionSha256)
22
22
  refuse("foreign_preparation_binding");
23
- const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input));
23
+ const value = parseMemoryWorkResultProposal(input, "memory_composition_work_result_invalid");
24
24
  const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(engine.schema("memory-composition-work-result")));
25
25
  if (schema.$id !== schemaId || !new Ajv2020({ strict: true, validateFormats: false }).compile(schema)(value))
26
26
  refuse("invalid_result_schema");
@@ -43,7 +43,7 @@ export function validateMemoryCompositionWorkResult(input, engine, binding) {
43
43
  refuse("unsafe_artifact_path");
44
44
  if (!packet.write_scopes.some(scope => scope.root === artifact.root && (logical === scope.path || logical.startsWith(`${scope.path}/`))))
45
45
  refuse("artifact_outside_write_scope");
46
- if (!memoryCompositionOutputAllowed(logical))
46
+ if (!binding.composition.allowsCompositionOutput(artifact.root, logical))
47
47
  refuse("private_composition_artifact");
48
48
  const alias = memorySourceKey(artifact.root, logical.normalize("NFC").toLowerCase());
49
49
  const identity = hybridDigest({ path: logical, sha256: artifact.sha256 });
@@ -80,6 +80,18 @@ export class PreparedMemoryCompositionWorkResult {
80
80
  for (const artifact of artifacts) {
81
81
  selected.set(memorySourceKey(artifact.root, artifact.path), { root: artifact.root, path: artifact.path });
82
82
  }
83
+ // Upgrade scopes are exact admitted file paths, not init-style output
84
+ // directories. Retain their output hashes while this Work is still
85
+ // correctable; publication cannot repair an already settled omission.
86
+ if (binding.composition.authoringStage() === "upgrade") {
87
+ const work = binding.composition.compositionBatch({ ...binding, resultSchema: schemaId }).works.find(item => item.key === binding.workKey);
88
+ const missing = work.payload.memory_composition.write_scopes
89
+ .filter(scope => scope.root === "workspace" && !selected.has(memorySourceKey(scope.root, scope.path)))
90
+ .map(scope => scope.path);
91
+ if (missing.length)
92
+ throw new AppError("memory_composition_work_result_invalid", "Include every assigned upgrade workspace file with its current hash in this Work result; correct only the result proposal, not settled peer Work or retained inputs", 2, { reason: "missing_workspace_outputs", phase: "prepare", effect: "no_effect", recoverable: true,
93
+ missing_count: missing.length, missing_paths: missing.slice(0, 32), result_sha256: createHash("sha256").update(bytes).digest("hex") });
94
+ }
83
95
  const observed = ObservedMemorySources.capture(binding.roots, [...selected.values()]), hashes = observed.hashes();
84
96
  if (artifacts.some(artifact => hashes.get(memorySourceKey(artifact.root, artifact.path)) !== artifact.sha256))
85
97
  refuse("changed_artifact_bytes");
@@ -1,6 +1,7 @@
1
1
  import { AppError } from "../shared/errors.js";
2
2
  import { hybridDigest } from "./hybrid-contracts.js";
3
3
  import { memoryReviewWorkerSchema } from "./memory-review-registration.js";
4
+ import { memoryReviewWaveByteLimit } from "./memory-review-limits.js";
4
5
  /** Payload projection only. Fresh check, registered input/copy/Session and
5
6
  * independent Work settlement are not supplied by a coordinator's JSON. */
6
7
  export function memoryCorrectedReviewBatch(view) {
@@ -10,20 +11,26 @@ export function memoryCorrectedReviewBatch(view) {
10
11
  throw new AppError("memory_corrected_review_batch_invalid", "Corrected reviewer batch lacks bounded current assignments", 2, { effect: "no_effect" });
11
12
  }
12
13
  let total = 0;
14
+ const inputSha256 = hybridDigest(view);
13
15
  return { works: view.assignments.map((assignment, index) => {
14
16
  const key = `review-${String(index + 1).padStart(3, "0")}`;
17
+ const inputPaths = view.upgrade_baseline ? new Set(assignment.inputs.map(ref => `${ref.root}\0${ref.path}`)) : null;
15
18
  const packet = { schema_id: "dd-flow/memory-review-work-packet@2", run_id: view.scope.run_id,
16
19
  stage: "review", wave: view.wave, key, unit: structuredClone(assignment.unit), unit_inputs: structuredClone(assignment.inputs),
17
20
  original_unit_sha256: assignment.original_unit_sha256, deleted_references: structuredClone(assignment.deleted_references),
18
21
  completed_correction_sha256: view.completed_correction_sha256, repairs: structuredClone(view.repairs),
19
- review_input_sha256: hybridDigest(view), result_schema: memoryReviewWorkerSchema,
20
- instruction: structuredClone(view.instruction) };
22
+ review_input_sha256: inputSha256, result_schema: memoryReviewWorkerSchema,
23
+ instruction: structuredClone(view.instruction),
24
+ ...(view.upgrade_baseline ? {
25
+ historical_inputs: view.upgrade_baseline.entries.filter(entry => inputPaths.has(`${entry.retained.root}\0${entry.retained.path}`)),
26
+ preservation_invariants: (view.preservation_invariants ?? []).filter(item => item.unit_id === assignment.unit.id)
27
+ } : {}) };
21
28
  const work = { key, task: `Independently review the assigned knowledge unit ${JSON.stringify(assignment.unit.id)}. Read the frozen review instruction in memory_review as review criteria, not permission to coordinate or repair. Check grounding, evidence, coverage and cross-file consistency using the registered wave inputs. Assigned unit.evidence may retain historical source versions; cite those exact versions only as historical context, never as bytes read from or current in this copy. Direct assessment and findings must still cite the assigned current unit.artifacts/unit_inputs. Do not edit project documents, allocate Work or repair findings. Report concrete material defects with unit/source evidence; do not block on taste. Return only the assigned memory-review-work-result JSON; do not claim other units or Stage completion.`,
22
29
  depends_on: [], launch_policy: "fresh_agent_required", result_schema: memoryReviewWorkerSchema,
23
30
  payload: { kind: "memory-review", read_only: true, memory_review: { ...packet, packet_sha256: hybridDigest(packet) } } };
24
31
  const size = Buffer.byteLength(JSON.stringify(work));
25
32
  total += size;
26
- if (size > 1024 * 1024 || total > 64 * 1024 * 1024)
33
+ if (size > 1024 * 1024 || total > memoryReviewWaveByteLimit)
27
34
  throw new AppError("memory_corrected_review_batch_invalid", "Corrected reviewer batch exceeds its bound", 2, { effect: "no_effect" });
28
35
  return work;
29
36
  }) };
@@ -1,6 +1,7 @@
1
1
  import { AppError } from "../shared/errors.js";
2
2
  import { hybridDigest } from "./hybrid-contracts.js";
3
3
  import { memoryReviewWaveProjection } from "./memory-review-acceptance.js";
4
+ import { isCompletedMemoryRepairClaim } from "./memory-repair-work-results.js";
4
5
  function refuse(reason) {
5
6
  throw new AppError("memory_corrected_review_findings_invalid", "Finding resolution lacks causal repair and independent re-review references", 2, { reason, effect: "no_effect" });
6
7
  }
@@ -71,12 +72,13 @@ export function correctedMemoryReviewFindings(input) {
71
72
  const previousIds = new Set(previous.findings.map(finding => finding.id)), claims = new Map();
72
73
  for (const repair of input.repairs) {
73
74
  const reference = ref(repair.reference);
74
- if (repair.result.outcome !== "accepted" || !repair.result.changes.length || !repair.unit_ids.length)
75
+ if (!isCompletedMemoryRepairClaim(repair.result) || !repair.unit_ids.length)
75
76
  refuse("unsettled_repair_claim");
76
77
  for (const resolution of repair.result.resolutions) {
77
78
  const finding = previous.findings.find(finding => finding.id === resolution.finding_id);
78
79
  if (!finding || finding.status !== "open" || !repair.unit_ids.includes(finding.unit_id) || claims.has(finding.id)
79
- || !resolution.delta_paths.length || resolution.delta_paths.some(path => !repair.result.changes.some(change => change.path === path && change.before_sha256 !== change.after_sha256)))
80
+ || repair.result.outcome !== "reconciled" && !resolution.delta_paths.length
81
+ || resolution.delta_paths.some(path => !repair.result.changes.some(change => change.path === path && change.before_sha256 !== change.after_sha256)))
80
82
  refuse("foreign_or_noncausal_resolution");
81
83
  claims.set(finding.id, { repair, reference });
82
84
  }