@deksden-com/dd-flow-cli 0.9.0-beta.103 → 0.9.0-beta.105
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 +6 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +32 -11
- package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +11 -6
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/services/cleanup.js +81 -32
- package/dist/services/code-checks.js +18 -19
- package/dist/services/continuation-outcome.js +1 -1
- package/dist/services/controller-fanout.js +3 -3
- package/dist/services/eval-snapshots.js +69 -41
- package/dist/services/execution-policy.js +12 -1
- package/dist/services/hooks.js +150 -42
- package/dist/services/lanes.js +23 -18
- package/dist/services/lifecycle-command.js +11 -2
- package/dist/services/lifecycle-invocations.js +291 -54
- package/dist/services/managed-processes.js +35 -23
- package/dist/services/merge-queue.js +71 -67
- package/dist/services/merge-server.js +3 -11
- package/dist/services/migrations.js +28 -12
- package/dist/services/plan-runtime.js +8 -4
- package/dist/services/projects.js +3 -11
- package/dist/services/protocols.js +98 -68
- package/dist/services/recovery-snapshot-database.js +43 -8
- package/dist/services/run-control-worker.js +15 -32
- package/dist/services/run-control.js +3 -17
- package/dist/services/run-controller-capture.js +58 -27
- package/dist/services/run-controller-process.js +21 -30
- package/dist/services/run-controller-recovery.js +36 -37
- package/dist/services/run-controller.js +81 -86
- package/dist/services/run-fork.js +10 -6
- package/dist/services/run-projection.js +19 -64
- package/dist/services/run-recovery-runtime.js +24 -14
- package/dist/services/run-recovery.js +56 -54
- package/dist/services/runs.js +218 -160
- package/dist/services/runtime-budget.js +19 -45
- package/dist/services/runtime-scope-capture.js +8 -6
- package/dist/services/runtime-scope-control.js +49 -39
- package/dist/services/runtime-scope-resume.js +134 -122
- package/dist/services/runtime-scope-worker.js +48 -49
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/stage-context.js +6 -1
- package/dist/services/stage-lifecycle.js +1 -1
- package/dist/services/stage-pause.js +13 -29
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/usage.js +2 -9
- package/dist/services/vnext-code-review.js +10 -7
- package/dist/services/vnext-code.js +9 -17
- package/dist/services/vnext-merge.js +64 -21
- package/dist/services/vnext-plan-review.js +43 -30
- package/dist/services/vnext-plan.js +28 -24
- package/dist/services/vnext-protocolize.js +183 -20
- package/dist/services/vnext-specify.js +27 -19
- package/dist/services/work-registry.js +229 -150
- package/dist/storage/database.js +286 -56
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/writer-contract.js +3 -3
- package/dist/storage/writer-migration.js +24 -41
- package/package.json +1 -1
- package/tools/repair-paused-run-status.mjs +48 -23
|
@@ -9,9 +9,10 @@ import { managedProcessBinding, processIsAlive, processTreeIsAlive, resourceHome
|
|
|
9
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
|
-
import {
|
|
12
|
+
import { prepareControllerResume, resumeRunController } from "./run-controller-recovery.js";
|
|
13
13
|
import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
14
14
|
import { assertHistoricalDaemonSettlement } from "./native-daemon-history.js";
|
|
15
|
+
import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
|
|
15
16
|
const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
16
17
|
const nativeLaunchEnvironmentKeys = ["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"];
|
|
17
18
|
function nativeLaunchEnvironment(context, saved) {
|
|
@@ -67,16 +68,8 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
|
|
|
67
68
|
if (!project || manifest?.budget?.scope_id !== scopeId || manifest.resource_home !== resourceHome(context))
|
|
68
69
|
throw new AppError("runtime_scope_owner_unknown", "Resume RUN no longer belongs to this retained scope", 1);
|
|
69
70
|
const input = { projectRoot: project.root, runId: source.run_id, requestId: source.resume_request_id, controlId: source.control_id };
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
assertCurrent();
|
|
73
|
-
recordControllerResume(target, input);
|
|
74
|
-
db.exec("COMMIT");
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
db.exec("ROLLBACK");
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
71
|
+
const commitResume = prepareControllerResume(target, input);
|
|
72
|
+
db.writeTransaction(() => { assertCurrent(); commitResume(); });
|
|
80
73
|
assertCurrent();
|
|
81
74
|
const receipt = await resumeRunController(target, { ...input, waitMs: 0 });
|
|
82
75
|
assertCurrent();
|
|
@@ -108,9 +101,19 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
|
|
|
108
101
|
const lockedRuns = new Map(), heldRuns = [];
|
|
109
102
|
let registryHeld = false, runPreparationConsistent = false;
|
|
110
103
|
let released = null, releaseError = null, releaseMessage = null;
|
|
104
|
+
let cleanupErrors = [];
|
|
111
105
|
try {
|
|
112
106
|
for (const home of [...new Set(intent.runs.map((source) => source.runtime_home))].sort())
|
|
113
107
|
lockedRuns.set(home, createContext({ ...context.env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome(context), DD_FLOW_ENGINE_MODE: "1" }));
|
|
108
|
+
const checks = [];
|
|
109
|
+
for (const [index, source] of intent.runs.entries()) {
|
|
110
|
+
const target = lockedRuns.get(source.runtime_home);
|
|
111
|
+
const prepared = preparedScopeRun(target, { scopeId, requestId: source.resume_request_id, projectId: source.project_id, runId: source.run_id, controlId: source.control_id, recoveryId: source.recovery_id }, assertCurrent);
|
|
112
|
+
const proof = prepared.proof;
|
|
113
|
+
checks.push(prepared.assertUnchanged);
|
|
114
|
+
runs[index] = { ...runs[index], ...proof, ...(proof.prepared === true ? { pending: false, reason: null } : {}) };
|
|
115
|
+
}
|
|
116
|
+
const commitRelease = releaseRequestId === undefined ? null : prepareScopeRelease(context, intent, runs, native);
|
|
114
117
|
db.beginWriteTransaction();
|
|
115
118
|
registryHeld = true;
|
|
116
119
|
for (const target of lockedRuns.values()) {
|
|
@@ -118,14 +121,11 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
|
|
|
118
121
|
heldRuns.push(target);
|
|
119
122
|
}
|
|
120
123
|
assertCurrent();
|
|
121
|
-
for (const
|
|
122
|
-
|
|
123
|
-
const proof = preparedScopeRun(target, { scopeId, requestId: source.resume_request_id, projectId: source.project_id, runId: source.run_id, controlId: source.control_id, recoveryId: source.recovery_id }, assertCurrent);
|
|
124
|
-
runs[index] = { ...runs[index], ...proof, ...(proof.prepared === true ? { pending: false, reason: null } : {}) };
|
|
125
|
-
}
|
|
124
|
+
for (const check of checks)
|
|
125
|
+
check();
|
|
126
126
|
runPreparationConsistent = true;
|
|
127
|
-
if (
|
|
128
|
-
const receipt =
|
|
127
|
+
if (commitRelease) {
|
|
128
|
+
const receipt = commitRelease();
|
|
129
129
|
db.exec("COMMIT");
|
|
130
130
|
registryHeld = false;
|
|
131
131
|
released = receipt;
|
|
@@ -138,28 +138,21 @@ export async function prepareRuntimeScopeResume(context, scopeId, generation, re
|
|
|
138
138
|
runs[index] = { ...run, prepared: false, pending: true, reason: error instanceof AppError ? error.code : "scope_writer_boundary_unavailable" };
|
|
139
139
|
}
|
|
140
140
|
finally {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
finally {
|
|
146
|
-
try {
|
|
147
|
-
if (registryHeld)
|
|
148
|
-
db.exec("ROLLBACK");
|
|
149
|
-
}
|
|
150
|
-
finally {
|
|
151
|
-
for (const target of lockedRuns.values())
|
|
152
|
-
target.db.close?.();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
141
|
+
cleanupErrors = cleanupScopeWriters([
|
|
142
|
+
...[...lockedRuns.values()].reverse().map(target => ({ name: target.ddFlowHome, db: target.db, rollback: heldRuns.includes(target), close: "always" })),
|
|
143
|
+
{ name: resourceHome(context), db, rollback: registryHeld, close: "on_error" }
|
|
144
|
+
]);
|
|
155
145
|
}
|
|
156
146
|
if (released)
|
|
157
|
-
return { ...released, status: "released", dispatch_blocked: false, current: true, settled: false, reused: false };
|
|
147
|
+
return { ...released, status: "released", dispatch_blocked: false, current: true, settled: false, reused: false, ...(cleanupErrors.length ? { cleanup_errors: cleanupErrors } : {}) };
|
|
148
|
+
if (cleanupErrors.length)
|
|
149
|
+
throw new AppError(releaseError ?? "scope_writer_cleanup_failed", releaseMessage ?? "Scope preparation cleanup failed", 1, { scope_cleanup_errors: cleanupErrors });
|
|
158
150
|
assertCurrent();
|
|
159
151
|
return { scope_id: scopeId, generation, request_id: intent.request_id, status: "preparing", dispatch_blocked: true, settled: false, release_error: releaseError ? { code: releaseError, message: releaseMessage } : null, run_preparation_consistent: runPreparationConsistent, runs, native, probes: intent.probes, pending_reasons: [...new Set(["scope_release_required", ...(releaseError ? [releaseError] : []), ...(!runPreparationConsistent ? ["scope_writer_boundary_unavailable"] : []), ...(runs.some(item => item.prepared !== true) ? ["run_recovery_required"] : []), ...(native.some(item => item?.prepared !== true) ? ["non_work_recovery_required"] : []), ...(intent.probes.length ? ["probe_reconciliation_required"] : [])])] };
|
|
160
152
|
}
|
|
161
|
-
/**
|
|
162
|
-
|
|
153
|
+
/** Read filesystem/native evidence before reserving writers. The returned
|
|
154
|
+
* SQL-only commit is called with registry -> all-RUN barriers held. */
|
|
155
|
+
function prepareScopeRelease(context, intent, runs, native) {
|
|
163
156
|
const db = getResourceDatabase(resourceHome(context));
|
|
164
157
|
if (runs.some(run => run.prepared !== true) || native.some(owner => owner?.prepared !== true))
|
|
165
158
|
throw new AppError("runtime_scope_preparation_required", "Every owned RUN and native Session must be prepared", 1);
|
|
@@ -251,27 +244,42 @@ function publishScopeRelease(context, intent, runs, native) {
|
|
|
251
244
|
throw new AppError("runtime_scope_inventory_changed", "Release has an unverified process registration", 1);
|
|
252
245
|
const key = hash(JSON.stringify([intent.scope_id, intent.generation])), now = context.now();
|
|
253
246
|
const receipt = { scope_id: intent.scope_id, generation: intent.generation, request_id: intent.request_id, source_request_id: intent.source_request_id, capture_key: intent.capture_key, journal_sha256: hash(artifact.journal_bytes), run_operations: runs.map(run => run.run_operation), native, probes, process_bindings: actual, released_at: now };
|
|
254
|
-
db.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
247
|
+
const resumeRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [intent.scope_id]);
|
|
248
|
+
return () => {
|
|
249
|
+
const latest = runtimeBudgetStatus(context, intent.scope_id);
|
|
250
|
+
const latestBindings = latest.processes.map(record => managedProcessBinding(record, resourceHome(context)));
|
|
251
|
+
if (latest.fence || latest.control?.generation !== intent.generation || latest.provider_turns.length
|
|
252
|
+
|| !isDeepStrictEqual(latestBindings, actual)
|
|
253
|
+
|| db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-capture' AND resource_key = ?", [intent.capture_key])?.metadata_json !== artifactRow.metadata_json
|
|
254
|
+
|| db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [intent.scope_id])?.metadata_json !== resumeRow?.metadata_json)
|
|
255
|
+
throw new AppError("runtime_scope_inventory_changed", "Scope changed while release evidence was prepared", 1);
|
|
256
|
+
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-release',?,?,?,?,NULL,?,?,?)", [key, intent.scope_id, key, now, JSON.stringify(receipt), now, now]);
|
|
257
|
+
if (db.run("DELETE FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ? AND json_extract(metadata_json, '$.generation') = ?", [intent.scope_id, intent.generation]).changes !== 1)
|
|
258
|
+
throw new AppError("runtime_scope_control_superseded", "Release lost its control generation", 1);
|
|
259
|
+
db.run("UPDATE managed_resources SET metadata_json = json_set(metadata_json, '$.status', 'released'), updated_at = ? WHERE resource_kind = 'scope-resume' AND resource_key = ?", [now, intent.scope_id]);
|
|
260
|
+
return receipt;
|
|
261
|
+
};
|
|
259
262
|
}
|
|
260
263
|
/** A preparation snapshot, not an unfence. The release arbiter must repeat
|
|
261
264
|
* these reads while holding every participating RUN writer boundary. */
|
|
262
265
|
function preparedScopeRun(context, input, assertCurrent) {
|
|
263
266
|
const registry = getResourceDatabase(resourceHome(context));
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
context.db.
|
|
268
|
-
|
|
267
|
+
const checks = [];
|
|
268
|
+
const read = (sql, params) => {
|
|
269
|
+
const value = context.db.get(sql, params);
|
|
270
|
+
checks.push(() => { if (!isDeepStrictEqual(context.db.get(sql, params), value))
|
|
271
|
+
throw new AppError("runtime_scope_inventory_changed", "RUN preparation changed before release", 1); });
|
|
272
|
+
return value;
|
|
273
|
+
};
|
|
274
|
+
const finish = (proof) => ({ proof, assertUnchanged: () => { assertCurrent(); for (const check of checks)
|
|
275
|
+
check(); } });
|
|
276
|
+
return registry.readSnapshot(() => context.db.readSnapshot(() => {
|
|
269
277
|
assertCurrent();
|
|
270
|
-
const controller =
|
|
278
|
+
const controller = read("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded')", [input.projectId, input.runId]);
|
|
271
279
|
if (!controller)
|
|
272
280
|
throw new AppError("runtime_scope_owner_unknown", "Prepared RUN lost its controller", 1);
|
|
273
281
|
const state = JSON.parse(controller.state_json), manifest = JSON.parse(controller.manifest_json);
|
|
274
|
-
const operation =
|
|
282
|
+
const operation = read(`SELECT o.operation_id, o.receipt_json FROM run_controller_operations o
|
|
275
283
|
JOIN run_controls ctl ON ctl.control_id = ? AND ctl.project_id = ? AND ctl.run_id = ?
|
|
276
284
|
JOIN run_recovery_guards g ON g.recovery_id = ctl.recovery_id
|
|
277
285
|
WHERE o.controller_id = ? AND o.generation = ? AND o.status = 'accepted'
|
|
@@ -279,61 +287,57 @@ function preparedScopeRun(context, input, assertCurrent) {
|
|
|
279
287
|
AND json_extract(o.input_json, '$.action') = 'control_resume' AND json_extract(o.input_json, '$.request_id') = ?`, [input.controlId, input.projectId, input.runId, controller.controller_id, controller.generation, input.recoveryId, input.requestId]);
|
|
280
288
|
const prepared = operation?.receipt_json ? JSON.parse(operation.receipt_json) : null;
|
|
281
289
|
if (prepared?.prepared !== true)
|
|
282
|
-
return { prepared: false, pending: true, reason: "run_recovery_preparation_required" };
|
|
290
|
+
return finish({ prepared: false, pending: true, reason: "run_recovery_preparation_required" });
|
|
283
291
|
const session = state.current_session === null ? null : state.sessions[state.current_session];
|
|
284
292
|
const owner = registry.get("SELECT * FROM managed_processes WHERE id = ?", [controller.process_id]);
|
|
285
293
|
if (!session || manifest.budget?.scope_id !== input.scopeId || manifest.resource_home !== resourceHome(context) || state.recovery?.operation_id !== operation.operation_id || state.recovery?.control_id !== input.controlId || state.recovery?.recovery_id !== input.recoveryId || prepared.recovery_id !== input.recoveryId || prepared.session_id !== session.id || !owner || owner.lease_token !== controller.process_lease_token || owner.pid !== controller.owner_pid || owner.owner_id !== controller.controller_id || !processIsAlive(owner))
|
|
286
294
|
throw new AppError("runtime_scope_owner_unknown", "RUN preparation lost its current owner or Session", 1);
|
|
287
295
|
const native = requireManagedDaemonBinding(context, { stateDir: session.state_dir, projectId: input.projectId, runId: input.runId, ownerId: session.owner.owner_id });
|
|
288
|
-
const loaded =
|
|
296
|
+
const loaded = read("SELECT receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'completed'", [prepared.resume_operation_id, controller.controller_id, controller.generation]);
|
|
289
297
|
const loadReceipt = loaded?.receipt_json ? JSON.parse(loaded.receipt_json) : null;
|
|
290
298
|
if (!processIsAlive(native.process) || native.process.id !== prepared.process_id || native.state.daemon_id !== prepared.daemon_id || loadReceipt?.settled !== true || adapterSessionId(loadReceipt) !== session.id)
|
|
291
299
|
throw new AppError("runtime_scope_owner_unknown", "RUN preparation has no current native load acknowledgement", 1);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
finally {
|
|
300
|
-
registry.exec("RELEASE scope_run_registry");
|
|
300
|
+
for (const record of [owner, native.process]) {
|
|
301
|
+
const expected = managedProcessBinding(record, resourceHome(context));
|
|
302
|
+
checks.push(() => {
|
|
303
|
+
const latest = registry.get("SELECT * FROM managed_processes WHERE id = ?", [record.id]);
|
|
304
|
+
if (!latest || !isDeepStrictEqual(managedProcessBinding(latest, resourceHome(context)), expected))
|
|
305
|
+
throw new AppError("runtime_scope_owner_unknown", "Prepared RUN lost its process binding", 1);
|
|
306
|
+
});
|
|
301
307
|
}
|
|
302
|
-
|
|
308
|
+
return finish({ prepared: true, run_operation: { runtime_home: context.ddFlowHome, controller_id: controller.controller_id, operation_id: operation.operation_id }, process_bindings: [managedProcessBinding(owner, resourceHome(context)), managedProcessBinding(native.process, resourceHome(context))], native: { state_dir: session.state_dir, daemon_id: prepared.daemon_id, session_id: session.id, inventory_version: nativeInventoryVersion(session.state_dir) } });
|
|
309
|
+
}));
|
|
303
310
|
}
|
|
304
311
|
async function restartScopeNative(context, intent, source, assertCurrent) {
|
|
305
312
|
const db = getResourceDatabase(resourceHome(context)), key = hash(JSON.stringify([intent.scope_id, intent.generation, source.process_id]));
|
|
306
313
|
const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]);
|
|
307
|
-
|
|
314
|
+
let operation = JSON.parse(saved.metadata_json);
|
|
308
315
|
const startup = source.operations.find(item => item.operation === "daemon.start" && item.result?.daemon_id === source.daemon_id);
|
|
309
316
|
const launch = startup.launch;
|
|
310
317
|
const state = () => JSON.parse(fs.readFileSync(path.join(source.state_dir, "daemon.json"), "utf8"));
|
|
311
|
-
const assertRetired = () => {
|
|
318
|
+
const assertRetired = (physical = true) => {
|
|
312
319
|
assertCurrent();
|
|
313
320
|
const old = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]);
|
|
314
|
-
if (!old || processTreeIsAlive(old) || JSON.stringify(managedProcessBinding(old, resourceHome(context))) !== JSON.stringify(intent.process_bindings.find(item => item.process_id === source.process_id)))
|
|
321
|
+
if (!old || physical && processTreeIsAlive(old) || JSON.stringify(managedProcessBinding(old, resourceHome(context))) !== JSON.stringify(intent.process_bindings.find(item => item.process_id === source.process_id)))
|
|
315
322
|
throw new AppError("runtime_scope_owner_unknown", "The retired native tree changed or is still alive", 1);
|
|
316
323
|
};
|
|
317
324
|
assertRetired();
|
|
325
|
+
const predecessor = operation.status === "retired" ? state() : null;
|
|
326
|
+
if (predecessor && (predecessor.daemon_id !== source.daemon_id || predecessor.shutdown_state !== "clean"))
|
|
327
|
+
throw new AppError("runtime_scope_owner_unknown", "Native restart lost its clean predecessor", 1);
|
|
318
328
|
let dispatch = false;
|
|
319
|
-
db.
|
|
320
|
-
|
|
321
|
-
|
|
329
|
+
db.writeTransaction(() => {
|
|
330
|
+
assertRetired(false);
|
|
331
|
+
operation = JSON.parse(db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]).metadata_json);
|
|
322
332
|
if (operation.status === "retired") {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
throw new AppError("runtime_scope_owner_unknown", "Native restart lost its clean predecessor", 1);
|
|
333
|
+
if (!predecessor)
|
|
334
|
+
throw new AppError("runtime_scope_inventory_changed", "Native retirement changed during restart preparation", 1);
|
|
326
335
|
operation.status = "starting";
|
|
327
336
|
operation.restart_operation_id = `scope-restart:${key}`;
|
|
328
337
|
dispatch = true;
|
|
329
338
|
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [JSON.stringify(operation), context.now(), key]);
|
|
330
339
|
}
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
db.exec("ROLLBACK");
|
|
335
|
-
throw error;
|
|
336
|
-
}
|
|
340
|
+
});
|
|
337
341
|
if (!operation.restart_receipt) {
|
|
338
342
|
if (dispatch) {
|
|
339
343
|
const env = nativeLaunchEnvironment(context, launch.env);
|
|
@@ -358,10 +362,11 @@ async function restartScopeNative(context, intent, source, assertCurrent) {
|
|
|
358
362
|
operation.status = "restarted";
|
|
359
363
|
operation.replacement_binding = binding;
|
|
360
364
|
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [JSON.stringify(operation), context.now(), key]);
|
|
361
|
-
const assertReplacement = () => {
|
|
362
|
-
assertRetired();
|
|
363
|
-
const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [replacement.id]), latest = state();
|
|
364
|
-
if (!current ||
|
|
365
|
+
const assertReplacement = (physical = true) => {
|
|
366
|
+
assertRetired(physical);
|
|
367
|
+
const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [replacement.id]), latest = physical ? state() : null;
|
|
368
|
+
if (!current || JSON.stringify(managedProcessBinding(current, resourceHome(context))) !== JSON.stringify(binding)
|
|
369
|
+
|| physical && (!processIsAlive(current) || latest.daemon_id !== daemon.daemon_id || latest.resource_process?.id !== current.id || latest.resource_process?.lease_token !== current.lease_token || JSON.stringify(latest.config?.runtime_owner) !== JSON.stringify(source.runtime_owner)))
|
|
365
370
|
throw new AppError("runtime_scope_owner_unknown", "Session loading lost its native replacement", 1);
|
|
366
371
|
};
|
|
367
372
|
if (!Array.isArray(source.sessions) || !source.sessions.length || source.sessions.some(item => typeof item.session_id !== "string" || !item.session_id) || new Set(source.sessions.map(item => item.session_id)).size !== source.sessions.length)
|
|
@@ -369,10 +374,11 @@ async function restartScopeNative(context, intent, source, assertCurrent) {
|
|
|
369
374
|
const sessions = [];
|
|
370
375
|
for (const session of source.sessions) {
|
|
371
376
|
const operationId = `scope-load:${hash(JSON.stringify([intent.scope_id, intent.generation, source.process_id, session.session_id]))}`;
|
|
372
|
-
let
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
377
|
+
let dispatchLoad = false;
|
|
378
|
+
assertReplacement();
|
|
379
|
+
const load = db.writeTransaction(() => {
|
|
380
|
+
let load;
|
|
381
|
+
assertReplacement(false);
|
|
376
382
|
const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native-load' AND resource_key = ?", [operationId]);
|
|
377
383
|
if (row)
|
|
378
384
|
load = JSON.parse(row.metadata_json);
|
|
@@ -384,12 +390,8 @@ async function restartScopeNative(context, intent, source, assertCurrent) {
|
|
|
384
390
|
}
|
|
385
391
|
if (load.scope_id !== intent.scope_id || load.generation !== intent.generation || load.request_id !== intent.request_id || load.source_process_id !== source.process_id || load.process_id !== replacement.id || load.daemon_id !== daemon.daemon_id || load.session_id !== session.session_id)
|
|
386
392
|
throw new AppError("runtime_scope_owner_unknown", "Retained Session load belongs to another physical owner", 1);
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
catch (error) {
|
|
390
|
-
db.exec("ROLLBACK");
|
|
391
|
-
throw error;
|
|
392
|
-
}
|
|
393
|
+
return load;
|
|
394
|
+
});
|
|
393
395
|
if (!load.receipt) {
|
|
394
396
|
if (dispatchLoad) {
|
|
395
397
|
assertReplacement();
|
|
@@ -417,10 +419,11 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
|
|
|
417
419
|
const record = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]);
|
|
418
420
|
const frozen = intent.process_bindings.find((binding) => binding.process_id === source.process_id);
|
|
419
421
|
const state = () => JSON.parse(fs.readFileSync(path.join(source.state_dir, "daemon.json"), "utf8"));
|
|
420
|
-
const assertOwner = () => {
|
|
422
|
+
const assertOwner = (physical = true) => {
|
|
421
423
|
assertCurrent();
|
|
422
|
-
const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]), daemon = state();
|
|
423
|
-
if (!current || current.run_id || !["judge", "probe"].includes(source.role) || JSON.stringify(managedProcessBinding(current, resourceHome(context))) !== JSON.stringify(frozen)
|
|
424
|
+
const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]), daemon = physical ? state() : null;
|
|
425
|
+
if (!current || current.run_id || !["judge", "probe"].includes(source.role) || JSON.stringify(managedProcessBinding(current, resourceHome(context))) !== JSON.stringify(frozen)
|
|
426
|
+
|| physical && (daemon.daemon_id !== source.daemon_id || daemon.resource_process?.id !== current.id || daemon.resource_process?.lease_token !== current.lease_token || JSON.stringify(daemon.config?.runtime_owner) !== JSON.stringify(source.runtime_owner)))
|
|
424
427
|
throw new AppError("runtime_scope_owner_unknown", "Native retirement lost its frozen physical owner", 1);
|
|
425
428
|
};
|
|
426
429
|
assertOwner();
|
|
@@ -437,28 +440,24 @@ async function retireScopeNative(context, intent, source, assertCurrent) {
|
|
|
437
440
|
throw new AppError("runtime_scope_native_launch_required", "Native recovery requires the original owned launch contract", 1);
|
|
438
441
|
if (!launch.env || Object.keys(launch.env).some(name => !nativeLaunchEnvironmentKeys.includes(name)) || nativeLaunchEnvironmentKeys.some(name => launch.env[name] !== null && typeof launch.env[name] !== "string") || launch.args.some(arg => typeof arg !== "string"))
|
|
439
442
|
throw new AppError("runtime_scope_native_launch_required", "Native recovery requires explicit configuration presence before retiring its owner", 1);
|
|
440
|
-
let operation;
|
|
441
443
|
let dispatch = false;
|
|
442
|
-
db.
|
|
443
|
-
|
|
444
|
-
|
|
444
|
+
const preparedInventory = db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]) ? null : nativeInventoryVersion(source.state_dir);
|
|
445
|
+
const operation = db.writeTransaction(() => {
|
|
446
|
+
let operation;
|
|
447
|
+
assertOwner(false);
|
|
445
448
|
const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]);
|
|
446
449
|
if (saved)
|
|
447
450
|
operation = JSON.parse(saved.metadata_json);
|
|
448
451
|
else {
|
|
449
|
-
if (
|
|
452
|
+
if (preparedInventory !== source.inventory_version)
|
|
450
453
|
throw new AppError("runtime_scope_inventory_changed", "Native owner changed before retirement", 1);
|
|
451
454
|
operation = { scope_id: intent.scope_id, generation: intent.generation, request_id: intent.request_id, process_id: source.process_id, daemon_id: source.daemon_id, operation_id: `scope-retire:${key}`, status: "retiring" };
|
|
452
455
|
const now = context.now();
|
|
453
456
|
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-native',?,?,?,?,NULL,?,?,?)", [key, intent.scope_id, key, now, JSON.stringify(operation), now, now]);
|
|
454
457
|
dispatch = true;
|
|
455
458
|
}
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
catch (error) {
|
|
459
|
-
db.exec("ROLLBACK");
|
|
460
|
-
throw error;
|
|
461
|
-
}
|
|
459
|
+
return operation;
|
|
460
|
+
});
|
|
462
461
|
if (!operation.receipt) {
|
|
463
462
|
if (dispatch) {
|
|
464
463
|
assertOwner();
|
|
@@ -539,27 +538,40 @@ export function prepareRuntimeScopeResumeRequest(context, input) {
|
|
|
539
538
|
}
|
|
540
539
|
}
|
|
541
540
|
export function recordRuntimeScopeResume(context, input) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
541
|
+
prepareRuntimeScopeResumeInput(input);
|
|
542
|
+
let db = getResourceDatabase(resourceHome(context), "read_existing");
|
|
543
|
+
const key = hash(JSON.stringify([input.scopeId, input.requestId]));
|
|
544
|
+
const authority = () => {
|
|
545
|
+
const status = runtimeBudgetStatus(context, input.scopeId);
|
|
546
|
+
return {
|
|
547
|
+
records: db.all("SELECT resource_kind, resource_key, metadata_json FROM managed_resources WHERE (resource_kind = 'scope-control' AND resource_key = ?) OR (resource_kind = 'scope-resume' AND resource_key = ?) OR (resource_kind = 'scope-control-drain' AND resource_key = ?) OR (resource_kind = 'scope-control-capture' AND resource_key = ?) ORDER BY resource_kind, resource_key", [input.scopeId, input.scopeId, hash(JSON.stringify([input.scopeId, input.generation])), input.captureKey]),
|
|
548
|
+
fence: status.fence, manifest: status.manifest, turns: status.provider_turns,
|
|
549
|
+
processes: status.processes.map(record => managedProcessBinding(record, resourceHome(context)))
|
|
550
|
+
};
|
|
551
|
+
};
|
|
552
|
+
let before, prepared;
|
|
547
553
|
try {
|
|
548
|
-
|
|
549
|
-
|
|
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;
|
|
554
|
+
before = authority();
|
|
555
|
+
prepared = prepareRuntimeScopeResumeRequest(context, input);
|
|
560
556
|
}
|
|
561
|
-
|
|
562
|
-
db.
|
|
563
|
-
throw error;
|
|
557
|
+
finally {
|
|
558
|
+
db.close?.();
|
|
564
559
|
}
|
|
560
|
+
if (prepared.reused)
|
|
561
|
+
return prepared;
|
|
562
|
+
db = getResourceDatabase(resourceHome(context));
|
|
563
|
+
return db.writeTransaction(() => {
|
|
564
|
+
// This branch is SQL-only: an existing request bypasses evidence reads.
|
|
565
|
+
if (db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'scope-resume-request' AND resource_key = ?", [key]))
|
|
566
|
+
return prepareRuntimeScopeResumeRequest(context, input);
|
|
567
|
+
if (!isDeepStrictEqual(authority(), before))
|
|
568
|
+
throw new AppError("runtime_scope_inventory_changed", "Resume authority changed while evidence was prepared", 1);
|
|
569
|
+
const receipt = { ...prepared };
|
|
570
|
+
delete receipt.current;
|
|
571
|
+
delete receipt.reused;
|
|
572
|
+
const payload = JSON.stringify(receipt), now = context.now();
|
|
573
|
+
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]);
|
|
574
|
+
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]);
|
|
575
|
+
return prepared;
|
|
576
|
+
});
|
|
565
577
|
}
|
|
@@ -53,45 +53,49 @@ export async function reconcileRuntimeScopeControl(context, scopeId, generation,
|
|
|
53
53
|
}
|
|
54
54
|
export async function launchRuntimeScopeControl(context, scopeId, requestId, mode, manifest, reconcileId, resumeId, prepared) {
|
|
55
55
|
const control = requestRuntimeScopeControl(context, scopeId, requestId, mode, manifest, prepared), db = getResourceDatabase(resourceHome(context)), key = keyFor(scopeId, control.generation);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
56
|
+
const launcher = birth();
|
|
57
|
+
const publication = (() => {
|
|
58
|
+
for (let attempt = 0;; attempt++) {
|
|
59
|
+
const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ?", [key]);
|
|
60
|
+
const existing = prior ? JSON.parse(prior.metadata_json) : null;
|
|
61
|
+
if (existing) {
|
|
62
|
+
const identity = existing.owner ?? existing.launcher;
|
|
63
|
+
if (!identity || !Number.isSafeInteger(identity.pid) || identity.pid < 1 || typeof identity.pid_started_at !== "string" || !identity.pid_started_at || !["starting", "running", "completed", "failed", "superseded"].includes(existing.status))
|
|
64
|
+
throw new AppError("runtime_scope_owner_unknown", "Retained scope owner has no verifiable birth identity", 1);
|
|
65
|
+
}
|
|
66
|
+
const existingAlive = !!existing && (existing.owner ? processIsAlive(existing.owner) : existing.status !== "failed" && processIsAlive(existing.launcher));
|
|
67
|
+
try {
|
|
68
|
+
return db.writeTransaction(() => {
|
|
69
|
+
const current = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
|
|
70
|
+
if (db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ?", [key])?.metadata_json !== prior?.metadata_json)
|
|
71
|
+
throw new AppError("runtime_scope_owner_changed", "Scope owner changed while its physical identity was checked", 1);
|
|
72
|
+
if (!current || JSON.parse(current.metadata_json).generation !== control.generation || db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]))
|
|
73
|
+
return { current: false, existing };
|
|
74
|
+
if (existing && existingAlive)
|
|
75
|
+
return { current: true, existing };
|
|
76
|
+
if (existing?.status === "completed" && !reconcileId && !resumeId)
|
|
77
|
+
return { current: true, existing };
|
|
78
|
+
const renewal = resumeId ? `resume:${resumeId}` : reconcileId;
|
|
79
|
+
const snapshot = renewal ? renewRecoveryObservation(existing?.snapshot ?? {}, renewal) : existing?.snapshot;
|
|
80
|
+
if (existing && recoveryObservationBudget(snapshot).state().remaining_ms === 0)
|
|
81
|
+
return { current: true, existing };
|
|
82
|
+
const cleanupRequest = resumeId ? undefined : reconcileId ?? existing?.reconcile_id;
|
|
83
|
+
const worker = { scope_id: scopeId, generation: control.generation, request_id: requestId, mode, token: crypto.randomUUID(), status: "starting", launcher, ...(snapshot ? { snapshot } : {}), ...(cleanupRequest ? { reconcile_id: cleanupRequest } : {}) };
|
|
84
|
+
const now = context.now();
|
|
85
|
+
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-control-worker',?,?,?,?,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", [key, scopeId, worker.token, now, JSON.stringify(worker), now, now]);
|
|
86
|
+
return { worker };
|
|
87
|
+
}, "runtimeScope.workerReserve");
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// No worker has spawned yet. Recheck physical identity after a racing update.
|
|
91
|
+
if (!(error instanceof AppError) || error.code !== "runtime_scope_owner_changed" || attempt >= 3)
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
84
94
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
db.exec("COMMIT");
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
db.exec("ROLLBACK");
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
95
|
+
})();
|
|
96
|
+
if (!publication.worker)
|
|
97
|
+
return { scope_id: scopeId, control, current: publication.current, settled: false, worker: publication.existing ? publicRuntimeScopeWorker(publication.existing) : null };
|
|
98
|
+
const worker = publication.worker;
|
|
95
99
|
try {
|
|
96
100
|
const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
97
101
|
if (!fs.existsSync(entry))
|
|
@@ -145,19 +149,14 @@ function requireStartingScopeWorker(db, key, token) {
|
|
|
145
149
|
export async function serveRuntimeScopeControl(context, scopeId, generation) {
|
|
146
150
|
const { key, token } = prepareRuntimeScopeServing(context, scopeId, generation);
|
|
147
151
|
const db = getResourceDatabase(resourceHome(context));
|
|
148
|
-
|
|
149
|
-
db.
|
|
150
|
-
|
|
151
|
-
worker =
|
|
152
|
-
worker.owner = birth();
|
|
152
|
+
const owner = birth();
|
|
153
|
+
const worker = db.writeTransaction(() => {
|
|
154
|
+
const worker = requireStartingScopeWorker(db, key, token);
|
|
155
|
+
worker.owner = owner;
|
|
153
156
|
worker.status = "running";
|
|
154
157
|
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]);
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
db.exec("ROLLBACK");
|
|
159
|
-
throw error;
|
|
160
|
-
}
|
|
158
|
+
return worker;
|
|
159
|
+
});
|
|
161
160
|
let heartbeat;
|
|
162
161
|
// A fence must never be held indefinitely by an observer that cannot obtain
|
|
163
162
|
// fresh provider evidence. A later operator request continues the retained
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Cleanup is secondary to the arbiter's known commit outcome. Always visit
|
|
2
|
+
* every connection, including after a failed rollback or close. */
|
|
3
|
+
export function cleanupScopeWriters(targets) {
|
|
4
|
+
const errors = [];
|
|
5
|
+
for (const target of targets) {
|
|
6
|
+
let failed = false;
|
|
7
|
+
if (target.rollback) {
|
|
8
|
+
try {
|
|
9
|
+
target.db.exec("ROLLBACK");
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
failed = true;
|
|
13
|
+
errors.push({ target: target.name, phase: "rollback", message: String(error) });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (target.close === "always" || failed) {
|
|
17
|
+
try {
|
|
18
|
+
target.db.close?.();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
errors.push({ target: target.name, phase: "close", message: String(error) });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return errors;
|
|
26
|
+
}
|
|
@@ -3,6 +3,8 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { prepareFileBytes, prepareJsonText, prepareOutputFile } from "../cli/input-preparation.js";
|
|
6
|
+
/** One precedence rule for coordinator and delegated Work packets. */
|
|
7
|
+
export const acceptedDecisionPrecedence = "Read the accepted RUN decisions before applying older general project rules. An accepted, explicitly scoped user decision supersedes a conflicting general rule only within that scope; preserve the rule elsewhere. Do not ask the user again about a decision already settled in those artifacts or invent a broader exception.";
|
|
6
8
|
export function loadExternalStageContext(input) {
|
|
7
9
|
const { file: sourcePath, bytes } = prepareFileBytes(input.file, "Stage context");
|
|
8
10
|
const materializedSha256 = sha256(bytes);
|
|
@@ -81,7 +83,10 @@ export function renderExternalStageContext(input) {
|
|
|
81
83
|
"</project_context>", "",
|
|
82
84
|
"<accepted_decisions>",
|
|
83
85
|
...(decisions.length ? decisions.map((item) => `- ${typeof item === "string" ? item : `${item.id ? `${item.id}: ` : ""}${item.statement ?? ""}`}`) : ["- No additional accepted decisions."]),
|
|
84
|
-
"</accepted_decisions>"
|
|
86
|
+
"</accepted_decisions>", "",
|
|
87
|
+
"<source_precedence>",
|
|
88
|
+
acceptedDecisionPrecedence,
|
|
89
|
+
"</source_precedence>"
|
|
85
90
|
].join("\n");
|
|
86
91
|
}
|
|
87
92
|
function validateContext(value, expectedStage) {
|
|
@@ -172,7 +172,7 @@ export function startStage(context, input, prepared = prepareLegacyStageStart(co
|
|
|
172
172
|
continuation_policy: "go_router",
|
|
173
173
|
session_kind: "orchestrator",
|
|
174
174
|
current_stage: input.stage,
|
|
175
|
-
cwd:
|
|
175
|
+
cwd: identity?.cwd ?? null
|
|
176
176
|
})
|
|
177
177
|
});
|
|
178
178
|
}
|