@deksden-com/dd-flow-cli 0.6.0 → 0.8.0-beta.135
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 +636 -0
- package/README.md +13 -1
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +108 -18
- package/dist/cli/run-cli.js +626 -49
- package/dist/domain/flow-contract.js +11 -0
- package/dist/domain/stage-catalog.js +22 -0
- package/dist/runtime/context.js +8 -2
- package/dist/schemas/code-review-decision.schema.json +26 -0
- package/dist/schemas/code-review-result.schema.json +14 -0
- package/dist/schemas/code-stage-report.schema.json +7 -2
- package/dist/schemas/code-verification.schema.json +14 -0
- package/dist/schemas/code-work-batch.schema.json +24 -0
- package/dist/schemas/code-work-result.schema.json +16 -0
- package/dist/schemas/engine-manifest.schema.json +22 -0
- package/dist/schemas/flow-contract.schema.json +6 -3
- package/dist/schemas/flow-run.schema.json +16 -122
- package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
- package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
- package/dist/schemas/plan-aspect-map.schema.json +22 -0
- package/dist/schemas/plan-review-decision.schema.json +14 -0
- package/dist/schemas/plan-review-result.schema.json +42 -0
- package/dist/schemas/protocol-plan.schema.json +15 -182
- package/dist/schemas/run-engine-binding.schema.json +37 -0
- package/dist/schemas/stage-finish-input.schema.json +16 -2
- package/dist/schemas/stage-prompt.schema.json +4 -4
- package/dist/schemas/stage-report.schema.json +8 -7
- package/dist/schemas/vnext-protocol-plan.schema.json +37 -0
- package/dist/schemas/vnext-protocolize-result.schema.json +29 -0
- package/dist/schemas/vnext-specify.schema.json +45 -0
- package/dist/services/branch-context.js +1 -1
- package/dist/services/canon.js +15 -1
- package/dist/services/cleanup.js +8 -8
- package/dist/services/cli-operation-classifier.js +60 -8
- package/dist/services/code-checks.js +244 -0
- package/dist/services/compatibility-preflight.js +1 -1
- package/dist/services/config.js +7 -1
- package/dist/services/dashboard.js +14 -14
- package/dist/services/engines.js +408 -30
- package/dist/services/eval-snapshots.js +404 -0
- package/dist/services/hooks.js +775 -23
- package/dist/services/ids.js +16 -6
- package/dist/services/lanes.js +1 -5
- package/dist/services/merge-queue.js +53 -5
- package/dist/services/merge-worker.js +5 -6
- package/dist/services/migrations.js +307 -44
- package/dist/services/plan-runtime.js +5 -5
- package/dist/services/plans.js +5 -3
- package/dist/services/projects.js +4 -4
- package/dist/services/prompts.js +1 -1
- package/dist/services/protocols.js +31 -10
- package/dist/services/run-engine-bindings.js +157 -0
- package/dist/services/run-projection.js +49 -13
- package/dist/services/runs.js +525 -58
- package/dist/services/schema-validation.js +116 -2
- package/dist/services/sessions.js +51 -12
- package/dist/services/stage-blocker.js +57 -0
- package/dist/services/stage-context.js +90 -0
- package/dist/services/stage-lifecycle.js +288 -77
- package/dist/services/stage-pause.js +175 -0
- package/dist/services/stage-report-renderer.js +65 -0
- package/dist/services/status.js +8 -3
- package/dist/services/usage.js +526 -18
- package/dist/services/vnext-code-review.js +308 -0
- package/dist/services/vnext-code.js +616 -0
- package/dist/services/vnext-contracts.js +1 -0
- package/dist/services/vnext-execution-profile.js +27 -0
- package/dist/services/vnext-fanout.js +79 -0
- package/dist/services/vnext-plan-review.js +499 -0
- package/dist/services/vnext-plan.js +576 -0
- package/dist/services/vnext-protocolize.js +542 -0
- package/dist/services/vnext-specify.js +595 -0
- package/dist/services/vnext-workspace-policy.js +87 -0
- package/dist/services/work-registry.js +499 -0
- package/dist/services/worktrees.js +58 -37
- package/dist/storage/database.js +292 -42
- package/dist/storage/paths.js +47 -1
- package/package.json +12 -12
package/dist/services/ids.js
CHANGED
|
@@ -4,8 +4,9 @@ import { formatFullId, parseFullEntityId } from "../domain/entity-ids.js";
|
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
6
6
|
const kindConfig = {
|
|
7
|
-
protocol: { type: "PRT", table: "protocols", fileRoot: ".memory-bank/protocol" },
|
|
8
|
-
run: { type: "RUN", table: "
|
|
7
|
+
protocol: { type: "PRT", table: "protocols", idField: "id", fileRoot: ".memory-bank/protocol" },
|
|
8
|
+
run: { type: "RUN", table: "runs", idField: "id" },
|
|
9
|
+
work: { type: "WRK", table: "works", idField: "work_id" }
|
|
9
10
|
};
|
|
10
11
|
export function previewNextEntityId(context, input) {
|
|
11
12
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -14,7 +15,7 @@ export function previewNextEntityId(context, input) {
|
|
|
14
15
|
const config = kindConfig[kind];
|
|
15
16
|
const used = new Set();
|
|
16
17
|
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
|
|
17
|
-
for (const id of databaseIds(context, config.table, config.type, project?.id)) {
|
|
18
|
+
for (const id of databaseIds(context, config.table, config.idField, config.type, project?.id)) {
|
|
18
19
|
addSequence(used, id, config.type);
|
|
19
20
|
}
|
|
20
21
|
if (config.fileRoot) {
|
|
@@ -39,19 +40,28 @@ export function previewNextEntityId(context, input) {
|
|
|
39
40
|
}
|
|
40
41
|
};
|
|
41
42
|
}
|
|
43
|
+
/** Work ids are globally allocated because work_id is the database primary key. */
|
|
44
|
+
export function nextWorkId(context, _projectId, slug) {
|
|
45
|
+
const used = new Set();
|
|
46
|
+
for (const row of context.db.all("SELECT work_id AS id FROM works WHERE work_id LIKE ?", ["WRK-%"]))
|
|
47
|
+
addSequence(used, row.id, "WRK");
|
|
48
|
+
return formatFullId("WRK", Math.max(0, ...used) + 1, normalizeSlug(slug));
|
|
49
|
+
}
|
|
42
50
|
function parseEntityKind(value) {
|
|
43
51
|
const normalized = value.toLowerCase();
|
|
44
52
|
if (normalized === "protocol" || normalized === "prt")
|
|
45
53
|
return "protocol";
|
|
46
54
|
if (normalized === "run")
|
|
47
55
|
return "run";
|
|
48
|
-
|
|
56
|
+
if (normalized === "work" || normalized === "wrk")
|
|
57
|
+
return "work";
|
|
58
|
+
throw new AppError("validation", "--type must be protocol, run, or work", 2, { type: value });
|
|
49
59
|
}
|
|
50
|
-
function databaseIds(context, table, type, projectId) {
|
|
60
|
+
function databaseIds(context, table, idField, type, projectId) {
|
|
51
61
|
if (!projectId)
|
|
52
62
|
return [];
|
|
53
63
|
return context.db
|
|
54
|
-
.all(`SELECT id FROM ${table} WHERE project_id = ? AND
|
|
64
|
+
.all(`SELECT ${idField} AS id FROM ${table} WHERE project_id = ? AND ${idField} LIKE ?`, [projectId, `${type}-%`])
|
|
55
65
|
.map((row) => row.id);
|
|
56
66
|
}
|
|
57
67
|
function filesystemIds(projectRoot, relativeRoot, type) {
|
package/dist/services/lanes.js
CHANGED
|
@@ -10,8 +10,6 @@ const defaultTtlSeconds = 300;
|
|
|
10
10
|
const defaultPollIntervalSeconds = 10;
|
|
11
11
|
export function getLaneStatus(context, input) {
|
|
12
12
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
13
|
-
expireStaleLocks(context, project.id);
|
|
14
|
-
expireStaleWaiters(context, project.id);
|
|
15
13
|
return {
|
|
16
14
|
ok: true,
|
|
17
15
|
lanes: lanesForProject(context, project.id, input.lane),
|
|
@@ -179,8 +177,6 @@ export async function waitForLaneLock(context, input) {
|
|
|
179
177
|
}
|
|
180
178
|
export function getLaneWaiters(context, input) {
|
|
181
179
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
182
|
-
expireStaleLocks(context, project.id);
|
|
183
|
-
expireStaleWaiters(context, project.id);
|
|
184
180
|
return {
|
|
185
181
|
ok: true,
|
|
186
182
|
lane: input.lane ? normalizeLane(input.lane) : null,
|
|
@@ -463,7 +459,7 @@ function assertWorkerMayAcquireLaneLock(context, projectId, lane, workerId) {
|
|
|
463
459
|
if (lane !== "merge") {
|
|
464
460
|
return;
|
|
465
461
|
}
|
|
466
|
-
const latest = context.db.get(`SELECT status, stop_reason, flow_kind FROM
|
|
462
|
+
const latest = context.db.get(`SELECT status, stop_reason, flow_kind FROM sessions
|
|
467
463
|
WHERE project_id = ? AND worker_id = ? AND flow_kind IN ('merge_worker', 'merge_job')
|
|
468
464
|
ORDER BY updated_at DESC, rowid DESC LIMIT 1`, [projectId, workerId]);
|
|
469
465
|
if (latest && ["stopped", "stopping"].includes(latest.status)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { flowContractForState } from "../domain/flow-contract.js";
|
|
2
2
|
import { AppError } from "../shared/errors.js";
|
|
3
3
|
import { appendAudit } from "./audit.js";
|
|
4
4
|
import { requireProjectByRoot } from "./projects.js";
|
|
@@ -73,6 +73,54 @@ export function claimNextMergeJob(context, input) {
|
|
|
73
73
|
claimed: Boolean(claimedProtocolId)
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
+
export function claimMergeJob(context, input) {
|
|
77
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
78
|
+
const stopState = stoppedMergeWorkerState(context, project.id, input.workerId);
|
|
79
|
+
if (stopState.stopped) {
|
|
80
|
+
throw new AppError("merge_worker_stopped", "Stopped or stopping merge worker cannot claim another merge job", 1, {
|
|
81
|
+
worker_id: input.workerId,
|
|
82
|
+
status: stopState.status,
|
|
83
|
+
reason: stopState.reason
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
requireLaneLockOwner(context, {
|
|
87
|
+
projectRoot: project.root,
|
|
88
|
+
lane: "merge",
|
|
89
|
+
workerId: input.workerId,
|
|
90
|
+
workspacePath: input.workspacePath
|
|
91
|
+
});
|
|
92
|
+
const now = context.now();
|
|
93
|
+
context.db.exec("BEGIN IMMEDIATE");
|
|
94
|
+
try {
|
|
95
|
+
const job = queueJobByProtocol(context, project.id, input.protocolId);
|
|
96
|
+
if (!job || !["ready", "requeued"].includes(job.status)) {
|
|
97
|
+
throw new AppError("merge_job_not_claimable", "Requested merge job is not ready to claim", 1, {
|
|
98
|
+
protocol_id: input.protocolId,
|
|
99
|
+
status: job?.status ?? "missing",
|
|
100
|
+
allowed: ["ready", "requeued"]
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const update = context.db.run(`UPDATE merge_queue
|
|
104
|
+
SET status = 'claimed', claimed_by_session_id = ?, claimed_at = ?, updated_at = ?
|
|
105
|
+
WHERE id = ? AND status IN ('ready', 'requeued')`, [input.workerId, now, now, job.id]);
|
|
106
|
+
if (update.changes !== 1) {
|
|
107
|
+
throw new AppError("merge_job_claim_race", "Requested merge job changed while claiming", 1, { protocol_id: input.protocolId });
|
|
108
|
+
}
|
|
109
|
+
appendAudit(context, {
|
|
110
|
+
protocolId: job.protocol_id,
|
|
111
|
+
projectId: project.id,
|
|
112
|
+
eventType: "merge_queue.claimed",
|
|
113
|
+
payload: { protocol_id: job.protocol_id, worker_id: input.workerId, targeted: true }
|
|
114
|
+
});
|
|
115
|
+
transitionClaimedProtocolToIntegration(context, project.id, job.protocol_id, input.workerId, now);
|
|
116
|
+
context.db.exec("COMMIT");
|
|
117
|
+
return mergeQueueResult(queueJobByProtocol(context, project.id, job.protocol_id), { ok: true, outcome: "claimed", claimed: true, targeted: true });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
context.db.exec("ROLLBACK");
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
76
124
|
export function claimMergeBundle(context, input) {
|
|
77
125
|
const { project, protocolIds } = requireClaimableBranchBundle(context, {
|
|
78
126
|
projectRoot: input.projectRoot,
|
|
@@ -292,7 +340,7 @@ export function completeMergeJob(context, input) {
|
|
|
292
340
|
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
293
341
|
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
294
342
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
295
|
-
const flowContract =
|
|
343
|
+
const flowContract = flowContractForState(state);
|
|
296
344
|
const targetStage = flowContract.merge_queue.complete.target_stage;
|
|
297
345
|
const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
|
|
298
346
|
persistProtocolState(context, protocol, {
|
|
@@ -342,7 +390,7 @@ export function completeMergeBundle(context, input) {
|
|
|
342
390
|
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
343
391
|
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
344
392
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
345
|
-
const flowContract =
|
|
393
|
+
const flowContract = flowContractForState(state);
|
|
346
394
|
const targetStage = flowContract.merge_queue.complete.target_stage;
|
|
347
395
|
const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
|
|
348
396
|
persistProtocolState(context, protocol, {
|
|
@@ -643,7 +691,7 @@ function requireClaimedJob(context, projectId, protocolId, sessionId) {
|
|
|
643
691
|
function transitionClaimedProtocolToIntegration(context, projectId, protocolId, workerId, now) {
|
|
644
692
|
const protocol = requireProtocol(context, protocolId, projectId);
|
|
645
693
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
646
|
-
const flowContract =
|
|
694
|
+
const flowContract = flowContractForState(state);
|
|
647
695
|
if (!["ready_for_merge", "queued_for_merge"].includes(state.stage)) {
|
|
648
696
|
throw new AppError("merge_protocol_not_ready", "Cannot claim a protocol whose runtime state is not ready for merge", 1, {
|
|
649
697
|
protocol_id: protocolId,
|
|
@@ -685,7 +733,7 @@ function releaseLaneLockIfOwned(context, input) {
|
|
|
685
733
|
}
|
|
686
734
|
}
|
|
687
735
|
function stopAfterCurrentIfRequested(context, projectId, projectRoot, workerId, reason) {
|
|
688
|
-
const requested = context.db.get(`SELECT session_id FROM
|
|
736
|
+
const requested = context.db.get(`SELECT session_id FROM sessions
|
|
689
737
|
WHERE project_id = ? AND worker_id = ? AND flow_kind = 'merge_worker'
|
|
690
738
|
AND status = 'stopping' AND current_stage = 'stop_after_current'
|
|
691
739
|
ORDER BY updated_at DESC, rowid DESC LIMIT 1`, [projectId, workerId]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AppError } from "../shared/errors.js";
|
|
2
2
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
3
3
|
import { appendAudit } from "./audit.js";
|
|
4
|
-
import { ensureLaneWorkspace, acquireLaneLock, releaseLaneLock
|
|
4
|
+
import { ensureLaneWorkspace, acquireLaneLock, releaseLaneLock } from "./lanes.js";
|
|
5
5
|
import { claimNextMergeJob, queueForProject } from "./merge-queue.js";
|
|
6
6
|
import { requireProjectByRoot } from "./projects.js";
|
|
7
7
|
import { buildStaticFlowGuidance } from "./flow-guidance.js";
|
|
@@ -72,7 +72,7 @@ export function stopProjectMergeWorker(context, input) {
|
|
|
72
72
|
if (claimed.length > 0) {
|
|
73
73
|
const now = context.now();
|
|
74
74
|
for (const session of targetSessions) {
|
|
75
|
-
context.db.run(`UPDATE
|
|
75
|
+
context.db.run(`UPDATE sessions
|
|
76
76
|
SET status = 'stopping', stop_reason = ?, current_stage = ?, next_action = ?, updated_at = ?
|
|
77
77
|
WHERE project_id = ? AND session_id = ?`, [input.reason, "stop_after_current", "finish_current_merge_job_then_stop", now, project.id, session.session_id]);
|
|
78
78
|
}
|
|
@@ -175,15 +175,14 @@ function withJobGuidance(context, job) {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
function detectMergeWorkerState(context, projectId) {
|
|
178
|
-
expireProjectLaneLocks(context, projectId);
|
|
179
178
|
const workers = activeMergeWorkerSessions(context, projectId);
|
|
180
179
|
const workerIds = [...new Set(workers.map((worker) => worker.worker_id).filter((value) => Boolean(value)))];
|
|
181
180
|
const claimed = context.db.all(`SELECT protocol_id, claimed_by_session_id, status, updated_at FROM merge_queue
|
|
182
181
|
WHERE project_id = ? AND status = 'claimed'
|
|
183
182
|
ORDER BY updated_at DESC, id DESC`, [projectId]);
|
|
184
183
|
const lock = context.db.get(`SELECT worker_id, status, expires_at, reason FROM lane_locks
|
|
185
|
-
WHERE project_id = ? AND lane = 'merge' AND status = 'active'
|
|
186
|
-
ORDER BY updated_at DESC, id DESC LIMIT 1`, [projectId]);
|
|
184
|
+
WHERE project_id = ? AND lane = 'merge' AND status = 'active' AND expires_at > ?
|
|
185
|
+
ORDER BY updated_at DESC, id DESC LIMIT 1`, [projectId, context.now()]);
|
|
187
186
|
if (workerIds.length > 1) {
|
|
188
187
|
return { state: "blocked", reason: "multiple_active_merge_workers", workers };
|
|
189
188
|
}
|
|
@@ -199,7 +198,7 @@ function detectMergeWorkerState(context, projectId) {
|
|
|
199
198
|
return { state: "clear" };
|
|
200
199
|
}
|
|
201
200
|
function activeMergeWorkerSessions(context, projectId) {
|
|
202
|
-
return context.db.all(`SELECT session_id, worker_id, status, current_stage, next_action, updated_at FROM
|
|
201
|
+
return context.db.all(`SELECT session_id, worker_id, status, current_stage, next_action, updated_at FROM sessions
|
|
203
202
|
WHERE project_id = ? AND flow_kind = 'merge_worker' AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')
|
|
204
203
|
ORDER BY updated_at DESC, rowid DESC`, [projectId]);
|
|
205
204
|
}
|