@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
@@ -6,7 +6,7 @@ import { AppError } from "../shared/errors.js";
6
6
  import { agyWorkspaceHooks } from "../harness-runtime/lib/agy-hooks.mjs";
7
7
  /** Local environment/credential files are never an exported reviewer or EVAL input. */
8
8
  export function localEnvironmentAllowed(source) {
9
- const name = path.basename(source);
9
+ const name = path.basename(source).normalize("NFC").toLowerCase();
10
10
  return name === ".env.example" || (name !== ".env" && !name.startsWith(".env."));
11
11
  }
12
12
  /** A damaged/inaccessible repository is not an intentional gitless workspace. */
@@ -27,6 +27,18 @@ function closeAfterOpenFailure(db, error) {
27
27
  const require = createRequire(import.meta.url);
28
28
  const { DatabaseSync } = require("node:sqlite");
29
29
  const resourceDatabases = new Map();
30
+ const resourceDatabaseIdentities = new WeakMap();
31
+ function assertResourceDatabaseIdentity(database) {
32
+ const expected = resourceDatabaseIdentities.get(database);
33
+ let actual;
34
+ try {
35
+ actual = fs.lstatSync(database.path);
36
+ }
37
+ catch { /* Refuse missing targets without reopening or initializing. */ }
38
+ if (!expected || !actual?.isFile() || actual.dev !== expected.dev || actual.ino !== expected.ino) {
39
+ throw new AppError("resource_storage_changed", "The cached resource registry no longer matches its physical database; close the owned connection before retrying", 1);
40
+ }
41
+ }
30
42
  const publicationScopes = [];
31
43
  /** Defer committed cross-store projections until the surrounding writer exits. */
32
44
  export function withDeferredPublications(operation) {
@@ -334,6 +346,31 @@ export function ensureWorkLaunchStorage(db) {
334
346
  CREATE INDEX IF NOT EXISTS idx_work_launches_status ON work_launches(status, updated_at DESC);
335
347
  `);
336
348
  }
349
+ /** Exclusive fresh creation; never reinterpret an empty/old database as fresh. */
350
+ function claimNewStore(file) {
351
+ for (const candidate of [file, `${file}-wal`, `${file}-shm`, `${file}-journal`]) {
352
+ try {
353
+ fs.lstatSync(candidate);
354
+ }
355
+ catch (error) {
356
+ if (error.code === "ENOENT")
357
+ continue;
358
+ throw error;
359
+ }
360
+ throw new AppError("store_already_exists", "Fresh store creation refuses existing database or sidecar files", 1, { effect: "no_effect", path: candidate });
361
+ }
362
+ ensureDir(path.dirname(file));
363
+ let fd;
364
+ try {
365
+ fd = fs.openSync(file, "wx", 0o600);
366
+ }
367
+ catch (error) {
368
+ if (error.code === "EEXIST")
369
+ throw new AppError("store_already_exists", "Another owner already created this store", 1, { effect: "no_effect", path: file });
370
+ throw error;
371
+ }
372
+ fs.closeSync(fd);
373
+ }
337
374
  export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hookDeadlineAt) {
338
375
  channel("dd-flow.sqlite.ingress").publish({ phase: "store_open", store_kind: "run" });
339
376
  if (hookDeadlineAt !== undefined && !Number.isSafeInteger(hookDeadlineAt))
@@ -348,7 +385,11 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
348
385
  if (mode !== "read_existing")
349
386
  assertImportAdmission(ddFlowHome, importPermitId);
350
387
  const dbPath = path.join(ddFlowHome, "db.sqlite");
388
+ if (mode === "create_new")
389
+ claimNewStore(dbPath);
351
390
  const exists = fs.existsSync(dbPath);
391
+ if (mode === "prepared_existing" && (!exists || !fs.lstatSync(dbPath).isFile()))
392
+ throw new AppError("run_storage_unprepared", "Prepare a regular existing RUN store before execution admission", 1);
352
393
  if (mode === "read_existing" && !exists) {
353
394
  return emptyReadOnlyDatabase(dbPath);
354
395
  }
@@ -387,7 +428,12 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
387
428
  if (mode !== "read_existing" && mode !== "reconcile") {
388
429
  try {
389
430
  const initialize = () => { migrate(db); ensureRecoveryStorage(db); ensureLifecycleInvocationStorage(db); };
390
- if (mode !== "hook") {
431
+ if (mode === "prepared_existing") {
432
+ const required = readStoreWriterContract(db);
433
+ if (required !== STORE_WRITER_VERSION)
434
+ throw new AppError("store_writer_incompatible", "Existing RUN store has no compatible writer contract", 1, { required, supported: STORE_WRITER_VERSION });
435
+ }
436
+ else if (mode !== "hook") {
391
437
  const fresh = !db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get();
392
438
  if (fresh || readStoreWriterContract(db) !== null)
393
439
  executeStoreSchema(db, initialize, fresh);
@@ -486,6 +532,11 @@ export function getResourceDatabase(resourceHome, mode = "initialize", deadlineA
486
532
  deadline?.("resource_open");
487
533
  const dbPath = path.join(resourceHome, "runtime.sqlite");
488
534
  const cached = resourceDatabases.get(dbPath);
535
+ if (mode === "create_new") {
536
+ if (cached)
537
+ throw new AppError("store_already_exists", "Fresh store creation refuses a cached resource owner", 1, { effect: "no_effect", path: dbPath });
538
+ claimNewStore(dbPath);
539
+ }
489
540
  if (mode === "read_existing" || mode === "read_isolated") {
490
541
  // Reuse a current transaction's view without exposing its write capability.
491
542
  if (cached && mode === "read_existing") {
@@ -494,6 +545,7 @@ export function getResourceDatabase(resourceHome, mode = "initialize", deadlineA
494
545
  }
495
546
  if (!fs.existsSync(dbPath))
496
547
  return emptyReadOnlyDatabase(dbPath);
548
+ channel("dd-flow.sqlite.ingress").publish({ phase: "store_connection", store_kind: "resource" });
497
549
  const db = new DatabaseSync(dbPath, { readOnly: true });
498
550
  try {
499
551
  db.exec(`PRAGMA busy_timeout = ${deadline?.("resource_open") ?? 4000}; PRAGMA query_only = ON`);
@@ -510,6 +562,8 @@ export function getResourceDatabase(resourceHome, mode = "initialize", deadlineA
510
562
  if (mode === "prepared_existing" && !fs.existsSync(dbPath))
511
563
  throw new AppError("resource_storage_unprepared", "Prepare the resource registry before maintenance", 1);
512
564
  if (cached) {
565
+ if (mode === "prepared_existing")
566
+ assertResourceDatabaseIdentity(cached);
513
567
  const bounded = deadlineAt === undefined ? cached : cached.withDeadline(deadlineAt);
514
568
  if (mode === "prepared_existing")
515
569
  assertResourceStoreReady({ prepare: sql => ({ get: (...params) => bounded.get(sql, params), all: (...params) => bounded.all(sql, params), run: (...params) => bounded.run(sql, params) }) });
@@ -517,9 +571,11 @@ export function getResourceDatabase(resourceHome, mode = "initialize", deadlineA
517
571
  }
518
572
  if (mode !== "prepared_existing")
519
573
  ensureDir(resourceHome);
574
+ channel("dd-flow.sqlite.ingress").publish({ phase: "store_connection", store_kind: "resource" });
520
575
  const db = new DatabaseSync(dbPath);
521
- db.exec(`PRAGMA busy_timeout = ${deadline?.("resource_open") ?? 4000}`);
522
576
  try {
577
+ channel("dd-flow.sqlite.ingress").publish({ phase: "store_writer_registration", store_kind: "resource" });
578
+ db.exec(`PRAGMA busy_timeout = ${deadline?.("resource_open") ?? 4000}`);
523
579
  registerStoreWriter(db);
524
580
  }
525
581
  catch (error) {
@@ -535,8 +591,14 @@ export function getResourceDatabase(resourceHome, mode = "initialize", deadlineA
535
591
  catch (error) {
536
592
  return closeAfterOpenFailure(db, error);
537
593
  }
538
- db.exec("PRAGMA foreign_keys = ON");
539
- const fresh = !db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get();
594
+ let fresh;
595
+ try {
596
+ db.exec("PRAGMA foreign_keys = ON");
597
+ fresh = !db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get();
598
+ }
599
+ catch (error) {
600
+ return closeAfterOpenFailure(db, error);
601
+ }
540
602
  try {
541
603
  if (mode !== "prepared_existing")
542
604
  executeStoreSchema(db, `
@@ -587,6 +649,15 @@ export function getResourceDatabase(resourceHome, mode = "initialize", deadlineA
587
649
  resourceDatabases.delete(dbPath);
588
650
  db.close?.();
589
651
  });
652
+ try {
653
+ const identity = fs.lstatSync(dbPath);
654
+ if (!identity.isFile())
655
+ throw new AppError("resource_storage_changed", "Resource registry target is not a regular file", 1);
656
+ resourceDatabaseIdentities.set(database, { dev: identity.dev, ino: identity.ino });
657
+ }
658
+ catch (error) {
659
+ return closeAfterOpenFailure(db, error);
660
+ }
590
661
  resourceDatabases.set(dbPath, database);
591
662
  return deadlineAt === undefined ? database : database.withDeadline(deadlineAt);
592
663
  }
@@ -644,6 +715,7 @@ function databaseFacade(dbPath, writable, db, close = () => db.close?.(), deadli
644
715
  let afterCommit = [];
645
716
  let manualFrames = [];
646
717
  let readDepth = 0;
718
+ let ownedWriterToken = null;
647
719
  const writerTiming = channel("dd-flow.sqlite.writer");
648
720
  const ingressPhase = channel("dd-flow.sqlite.ingress");
649
721
  const storeKind = path.basename(dbPath) === "runtime.sqlite" ? "resource" : "run";
@@ -674,6 +746,7 @@ function databaseFacade(dbPath, writable, db, close = () => db.close?.(), deadli
674
746
  bound("writer_reservation");
675
747
  db.exec("BEGIN IMMEDIATE");
676
748
  reserved = true;
749
+ ownedWriterToken = Object.freeze({});
677
750
  if (ingressPhase.hasSubscribers)
678
751
  ingressPhase.publish({ phase: "writer_reserved", store_kind: storeKind });
679
752
  };
@@ -841,6 +914,9 @@ function databaseFacade(dbPath, writable, db, close = () => db.close?.(), deadli
841
914
  assertUsable();
842
915
  if (readDepth)
843
916
  throw new AppError("read_snapshot_write", "Cannot execute SQL inside a read snapshot", 1);
917
+ // Raw SQL may replace the transaction (including behind comments).
918
+ // Do not infer continuity by parsing SQL or its final isTransaction.
919
+ ownedWriterToken = null;
844
920
  try {
845
921
  db.exec(sql);
846
922
  applyManualBoundary(sql);
@@ -866,9 +942,11 @@ function databaseFacade(dbPath, writable, db, close = () => db.close?.(), deadli
866
942
  }
867
943
  },
868
944
  execSchema: (sql) => { assertUsable(); if (db.isTransaction && !reserved)
869
- throw new AppError("write_transaction_unowned", "Cannot change schema inside an unowned transaction", 1); executeStoreSchema(db, sql); },
945
+ throw new AppError("write_transaction_unowned", "Cannot change schema inside an unowned transaction", 1); ownedWriterToken = null; executeStoreSchema(db, sql); },
870
946
  beginWriteTransaction,
871
947
  writeTransaction,
948
+ isOwnedWriteTransaction: () => { assertUsable(); return writable && reserved && ownedWriterToken !== null && db.isTransaction && readDepth === 0; },
949
+ ownedWriteTransactionToken: () => { assertUsable(); return writable && reserved && db.isTransaction && readDepth === 0 ? ownedWriterToken : null; },
872
950
  readSnapshot,
873
951
  afterCommit: (operation) => { assertUsable(); if (readDepth)
874
952
  throw new AppError("read_snapshot_write", "Cannot queue publication inside a read snapshot", 1); if (db.isTransaction)
@@ -904,7 +982,7 @@ function errorDetails(error) {
904
982
  return { message: String(value?.message ?? error), ...(typeof value?.code === "string" ? { code: value.code } : {}), ...(typeof value?.errcode === "number" ? { sqlite_extended_code: value.errcode } : {}) };
905
983
  }
906
984
  function configureDatabase(db, mode, ingressDeadline) {
907
- if (mode !== "read_existing") {
985
+ if (mode !== "read_existing" && mode !== "prepared_existing") {
908
986
  // Concurrent first openers can collide while changing journal mode, even
909
987
  // with busy_timeout set. Retrying this configuration does not replay writes.
910
988
  const deadline = performance.now() + 4000, pause = new Int32Array(new SharedArrayBuffer(4));
@@ -5,6 +5,15 @@ export const STORE_WRITER_VERSION = 2;
5
5
  const version = STORE_WRITER_VERSION;
6
6
  const table = "dd_flow_writer_contract";
7
7
  const quote = (name) => `"${name.replaceAll('"', '""')}"`;
8
+ /** The executing writer's fence definition, also used for readonly admission. */
9
+ export function storeWriterTriggerSql(name, action) {
10
+ const key = crypto.createHash("sha256").update(name).digest("hex").slice(0, 24);
11
+ const trigger = `dd_flow_writer_${key}_${action.toLowerCase()}`;
12
+ return { name: trigger, sql: `CREATE TRIGGER ${quote(trigger)} BEFORE ${action} ON ${quote(name)} BEGIN
13
+ SELECT CASE WHEN dd_flow_writer_version() <> (SELECT version FROM ${table} WHERE id = 1)
14
+ OR (SELECT COUNT(*) FROM ${table}) <> 1 THEN RAISE(ABORT, 'store_writer_incompatible') END;
15
+ END;` };
16
+ }
8
17
  /** Publish additive schema and its writer fences in the same transaction. */
9
18
  export function executeStoreSchema(db, sql, initialize = false) {
10
19
  const ownsTransaction = !db.isTransaction;
@@ -64,14 +73,9 @@ export function installStoreWriterContract(db) {
64
73
  throw new AppError("store_writer_incompatible", "Store writer contract changed before migration", 1);
65
74
  const tables = db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all();
66
75
  for (const target of tables) {
67
- const key = crypto.createHash("sha256").update(target.name).digest("hex").slice(0, 24);
68
76
  for (const action of ["INSERT", "UPDATE", "DELETE"]) {
69
- const trigger = quote(`dd_flow_writer_${key}_${action.toLowerCase()}`);
70
- db.exec(`DROP TRIGGER IF EXISTS ${trigger};
71
- CREATE TRIGGER ${trigger} BEFORE ${action} ON ${quote(target.name)} BEGIN
72
- SELECT CASE WHEN dd_flow_writer_version() <> (SELECT version FROM ${table} WHERE id = 1)
73
- OR (SELECT COUNT(*) FROM ${table}) <> 1 THEN RAISE(ABORT, 'store_writer_incompatible') END;
74
- END;`);
77
+ const trigger = storeWriterTriggerSql(target.name, action);
78
+ db.exec(`DROP TRIGGER IF EXISTS ${quote(trigger.name)}; ${trigger.sql}`);
75
79
  }
76
80
  }
77
81
  if (ownsTransaction)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.137",
3
+ "version": "0.9.0-beta.138",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",
@@ -27,7 +27,13 @@
27
27
  "build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
28
28
  "typecheck": "tsc --noEmit",
29
29
  "lint": "eslint . --max-warnings=0",
30
- "test": "pnpm test:release && pnpm test:integration && pnpm test:runtime-sensitive",
30
+ "test": "pnpm test:hybrid && pnpm test:e2 && pnpm test:e3 && pnpm test:release && pnpm test:integration && pnpm test:runtime-sensitive",
31
+ "test:hybrid": "node --test scripts/hybrid-contracts.test.mjs",
32
+ "test:hybrid:canon": "node --import ./scripts/source-test-loader.mjs --test scripts/hybrid-canon.test.mjs scripts/hybrid-inventory.test.mjs scripts/hybrid-consumers.test.mjs scripts/hybrid-schema-copies.test.mjs",
33
+ "test:e2": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/e2-*.test.mjs",
34
+ "test:e2:installed": "node --test scripts/verify-e2-built-cli.test.mjs scripts/verify-e2-installed-cli.test.mjs",
35
+ "test:e3": "node --import ./scripts/source-test-loader.mjs --test --test-concurrency=1 scripts/e3-*.test.mjs",
36
+ "test:e3:canon": "node --import ./scripts/source-test-loader.mjs --test scripts/verify-e3-canon.test.mjs",
31
37
  "test:release": "node --test scripts/verify-release-build.test.mjs && vitest run --project=release",
32
38
  "test:integration": "vitest run --project='integration-*'",
33
39
  "test:fast": "vitest run --project=integration-fast",
@@ -38,7 +44,9 @@
38
44
  "version-packages": "changeset version",
39
45
  "release:candidate:pack": "node scripts/release-candidate.mjs pack",
40
46
  "release:candidate:accept": "node scripts/release-candidate.mjs accept",
41
- "release:publish": "node scripts/publish-release.mjs"
47
+ "release:publish": "node scripts/publish-release.mjs",
48
+ "release:hybrid:produce": "node tools/produce-hybrid-release.mjs",
49
+ "release:canon:archive": "node tools/create-canon-release.mjs"
42
50
  },
43
51
  "devDependencies": {
44
52
  "@opencode/client": "2.0.28",
@@ -46,6 +54,7 @@
46
54
  "@changesets/cli": "^2.31.0",
47
55
  "@eslint/js": "^9.39.1",
48
56
  "@types/node": "^24.10.1",
57
+ "@types/semver": "7.8.0",
49
58
  "eslint": "^9.39.1",
50
59
  "typescript": "^5.9.3",
51
60
  "typescript-eslint": "^8.47.0",
@@ -54,6 +63,7 @@
54
63
  "dependencies": {
55
64
  "ajv": "^8.20.0",
56
65
  "jsonc-parser": "3.3.1",
66
+ "semver": "7.8.1",
57
67
  "shell-quote": "^1.10.0",
58
68
  "smol-toml": "^1.6.1",
59
69
  "tree-sitter": "0.25.1",
@@ -0,0 +1,33 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { produceCanonArchive } from "../dist/services/release-provenance.js";
6
+
7
+ const args = process.argv.slice(2), names = ["canon-root", "commit", "output"];
8
+ if (args.includes("--help") || args.includes("-h")) {
9
+ process.stdout.write("Usage: node tools/create-canon-release.mjs --canon-root <checkout> --commit <40-hex-commit> --output <new-archive.tgz>\nCreates an immutable Git-object archive with reviewed aliases materialized. Does not publish or qualify a release.\n");
10
+ } else {
11
+ let temporary;
12
+ let ownsTemporary = false;
13
+ try {
14
+ const options = new Map();
15
+ for (let index = 0; index < args.length; index += 2) {
16
+ const name = args[index]?.slice(2), value = args[index + 1];
17
+ if (!args[index]?.startsWith("--") || !names.includes(name) || options.has(name) || !value || value.startsWith("--")) throw new Error("invalid arguments");
18
+ options.set(name, value);
19
+ }
20
+ if (names.some(name => !options.has(name))) throw new Error("missing arguments");
21
+ const bytes = produceCanonArchive(options.get("canon-root"), options.get("commit"));
22
+ const output = path.resolve(options.get("output"));
23
+ temporary = path.join(path.dirname(output), `.canon-archive-${randomUUID()}.tmp`);
24
+ const fd = fs.openSync(temporary, "wx", 0o600);
25
+ ownsTemporary = true;
26
+ try { fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
27
+ fs.linkSync(temporary, output); // Atomic no-replace publication, including dangling symlink targets.
28
+ process.stdout.write(`${JSON.stringify({ ok: true, commit: options.get("commit"), output, bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") })}\n`);
29
+ } catch {
30
+ process.stderr.write("Canonical archive production refused; check committed source and a new output path.\n");
31
+ process.exitCode = 1;
32
+ } finally { if (temporary && ownsTemporary) fs.rmSync(temporary, { force: true }); }
33
+ }
@@ -0,0 +1,41 @@
1
+ import { createPrivateKey } from "node:crypto";
2
+ import { Buffer } from "node:buffer";
3
+ import process from "node:process";
4
+ import { publishReleaseBundle } from "../dist/services/release-producer.js";
5
+ import { readBoundedReleaseFile } from "../dist/services/release-source.js";
6
+ import { createReleaseCompatibility, installedCatalogVersions } from "../dist/services/release-compatibility.js";
7
+ import { getCliVersionReport } from "../dist/services/build-info.js";
8
+ import { verifyProducerSource, verifyCanonArchiveSource } from "../dist/services/release-provenance.js";
9
+
10
+ const names = ["payload-file", "canon-root", "canon-archive", "engine-archive", "key-file", "output", "channel"];
11
+ const args = process.argv.slice(2);
12
+ if (args.includes("--help") || args.includes("-h")) {
13
+ process.stdout.write(`Produce an immutable local signed release bundle from existing qualification evidence.\nUsage: node tools/produce-hybrid-release.mjs ${names.map(name => `--${name} <value>`).join(" ")}\nThis does not publish remotely or qualify a route. Private keys never enter the output.\n`);
14
+ } else {
15
+ try {
16
+ const options = new Map();
17
+ for (let index = 0; index < args.length; index += 2) {
18
+ const name = args[index]?.slice(2), value = args[index + 1];
19
+ if (!args[index]?.startsWith("--") || !names.includes(name) || options.has(name) || !value || value.startsWith("--")) throw new Error("invalid arguments");
20
+ options.set(name, value);
21
+ }
22
+ if (names.some(name => !options.has(name))) throw new Error("missing arguments");
23
+ const compatibility = createReleaseCompatibility({ channel: options.get("channel"), catalogVersions: installedCatalogVersions, routerVersion: getCliVersionReport().cli.version });
24
+ const payload = JSON.parse(readBoundedReleaseFile(options.get("payload-file"), 16 * 1024 * 1024).toString("utf8"));
25
+ const manifest = JSON.parse(Buffer.from(payload.manifest_base64, "base64").toString("utf8"));
26
+ verifyProducerSource(options.get("canon-root"), payload.source, manifest.canon.version);
27
+ const canonArchive = readBoundedReleaseFile(options.get("canon-archive"), 256 * 1024 * 1024);
28
+ verifyCanonArchiveSource(options.get("canon-root"), payload.source.commit, manifest.canon.archive, canonArchive);
29
+ const result = publishReleaseBundle(options.get("output"), {
30
+ payload,
31
+ privateKey: createPrivateKey(readBoundedReleaseFile(options.get("key-file"), 64 * 1024)), compatibility,
32
+ canonArchive,
33
+ engineArchive: readBoundedReleaseFile(options.get("engine-archive"), 256 * 1024 * 1024)
34
+ });
35
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
36
+ } catch {
37
+ // Key/parser/network excerpts must never appear in producer diagnostics.
38
+ process.stderr.write("Release production refused; check explicit inputs and qualification evidence.\n");
39
+ process.exitCode = 1;
40
+ }
41
+ }