@deksden-com/dd-flow-cli 0.9.0-beta.75 → 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/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 +2 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
- 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 +2 -2
- 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 +84 -41
- 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 +195 -92
- 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
package/dist/services/lanes.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
import { spawnSync } from "node:child_process";
|
|
5
3
|
import { AppError } from "../shared/errors.js";
|
|
6
4
|
import { appendAudit } from "./audit.js";
|
|
@@ -8,6 +6,32 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
8
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
9
7
|
const defaultTtlSeconds = 300;
|
|
10
8
|
const defaultPollIntervalSeconds = 10;
|
|
9
|
+
export function prepareLaneWaitTiming(input) {
|
|
10
|
+
return {
|
|
11
|
+
timeoutSeconds: normalizeNonNegativeNumber(input.timeoutSeconds, 0, "timeout"),
|
|
12
|
+
pollIntervalSeconds: normalizePositiveNumber(input.pollIntervalSeconds, defaultPollIntervalSeconds, "poll-interval")
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function prepareLaneLock(context, input, action) {
|
|
16
|
+
if (typeof input.workerId !== "string" || !input.workerId.trim())
|
|
17
|
+
throw new AppError("validation", "Lane lock requires a nonempty worker ID", 2);
|
|
18
|
+
if (["acquire", "release", "wait-acquire"].includes(action) && (typeof input.reason !== "string" || !input.reason.trim()))
|
|
19
|
+
throw new AppError("validation", "Lane lock requires a nonempty reason", 2);
|
|
20
|
+
if (input.leaseToken !== undefined && (typeof input.leaseToken !== "string" || !input.leaseToken.trim()))
|
|
21
|
+
throw new AppError("validation", "Lease token must be nonempty text", 2);
|
|
22
|
+
const ttlSeconds = normalizePositiveNumber(input.ttlSeconds, defaultTtlSeconds, "ttl");
|
|
23
|
+
const { timeoutSeconds, pollIntervalSeconds } = prepareLaneWaitTiming(input);
|
|
24
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
25
|
+
const lane = normalizeLane(input.lane);
|
|
26
|
+
if (input.workspacePath !== undefined)
|
|
27
|
+
requireMatchingLaneWorkspace(context, project.id, lane, input.workspacePath);
|
|
28
|
+
else if (["acquire", "wait", "wait-acquire"].includes(action))
|
|
29
|
+
throw new AppError("validation", "Lane lock requires a workspace path", 2);
|
|
30
|
+
if (action === "acquire" || action === "wait-acquire")
|
|
31
|
+
assertWorkerMayAcquireLaneLock(context, project.id, lane, input.workerId);
|
|
32
|
+
const lock = action === "heartbeat" || action === "release" ? requireOwnedActiveLock(context, project.id, lane, input.workerId, input.leaseToken) : undefined;
|
|
33
|
+
return { project, lane, ttlSeconds, timeoutSeconds, pollIntervalSeconds, lock };
|
|
34
|
+
}
|
|
11
35
|
export function getLaneStatus(context, input) {
|
|
12
36
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
13
37
|
return {
|
|
@@ -23,10 +47,16 @@ export function expireProjectLaneLocks(context, projectId) {
|
|
|
23
47
|
export function expireProjectLaneWaiters(context, projectId) {
|
|
24
48
|
expireStaleWaiters(context, projectId);
|
|
25
49
|
}
|
|
26
|
-
export function
|
|
50
|
+
export function prepareLaneWorkspace(context, input) {
|
|
51
|
+
if (input.branch !== undefined && (typeof input.branch !== "string" || !input.branch.trim()))
|
|
52
|
+
throw new AppError("validation", "Expected branch must be nonempty text", 2);
|
|
27
53
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
28
54
|
const lane = normalizeLane(input.lane);
|
|
29
55
|
const workspacePath = resolveExistingPath(input.workspacePath);
|
|
56
|
+
return { project, lane, workspacePath };
|
|
57
|
+
}
|
|
58
|
+
export function setLaneWorkspace(context, input) {
|
|
59
|
+
const { project, lane, workspacePath } = prepareLaneWorkspace(context, input);
|
|
30
60
|
const now = context.now();
|
|
31
61
|
const existing = laneRecord(context, project.id, lane);
|
|
32
62
|
context.db.run(`INSERT INTO lanes
|
|
@@ -69,11 +99,7 @@ export function checkLaneWorkspace(context, input) {
|
|
|
69
99
|
};
|
|
70
100
|
}
|
|
71
101
|
export function acquireLaneLock(context, input) {
|
|
72
|
-
const project =
|
|
73
|
-
const lane = normalizeLane(input.lane);
|
|
74
|
-
requireMatchingLaneWorkspace(context, project.id, lane, input.workspacePath);
|
|
75
|
-
assertWorkerMayAcquireLaneLock(context, project.id, lane, input.workerId);
|
|
76
|
-
const ttlSeconds = normalizePositiveNumber(input.ttlSeconds, defaultTtlSeconds, "ttl");
|
|
102
|
+
const { project, lane, ttlSeconds } = prepareLaneLock(context, input, "acquire");
|
|
77
103
|
expireStaleLocks(context, project.id);
|
|
78
104
|
const active = activeLaneLock(context, project.id, lane);
|
|
79
105
|
if (active) {
|
|
@@ -116,13 +142,8 @@ export function acquireLaneLock(context, input) {
|
|
|
116
142
|
};
|
|
117
143
|
}
|
|
118
144
|
export function heartbeatLaneLock(context, input) {
|
|
119
|
-
const project =
|
|
120
|
-
const
|
|
121
|
-
if (input.workspacePath) {
|
|
122
|
-
requireMatchingLaneWorkspace(context, project.id, lane, input.workspacePath);
|
|
123
|
-
}
|
|
124
|
-
const lock = requireOwnedActiveLock(context, project.id, lane, input.workerId, input.leaseToken);
|
|
125
|
-
const ttlSeconds = normalizePositiveNumber(input.ttlSeconds, defaultTtlSeconds, "ttl");
|
|
145
|
+
const { project, lock: preparedLock, ttlSeconds } = prepareLaneLock(context, input, "heartbeat");
|
|
146
|
+
const lock = preparedLock;
|
|
126
147
|
const now = context.now();
|
|
127
148
|
context.db.run(`UPDATE lane_locks SET heartbeat_at = ?, expires_at = ?, updated_at = ? WHERE id = ?`, [now, expiresAt(now, ttlSeconds), now, lock.id]);
|
|
128
149
|
appendAudit(context, {
|
|
@@ -133,12 +154,8 @@ export function heartbeatLaneLock(context, input) {
|
|
|
133
154
|
return { ok: true, lock: activeLaneLock(context, project.id, lock.lane) };
|
|
134
155
|
}
|
|
135
156
|
export function releaseLaneLock(context, input) {
|
|
136
|
-
const project =
|
|
137
|
-
const
|
|
138
|
-
if (input.workspacePath) {
|
|
139
|
-
requireMatchingLaneWorkspace(context, project.id, lane, input.workspacePath);
|
|
140
|
-
}
|
|
141
|
-
const lock = requireOwnedActiveLock(context, project.id, lane, input.workerId, input.leaseToken);
|
|
157
|
+
const { project, lock: preparedLock } = prepareLaneLock(context, input, "release");
|
|
158
|
+
const lock = preparedLock;
|
|
142
159
|
const now = context.now();
|
|
143
160
|
context.db.run(`UPDATE lane_locks
|
|
144
161
|
SET status = 'released', released_at = ?, reason = ?, updated_at = ?
|
|
@@ -152,11 +169,7 @@ export function releaseLaneLock(context, input) {
|
|
|
152
169
|
return { ok: true, lock: laneLockById(context, lock.id) };
|
|
153
170
|
}
|
|
154
171
|
export async function waitForLaneLock(context, input) {
|
|
155
|
-
const project =
|
|
156
|
-
const lane = normalizeLane(input.lane);
|
|
157
|
-
requireMatchingLaneWorkspace(context, project.id, lane, input.workspacePath);
|
|
158
|
-
const timeoutSeconds = normalizeNonNegativeNumber(input.timeoutSeconds, 0, "timeout");
|
|
159
|
-
const pollIntervalSeconds = normalizePositiveNumber(input.pollIntervalSeconds, defaultPollIntervalSeconds, "poll-interval");
|
|
172
|
+
const { project, lane, timeoutSeconds, pollIntervalSeconds } = prepareLaneLock(context, input, "wait");
|
|
160
173
|
const start = Date.now();
|
|
161
174
|
while (true) {
|
|
162
175
|
expireStaleLocks(context, project.id);
|
|
@@ -186,13 +199,18 @@ export function getLaneWaiters(context, input) {
|
|
|
186
199
|
}))
|
|
187
200
|
};
|
|
188
201
|
}
|
|
189
|
-
export function
|
|
202
|
+
export function prepareLaneWaiterCancellation(context, input) {
|
|
203
|
+
if (typeof input.workerId !== "string" || !input.workerId.trim())
|
|
204
|
+
throw new AppError("validation", "Lane waiter requires a nonempty worker ID", 2);
|
|
205
|
+
if (typeof input.reason !== "string" || !input.reason.trim())
|
|
206
|
+
throw new AppError("validation", "lane waiter cancel requires --reason", 2);
|
|
190
207
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
191
208
|
const lane = normalizeLane(input.lane);
|
|
192
209
|
const reason = input.reason.trim();
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
210
|
+
return { project, lane, reason };
|
|
211
|
+
}
|
|
212
|
+
export function cancelLaneWaiter(context, input) {
|
|
213
|
+
const { project, lane, reason } = prepareLaneWaiterCancellation(context, input);
|
|
196
214
|
expireStaleWaiters(context, project.id);
|
|
197
215
|
const now = context.now();
|
|
198
216
|
const waiter = activeLaneWaiterForWorker(context, project.id, lane, input.workerId);
|
|
@@ -245,13 +263,7 @@ export function expireLaneWaiterById(context, input) {
|
|
|
245
263
|
return { ok: true, expired: result.changes === 1, waiter_id: input.waiterId };
|
|
246
264
|
}
|
|
247
265
|
export async function waitAcquireLaneLock(context, input) {
|
|
248
|
-
const project =
|
|
249
|
-
const lane = normalizeLane(input.lane);
|
|
250
|
-
requireMatchingLaneWorkspace(context, project.id, lane, input.workspacePath);
|
|
251
|
-
assertWorkerMayAcquireLaneLock(context, project.id, lane, input.workerId);
|
|
252
|
-
const timeoutSeconds = normalizeNonNegativeNumber(input.timeoutSeconds, 0, "timeout");
|
|
253
|
-
const pollIntervalSeconds = normalizePositiveNumber(input.pollIntervalSeconds, defaultPollIntervalSeconds, "poll-interval");
|
|
254
|
-
const ttlSeconds = normalizePositiveNumber(input.ttlSeconds, defaultTtlSeconds, "ttl");
|
|
266
|
+
const { project, lane, timeoutSeconds, pollIntervalSeconds, ttlSeconds } = prepareLaneLock(context, input, "wait-acquire");
|
|
255
267
|
const startedAt = Date.now();
|
|
256
268
|
const waiterDeadline = timeoutSeconds > 0 ? new Date(startedAt + timeoutSeconds * 1000).toISOString() : null;
|
|
257
269
|
while (true) {
|
|
@@ -375,10 +387,9 @@ function laneWaitersForProject(context, projectId, lane) {
|
|
|
375
387
|
LIMIT 100`, [projectId]);
|
|
376
388
|
}
|
|
377
389
|
function activeLaneLock(context, projectId, lane) {
|
|
378
|
-
expireStaleLocks(context, projectId);
|
|
379
390
|
return context.db.get(`SELECT * FROM lane_locks
|
|
380
|
-
WHERE project_id = ? AND lane = ? AND status = 'active'
|
|
381
|
-
ORDER BY updated_at DESC, id DESC LIMIT 1`, [projectId, lane]);
|
|
391
|
+
WHERE project_id = ? AND lane = ? AND status = 'active' AND expires_at > ?
|
|
392
|
+
ORDER BY updated_at DESC, id DESC LIMIT 1`, [projectId, lane, context.now()]);
|
|
382
393
|
}
|
|
383
394
|
function activeLaneWaiterForWorker(context, projectId, lane, workerId) {
|
|
384
395
|
return context.db.get(`SELECT * FROM lane_waiters
|
|
@@ -513,10 +524,10 @@ function ensureQueuedLaneWaiter(context, input) {
|
|
|
513
524
|
return waiter;
|
|
514
525
|
}
|
|
515
526
|
function tryAcquireQueuedLaneLock(context, input) {
|
|
516
|
-
expireStaleLocks(context, input.projectId);
|
|
517
|
-
expireStaleWaiters(context, input.projectId);
|
|
518
527
|
context.db.beginWriteTransaction();
|
|
519
528
|
try {
|
|
529
|
+
expireStaleLocks(context, input.projectId);
|
|
530
|
+
expireStaleWaiters(context, input.projectId);
|
|
520
531
|
const existingLock = activeLaneLock(context, input.projectId, input.lane);
|
|
521
532
|
if (existingLock?.worker_id === input.workerId) {
|
|
522
533
|
context.db.exec("COMMIT");
|
|
@@ -618,11 +629,7 @@ function tryAcquireQueuedLaneLock(context, input) {
|
|
|
618
629
|
}
|
|
619
630
|
}
|
|
620
631
|
function resolveExistingPath(value) {
|
|
621
|
-
|
|
622
|
-
if (!fs.existsSync(absolute)) {
|
|
623
|
-
throw new AppError("not_found", `Workspace path does not exist: ${absolute}`, 1);
|
|
624
|
-
}
|
|
625
|
-
return fs.realpathSync(absolute);
|
|
632
|
+
return resolveProjectRoot(value);
|
|
626
633
|
}
|
|
627
634
|
function detectBranch(workspacePath) {
|
|
628
635
|
const result = spawnSync("git", ["-C", workspacePath, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" });
|
|
@@ -635,7 +642,7 @@ function expiresAt(now, ttlSeconds) {
|
|
|
635
642
|
return new Date(new Date(now).getTime() + ttlSeconds * 1000).toISOString();
|
|
636
643
|
}
|
|
637
644
|
function normalizeLane(value) {
|
|
638
|
-
const normalized = value.trim().toLowerCase();
|
|
645
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
639
646
|
if (!normalized || !/^[a-z0-9._-]+$/.test(normalized)) {
|
|
640
647
|
throw new AppError("validation", "Lane name must contain only lowercase letters, numbers, dot, underscore, or hyphen", 2);
|
|
641
648
|
}
|
|
@@ -643,15 +650,16 @@ function normalizeLane(value) {
|
|
|
643
650
|
}
|
|
644
651
|
function normalizePositiveNumber(value, fallback, label) {
|
|
645
652
|
const normalized = value ?? fallback;
|
|
646
|
-
if (!Number.isFinite(normalized) || normalized <= 0
|
|
647
|
-
|
|
653
|
+
if (!Number.isFinite(normalized) || normalized <= 0 ||
|
|
654
|
+
(label === "poll-interval" ? normalized * 1000 > 2 ** 31 - 1 : !Number.isFinite(new Date(Date.now() + normalized * 1000).getTime()))) {
|
|
655
|
+
throw new AppError("validation", `--${label} must be a positive number within the supported timer/date range`, 2);
|
|
648
656
|
}
|
|
649
657
|
return normalized;
|
|
650
658
|
}
|
|
651
659
|
function normalizeNonNegativeNumber(value, fallback, label) {
|
|
652
660
|
const normalized = value ?? fallback;
|
|
653
|
-
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
654
|
-
throw new AppError("validation", `--${label} must be zero or a positive number`, 2);
|
|
661
|
+
if (!Number.isFinite(normalized) || normalized < 0 || !Number.isFinite(new Date(Date.now() + normalized * 1000).getTime())) {
|
|
662
|
+
throw new AppError("validation", `--${label} must be zero or a positive number within the supported date range`, 2);
|
|
655
663
|
}
|
|
656
664
|
return normalized;
|
|
657
665
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { parse, quote } from "shell-quote";
|
|
3
|
+
/** Native hooks and the lightweight ingress must recognize the same shell. */
|
|
4
|
+
export function lifecycleCommandFromPayload(payload) {
|
|
5
|
+
const input = payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : {};
|
|
6
|
+
const command = [payload.command, input.command, input.cmd].find(value => typeof value === "string" && value.length > 0);
|
|
7
|
+
return typeof command === "string" ? unwrapShellCommand(command) : undefined;
|
|
8
|
+
}
|
|
3
9
|
const controlOperators = new Set(["&&", "||", ";", "|", "|&", "&", "(", ")", "<(", ">("]);
|
|
4
10
|
const redirectionOperators = new Set(["<", ">", ">>", ">|", "<&", ">&", "<<<"]);
|
|
5
11
|
/** Parse the deliberately small direct-command surface accepted by lifecycle hooks. */
|
|
@@ -127,7 +133,7 @@ function invocationFromSegment(segment, heredoc, wrapped, inheritedEnv) {
|
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
const executable = segment.words[index];
|
|
130
|
-
if (!executable || path.basename(executable) !== "dd-flow")
|
|
136
|
+
if (!executable || path.basename(executable) !== "dd-flow" && executable !== "$DD_FLOW_BIN")
|
|
131
137
|
return null;
|
|
132
138
|
const argv = segment.words.slice(index + 1);
|
|
133
139
|
const operation = lifecycleOperation(argv);
|
|
@@ -200,6 +206,12 @@ export function parseCommandArgs(argv) {
|
|
|
200
206
|
for (let index = 0; index < argv.length; index += 1) {
|
|
201
207
|
const value = argv[index];
|
|
202
208
|
if (value?.startsWith("--")) {
|
|
209
|
+
const equals = value.indexOf("=");
|
|
210
|
+
if (equals > 2) {
|
|
211
|
+
const key = value.slice(2, equals);
|
|
212
|
+
options.set(key, [...(options.get(key) ?? []), value.slice(equals + 1)]);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
203
215
|
const key = value.slice(2);
|
|
204
216
|
const next = argv[index + 1];
|
|
205
217
|
const optionValue = !next || next.startsWith("--") ? "__dd_flow_flag__" : next;
|