@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.80
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.
- package/CHANGELOG.md +396 -0
- package/README.md +65 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +81 -9
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1342 -375
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +57 -13
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +543 -96
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +750 -73
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +553 -285
- package/dist/services/lanes.js +61 -53
- package/dist/services/lifecycle-command.js +87 -7
- package/dist/services/lifecycle-invocations.js +828 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +283 -27
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +73 -28
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +222 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +779 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +342 -0
- package/dist/services/runs.js +368 -74
- package/dist/services/runtime-budget.js +434 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +565 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +99 -102
- package/dist/services/stage-pause.js +65 -23
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +189 -85
- package/dist/services/vnext-code.js +257 -243
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +134 -19
- package/dist/services/vnext-merge.js +285 -102
- package/dist/services/vnext-plan-review.js +148 -94
- package/dist/services/vnext-plan.js +141 -72
- package/dist/services/vnext-protocolize.js +129 -56
- package/dist/services/vnext-specify.js +87 -64
- package/dist/services/work-registry.js +707 -156
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +444 -32
- package/dist/storage/paths.js +17 -4
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +11 -4
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -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.
|
|
3
|
+
"version": "0.9.0-beta.80",
|
|
4
4
|
"description": "Mechanical runtime CLI for dd-flow workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"dd-flow": "
|
|
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",
|
|
@@ -39,9 +45,10 @@
|
|
|
39
45
|
"build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
|
|
40
46
|
"typecheck": "tsc --noEmit",
|
|
41
47
|
"lint": "eslint . --max-warnings=0",
|
|
42
|
-
"test": "vitest run",
|
|
48
|
+
"test": "vitest run --pool=forks --no-file-parallelism",
|
|
43
49
|
"changeset": "changeset",
|
|
44
50
|
"version-packages": "changeset version",
|
|
45
|
-
"release": "pnpm run build && changeset publish"
|
|
51
|
+
"release": "pnpm run build && changeset publish",
|
|
52
|
+
"release:publish": "node scripts/publish-release.mjs"
|
|
46
53
|
}
|
|
47
54
|
}
|
|
@@ -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(); }
|