@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.138

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 (260) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/argument-values.js +5 -5
  5. package/dist/cli/command-inputs.js +89 -20
  6. package/dist/cli/command-metadata.js +197 -0
  7. package/dist/cli/command-option-descriptions.js +191 -0
  8. package/dist/cli/completion-cli.js +70 -0
  9. package/dist/cli/completion.js +57 -0
  10. package/dist/cli/error-output.js +52 -0
  11. package/dist/cli/executor-onboarding.js +76 -0
  12. package/dist/cli/flows.js +29 -0
  13. package/dist/cli/harnesses.js +84 -0
  14. package/dist/cli/help.js +240 -6
  15. package/dist/cli/input-preparation.js +54 -5
  16. package/dist/cli/memory-flow-admission.js +44 -0
  17. package/dist/cli/memory-init-invocation.js +78 -0
  18. package/dist/cli/memory-init.js +240 -0
  19. package/dist/cli/memory-storage-admission.js +53 -0
  20. package/dist/cli/models.js +39 -0
  21. package/dist/cli/native-hook-ingress.js +1 -1
  22. package/dist/cli/operator-profiles.js +61 -0
  23. package/dist/cli/output-options.js +35 -0
  24. package/dist/cli/response-file.js +71 -0
  25. package/dist/cli/run-cli.js +283 -160
  26. package/dist/cli/setup.js +149 -0
  27. package/dist/cli.js +54 -2
  28. package/dist/domain/stage-handler-identities.js +20 -0
  29. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  30. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  31. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
  32. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
  33. package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
  34. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  35. package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
  36. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
  37. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
  38. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +9 -9
  39. package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
  40. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  41. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  42. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  43. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  44. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  45. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  46. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  47. package/dist/harness-runtime/lib/opencode-plugin.mjs +2 -0
  48. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  49. package/dist/runtime/context.js +1 -1
  50. package/dist/schemas/discovery-result.schema.json +184 -0
  51. package/dist/schemas/execution-bundle.schema.json +205 -0
  52. package/dist/schemas/execution-settings.schema.json +162 -0
  53. package/dist/schemas/flow-descriptor.schema.json +54 -0
  54. package/dist/schemas/flow-run.schema.json +22 -0
  55. package/dist/schemas/harness-catalog.schema.json +80 -0
  56. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  57. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  58. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  59. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  60. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  61. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  62. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  63. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  64. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  65. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  66. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  67. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  68. package/dist/schemas/model-catalog.schema.json +153 -0
  69. package/dist/schemas/profile-settings.schema.json +122 -0
  70. package/dist/schemas/release-manifest.schema.json +291 -0
  71. package/dist/schemas/run-engine-binding.schema.json +1 -1
  72. package/dist/services/check-runner-evidence.js +51 -4
  73. package/dist/services/cli-operation-classifier.js +8 -47
  74. package/dist/services/cli-upgrade-allowlist.js +27 -0
  75. package/dist/services/code-checks.js +387 -34
  76. package/dist/services/controller-execution.js +88 -0
  77. package/dist/services/controller-fanout.js +71 -12
  78. package/dist/services/dashboard.js +8 -2
  79. package/dist/services/descriptor-coordinator-session.js +35 -0
  80. package/dist/services/descriptor-root-work.js +53 -0
  81. package/dist/services/descriptor-run-settlement.js +105 -0
  82. package/dist/services/descriptor-run-start.js +449 -0
  83. package/dist/services/descriptor-stage-coordinator.js +197 -0
  84. package/dist/services/descriptor-stage-settlement.js +222 -0
  85. package/dist/services/descriptor-stage-start.js +139 -0
  86. package/dist/services/discovery/agy-discovery.js +2 -0
  87. package/dist/services/discovery/codex-discovery.js +2 -0
  88. package/dist/services/discovery/droid-discovery.js +2 -0
  89. package/dist/services/discovery/grok-discovery.js +2 -0
  90. package/dist/services/discovery/opencode-discovery.js +2 -0
  91. package/dist/services/discovery/static.js +31 -0
  92. package/dist/services/discovery/zcode-discovery.js +2 -0
  93. package/dist/services/engines.js +39 -22
  94. package/dist/services/eval-snapshots.js +83 -25
  95. package/dist/services/execution-bundles.js +263 -0
  96. package/dist/services/execution-policy.js +20 -16
  97. package/dist/services/executor-onboarding.js +56 -0
  98. package/dist/services/external-work-launch.js +91 -11
  99. package/dist/services/flow-catalog.js +21 -0
  100. package/dist/services/flow-descriptors.js +82 -0
  101. package/dist/services/frozen-engine.js +242 -0
  102. package/dist/services/generic-execution.js +164 -0
  103. package/dist/services/harness-adapter.js +28 -2
  104. package/dist/services/harness-discovery.js +29 -0
  105. package/dist/services/harness-doctor.js +75 -0
  106. package/dist/services/harness-probe-effects.js +51 -0
  107. package/dist/services/harness-version-probe.js +100 -0
  108. package/dist/services/hooks.js +24 -6
  109. package/dist/services/hybrid-contracts.js +483 -0
  110. package/dist/services/ids.js +29 -1
  111. package/dist/services/installation-lock.js +95 -0
  112. package/dist/services/installed-flow-source.js +183 -0
  113. package/dist/services/lifecycle-invocations.js +257 -78
  114. package/dist/services/memory-accepted-composition.js +77 -0
  115. package/dist/services/memory-baseline-preservation.js +65 -0
  116. package/dist/services/memory-completed-correction-wave.js +320 -0
  117. package/dist/services/memory-compose-handler.js +258 -0
  118. package/dist/services/memory-composition-acceptance.js +199 -0
  119. package/dist/services/memory-composition-graph.js +134 -0
  120. package/dist/services/memory-composition-registration.js +146 -0
  121. package/dist/services/memory-composition-work-custody.js +151 -0
  122. package/dist/services/memory-composition-work-results.js +96 -0
  123. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  124. package/dist/services/memory-corrected-review-batch.js +30 -0
  125. package/dist/services/memory-corrected-review-findings.js +106 -0
  126. package/dist/services/memory-corrected-review-inputs.js +210 -0
  127. package/dist/services/memory-corrected-review-projection.js +78 -0
  128. package/dist/services/memory-corrected-review-receipt.js +170 -0
  129. package/dist/services/memory-corrected-review-registration.js +191 -0
  130. package/dist/services/memory-correction-check-receipt.js +219 -0
  131. package/dist/services/memory-correction-input-owner.js +6 -0
  132. package/dist/services/memory-discover-handler.js +104 -0
  133. package/dist/services/memory-finish-evidence.js +435 -0
  134. package/dist/services/memory-finish-handler.js +215 -0
  135. package/dist/services/memory-finish-integration.js +280 -0
  136. package/dist/services/memory-finish-readiness.js +122 -0
  137. package/dist/services/memory-git-integration.js +151 -0
  138. package/dist/services/memory-historical-corrected-review.js +206 -0
  139. package/dist/services/memory-historical-original-review.js +130 -0
  140. package/dist/services/memory-historical-repair-registration.js +99 -0
  141. package/dist/services/memory-historical-second-review.js +128 -0
  142. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  143. package/dist/services/memory-init-contexts.js +89 -0
  144. package/dist/services/memory-init-controller-start.js +84 -0
  145. package/dist/services/memory-init-engine-release.js +223 -0
  146. package/dist/services/memory-init-git-policy.js +136 -0
  147. package/dist/services/memory-init-lint-capability.js +185 -0
  148. package/dist/services/memory-init-native-capacity.js +241 -0
  149. package/dist/services/memory-init-preflight.js +312 -0
  150. package/dist/services/memory-init-project-bootstrap.js +599 -0
  151. package/dist/services/memory-init-request-candidate.js +157 -0
  152. package/dist/services/memory-init-request-input.js +66 -0
  153. package/dist/services/memory-init-resource-storage.js +125 -0
  154. package/dist/services/memory-init-run-allocation.js +81 -0
  155. package/dist/services/memory-init-run-bundle.js +98 -0
  156. package/dist/services/memory-init-start.js +175 -0
  157. package/dist/services/memory-init-startup-custody.js +80 -0
  158. package/dist/services/memory-init-startup-input.js +240 -0
  159. package/dist/services/memory-init-storage.js +81 -0
  160. package/dist/services/memory-init-stores.js +359 -0
  161. package/dist/services/memory-init-worktrunk-capability.js +172 -0
  162. package/dist/services/memory-layer-publication.js +611 -0
  163. package/dist/services/memory-layer.js +156 -0
  164. package/dist/services/memory-lint-config.js +85 -0
  165. package/dist/services/memory-native-review-copy.js +165 -0
  166. package/dist/services/memory-normal-flow-entry.js +75 -0
  167. package/dist/services/memory-ordinary-defaults.js +112 -0
  168. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  169. package/dist/services/memory-preflight-handler.js +259 -0
  170. package/dist/services/memory-repair-current-view.js +299 -0
  171. package/dist/services/memory-repair-document-delta.js +451 -0
  172. package/dist/services/memory-repair-history.js +491 -0
  173. package/dist/services/memory-repair-physical-binding.js +312 -0
  174. package/dist/services/memory-repair-publication-files.js +152 -0
  175. package/dist/services/memory-repair-publication-intent.js +27 -0
  176. package/dist/services/memory-repair-publication-plan.js +160 -0
  177. package/dist/services/memory-repair-publication-source.js +225 -0
  178. package/dist/services/memory-repair-publication.js +794 -0
  179. package/dist/services/memory-repair-registration.js +239 -0
  180. package/dist/services/memory-repair-work-baseline.js +413 -0
  181. package/dist/services/memory-repair-work-completion.js +97 -0
  182. package/dist/services/memory-repair-work-custody.js +73 -0
  183. package/dist/services/memory-repair-work-results.js +89 -0
  184. package/dist/services/memory-repair-workspace.js +252 -0
  185. package/dist/services/memory-repair-writer-copy.js +735 -0
  186. package/dist/services/memory-result-binding.js +173 -0
  187. package/dist/services/memory-retained-review-authority.js +129 -0
  188. package/dist/services/memory-retained-review-results.js +70 -0
  189. package/dist/services/memory-retained-second-baseline.js +102 -0
  190. package/dist/services/memory-retained-second-repairs.js +156 -0
  191. package/dist/services/memory-review-acceptance.js +214 -0
  192. package/dist/services/memory-review-correction-plan.js +171 -0
  193. package/dist/services/memory-review-handler.js +585 -0
  194. package/dist/services/memory-review-inputs.js +200 -0
  195. package/dist/services/memory-review-receipt.js +180 -0
  196. package/dist/services/memory-review-registration.js +108 -0
  197. package/dist/services/memory-review-work-custody.js +124 -0
  198. package/dist/services/memory-review-work-input.js +72 -0
  199. package/dist/services/memory-review-work-results.js +73 -0
  200. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  201. package/dist/services/memory-second-correction-plan.js +76 -0
  202. package/dist/services/memory-second-repair-baseline.js +91 -0
  203. package/dist/services/memory-second-repair-launch.js +614 -0
  204. package/dist/services/memory-second-repair-registration.js +88 -0
  205. package/dist/services/memory-second-repair-source.js +187 -0
  206. package/dist/services/memory-second-review-acceptance.js +115 -0
  207. package/dist/services/memory-source-observations.js +117 -0
  208. package/dist/services/memory-stage-dispatch.js +28 -0
  209. package/dist/services/memory-stage-results.js +199 -0
  210. package/dist/services/memory-verification-acceptance.js +135 -0
  211. package/dist/services/memory-verification-checks.js +252 -0
  212. package/dist/services/memory-verification-receipt.js +159 -0
  213. package/dist/services/memory-verify-handler.js +135 -0
  214. package/dist/services/memory-work-result-preparation.js +63 -0
  215. package/dist/services/model-catalog.js +109 -0
  216. package/dist/services/model-discovery.js +46 -0
  217. package/dist/services/operator-profiles.js +236 -0
  218. package/dist/services/projects.js +17 -2
  219. package/dist/services/recovery-snapshot-database.js +416 -1
  220. package/dist/services/release-archive-producer.js +52 -0
  221. package/dist/services/release-archive.js +143 -0
  222. package/dist/services/release-cache.js +142 -0
  223. package/dist/services/release-compatibility.js +18 -0
  224. package/dist/services/release-download.js +67 -0
  225. package/dist/services/release-envelope.js +104 -0
  226. package/dist/services/release-manifest.js +45 -0
  227. package/dist/services/release-payload.js +114 -0
  228. package/dist/services/release-producer.js +63 -0
  229. package/dist/services/release-provenance.js +160 -0
  230. package/dist/services/release-registry.js +66 -0
  231. package/dist/services/release-source.js +194 -0
  232. package/dist/services/repair-intents.js +1 -1
  233. package/dist/services/review-copy.js +10 -2
  234. package/dist/services/run-control.js +80 -9
  235. package/dist/services/run-controller-capture.js +10 -3
  236. package/dist/services/run-controller-process.js +21 -0
  237. package/dist/services/run-controller-recovery.js +3 -1
  238. package/dist/services/run-controller-state.js +1 -1
  239. package/dist/services/run-controller.js +101 -40
  240. package/dist/services/run-recovery-guard.js +2 -0
  241. package/dist/services/run-recovery.js +145 -5
  242. package/dist/services/run-start-custody.js +177 -0
  243. package/dist/services/runs.js +319 -25
  244. package/dist/services/setup.js +377 -0
  245. package/dist/services/stage-context.js +9 -1
  246. package/dist/services/stage-handlers.js +74 -0
  247. package/dist/services/stage-report-renderer.js +63 -0
  248. package/dist/services/stage-work-graph.js +17 -0
  249. package/dist/services/vnext-code.js +3 -3
  250. package/dist/services/vnext-execution-profile.js +2 -2
  251. package/dist/services/vnext-fanout.js +70 -9
  252. package/dist/services/vnext-merge.js +8 -3
  253. package/dist/services/vnext-plan-review.js +6 -4
  254. package/dist/services/work-registry.js +616 -71
  255. package/dist/services/workspace-files.js +1 -1
  256. package/dist/storage/database.js +84 -6
  257. package/dist/storage/writer-contract.js +11 -7
  258. package/package.json +13 -3
  259. package/tools/create-canon-release.mjs +33 -0
  260. package/tools/produce-hybrid-release.mjs +41 -0
@@ -0,0 +1,611 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { AppError, errorRecord } from "../shared/errors.js";
5
+ import { hybridDigest } from "./hybrid-contracts.js";
6
+ import { acquireInstallationLock } from "./installation-lock.js";
7
+ import { readBoundedReleaseFile } from "./release-source.js";
8
+ import { PreparedMemoryInitTarget } from "./memory-init-preflight.js";
9
+ import { PreparedMemoryLayer } from "./memory-layer.js";
10
+ import { defaultMemoryLintConfiguration } from "./memory-lint-config.js";
11
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
12
+ const limit = 1024 * 1024;
13
+ function refuse(reason) {
14
+ throw new AppError("memory_layer_publication_conflict", "Memory layer publication requires reconciliation", 1, { reason, phase: "publication", effect: "no_effect" });
15
+ }
16
+ function exists(file) {
17
+ try {
18
+ fs.lstatSync(file);
19
+ return true;
20
+ }
21
+ catch (error) {
22
+ if (error.code === "ENOENT")
23
+ return false;
24
+ throw error;
25
+ }
26
+ }
27
+ function directory(file) {
28
+ const stat = fs.lstatSync(file);
29
+ if (!stat.isDirectory())
30
+ refuse("unsafe_directory");
31
+ return stat;
32
+ }
33
+ function homeDirectory(home) {
34
+ try {
35
+ if (!path.isAbsolute(home) || fs.realpathSync(home) !== path.resolve(home))
36
+ refuse("unsafe_home");
37
+ directory(home);
38
+ fs.accessSync(home, fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK);
39
+ }
40
+ catch (error) {
41
+ if (error instanceof AppError)
42
+ throw error;
43
+ refuse("home_unavailable");
44
+ }
45
+ }
46
+ function childDirectory(parent, name) {
47
+ directory(parent);
48
+ const child = path.join(parent, name);
49
+ if (!exists(child))
50
+ fs.mkdirSync(child, { mode: 0o700 });
51
+ directory(child);
52
+ return child;
53
+ }
54
+ function bytes(value) { return Buffer.from(`${JSON.stringify(value)}\n`); }
55
+ function match(file, expected) {
56
+ const stat = fs.lstatSync(file);
57
+ if (!stat.isFile() || stat.nlink !== 1 || !readBoundedReleaseFile(file, limit).equals(expected))
58
+ refuse("retained_file_changed_or_linked");
59
+ }
60
+ /** The exact journal owns deterministic temporary files. Hard links publish complete bytes
61
+ * without rename's ability to replace a concurrently created foreign destination.
62
+ */
63
+ function immutable(root, logical, content) {
64
+ let parent = root;
65
+ for (const segment of logical.split("/").slice(0, -1))
66
+ parent = childDirectory(parent, segment);
67
+ const file = path.join(root, logical), temporary = path.join(root, `.dd-flow-write-${hash(Buffer.from(logical))}.tmp`);
68
+ if (exists(file)) {
69
+ if (exists(temporary)) {
70
+ const retained = fs.lstatSync(file), temp = fs.lstatSync(temporary);
71
+ if (!retained.isFile() || !temp.isFile() || retained.ino !== temp.ino || retained.dev !== temp.dev
72
+ || retained.nlink !== 2 || !readBoundedReleaseFile(file, limit).equals(content))
73
+ refuse("foreign_pending_write");
74
+ fs.unlinkSync(temporary);
75
+ }
76
+ match(file, content);
77
+ return;
78
+ }
79
+ if (exists(temporary)) {
80
+ const stat = fs.lstatSync(temporary);
81
+ if (!stat.isFile() || stat.nlink !== 1)
82
+ refuse("foreign_pending_write");
83
+ fs.unlinkSync(temporary);
84
+ }
85
+ const fd = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
86
+ try {
87
+ fs.writeFileSync(fd, content);
88
+ fs.fsyncSync(fd);
89
+ }
90
+ finally {
91
+ fs.closeSync(fd);
92
+ }
93
+ fs.linkSync(temporary, file);
94
+ fs.unlinkSync(temporary);
95
+ match(file, content);
96
+ }
97
+ function layerIndex(layer) {
98
+ return bytes({ schema_id: "dd-flow/retained-memory-layer@1", layer_sha256: layer.sha256, pack_sha256: layer.packSha256,
99
+ installation_sha256: layer.installationSha256, inventory: [...layer.files()].map(([logical, content]) => ({ path: logical, sha256: hash(content) })) });
100
+ }
101
+ /** The request cache, not the mutable setup selection, is the restart source. */
102
+ export function readRetainedMemoryLayer(home, selected) {
103
+ try {
104
+ homeDirectory(home);
105
+ if (!path.isAbsolute(selected.projectRoot) || !path.isAbsolute(selected.workspaceRoot)
106
+ || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(selected.requestId)
107
+ || !/^[a-f0-9]{64}$/.test(selected.targetSha256) || !/^[a-f0-9]{64}$/.test(selected.layerSha256)
108
+ || !/^[a-f0-9]{64}$/.test(selected.installationSha256))
109
+ refuse("invalid_retained_selection");
110
+ const namespace = path.join(home, "memory-layer-publications");
111
+ directory(namespace);
112
+ const journal = path.join(namespace, hybridDigest([selected.projectRoot, selected.workspaceRoot, selected.requestId]));
113
+ directory(journal);
114
+ const intentFile = path.join(journal, "intent.json"), indexFile = path.join(journal, "layer.json");
115
+ const intent = JSON.parse(readBoundedReleaseFile(intentFile, limit).toString("utf8"));
116
+ if (intent.target_sha256 !== selected.targetSha256 || intent.layer_sha256 !== selected.layerSha256
117
+ || intent.installation_sha256 !== selected.installationSha256
118
+ || intent.facts.request_id !== selected.requestId || intent.facts.project_root !== selected.projectRoot
119
+ || intent.facts.workspace_root !== selected.workspaceRoot || hybridDigest(intent.facts) !== selected.targetSha256)
120
+ refuse("foreign_retained_selection");
121
+ const index = JSON.parse(readBoundedReleaseFile(indexFile, limit).toString("utf8"));
122
+ if (!Array.isArray(index.inventory) || index.inventory.length < 1 || index.inventory.length > 20_000)
123
+ refuse("invalid_retained_inventory");
124
+ const files = new Map();
125
+ let total = 0;
126
+ for (const item of index.inventory) {
127
+ if (!item || typeof item.path !== "string" || !/^(?:mbb|dd-flow)\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(item.path)
128
+ || item.path.split("/").some(segment => segment === "." || segment === "..") || !/^[a-f0-9]{64}$/.test(item.sha256)
129
+ || files.has(item.path))
130
+ refuse("invalid_retained_inventory");
131
+ let parent = path.join(journal, "source");
132
+ directory(parent);
133
+ for (const segment of item.path.split("/").slice(0, -1)) {
134
+ parent = path.join(parent, segment);
135
+ directory(parent);
136
+ }
137
+ const file = path.join(journal, "source", item.path), content = readBoundedReleaseFile(file, limit);
138
+ if (fs.lstatSync(file).nlink !== 1 || hash(content) !== item.sha256 || (total += content.length) > 64 * limit)
139
+ refuse("retained_source_changed_or_linked");
140
+ files.set(item.path, content);
141
+ }
142
+ const layer = PreparedMemoryLayer.restoreRetained(files, selected.layerSha256, intent.installation_sha256);
143
+ match(indexFile, layerIndex(layer));
144
+ const identity = hybridDigest({ facts: intent.facts, target_sha256: selected.targetSha256, layer_sha256: layer.sha256,
145
+ pack_sha256: layer.packSha256, installation_sha256: layer.installationSha256 });
146
+ match(intentFile, bytes({ schema_id: "dd-flow/memory-layer-intent@1", identity, facts: intent.facts,
147
+ target_sha256: selected.targetSha256, layer_sha256: layer.sha256, installation_sha256: layer.installationSha256 }));
148
+ return layer;
149
+ }
150
+ catch (error) {
151
+ if (error instanceof AppError && error.code === "memory_layer_publication_conflict")
152
+ throw error;
153
+ return refuse("retained_layer_unavailable_or_changed");
154
+ }
155
+ }
156
+ /** Ownership of the exclusions used by source preservation. No canon pack
157
+ * restoration or tool capability qualification is needed to read user sources. */
158
+ export function assertPublishedMemoryLayerOwnership(home, selected, facts, publicationSha256, draftIdentitySha256) {
159
+ try {
160
+ homeDirectory(home);
161
+ if (!path.isAbsolute(selected.projectRoot) || !path.isAbsolute(selected.workspaceRoot)
162
+ || fs.realpathSync(selected.workspaceRoot) !== selected.workspaceRoot
163
+ || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(selected.requestId)
164
+ || [selected.targetSha256, selected.layerSha256, selected.packSha256, selected.installationSha256, publicationSha256].some(value => !/^[a-f0-9]{64}$/.test(value)))
165
+ refuse("invalid_retained_selection");
166
+ if (hybridDigest(facts) !== selected.targetSha256 || facts.request_id !== selected.requestId
167
+ || facts.project_root !== selected.projectRoot || facts.workspace_root !== selected.workspaceRoot)
168
+ refuse("foreign_retained_selection");
169
+ const namespace = path.join(home, "memory-layer-publications");
170
+ directory(namespace);
171
+ const journal = path.join(namespace, hybridDigest([selected.projectRoot, selected.workspaceRoot, selected.requestId]));
172
+ directory(journal);
173
+ const root = path.join(selected.workspaceRoot, ".memory-bank"), stat = directory(root);
174
+ const identity = hybridDigest({ facts, target_sha256: selected.targetSha256, layer_sha256: selected.layerSha256,
175
+ pack_sha256: selected.packSha256, installation_sha256: selected.installationSha256 });
176
+ const receipt = { schema_id: "dd-flow/memory-layer-publication@1", identity,
177
+ request_id: selected.requestId, project_root: selected.projectRoot, workspace_root: selected.workspaceRoot,
178
+ target_sha256: selected.targetSha256, layer_sha256: selected.layerSha256, pack_sha256: selected.packSha256,
179
+ installation_sha256: selected.installationSha256, root_device: stat.dev, root_inode: stat.ino };
180
+ const expected = bytes(receipt);
181
+ if (hash(expected) !== publicationSha256)
182
+ refuse("published_layer_owner_changed");
183
+ for (const file of [path.join(journal, "owner.json"), path.join(journal, "receipt.json"), path.join(root, ".dd-flow-layer-owner.json")])
184
+ match(file, expected);
185
+ if (facts.generated_lint_config_sha256) {
186
+ const draftBytes = readBoundedReleaseFile(path.join(journal, "draft-identity-receipt.json"), limit);
187
+ const draft = JSON.parse(draftBytes.toString("utf8"));
188
+ if (hash(draftBytes) !== draftIdentitySha256 || draft.schema_id !== "dd-flow/memory-init-draft-identity@2"
189
+ || draft.layer_publication_sha256 !== publicationSha256 || draft.project_root !== selected.projectRoot
190
+ || draft.workspace_root !== selected.workspaceRoot || draft.request_id !== selected.requestId
191
+ || draft.lint_configuration?.path !== ".mb-lint.json" || draft.lint_configuration.sha256 !== facts.generated_lint_config_sha256)
192
+ refuse("generated_lint_configuration_custody_changed");
193
+ match(path.join(selected.workspaceRoot, ".mb-lint.json"), defaultMemoryLintConfiguration());
194
+ }
195
+ return receipt;
196
+ }
197
+ catch (error) {
198
+ if (error instanceof AppError)
199
+ throw error;
200
+ return refuse("published_layer_unavailable_or_changed");
201
+ }
202
+ }
203
+ /** Read-only restart of a completed, independently selected publication. It
204
+ * does not republish drafts or treat a project-local marker as its authority.
205
+ * The caller selects the publication checksum from its registered SQL receipt.
206
+ */
207
+ export function restorePublishedMemoryLayer(home, selected, publicationSha256, env, draftIdentitySha256) {
208
+ try {
209
+ if (!/^[a-f0-9]{64}$/.test(publicationSha256))
210
+ refuse("invalid_publication_identity");
211
+ const layer = readRetainedMemoryLayer(home, selected);
212
+ const journal = path.join(home, "memory-layer-publications", hybridDigest([selected.projectRoot, selected.workspaceRoot, selected.requestId]));
213
+ const intent = JSON.parse(readBoundedReleaseFile(path.join(journal, "intent.json"), limit).toString("utf8"));
214
+ const facts = intent.facts;
215
+ const receipt = assertPublishedMemoryLayerOwnership(home, { ...selected, packSha256: layer.packSha256 }, facts, publicationSha256, draftIdentitySha256);
216
+ const target = PreparedMemoryInitTarget.restoreOwnedPublication(facts, selected.targetSha256, env);
217
+ return { target, layer, receipt: structuredClone(receipt) };
218
+ }
219
+ catch (error) {
220
+ if (error instanceof AppError)
221
+ throw error;
222
+ return refuse("published_layer_unavailable_or_changed");
223
+ }
224
+ }
225
+ /** Restart ingress requires independently retained selection, not values from a project marker.
226
+ * Mutable operator defaults and a fresh target preparation are never fallback sources.
227
+ */
228
+ export function resumeMemoryLayerPublication(home, selected, layer, env, onCheckpoint) {
229
+ try {
230
+ return resumeRetainedPublication(home, selected, layer, env, onCheckpoint);
231
+ }
232
+ catch (error) {
233
+ if (error instanceof AppError)
234
+ throw error;
235
+ return refuse("retained_publication_unavailable");
236
+ }
237
+ }
238
+ function resumeRetainedPublication(home, selected, layer, env, onCheckpoint) {
239
+ homeDirectory(home);
240
+ if (!path.isAbsolute(selected.projectRoot) || !path.isAbsolute(selected.workspaceRoot)
241
+ || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(selected.requestId)
242
+ || !/^[a-f0-9]{64}$/.test(selected.targetSha256) || layer.sha256 !== selected.layerSha256
243
+ || layer.installationSha256 !== selected.installationSha256)
244
+ refuse("invalid_retained_selection");
245
+ const namespace = path.join(home, "memory-layer-publications");
246
+ directory(namespace);
247
+ const journal = path.join(namespace, hybridDigest([selected.projectRoot, selected.workspaceRoot, selected.requestId]));
248
+ directory(journal);
249
+ const intentFile = path.join(journal, "intent.json"), ownerFile = path.join(journal, "owner.json");
250
+ const intent = JSON.parse(readBoundedReleaseFile(intentFile, limit).toString("utf8"));
251
+ if (intent.target_sha256 !== selected.targetSha256 || intent.layer_sha256 !== selected.layerSha256
252
+ || intent.facts.project_root !== selected.projectRoot || intent.facts.workspace_root !== selected.workspaceRoot
253
+ || intent.facts.request_id !== selected.requestId || hybridDigest(intent.facts) !== selected.targetSha256)
254
+ refuse("foreign_retained_selection");
255
+ const identity = hybridDigest({ facts: intent.facts, target_sha256: selected.targetSha256, layer_sha256: layer.sha256,
256
+ pack_sha256: layer.packSha256, installation_sha256: layer.installationSha256 });
257
+ const expectedIntent = bytes({ schema_id: "dd-flow/memory-layer-intent@1", identity, facts: intent.facts,
258
+ target_sha256: selected.targetSha256, layer_sha256: layer.sha256, installation_sha256: layer.installationSha256 });
259
+ match(intentFile, expectedIntent);
260
+ const root = path.join(selected.workspaceRoot, ".memory-bank");
261
+ if (exists(root)) {
262
+ const owner = bytes({ schema_id: "dd-flow/memory-layer-publication@1", identity, request_id: selected.requestId,
263
+ project_root: selected.projectRoot, workspace_root: selected.workspaceRoot, target_sha256: selected.targetSha256,
264
+ layer_sha256: layer.sha256, pack_sha256: layer.packSha256, installation_sha256: layer.installationSha256,
265
+ root_device: directory(root).dev, root_inode: directory(root).ino });
266
+ match(ownerFile, owner);
267
+ match(path.join(root, ".dd-flow-layer-owner.json"), owner);
268
+ }
269
+ else if (exists(ownerFile))
270
+ refuse("owned_root_missing");
271
+ const target = PreparedMemoryInitTarget.restoreOwnedPublication(intent.facts, selected.targetSha256, env);
272
+ return publishMemoryLayer(home, target, layer, env, onCheckpoint);
273
+ }
274
+ /** File-custody operation only: not registration, storage readiness, profile qualification
275
+ * or a memory-bootstrap-receipt. The productive dispatcher must admit those separately.
276
+ * A retained prepared target and selected layer can reconcile after process restart;
277
+ * a project-local owner marker without the independent CLI-home intent cannot authorize it.
278
+ */
279
+ export function publishMemoryLayer(home, target, layer, env, onCheckpoint) {
280
+ const facts = target.facts(), files = layer.files();
281
+ const identity = hybridDigest({ facts, target_sha256: target.sha256, layer_sha256: layer.sha256,
282
+ pack_sha256: layer.packSha256, installation_sha256: layer.installationSha256 });
283
+ const intent = bytes({ schema_id: "dd-flow/memory-layer-intent@1", identity, facts,
284
+ target_sha256: target.sha256, layer_sha256: layer.sha256, installation_sha256: layer.installationSha256 });
285
+ const indexBytes = layerIndex(layer);
286
+ if (indexBytes.length > limit)
287
+ refuse("layer_index_exceeds_bound");
288
+ homeDirectory(home);
289
+ const namespace = path.join(home, "memory-layer-publications"), root = path.join(facts.workspace_root, ".memory-bank");
290
+ const key = hybridDigest([facts.project_root, facts.workspace_root, facts.request_id]);
291
+ const journal = path.join(namespace, key), intentFile = path.join(journal, "intent.json");
292
+ if (exists(namespace))
293
+ directory(namespace);
294
+ if (exists(journal))
295
+ directory(journal);
296
+ // Fresh input is revalidated before even creating the request namespace.
297
+ const replay = exists(intentFile);
298
+ if (!replay)
299
+ target.revalidate();
300
+ let lock;
301
+ let started = replay;
302
+ let phase = "namespace_retained";
303
+ let primaryFailure;
304
+ let failed = false;
305
+ let result;
306
+ try {
307
+ if (!exists(namespace)) {
308
+ fs.mkdirSync(namespace, { mode: 0o700 });
309
+ started = true;
310
+ }
311
+ directory(namespace);
312
+ lock = acquireInstallationLock(path.join(namespace, `${hybridDigest(facts.workspace_root)}.lock`), identity);
313
+ if (replay) {
314
+ directory(journal);
315
+ match(intentFile, intent);
316
+ }
317
+ else {
318
+ // The lock does not make a foreign journal or partial MB ours.
319
+ target.revalidate();
320
+ if (exists(journal))
321
+ refuse("foreign_request_journal");
322
+ fs.mkdirSync(journal, { mode: 0o700 });
323
+ started = true;
324
+ immutable(journal, "intent.json", intent);
325
+ }
326
+ phase = "intent_retained";
327
+ onCheckpoint?.(phase);
328
+ const selected = { projectRoot: facts.project_root, workspaceRoot: facts.workspace_root,
329
+ requestId: facts.request_id, targetSha256: target.sha256, layerSha256: layer.sha256, installationSha256: layer.installationSha256 };
330
+ if (exists(path.join(journal, "layer.json")))
331
+ readRetainedMemoryLayer(home, selected);
332
+ else {
333
+ if (exists(path.join(journal, "owner.json")) || exists(path.join(journal, "receipt.json")))
334
+ refuse("retained_layer_index_missing");
335
+ for (const [logical, content] of files)
336
+ immutable(journal, `source/${logical}`, content);
337
+ immutable(journal, "layer.json", indexBytes);
338
+ readRetainedMemoryLayer(home, selected);
339
+ }
340
+ phase = "layer_retained";
341
+ onCheckpoint?.(phase);
342
+ const ownerFile = path.join(journal, "owner.json"), ownerLogical = ".dd-flow-layer-owner.json";
343
+ let receipt;
344
+ if (exists(root)) {
345
+ if (!exists(ownerFile))
346
+ refuse("unowned_memory_bank");
347
+ const stat = directory(root);
348
+ const expected = makeReceipt(facts, target, layer, identity, stat);
349
+ match(ownerFile, bytes(expected));
350
+ match(path.join(root, ownerLogical), bytes(expected));
351
+ receipt = expected;
352
+ }
353
+ else {
354
+ if (exists(ownerFile))
355
+ refuse("owned_root_missing");
356
+ target.revalidate();
357
+ // Exclusive mkdir is the no-clobber reservation. A crash before owner retention
358
+ // leaves an explicit uncertain partial, never an authorization to replace it.
359
+ fs.mkdirSync(root, { mode: 0o700 });
360
+ receipt = makeReceipt(facts, target, layer, identity, directory(root));
361
+ immutable(root, ownerLogical, bytes(receipt));
362
+ immutable(journal, "owner.json", bytes(receipt));
363
+ }
364
+ phase = "root_owned";
365
+ onCheckpoint?.(phase);
366
+ PreparedMemoryInitTarget.revalidateOwnedPublication(facts, target.sha256, env);
367
+ const completion = path.join(journal, "receipt.json"), wasComplete = exists(completion);
368
+ if (wasComplete)
369
+ match(completion, bytes(receipt));
370
+ for (const [logical, content] of files) {
371
+ if (wasComplete)
372
+ match(path.join(root, logical), content);
373
+ else
374
+ immutable(root, logical, content);
375
+ phase = "file_published";
376
+ onCheckpoint?.(phase);
377
+ }
378
+ const allowed = new Set([...files.keys(), ownerLogical]);
379
+ const inspect = (parent, prefix = "") => {
380
+ for (const name of fs.readdirSync(parent)) {
381
+ const logical = prefix ? `${prefix}/${name}` : name, file = path.join(parent, name), stat = fs.lstatSync(file);
382
+ if (stat.isDirectory()) {
383
+ if (![...allowed].some(item => item.startsWith(`${logical}/`)))
384
+ refuse("unlisted_publication_directory");
385
+ inspect(file, logical);
386
+ }
387
+ else {
388
+ if (!allowed.has(logical))
389
+ refuse("unlisted_publication_file");
390
+ match(file, logical === ownerLogical ? bytes(receipt) : files.get(logical));
391
+ }
392
+ }
393
+ };
394
+ // After completion, project-owned documents/runtime artifacts may legitimately
395
+ // be created by later bootstrap/stages. Replay reads only our retained inventory;
396
+ // it is neither a project-tree restoration nor a claim about semantic grounding.
397
+ if (!wasComplete)
398
+ inspect(root);
399
+ PreparedMemoryInitTarget.revalidateOwnedPublication(facts, target.sha256, env);
400
+ phase = "layer_verified";
401
+ onCheckpoint?.(phase);
402
+ immutable(journal, "receipt.json", bytes(receipt));
403
+ phase = "receipt_retained";
404
+ onCheckpoint?.(phase);
405
+ result = { receipt, sha256: hash(bytes(receipt)), changed: !wasComplete };
406
+ }
407
+ catch (error) {
408
+ failed = true;
409
+ primaryFailure = !started ? error : new AppError("memory_layer_publication_failed", "Memory layer publication retained an incomplete outcome", 1, { reason: error instanceof AppError ? error.details?.reason ?? error.code : "publication_io_failure",
410
+ phase, effect: "partial", identity, journal_path: journal, workspace_root: facts.workspace_root,
411
+ next_action: "reconcile_exact_request" });
412
+ }
413
+ try {
414
+ lock?.release();
415
+ }
416
+ catch {
417
+ throw new AppError("memory_layer_publication_failed", "Memory layer owner cleanup requires reconciliation", 1, { reason: "lock_cleanup_failed", phase, effect: "partial", identity, journal_path: journal,
418
+ ...(failed ? { primary_error: errorRecord(primaryFailure) } : {}), next_action: "reconcile_exact_request" });
419
+ }
420
+ if (failed)
421
+ throw primaryFailure;
422
+ return result;
423
+ }
424
+ /** Selected publication inventory, without restoring or qualifying tools or
425
+ * reading every canon file. Referenced physical bytes are checked by the caller. */
426
+ export function readPublishedMemoryReferenceInventory(home, selected, facts, publicationSha256, draftIdentitySha256) {
427
+ assertPublishedMemoryLayerOwnership(home, selected, facts, publicationSha256, draftIdentitySha256);
428
+ const journal = path.join(home, "memory-layer-publications", hybridDigest([selected.projectRoot, selected.workspaceRoot, selected.requestId]));
429
+ const index = JSON.parse(readBoundedReleaseFile(path.join(journal, "layer.json"), limit).toString("utf8"));
430
+ if (index.schema_id !== "dd-flow/retained-memory-layer@1" || index.layer_sha256 !== selected.layerSha256
431
+ || index.pack_sha256 !== selected.packSha256 || index.installation_sha256 !== selected.installationSha256
432
+ || !Array.isArray(index.inventory) || index.inventory.length > 20_000 || hybridDigest(index.inventory) !== selected.layerSha256)
433
+ refuse("invalid_retained_inventory");
434
+ const canon = new Map();
435
+ for (const item of index.inventory) {
436
+ if (!item || !/^(?:mbb|dd-flow)\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(item.path)
437
+ || item.path.split("/").some(segment => segment === "." || segment === "..")
438
+ || !/^[a-f0-9]{64}$/.test(item.sha256) || canon.has(item.path))
439
+ refuse("invalid_retained_inventory");
440
+ canon.set(item.path, item.sha256);
441
+ }
442
+ const draftBytes = readBoundedReleaseFile(path.join(journal, "draft-identity-receipt.json"), limit);
443
+ const draft = JSON.parse(draftBytes.toString("utf8"));
444
+ if (hash(draftBytes) !== draftIdentitySha256 || !["dd-flow/memory-init-draft-identity@1", "dd-flow/memory-init-draft-identity@2"].includes(draft.schema_id)
445
+ || draft.request_id !== selected.requestId || draft.project_root !== selected.projectRoot || draft.workspace_root !== selected.workspaceRoot
446
+ || draft.layer_publication_sha256 !== publicationSha256 || !Array.isArray(draft.files))
447
+ refuse("draft_identity_custody_changed");
448
+ const artifacts = new Map([...canon].map(([logical, sha256]) => [`.memory-bank/${logical}`, sha256]));
449
+ for (const item of draft.files) {
450
+ if (!item || !["index.md", "project-policy.md", "structure.md"].includes(item.path) || !/^[a-f0-9]{64}$/.test(item.sha256))
451
+ refuse("draft_identity_custody_changed");
452
+ artifacts.set(`.memory-bank/${item.path}`, item.sha256);
453
+ }
454
+ artifacts.set(".memory-bank/.dd-flow-layer-owner.json", publicationSha256);
455
+ if (draft.lint_configuration)
456
+ artifacts.set(draft.lint_configuration.path, draft.lint_configuration.sha256);
457
+ return { canon, artifacts };
458
+ }
459
+ /** Publish draft identity and a reserved absent root lint policy after the
460
+ * retained layer's custody operation; existing user policy is never replaced.
461
+ * This does not qualify prerequisites, register a project, accept project facts
462
+ * or issue bootstrap readiness. The productive bootstrap must perform admission
463
+ * before calling either publication operation.
464
+ */
465
+ export function publishMemoryInitDraftIdentity(home, target, layer, env, onCheckpoint) {
466
+ const published = publishMemoryLayer(home, target, layer, env), facts = target.facts();
467
+ const namespace = path.join(home, "memory-layer-publications"), journal = path.join(namespace, hybridDigest([facts.project_root, facts.workspace_root, facts.request_id]));
468
+ const root = path.join(facts.workspace_root, ".memory-bank");
469
+ const intent = path.join(journal, "draft-identity-intent.json"), completion = path.join(journal, "draft-identity-receipt.json");
470
+ let lock;
471
+ let phase = "draft_prepare", started = false, primary, failed = false;
472
+ let result;
473
+ try {
474
+ lock = acquireInstallationLock(path.join(namespace, `${hybridDigest(facts.workspace_root)}.lock`), published.receipt.identity);
475
+ // A project marker alone never authorizes scaffold writes or replay.
476
+ match(path.join(journal, "owner.json"), bytes(published.receipt));
477
+ match(path.join(root, ".dd-flow-layer-owner.json"), bytes(published.receipt));
478
+ match(path.join(journal, "receipt.json"), bytes(published.receipt));
479
+ const stat = directory(root);
480
+ if (stat.dev !== published.receipt.root_device || stat.ino !== published.receipt.root_inode)
481
+ refuse("draft_identity_root_changed");
482
+ PreparedMemoryInitTarget.revalidateOwnedPublication(facts, target.sha256, env);
483
+ const preparedAt = exists(intent)
484
+ ? JSON.parse(readBoundedReleaseFile(intent, limit).toString("utf8")).receipt?.prepared_at
485
+ : new Date().toISOString();
486
+ if (typeof preparedAt !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(preparedAt)
487
+ || !Number.isFinite(Date.parse(preparedAt)) || new Date(preparedAt).toISOString() !== preparedAt)
488
+ refuse("draft_identity_time_invalid");
489
+ const documents = draftIdentityDocuments(facts, preparedAt.slice(0, 10));
490
+ const lintBytes = facts.generated_lint_config_sha256 ? defaultMemoryLintConfiguration() : undefined;
491
+ const lintFile = path.join(facts.workspace_root, ".mb-lint.json");
492
+ if (lintBytes && hash(lintBytes) !== facts.generated_lint_config_sha256)
493
+ refuse("lint_configuration_reservation_changed");
494
+ const receipt = {
495
+ schema_id: lintBytes ? "dd-flow/memory-init-draft-identity@2" : "dd-flow/memory-init-draft-identity@1", request_id: facts.request_id,
496
+ project_root: facts.project_root, workspace_root: facts.workspace_root,
497
+ layer_publication_sha256: published.sha256, prepared_at: preparedAt,
498
+ files: [...documents].map(([logical, content]) => ({ path: logical, sha256: hash(content) })),
499
+ semantic_status: "draft_not_accepted",
500
+ ...(lintBytes ? { lint_configuration: { path: ".mb-lint.json", sha256: hash(lintBytes) } } : {})
501
+ };
502
+ const receiptBytes = bytes(receipt), intentBytes = bytes({ receipt,
503
+ documents: [...documents].map(([logical, content]) => ({ path: logical, base64: content.toString("base64") })),
504
+ ...(lintBytes ? { lint_configuration: { path: ".mb-lint.json", base64: lintBytes.toString("base64") } } : {}) });
505
+ if (exists(completion)) {
506
+ match(intent, intentBytes);
507
+ match(completion, receiptBytes);
508
+ if (lintBytes)
509
+ match(lintFile, lintBytes);
510
+ // Drafts become project-owned inputs after publication. Do not replace,
511
+ // restore or reject later semantic edits, and do not attest their contents.
512
+ result = { receipt, sha256: hash(receiptBytes), changed: false };
513
+ }
514
+ else {
515
+ const replay = exists(intent);
516
+ if (replay) {
517
+ match(intent, intentBytes);
518
+ started = true;
519
+ }
520
+ if (lintBytes && !replay && exists(path.join(facts.workspace_root, `.dd-flow-write-${hash(Buffer.from(".mb-lint.json"))}.tmp`)))
521
+ refuse("foreign_lint_configuration_temporary");
522
+ if (lintBytes && exists(lintFile)) {
523
+ if (!replay)
524
+ refuse("foreign_lint_configuration");
525
+ const stat = fs.lstatSync(lintFile), temporary = path.join(facts.workspace_root, `.dd-flow-write-${hash(Buffer.from(".mb-lint.json"))}.tmp`);
526
+ if (stat.nlink === 2 && exists(temporary)) {
527
+ const pending = fs.lstatSync(temporary);
528
+ if (!stat.isFile() || !pending.isFile() || pending.nlink !== 2 || stat.dev !== pending.dev || stat.ino !== pending.ino
529
+ || !readBoundedReleaseFile(lintFile, limit).equals(lintBytes))
530
+ refuse("foreign_pending_write");
531
+ }
532
+ else
533
+ match(lintFile, lintBytes);
534
+ }
535
+ // Inspect every existing destination before publishing any draft. In an
536
+ // incomplete own request only the original retained bytes are resumable.
537
+ for (const [logical, content] of documents) {
538
+ const destination = path.join(root, logical);
539
+ if (exists(destination)) {
540
+ if (!replay)
541
+ refuse("foreign_draft_identity_file");
542
+ const stat = fs.lstatSync(destination);
543
+ const temporary = path.join(root, `.dd-flow-write-${hash(Buffer.from(logical))}.tmp`);
544
+ // A crash between link and unlink leaves two names for our exact
545
+ // pending inode. Validate without effects; immutable() reconciles it.
546
+ if (stat.nlink === 2 && exists(temporary)) {
547
+ const pending = fs.lstatSync(temporary);
548
+ if (!stat.isFile() || !pending.isFile() || pending.nlink !== 2
549
+ || stat.dev !== pending.dev || stat.ino !== pending.ino
550
+ || !readBoundedReleaseFile(destination, limit).equals(content))
551
+ refuse("foreign_pending_write");
552
+ }
553
+ else
554
+ match(destination, content);
555
+ }
556
+ }
557
+ started = true;
558
+ immutable(journal, "draft-identity-intent.json", intentBytes);
559
+ phase = "draft_intent_retained";
560
+ onCheckpoint?.("draft_intent_retained");
561
+ if (lintBytes)
562
+ immutable(facts.workspace_root, ".mb-lint.json", lintBytes);
563
+ for (const [logical, content] of documents) {
564
+ immutable(root, logical, content);
565
+ phase = "draft_file_published";
566
+ onCheckpoint?.("draft_file_published");
567
+ }
568
+ PreparedMemoryInitTarget.revalidateOwnedPublication(facts, target.sha256, env);
569
+ immutable(journal, "draft-identity-receipt.json", receiptBytes);
570
+ phase = "draft_receipt_retained";
571
+ onCheckpoint?.("draft_receipt_retained");
572
+ result = { receipt, sha256: hash(receiptBytes), changed: true };
573
+ }
574
+ }
575
+ catch (error) {
576
+ primary = error;
577
+ failed = true;
578
+ }
579
+ try {
580
+ lock?.release();
581
+ }
582
+ catch {
583
+ throw new AppError("memory_init_draft_publication_failed", "Draft identity owner cleanup requires reconciliation", 1, { reason: "lock_cleanup_failed", phase, effect: "partial", ...(failed ? { primary_error: errorRecord(primary) } : {}), next_action: "reconcile_exact_request" });
584
+ }
585
+ if (failed)
586
+ throw new AppError("memory_init_draft_publication_failed", "Draft identity publication requires reconciliation", 1, { reason: primary instanceof AppError ? primary.details?.reason ?? primary.code : "draft_publication_io_failure",
587
+ phase, effect: started || published.changed ? "partial" : "no_effect", next_action: "reconcile_exact_request" });
588
+ return result;
589
+ }
590
+ function draftIdentityDocuments(facts, date) {
591
+ const header = (file, description, parent) => `---\nfile: ${JSON.stringify(`.memory-bank/${file}`)}\ndescription: ${JSON.stringify(description)}\npurpose: "Draft bootstrap navigation; project facts and policy require agent acceptance."\nversion: '0.1.0'\ndate: '${date}'\nstatus: 'DRAFT'\nc4_level: 'documentation'\nparent: ${parent === null ? "null" : JSON.stringify(parent)}\ntags: [memory-init, draft]\nbootstrap_request: ${JSON.stringify(facts.request_id)}\n---\n\n`;
592
+ // No source-code facts, build commands, integration branch or user policy are
593
+ // inferred from the directory name or the presence of a canonical template.
594
+ return new Map([
595
+ ["index.md", Buffer.from(header("index.md", "Draft project Memory Bank entry point.", null)
596
+ + "# Project Memory Bank — draft\n\nProject identity, scope and facts are not accepted yet. Complete discovery and composition before using this draft as project truth.\n\n"
597
+ + "- [Structure](structure.md): draft folder map.\n- [Project policy](project-policy.md): unresolved project policy.\n- [MBB](mbb/index.md): selected canonical guidance.\n- [DD Flow](dd-flow/index.md): installed flow pack; not proof of project readiness.\n")],
598
+ ["structure.md", Buffer.from(header("structure.md", "Draft project Memory Bank folder map.", ".memory-bank/index.md")
599
+ + "# Memory Bank structure — draft\n\nOnly the bootstrap layer is described here; project architecture and document shelves remain unknown.\n\n"
600
+ + "- `mbb/`: selected canonical rules and templates.\n- `dd-flow/`: selected curated flow pack.\n- Root identity documents: unaccepted bootstrap drafts.\n\nNext action: derive the project folder map from discovery evidence and update this document during composition.\n")],
601
+ ["project-policy.md", Buffer.from(header("project-policy.md", "Draft project policy with unresolved decisions.", ".memory-bank/index.md")
602
+ + "# Project policy — draft\n\nGit integration route, integration branch, required checks, workspace bootstrap, delivery authority and secrets policy are unknown. This draft grants no merge, release, deployment or publication authority.\n\n"
603
+ + "Reason: bootstrap has not established project-specific policy evidence.\n\nNext action: inspect existing project rules during discovery; ask the user only for material decisions that cannot be established from evidence. Unresolved integration policy blocks completion.\n")]
604
+ ]);
605
+ }
606
+ function makeReceipt(facts, target, layer, identity, stat) {
607
+ return { schema_id: "dd-flow/memory-layer-publication@1", identity, request_id: facts.request_id,
608
+ project_root: facts.project_root, workspace_root: facts.workspace_root, target_sha256: target.sha256,
609
+ layer_sha256: layer.sha256, pack_sha256: layer.packSha256, installation_sha256: layer.installationSha256,
610
+ root_device: stat.dev, root_inode: stat.ino };
611
+ }