@deksden-com/dd-flow-cli 0.9.0-beta.143 → 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 +65 -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 +74 -9
  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
@@ -137,15 +137,16 @@ function accepted(context, scope) {
137
137
  return refuse("invalid_receipt_json");
138
138
  }
139
139
  if (!receipt || typeof receipt !== "object" || Array.isArray(receipt) || hybridDigest({ ...receipt, checks: undefined }) !== hybridDigest({ schema_id: "dd-flow/memory-verification-acceptance@1", run_id: scope.runId, stage: "verify", project_id: scope.projectId,
140
- attempt: selected.attempt, input_sha256: selected.input_sha256, result_sha256: data.result.sha256 }) || data.data_schema_id !== "dd-flow/mb-init-verify-result@1"
140
+ attempt: selected.attempt, input_sha256: selected.input_sha256, result_sha256: data.result.sha256 }) || !["dd-flow/mb-init-verify-result@1", "dd-flow/mb-upgrade-stage-result@1"].includes(data.data_schema_id ?? "")
141
141
  || data.input_sha256 !== selected.input_sha256)
142
142
  refuse("accepted_receipt_changed");
143
143
  const value = JSON.parse(data.bytes.toString("utf8"));
144
- const compose = readAcceptedStageData(context, { ...scope, stage: "compose" }), composed = JSON.parse(compose.bytes.toString("utf8"));
145
- if (compose.data_schema_id !== "dd-flow/mb-init-compose-result@1"
146
- || composed.schema_id !== compose.data_schema_id || composed.stage !== "compose" || composed.run_id !== scope.runId
144
+ const authoring = data.data_schema_id === "dd-flow/mb-upgrade-stage-result@1" ? "upgrade" : "compose";
145
+ const compose = readAcceptedStageData(context, { ...scope, stage: authoring }), composed = JSON.parse(compose.bytes.toString("utf8"));
146
+ if (compose.data_schema_id !== (authoring === "upgrade" ? data.data_schema_id : "dd-flow/mb-init-compose-result@1")
147
+ || composed.schema_id !== compose.data_schema_id || composed.stage !== authoring || composed.run_id !== scope.runId
147
148
  || composed.outcome !== "accepted" || composed.input_sha256 !== compose.input_sha256
148
- || hybridDigest(value.predecessors) !== hybridDigest([{ stage: "compose", result: compose.result, input_sha256: compose.input_sha256 }]))
149
+ || hybridDigest(value.predecessors) !== hybridDigest([{ stage: authoring, result: compose.result, input_sha256: compose.input_sha256 }]))
149
150
  refuse("accepted_predecessor_changed");
150
151
  if (value.stage !== "verify" || value.run_id !== scope.runId || !["accepted", "needs_repair"].includes(value.outcome) || value.input_sha256 !== selected.input_sha256
151
152
  || !Array.isArray(receipt.checks) || receipt.checks.some(reference => !reference || typeof reference !== "object" || Array.isArray(reference)
@@ -24,14 +24,14 @@ function refuse(reason) {
24
24
  */
25
25
  export function runMemoryVerificationChecks(context, input) {
26
26
  const bundle = readFlowRunExecutionBundle(context, input);
27
- if (!bundle || bundle.descriptor.descriptor.id !== "mb-init" || bundle.descriptor.successor("compose", "accepted") !== "verify"
27
+ if (!bundle || !["mb-init", "mb-upgrade"].includes(bundle.descriptor.descriptor.id) || bundle.descriptor.successor(bundle.descriptor.descriptor.id === "mb-upgrade" ? "upgrade" : "compose", "accepted") !== "verify"
28
28
  || bundle.descriptor.successor("verify", "accepted") !== "review")
29
29
  refuse("registered_memory_bundle_required");
30
30
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)), scope = { projectId: project.id, runId: input.runId };
31
31
  const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), index = JSON.parse(run.index_json);
32
32
  if (index.current_stage !== "verify" || index.stage_runs.find((stage) => stage.stage === "verify")?.status !== "running")
33
33
  refuse("active_verification_required");
34
- return PreparedMemoryVerificationChecks.prepare(context, { ...scope, execution: bundle.execution, flow: bundle.descriptor }).execute(context);
34
+ return PreparedMemoryVerificationChecks.prepare(context, { ...scope, execution: bundle.execution, flow: bundle.descriptor, bundle }).execute(context);
35
35
  }
36
36
  /** Productive entry implementation, not registered until the complete six-stage
37
37
  * memory group is ready. Checks and semantic assessment acceptance belong to
@@ -41,22 +41,23 @@ export function startMemoryVerification(context, input) {
41
41
  if (!(input.bytes instanceof Uint8Array) || input.bytes.length > 1024 * 1024)
42
42
  refuse("invalid_input_bytes");
43
43
  const bundle = readFlowRunExecutionBundle(context, input);
44
- if (!bundle || bundle.descriptor.descriptor.id !== "mb-init" || bundle.descriptor.successor("compose", "accepted") !== "verify"
44
+ if (!bundle || !["mb-init", "mb-upgrade"].includes(bundle.descriptor.descriptor.id) || bundle.descriptor.successor(bundle.descriptor.descriptor.id === "mb-upgrade" ? "upgrade" : "compose", "accepted") !== "verify"
45
45
  || bundle.descriptor.successor("verify", "accepted") !== "review")
46
46
  refuse("registered_memory_bundle_required");
47
47
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)), scope = { projectId: project.id, runId: input.runId, stage: "verify" };
48
48
  const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), index = JSON.parse(run.index_json);
49
49
  if (index.stage_runs.find((stage) => stage.stage === "verify")?.status === "done")
50
50
  refuse("use_accepted_finish_replay");
51
- const preservation = PreparedMemoryBaselinePreservation.prepare(context, bundle.execution);
52
- const inherited = context.db.readSnapshot(() => ({ compose: readAcceptedStageData(context, { ...scope, stage: "compose" }), preflight: readAcceptedStageData(context, { ...scope, stage: "preflight" }) }));
51
+ const upgrade = bundle.descriptor.descriptor.id === "mb-upgrade", authoring = upgrade ? "upgrade" : "compose";
52
+ const preservation = upgrade ? PreparedAcceptedMemoryComposition.prepare(context, { scope, bundle }) : PreparedMemoryBaselinePreservation.prepare(context, bundle.execution);
53
+ const inherited = context.db.readSnapshot(() => ({ compose: readAcceptedStageData(context, { ...scope, stage: authoring }), preflight: readAcceptedStageData(context, { ...scope, stage: "preflight" }) }));
53
54
  const compose = JSON.parse(inherited.compose.bytes.toString("utf8")), preflight = JSON.parse(inherited.preflight.bytes.toString("utf8"));
54
- if (compose.schema_id !== "dd-flow/mb-init-compose-result@1" || compose.run_id !== input.runId || compose.outcome !== "accepted"
55
- || preflight.schema_id !== "dd-flow/mb-init-preflight-result@1" || preflight.run_id !== input.runId || preflight.outcome !== "accepted")
55
+ if (compose.schema_id !== (upgrade ? "dd-flow/mb-upgrade-stage-result@1" : "dd-flow/mb-init-compose-result@1") || compose.run_id !== input.runId || compose.outcome !== "accepted"
56
+ || preflight.schema_id !== (upgrade ? compose.schema_id : "dd-flow/mb-init-preflight-result@1") || preflight.run_id !== input.runId || preflight.outcome !== "accepted")
56
57
  refuse("accepted_composition_and_baseline_required");
57
58
  const fence = (writer) => {
58
59
  preservation.assertCustody(writer);
59
- if (readAcceptedStageData(writer, { ...scope, stage: "compose" }).authority_sha256 !== inherited.compose.authority_sha256
60
+ if (readAcceptedStageData(writer, { ...scope, stage: authoring }).authority_sha256 !== inherited.compose.authority_sha256
60
61
  || readAcceptedStageData(writer, { ...scope, stage: "preflight" }).authority_sha256 !== inherited.preflight.authority_sha256)
61
62
  refuse("inherited_custody_changed");
62
63
  };
@@ -75,7 +76,7 @@ export function finishMemoryVerification(context, input) {
75
76
  if (!(input.bytes instanceof Uint8Array) || !input.bytes.length || input.bytes.length > 1024 * 1024)
76
77
  refuse("invalid_result_bytes");
77
78
  const bundle = readFlowRunExecutionBundle(context, input);
78
- if (!bundle || bundle.descriptor.descriptor.id !== "mb-init" || bundle.descriptor.successor("compose", "accepted") !== "verify"
79
+ if (!bundle || !["mb-init", "mb-upgrade"].includes(bundle.descriptor.descriptor.id) || bundle.descriptor.successor(bundle.descriptor.descriptor.id === "mb-upgrade" ? "upgrade" : "compose", "accepted") !== "verify"
79
80
  || bundle.descriptor.successor("verify", "accepted") !== "review")
80
81
  refuse("registered_memory_bundle_required");
81
82
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)), scope = { projectId: project.id, runId: input.runId };
@@ -99,11 +100,11 @@ export function finishMemoryVerification(context, input) {
99
100
  }
100
101
  const composition = PreparedAcceptedMemoryComposition.prepare(context, { scope, bundle }), compose = composition.result();
101
102
  const composedUnits = compose.value().units;
102
- const preservation = PreparedMemoryBaselinePreservation.prepare(context, bundle.execution);
103
- const policy = PreparedMemoryVerificationChecks.prepare(context, { ...scope, execution: bundle.execution, flow: bundle.descriptor });
103
+ const preservation = bundle.descriptor.descriptor.id === "mb-upgrade" ? composition : PreparedMemoryBaselinePreservation.prepare(context, bundle.execution);
104
+ const policy = PreparedMemoryVerificationChecks.prepare(context, { ...scope, execution: bundle.execution, flow: bundle.descriptor, bundle, composition });
104
105
  const checks = policy.evidence(context, "diagnostic");
105
106
  const binding = PreparedMemoryResultBinding.prepare(context, { ...scope, stage: "verify", flow: bundle.descriptor, execution: bundle.execution, bundle,
106
- sources: composition.captureSources(context, input.bytes, checks), checks });
107
+ sources: composition.captureSources(context, input.bytes, checks), checks, historicalEvidence: composition.historicalEvidence() });
107
108
  let proposal;
108
109
  try {
109
110
  proposal = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes));
@@ -114,7 +115,8 @@ export function finishMemoryVerification(context, input) {
114
115
  }
115
116
  const references = checks.sourceReferences().map(({ path, sha256 }) => ({ path, sha256 }));
116
117
  const result = PreparedMemoryStageResult.prepare(Buffer.from(JSON.stringify(projectMemoryVerificationChecks(projectMemoryVerificationArtifacts(proposal, composedUnits, composition.outputReferences(), createHash("sha256").update(input.bytes).digest("hex")), references, bundle.descriptor.stage("verify").directory, createHash("sha256").update(input.bytes).digest("hex")))), bundle.descriptor, binding.binding());
117
- const acceptance = PreparedMemoryVerificationAcceptance.prepare({ scope, result, compose, checks, composition, roots: bundle.execution.snapshot().scope });
118
+ const acceptance = PreparedMemoryVerificationAcceptance.prepare({ scope, result, compose, checks, composition, roots: bundle.execution.snapshot().scope,
119
+ proposalSha256: createHash("sha256").update(input.bytes).digest("hex") });
118
120
  const receipt = PreparedMemoryVerificationReceipt.prepare(context, { scope, result, checks, acceptance });
119
121
  const settlement = PreparedDescriptorStageSettlement.prepare(context, { ...input, stage: "verify", bytes: result.bytes() });
120
122
  composition.revalidate(context);
@@ -0,0 +1,12 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ /** Syntax only: the caller still owns bounds, assignment and evidence checks. */
4
+ export function parseMemoryWorkResultProposal(bytes, code) {
5
+ try {
6
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
7
+ }
8
+ catch {
9
+ throw new AppError(code, "Memory result proposal is not valid UTF-8 JSON. Correct only this proposal using its assigned packet; do not rerun settled work or change retained inputs", 2, { reason: "invalid_result_json", phase: "prepare", effect: "no_effect", recoverable: true,
10
+ result_sha256: createHash("sha256").update(bytes).digest("hex") });
11
+ }
12
+ }
@@ -10,7 +10,7 @@ import { restoreMemoryRepairPublicationFromStage, prepareMemorySecondRepairPubli
10
10
  * from worker JSON, mutable schemas, caller profile or a supplied request key.
11
11
  * Preparation is read-only and occurs before the settlement writer.
12
12
  */
13
- export function prepareRegisteredMemoryWorkResult(context, work, result) {
13
+ export function prepareRegisteredMemoryWorkResult(context, work, result, progress) {
14
14
  if (!["dd-flow/memory-composition-work-result@1", "dd-flow/memory-review-work-result@1", "dd-flow/memory-repair-work-result@1"].includes(work.result_schema ?? ""))
15
15
  return null;
16
16
  const bytes = Buffer.from(result, "utf8");
@@ -44,15 +44,16 @@ export function prepareRegisteredMemoryWorkResult(context, work, result) {
44
44
  return PreparedPublishedMemoryRepairWorkResult.prepare(context, publication);
45
45
  }
46
46
  if (work.result_schema === "dd-flow/memory-review-work-result@1") {
47
- return PreparedRegisteredMemoryReviewWorkResult.prepare(context, { workId: work.work_id, bytes });
47
+ return PreparedRegisteredMemoryReviewWorkResult.prepare(context, { workId: work.work_id, bytes, ...(progress ? { progress } : {}) });
48
48
  }
49
49
  const run = context.db.get("SELECT project_root FROM runs WHERE project_id=? AND id=?", [work.project_id, work.run_id]);
50
50
  const bundle = run && readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: work.run_id });
51
- if (!bundle || bundle.descriptor.descriptor.id !== "mb-init")
51
+ if (!bundle || !["mb-init", "mb-upgrade"].includes(bundle.descriptor.descriptor.id))
52
52
  throw new AppError("memory_work_result_preparation_required", "Composition finish requires its retained memory flow", 1, { effect: "no_effect" });
53
+ const stage = bundle.descriptor.descriptor.id === "mb-init" ? "compose" : "upgrade";
53
54
  const custody = readWorkBatchCustody(context, work.work_id);
54
55
  const registration = selectMemoryCompositionRegistration(context, {
55
- project_id: work.project_id, run_id: work.run_id, stage: "compose", attempt: custody.scope.attempt,
56
+ project_id: work.project_id, run_id: work.run_id, stage, attempt: custody.scope.attempt,
56
57
  cycle: custody.scope.cycle, parent_work_id: custody.scope.parent_work_id
57
58
  });
58
59
  return PreparedRegisteredMemoryCompositionWorkResult.prepareFromStageCustody(context, bytes, bundle.engine(), {
@@ -1,6 +1,6 @@
1
1
  import { managedInvocationContext, managedLifecycleCommand } from "./lifecycle-invocations.js";
2
2
  import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
3
- import { continueProviderCapacity } from "./provider-capacity.js";
3
+ import { continueProviderCapacity, ownedProviderBoundary } from "./provider-capacity.js";
4
4
  import { errorRecord } from "./run-controller-state.js";
5
5
  import crypto from "node:crypto";
6
6
  import fs from "node:fs";
@@ -185,7 +185,9 @@ async function dispatch(context, input) {
185
185
  if (!["created", "running", "paused", "completed"].includes(context.db.get("SELECT status FROM works WHERE work_id = ?", [input.request.executor_work_id])?.status ?? ""))
186
186
  throw new AppError("capacity_continuation_blocked", "MERGE Work has failed or lost its binding", 1);
187
187
  },
188
- decision: () => {
188
+ decision: (boundary) => {
189
+ if (boundary && !ownedProviderBoundary(context, { projectId: input.request.project_id, runId: input.request.run_id, stage: "merge", sessionId, generation, workId: input.request.executor_work_id, scope: "stage" }, boundary))
190
+ return "reconciliation_blocked";
189
191
  const status = context.db.get("SELECT status FROM merge_requests WHERE merge_request_id = ?", [input.request.merge_request_id])?.status;
190
192
  const workStatus = context.db.get("SELECT status FROM works WHERE work_id = ?", [input.request.executor_work_id])?.status;
191
193
  return status === "completed" ? "completed" : status === "action_required" || workStatus === "paused" ? "paused" : "continue";
@@ -1,6 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import crypto from "node:crypto";
4
+ import { fileURLToPath } from "node:url";
5
+ import semver from "semver";
4
6
  import { AppError } from "../shared/errors.js";
5
7
  import { requireProjectByRoot } from "./projects.js";
6
8
  import { getProjectVersionStatus, resolveStatusProjectRoot } from "./version-status.js";
@@ -12,11 +14,16 @@ import { classifyCliOperation } from "./cli-operation-classifier.js";
12
14
  import { compatibilityContexts, selectEngine } from "./engines.js";
13
15
  import { getCliBuildInfo } from "./build-info.js";
14
16
  import { prepareJsonFile } from "../cli/input-preparation.js";
17
+ import { validateSchema } from "./schema-validation.js";
18
+ import { PreparedMemoryInitRuntimeStorage } from "./memory-init-storage.js";
19
+ import { PreparedMemoryInitResourceStorage } from "./memory-init-resource-storage.js";
20
+ import { AdmittedFrozenEngine } from "./frozen-engine.js";
21
+ import { resourceHome } from "./managed-processes.js";
15
22
  export function assessMigrationImpact(context, input) {
16
23
  const root = resolveStatusProjectRoot({ requestedRoot: input.projectRoot, cwd: process.cwd() });
17
24
  if (!root.root)
18
25
  throw new AppError("not_found", `Project root does not exist: ${input.projectRoot}`, 1);
19
- const canon = resolveCanonRoot(context);
26
+ const canon = upgradeCanon(context);
20
27
  if (!canon.ok || !canon.canon)
21
28
  throw new AppError("canon_unavailable", "Cannot resolve canonical Memory Bank for upgrade impact assessment", 1, { blockers: canon.blockers, bootstrap: canon.bootstrap });
22
29
  const status = getProjectVersionStatus({ projectRoot: root.root, rootSource: root.root_source, canon: { root: canon.canon.root, metadata: canon.canon.metadata } });
@@ -24,53 +31,65 @@ export function assessMigrationImpact(context, input) {
24
31
  const to = input.targetVersion ?? canon.canon.version;
25
32
  const forced = input.mode === "full_migration";
26
33
  const impacts = readImpacts(path.join(canon.canon.memorybank_root, "release-impact"));
27
- const chain = [];
34
+ const resolved = from && to ? adjacentMigrationChain(from, to, impacts) : null;
35
+ const chain = resolved?.status === "ok"
36
+ ? resolved.steps.map(step => impacts.find(item => item.from_version === step.from && item.to_version === step.to))
37
+ : [];
28
38
  const domains = new Set();
29
39
  const blockers = [];
30
40
  let mode = "patch";
31
41
  if (!from || !to)
32
42
  blockers.push("project or canonical Memory Bank version is unavailable");
33
- let current = from;
34
- while (current && to && current !== to) {
35
- const impact = impacts.find((item) => item.from_version === current);
36
- if (!impact) {
37
- blockers.push(`missing adjacent release impact from ${current}`);
38
- break;
39
- }
40
- chain.push(impact);
43
+ if (to !== canon.canon.version)
44
+ blockers.push("requested target version does not match the selected canon");
45
+ if (resolved?.status === "unsupported")
46
+ blockers.push(resolved.reason ?? "release impact chain is unsupported");
47
+ for (const impact of chain) {
41
48
  for (const domain of impact.domains)
42
49
  domains.add(domain);
43
50
  mode = stricterMode(mode, impact.minimum_upgrade_mode);
44
51
  if (impact.path_migration || impact.runtime_migration)
45
52
  mode = "full_migration";
46
- current = impact.to_version;
47
53
  }
48
- if (current && to && current !== to)
49
- blockers.push(`release impact chain does not reach ${to}`);
50
- if (status?.flow_pack.status !== "present" && from !== to)
54
+ if (status?.flow_pack.status !== "present")
51
55
  blockers.push("project flow-pack provenance is not present");
56
+ else {
57
+ try {
58
+ const file = path.resolve(root.root, status.flow_pack.manifest_path);
59
+ const prepared = prepareJsonFile(file, "Project flow-pack provenance");
60
+ validateSchema({ schemaName: "project-flow-pack-manifest", file, data: prepared.value,
61
+ schemaDir: fileURLToPath(new URL("../schemas/", import.meta.url)) });
62
+ if (!/^[a-f0-9]{40}$/.test(status.flow_pack.source_commit ?? "")
63
+ || status.flow_pack.canon_version_at_source_commit !== from)
64
+ blockers.push("project flow-pack provenance does not match its declared current version and commit");
65
+ }
66
+ catch (error) {
67
+ if (!(error instanceof AppError) || !["schema_validation", "invalid_json", "validation"].includes(error.code)
68
+ && !error.code.startsWith("input_file_"))
69
+ throw error;
70
+ blockers.push("project flow-pack provenance is invalid");
71
+ }
72
+ }
52
73
  if (blockers.length > 0)
53
74
  mode = "full_migration";
54
75
  if (forced)
55
76
  mode = "full_migration";
56
77
  return { ok: true, schema_id: "dd-flow/mb-upgrade-impact-assessment@1", project_root: root.root, source_version: from, target_version: to, mode, forced_full_migration: forced, chain, domains: [...domains].sort(), blockers, required_checks: [...new Set(chain.flatMap((item) => item.required_checks))], next_action: mode === "full_migration" ? "run full mb-upgrade contour" : "run selected mb-upgrade contour" };
57
78
  }
58
- function readImpacts(dir) {
79
+ export function readImpacts(dir) {
59
80
  if (!fs.existsSync(dir))
60
81
  return [];
61
- return fs.readdirSync(dir).filter((file) => file.endsWith(".json")).flatMap((file) => {
82
+ return fs.readdirSync(dir).filter(file => file.endsWith(".json")).sort().map(file => {
83
+ const source = path.join(dir, file);
62
84
  try {
63
- const value = JSON.parse(fs.readFileSync(path.join(dir, file), "utf8"));
64
- if (value.schema_id !== "dd-flow/release-impact@1")
65
- return [];
66
- if (typeof value.from_version !== "string" || typeof value.to_version !== "string" || !Array.isArray(value.domains) || !Array.isArray(value.required_checks))
67
- return [];
68
- if (!["patch", "targeted_minor", "full_migration"].includes(String(value.minimum_upgrade_mode)))
69
- return [];
70
- return [{ from_version: value.from_version, to_version: value.to_version, minimum_upgrade_mode: value.minimum_upgrade_mode, domains: value.domains.filter((item) => typeof item === "string"), path_migration: value.path_migration === true, runtime_migration: value.runtime_migration === true, required_checks: value.required_checks.filter((item) => typeof item === "string") }];
85
+ const prepared = prepareJsonFile(source, "Canonical release impact");
86
+ validateSchema({ schemaName: "release-impact", file: prepared.file, data: prepared.value,
87
+ schemaDir: fileURLToPath(new URL("../schemas/", import.meta.url)) });
88
+ return { ...prepared.value, source_reference: { path: prepared.file,
89
+ sha256: crypto.createHash("sha256").update(prepared.text).digest("hex") } };
71
90
  }
72
- catch {
73
- return [];
91
+ catch (error) {
92
+ throw new AppError("release_impact_invalid", "Canonical release impact is invalid; it cannot authorize a lightweight upgrade", 1, { file: source, effect: "no_effect", cause: error instanceof AppError ? error.code : "unreadable_release_impact" });
74
93
  }
75
94
  });
76
95
  }
@@ -267,7 +286,7 @@ function applyPreparedMigration(context, prepared, writeDb) {
267
286
  }
268
287
  return { ok: true, schema_id: "dd-flow/mb-upgrade-migration-apply@1", status: "applied", changed: true, backup: verifiedBackup, recovery: result };
269
288
  }
270
- export function assertUpgradeRunStorageReady(context, projectRoot) {
289
+ export function assertUpgradeRunStorageReady(context, projectRoot, engine) {
271
290
  const project = context.db.get("SELECT id FROM projects WHERE root = ?", [resolveProjectRoot(projectRoot)]);
272
291
  if (!project)
273
292
  throw new AppError("migration_not_applied", "mb-upgrade RUN requires a registered project with explicit migration apply", 1);
@@ -277,12 +296,57 @@ export function assertUpgradeRunStorageReady(context, projectRoot) {
277
296
  if (!source || !target)
278
297
  throw new AppError("migration_not_applied", "mb-upgrade RUN requires source and target Memory Bank versions", 1);
279
298
  const applied = context.db.get("SELECT value_json FROM runtime_config WHERE key = ?", [applyKey(project.id, source, target)]);
280
- if (!applied)
299
+ if (!applied || engine !== undefined) {
300
+ const assessment = assessMigrationImpact(context, { projectRoot });
301
+ const blockers = assessment.blockers;
302
+ const chain = assessment.chain;
303
+ if (!blockers.length && (engine !== undefined || chain.every(step => !step.runtime_migration))) {
304
+ const canon = upgradeCanon(context).canon;
305
+ const contract = prepareJsonFile(path.join(canon.flow_root, "flow-contract.json"), "Target flow contract");
306
+ let selectedEngineStorage;
307
+ if (engine !== undefined) {
308
+ if (!(engine instanceof AdmittedFrozenEngine))
309
+ throw new AppError("upgrade_storage_incompatible", "Upgrade requires an admitted selected engine", 1, { effect: "no_effect" });
310
+ const compatibility = prepareJsonFile(path.join(canon.flow_root, "compatibility.json"), "Target engine compatibility");
311
+ validateSchema({ schemaName: "compatibility", file: compatibility.file, data: compatibility.value,
312
+ schemaDir: fileURLToPath(new URL("../schemas/", import.meta.url)) });
313
+ const manifest = engine.manifest();
314
+ if (compatibility.value.memory_bank_version !== target || compatibility.value.dd_flow_cli.flow_contract !== contract.value.id
315
+ || manifest.package_name !== compatibility.value.dd_flow_cli.package_name || !semver.valid(compatibility.value.dd_flow_cli.min_version)
316
+ || semver.lt(manifest.package_version, compatibility.value.dd_flow_cli.min_version)
317
+ || manifest.package_name !== compatibility.value.engine.package_name
318
+ || !semver.satisfies(manifest.package_version, compatibility.value.engine.version_range, { includePrerelease: true }))
319
+ throw new AppError("upgrade_storage_incompatible", "Selected engine is incompatible with target canon", 1, { effect: "no_effect" });
320
+ selectedEngineStorage = engine.admitMemoryUpgradeStorage(context.ddFlowHome, resourceHome(context));
321
+ }
322
+ // Reuse the executing CLI's physical compatibility checks, not version text
323
+ // or a synthetic apply marker. This does not authorize storage migration.
324
+ const runOwner = fs.lstatSync(context.db.path);
325
+ if (!runOwner.isFile() || runOwner.nlink !== 1)
326
+ throw new AppError("upgrade_storage_incompatible", "Upgrade requires a regular single-linked runtime store", 1, { effect: "no_effect" });
327
+ const run = PreparedMemoryInitRuntimeStorage.prepareUpgrade(context);
328
+ const resource = PreparedMemoryInitResourceStorage.prepare(context);
329
+ const currentOwner = fs.lstatSync(context.db.path);
330
+ if (currentOwner.dev !== runOwner.dev || currentOwner.ino !== runOwner.ino)
331
+ throw new AppError("upgrade_storage_changed", "Runtime store changed during upgrade admission", 1, { effect: "no_effect" });
332
+ const manifest = prepareJsonFile(path.resolve(projectRoot, status.flow_pack.manifest_path), "Current flow-pack provenance");
333
+ return { schema_id: "dd-flow/mb-upgrade-storage-readiness@1", route: "verified-no-op",
334
+ source_version: source, target_version: target, chain,
335
+ current_provenance: { path: manifest.file, sha256: crypto.createHash("sha256").update(manifest.text).digest("hex") },
336
+ target: { root: canon.root, commit: canon.commit, contract_sha256: crypto.createHash("sha256").update(contract.text).digest("hex") },
337
+ ...(selectedEngineStorage ? { selected_engine_storage: selectedEngineStorage } : {}),
338
+ stores: [{ ...run.facts(), store_dev: runOwner.dev, store_ino: runOwner.ino }, resource.facts()].map(store => ({ store_kind: store.store_kind,
339
+ store_dev: store.store_dev, store_ino: store.store_ino,
340
+ store_path: store.store_path, schema_sha256: store.schema_sha256,
341
+ executing_writer_version: store.executing_writer_version })) };
342
+ }
281
343
  throw new AppError("migration_not_applied", "mb-upgrade RUN requires explicit migration apply before RUN creation", 1, {
282
344
  source_version: source,
283
345
  target_version: target,
346
+ blockers,
284
347
  next_action: "run migration plan, verify and explicit migration apply first"
285
348
  });
349
+ }
286
350
  const value = recordValue(parseJsonValue(applied.value_json));
287
351
  if (!value || !["applied", "verified_noop"].includes(stringValue(value.status) ?? "")) {
288
352
  throw new AppError("migration_not_applied", "mb-upgrade RUN has no successful explicit migration apply evidence", 1, {
@@ -290,6 +354,8 @@ export function assertUpgradeRunStorageReady(context, projectRoot) {
290
354
  target_version: target
291
355
  });
292
356
  }
357
+ return { schema_id: "dd-flow/mb-upgrade-storage-readiness@1", route: "existing-apply",
358
+ source_version: source, target_version: target, apply: value };
293
359
  }
294
360
  export function verifyMigrationReport(_context, input) {
295
361
  const filePath = path.resolve(input.file);
@@ -452,61 +518,45 @@ function hasActiveState(active) {
452
518
  function applyKey(projectId, source, target) {
453
519
  return `mb-upgrade.apply.${projectId}.${source}.${target}`;
454
520
  }
455
- function adjacentMigrationChain(source, target, impacts = []) {
456
- if (source === target)
457
- return { status: "ok", steps: [] };
458
- const sourceParts = parseSemver(source);
459
- const targetParts = parseSemver(target);
460
- if (!sourceParts || !targetParts) {
521
+ export function adjacentMigrationChain(source, target, impacts = []) {
522
+ if (semver.valid(source) !== source || semver.valid(target) !== target) {
461
523
  return { status: "unsupported", reason: "source or target version is not semver", steps: [] };
462
524
  }
463
- if (impacts.length > 0) {
464
- const steps = [];
465
- const visited = new Set();
466
- let current = source;
467
- while (current !== target) {
468
- if (visited.has(current))
469
- return { status: "unsupported", reason: "release impact chain contains a cycle", steps: [] };
470
- visited.add(current);
471
- const impact = impacts.find((item) => item.from_version === current);
472
- if (!impact)
473
- return { status: "unsupported", reason: `missing adjacent release impact from ${current}`, steps: [] };
474
- steps.push({
475
- id: `${impact.from_version}-to-${impact.to_version}`,
476
- from: impact.from_version,
477
- to: impact.to_version,
478
- status: "planned",
479
- minimum_upgrade_mode: impact.minimum_upgrade_mode,
480
- domains: impact.domains,
481
- path_migration: impact.path_migration,
482
- runtime_migration: impact.runtime_migration,
483
- required_checks: impact.required_checks
484
- });
485
- current = impact.to_version;
486
- const next = parseSemver(current);
487
- if (!next || compareParts(next, targetParts) > 0) {
488
- return { status: "unsupported", reason: `release impact chain overshoots target ${target}`, steps: [] };
489
- }
490
- }
491
- return { status: "ok", steps };
492
- }
493
- if (sourceParts.major !== targetParts.major) {
494
- return { status: "unsupported", reason: "major version migration requires canonical release-impact units", steps: [] };
495
- }
496
- if (targetParts.minor < sourceParts.minor || (targetParts.minor === sourceParts.minor && targetParts.patch < sourceParts.patch)) {
525
+ if (semver.gt(source, target))
497
526
  return { status: "unsupported", reason: "downgrade migrations are not supported", steps: [] };
498
- }
527
+ if (source === target)
528
+ return { status: "ok", steps: [] };
499
529
  const steps = [];
500
- let current = { ...sourceParts };
501
- while (compareParts(current, targetParts) < 0) {
502
- const next = nextAdjacent(current, targetParts);
530
+ const visited = new Set();
531
+ let current = source;
532
+ while (current !== target) {
533
+ if (visited.has(current))
534
+ return { status: "unsupported", reason: "release impact chain contains a cycle", steps: [] };
535
+ visited.add(current);
536
+ const matches = impacts.filter(item => item.from_version === current);
537
+ if (matches.length > 1)
538
+ return { status: "unsupported", reason: `ambiguous adjacent release impacts from ${current}`, steps: [] };
539
+ const impact = matches[0];
540
+ if (!impact)
541
+ return { status: "unsupported", reason: `missing adjacent release impact from ${current}`, steps: [] };
542
+ if (semver.valid(impact.to_version) !== impact.to_version || !semver.gt(impact.to_version, current)) {
543
+ return { status: "unsupported", reason: "release impact chain must advance through valid versions", steps: [] };
544
+ }
545
+ if (semver.gt(impact.to_version, target))
546
+ return { status: "unsupported", reason: `release impact chain overshoots target ${target}`, steps: [] };
503
547
  steps.push({
504
- id: `${formatParts(current)}-to-${formatParts(next)}`,
505
- from: formatParts(current),
506
- to: formatParts(next),
507
- status: "planned"
548
+ id: `${impact.from_version}-to-${impact.to_version}`,
549
+ from: impact.from_version,
550
+ to: impact.to_version,
551
+ status: "planned",
552
+ minimum_upgrade_mode: impact.minimum_upgrade_mode,
553
+ domains: impact.domains,
554
+ path_migration: impact.path_migration,
555
+ runtime_migration: impact.runtime_migration,
556
+ required_checks: impact.required_checks,
557
+ ...(impact.source_reference ? { source_reference: impact.source_reference } : {})
508
558
  });
509
- current = next;
559
+ current = impact.to_version;
510
560
  }
511
561
  return { status: "ok", steps };
512
562
  }
@@ -556,24 +606,3 @@ function recordValue(value) {
556
606
  function stringValue(value) {
557
607
  return typeof value === "string" && value.length > 0 ? value : null;
558
608
  }
559
- function parseSemver(value) {
560
- const match = value.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)/);
561
- if (!match?.[1] || !match[2] || !match[3])
562
- return null;
563
- return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
564
- }
565
- function compareParts(left, right) {
566
- if (left.major !== right.major)
567
- return left.major - right.major;
568
- if (left.minor !== right.minor)
569
- return left.minor - right.minor;
570
- return left.patch - right.patch;
571
- }
572
- function nextAdjacent(current, target) {
573
- if (current.minor < target.minor)
574
- return { major: current.major, minor: current.minor + 1, patch: 0 };
575
- return { major: current.major, minor: current.minor, patch: Math.min(current.patch + 1, target.patch) };
576
- }
577
- function formatParts(parts) {
578
- return `${parts.major}.${parts.minor}.${parts.patch}`;
579
- }
@@ -0,0 +1,45 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { createRenewalState, maintenanceRetryable, renewalDelay, renewalFailure, RENEWAL_POLICY } from "../harness-runtime/lib/lease-renewal.mjs";
4
+ import { processStartedAt } from "./managed-processes.js";
5
+ /** Birth and confirmation share one startup horizon. Retrying observations
6
+ * neither launches another child nor renews an unconfirmed resource lease. */
7
+ export function physicalStartup(databaseDeadlineAt) {
8
+ const deadline = performance.now() + RENEWAL_POLICY.startupMs;
9
+ const episode = createRenewalState();
10
+ const unavailable = () => new AppError("process_ownership_unknown", "Managed physical identity observation is unavailable", 1, { phase: "process_confirmation", effect: "no_effect", native_dispatch_started: false, reason: "physical_identity_unavailable" });
11
+ async function observe(operation, assertCurrent, leaseExpiresAt) {
12
+ let last;
13
+ const remaining = () => Math.max(0, Math.min(deadline - performance.now(), databaseDeadlineAt === undefined ? Infinity : databaseDeadlineAt - Date.now(), leaseExpiresAt === undefined ? Infinity : Date.parse(leaseExpiresAt) - Date.now() - RENEWAL_POLICY.safetyMs));
14
+ for (;;) {
15
+ assertCurrent();
16
+ if (!(remaining() > 0))
17
+ throw last ?? unavailable();
18
+ try {
19
+ const result = operation();
20
+ assertCurrent();
21
+ if (!(remaining() > 0))
22
+ throw new AppError("process_startup_expired", "Physical observation completed after its original admission horizon", 1, { phase: "process_confirmation", effect: "unknown", native_dispatch_started: false });
23
+ return result;
24
+ }
25
+ catch (error) {
26
+ // A stale owner/cancellation wins over a transient observation error.
27
+ assertCurrent();
28
+ const details = error instanceof AppError ? error.details : null;
29
+ if (!(error instanceof AppError) || error.code !== "process_ownership_unknown" || details?.reason !== "physical_identity_unavailable" || !maintenanceRetryable(error))
30
+ throw error;
31
+ last = error;
32
+ renewalFailure(episode);
33
+ if (!(remaining() > 0))
34
+ throw error;
35
+ await delay(Math.min(renewalDelay(episode), remaining()));
36
+ }
37
+ }
38
+ }
39
+ return { observe, birth: (assertCurrent) => observe(() => {
40
+ const started = processStartedAt(process.pid);
41
+ if (!started)
42
+ throw unavailable();
43
+ return { pid: process.pid, pid_started_at: started };
44
+ }, assertCurrent) };
45
+ }
@@ -0,0 +1,97 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { projectRuntimeRoot, resolveProjectRoot } from "../storage/paths.js";
5
+ import { formatFullId, parseFullEntityId, slugFromRoot } from "../domain/entity-ids.js";
6
+ import { appendAudit } from "./audit.js";
7
+ /** Resolve filesystem identity before entering a composite SQL writer. */
8
+ export class PreparedProjectRegistration {
9
+ #root;
10
+ constructor(root) { this.#root = root; Object.freeze(this); }
11
+ static prepare(input) {
12
+ return new PreparedProjectRegistration(resolveProjectRoot(input.root));
13
+ }
14
+ root() { return this.#root; }
15
+ }
16
+ export function registerProject(context, input) {
17
+ return registerPreparedProject(context, PreparedProjectRegistration.prepare(input));
18
+ }
19
+ /** Metadata-only registration; the composite caller owns its SQL transaction. */
20
+ export function registerPreparedProject(context, prepared) {
21
+ if (!(prepared instanceof PreparedProjectRegistration))
22
+ throw new AppError("validation", "Project registration must be prepared", 2);
23
+ const root = prepared.root();
24
+ const existing = context.db.get("SELECT * FROM projects WHERE root = ?", [root]);
25
+ const now = context.now();
26
+ const project = existing ?? newProjectRecord(context, root, now);
27
+ if (existing) {
28
+ context.db.run("UPDATE projects SET status = 'active', updated_at = ? WHERE id = ?", [now, existing.id]);
29
+ }
30
+ else {
31
+ context.db.run("INSERT INTO projects (id, short_id, slug, root, status, state_root, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
32
+ project.id,
33
+ project.short_id,
34
+ project.slug,
35
+ project.root,
36
+ project.status,
37
+ project.state_root,
38
+ project.created_at,
39
+ project.updated_at
40
+ ]);
41
+ appendAudit(context, {
42
+ projectId: project.id,
43
+ eventType: "project.registered",
44
+ payload: { project_id: project.id, root }
45
+ });
46
+ }
47
+ return { ok: true, project: { ...project, status: "active", updated_at: now }, db_path: context.db.path };
48
+ }
49
+ export function normalizeStoredRoot(root) {
50
+ const absolute = path.resolve(root);
51
+ if (fs.existsSync(absolute)) {
52
+ return fs.realpathSync(absolute);
53
+ }
54
+ const parent = path.dirname(absolute);
55
+ if (fs.existsSync(parent)) {
56
+ return path.join(fs.realpathSync(parent), path.basename(absolute));
57
+ }
58
+ return absolute;
59
+ }
60
+ export function findProjectByRoot(context, root) {
61
+ return context.db.get("SELECT * FROM projects WHERE root = ?", [root]);
62
+ }
63
+ export function requireProjectByRoot(context, root) {
64
+ // Callers may receive a path through a native adapter or shell. Resolve
65
+ // aliases such as macOS `/var` -> `/private/var` before comparing with the
66
+ // canonical root persisted in the project registry.
67
+ const canonical = normalizeStoredRoot(root);
68
+ const project = findProjectByRoot(context, canonical);
69
+ if (!project) {
70
+ throw new AppError("not_found", `Project is not registered: ${canonical}`, 1);
71
+ }
72
+ return project;
73
+ }
74
+ export function newProjectRecord(context, root, now) {
75
+ const slug = slugFromRoot(root);
76
+ const id = nextFullProjectId(context, slug);
77
+ const { shortId } = parseFullEntityId(id);
78
+ return {
79
+ id,
80
+ short_id: shortId,
81
+ slug,
82
+ root,
83
+ status: "active",
84
+ state_root: projectRuntimeRoot(context.ddFlowHome, id),
85
+ created_at: now,
86
+ updated_at: now
87
+ };
88
+ }
89
+ export function nextFullProjectId(context, slug) {
90
+ const rows = context.db.all("SELECT id FROM projects WHERE id LIKE 'PRJ-%'");
91
+ const used = rows
92
+ .map((row) => /^PRJ-(\d{3})-/.exec(row.id)?.[1])
93
+ .filter((value) => Boolean(value))
94
+ .map((value) => Number(value));
95
+ const next = Math.max(0, ...used) + 1;
96
+ return formatFullId("PRJ", next, slug);
97
+ }