@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
@@ -18,7 +18,7 @@ const primaryKeys = {
18
18
  };
19
19
  function refuse(reason) { throw new AppError("memory_init_storage_incompatible", "Executing CLI cannot admit memory-init runtime storage without migration or repair", 1, { reason, effect: "no_effect" }); }
20
20
  const normalize = (sql) => sql.replace(/\s+/g, " ").trim().replace(/;$/, "");
21
- function inspect(context) {
21
+ function inspect(context, upgrade = false) {
22
22
  if (!context.db.writable || !path.isAbsolute(context.ddFlowHome) || path.resolve(context.db.path) !== path.join(path.resolve(context.ddFlowHome), "db.sqlite"))
23
23
  refuse("foreign_or_readonly_store");
24
24
  const contract = context.db.all("SELECT id, version FROM dd_flow_writer_contract");
@@ -26,14 +26,15 @@ function inspect(context) {
26
26
  || context.db.get("SELECT dd_flow_writer_version() AS version")?.version !== STORE_WRITER_VERSION)
27
27
  refuse("writer_contract_mismatch");
28
28
  const schema = {};
29
- for (const [table, required] of Object.entries(footprint)) {
29
+ const selectedFootprint = upgrade ? { ...footprint, runtime_config: ["key", "value_json", "created_at", "updated_at"] } : footprint;
30
+ for (const [table, required] of Object.entries(selectedFootprint)) {
30
31
  const columns = context.db.all(`PRAGMA table_info("${table}")`);
31
32
  if (required.some(name => !columns.some(column => column.name === name)))
32
33
  refuse(`missing_operation_column:${table}`);
33
34
  if (required.some(name => columns.find(column => column.name === name)?.type.toUpperCase() !== (table === "audit_events" && ["id", "forced"].includes(name) ? "INTEGER" : "TEXT")))
34
35
  refuse(`operation_column_type_mismatch:${table}`);
35
36
  const key = columns.filter(column => column.pk > 0).sort((a, b) => a.pk - b.pk).map(column => column.name);
36
- if (JSON.stringify(key) !== JSON.stringify(primaryKeys[table]))
37
+ if (JSON.stringify(key) !== JSON.stringify(table === "runtime_config" ? ["key"] : primaryKeys[table]))
37
38
  refuse(`operation_primary_key_mismatch:${table}`);
38
39
  const fences = [];
39
40
  for (const action of ["INSERT", "UPDATE", "DELETE"]) {
@@ -56,14 +57,23 @@ function inspect(context) {
56
57
  export class PreparedMemoryInitRuntimeStorage {
57
58
  sha256;
58
59
  #facts;
59
- constructor(facts) { this.#facts = facts; this.sha256 = hybridDigest(facts); Object.freeze(this); }
60
+ #upgrade;
61
+ constructor(facts, upgrade) { this.#facts = facts; this.#upgrade = upgrade; this.sha256 = hybridDigest(facts); Object.freeze(this); }
60
62
  static prepare(context) {
63
+ return this.prepareFor(context, false);
64
+ }
65
+ /** Upgrade reuses the RUN/Work footprint and additionally needs retained
66
+ * request custody. This remains a non-migrating connection observation. */
67
+ static prepareUpgrade(context) {
68
+ return this.prepareFor(context, true);
69
+ }
70
+ static prepareFor(context, upgrade) {
61
71
  try {
62
72
  return context.db.readSnapshot(() => {
63
- const facts = inspect(context), integrity = context.db.all("PRAGMA quick_check");
73
+ const facts = inspect(context, upgrade), integrity = context.db.all("PRAGMA quick_check");
64
74
  if (integrity.length !== 1 || integrity[0]?.quick_check !== "ok" || context.db.all("PRAGMA foreign_key_check").length)
65
75
  refuse("storage_integrity_failed");
66
- return new PreparedMemoryInitRuntimeStorage(facts);
76
+ return new PreparedMemoryInitRuntimeStorage(facts, upgrade);
67
77
  });
68
78
  }
69
79
  catch (error) {
@@ -75,7 +85,7 @@ export class PreparedMemoryInitRuntimeStorage {
75
85
  facts() { return { ...this.#facts }; }
76
86
  /** Writer-safe metadata fence; expensive integrity scan remains preparation. */
77
87
  assertCompatibility(context) {
78
- if (hybridDigest(inspect(context)) !== this.sha256)
88
+ if (hybridDigest(inspect(context, this.#upgrade)) !== this.sha256)
79
89
  refuse("storage_changed_after_preparation");
80
90
  }
81
91
  }
@@ -1,12 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { spawnSync } from "node:child_process";
5
4
  import { createHash } from "node:crypto";
6
5
  import { AppError } from "../shared/errors.js";
7
6
  import { hybridDigest } from "./hybrid-contracts.js";
8
7
  import { PreparedMemoryInitTarget } from "./memory-init-preflight.js";
9
8
  import { readBoundedReleaseFile } from "./release-source.js";
9
+ import { runMemoryToolProbe } from "./memory-tool-probe.js";
10
10
  const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
11
11
  function refuse(reason, diagnostics) {
12
12
  throw new AppError("memory_init_worktrunk_capability_failed", "Selected Worktrunk cannot create the required workspace", 1, { reason, effect: "no_effect", next_action: "repair_prerequisite_then_retry", ...(diagnostics ? { tool_probe: diagnostics } : {}) });
@@ -83,14 +83,10 @@ export class PreparedMemoryInitWorktrunkCapability {
83
83
  const run = (file, args) => {
84
84
  ownRoot();
85
85
  unchanged();
86
- const started = performance.now();
87
- const result = spawnSync(file, args, { cwd: repo, env: probeEnv, shell: false, windowsHide: true, timeout: 5000, maxBuffer: 256 * 1024 });
86
+ const { result, diagnostics } = runMemoryToolProbe(file, args, { cwd: repo, env: probeEnv });
88
87
  if (result.error || result.signal || result.status !== 0)
89
88
  refuse("worktrunk_probe_execution_failed", {
90
- name: path.basename(file), phase: "workspace_probe", timeout_ms: 5000, elapsed_ms: Math.round(performance.now() - started),
91
- status: result.status, signal: result.signal, error_code: result.error?.code ?? null,
92
- timed_out: result.error?.code === "ETIMEDOUT",
93
- stdout_bytes: result.stdout?.length ?? null, stderr_bytes: result.stderr?.length ?? null
89
+ ...diagnostics, name: path.basename(file), phase: "workspace_probe", expected_status: 0
94
90
  });
95
91
  ownRoot();
96
92
  unchanged();
@@ -5,13 +5,19 @@ import { Ajv } from "ajv/dist/ajv.js";
5
5
  import semver from "semver";
6
6
  import { AppError } from "../shared/errors.js";
7
7
  import { hybridDigest } from "./hybrid-contracts.js";
8
- import { readSetupStatus } from "./setup.js";
8
+ import { readSetupStatus, readCommittedCanonRelease } from "./setup.js";
9
9
  import { readBoundedReleaseFile } from "./release-source.js";
10
10
  import { normalizeFlowContract } from "../domain/flow-contract.js";
11
+ import { inspectReleaseArchive } from "./release-archive.js";
12
+ import { canonicalOnlyFlowNames } from "./version-status.js";
13
+ export function isCanonicalOnlyMemoryPath(logical) {
14
+ return logical.startsWith("dd-flow/project-review/")
15
+ || canonicalOnlyFlowNames.some(name => logical === `dd-flow/${name}` || logical.startsWith(`dd-flow/${name.slice(0, -3)}/`));
16
+ }
11
17
  const rootFiles = ["README.md", "index.md", "compatibility.json", "flow-contract.json", "go.md", "f.md", "prime.md", "protocol.md",
12
18
  "protocol-implement.md", "plan.md", "code.md", "merge.md", "merge-start.md", "merge-stop.md", "release.md", "deploy.md", "publish.md",
13
- "review.md", "review-fix.md", "mb-audit.md", "mb-fix.md", "mb-lint.md", "interactive.md", "finish.md", "fix-def.md"];
14
- const supportDirectories = ["common", "prime", "mb-sdlc", "aspects", "def", "workers", "mb-audit", "schemas", "vnext", "specify", "stage-reports", "dashboard", "plan", "code", "merge"];
19
+ "review-fix.md", "mb-fix.md", "mb-lint.md", "interactive.md", "finish.md", "fix-def.md"];
20
+ const supportDirectories = ["common", "prime", "mb-sdlc", "aspects", "def", "workers", "schemas", "vnext", "specify", "stage-reports", "dashboard", "plan", "code", "merge"];
15
21
  const required = ["README.md", "index.md", "flow-contract.json", "compatibility.json", "prime.md", "go.md",
16
22
  "common/style.md", "common/runtime-contract.md", "common/lifecycle-guards.md", "common/workspace-bootstrap.md", "common/worker-session.md",
17
23
  "vnext/start.md", "vnext/specify.md", "vnext/protocolize.md", "vnext/plan.md", "vnext/plan-review.md", "vnext/code.md", "vnext/code-review.md", "vnext/merge.md",
@@ -112,7 +118,7 @@ export class PreparedMemoryLayer {
112
118
  source_commit: installation.manifest.canon.commit, canon_version_at_source_commit: installation.manifest.canon.version,
113
119
  flow_contract: contract.id, installed_at: installedAt, included_files: included,
114
120
  file_hashes: Object.fromEntries(included.map(logical => [logical, hash(files.get(`dd-flow/${logical}`))])),
115
- excluded_canonical_only_flows: ["mb-init.md", "mb-upgrade.md", "mb-distill.md"], generated_allow_patterns: [".generated/**"] };
121
+ excluded_canonical_only_flows: canonicalOnlyFlowNames, generated_allow_patterns: [".generated/**"] };
116
122
  const schema = JSON.parse(fs.readFileSync(new URL("../schemas/project-flow-pack-manifest.schema.json", import.meta.url), "utf8"));
117
123
  if (!new Ajv({ strict: false, validateFormats: false }).compile(schema)(manifest))
118
124
  refuse("invalid_pack_manifest");
@@ -130,10 +136,69 @@ export class PreparedMemoryLayer {
130
136
  }
131
137
  }
132
138
  files() { return new Map([...this.#files].map(([logical, bytes]) => [logical, Buffer.from(bytes)])); }
139
+ /** Historical content baseline, not an executable target layer. Project
140
+ * paths select a subset; every byte/provenance fact comes from the committed
141
+ * setup canon. Old runtime compatibility and current installation defaults
142
+ * are irrelevant, and no release is activated or downloaded here.
143
+ */
144
+ static fromCommittedCanon(home, current, expectedInstallationSha256) {
145
+ if (!path.isAbsolute(home) || !current || !Array.isArray(current.included_files) || current.included_files.length > 20_000
146
+ || typeof current.installed_at !== "string" || !current.installed_at || current.installed_at.length > 256)
147
+ refuse("invalid_baseline_selection");
148
+ const installation = readCommittedCanonRelease(home, current.canon_version_at_source_commit, current.source_commit, expectedInstallationSha256);
149
+ if (!installation)
150
+ return null;
151
+ const archive = inspectReleaseArchive(installation.manifest.canon.archive, readBoundedReleaseFile(path.join(path.dirname(installation.canon_root), "archive.tgz"), installation.manifest.canon.archive.bytes));
152
+ const roots = [...new Set(archive.map(entry => entry.path.split("/")[0]))];
153
+ if (roots.length !== 1)
154
+ refuse("ambiguous_archive_root");
155
+ const prefix = archive.some(entry => entry.path.startsWith(`${roots[0]}/.memory-bank/`)) ? `${roots[0]}/.memory-bank` : roots[0];
156
+ const canonRoot = path.join(installation.canon_root, roots[0]), memoryRoot = path.join(installation.canon_root, prefix);
157
+ const sourceFiles = new Map(archive.filter(entry => entry.kind === "file").map(entry => [entry.path, entry.bytes]));
158
+ const files = new Map(), folded = new Set();
159
+ let total = 0;
160
+ const put = (logical, content) => {
161
+ if (!/^(?:dd-flow|mbb)\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(logical)
162
+ || content.length > 1024 * 1024 || files.size >= 20_000 || (total += content.length) > 64 * 1024 * 1024
163
+ || folded.has(logical.toLowerCase()))
164
+ refuse("unsafe_or_unbounded_baseline");
165
+ folded.add(logical.toLowerCase());
166
+ files.set(logical, Buffer.from(content));
167
+ };
168
+ const flowRoot = path.join(memoryRoot, "dd-flow");
169
+ for (const logical of [...new Set(current.included_files)].sort()) {
170
+ if (typeof logical !== "string" || !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(logical)
171
+ || logical.split("/").some(part => [".", ".."].includes(part)))
172
+ refuse("unsafe_baseline_path");
173
+ if (logical === "manifest.json")
174
+ continue;
175
+ const content = sourceFiles.get(`${prefix}/dd-flow/${logical}`);
176
+ if (content)
177
+ put(`dd-flow/${logical}`, content);
178
+ }
179
+ const included = [...files.keys()].map(logical => logical.slice("dd-flow/".length));
180
+ if (!included.length)
181
+ return null;
182
+ for (const [logical, content] of sourceFiles) {
183
+ const relative = logical.slice(prefix.length + 1);
184
+ if (logical.startsWith(`${prefix}/`) && (relative.startsWith("mbb/") || isCanonicalOnlyMemoryPath(relative)))
185
+ put(relative, content);
186
+ }
187
+ const manifestBytes = Buffer.from(JSON.stringify({ schema_id: "dd-flow/project-flow-pack-manifest@1",
188
+ pack_version: installation.manifest.canon.version, flow_origin_policy: "project_local_pack", canon_root: canonRoot,
189
+ canon_memory_bank_root: memoryRoot, canon_flow_root: flowRoot, source_commit: installation.manifest.canon.commit,
190
+ canon_version_at_source_commit: installation.manifest.canon.version, installed_at: current.installed_at,
191
+ included_files: included, file_hashes: Object.fromEntries(included.map(logical => [logical, hash(files.get(`dd-flow/${logical}`))])),
192
+ excluded_canonical_only_flows: canonicalOnlyFlowNames }));
193
+ if (files.size >= 20_000 || manifestBytes.length > 1024 * 1024 || total + manifestBytes.length > 64 * 1024 * 1024)
194
+ refuse("unsafe_or_unbounded_baseline");
195
+ files.set("dd-flow/manifest.json", manifestBytes);
196
+ return new PreparedMemoryLayer(new Map([...files].sort(([left], [right]) => left.localeCompare(right, "en"))), installation.manifest_sha256);
197
+ }
133
198
  /** Restore only with an independently retained hash of an already admitted layer.
134
199
  * SHA admission proves the original verified bytes; mutable installation sources are not read.
135
200
  */
136
- static restoreRetained(files, expectedSha256, installationSha256) {
201
+ static restoreRetained(files, expectedSha256, installationSha256, allowEmpty = false) {
137
202
  if (!/^[a-f0-9]{64}$/.test(expectedSha256) || !/^[a-f0-9]{64}$/.test(installationSha256)
138
203
  || files.size < 1 || files.size > 20_000)
139
204
  refuse("invalid_retained_selection");
@@ -142,7 +207,7 @@ export class PreparedMemoryLayer {
142
207
  for (const [logical, content] of files) {
143
208
  if (!/^(?:mbb|dd-flow)\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(logical)
144
209
  || logical.split("/").some(segment => segment === "." || segment === "..")
145
- || folded.has(logical.toLowerCase()) || content.length < 1 || content.length > 1024 * 1024
210
+ || folded.has(logical.toLowerCase()) || !allowEmpty && content.length < 1 || content.length > 1024 * 1024
146
211
  || (total += content.length) > 64 * 1024 * 1024)
147
212
  refuse("unsafe_or_unbounded_inventory");
148
213
  folded.add(logical.toLowerCase());
@@ -1,10 +1,101 @@
1
1
  import { AppError } from "../shared/errors.js";
2
+ import { createHash } from "node:crypto";
3
+ import { parseDocument } from "yaml";
4
+ /** Source templates are assets, not instantiated project documents. A known
5
+ * canonical asset may retain customized placement or a fixed example date;
6
+ * its header must still be a template. A directory name grants no exemption. */
7
+ export function memoryCanonicalTemplatePaths(entries, files, current) {
8
+ const result = new Set();
9
+ const placeholder = (value) => typeof value === "string" && /<[^>]+>/.test(value);
10
+ const header = (bytes) => {
11
+ const match = new TextDecoder("utf-8", { fatal: true }).decode(bytes).match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
12
+ if (!match)
13
+ return null;
14
+ const document = parseDocument(match[1]);
15
+ return document.errors.length ? null : document.toJS();
16
+ };
17
+ for (const entry of entries) {
18
+ if (!/^mbb\/templates\/.+\.(?:md|markdown)$/.test(entry.path) || !entry.target || !["canonical", "local-customized"].includes(entry.ownership))
19
+ continue;
20
+ const target = files.get(entry.path);
21
+ if (!target || createHash("sha256").update(target).digest("hex") !== entry.target.sha256)
22
+ refuse("canonical_template_source_changed");
23
+ const bytes = current.get(entry.path);
24
+ if (!bytes)
25
+ continue;
26
+ try {
27
+ const source = header(target), observed = header(bytes);
28
+ if (source && observed && typeof observed.date === "string" && /^(?:YYYY-MM-DD|\d{4}-\d{2}-\d{2})$/.test(observed.date)
29
+ && ["file", "description", "purpose"].some(key => placeholder(source[key]) && placeholder(observed[key])))
30
+ result.add(`.memory-bank/${entry.path}`);
31
+ }
32
+ catch { /* Malformed headers remain active and are linted. */ }
33
+ }
34
+ return result;
35
+ }
36
+ /** A copied canonical guide may name its source repository's specs/protocols.
37
+ * Only those exact frozen relations are foreign provenance, not a promise that
38
+ * the consumer contains them. New project targets and body links stay checked. */
39
+ export function memoryCanonicalLintExceptions(entries, files, selected) {
40
+ const result = [];
41
+ for (const file of new Set(selected)) {
42
+ if (!/^\.memory-bank\/(?:dd-flow|mbb)\/.+\.(?:md|markdown)$/.test(file))
43
+ continue;
44
+ if (["*", "?", "[", "]", "{", "}", "!", "(", ")", "\\"].some(character => file.includes(character)))
45
+ continue;
46
+ const logical = file.slice(13), entry = entries.find(entry => entry.path === logical);
47
+ if (!entry?.target || !["canonical", "local-customized"].includes(entry.ownership))
48
+ continue;
49
+ const bytes = files.get(logical);
50
+ if (!bytes || createHash("sha256").update(bytes).digest("hex") !== entry.target.sha256)
51
+ refuse("canonical_guidance_source_changed");
52
+ let header;
53
+ try {
54
+ const match = new TextDecoder("utf-8", { fatal: true }).decode(bytes).match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
55
+ if (!match)
56
+ continue;
57
+ const document = parseDocument(match[1]);
58
+ if (document.errors.length)
59
+ continue;
60
+ const value = document.toJS();
61
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.file !== file)
62
+ continue;
63
+ header = value;
64
+ }
65
+ catch {
66
+ continue;
67
+ }
68
+ const targets = new Set(["related_specs", "related_protocols"].flatMap(key => Array.isArray(header[key]) ? header[key] : [])
69
+ .filter((target) => typeof target === "string" && /^\.memory-bank\/(?:spec|protocol)\//.test(target)
70
+ && !["*", "?", "[", "]", "{", "}", "!", "(", ")"].some(character => target.includes(character))
71
+ && !target.includes("\\") && !target.includes("\0") && target.split("/").every(part => part && part !== "." && part !== "..")));
72
+ for (const target of targets)
73
+ result.push({ ruleId: "links/frontmatter-target-exists", file, target,
74
+ reason: "Source-canon spec/protocol provenance, not a required consumer-owned document; source resolution is not claimed." });
75
+ }
76
+ return result;
77
+ }
78
+ /** Pure ownership projection from retained inventory and accepted diff. Exact
79
+ * retained-only bytes keep their existing lint policy; they are preservation
80
+ * artifacts, not new authoring. Changed or semantic-work files stay protected.
81
+ * Mechanical canonical references remain verified by their publication owner. */
82
+ export function memoryUpgradeLintGuidance(entries, changes) {
83
+ const authored = new Set(changes.filter(change => change.action === "semantic-work").map(change => change.to));
84
+ const retained = new Set(changes.filter(change => change.action === "retain" && change.from === change.to).map(change => change.from));
85
+ const preserved = entries.filter(entry => entry.current?.kind === "file" && /\.(?:md|markdown)$/.test(entry.path)
86
+ && retained.has(`.memory-bank/${entry.path}`) && !authored.has(`.memory-bank/${entry.path}`))
87
+ .map(entry => [`.memory-bank/${entry.path}`, entry.current.sha256]);
88
+ return new Map([...preserved, ...entries.filter(entry => entry.ownership === "canonical" && entry.target
89
+ && /^(?:dd-flow|mbb)\//.test(entry.path) && !authored.has(`.memory-bank/${entry.path}`))
90
+ .map(entry => [`.memory-bank/${entry.path}`, entry.target.sha256])]);
91
+ }
2
92
  /** Review inputs also contain immutable guidance; only accepted current unit
3
93
  * artifacts are project output that lint configuration must not suppress. */
4
- export function memoryVerificationActivePaths(units, assignments = []) {
94
+ export function memoryVerificationActivePaths(units, assignments = [], guidance, templates) {
5
95
  return [".memory-bank/index.md", ".memory-bank/structure.md", ".memory-bank/project-policy.md",
6
96
  ...units.flatMap(unit => (assignments.find(assignment => assignment.unit.id === unit.id)?.unit ?? unit).artifacts
7
- .filter(artifact => artifact.root === "workspace").map(artifact => artifact.path))];
97
+ .filter(artifact => artifact.root === "workspace" && !templates?.has(artifact.path) && !(typeof artifact.sha256 === "string" && guidance?.get(artifact.path) === artifact.sha256))
98
+ .map(artifact => artifact.path))];
8
99
  }
9
100
  /** Bootstrap-owned policy for immutable guidance, never project knowledge or
10
101
  * the two project-owned JSON defaults beside the canonical flow pack. */
@@ -83,3 +174,21 @@ export function validateMemoryLintConfig(bytes, activePaths) {
83
174
  refuse("invalid_git_config");
84
175
  }
85
176
  }
177
+ /** Preserve project lint policy, but independently check every active Markdown
178
+ * document it suppresses. JSON artifacts belong to their schema owners, not
179
+ * mb-lint's Markdown scanner. Invalid policy still refuses instead of fallback. */
180
+ export function memoryVerificationSupplementalLintFiles(bytes, activePaths) {
181
+ validateMemoryLintConfig(bytes, []);
182
+ return [...new Set(activePaths)].filter(file => file.startsWith(".memory-bank/") && /\.(?:md|markdown)$/.test(file)).filter(file => {
183
+ try {
184
+ validateMemoryLintConfig(bytes, [file]);
185
+ return false;
186
+ }
187
+ catch (error) {
188
+ if (error instanceof AppError && error.code === "memory_lint_config_invalid"
189
+ && ["active_document_ignored", "active_finding_suppressed"].includes(String(error.details.reason)))
190
+ return true;
191
+ throw error;
192
+ }
193
+ });
194
+ }
@@ -1,6 +1,5 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { createHash } from "node:crypto";
4
3
  import { AppError } from "../shared/errors.js";
5
4
  import { appendAudit } from "./audit.js";
6
5
  import { hybridDigest } from "./hybrid-contracts.js";
@@ -8,70 +7,9 @@ import { prepareMemoryReviewWorkInput, assertPreparedMemoryReviewWorkInput } fro
8
7
  import { assertRunMutationAllowed } from "./run-recovery-guard.js";
9
8
  import { reviewSourceFilter, reviewSourceHash, reviewExecutionCopyHash } from "./review-copy.js";
10
9
  import { writeReviewGitEvidence } from "./external-work-launch.js";
10
+ import { memoryReviewPhysicalDestination } from "./hook-memory-routing.js";
11
11
  function refuse(reason) { throw new AppError("memory_native_review_copy_invalid", "Native Memory review requires an independently owned immutable execution copy", 1, { reason, effect: "no_effect" }); }
12
- function physicalDestination(runRoot, relative) {
13
- if (fs.realpathSync(runRoot) !== path.resolve(runRoot) || !fs.lstatSync(runRoot).isDirectory())
14
- refuse("noncanonical_run_root");
15
- let current = runRoot;
16
- for (const part of relative.split("/")) {
17
- if (!part || part === "." || part === "..")
18
- refuse("invalid_copy_path");
19
- current = path.join(current, part);
20
- const stat = fs.lstatSync(current, { throwIfNoEntry: false });
21
- if (stat && !stat.isDirectory())
22
- refuse("linked_or_non_directory_copy_path");
23
- if (!stat)
24
- break;
25
- }
26
- }
27
- /** Routing is not fresh source admission: an already claimed reviewer retains
28
- * its committed private directory even after a later correction changes MB. */
29
- export function memoryNativeReviewAssignedHookCwd(context, input) {
30
- const work = context.db.get("SELECT status,payload_json FROM works WHERE project_id=? AND run_id=? AND work_id=?", [input.projectId, input.runId, input.workId]);
31
- const payload = JSON.parse(work?.payload_json ?? "null");
32
- if (payload?.kind !== "memory-review" || payload.read_only !== true)
33
- return null;
34
- const ready = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.native_review_copy_ready' AND json_extract(payload_json,'$.intent.run_id')=? AND json_extract(payload_json,'$.intent.work_id')=?", [input.projectId, input.runId, input.workId]);
35
- if (!ready)
36
- return null;
37
- if (work?.status === "created") {
38
- // Hook ingress routes an already allocated copy. Source admission belongs
39
- // to the actual work-start owner, not a second physical graph in the Hook.
40
- const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
41
- const run = context.db.get("SELECT run_root,run_dir FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]);
42
- if (!run)
43
- refuse("owned_run_required");
44
- const rows = (event) => context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND COALESCE(json_extract(payload_json,'$.run_id'),json_extract(payload_json,'$.intent.run_id'))=? AND COALESCE(json_extract(payload_json,'$.work_id'),json_extract(payload_json,'$.intent.work_id'))=? AND COALESCE(json_extract(payload_json,'$.generation'),json_extract(payload_json,'$.intent.generation'))=? LIMIT 2", [input.projectId, event, input.runId, input.workId, generation]);
45
- const intents = rows("memory.native_review_copy_intent"), copies = rows("memory.native_review_copy_ready");
46
- if (intents.length !== 1 || copies.length !== 1)
47
- refuse("registered_copy_required");
48
- const intent = JSON.parse(intents[0].payload_json), copy = JSON.parse(copies[0].payload_json);
49
- const relative = `works/${input.workId}/native-review-workspaces/generation-${generation}`, runRoot = run.run_root ?? run.run_dir;
50
- if (intent.schema_id !== "dd-flow/memory-native-review-copy@1" || intent.project_id !== input.projectId || intent.run_id !== input.runId
51
- || intent.work_id !== input.workId || intent.generation !== generation || intent.root !== relative
52
- || !/^[a-f0-9]{64}$/.test(intent.input_sha256) || !/^[a-f0-9]{64}$/.test(intent.source_sha256)
53
- || hybridDigest(copy.intent) !== hybridDigest(intent) || !/^[a-f0-9]{64}$/.test(copy.sha256))
54
- refuse("registered_copy_changed");
55
- physicalDestination(runRoot, relative);
56
- const root = path.join(runRoot, relative);
57
- if (!fs.lstatSync(root, { throwIfNoEntry: false })?.isDirectory())
58
- refuse("registered_copy_required");
59
- return root;
60
- }
61
- const rows = context.db.all("SELECT ws.start_receipt_json,r.run_root,r.run_dir,s.cwd FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN runs r ON r.project_id=w.project_id AND r.id=w.run_id JOIN sessions s ON s.project_id=w.project_id AND s.session_id=ws.session_id WHERE w.project_id=? AND w.run_id=? AND w.work_id=? AND s.provider_session_id=? AND s.worker_id=w.work_id AND EXISTS(SELECT 1 FROM audit_events a WHERE a.project_id=w.project_id AND a.event_type='memory.native_review_binding' AND json_extract(a.payload_json,'$.work_session_id')=ws.id AND json_extract(a.payload_json,'$.run_id')=w.run_id AND json_extract(a.payload_json,'$.work_id')=w.work_id) LIMIT 2", [input.projectId, input.runId, input.workId, input.providerSessionId]);
62
- if (rows.length !== 1)
63
- refuse("registered_session_required");
64
- const row = rows[0];
65
- if (typeof row.start_receipt_json !== "string" || Buffer.byteLength(row.start_receipt_json) > 16 * 1024 * 1024)
66
- refuse("registered_copy_required");
67
- const receipt = JSON.parse(row.start_receipt_json), packet = JSON.parse(receipt.context);
68
- const relative = `works/${input.workId}/native-review-workspaces/generation-${receipt.generation}`, root = path.join(row.run_root ?? row.run_dir, relative);
69
- if (receipt.version !== 2 || createHash("sha256").update(receipt.context).digest("hex") !== receipt.context_sha256
70
- || packet.work_id !== input.workId || packet.run_id !== input.runId || packet.workspace_root !== root
71
- || packet.review_copy?.root !== relative || row.cwd !== root)
72
- refuse("registered_copy_changed");
73
- return root;
74
- }
12
+ export { memoryNativeReviewAssignedHookCwd } from "./hook-memory-routing.js";
75
13
  /** Existing source/copy policy, separate native physical custody: no external
76
14
  * launch row is invented. An interrupted allocation is never overwritten. */
77
15
  export function prepareMemoryNativeReviewCopy(context, workId, allocate = false, preparedOwner) {
@@ -89,7 +27,7 @@ export function prepareMemoryNativeReviewCopy(context, workId, allocate = false,
89
27
  refuse("native_route_required");
90
28
  const relative = `works/${workId}/native-review-workspaces/generation-${generation}`;
91
29
  const runRoot = run.run_root ?? run.run_dir, root = path.join(runRoot, relative);
92
- physicalDestination(runRoot, relative);
30
+ memoryReviewPhysicalDestination(runRoot, relative);
93
31
  const inputs = owner.input.run_inputs.filter(ref => ref.root === "workspace").map(ref => ref.path);
94
32
  const selectedBase = JSON.parse(run.index_json).workspace?.base;
95
33
  const base = selectedBase && selectedBase !== "unknown" ? selectedBase : "HEAD";
@@ -125,9 +63,9 @@ export function prepareMemoryNativeReviewCopy(context, workId, allocate = false,
125
63
  });
126
64
  try {
127
65
  fs.mkdirSync(path.dirname(root), { recursive: true, mode: 0o700 });
128
- physicalDestination(runRoot, relative);
66
+ memoryReviewPhysicalDestination(runRoot, relative);
129
67
  fs.mkdirSync(root, { mode: 0o700 }); // Exclusive: never adopt an existing or partial destination.
130
- physicalDestination(runRoot, relative);
68
+ memoryReviewPhysicalDestination(runRoot, relative);
131
69
  fs.cpSync(run.workspace_root, root, { recursive: true, verbatimSymlinks: true, force: false, errorOnExist: true, filter: reviewSourceFilter(run.workspace_root, inputs, base) });
132
70
  if (reviewSourceHash(run.workspace_root, inputs, base) !== intent.source_sha256
133
71
  || reviewExecutionCopyHash(root, run.workspace_root, false, inputs, base) !== intent.source_sha256)
@@ -9,6 +9,18 @@ import { prepareVnextSpecifyStart } from "./vnext-specify.js";
9
9
  import { loadVnextWorkspacePolicy } from "./vnext-workspace-policy.js";
10
10
  import { hybridDigest } from "./hybrid-contracts.js";
11
11
  const identity = [".memory-bank/index.md", ".memory-bank/structure.md", ".memory-bank/project-policy.md"];
12
+ /** VERIFY diagnostics must remain reviewable when preparation fails. Neither
13
+ * outcome creates a future task or establishes provider access. */
14
+ export function observeMemoryNormalFlowEntryDiagnostic(context, projectRoot) {
15
+ try {
16
+ return { status: "prepared", observation: observeMemoryNormalFlowEntry(context, projectRoot) };
17
+ }
18
+ catch (error) {
19
+ if (!(error instanceof AppError))
20
+ throw error;
21
+ return { status: "unavailable", error: { code: error.code, reason: error.details?.reason ?? null } };
22
+ }
23
+ }
12
24
  function refuse(reason) {
13
25
  throw new AppError("memory_normal_flow_entry_invalid", "Ordinary SDLC entry cannot be prepared from initialized memory", 2, { reason, effect: "no_effect" });
14
26
  }
@@ -28,7 +28,7 @@ function retainedUnbornBranch(context, workspace, scope) {
28
28
  return policy.integration_branch;
29
29
  }
30
30
  /** Project-owned technical defaults only; never grants authority to a future task. */
31
- export function prepareMemoryOrdinaryDefaults(context, workspace, selectedProfile, scope) {
31
+ export function prepareMemoryOrdinaryDefaults(context, workspace, selectedProfile, scope, integrationBranch, targetFeatureBranchTemplate) {
32
32
  const proposals = [], missing = [];
33
33
  const existing = ordinaryConfigurationPaths.map(logical => fs.existsSync(path.join(workspace, logical)));
34
34
  if (existing[0])
@@ -61,14 +61,20 @@ export function prepareMemoryOrdinaryDefaults(context, workspace, selectedProfil
61
61
  const remote = git(["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
62
62
  const branches = git(["for-each-ref", "--format=%(refname:short)", "refs/heads/"])?.split("\n").filter(Boolean) ?? [];
63
63
  const unborn = !remote && !branches.length ? retainedUnbornBranch(context, workspace, scope) : null;
64
- const branch = remote?.startsWith("refs/remotes/origin/") ? remote.slice("refs/remotes/origin/".length) : branches.length === 1 ? branches[0] : unborn;
65
- reason = remote ? "verified_origin_head" : unborn ? "retained_unborn_init_branch_technical_default" : branch ? "verified_single_local_branch_technical_default" : "integration_branch_choice_required";
66
- if (!branch || !unborn && !git(["rev-parse", "--verify", `${remote ? `refs/remotes/origin/${branch}` : `refs/heads/${branch}`}^{commit}`]))
64
+ const remoteBranch = remote?.startsWith("refs/remotes/origin/") ? remote.slice("refs/remotes/origin/".length) : null;
65
+ // The Memory flow's retained explicit choice is also a technical default, not
66
+ // new merge/push authority. Existing ordinary configuration wins above.
67
+ const branch = integrationBranch ?? remoteBranch ?? (branches.length === 1 ? branches[0] : unborn);
68
+ reason = integrationBranch !== undefined ? "retained_explicit_integration_branch" : remoteBranch ? "verified_origin_head" : unborn ? "retained_unborn_init_branch_technical_default" : branch ? "verified_single_local_branch_technical_default" : "integration_branch_choice_required";
69
+ if (!branch || branch !== unborn && !git(["rev-parse", "--verify", `${integrationBranch === undefined && remoteBranch ? `refs/remotes/origin/${branch}` : `refs/heads/${branch}`}^{commit}`]))
67
70
  missing.push("ordinary integration branch (multiple or unavailable candidates)");
68
71
  else {
69
72
  const contractFile = path.join(workspace, ".memory-bank/dd-flow/flow-contract.json");
70
73
  const contract = fs.existsSync(contractFile) ? JSON.parse(fs.readFileSync(contractFile, "utf8")) : null;
71
- const template = contract?.workspace?.feature_branch_template;
74
+ const localTemplate = contract?.workspace?.feature_branch_template;
75
+ // Preserve compatible project routing; legacy PRT contracts use the
76
+ // retained upgrade target's default, not an unrelated local canon.
77
+ const template = typeof localTemplate === "string" && localTemplate.includes("<RUN>") ? localTemplate : targetFeatureBranchTemplate;
72
78
  if (typeof template !== "string" || !template.includes("<RUN>"))
73
79
  missing.push("ordinary feature branch template from selected flow contract");
74
80
  else
@@ -110,3 +116,14 @@ export function assertMemoryOrdinaryConfigurationCoverage(value, defaults) {
110
116
  throw new AppError("memory_stage_result_invalid", "Compose must assign the missing ordinary configuration to a reviewed unit and an exact write scope", 2, { phase: "prepare", effect: "no_effect", recoverable: true, reason: "ordinary_configuration_unassigned", path: logical });
111
117
  }
112
118
  }
119
+ /** Upgrade must admit missing project-owned paths before its diff is immutable. */
120
+ export function assertMemoryOrdinaryUpgradeConfigurationCoverage(value, defaults) {
121
+ const changes = value.details.path_map;
122
+ if (changes.some(change => change.action !== "retain" && [change.from, change.to].some(path => path !== null && defaults.preserved_paths.includes(path)))) {
123
+ throw new AppError("memory_stage_result_invalid", "Upgrade preserves valid existing ordinary configuration", 2, { phase: "prepare", effect: "no_effect", recoverable: true, reason: "existing_ordinary_configuration_write" });
124
+ }
125
+ for (const logical of defaults.required_paths)
126
+ if (!changes.some(change => change.from === null && change.to === logical && change.ownership === "project" && change.action === "add")) {
127
+ throw new AppError("memory_stage_result_invalid", "Upgrade analysis must assign each missing ordinary configuration as an exact project-owned addition", 2, { phase: "prepare", effect: "no_effect", recoverable: true, reason: "ordinary_configuration_unassigned", path: logical });
128
+ }
129
+ }
@@ -20,6 +20,7 @@ import { resolveProjectRoot } from "../storage/paths.js";
20
20
  import { readAcceptedStageData } from "./stage-report-renderer.js";
21
21
  import { readMemoryCompositionRegistration } from "./memory-composition-registration.js";
22
22
  import { hybridDigest } from "./hybrid-contracts.js";
23
+ import { captureMemoryUpgradeSources } from "./memory-upgrade-analysis-handler.js";
23
24
  function refuse(reason) {
24
25
  throw new AppError("memory_preflight_handler_invalid", "Memory preflight cannot accept this execution or result", 2, { reason, effect: "no_effect" });
25
26
  }
@@ -51,6 +52,20 @@ export function captureRegisteredMemoryCompositionSources(context, bundle, regis
51
52
  const registration = readMemoryCompositionRegistration(context, registrationScope);
52
53
  if (registration.run_id !== bundle.manifest.run_id || registration.execution_sha256 !== bundle.execution.sha256)
53
54
  refuse("foreign_composition_registration");
55
+ if (bundle.descriptor.descriptor.id === "mb-upgrade") {
56
+ const blueprint = JSON.parse(Buffer.from(registration.result_base64, "base64").toString("utf8"));
57
+ const historical = new Map();
58
+ // Only restoring the independently registered blueprint. Its initial
59
+ // artifact references are not freshly produced Work or current outputs;
60
+ // final acceptance selects those independently from settled SQL results.
61
+ for (const unit of blueprint.units) {
62
+ for (const artifact of unit.artifacts)
63
+ historical.set(`${artifact.root}\0${artifact.path}`, artifact.sha256);
64
+ unit.artifacts = [];
65
+ }
66
+ return { sources: captureMemoryUpgradeSources(context, bundle, registrationScope.stage, Buffer.from(JSON.stringify(blueprint))),
67
+ historical, registrationSha256: hybridDigest(registration), inputSha256: registration.input_sha256 };
68
+ }
54
69
  return { ...captureSources(context, bundle, Buffer.from(registration.result_base64, "base64"), true),
55
70
  registrationSha256: hybridDigest(registration), inputSha256: registration.input_sha256 };
56
71
  }
@@ -115,7 +115,7 @@ function compose(histories, paths, registration) {
115
115
  for (const history of histories) {
116
116
  if (!versions.has(hybridDigest(history.before().entries())))
117
117
  refuse("unexplained_historical_input_view");
118
- baseline = baseline.composeSettledDelta(history.before(), history.after());
118
+ baseline = baseline.composeSettledDelta(history.before(), history.after(), history.result().outcome);
119
119
  versions.add(hybridDigest(baseline.entries()));
120
120
  }
121
121
  return baseline;