@deksden-com/dd-flow-cli 0.9.0-beta.74 → 0.9.0-beta.79
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 +8 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +1 -1
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1015 -423
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -2
- package/dist/harness-runtime/lib/dd-codex.mjs +137 -13
- package/dist/harness-runtime/lib/dd-droid.mjs +3 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- 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/native-hook-command.d.mts +1 -0
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +49 -8
- package/dist/services/cli-operation-classifier.js +12 -24
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +7 -11
- package/dist/services/dashboard.js +94 -31
- package/dist/services/engines.js +75 -61
- package/dist/services/eval-snapshots.js +138 -38
- package/dist/services/execution-policy.js +5 -12
- package/dist/services/hooks.js +226 -203
- package/dist/services/lanes.js +60 -52
- package/dist/services/lifecycle-command.js +13 -1
- package/dist/services/lifecycle-invocations.js +285 -94
- package/dist/services/managed-processes.js +158 -18
- package/dist/services/merge-queue.js +173 -98
- package/dist/services/migrations.js +13 -8
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +11 -2
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +54 -19
- package/dist/services/recovery-observation-budget.js +1 -1
- package/dist/services/run-controller-adapter.js +31 -20
- package/dist/services/run-controller-state.js +5 -0
- package/dist/services/run-controller.js +102 -56
- package/dist/services/run-fork.js +36 -22
- package/dist/services/run-recovery.js +32 -17
- package/dist/services/runs.js +110 -38
- package/dist/services/runtime-budget.js +120 -48
- package/dist/services/runtime-scope-capture.js +2 -2
- package/dist/services/runtime-scope-control.js +2 -2
- package/dist/services/runtime-scope-resume.js +34 -12
- package/dist/services/runtime-scope-worker.js +49 -16
- package/dist/services/runtime-service.js +8 -7
- package/dist/services/schema-validation.js +9 -7
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +42 -16
- package/dist/services/stage-lifecycle.js +82 -100
- package/dist/services/stage-pause.js +54 -24
- package/dist/services/vnext-code-review.js +82 -32
- package/dist/services/vnext-code.js +78 -45
- package/dist/services/vnext-fanout.js +13 -12
- package/dist/services/vnext-merge.js +68 -35
- package/dist/services/vnext-plan-review.js +85 -42
- package/dist/services/vnext-plan.js +91 -70
- package/dist/services/vnext-protocolize.js +116 -50
- package/dist/services/vnext-specify.js +70 -57
- package/dist/services/work-registry.js +207 -94
- package/dist/services/worktrees.js +85 -35
- package/dist/storage/database.js +104 -10
- package/dist/storage/paths.js +17 -4
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { createContext } from "../runtime/context.js";
|
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
7
|
import { getResourceDatabase } from "../storage/database.js";
|
|
8
8
|
import { managedProcessBinding, processIsAlive, processTreeIsAlive, resourceHome } from "./managed-processes.js";
|
|
9
|
-
import { runtimeBudgetStatus, scopeLocalProcessRole,
|
|
9
|
+
import { runtimeBudgetStatus, scopeLocalProcessRole, validateRetainedRuntimeScopeManifest, prepareRuntimeScopeFence } from "./runtime-budget.js";
|
|
10
10
|
import { nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
|
|
11
11
|
import { adapterSessionId, runHarnessAdapter } from "./harness-adapter.js";
|
|
12
12
|
import { recordControllerResume, resumeRunController } from "./run-controller-recovery.js";
|
|
@@ -163,7 +163,7 @@ function publishScopeRelease(context, intent, runs, native) {
|
|
|
163
163
|
const db = getResourceDatabase(resourceHome(context));
|
|
164
164
|
if (runs.some(run => run.prepared !== true) || native.some(owner => owner?.prepared !== true))
|
|
165
165
|
throw new AppError("runtime_scope_preparation_required", "Every owned RUN and native Session must be prepared", 1);
|
|
166
|
-
const manifest =
|
|
166
|
+
const manifest = validateRetainedRuntimeScopeManifest(intent.manifest, intent.scope_id);
|
|
167
167
|
const artifactRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-capture' AND resource_key = ?", [intent.capture_key]);
|
|
168
168
|
const artifact = artifactRow ? JSON.parse(artifactRow.metadata_json) : null;
|
|
169
169
|
const journal = path.join(path.dirname(manifest.manifest_path), "events.jsonl");
|
|
@@ -482,11 +482,14 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
|
|
|
482
482
|
}
|
|
483
483
|
/** Freeze the preparation plan under the same arbiter as stop. This accepts
|
|
484
484
|
* ownership work, not productive dispatch; only all-role release may unfence. */
|
|
485
|
-
export function
|
|
486
|
-
|
|
485
|
+
export function prepareRuntimeScopeResumeInput(input) {
|
|
486
|
+
prepareRuntimeScopeFence(input.scopeId, input.requestId);
|
|
487
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !Number.isSafeInteger(input.generation) || input.generation < 1 || typeof input.captureKey !== "string" || !/^[a-f0-9]{64}$/.test(input.captureKey))
|
|
487
488
|
throw new AppError("runtime_scope_resume_invalid", "Scope resume requires its exact generation, capture and request identity", 2);
|
|
488
|
-
|
|
489
|
-
|
|
489
|
+
}
|
|
490
|
+
export function prepareRuntimeScopeResumeRequest(context, input) {
|
|
491
|
+
prepareRuntimeScopeResumeInput(input);
|
|
492
|
+
const db = getResourceDatabase(resourceHome(context), "read_existing"), key = hash(JSON.stringify([input.scopeId, input.requestId]));
|
|
490
493
|
try {
|
|
491
494
|
const latest = runtimeBudgetStatus(context, input.scopeId);
|
|
492
495
|
const control = latest.control;
|
|
@@ -497,7 +500,6 @@ export function recordRuntimeScopeResume(context, input) {
|
|
|
497
500
|
throw new AppError("runtime_scope_resume_conflict", "Resume request identity was used for another source", 1);
|
|
498
501
|
const active = latest.resume;
|
|
499
502
|
const sameRequest = active?.generation === input.generation && active.request_id === input.requestId;
|
|
500
|
-
db.exec("COMMIT");
|
|
501
503
|
return { ...receipt, ...(sameRequest ? active : {}), ...(sameRequest && active.status === "released" ? { release: latest.release } : {}), current: sameRequest && active.current === true, reused: true };
|
|
502
504
|
}
|
|
503
505
|
if (latest.fence || control?.generation !== input.generation)
|
|
@@ -509,7 +511,7 @@ export function recordRuntimeScopeResume(context, input) {
|
|
|
509
511
|
const drain = drainRow ? JSON.parse(drainRow.metadata_json) : null;
|
|
510
512
|
if (drain?.scope_id !== input.scopeId || drain.generation !== input.generation || drain.requested_mode !== control.requested_mode || drain.physical_settled !== true || drain.capture?.journal?.artifact_key !== input.captureKey || drain.capture.journal.trailing_bytes !== 0 || !Array.isArray(drain.capture.run_captures) || drain.capture.run_captures.some((run) => run.status !== "verified"))
|
|
511
513
|
throw new AppError("runtime_scope_capture_required", "Resume requires a complete all-role drain and verified RUN captures", 1);
|
|
512
|
-
const manifest =
|
|
514
|
+
const manifest = validateRetainedRuntimeScopeManifest(drain.capture.manifest, input.scopeId);
|
|
513
515
|
if (JSON.stringify(manifest) !== JSON.stringify(latest.manifest))
|
|
514
516
|
throw new AppError("runtime_scope_manifest_changed", "Resume scope manifest changed after capture", 1);
|
|
515
517
|
const bindings = latest.processes.map(record => managedProcessBinding(record, resourceHome(context)));
|
|
@@ -530,12 +532,32 @@ export function recordRuntimeScopeResume(context, input) {
|
|
|
530
532
|
throw new AppError("runtime_scope_journal_changed", "Resume requires the current immutable EVAL journal capture", 1);
|
|
531
533
|
const runs = drain.capture.run_captures.map((run) => ({ ...run, resume_request_id: `scope-resume:${hash(JSON.stringify([input.scopeId, input.generation, input.requestId, run.runtime_home, run.project_id, run.run_id]))}` }));
|
|
532
534
|
const receipt = { scope_id: input.scopeId, generation: input.generation, request_id: input.requestId, source_request_id: control.request_id, capture_key: input.captureKey, status: "preparing", dispatch_blocked: true, manifest, process_bindings: bindings, runs, native: drain.capture.native_bindings, probes: drain.capture.probe_operations, requested_at: context.now() };
|
|
533
|
-
const payload = JSON.stringify(receipt), now = context.now();
|
|
534
|
-
db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-resume-request',?,?,?,?,NULL,?,?,?)", [key, input.scopeId, key, now, payload, now, now]);
|
|
535
|
-
db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-resume',?,?,?,?,NULL,?,?,?) ON CONFLICT(resource_kind,resource_key) DO UPDATE SET lease_token = excluded.lease_token, metadata_json = excluded.metadata_json, updated_at = excluded.updated_at", [input.scopeId, input.scopeId, key, now, payload, now, now]);
|
|
536
|
-
db.exec("COMMIT");
|
|
537
535
|
return { ...receipt, current: true, reused: false };
|
|
538
536
|
}
|
|
537
|
+
finally {
|
|
538
|
+
db.close?.();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
export function recordRuntimeScopeResume(context, input) {
|
|
542
|
+
const prepared = prepareRuntimeScopeResumeRequest(context, input);
|
|
543
|
+
if (prepared.reused)
|
|
544
|
+
return prepared;
|
|
545
|
+
const db = getResourceDatabase(resourceHome(context)), key = hash(JSON.stringify([input.scopeId, input.requestId]));
|
|
546
|
+
db.beginWriteTransaction();
|
|
547
|
+
try {
|
|
548
|
+
// Recheck mutable capture/ownership authority under the existing arbiter.
|
|
549
|
+
const result = prepareRuntimeScopeResumeRequest(context, input);
|
|
550
|
+
if (!result.reused) {
|
|
551
|
+
const receipt = { ...result };
|
|
552
|
+
delete receipt.current;
|
|
553
|
+
delete receipt.reused;
|
|
554
|
+
const payload = JSON.stringify(receipt), now = context.now();
|
|
555
|
+
db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-resume-request',?,?,?,?,NULL,?,?,?)", [key, input.scopeId, key, now, payload, now, now]);
|
|
556
|
+
db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-resume',?,?,?,?,NULL,?,?,?) ON CONFLICT(resource_kind,resource_key) DO UPDATE SET lease_token = excluded.lease_token, metadata_json = excluded.metadata_json, updated_at = excluded.updated_at", [input.scopeId, input.scopeId, key, now, payload, now, now]);
|
|
557
|
+
}
|
|
558
|
+
db.exec("COMMIT");
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
539
561
|
catch (error) {
|
|
540
562
|
db.exec("ROLLBACK");
|
|
541
563
|
throw error;
|
|
@@ -7,7 +7,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
7
7
|
import { AppError } from "../shared/errors.js";
|
|
8
8
|
import { getResourceDatabase } from "../storage/database.js";
|
|
9
9
|
import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, processIsAlive, processStartedAt, registerManagedProcess, resourceHome } from "./managed-processes.js";
|
|
10
|
-
import { publicRuntimeScopeWorker, requestRuntimeScopeControl, runtimeBudgetStatus } from "./runtime-budget.js";
|
|
10
|
+
import { publicRuntimeScopeWorker, requestRuntimeScopeControl, runtimeBudgetStatus, prepareRuntimeScopeFence, parseRuntimeBudget } from "./runtime-budget.js";
|
|
11
11
|
import { controlRuntimeScopeRuns } from "./runtime-scope-control.js";
|
|
12
12
|
import { prepareRuntimeScopeResume, recordRuntimeScopeResume } from "./runtime-scope-resume.js";
|
|
13
13
|
import { recoveryObservationBudget, renewRecoveryObservation, withRecoveryObservation } from "./recovery-observation-budget.js";
|
|
@@ -30,15 +30,29 @@ function birth() {
|
|
|
30
30
|
throw new AppError("runtime_scope_owner_unknown", "Scope owner birth identity is unavailable", 1);
|
|
31
31
|
return { pid: process.pid, pid_started_at: started };
|
|
32
32
|
}
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
if (!
|
|
36
|
-
throw new AppError("
|
|
33
|
+
export function prepareRuntimeScopeReconciliation(context, scopeId, generation, requestId) {
|
|
34
|
+
prepareRuntimeScopeFence(scopeId, requestId);
|
|
35
|
+
if (!Number.isSafeInteger(generation) || generation < 1)
|
|
36
|
+
throw new AppError("validation", "Scope reconciliation requires a positive integer generation", 2);
|
|
37
37
|
renewRecoveryObservation({}, requestId);
|
|
38
|
+
const db = getResourceDatabase(resourceHome(context), "read_existing");
|
|
39
|
+
try {
|
|
40
|
+
const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
|
|
41
|
+
const control = row ? JSON.parse(row.metadata_json) : null;
|
|
42
|
+
if (!control || control.generation !== generation)
|
|
43
|
+
throw new AppError("runtime_scope_control_superseded", "Reconciliation requires the current controlled scope generation", 1);
|
|
44
|
+
return control;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
db.close?.();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function reconcileRuntimeScopeControl(context, scopeId, generation, requestId) {
|
|
51
|
+
const control = prepareRuntimeScopeReconciliation(context, scopeId, generation, requestId);
|
|
38
52
|
return launchRuntimeScopeControl(context, scopeId, control.request_id, control.requested_mode, undefined, requestId);
|
|
39
53
|
}
|
|
40
|
-
export async function launchRuntimeScopeControl(context, scopeId, requestId, mode, manifest, reconcileId, resumeId) {
|
|
41
|
-
const control = requestRuntimeScopeControl(context, scopeId, requestId, mode, manifest), db = getResourceDatabase(resourceHome(context)), key = keyFor(scopeId, control.generation);
|
|
54
|
+
export async function launchRuntimeScopeControl(context, scopeId, requestId, mode, manifest, reconcileId, resumeId, prepared) {
|
|
55
|
+
const control = requestRuntimeScopeControl(context, scopeId, requestId, mode, manifest, prepared), db = getResourceDatabase(resourceHome(context)), key = keyFor(scopeId, control.generation);
|
|
42
56
|
let worker;
|
|
43
57
|
db.beginWriteTransaction();
|
|
44
58
|
try {
|
|
@@ -103,19 +117,38 @@ export async function launchRuntimeScopeControl(context, scopeId, requestId, mod
|
|
|
103
117
|
const retained = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ?", [key]);
|
|
104
118
|
return { scope_id: scopeId, control, current: true, settled: false, worker: publicRuntimeScopeWorker(JSON.parse(retained.metadata_json)) };
|
|
105
119
|
}
|
|
120
|
+
export function prepareRuntimeScopeServing(context, scopeId, generation) {
|
|
121
|
+
parseRuntimeBudget(JSON.stringify({ schema_id: "dd-flow/runtime-budget@1", scope_id: scopeId, per_harness: {} }));
|
|
122
|
+
if (!Number.isSafeInteger(generation) || generation < 1)
|
|
123
|
+
throw new AppError("validation", "Scope worker requires a positive integer generation", 2);
|
|
124
|
+
const token = context.env.DD_FLOW_SCOPE_CONTROL_TOKEN;
|
|
125
|
+
if (!token)
|
|
126
|
+
throw new AppError("runtime_scope_owner_unknown", "Scope worker requires its launch token", 1);
|
|
127
|
+
const key = keyFor(scopeId, generation), db = getResourceDatabase(resourceHome(context), "read_existing");
|
|
128
|
+
try {
|
|
129
|
+
requireStartingScopeWorker(db, key, token);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
db.close?.();
|
|
133
|
+
}
|
|
134
|
+
return { key, token };
|
|
135
|
+
}
|
|
136
|
+
function requireStartingScopeWorker(db, key, token) {
|
|
137
|
+
const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [key, token]);
|
|
138
|
+
if (!row)
|
|
139
|
+
throw new AppError("runtime_scope_owner_unknown", "Scope worker claim changed", 1);
|
|
140
|
+
const worker = JSON.parse(row.metadata_json);
|
|
141
|
+
if (worker.owner || worker.status !== "starting")
|
|
142
|
+
throw new AppError("runtime_scope_already_owned", "Scope control already has a physical owner", 1);
|
|
143
|
+
return worker;
|
|
144
|
+
}
|
|
106
145
|
export async function serveRuntimeScopeControl(context, scopeId, generation) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const db = getResourceDatabase(resourceHome(context)), key = keyFor(scopeId, generation), token = context.env.DD_FLOW_SCOPE_CONTROL_TOKEN;
|
|
146
|
+
const { key, token } = prepareRuntimeScopeServing(context, scopeId, generation);
|
|
147
|
+
const db = getResourceDatabase(resourceHome(context));
|
|
110
148
|
let worker;
|
|
111
149
|
db.beginWriteTransaction();
|
|
112
150
|
try {
|
|
113
|
-
|
|
114
|
-
if (!row)
|
|
115
|
-
throw new AppError("runtime_scope_owner_unknown", "Scope worker claim changed", 1);
|
|
116
|
-
worker = JSON.parse(row.metadata_json);
|
|
117
|
-
if (worker.owner || worker.status !== "starting")
|
|
118
|
-
throw new AppError("runtime_scope_already_owned", "Scope control already has a physical owner", 1);
|
|
151
|
+
worker = requireStartingScopeWorker(db, key, token);
|
|
119
152
|
worker.owner = birth();
|
|
120
153
|
worker.status = "running";
|
|
121
154
|
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
|
|
@@ -7,22 +7,23 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
7
7
|
import { resolveRun } from "./runs.js";
|
|
8
8
|
import { codeExecutionEnvironment, runCheck } from "./code-checks.js";
|
|
9
9
|
import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, processTreeIsAlive, registerManagedProcess, reservePorts, resourceHome, stopManagedProcess } from "./managed-processes.js";
|
|
10
|
-
|
|
11
|
-
* Its ready receipt is emitted immediately; a separate stop ends this command.
|
|
12
|
-
* Readiness is infrastructure evidence, never a successful scenario verdict. */
|
|
13
|
-
export async function startRuntimeService(context, input) {
|
|
10
|
+
export function prepareRuntimeService(context, input) {
|
|
14
11
|
if (!Number.isFinite(input.timeoutMs) || input.timeoutMs <= 0)
|
|
15
12
|
throw new AppError("validation", "Readiness timeout must be positive and finite", 2);
|
|
16
|
-
if (!input.command.trim() || !input.ports.length || input.ports.some(name => !/^[a-z][a-z0-9_]*$/.test(name)) || new Set(input.ports).size !== input.ports.length || !input.ports.includes(input.readyPort) || !input.readyPath.startsWith("/") || input.readyPath.startsWith("//"))
|
|
13
|
+
if (typeof input.command !== "string" || !input.command.trim() || !Array.isArray(input.ports) || !input.ports.length || input.ports.some(name => typeof name !== "string" || !/^[a-z][a-z0-9_]*$/.test(name)) || new Set(input.ports).size !== input.ports.length || !input.ports.includes(input.readyPort) || typeof input.readyPath !== "string" || !input.readyPath.startsWith("/") || input.readyPath.startsWith("//"))
|
|
17
14
|
throw new AppError("validation", "Declare unique lowercase port names, a ready-port from that list and a local /ready-path", 2);
|
|
18
15
|
const project = requireProjectByRoot(context, path.resolve(input.projectRoot));
|
|
19
16
|
const run = resolveRun(context, project.id, input.runId);
|
|
20
17
|
if (!run?.run_root || !run.workspace_root)
|
|
21
18
|
throw new AppError("not_found", "Registered RUN with a materialized workspace is required", 2);
|
|
22
|
-
input = { ...input, runId: run.id };
|
|
23
19
|
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
20
|
+
return { project, run, runHome: run.run_root, generation };
|
|
21
|
+
}
|
|
22
|
+
export async function startRuntimeService(context, input) {
|
|
23
|
+
const { project, run, runHome, generation } = prepareRuntimeService(context, input);
|
|
24
|
+
input = { ...input, runId: run.id };
|
|
24
25
|
const id = `PROC-${crypto.randomUUID()}`;
|
|
25
|
-
const directory = path.join(
|
|
26
|
+
const directory = path.join(runHome, "runtime", id);
|
|
26
27
|
fs.mkdirSync(directory, { recursive: true });
|
|
27
28
|
const record = registerManagedProcess(context, { id, kind: "runtime-service", ownerId: `run:${input.runId}`, ownerPid: process.pid, runId: input.runId, projectId: project.id, stdoutPath: path.join(directory, "stdout.log"), stderrPath: path.join(directory, "stderr.log"), metadata: { command: input.command } });
|
|
28
29
|
const receiptPath = path.join(directory, "service.json");
|
|
@@ -95,7 +95,10 @@ export function validateSchema(options) {
|
|
|
95
95
|
}
|
|
96
96
|
const filePath = path.resolve(options.file);
|
|
97
97
|
const data = Object.hasOwn(options, "data") ? options.data : readJson(filePath, "input file");
|
|
98
|
-
|
|
98
|
+
// `data` may have come from stdin or from the prepare phase. Do not reopen
|
|
99
|
+
// `options.file` merely to infer the RUN-bound schema: apart from being a
|
|
100
|
+
// second read of caller input, `<stdin>` is not a filesystem path at all.
|
|
101
|
+
const schemaResolution = resolveSchema(options, data);
|
|
99
102
|
const schema = readJson(schemaResolution.path, "schema");
|
|
100
103
|
// Ajv's default export only carries the draft-07 meta-schema. New stage
|
|
101
104
|
// contracts may deliberately use draft 2020-12, so choose the validator
|
|
@@ -133,8 +136,8 @@ export function validateSchema(options) {
|
|
|
133
136
|
errors: []
|
|
134
137
|
};
|
|
135
138
|
}
|
|
136
|
-
function resolveSchema(options) {
|
|
137
|
-
const bound = resolveRunBoundSchema(options);
|
|
139
|
+
function resolveSchema(options, data) {
|
|
140
|
+
const bound = resolveRunBoundSchema(options, data);
|
|
138
141
|
if (bound)
|
|
139
142
|
return bound;
|
|
140
143
|
const fileNames = [`${options.schemaName}.schema.json`];
|
|
@@ -180,10 +183,9 @@ const historicalSchemaRegistry = {
|
|
|
180
183
|
checksum: "d9889af7f2c0afb47e2abab330aea3e1750879d0738d87d38d2d20e3fe0403f3"
|
|
181
184
|
}]
|
|
182
185
|
};
|
|
183
|
-
function resolveRunBoundSchema(options) {
|
|
186
|
+
function resolveRunBoundSchema(options, data) {
|
|
184
187
|
if (!options.runRoot || options.schemaDir)
|
|
185
188
|
return null;
|
|
186
|
-
const data = readJson(path.resolve(options.file), "input file");
|
|
187
189
|
const root = asRecord(data);
|
|
188
190
|
const run = root ? asRecord(objectValue(root, "run")) : undefined;
|
|
189
191
|
const inferredRunId = run ? objectValue(run, "run_id") : objectValue(root ?? {}, "run_id");
|
|
@@ -268,13 +270,13 @@ function readJson(file, label) {
|
|
|
268
270
|
text = fs.readFileSync(file, "utf8");
|
|
269
271
|
}
|
|
270
272
|
catch (error) {
|
|
271
|
-
throw new AppError("validation", `Cannot read ${label}: ${file}`, 2, { cause: String(error) });
|
|
273
|
+
throw new AppError(label === "input file" ? "validation" : "schema_registry_invalid", `Cannot read ${label}: ${file}`, label === "input file" ? 2 : 1, { cause: String(error) });
|
|
272
274
|
}
|
|
273
275
|
try {
|
|
274
276
|
return JSON.parse(text);
|
|
275
277
|
}
|
|
276
278
|
catch (error) {
|
|
277
|
-
throw new AppError("invalid_json", `Invalid JSON in ${label}: ${file}`, 2, { cause: String(error) });
|
|
279
|
+
throw new AppError(label === "input file" ? "invalid_json" : "schema_registry_invalid", `Invalid JSON in ${label}: ${file}`, label === "input file" ? 2 : 1, { cause: String(error) });
|
|
278
280
|
}
|
|
279
281
|
}
|
|
280
282
|
function formatAjvError(error) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
2
|
import { continuationPolicies, flowKinds } from "../domain/contracts.js";
|
|
4
3
|
import { AppError } from "../shared/errors.js";
|
|
5
|
-
import {
|
|
4
|
+
import { hookSessionIdentity } from "./hooks.js";
|
|
6
5
|
import { parseJsonObject } from "../shared/json.js";
|
|
6
|
+
import { prepareTextFile } from "../cli/input-preparation.js";
|
|
7
7
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
8
8
|
import { appendAudit } from "./audit.js";
|
|
9
9
|
import { cancelLaneWaitersForWorker } from "./lanes.js";
|
|
@@ -30,8 +30,8 @@ export function recordFlowSessionObservation(context, input) {
|
|
|
30
30
|
const open = context.db.get(`SELECT id, run_id, protocol_id FROM flow_session_segments
|
|
31
31
|
WHERE project_id = ? AND session_id = ? AND ended_at IS NULL ORDER BY started_at DESC, id DESC LIMIT 1`, [input.projectId, input.sessionId]);
|
|
32
32
|
const now = context.now();
|
|
33
|
-
const runId = input.runId ?? null;
|
|
34
|
-
const protocolId = input.protocolId ?? null;
|
|
33
|
+
const runId = input.runId === undefined ? open?.run_id ?? null : input.runId;
|
|
34
|
+
const protocolId = input.protocolId === undefined ? open?.protocol_id ?? null : input.protocolId;
|
|
35
35
|
if (open && open.run_id === runId && open.protocol_id === protocolId) {
|
|
36
36
|
return { changed: false, segment_id: open.id };
|
|
37
37
|
}
|
|
@@ -42,27 +42,6 @@ export function recordFlowSessionObservation(context, input) {
|
|
|
42
42
|
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)`, [input.projectId, input.sessionId, runId, protocolId, now, input.cwd ?? null, input.toolName ?? null, input.eventKey]);
|
|
43
43
|
return { changed: true, segment_id: Number(result.lastInsertRowid) };
|
|
44
44
|
}
|
|
45
|
-
export function bindObservedFlowSession(context, project, payload, sessionId) {
|
|
46
|
-
if (resolveProjectRoot(payload.project_root) !== project.root) {
|
|
47
|
-
throw new AppError("project_mismatch", "Observed session payload project_root does not match hook cwd project", 1, {
|
|
48
|
-
expected: project.root,
|
|
49
|
-
actual: payload.project_root
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
const session = upsertFlowSession(context, project, payload, sessionId);
|
|
53
|
-
if (session.session_kind !== "orchestrator") {
|
|
54
|
-
const now = context.now();
|
|
55
|
-
for (const unit of payload.coverage_units ?? []) {
|
|
56
|
-
if (!unit.job_id)
|
|
57
|
-
continue;
|
|
58
|
-
context.db.run(`UPDATE flow_jobs SET status = 'running', worker_session_id = ?, attempts = attempts + 1, started_at = COALESCE(started_at, ?), updated_at = ?
|
|
59
|
-
WHERE project_id = ? AND run_id = ? AND job_id = ? AND status IN ('pending', 'failed')`, [session.session_id, now, now, project.id, session.run_id ?? "", unit.job_id]);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (session.run_id)
|
|
63
|
-
refreshRunSessionProjection(context, project.id, session.run_id);
|
|
64
|
-
return session;
|
|
65
|
-
}
|
|
66
45
|
export function registerFlowJob(context, input) {
|
|
67
46
|
const jobId = input.jobId ?? `JOB-${input.runId}-${input.planItemId}`.replace(/[^A-Za-z0-9_-]+/g, "-");
|
|
68
47
|
const now = context.now();
|
|
@@ -76,9 +55,33 @@ export function flowJobsForRun(context, projectId, runId) {
|
|
|
76
55
|
return context.db.all("SELECT * FROM flow_jobs WHERE project_id = ? AND run_id = ? ORDER BY plan_item_id", [projectId, runId]);
|
|
77
56
|
}
|
|
78
57
|
export function registerFlowSession(context, input) {
|
|
79
|
-
const payload =
|
|
58
|
+
const payload = input.preparedPayload ?? prepareFlowSessionPayload(input);
|
|
80
59
|
const project = requireProjectByRoot(context, resolveProjectRoot(payload.project_root));
|
|
81
|
-
|
|
60
|
+
if (payload.run_id && !context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [project.id, payload.run_id])) {
|
|
61
|
+
throw new AppError("not_found", `Run is not registered: ${payload.run_id}`, 1, { run_id: payload.run_id, effect: "no_effect" });
|
|
62
|
+
}
|
|
63
|
+
const identity = input.hookEventId ? hookSessionIdentity(context, project.id, input.hookEventId) : undefined;
|
|
64
|
+
if (!identity && input.sessionId && payload.provider_session_id && payload.provider_session_id !== input.sessionId) {
|
|
65
|
+
throw new AppError("validation", "--session-id conflicts with payload provider_session_id", 2);
|
|
66
|
+
}
|
|
67
|
+
const session = upsertFlowSession(context, project, identity ? {
|
|
68
|
+
...payload, harness: identity.harness, provider_session_id: identity.providerSessionId,
|
|
69
|
+
parent_session_id: identity.parentSessionId, agent_id: identity.agentId,
|
|
70
|
+
provider: identity.provider, model: identity.model, reasoning: identity.reasoning,
|
|
71
|
+
mode: identity.mode, agent_type: identity.agentType
|
|
72
|
+
} : { ...payload, ...(input.sessionId ? { provider_session_id: input.sessionId } : {}) }, identity?.sessionId ?? input.sessionId);
|
|
73
|
+
recordFlowSessionObservation(context, { projectId: project.id, sessionId: session.session_id,
|
|
74
|
+
runId: session.run_id, protocolId: session.protocol_id, cwd: session.cwd,
|
|
75
|
+
eventKey: `session-register-${crypto.randomUUID()}` });
|
|
76
|
+
// Logical Work/job binding belongs to the successful CLI registration, never
|
|
77
|
+
// to observation of model-supplied payload by a pre-tool hook.
|
|
78
|
+
if (session.session_kind !== "orchestrator")
|
|
79
|
+
for (const unit of payload.coverage_units ?? []) {
|
|
80
|
+
if (!unit.job_id)
|
|
81
|
+
continue;
|
|
82
|
+
context.db.run(`UPDATE flow_jobs SET status = 'running', worker_session_id = ?, attempts = attempts + 1, started_at = COALESCE(started_at, ?), updated_at = ?
|
|
83
|
+
WHERE project_id = ? AND run_id = ? AND job_id = ? AND status IN ('pending', 'failed')`, [session.session_id, context.now(), context.now(), project.id, session.run_id ?? "", unit.job_id]);
|
|
84
|
+
}
|
|
82
85
|
if (session.run_id)
|
|
83
86
|
appendFlowRunTimelineEvent(context, project.id, session.run_id, { type: "session_bound", session_id: session.session_id });
|
|
84
87
|
if (session.run_id)
|
|
@@ -195,52 +198,15 @@ export function updateFlowSessionContinuation(context, projectId, sessionId, act
|
|
|
195
198
|
refreshRunSessionProjection(context, projectId, session.run_id);
|
|
196
199
|
return nextCount;
|
|
197
200
|
}
|
|
198
|
-
export function
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return undefined;
|
|
202
|
-
if (invocation.operation === "stage_start")
|
|
203
|
-
return flowSessionPayloadFromStageStartCommand(invocation);
|
|
204
|
-
if (invocation.operation !== "session_register")
|
|
205
|
-
return undefined;
|
|
206
|
-
const payloadBase64 = commandOption(invocation, "payload-base64");
|
|
207
|
-
const payloadJson = commandOption(invocation, "payload-json");
|
|
208
|
-
const payloadFile = commandOption(invocation, "payload-file");
|
|
209
|
-
if (!payloadBase64 && !payloadJson && !payloadFile) {
|
|
210
|
-
return undefined;
|
|
211
|
-
}
|
|
212
|
-
return decodeFlowSessionPayload({ payloadBase64, payloadJson, payloadFile });
|
|
213
|
-
}
|
|
214
|
-
function flowSessionPayloadFromStageStartCommand(invocation) {
|
|
215
|
-
const projectRoot = commandOption(invocation, "project-root");
|
|
216
|
-
const stage = commandOption(invocation, "stage");
|
|
217
|
-
const run = commandPosition(invocation, 0);
|
|
218
|
-
if (!projectRoot || !stage || !run || run.startsWith("--"))
|
|
219
|
-
return undefined;
|
|
220
|
-
return {
|
|
221
|
-
project_root: projectRoot,
|
|
222
|
-
flow_kind: ["code", "implementation", "readiness", "merge"].includes(stage) ? "implementation" : "planning",
|
|
223
|
-
run_id: run,
|
|
224
|
-
protocol_id: null,
|
|
225
|
-
worker_id: null,
|
|
226
|
-
workspace_path: null,
|
|
227
|
-
continuation_policy: "go_router",
|
|
228
|
-
session_kind: "orchestrator",
|
|
229
|
-
current_stage: stage,
|
|
230
|
-
coverage_units: []
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
function invocationFromCommand(command) {
|
|
234
|
-
const analysis = parseLifecycleCommand(command);
|
|
235
|
-
return analysis.kind === "none" ? undefined : analysis.invocation;
|
|
236
|
-
}
|
|
237
|
-
function decodeFlowSessionPayload(input) {
|
|
201
|
+
export function prepareFlowSessionPayload(input) {
|
|
202
|
+
if ([input.payloadFile, input.payloadBase64, input.payloadJson].filter(value => value !== undefined).length !== 1)
|
|
203
|
+
throw new AppError("usage", "session register requires exactly one of --payload-file, --payload-base64, or --payload-json", 2);
|
|
238
204
|
if (input.payloadFile) {
|
|
239
|
-
return normalizeFlowSessionPayload(parseJsonObject(
|
|
205
|
+
return normalizeFlowSessionPayload(parseJsonObject(prepareTextFile(input.payloadFile, "session payload").text, "session payload"));
|
|
240
206
|
}
|
|
241
207
|
if (input.payloadBase64) {
|
|
242
208
|
if (/\$[A-Za-z_{(]/.test(input.payloadBase64)) {
|
|
243
|
-
throw new AppError("validation", "session register
|
|
209
|
+
throw new AppError("validation", "session register received an unexpanded shell variable as --payload-base64; expand it before invoking the CLI or use --payload-file", 2);
|
|
244
210
|
}
|
|
245
211
|
return normalizeFlowSessionPayload(parseJsonObject(Buffer.from(input.payloadBase64, "base64").toString("utf8"), "session payload"));
|
|
246
212
|
}
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { AppError } from "../shared/errors.js";
|
|
2
2
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
3
3
|
import { requireProjectByRoot } from "./projects.js";
|
|
4
|
-
import { blockFlowRunStage, unblockFlowRunStage } from "./runs.js";
|
|
4
|
+
import { blockFlowRunStage, unblockFlowRunStage, prepareFlowRunStageBlock, prepareFlowRunStageUnblock } from "./runs.js";
|
|
5
5
|
import { flowCommand } from "./stage-pause.js";
|
|
6
6
|
import { refreshRunWorkProjection } from "./work-registry.js";
|
|
7
|
-
export function
|
|
7
|
+
export function prepareStageRuntimeBlock(context, input) {
|
|
8
|
+
if (typeof input.code !== "string" || !input.code.trim() || typeof input.summary !== "string" || !input.summary.trim())
|
|
9
|
+
throw new AppError("validation", "stage block requires a code and summary", 2);
|
|
8
10
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
9
11
|
const project = requireProjectByRoot(context, projectRoot);
|
|
10
12
|
const run = requireRun(context, project.id, input.runId);
|
|
11
13
|
const work = requireWork(context, project.id, run.id, input.workId);
|
|
12
14
|
if (work.status !== "running")
|
|
13
15
|
throw new AppError("invalid_work_state", "Only a running Work can be blocked", 1, { work_id: work.work_id, status: work.status });
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const flow = prepareFlowRunStageBlock(context, { ...input, projectRoot });
|
|
17
|
+
return { projectRoot, project, work, flow };
|
|
18
|
+
}
|
|
19
|
+
export function blockStageForRuntime(context, input) {
|
|
20
|
+
const { projectRoot, project, work, flow } = prepareStageRuntimeBlock(context, input);
|
|
21
|
+
const state = blockFlowRunStage(context, { ...input, projectRoot }, flow);
|
|
17
22
|
const now = context.now();
|
|
18
23
|
context.db.run("UPDATE works SET status = 'paused', updated_at = ? WHERE work_id = ? AND status = 'running'", [now, work.work_id]);
|
|
19
24
|
refreshRunWorkProjection(context, project.id, work.run_id);
|
|
@@ -30,14 +35,19 @@ export function blockStageForRuntime(context, input) {
|
|
|
30
35
|
state
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
|
-
export function
|
|
38
|
+
export function prepareStageRuntimeUnblock(context, input) {
|
|
34
39
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
35
40
|
const project = requireProjectByRoot(context, projectRoot);
|
|
36
41
|
const run = requireRun(context, project.id, input.runId);
|
|
37
42
|
const work = requireWork(context, project.id, run.id, input.workId);
|
|
38
43
|
if (work.status !== "paused")
|
|
39
44
|
throw new AppError("invalid_work_state", "stage unblock requires its paused Work", 1, { work_id: work.work_id, status: work.status });
|
|
40
|
-
const
|
|
45
|
+
const flow = prepareFlowRunStageUnblock(context, { ...input, projectRoot, runId: work.run_id });
|
|
46
|
+
return { projectRoot, project, work, flow };
|
|
47
|
+
}
|
|
48
|
+
export function unblockStageAfterRuntimeRepair(context, input) {
|
|
49
|
+
const { projectRoot, project, work, flow } = prepareStageRuntimeUnblock(context, input);
|
|
50
|
+
const state = unblockFlowRunStage(context, { projectRoot, runId: work.run_id, stage: input.stage, workId: work.work_id }, flow);
|
|
41
51
|
const now = context.now();
|
|
42
52
|
context.db.run("UPDATE works SET status = 'running', updated_at = ? WHERE work_id = ? AND status = 'paused'", [now, work.work_id]);
|
|
43
53
|
refreshRunWorkProjection(context, project.id, work.run_id);
|
|
@@ -2,33 +2,42 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
|
-
import {
|
|
5
|
+
import { prepareFileBytes, prepareJsonText, prepareOutputFile } from "../cli/input-preparation.js";
|
|
6
6
|
export function loadExternalStageContext(input) {
|
|
7
|
-
const sourcePath =
|
|
8
|
-
if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile())
|
|
9
|
-
throw new AppError("not_found", "Stage context file is missing", 2, { context_file: sourcePath });
|
|
10
|
-
const bytes = fs.readFileSync(sourcePath);
|
|
7
|
+
const { file: sourcePath, bytes } = prepareFileBytes(input.file, "Stage context");
|
|
11
8
|
const materializedSha256 = sha256(bytes);
|
|
12
9
|
if (materializedSha256 !== input.expectedSha256)
|
|
13
10
|
throw new AppError("context_checksum_mismatch", "Stage context checksum does not match --context-sha256", 2, { context_file: sourcePath, expected_sha256: input.expectedSha256, actual_sha256: materializedSha256 });
|
|
14
|
-
const value =
|
|
11
|
+
const value = prepareJsonText(bytes.toString("utf8"), { file: sourcePath, label: "Stage context" }).value;
|
|
15
12
|
validateContext(value, input.stage);
|
|
16
|
-
return { value, sourcePath, materializedSha256 };
|
|
13
|
+
return { value, sourcePath, materializedSha256, bytes };
|
|
17
14
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
fs.
|
|
15
|
+
export function prepareExternalStageContext(input) {
|
|
16
|
+
const destination = externalStageContextDestination(input);
|
|
17
|
+
if (!fs.existsSync(destination))
|
|
18
|
+
prepareOutputFile(destination);
|
|
19
|
+
return destination;
|
|
20
|
+
}
|
|
21
|
+
function externalStageContextDestination(input) {
|
|
21
22
|
const destination = path.join(input.stageRoot, "stage-context.json");
|
|
22
23
|
if (fs.existsSync(destination)) {
|
|
23
24
|
const existing = sha256(fs.readFileSync(destination));
|
|
24
25
|
if (existing !== input.loaded.materializedSha256)
|
|
25
26
|
throw new AppError("stage_context_replacement", "A different stage context is already installed for this Stage", 2, { stage_context: destination, expected_sha256: input.loaded.materializedSha256, actual_sha256: existing });
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
return destination;
|
|
29
|
+
}
|
|
30
|
+
/** Installs exactly one immutable external context for a stage attempt. */
|
|
31
|
+
export function installExternalStageContext(input) {
|
|
32
|
+
// Publication may follow earlier effects. Recheck immutable identity here,
|
|
33
|
+
// but never reclassify a late output failure as a no-effect input rejection.
|
|
34
|
+
const destination = externalStageContextDestination(input);
|
|
35
|
+
fs.mkdirSync(input.stageRoot, { recursive: true });
|
|
36
|
+
if (!fs.existsSync(destination)) {
|
|
28
37
|
// The receipt is the hash of the runner-materialized bytes, not of a
|
|
29
38
|
// convenient reserialization. Preserve those exact bytes as evidence.
|
|
30
39
|
const temporary = `${destination}.${process.pid}.tmp`;
|
|
31
|
-
fs.
|
|
40
|
+
fs.writeFileSync(temporary, input.loaded.bytes);
|
|
32
41
|
fs.renameSync(temporary, destination);
|
|
33
42
|
}
|
|
34
43
|
return {
|
|
@@ -76,15 +85,32 @@ export function renderExternalStageContext(input) {
|
|
|
76
85
|
].join("\n");
|
|
77
86
|
}
|
|
78
87
|
function validateContext(value, expectedStage) {
|
|
88
|
+
const object = (item) => Boolean(item && typeof item === "object" && !Array.isArray(item));
|
|
89
|
+
const text = (item) => typeof item === "string" && Boolean(item.trim());
|
|
90
|
+
if (!object(value))
|
|
91
|
+
throw new AppError("validation", "Stage context must be an object", 2);
|
|
79
92
|
if (value.schema_id !== "dd-flow/stage-context@1" && value.schema_id !== "dd-eval/stage-context@1")
|
|
80
93
|
throw new AppError("validation", "Stage context must use dd-flow/stage-context@1", 2);
|
|
81
94
|
if (value.stage !== expectedStage)
|
|
82
95
|
throw new AppError("validation", "Stage context does not match --stage", 2, { expected_stage: expectedStage, context_stage: value.stage });
|
|
83
|
-
if (!value.objective
|
|
96
|
+
if (!text(value.objective))
|
|
84
97
|
throw new AppError("validation", "Stage context objective is required", 2);
|
|
85
|
-
for (const
|
|
86
|
-
if (
|
|
87
|
-
|
|
98
|
+
for (const key of ["task_input", "sources", "accepted_decisions", "dynamic_roles"]) {
|
|
99
|
+
if (value[key] === undefined)
|
|
100
|
+
continue;
|
|
101
|
+
if (!Array.isArray(value[key]))
|
|
102
|
+
throw new AppError("validation", `Stage context ${key} must be an array`, 2);
|
|
103
|
+
for (const item of value[key]) {
|
|
104
|
+
const valid = key === "accepted_decisions" ? text(item) || object(item) && (text(item.id) || text(item.statement)) && [item.id, item.statement].every(field => field === undefined || text(field))
|
|
105
|
+
: object(item) && text(item.role) && (key === "dynamic_roles" ? item.reason === undefined || text(item.reason)
|
|
106
|
+
: text(item.path) && (key === "sources" ? text(item.reason) && (item.required === undefined || typeof item.required === "boolean") && (item.root === undefined || typeof item.root === "string" && ["project", "workspace", "run"].includes(item.root)) : item.sha256 === undefined || text(item.sha256)));
|
|
107
|
+
if (!valid)
|
|
108
|
+
throw new AppError("validation", `Invalid stage context ${key} item`, 2, { parameter: key });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const key of ["semantic_package_sha256", "context_slice_sha256"]) {
|
|
112
|
+
if (value[key] !== undefined && !text(value[key]))
|
|
113
|
+
throw new AppError("validation", `Stage context ${key} must be a nonempty string`, 2);
|
|
88
114
|
}
|
|
89
115
|
}
|
|
90
116
|
function sha256(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
|