@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
@@ -5,9 +5,14 @@ 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 canonicalOnlyFlowNames.some(name => logical === `dd-flow/${name}` || logical.startsWith(`dd-flow/${name.slice(0, -3)}/`));
15
+ }
11
16
  const rootFiles = ["README.md", "index.md", "compatibility.json", "flow-contract.json", "go.md", "f.md", "prime.md", "protocol.md",
12
17
  "protocol-implement.md", "plan.md", "code.md", "merge.md", "merge-start.md", "merge-stop.md", "release.md", "deploy.md", "publish.md",
13
18
  "review.md", "review-fix.md", "mb-audit.md", "mb-fix.md", "mb-lint.md", "interactive.md", "finish.md", "fix-def.md"];
@@ -112,7 +117,7 @@ export class PreparedMemoryLayer {
112
117
  source_commit: installation.manifest.canon.commit, canon_version_at_source_commit: installation.manifest.canon.version,
113
118
  flow_contract: contract.id, installed_at: installedAt, included_files: included,
114
119
  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/**"] };
120
+ excluded_canonical_only_flows: canonicalOnlyFlowNames, generated_allow_patterns: [".generated/**"] };
116
121
  const schema = JSON.parse(fs.readFileSync(new URL("../schemas/project-flow-pack-manifest.schema.json", import.meta.url), "utf8"));
117
122
  if (!new Ajv({ strict: false, validateFormats: false }).compile(schema)(manifest))
118
123
  refuse("invalid_pack_manifest");
@@ -130,10 +135,69 @@ export class PreparedMemoryLayer {
130
135
  }
131
136
  }
132
137
  files() { return new Map([...this.#files].map(([logical, bytes]) => [logical, Buffer.from(bytes)])); }
138
+ /** Historical content baseline, not an executable target layer. Project
139
+ * paths select a subset; every byte/provenance fact comes from the committed
140
+ * setup canon. Old runtime compatibility and current installation defaults
141
+ * are irrelevant, and no release is activated or downloaded here.
142
+ */
143
+ static fromCommittedCanon(home, current, expectedInstallationSha256) {
144
+ if (!path.isAbsolute(home) || !current || !Array.isArray(current.included_files) || current.included_files.length > 20_000
145
+ || typeof current.installed_at !== "string" || !current.installed_at || current.installed_at.length > 256)
146
+ refuse("invalid_baseline_selection");
147
+ const installation = readCommittedCanonRelease(home, current.canon_version_at_source_commit, current.source_commit, expectedInstallationSha256);
148
+ if (!installation)
149
+ return null;
150
+ const archive = inspectReleaseArchive(installation.manifest.canon.archive, readBoundedReleaseFile(path.join(path.dirname(installation.canon_root), "archive.tgz"), installation.manifest.canon.archive.bytes));
151
+ const roots = [...new Set(archive.map(entry => entry.path.split("/")[0]))];
152
+ if (roots.length !== 1)
153
+ refuse("ambiguous_archive_root");
154
+ const prefix = archive.some(entry => entry.path.startsWith(`${roots[0]}/.memory-bank/`)) ? `${roots[0]}/.memory-bank` : roots[0];
155
+ const canonRoot = path.join(installation.canon_root, roots[0]), memoryRoot = path.join(installation.canon_root, prefix);
156
+ const sourceFiles = new Map(archive.filter(entry => entry.kind === "file").map(entry => [entry.path, entry.bytes]));
157
+ const files = new Map(), folded = new Set();
158
+ let total = 0;
159
+ const put = (logical, content) => {
160
+ if (!/^(?:dd-flow|mbb)\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(logical)
161
+ || content.length > 1024 * 1024 || files.size >= 20_000 || (total += content.length) > 64 * 1024 * 1024
162
+ || folded.has(logical.toLowerCase()))
163
+ refuse("unsafe_or_unbounded_baseline");
164
+ folded.add(logical.toLowerCase());
165
+ files.set(logical, Buffer.from(content));
166
+ };
167
+ const flowRoot = path.join(memoryRoot, "dd-flow");
168
+ for (const logical of [...new Set(current.included_files)].sort()) {
169
+ if (typeof logical !== "string" || !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(logical)
170
+ || logical.split("/").some(part => [".", ".."].includes(part)))
171
+ refuse("unsafe_baseline_path");
172
+ if (logical === "manifest.json")
173
+ continue;
174
+ const content = sourceFiles.get(`${prefix}/dd-flow/${logical}`);
175
+ if (content)
176
+ put(`dd-flow/${logical}`, content);
177
+ }
178
+ const included = [...files.keys()].map(logical => logical.slice("dd-flow/".length));
179
+ if (!included.length)
180
+ return null;
181
+ for (const [logical, content] of sourceFiles) {
182
+ const relative = logical.slice(prefix.length + 1);
183
+ if (logical.startsWith(`${prefix}/`) && (relative.startsWith("mbb/") || isCanonicalOnlyMemoryPath(relative)))
184
+ put(relative, content);
185
+ }
186
+ const manifestBytes = Buffer.from(JSON.stringify({ schema_id: "dd-flow/project-flow-pack-manifest@1",
187
+ pack_version: installation.manifest.canon.version, flow_origin_policy: "project_local_pack", canon_root: canonRoot,
188
+ canon_memory_bank_root: memoryRoot, canon_flow_root: flowRoot, source_commit: installation.manifest.canon.commit,
189
+ canon_version_at_source_commit: installation.manifest.canon.version, installed_at: current.installed_at,
190
+ included_files: included, file_hashes: Object.fromEntries(included.map(logical => [logical, hash(files.get(`dd-flow/${logical}`))])),
191
+ excluded_canonical_only_flows: canonicalOnlyFlowNames }));
192
+ if (files.size >= 20_000 || manifestBytes.length > 1024 * 1024 || total + manifestBytes.length > 64 * 1024 * 1024)
193
+ refuse("unsafe_or_unbounded_baseline");
194
+ files.set("dd-flow/manifest.json", manifestBytes);
195
+ return new PreparedMemoryLayer(new Map([...files].sort(([left], [right]) => left.localeCompare(right, "en"))), installation.manifest_sha256);
196
+ }
133
197
  /** Restore only with an independently retained hash of an already admitted layer.
134
198
  * SHA admission proves the original verified bytes; mutable installation sources are not read.
135
199
  */
136
- static restoreRetained(files, expectedSha256, installationSha256) {
200
+ static restoreRetained(files, expectedSha256, installationSha256, allowEmpty = false) {
137
201
  if (!/^[a-f0-9]{64}$/.test(expectedSha256) || !/^[a-f0-9]{64}$/.test(installationSha256)
138
202
  || files.size < 1 || files.size > 20_000)
139
203
  refuse("invalid_retained_selection");
@@ -142,7 +206,7 @@ export class PreparedMemoryLayer {
142
206
  for (const [logical, content] of files) {
143
207
  if (!/^(?:mbb|dd-flow)\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(logical)
144
208
  || logical.split("/").some(segment => segment === "." || segment === "..")
145
- || folded.has(logical.toLowerCase()) || content.length < 1 || content.length > 1024 * 1024
209
+ || folded.has(logical.toLowerCase()) || !allowEmpty && content.length < 1 || content.length > 1024 * 1024
146
210
  || (total += content.length) > 64 * 1024 * 1024)
147
211
  refuse("unsafe_or_unbounded_inventory");
148
212
  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;