@deksden-com/dd-flow-cli 0.9.0-beta.9 → 0.9.0-beta.91

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 (144) hide show
  1. package/CHANGELOG.md +467 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +305 -0
  5. package/dist/cli/help.js +89 -13
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +116 -0
  8. package/dist/cli/run-cli.js +1421 -384
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  11. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  12. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  13. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  14. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  15. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  16. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  17. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  18. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  19. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  21. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  22. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  23. package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
  24. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  25. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  26. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  27. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  28. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  29. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  30. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  32. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  33. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  34. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  36. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  37. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  38. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  39. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  40. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  41. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  42. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  43. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  44. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  45. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  46. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  47. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  48. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  49. package/dist/schemas/agent-profile.schema.json +1 -1
  50. package/dist/schemas/code-review-result.schema.json +2 -2
  51. package/dist/schemas/code-work-batch.schema.json +4 -3
  52. package/dist/schemas/code-work-result.schema.json +4 -4
  53. package/dist/schemas/plan-review-result.schema.json +2 -2
  54. package/dist/schemas/run-control-receipt.schema.json +99 -0
  55. package/dist/schemas/run-control-request.schema.json +36 -0
  56. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  57. package/dist/services/canon.js +11 -8
  58. package/dist/services/cleanup.js +57 -13
  59. package/dist/services/cli-operation-classifier.js +22 -24
  60. package/dist/services/code-checks.js +543 -96
  61. package/dist/services/codex-hook-delivery.js +28 -0
  62. package/dist/services/command-context.js +80 -0
  63. package/dist/services/config.js +11 -8
  64. package/dist/services/continuation-outcome.js +20 -0
  65. package/dist/services/controller-fanout.js +132 -0
  66. package/dist/services/dashboard.js +103 -31
  67. package/dist/services/engines.js +121 -76
  68. package/dist/services/eval-snapshots.js +750 -73
  69. package/dist/services/execution-policy.js +170 -0
  70. package/dist/services/external-work-launch.js +110 -0
  71. package/dist/services/harness-adapter.js +147 -24
  72. package/dist/services/harness-config.js +2 -0
  73. package/dist/services/hooks.js +587 -299
  74. package/dist/services/lanes.js +61 -53
  75. package/dist/services/lifecycle-command.js +103 -17
  76. package/dist/services/lifecycle-contract.js +30 -0
  77. package/dist/services/lifecycle-invocations.js +1122 -0
  78. package/dist/services/managed-daemon-binding.js +38 -0
  79. package/dist/services/managed-processes.js +283 -27
  80. package/dist/services/merge-queue.js +179 -104
  81. package/dist/services/merge-server.js +73 -28
  82. package/dist/services/migrations.js +14 -9
  83. package/dist/services/native-daemon-history.js +49 -0
  84. package/dist/services/native-session-control.js +39 -0
  85. package/dist/services/plan-runtime.js +19 -8
  86. package/dist/services/plans.js +28 -25
  87. package/dist/services/projects.js +18 -5
  88. package/dist/services/prompts.js +23 -16
  89. package/dist/services/protocols.js +64 -20
  90. package/dist/services/recovery-observation-budget.js +61 -0
  91. package/dist/services/recovery-snapshot-database.js +107 -0
  92. package/dist/services/repair-intents.js +40 -0
  93. package/dist/services/run-control-receipt.js +56 -0
  94. package/dist/services/run-control-worker.js +363 -0
  95. package/dist/services/run-control.js +876 -0
  96. package/dist/services/run-controller-adapter.js +222 -0
  97. package/dist/services/run-controller-capture.js +134 -0
  98. package/dist/services/run-controller-process.js +189 -0
  99. package/dist/services/run-controller-recovery.js +222 -0
  100. package/dist/services/run-controller-state.js +36 -0
  101. package/dist/services/run-controller.js +793 -0
  102. package/dist/services/run-engine-bindings.js +1 -1
  103. package/dist/services/run-fork.js +138 -0
  104. package/dist/services/run-observations.js +112 -0
  105. package/dist/services/run-recovery-runtime.js +69 -0
  106. package/dist/services/run-recovery.js +343 -0
  107. package/dist/services/runs.js +368 -74
  108. package/dist/services/runtime-budget.js +434 -0
  109. package/dist/services/runtime-command.js +43 -0
  110. package/dist/services/runtime-scope-capture.js +50 -0
  111. package/dist/services/runtime-scope-control.js +450 -0
  112. package/dist/services/runtime-scope-resume.js +565 -0
  113. package/dist/services/runtime-scope-stop.js +99 -0
  114. package/dist/services/runtime-scope-worker.js +243 -0
  115. package/dist/services/runtime-service.js +97 -0
  116. package/dist/services/schema-validation.js +10 -8
  117. package/dist/services/sessions.js +35 -69
  118. package/dist/services/stage-blocker.js +17 -7
  119. package/dist/services/stage-context.js +44 -18
  120. package/dist/services/stage-lifecycle.js +99 -102
  121. package/dist/services/stage-pause.js +84 -55
  122. package/dist/services/stage-work-graph.js +35 -0
  123. package/dist/services/usage.js +37 -93
  124. package/dist/services/vnext-code-review.js +195 -88
  125. package/dist/services/vnext-code.js +285 -252
  126. package/dist/services/vnext-execution-profile.js +5 -3
  127. package/dist/services/vnext-fanout.js +160 -17
  128. package/dist/services/vnext-merge.js +291 -104
  129. package/dist/services/vnext-plan-review.js +139 -81
  130. package/dist/services/vnext-plan.js +138 -69
  131. package/dist/services/vnext-protocolize.js +129 -56
  132. package/dist/services/vnext-specify.js +88 -65
  133. package/dist/services/work-registry.js +716 -168
  134. package/dist/services/workspace-bootstrap.js +76 -0
  135. package/dist/services/worktrees.js +85 -35
  136. package/dist/shared/entity-references.js +8 -0
  137. package/dist/shared/errors.js +12 -0
  138. package/dist/storage/database.js +459 -32
  139. package/dist/storage/paths.js +17 -4
  140. package/dist/storage/work-references.js +23 -0
  141. package/dist/storage/writer-contract.js +77 -0
  142. package/dist/storage/writer-migration.js +102 -0
  143. package/package.json +24 -13
  144. package/tools/repair-paused-run-status.mjs +59 -0
@@ -15,6 +15,8 @@ export function resolveProjectRoot(root) {
15
15
  if (!fs.existsSync(absolute)) {
16
16
  throw new AppError("not_found", `Project root does not exist: ${absolute}`, 1);
17
17
  }
18
+ if (!fs.statSync(absolute).isDirectory())
19
+ throw new AppError("validation", "Project root must be a directory", 2, { project_root: absolute });
18
20
  return fs.realpathSync(absolute);
19
21
  }
20
22
  /** Resolve an existing path to its filesystem identity, or normalize a future path. */
@@ -24,10 +26,16 @@ export function canonicalPath(value, mustExist = true) {
24
26
  return fs.realpathSync(absolute);
25
27
  if (mustExist)
26
28
  throw new AppError("not_found", `Path does not exist: ${absolute}`, 1, { path: absolute });
27
- const parent = path.dirname(absolute);
28
- if (!fs.existsSync(parent))
29
- return absolute;
30
- return path.join(fs.realpathSync(parent), path.basename(absolute));
29
+ const missing = [];
30
+ let parent = absolute;
31
+ while (!fs.existsSync(parent)) {
32
+ missing.unshift(path.basename(parent));
33
+ const next = path.dirname(parent);
34
+ if (next === parent)
35
+ return absolute;
36
+ parent = next;
37
+ }
38
+ return path.join(fs.realpathSync(parent), ...missing);
31
39
  }
32
40
  export function assertPathWithin(root, candidate, label = "path") {
33
41
  const canonicalRoot = canonicalPath(root);
@@ -100,6 +108,11 @@ export function stateJsonPath(projectRoot, protocolId) {
100
108
  export function planJsonPath(projectRoot, protocolId) {
101
109
  return path.join(protocolDir(projectRoot, protocolId), "plan.json");
102
110
  }
111
+ /** Project canonical plan takes precedence over a retained workspace location. */
112
+ export function resolveProtocolPlanPath(projectRoot, protocolId, recordedPath) {
113
+ const canonical = planJsonPath(projectRoot, protocolId);
114
+ return fs.existsSync(canonical) ? canonical : recordedPath;
115
+ }
103
116
  export function projectHome(ddFlowHome, projectId) {
104
117
  return path.join(ddFlowHome, "projects", projectId);
105
118
  }
@@ -0,0 +1,23 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ /** Read-only lookup; scope applies to exact IDs as well as short aliases. */
3
+ export function resolveWorkReference(context, id, scope = {}) {
4
+ const clauses = ["1 = 1"];
5
+ const params = [];
6
+ if (scope.projectId) {
7
+ clauses.push("project_id = ?");
8
+ params.push(scope.projectId);
9
+ }
10
+ if (scope.runId) {
11
+ clauses.push("run_id = ?");
12
+ params.push(scope.runId);
13
+ }
14
+ const where = clauses.join(" AND ");
15
+ const exact = context.db.get(`SELECT * FROM works WHERE ${where} AND work_id = ?`, [...params, id]);
16
+ if (exact)
17
+ return exact;
18
+ const matches = /^WRK-\d{3,}$/.test(id)
19
+ ? context.db.all(`SELECT * FROM works WHERE ${where} AND work_id LIKE ? ORDER BY work_id`, [...params, `${id}-%`]) : [];
20
+ if (matches.length !== 1)
21
+ throw new AppError(matches.length ? "ambiguous_work_alias" : "not_found", matches.length ? "Short Work alias is ambiguous in the current RUN" : "Work is not registered in the current RUN", 1, { work_id: id, matches: matches.map(work => work.work_id) });
22
+ return matches[0];
23
+ }
@@ -0,0 +1,77 @@
1
+ import crypto from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ export const STORE_WRITER_VERSION = 2;
4
+ const version = STORE_WRITER_VERSION;
5
+ const table = "dd_flow_writer_contract";
6
+ const quote = (name) => `"${name.replaceAll('"', '""')}"`;
7
+ /** Publish additive schema and its writer fences in the same transaction. */
8
+ export function executeStoreSchema(db, sql, initialize = false) {
9
+ const ownsTransaction = !db.isTransaction;
10
+ db.exec(ownsTransaction ? "BEGIN IMMEDIATE" : "SAVEPOINT dd_flow_schema_change");
11
+ try {
12
+ const protectedStore = readStoreWriterContract(db) !== null;
13
+ if (initialize && !protectedStore && db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' LIMIT 1").get()) {
14
+ throw new AppError("store_initialization_conflict", "Another writer initialized this store; explicit migration is required", 1);
15
+ }
16
+ const before = db.prepare("PRAGMA schema_version").get();
17
+ if (typeof sql === "string")
18
+ db.exec(sql);
19
+ else
20
+ sql();
21
+ const after = db.prepare("PRAGMA schema_version").get();
22
+ if (initialize || (protectedStore && before.schema_version !== after.schema_version))
23
+ installStoreWriterContract(db);
24
+ db.exec(ownsTransaction ? "COMMIT" : "RELEASE dd_flow_schema_change");
25
+ }
26
+ catch (error) {
27
+ db.exec(ownsTransaction ? "ROLLBACK" : "ROLLBACK TO dd_flow_schema_change; RELEASE dd_flow_schema_change");
28
+ throw error;
29
+ }
30
+ }
31
+ /** Registration is connection-local and never changes historical input bytes. */
32
+ export function registerStoreWriter(db) {
33
+ const current = readStoreWriterContract(db);
34
+ if (current !== null && current !== version)
35
+ throw new AppError("store_writer_incompatible", "This CLI cannot write the store's required contract", 1, { required: current, supported: version });
36
+ db.function("dd_flow_writer_version", () => version);
37
+ }
38
+ export function readStoreWriterContract(db) {
39
+ if (!db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?").get(table))
40
+ return null;
41
+ const rows = db.prepare(`SELECT id, version FROM ${table}`).all();
42
+ if (rows.length !== 1 || rows[0]?.id !== 1 || !Number.isSafeInteger(rows[0].version) || rows[0].version < 1)
43
+ throw new AppError("store_writer_contract_invalid", "Store writer contract is malformed", 1);
44
+ return rows[0].version;
45
+ }
46
+ /** Install only at an explicit migration boundary; ordinary reads never call it. */
47
+ export function installStoreWriterContract(db) {
48
+ registerStoreWriter(db);
49
+ const ownsTransaction = !db.isTransaction;
50
+ if (ownsTransaction)
51
+ db.exec("BEGIN IMMEDIATE");
52
+ try {
53
+ db.exec(`CREATE TABLE IF NOT EXISTS ${table} (id INTEGER PRIMARY KEY CHECK (id = 1), version INTEGER NOT NULL);
54
+ INSERT OR IGNORE INTO ${table} (id, version) VALUES (1, ${version});`);
55
+ if (readStoreWriterContract(db) !== version)
56
+ throw new AppError("store_writer_incompatible", "Store writer contract changed before migration", 1);
57
+ const tables = db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all();
58
+ for (const target of tables) {
59
+ const key = crypto.createHash("sha256").update(target.name).digest("hex").slice(0, 24);
60
+ for (const action of ["INSERT", "UPDATE", "DELETE"]) {
61
+ const trigger = quote(`dd_flow_writer_${key}_${action.toLowerCase()}`);
62
+ db.exec(`DROP TRIGGER IF EXISTS ${trigger};
63
+ CREATE TRIGGER ${trigger} BEFORE ${action} ON ${quote(target.name)} BEGIN
64
+ SELECT CASE WHEN dd_flow_writer_version() <> (SELECT version FROM ${table} WHERE id = 1)
65
+ OR (SELECT COUNT(*) FROM ${table}) <> 1 THEN RAISE(ABORT, 'store_writer_incompatible') END;
66
+ END;`);
67
+ }
68
+ }
69
+ if (ownsTransaction)
70
+ db.exec("COMMIT");
71
+ }
72
+ catch (error) {
73
+ if (ownsTransaction)
74
+ db.exec("ROLLBACK");
75
+ throw error;
76
+ }
77
+ }
@@ -0,0 +1,102 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { installStoreWriterContract, readStoreWriterContract, registerStoreWriter, STORE_WRITER_VERSION } from "./writer-contract.js";
7
+ /** Explicit one-store cutover. The caller must separately drain external owners. */
8
+ export function migrateStoreWriter(file, backup) {
9
+ if (!path.isAbsolute(file) || !path.isAbsolute(backup) || file === backup || !fs.existsSync(file) || !fs.statSync(file).isFile())
10
+ throw new AppError("store_migration_path_invalid", "Migration requires an existing database and a new absolute backup path", 1);
11
+ const source = fs.realpathSync(file);
12
+ const destination = path.join(fs.realpathSync(path.dirname(backup)), path.basename(backup));
13
+ if (fs.lstatSync(file).isSymbolicLink() || [source, `${source}-wal`, `${source}-shm`, `${source}-journal`].includes(destination))
14
+ throw new AppError("store_migration_path_invalid", "Backup cannot alias the source database or its SQLite sidecars", 1);
15
+ try {
16
+ const existing = fs.lstatSync(backup);
17
+ if (!existing.isFile() || existing.isSymbolicLink())
18
+ throw new AppError("store_migration_path_invalid", "Backup path already exists", 1);
19
+ const reader = new DatabaseSync(file, { readOnly: true });
20
+ try {
21
+ const recorded = reader.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'dd_flow_writer_migrations'").get()
22
+ ? reader.prepare("SELECT receipt_json FROM dd_flow_writer_migrations WHERE backup_path = ?").get(destination) : null;
23
+ const receipt = recorded ? JSON.parse(String(recorded.receipt_json)) : null;
24
+ if (!receipt || receipt.database !== file || receipt.backup !== backup || receipt.writer_version !== readStoreWriterContract(reader) || receipt.backup_sha256 !== crypto.createHash("sha256").update(fs.readFileSync(backup)).digest("hex")) {
25
+ throw new AppError("store_migration_path_invalid", "Existing backup has no matching committed migration receipt", 1);
26
+ }
27
+ return receipt;
28
+ }
29
+ finally {
30
+ reader.close();
31
+ }
32
+ }
33
+ catch (error) {
34
+ if (error.code !== "ENOENT")
35
+ throw error;
36
+ }
37
+ const db = new DatabaseSync(file);
38
+ try {
39
+ db.exec("PRAGMA busy_timeout = 1000");
40
+ registerStoreWriter(db);
41
+ db.exec("BEGIN IMMEDIATE");
42
+ try {
43
+ const previous = readStoreWriterContract(db);
44
+ const tables = new Set(db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table'").all().map(row => row.name));
45
+ const gates = [
46
+ ["managed_processes", "state IS NULL OR state NOT IN ('stopped','failed')"],
47
+ ["run_controllers", "status IS NULL OR status NOT IN ('completed','stop_target_reached','cancelled','superseded')"],
48
+ ["run_control_workers", "status IS NULL OR status NOT IN ('completed','failed','superseded')"],
49
+ ["work_sessions", "status IS NULL OR status NOT IN ('completed','failed','cancelled')"]
50
+ ];
51
+ for (const [table, predicate] of gates) {
52
+ if (tables.has(table) && db.prepare(`SELECT 1 FROM ${table} WHERE ${predicate} LIMIT 1`).get())
53
+ throw new AppError("store_migration_not_drained", "Store still contains unresolved writer ownership", 1, { table });
54
+ }
55
+ // A separate reader sees committed WAL while this connection prevents new writes.
56
+ const reader = new DatabaseSync(file, { readOnly: true });
57
+ try {
58
+ reader.exec(`VACUUM INTO '${backup.replaceAll("'", "''")}'`);
59
+ }
60
+ finally {
61
+ reader.close();
62
+ }
63
+ const descriptor = fs.openSync(backup, "r");
64
+ try {
65
+ fs.fsyncSync(descriptor);
66
+ }
67
+ finally {
68
+ fs.closeSync(descriptor);
69
+ }
70
+ const directory = fs.openSync(path.dirname(backup), "r");
71
+ try {
72
+ fs.fsyncSync(directory);
73
+ }
74
+ finally {
75
+ fs.closeSync(directory);
76
+ }
77
+ const verification = new DatabaseSync(backup, { readOnly: true });
78
+ try {
79
+ const check = verification.prepare("PRAGMA integrity_check").get();
80
+ if (Object.values(check)[0] !== "ok" || readStoreWriterContract(verification) !== previous)
81
+ throw new AppError("store_migration_backup_invalid", "Backup failed integrity or contract verification", 1);
82
+ }
83
+ finally {
84
+ verification.close();
85
+ }
86
+ const receipt = { database: file, backup, backup_sha256: crypto.createHash("sha256").update(fs.readFileSync(backup)).digest("hex"), previous_version: previous, writer_version: STORE_WRITER_VERSION };
87
+ db.exec("CREATE TABLE IF NOT EXISTS dd_flow_writer_migrations (backup_path TEXT PRIMARY KEY, receipt_json TEXT NOT NULL)");
88
+ db.prepare("INSERT INTO dd_flow_writer_migrations VALUES (?, ?)").run(destination, JSON.stringify(receipt));
89
+ installStoreWriterContract(db);
90
+ db.exec("COMMIT");
91
+ return receipt;
92
+ }
93
+ catch (error) {
94
+ if (db.isTransaction)
95
+ db.exec("ROLLBACK");
96
+ throw error;
97
+ }
98
+ }
99
+ finally {
100
+ db.close();
101
+ }
102
+ }
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.9",
3
+ "version": "0.9.0-beta.91",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "dd-flow": "./dist/cli.js"
7
+ "dd-flow": "dist/cli.js",
8
+ "dd-codex": "dist/harness-runtime/bin/dd-codex.mjs",
9
+ "dd-zcode": "dist/harness-runtime/bin/dd-zcode.mjs",
10
+ "dd-grok": "dist/harness-runtime/bin/dd-grok.mjs",
11
+ "dd-opencode": "dist/harness-runtime/bin/dd-opencode.mjs",
12
+ "dd-agy": "dist/harness-runtime/bin/dd-agy.mjs",
13
+ "dd-droid": "dist/harness-runtime/bin/dd-droid.mjs"
8
14
  },
9
15
  "files": [
10
16
  "dist",
@@ -16,6 +22,20 @@
16
22
  "node": ">=26.0.0",
17
23
  "pnpm": ">=10.0.0"
18
24
  },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "eslint . --max-warnings=0",
29
+ "test": "pnpm test:release && pnpm test:integration && pnpm test:runtime-sensitive",
30
+ "test:release": "node --test scripts/verify-release-build.test.mjs && vitest run --pool=forks --no-file-parallelism test/publish-release.test.ts",
31
+ "test:integration": "vitest run --pool=forks --no-file-parallelism --exclude test/publish-release.test.ts --exclude test/run-control-worker.test.ts --exclude test/native-adapter-contracts.test.ts",
32
+ "test:runtime-sensitive": "vitest run --pool=forks --no-file-parallelism test/run-control-worker.test.ts test/native-adapter-contracts.test.ts",
33
+ "changeset": "changeset",
34
+ "version-packages": "changeset version",
35
+ "release:candidate:pack": "node scripts/release-candidate.mjs pack",
36
+ "release:candidate:accept": "node scripts/release-candidate.mjs accept",
37
+ "release:publish": "node scripts/publish-release.mjs"
38
+ },
19
39
  "devDependencies": {
20
40
  "@changesets/cli": "^2.31.0",
21
41
  "@eslint/js": "^9.39.1",
@@ -34,14 +54,5 @@
34
54
  "publishConfig": {
35
55
  "access": "public"
36
56
  },
37
- "license": "MIT",
38
- "scripts": {
39
- "build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
40
- "typecheck": "tsc --noEmit",
41
- "lint": "eslint . --max-warnings=0",
42
- "test": "vitest run",
43
- "changeset": "changeset",
44
- "version-packages": "changeset version",
45
- "release": "pnpm run build && changeset publish"
46
- }
47
- }
57
+ "license": "MIT"
58
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ /* global process, console */
3
+ // Explicit repair of legacy fenced RUN projections; no migrations or process control.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import crypto from 'node:crypto';
7
+ import { parseArgs } from 'node:util';
8
+ import { DatabaseSync } from 'node:sqlite';
9
+ import { registerStoreWriter } from '../dist/storage/writer-contract.js';
10
+ import { setFlowRunRecoveryPaused, completeFlowRun } from '../dist/services/runs.js';
11
+
12
+ const { values } = parseArgs({ options: { home: { type: 'string' }, run: { type: 'string' }, project: { type: 'string' }, apply: { type: 'boolean', default: false }, 'cancel-abandoned': { type: 'boolean', default: false } } });
13
+ if (!values.home || !values.run || !values.project) throw new Error('Usage: --home <runtime> --project <project-id> --run <run-id> [--apply]');
14
+ const home = fs.realpathSync(values.home), file = path.join(home, 'db.sqlite');
15
+ const db = new DatabaseSync(file, { readOnly: !values.apply });
16
+ let runtimePath, original, backup, report, changed = false;
17
+ try {
18
+ db.exec('PRAGMA busy_timeout=4000');
19
+ if (values.apply) { registerStoreWriter(db); db.exec('BEGIN IMMEDIATE'); }
20
+ const run = db.prepare('SELECT * FROM runs WHERE project_id=? AND id=?').get(values.project, values.run);
21
+ if (!run) throw new Error('RUN not found');
22
+ const guard = db.prepare('SELECT * FROM run_recovery_guards WHERE project_id=? AND run_id=? ORDER BY generation DESC LIMIT 1').get(values.project, values.run);
23
+ const cancel = values['cancel-abandoned'];
24
+ if (cancel) {
25
+ if (fs.existsSync(run.project_root)) throw new Error('Abandoned-fixture cancellation requires an absent project directory');
26
+ for (const table of ['works', 'sessions', 'run_controllers']) {
27
+ if (!db.prepare("SELECT 1 FROM sqlite_schema WHERE type='table' AND name=?").get(table)) continue;
28
+ if (db.prepare(`SELECT 1 FROM ${table} WHERE project_id=? AND run_id=? LIMIT 1`).get(values.project, values.run)) throw new Error('Abandoned-fixture cancellation requires no Work, Session or controller history');
29
+ }
30
+ if (guard) throw new Error('A recovery-owned RUN requires recovery reconciliation');
31
+ }
32
+ if (!cancel && !["draining", "sealed", "resuming"].includes(guard?.status)) throw new Error("Repair requires the current recovery to fence productive admission");
33
+ if (run.status !== 'running') {
34
+ report = { run_id: run.id, status: run.status, changed: false };
35
+ } else {
36
+ runtimePath = fs.realpathSync(run.runtime_path);
37
+ const relative = path.relative(home, runtimePath);
38
+ if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('Runtime file is outside the selected home');
39
+ original = fs.readFileSync(runtimePath);
40
+ if (JSON.parse(original).status !== 'running' || JSON.parse(run.index_json).status !== 'running') throw new Error('RUN projections disagree; manual review required');
41
+ if (values.apply) {
42
+ backup = `${runtimePath}.status-repair-${crypto.randomUUID()}.json`;
43
+ fs.writeFileSync(backup, JSON.stringify({ recovery_id: guard?.recovery_id ?? null, row: run, runtime: original.toString('utf8') }), { flag: 'wx', mode: 0o600 });
44
+ const context = { ddFlowHome: home, env: { ...process.env, DD_FLOW_HOME: home }, now: () => new Date().toISOString(), db: {
45
+ path: file, writable: true, get: (sql, params = []) => db.prepare(sql).get(...params), all: (sql, params = []) => db.prepare(sql).all(...params), run: (sql, params = []) => db.prepare(sql).run(...params), exec: sql => db.exec(sql)
46
+ } };
47
+ changed = true;
48
+ if (cancel) completeFlowRun(context, { projectRoot: run.project_root, runId: run.id, status: 'cancelled', manualOverrideReason: 'Operator requested cleanup of abandoned fixture: project removed; no Work, Session or controller history.' });
49
+ else setFlowRunRecoveryPaused(context, values.project, values.run, true);
50
+ }
51
+ report = { run_id: run.id, from: 'running', to: cancel ? 'cancelled' : 'paused', recovery_id: guard?.recovery_id ?? null, applied: values.apply, backup };
52
+ }
53
+ if (values.apply) db.exec('COMMIT');
54
+ console.log(JSON.stringify(report));
55
+ } catch (error) {
56
+ if (values.apply && db.isTransaction) db.exec('ROLLBACK');
57
+ if (changed && original) { const temporary = `${runtimePath}.repair-rollback`; fs.writeFileSync(temporary, original); fs.renameSync(temporary, runtimePath); }
58
+ throw error;
59
+ } finally { db.close(); }