@deksden-com/dd-flow-cli 0.9.0-beta.134 → 0.9.0-beta.136
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 +12 -0
- package/README.md +33 -0
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +1 -0
- package/dist/cli/command-metadata.js +144 -0
- package/dist/cli/command-option-descriptions.js +186 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +71 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +69 -0
- package/dist/cli/run-cli.js +5 -0
- package/dist/cli/setup.js +147 -0
- package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +1 -1
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +13 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +193 -39
- package/dist/harness-runtime/lib/dd-opencode.mjs +295 -100
- package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
- package/dist/harness-runtime/lib/managed-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +60 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
- package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
- package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
- package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
- package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
- package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
- package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
- package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
- package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
- package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
- package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
- package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
- package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
- package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
- package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
- package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
- package/dist/services/cli-operation-classifier.js +4 -0
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +23 -25
- package/dist/services/eval-snapshots.js +1 -1
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +1 -1
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +1 -1
- package/dist/services/hybrid-contracts.js +455 -0
- package/dist/services/installation-lock.js +92 -0
- package/dist/services/managed-processes.js +30 -0
- package/dist/services/merge-server.js +1 -1
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/run-controller-recovery.js +2 -2
- package/dist/services/run-controller.js +3 -2
- package/dist/services/run-health.js +149 -0
- package/dist/services/runs.js +61 -13
- package/dist/services/setup.js +377 -0
- package/dist/storage/database.js +18 -3
- package/dist/storage/writer-contract.js +8 -0
- package/package.json +4 -1
package/dist/services/runs.js
CHANGED
|
@@ -911,6 +911,46 @@ export function setFlowRunRecoveryPaused(context, projectId, runId, paused) {
|
|
|
911
911
|
const project = requireProjectByRoot(context, run.project_root);
|
|
912
912
|
persistRunState(context, project, run, index);
|
|
913
913
|
}
|
|
914
|
+
/** Reconciliation repairs projections under the same revision CAS as normal
|
|
915
|
+
* lifecycle writes. Unknown physical ownership never closes child records. */
|
|
916
|
+
export function reconcileStoredFlowRun(context, expected, health, transition, childrenSettled = false) {
|
|
917
|
+
const run = resolveRun(context, expected.project_id, expected.id);
|
|
918
|
+
if (run.index_json !== expected.index_json || run.status !== expected.status)
|
|
919
|
+
throw new AppError("run_revision_changed", "RUN changed during health observation", 1);
|
|
920
|
+
const index = authoritativeIndex(run), now = context.now();
|
|
921
|
+
index.health = health;
|
|
922
|
+
if (transition && !["done", "failed", "cancelled", "discarded"].includes(run.status)) {
|
|
923
|
+
index.status = transition;
|
|
924
|
+
if (transition === "failed") {
|
|
925
|
+
index.verdict = "failed";
|
|
926
|
+
// Observation time is not the historical exit time. Without a retained
|
|
927
|
+
// finish receipt, keep timing unknown rather than manufacturing days of work.
|
|
928
|
+
delete index.completed_at;
|
|
929
|
+
delete index.finished_at;
|
|
930
|
+
index.duration_ms = null;
|
|
931
|
+
index.timing_status = 'legacy_incomplete';
|
|
932
|
+
closeOpenStagesForOverride(index, "failed", now, false);
|
|
933
|
+
closeOpenWorksForOverride(context, run.project_id, run.id, "failed", now, false);
|
|
934
|
+
}
|
|
935
|
+
else if (childrenSettled && health.state === "owner_lost") {
|
|
936
|
+
for (const stage of index.stage_runs)
|
|
937
|
+
if (stage.status === "running") {
|
|
938
|
+
stage.status = "paused";
|
|
939
|
+
stage.updated_at = now;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (childrenSettled && ["failed", "cancelled", "discarded"].includes(index.status)) {
|
|
944
|
+
closeOpenStagesForOverride(index, index.status === "failed" ? "failed" : "cancelled", now, false);
|
|
945
|
+
closeOpenWorksForOverride(context, run.project_id, run.id, index.status === "failed" ? "failed" : "cancelled", now, false);
|
|
946
|
+
context.db.run("UPDATE sessions SET status = 'stopped', stop_reason = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND status IN ('pending','active','waiting_user','blocked','stopping')", [`run_${index.status}`, now, run.project_id, run.id]);
|
|
947
|
+
context.db.run("UPDATE check_receipts SET status = 'aborted' WHERE project_id = ? AND run_id = ? AND status = 'running'", [run.project_id, run.id]);
|
|
948
|
+
}
|
|
949
|
+
// Health observation is not semantic progress or a new completion timestamp.
|
|
950
|
+
const project = requireProjectByRoot(context, run.project_root);
|
|
951
|
+
persistRunState(context, project, run, index, { healthOnly: true });
|
|
952
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.health_reconciled", payload: { run_id: run.id, previous_status: run.status, status: index.status, health } });
|
|
953
|
+
}
|
|
914
954
|
export function prepareFlowRunCompletion(context, input) {
|
|
915
955
|
if (input.manualOverrideReason !== undefined) {
|
|
916
956
|
requiredPlain(input.manualOverrideReason, "override reason");
|
|
@@ -1061,15 +1101,21 @@ export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
|
1061
1101
|
appendRunTimeline(context, project.id, run.id, { at: now, type: "merge_source_repair_prepared", run_id: run.id, merge_request_id: input.mergeRequestId, cycle });
|
|
1062
1102
|
return { run_id: run.id, cycle };
|
|
1063
1103
|
}
|
|
1064
|
-
function closeOpenStagesForOverride(index, status, now) {
|
|
1104
|
+
function closeOpenStagesForOverride(index, status, now, timingKnown = true) {
|
|
1065
1105
|
const stageStatus = status === "cancelled" ? "skipped" : "failed";
|
|
1066
1106
|
for (const stage of index.stage_runs) {
|
|
1067
|
-
if (stage.status !== "running" && stage.status !== "
|
|
1107
|
+
if (stage.status !== "running" && stage.status !== "paused" && stage.status !== "waiting_for_user" && stage.status !== "blocked")
|
|
1068
1108
|
continue;
|
|
1069
1109
|
stage.status = stageStatus;
|
|
1070
1110
|
stage.updated_at = now;
|
|
1071
|
-
|
|
1072
|
-
|
|
1111
|
+
if (timingKnown) {
|
|
1112
|
+
stage.completed_at = now;
|
|
1113
|
+
stage.duration_ms = durationMs(stage.started_at ?? null, now);
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
delete stage.completed_at;
|
|
1117
|
+
stage.duration_ms = null;
|
|
1118
|
+
}
|
|
1073
1119
|
}
|
|
1074
1120
|
index.attempts = index.stage_runs.map((stage) => ({
|
|
1075
1121
|
stage: stage.stage,
|
|
@@ -1077,20 +1123,20 @@ function closeOpenStagesForOverride(index, status, now) {
|
|
|
1077
1123
|
root: stage.dir,
|
|
1078
1124
|
archive: null,
|
|
1079
1125
|
status: stage.status,
|
|
1080
|
-
started_at: stage.started_at ??
|
|
1126
|
+
started_at: stage.started_at ?? null,
|
|
1081
1127
|
finished_at: stage.completed_at ?? null
|
|
1082
1128
|
}));
|
|
1083
1129
|
}
|
|
1084
|
-
function closeOpenWorksForOverride(context, projectId, runId, status, now) {
|
|
1130
|
+
function closeOpenWorksForOverride(context, projectId, runId, status, now, timingKnown = true) {
|
|
1085
1131
|
const workStatus = status === "cancelled" ? "cancelled" : "failed";
|
|
1086
1132
|
context.db.run(`UPDATE works
|
|
1087
|
-
SET status = ?, updated_at = ?, completed_at = ?
|
|
1133
|
+
SET status = ?, updated_at = ?, completed_at = CASE WHEN ? = 1 THEN ? ELSE completed_at END
|
|
1088
1134
|
WHERE project_id = ? AND run_id = ?
|
|
1089
|
-
AND status IN ('created', 'running', 'paused')`, [workStatus, now, now, projectId, runId]);
|
|
1135
|
+
AND status IN ('created', 'running', 'paused')`, [workStatus, now, timingKnown ? 1 : 0, now, projectId, runId]);
|
|
1090
1136
|
context.db.run(`UPDATE work_sessions
|
|
1091
|
-
SET status = ?, updated_at = ?, completed_at = ?
|
|
1137
|
+
SET status = ?, updated_at = ?, completed_at = CASE WHEN ? = 1 THEN ? ELSE completed_at END
|
|
1092
1138
|
WHERE work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)
|
|
1093
|
-
AND status <> 'completed'`, [workStatus, now, now, projectId, runId]);
|
|
1139
|
+
AND status <> 'completed'`, [workStatus, now, timingKnown ? 1 : 0, now, projectId, runId]);
|
|
1094
1140
|
}
|
|
1095
1141
|
function closeRunOrchestrators(context, projectId, run, status, now) {
|
|
1096
1142
|
if (!["done", "blocked", "cancelled", "failed"].includes(status))
|
|
@@ -1327,8 +1373,10 @@ function vnextSpecifyGuidance(run, index) {
|
|
|
1327
1373
|
function persistRunState(context, project, run, index, options = {}) {
|
|
1328
1374
|
// Fenced productive admission is a logical pause, even while physical work
|
|
1329
1375
|
// drains. Late admitted Stage results must not revive a stopped RUN.
|
|
1330
|
-
if (index.status === "running" && ["draining", "sealed", "resuming"].includes(recoveryGuard(context, project.id, run.id)?.status ?? ""))
|
|
1376
|
+
if (index.status === "running" && (!options.healthOnly || context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_recovery_guards'")) && ["draining", "sealed", "resuming"].includes(recoveryGuard(context, project.id, run.id)?.status ?? ""))
|
|
1331
1377
|
index.status = "paused";
|
|
1378
|
+
if (!options.healthOnly)
|
|
1379
|
+
delete index.health;
|
|
1332
1380
|
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, project.id, run.id);
|
|
1333
1381
|
context.db.writeTransaction(() => {
|
|
1334
1382
|
const stored = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, run.id]);
|
|
@@ -1344,7 +1392,7 @@ function persistRunState(context, project, run, index, options = {}) {
|
|
|
1344
1392
|
if (options.flagRevision)
|
|
1345
1393
|
history.push(options.flagRevision);
|
|
1346
1394
|
index.flag_revision_history = history.slice(-32);
|
|
1347
|
-
const updated = context.db.run(`UPDATE runs SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?, updated_at = ?, completed_at = ?
|
|
1395
|
+
const updated = options.healthOnly ? context.db.run("UPDATE runs SET status = ?, verdict = ?, next_action = ?, index_json = ?, updated_at = ?, completed_at = ? WHERE project_id = ? AND id = ? AND index_json = ?", [index.status, index.verdict, index.next_action, JSON.stringify(persistedVnextIndex(index)), index.updated_at, index.completed_at ?? null, project.id, run.id, stored.index_json]) : context.db.run(`UPDATE runs SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?, updated_at = ?, completed_at = ?
|
|
1348
1396
|
WHERE project_id = ? AND id = ? AND index_json = ?`, [index.status, index.verdict, index.next_action, JSON.stringify(persistedVnextIndex(index)), runtimePath, runtimePath, index.updated_at, index.completed_at ?? null, project.id, run.id, stored.index_json]);
|
|
1349
1397
|
if (updated.changes !== 1)
|
|
1350
1398
|
throw new AppError("run_revision_changed", "RUN changed during preparation", 1, { run_id: run.id });
|
|
@@ -1431,7 +1479,7 @@ export function publishRunProjections(context, projectId, runId) {
|
|
|
1431
1479
|
if (!latest)
|
|
1432
1480
|
throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
|
|
1433
1481
|
const index = authoritativeIndex(latest);
|
|
1434
|
-
const mutations = context.db.all("SELECT resulting_revision, result_json, created_at, idempotency_key FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? ORDER BY resulting_revision DESC LIMIT 32", [projectId, runId]);
|
|
1482
|
+
const mutations = context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'flow_run_flag_mutations'") ? context.db.all("SELECT resulting_revision, result_json, created_at, idempotency_key FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? ORDER BY resulting_revision DESC LIMIT 32", [projectId, runId]) : [];
|
|
1435
1483
|
const history = mutations.reverse().map((row) => ({
|
|
1436
1484
|
revision: row.resulting_revision,
|
|
1437
1485
|
checksum: String(JSON.parse(row.result_json).snapshot_checksum ?? ""),
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
|
|
6
|
+
import { admitReleaseManifest } from "./release-manifest.js";
|
|
7
|
+
import { validateReleasePayloadIdentity } from "./release-payload.js";
|
|
8
|
+
import { publishReleaseCache, verifyPublishedReleaseCache } from "./release-cache.js";
|
|
9
|
+
import { acquireInstallationLock } from "./installation-lock.js";
|
|
10
|
+
import { authenticateReleaseEnvelope } from "./release-envelope.js";
|
|
11
|
+
import { prepareOperatorProfile, validateProfileCreationIntent, commitProfileCreation, readOperatorProfiles } from "./operator-profiles.js";
|
|
12
|
+
/** Production ingress authenticates the source before constructing any installation authority. */
|
|
13
|
+
export function installAuthenticatedRelease(home, input) {
|
|
14
|
+
const authenticated = authenticateReleaseEnvelope(input.envelope, input.trustedProducers, input.compatibility);
|
|
15
|
+
return installRelease(home, { requestId: input.requestId, action: input.action,
|
|
16
|
+
manifest: authenticated.manifest, manifestBytes: authenticated.manifestBytes, authority: authenticated.authority,
|
|
17
|
+
canonArchive: input.canonArchive, engineArchive: input.engineArchive,
|
|
18
|
+
...(input.executor !== undefined ? { executor: input.executor } : {}),
|
|
19
|
+
...(input.profileCreation !== undefined ? { profileCreation: input.profileCreation } : {}),
|
|
20
|
+
...(input.deferredOnboarding ? { deferredOnboarding: true } : {}),
|
|
21
|
+
...(input.onCheckpoint ? { onCheckpoint: input.onCheckpoint } : {}) });
|
|
22
|
+
}
|
|
23
|
+
function refuse(reason, effect = "no_effect") {
|
|
24
|
+
throw new AppError("setup_conflict", "Setup request cannot proceed", 1, { reason, phase: "setup", effect, help: "setup", next_action: "dd-flow setup status --json" });
|
|
25
|
+
}
|
|
26
|
+
const bytesDigest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
27
|
+
function manifestFromBytes(bytes) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return refuse("invalid_manifest_bytes");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function regularJson(file) {
|
|
36
|
+
if (!fs.lstatSync(file).isFile())
|
|
37
|
+
refuse("unsafe_setup_file");
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return refuse("invalid_setup_file");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function atomicJson(file, value) {
|
|
46
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
47
|
+
const fd = fs.openSync(temporary, "wx", 0o600);
|
|
48
|
+
try {
|
|
49
|
+
try {
|
|
50
|
+
fs.writeFileSync(fd, JSON.stringify(value));
|
|
51
|
+
fs.fsyncSync(fd);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
fs.closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
fs.renameSync(temporary, file);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (fs.existsSync(temporary))
|
|
60
|
+
fs.unlinkSync(temporary);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function readSetupRequest(file) {
|
|
64
|
+
const value = regularJson(file);
|
|
65
|
+
const required = ["schema_id", "request_id", "identity", "previous_selection", "phase", "installation"];
|
|
66
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
67
|
+
|| required.some(key => !Object.hasOwn(value, key))
|
|
68
|
+
|| Object.keys(value).some(key => ![...required, "executor", "profileCreation", "deferredOnboarding"].includes(key)))
|
|
69
|
+
refuse("invalid_request_record");
|
|
70
|
+
const request = value;
|
|
71
|
+
if (Object.hasOwn(request, "deferredOnboarding") && (request.deferredOnboarding !== true || Object.hasOwn(request, "executor") || Object.hasOwn(request, "profileCreation")))
|
|
72
|
+
refuse("invalid_deferred_onboarding");
|
|
73
|
+
if (request.schema_id !== "dd-flow/setup-request@1"
|
|
74
|
+
|| typeof request.request_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(request.request_id)
|
|
75
|
+
|| typeof request.identity !== "string" || !/^[a-f0-9]{64}$/.test(request.identity)
|
|
76
|
+
|| request.previous_selection !== null && (typeof request.previous_selection !== "string" || !/^[a-f0-9]{64}$/.test(request.previous_selection))
|
|
77
|
+
|| !["prepared", "canon_cached", "engine_cached", "profiles_saved", "committed"].includes(request.phase)
|
|
78
|
+
|| !request.installation || typeof request.installation !== "object" || Array.isArray(request.installation))
|
|
79
|
+
refuse("invalid_request_record");
|
|
80
|
+
if (Object.hasOwn(request, "executor") && request.executor !== null) {
|
|
81
|
+
try {
|
|
82
|
+
prepareOperatorProfile(request.executor);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
refuse("invalid_request_executor");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (Object.hasOwn(request, "profileCreation")) {
|
|
89
|
+
try {
|
|
90
|
+
validateProfileCreationIntent(request.profileCreation);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
refuse("invalid_request_profile_creation");
|
|
94
|
+
}
|
|
95
|
+
if (!request.executor || hybridDigest(request.executor) !== hybridDigest(request.profileCreation.profile))
|
|
96
|
+
refuse("request_profile_executor_mismatch");
|
|
97
|
+
}
|
|
98
|
+
return request;
|
|
99
|
+
}
|
|
100
|
+
/** Read retained executor consent for an exact retry, without installation or profile effects.
|
|
101
|
+
* Full source/installation request identity is still checked by installRelease.
|
|
102
|
+
*/
|
|
103
|
+
export function readRetainedSetupExecutor(home, requestId) {
|
|
104
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(requestId))
|
|
105
|
+
refuse("invalid_request");
|
|
106
|
+
const root = path.resolve(home), directory = path.join(root, "setup-requests");
|
|
107
|
+
for (const candidate of [root, directory])
|
|
108
|
+
if (fs.existsSync(candidate) && !fs.lstatSync(candidate).isDirectory())
|
|
109
|
+
refuse("unsafe_setup_directory");
|
|
110
|
+
const file = path.join(directory, `${requestId}.json`);
|
|
111
|
+
if (!fs.existsSync(file))
|
|
112
|
+
return null;
|
|
113
|
+
const request = readSetupRequest(file);
|
|
114
|
+
if (request.request_id !== requestId)
|
|
115
|
+
refuse("request_identity_conflict");
|
|
116
|
+
if (!Object.hasOwn(request, "executor"))
|
|
117
|
+
return null;
|
|
118
|
+
return { executor: structuredClone(request.executor ?? null),
|
|
119
|
+
...(request.profileCreation ? { profileCreation: structuredClone(request.profileCreation) } : {}) };
|
|
120
|
+
}
|
|
121
|
+
/** A deferred request is immutable installation consent, not an implicit skip. */
|
|
122
|
+
export function hasDeferredSetupOnboarding(home, requestId) {
|
|
123
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(requestId))
|
|
124
|
+
refuse("invalid_request");
|
|
125
|
+
const root = path.resolve(home), directory = path.join(root, "setup-requests");
|
|
126
|
+
for (const candidate of [root, directory])
|
|
127
|
+
if (fs.existsSync(candidate) && !fs.lstatSync(candidate).isDirectory())
|
|
128
|
+
refuse("unsafe_setup_directory");
|
|
129
|
+
const file = path.join(directory, `${requestId}.json`);
|
|
130
|
+
if (!fs.existsSync(file))
|
|
131
|
+
return false;
|
|
132
|
+
const request = readSetupRequest(file);
|
|
133
|
+
if (request.request_id !== requestId)
|
|
134
|
+
refuse("request_identity_conflict");
|
|
135
|
+
return request.deferredOnboarding === true;
|
|
136
|
+
}
|
|
137
|
+
function deferredRequest(home, requestId) {
|
|
138
|
+
if (!hasDeferredSetupOnboarding(home, requestId))
|
|
139
|
+
refuse("onboarding_not_deferred");
|
|
140
|
+
const request = readSetupRequest(path.join(path.resolve(home), "setup-requests", `${requestId}.json`));
|
|
141
|
+
if (request.phase !== "committed")
|
|
142
|
+
refuse("installation_not_committed");
|
|
143
|
+
return request;
|
|
144
|
+
}
|
|
145
|
+
/** Retained consent allows recovery without asking again or importing current defaults. */
|
|
146
|
+
export function readSetupOnboarding(home, requestId) {
|
|
147
|
+
const request = deferredRequest(home, requestId);
|
|
148
|
+
const directory = path.join(path.resolve(home), "setup-onboarding");
|
|
149
|
+
if (fs.existsSync(directory) && !fs.lstatSync(directory).isDirectory())
|
|
150
|
+
refuse("unsafe_onboarding_directory");
|
|
151
|
+
const file = path.join(directory, `${requestId}.json`);
|
|
152
|
+
if (!fs.existsSync(file))
|
|
153
|
+
return null;
|
|
154
|
+
const receipt = regularJson(file);
|
|
155
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)
|
|
156
|
+
|| Object.keys(receipt).some(key => !["schema_id", "request_id", "installation_identity", "phase", "executor", "profileCreation"].includes(key))
|
|
157
|
+
|| receipt.schema_id !== "dd-flow/setup-onboarding@1" || receipt.request_id !== requestId
|
|
158
|
+
|| receipt.installation_identity !== request.identity || !["selected", "committed"].includes(receipt.phase)
|
|
159
|
+
|| !Object.hasOwn(receipt, "executor"))
|
|
160
|
+
refuse("invalid_onboarding_receipt");
|
|
161
|
+
try {
|
|
162
|
+
if (receipt.executor !== null)
|
|
163
|
+
prepareOperatorProfile(receipt.executor);
|
|
164
|
+
if (receipt.profileCreation !== undefined) {
|
|
165
|
+
validateProfileCreationIntent(receipt.profileCreation);
|
|
166
|
+
if (!receipt.executor || hybridDigest(receipt.executor) !== hybridDigest(receipt.profileCreation.profile))
|
|
167
|
+
refuse("onboarding_profile_mismatch");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
refuse("invalid_onboarding_receipt");
|
|
172
|
+
}
|
|
173
|
+
return structuredClone(receipt);
|
|
174
|
+
}
|
|
175
|
+
/** Persist human consent before profile effects; replay never restores later profile edits. */
|
|
176
|
+
export function commitSetupOnboarding(home, requestId, selection) {
|
|
177
|
+
const request = deferredRequest(home, requestId), root = path.resolve(home);
|
|
178
|
+
const executor = selection.executor === null ? null : prepareOperatorProfile(selection.executor);
|
|
179
|
+
if (selection.profileCreation) {
|
|
180
|
+
validateProfileCreationIntent(selection.profileCreation);
|
|
181
|
+
if (!executor || hybridDigest(executor) !== hybridDigest(selection.profileCreation.profile))
|
|
182
|
+
refuse("onboarding_profile_mismatch");
|
|
183
|
+
}
|
|
184
|
+
const candidate = { schema_id: "dd-flow/setup-onboarding@1", request_id: requestId,
|
|
185
|
+
installation_identity: request.identity, phase: "selected", executor,
|
|
186
|
+
...(selection.profileCreation ? { profileCreation: selection.profileCreation } : {}) };
|
|
187
|
+
const lease = acquireInstallationLock(path.join(root, "setup.lock"), hybridDigest({ request_id: requestId, identity: request.identity }));
|
|
188
|
+
let failure, result;
|
|
189
|
+
try {
|
|
190
|
+
const retained = readSetupOnboarding(root, requestId);
|
|
191
|
+
if (retained && hybridDigest({ ...retained, phase: "selected" }) !== hybridDigest(candidate))
|
|
192
|
+
refuse("onboarding_selection_conflict");
|
|
193
|
+
if (retained?.phase === "committed")
|
|
194
|
+
result = retained;
|
|
195
|
+
else {
|
|
196
|
+
if (hybridDigest(readSetupStatus(root).installation) !== hybridDigest(request.installation))
|
|
197
|
+
refuse("onboarding_installation_changed");
|
|
198
|
+
const directory = path.join(root, "setup-onboarding");
|
|
199
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
200
|
+
const file = path.join(directory, `${requestId}.json`);
|
|
201
|
+
if (!retained)
|
|
202
|
+
atomicJson(file, candidate);
|
|
203
|
+
if (candidate.profileCreation)
|
|
204
|
+
commitProfileCreation(root, candidate.profileCreation);
|
|
205
|
+
result = { ...candidate, phase: "committed" };
|
|
206
|
+
atomicJson(file, result);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
failure = error instanceof AppError ? error : new AppError("setup_onboarding_interrupted", "Installed release is retained; executor onboarding requires reconciliation", 1, { phase: "executor_onboarding", effect: "unknown", request_id: requestId, help: "setup", next_action: "Replay the exact setup request" });
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
try {
|
|
214
|
+
lease.release();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
if (failure)
|
|
218
|
+
failure.details.cleanup = "not_confirmed";
|
|
219
|
+
else
|
|
220
|
+
failure = new AppError("setup_cleanup_failed", "Onboarding owner cleanup requires reconciliation", 1, { effect: "unknown", cleanup: "not_confirmed", request_id: requestId, help: "setup" });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (failure)
|
|
224
|
+
throw failure;
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
/** Read-only global readiness. No SQLite connection, migration, project registration or probes. */
|
|
228
|
+
export function readSetupStatus(home) {
|
|
229
|
+
const root = path.resolve(home);
|
|
230
|
+
if (fs.existsSync(root) && !fs.lstatSync(root).isDirectory())
|
|
231
|
+
refuse("unsafe_setup_home");
|
|
232
|
+
const file = path.join(root, "installation.json");
|
|
233
|
+
const installation = fs.existsSync(file) ? regularJson(file) : null;
|
|
234
|
+
if (installation) {
|
|
235
|
+
if (!installation.manifest || typeof installation.manifest_bytes_base64 !== "string" || validateHybridShape("release-manifest", installation.manifest).length ||
|
|
236
|
+
installation.canon_root !== path.join(root, "release-cache", "canon", installation.manifest.canon.version, "payload") ||
|
|
237
|
+
installation.engine_root !== path.join(root, "release-cache", "engine", installation.manifest.engine.version, "payload"))
|
|
238
|
+
refuse("invalid_installation_selection");
|
|
239
|
+
const bytes = Buffer.from(installation.manifest_bytes_base64, "base64");
|
|
240
|
+
if (bytes.toString("base64") !== installation.manifest_bytes_base64 || bytesDigest(bytes) !== installation.manifest_sha256 ||
|
|
241
|
+
hybridDigest(manifestFromBytes(bytes)) !== hybridDigest(installation.manifest))
|
|
242
|
+
refuse("invalid_installation_selection");
|
|
243
|
+
}
|
|
244
|
+
let readiness = installation ? "installed" : "not_installed";
|
|
245
|
+
if (installation) {
|
|
246
|
+
try {
|
|
247
|
+
verifyPublishedReleaseCache(path.dirname(installation.canon_root), installation.manifest.canon.archive);
|
|
248
|
+
verifyPublishedReleaseCache(path.dirname(installation.engine_root), installation.manifest.engine.archive);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
readiness = "degraded";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const existingStorage = fs.existsSync(path.join(root, "db.sqlite"));
|
|
255
|
+
return { installation, installation_readiness: readiness,
|
|
256
|
+
storage_readiness: existingStorage ? "unknown_requires_diagnostic" : "not_initialized",
|
|
257
|
+
runtime_readiness: "unknown_requires_qualification",
|
|
258
|
+
next_action: !installation ? "dd-flow setup --help" : existingStorage ? "dd-flow migration plan --help" : "dd-flow engine doctor --json",
|
|
259
|
+
...(existingStorage ? { storage_continuation: { kind: "storage_admission",
|
|
260
|
+
status: "requires_diagnostic", reason: "existing_storage_not_inspected",
|
|
261
|
+
help_command: "dd-flow migration plan --help", automatic_migration: false } } : {}) };
|
|
262
|
+
}
|
|
263
|
+
/** Exact-request installation transaction. Selected immutable caches do not affect existing RUNs. */
|
|
264
|
+
export function installRelease(home, input) {
|
|
265
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(input.requestId) || !["install", "update"].includes(input.action))
|
|
266
|
+
refuse("invalid_request");
|
|
267
|
+
const executor = input.executor === undefined ? undefined : input.executor === null ? null : prepareOperatorProfile(input.executor);
|
|
268
|
+
const executorBinding = executor === undefined ? {} : { executor: structuredClone(executor) };
|
|
269
|
+
if (input.deferredOnboarding !== undefined && (input.deferredOnboarding !== true || executor !== undefined || input.profileCreation !== undefined))
|
|
270
|
+
refuse("invalid_deferred_onboarding");
|
|
271
|
+
const onboardingBinding = input.deferredOnboarding ? { deferredOnboarding: true } : {};
|
|
272
|
+
if (input.profileCreation !== undefined) {
|
|
273
|
+
validateProfileCreationIntent(input.profileCreation);
|
|
274
|
+
if (!executor || hybridDigest(executor) !== hybridDigest(input.profileCreation.profile))
|
|
275
|
+
refuse("profile_executor_mismatch");
|
|
276
|
+
}
|
|
277
|
+
const profileBinding = input.profileCreation === undefined ? {} : { profileCreation: structuredClone(input.profileCreation) };
|
|
278
|
+
const manifest = admitReleaseManifest(input.manifest, input.authority);
|
|
279
|
+
if (!Buffer.isBuffer(input.manifestBytes) || hybridDigest(manifestFromBytes(input.manifestBytes)) !== hybridDigest(manifest))
|
|
280
|
+
refuse("manifest_bytes_mismatch");
|
|
281
|
+
// Both archives must be valid before even creating the home or publishing the first cache.
|
|
282
|
+
validateReleasePayloadIdentity(manifest, input.canonArchive, input.engineArchive);
|
|
283
|
+
const root = path.resolve(home), requests = path.join(root, "setup-requests"), lock = path.join(root, "setup.lock");
|
|
284
|
+
const installation = { manifest, manifest_sha256: bytesDigest(input.manifestBytes), manifest_bytes_base64: input.manifestBytes.toString("base64"),
|
|
285
|
+
canon_root: path.join(root, "release-cache", "canon", manifest.canon.version, "payload"),
|
|
286
|
+
engine_root: path.join(root, "release-cache", "engine", manifest.engine.version, "payload") };
|
|
287
|
+
const identity = hybridDigest({ action: input.action, installation, ...executorBinding, ...profileBinding, ...onboardingBinding });
|
|
288
|
+
readSetupStatus(root);
|
|
289
|
+
for (const directory of [root, requests]) {
|
|
290
|
+
if (fs.existsSync(directory) && !fs.lstatSync(directory).isDirectory())
|
|
291
|
+
refuse("unsafe_setup_directory");
|
|
292
|
+
}
|
|
293
|
+
if (input.profileCreation) {
|
|
294
|
+
const existingFile = path.join(requests, `${input.requestId}.json`);
|
|
295
|
+
const retained = fs.existsSync(existingFile) ? readSetupRequest(existingFile) : null;
|
|
296
|
+
if (retained?.phase !== "committed") {
|
|
297
|
+
const revision = readOperatorProfiles(root).revision;
|
|
298
|
+
if (revision !== input.profileCreation.before.revision && revision !== input.profileCreation.after.revision)
|
|
299
|
+
refuse("profile_selection_changed");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
fs.mkdirSync(requests, { recursive: true, mode: 0o700 });
|
|
303
|
+
const lease = acquireInstallationLock(lock, hybridDigest({ request_id: input.requestId, identity }));
|
|
304
|
+
let failure;
|
|
305
|
+
const perform = () => {
|
|
306
|
+
const file = path.join(requests, `${input.requestId}.json`);
|
|
307
|
+
const current = readSetupStatus(root).installation;
|
|
308
|
+
let request = { schema_id: "dd-flow/setup-request@1", request_id: input.requestId, identity,
|
|
309
|
+
previous_selection: current ? hybridDigest(current) : null, phase: "prepared", installation, ...executorBinding, ...profileBinding, ...onboardingBinding };
|
|
310
|
+
if (fs.existsSync(file)) {
|
|
311
|
+
request = readSetupRequest(file);
|
|
312
|
+
if (request.schema_id !== "dd-flow/setup-request@1" || request.request_id !== input.requestId || request.identity !== identity || hybridDigest(request.installation) !== hybridDigest(installation))
|
|
313
|
+
refuse("request_identity_conflict");
|
|
314
|
+
if (request.deferredOnboarding !== input.deferredOnboarding)
|
|
315
|
+
refuse("request_onboarding_conflict");
|
|
316
|
+
if (hybridDigest({ executor: request.executor ?? null, present: Object.hasOwn(request, "executor") }) !==
|
|
317
|
+
hybridDigest({ executor: executor ?? null, present: executor !== undefined }))
|
|
318
|
+
refuse("request_executor_conflict");
|
|
319
|
+
if (hybridDigest({ profileCreation: request.profileCreation ?? null }) !== hybridDigest({ profileCreation: input.profileCreation ?? null }))
|
|
320
|
+
refuse("request_profile_creation_conflict");
|
|
321
|
+
if (!["prepared", "canon_cached", "engine_cached", "profiles_saved", "committed"].includes(request.phase))
|
|
322
|
+
refuse("invalid_request_phase");
|
|
323
|
+
}
|
|
324
|
+
// Replay still revalidates retained payload bytes; it never reactivates an older selection.
|
|
325
|
+
if (request.phase === "committed") {
|
|
326
|
+
publishReleaseCache(root, "canon", manifest.canon.version, manifest.canon.archive, input.canonArchive);
|
|
327
|
+
publishReleaseCache(root, "engine", manifest.engine.version, manifest.engine.archive, input.engineArchive);
|
|
328
|
+
return { ok: true, replayed: true, installation: request.installation, ...executorBinding };
|
|
329
|
+
}
|
|
330
|
+
const selectedDigest = current ? hybridDigest(current) : null;
|
|
331
|
+
if (selectedDigest !== request.previous_selection && selectedDigest !== hybridDigest(installation))
|
|
332
|
+
refuse("selection_changed_since_request");
|
|
333
|
+
if (input.action === "install" && current && selectedDigest !== hybridDigest(installation))
|
|
334
|
+
refuse("existing_installation_requires_update");
|
|
335
|
+
const checkpoint = (phase) => {
|
|
336
|
+
request.phase = phase;
|
|
337
|
+
atomicJson(file, request);
|
|
338
|
+
input.onCheckpoint?.(phase);
|
|
339
|
+
};
|
|
340
|
+
checkpoint("prepared");
|
|
341
|
+
publishReleaseCache(root, "canon", manifest.canon.version, manifest.canon.archive, input.canonArchive);
|
|
342
|
+
checkpoint("canon_cached");
|
|
343
|
+
publishReleaseCache(root, "engine", manifest.engine.version, manifest.engine.archive, input.engineArchive);
|
|
344
|
+
checkpoint("engine_cached");
|
|
345
|
+
if (request.profileCreation) {
|
|
346
|
+
commitProfileCreation(root, request.profileCreation);
|
|
347
|
+
checkpoint("profiles_saved");
|
|
348
|
+
}
|
|
349
|
+
atomicJson(path.join(root, "installation.json"), installation);
|
|
350
|
+
checkpoint("committed");
|
|
351
|
+
return { ok: true, replayed: false, installation, ...executorBinding };
|
|
352
|
+
};
|
|
353
|
+
let result;
|
|
354
|
+
try {
|
|
355
|
+
result = perform();
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
failure = error instanceof AppError ? error : new AppError("setup_interrupted", "Setup request requires reconciliation", 1, { phase: "setup", effect: "unknown", request_id: input.requestId, next_action: "Replay the exact setup request", help: "setup" });
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
try {
|
|
362
|
+
lease.release();
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Cleanup is secondary to the retained operation result. Never leak a
|
|
366
|
+
// raw filesystem cause or erase the original failure with an unlink error.
|
|
367
|
+
if (failure)
|
|
368
|
+
failure.details.cleanup = "not_confirmed";
|
|
369
|
+
else
|
|
370
|
+
failure = new AppError("setup_cleanup_failed", "Setup owner cleanup requires reconciliation", 1, { phase: "setup", effect: "unknown", cleanup: "not_confirmed", request_id: input.requestId,
|
|
371
|
+
next_action: "Inspect dd-flow setup status --json; replay the exact request after its owner exits", help: "setup" });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (failure)
|
|
375
|
+
throw failure;
|
|
376
|
+
return result;
|
|
377
|
+
}
|
package/dist/storage/database.js
CHANGED
|
@@ -5,7 +5,7 @@ import { channel } from "node:diagnostics_channel";
|
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { ensureDir } from "./paths.js";
|
|
7
7
|
import { AppError } from "../shared/errors.js";
|
|
8
|
-
import { executeStoreSchema, readStoreWriterContract, registerStoreWriter, STORE_WRITER_VERSION } from "./writer-contract.js";
|
|
8
|
+
import { executeStoreSchema, readStoreWriterContract, registerStoreWriter, registerReconciliationWriter, STORE_WRITER_VERSION } from "./writer-contract.js";
|
|
9
9
|
import { assertImportAdmission } from "./import-guard.js";
|
|
10
10
|
import { withStoreCleanup, rollbackStoreTransaction } from "./native-cleanup.js";
|
|
11
11
|
const workContinuationStorageSql = `
|
|
@@ -354,6 +354,8 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
|
|
|
354
354
|
}
|
|
355
355
|
if (mode === "hook" && !exists)
|
|
356
356
|
throw new AppError("hook_storage_unprepared", "Prepare the runtime store before starting native hooks", 1);
|
|
357
|
+
if (mode === "reconcile" && !exists)
|
|
358
|
+
throw new AppError("reconciliation_storage_unprepared", "Reconciliation cannot create a runtime store", 1);
|
|
357
359
|
if (!exists || mode === "migrate")
|
|
358
360
|
ensureDir(ddFlowHome);
|
|
359
361
|
const db = mode === "read_existing" ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
|
|
@@ -365,7 +367,10 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
|
|
|
365
367
|
}
|
|
366
368
|
if (mode !== "read_existing") {
|
|
367
369
|
try {
|
|
368
|
-
|
|
370
|
+
if (mode === "reconcile")
|
|
371
|
+
registerReconciliationWriter(db);
|
|
372
|
+
else
|
|
373
|
+
registerStoreWriter(db);
|
|
369
374
|
}
|
|
370
375
|
catch (error) {
|
|
371
376
|
return closeAfterOpenFailure(db, error);
|
|
@@ -379,7 +384,7 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
|
|
|
379
384
|
}
|
|
380
385
|
// Internal additive schema changes must be available to every write command.
|
|
381
386
|
// Higher-level Memory Bank migrations remain explicit in services/migrations.
|
|
382
|
-
if (mode !== "read_existing") {
|
|
387
|
+
if (mode !== "read_existing" && mode !== "reconcile") {
|
|
383
388
|
try {
|
|
384
389
|
const initialize = () => { migrate(db); ensureRecoveryStorage(db); ensureLifecycleInvocationStorage(db); };
|
|
385
390
|
if (mode !== "hook") {
|
|
@@ -402,6 +407,16 @@ export function getDatabase(ddFlowHome, mode = "initialize", importPermitId, hoo
|
|
|
402
407
|
}
|
|
403
408
|
if (mode === "read_existing")
|
|
404
409
|
db.exec("PRAGMA query_only = ON");
|
|
410
|
+
if (mode === "reconcile") {
|
|
411
|
+
try {
|
|
412
|
+
db.prepare("SELECT id, project_id, status, verdict, next_action, index_json, updated_at, completed_at FROM runs LIMIT 0");
|
|
413
|
+
db.prepare("SELECT id, root FROM projects LIMIT 0");
|
|
414
|
+
db.prepare("SELECT id, event_type, payload_json FROM audit_events LIMIT 0");
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
return closeAfterOpenFailure(db, error);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
405
420
|
db.exec("PRAGMA foreign_keys = ON");
|
|
406
421
|
return databaseFacade(dbPath, mode !== "read_existing", db, undefined, deadline);
|
|
407
422
|
}
|
|
@@ -35,6 +35,14 @@ export function registerStoreWriter(db) {
|
|
|
35
35
|
throw new AppError("store_writer_incompatible", "This CLI cannot write the store's required contract", 1, { required: current, supported: version });
|
|
36
36
|
db.function("dd_flow_writer_version", () => version);
|
|
37
37
|
}
|
|
38
|
+
/** Health maintenance uses only verified legacy lifecycle columns and never
|
|
39
|
+
* migrates schema. Keep the retained writer fence, including version 1. */
|
|
40
|
+
export function registerReconciliationWriter(db) {
|
|
41
|
+
const current = readStoreWriterContract(db);
|
|
42
|
+
if (current !== null && current !== 1 && current !== version)
|
|
43
|
+
throw new AppError("store_writer_incompatible", "Unsupported reconciliation writer contract", 1, { required: current, supported: [1, version] });
|
|
44
|
+
db.function("dd_flow_writer_version", () => current ?? version);
|
|
45
|
+
}
|
|
38
46
|
export function readStoreWriterContract(db) {
|
|
39
47
|
if (!db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?").get(table))
|
|
40
48
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.136",
|
|
4
4
|
"description": "Mechanical runtime CLI for dd-flow workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.23.0",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"release:publish": "node scripts/publish-release.mjs"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
+
"@opencode/client": "2.0.28",
|
|
45
|
+
"@opencode/plugin": "2.0.28",
|
|
44
46
|
"@changesets/cli": "^2.31.0",
|
|
45
47
|
"@eslint/js": "^9.39.1",
|
|
46
48
|
"@types/node": "^24.10.1",
|
|
@@ -51,6 +53,7 @@
|
|
|
51
53
|
},
|
|
52
54
|
"dependencies": {
|
|
53
55
|
"ajv": "^8.20.0",
|
|
56
|
+
"jsonc-parser": "3.3.1",
|
|
54
57
|
"shell-quote": "^1.10.0",
|
|
55
58
|
"smol-toml": "^1.6.1",
|
|
56
59
|
"tree-sitter": "0.25.1",
|