@deksden-com/dd-flow-cli 0.7.0 → 0.8.0
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 +666 -0
- package/README.md +7 -2
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +88 -10
- package/dist/cli/run-cli.js +523 -28
- package/dist/domain/stage-catalog.js +22 -0
- package/dist/domain/validation.js +1 -1
- package/dist/schemas/code-review-decision.schema.json +26 -0
- package/dist/schemas/code-review-result.schema.json +14 -0
- 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/compatibility.schema.json +32 -0
- package/dist/schemas/flow-contract.schema.json +9 -5
- package/dist/schemas/flow-run.schema.json +16 -123
- 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/stage-finish-input.schema.json +16 -2
- package/dist/schemas/stage-report.schema.json +8 -7
- package/dist/schemas/stage-start-response.schema.json +4 -2
- package/dist/schemas/status-report.schema.json +76 -0
- 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/cleanup.js +8 -8
- package/dist/services/cli-operation-classifier.js +10 -2
- package/dist/services/code-checks.js +244 -0
- package/dist/services/config.js +7 -1
- package/dist/services/dashboard.js +12 -12
- package/dist/services/engines.js +1 -1
- package/dist/services/eval-snapshots.js +404 -0
- package/dist/services/hooks.js +774 -18
- package/dist/services/ids.js +16 -6
- package/dist/services/lanes.js +1 -1
- package/dist/services/merge-queue.js +5 -5
- package/dist/services/merge-worker.js +2 -2
- package/dist/services/migrations.js +2 -2
- package/dist/services/plan-runtime.js +1 -1
- package/dist/services/projects.js +4 -4
- package/dist/services/prompts.js +17 -11
- package/dist/services/protocols.js +8 -8
- package/dist/services/run-projection.js +49 -13
- package/dist/services/runs.js +504 -51
- package/dist/services/schema-validation.js +21 -3
- 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 +198 -75
- package/dist/services/stage-pause.js +175 -0
- package/dist/services/stage-report-renderer.js +65 -0
- package/dist/services/usage.js +526 -18
- package/dist/services/vnext-code-review.js +305 -0
- package/dist/services/vnext-code.js +686 -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 +552 -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 +522 -0
- package/dist/services/worktrees.js +58 -37
- package/dist/storage/database.js +263 -34
- 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
|
@@ -459,7 +459,7 @@ function assertWorkerMayAcquireLaneLock(context, projectId, lane, workerId) {
|
|
|
459
459
|
if (lane !== "merge") {
|
|
460
460
|
return;
|
|
461
461
|
}
|
|
462
|
-
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
|
|
463
463
|
WHERE project_id = ? AND worker_id = ? AND flow_kind IN ('merge_worker', 'merge_job')
|
|
464
464
|
ORDER BY updated_at DESC, rowid DESC LIMIT 1`, [projectId, workerId]);
|
|
465
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";
|
|
@@ -340,7 +340,7 @@ export function completeMergeJob(context, input) {
|
|
|
340
340
|
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
341
341
|
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
342
342
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
343
|
-
const flowContract =
|
|
343
|
+
const flowContract = flowContractForState(state);
|
|
344
344
|
const targetStage = flowContract.merge_queue.complete.target_stage;
|
|
345
345
|
const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
|
|
346
346
|
persistProtocolState(context, protocol, {
|
|
@@ -390,7 +390,7 @@ export function completeMergeBundle(context, input) {
|
|
|
390
390
|
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
391
391
|
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
392
392
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
393
|
-
const flowContract =
|
|
393
|
+
const flowContract = flowContractForState(state);
|
|
394
394
|
const targetStage = flowContract.merge_queue.complete.target_stage;
|
|
395
395
|
const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
|
|
396
396
|
persistProtocolState(context, protocol, {
|
|
@@ -691,7 +691,7 @@ function requireClaimedJob(context, projectId, protocolId, sessionId) {
|
|
|
691
691
|
function transitionClaimedProtocolToIntegration(context, projectId, protocolId, workerId, now) {
|
|
692
692
|
const protocol = requireProtocol(context, protocolId, projectId);
|
|
693
693
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
694
|
-
const flowContract =
|
|
694
|
+
const flowContract = flowContractForState(state);
|
|
695
695
|
if (!["ready_for_merge", "queued_for_merge"].includes(state.stage)) {
|
|
696
696
|
throw new AppError("merge_protocol_not_ready", "Cannot claim a protocol whose runtime state is not ready for merge", 1, {
|
|
697
697
|
protocol_id: protocolId,
|
|
@@ -733,7 +733,7 @@ function releaseLaneLockIfOwned(context, input) {
|
|
|
733
733
|
}
|
|
734
734
|
}
|
|
735
735
|
function stopAfterCurrentIfRequested(context, projectId, projectRoot, workerId, reason) {
|
|
736
|
-
const requested = context.db.get(`SELECT session_id FROM
|
|
736
|
+
const requested = context.db.get(`SELECT session_id FROM sessions
|
|
737
737
|
WHERE project_id = ? AND worker_id = ? AND flow_kind = 'merge_worker'
|
|
738
738
|
AND status = 'stopping' AND current_stage = 'stop_after_current'
|
|
739
739
|
ORDER BY updated_at DESC, rowid DESC LIMIT 1`, [projectId, workerId]);
|
|
@@ -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
|
}
|
|
@@ -198,7 +198,7 @@ function detectMergeWorkerState(context, projectId) {
|
|
|
198
198
|
return { state: "clear" };
|
|
199
199
|
}
|
|
200
200
|
function activeMergeWorkerSessions(context, projectId) {
|
|
201
|
-
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
|
|
202
202
|
WHERE project_id = ? AND flow_kind = 'merge_worker' AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')
|
|
203
203
|
ORDER BY updated_at DESC, rowid DESC`, [projectId]);
|
|
204
204
|
}
|
|
@@ -486,12 +486,12 @@ function adjacentMigrationChain(source, target, impacts = []) {
|
|
|
486
486
|
function activeStateSummary(context, _projectId) {
|
|
487
487
|
return {
|
|
488
488
|
protocols: context.db.all("SELECT id, project_id, status, stage FROM protocols WHERE status NOT IN ('closed', 'cancelled') ORDER BY updated_at DESC"),
|
|
489
|
-
runs: context.db.all("SELECT id, project_id, status, verdict FROM
|
|
489
|
+
runs: context.db.all("SELECT id, project_id, status, verdict FROM runs WHERE status = 'running' ORDER BY updated_at DESC"),
|
|
490
490
|
merge_queue: context.db.all("SELECT protocol_id, project_id, status FROM merge_queue WHERE status IN ('ready', 'claimed', 'requeued') ORDER BY updated_at DESC"),
|
|
491
491
|
lane_locks: context.db.all("SELECT lane, project_id, worker_id, status FROM lane_locks WHERE status = 'active' ORDER BY updated_at DESC"),
|
|
492
492
|
lane_waiters: context.db.all("SELECT id, lane, project_id, worker_id, status FROM lane_waiters WHERE status = 'queued' ORDER BY queued_at ASC, id ASC"),
|
|
493
493
|
merge_sessions: context.db.all("SELECT session_id, project_id, status, current_protocol_id FROM merge_sessions WHERE status IN ('starting', 'active', 'stopping') ORDER BY updated_at DESC"),
|
|
494
|
-
sessions: context.db.all("SELECT session_id, project_id, status FROM
|
|
494
|
+
sessions: context.db.all("SELECT session_id, project_id, status FROM sessions WHERE status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping') ORDER BY updated_at DESC")
|
|
495
495
|
};
|
|
496
496
|
}
|
|
497
497
|
function activeStateBlockers(active) {
|
|
@@ -98,7 +98,7 @@ export function updatePlanProgress(context, input) {
|
|
|
98
98
|
refreshProtocolRuns(context, input.projectId, input.protocolId);
|
|
99
99
|
}
|
|
100
100
|
function refreshProtocolRuns(context, projectId, protocolId) {
|
|
101
|
-
for (const run of context.db.all("SELECT id FROM
|
|
101
|
+
for (const run of context.db.all("SELECT id FROM runs WHERE project_id = ? AND subject_type = 'protocol' AND subject_id = ?", [projectId, protocolId]))
|
|
102
102
|
refreshRunSessionProjection(context, projectId, run.id);
|
|
103
103
|
}
|
|
104
104
|
function mergeProgress(item, row, canonical) {
|
|
@@ -218,9 +218,9 @@ export function getProjectStatus(context, input) {
|
|
|
218
218
|
merge_queue: mergeQueue,
|
|
219
219
|
hook_status: hookStatusForProject(context, project.id),
|
|
220
220
|
codex_home_profiles: codexHomeProfilesForProject(context, project.id),
|
|
221
|
-
|
|
221
|
+
sessions: activeFlowSessionBindingsForProject(context, project.id),
|
|
222
222
|
codex_session_bindings: activeCodexSessionBindingsForProject(context, project.id),
|
|
223
|
-
|
|
223
|
+
hook_events: codexHookEventsForProject(context, project.id),
|
|
224
224
|
worktrees,
|
|
225
225
|
lane_status: {
|
|
226
226
|
lanes: context.db.all("SELECT * FROM lanes WHERE project_id = ? ORDER BY name ASC", [project.id]),
|
|
@@ -319,10 +319,10 @@ function projectReferenceTables() {
|
|
|
319
319
|
"codex_home_profiles",
|
|
320
320
|
"codex_session_bindings",
|
|
321
321
|
"project_config",
|
|
322
|
-
"
|
|
322
|
+
"sessions",
|
|
323
323
|
"flow_session_segments",
|
|
324
324
|
"flow_jobs",
|
|
325
|
-
"
|
|
325
|
+
"hook_events",
|
|
326
326
|
"worktree_records"
|
|
327
327
|
];
|
|
328
328
|
}
|
package/dist/services/prompts.js
CHANGED
|
@@ -69,12 +69,12 @@ export function renderWorkerPrompt(context, input) {
|
|
|
69
69
|
runId: run.id,
|
|
70
70
|
protocolId: protocol.id,
|
|
71
71
|
planItemId: item.id,
|
|
72
|
-
groupId: item.execution_context.
|
|
72
|
+
groupId: item.execution_context.planned_write_areas[0] ?? null
|
|
73
73
|
});
|
|
74
74
|
const staticInputs = profile.static_files.map((file) => readStaticInput(projectRoot, file));
|
|
75
75
|
const requiredRead = item.execution_context.required_read.map((file) => checkedReference(projectRoot, runHomePath(run), file, "required_read", true));
|
|
76
76
|
const discoveryBoundary = item.execution_context.discovery_boundary.map((file) => checkedReference(projectRoot, runHomePath(run), file, "discovery_boundary", false));
|
|
77
|
-
const
|
|
77
|
+
const plannedWriteAreas = item.execution_context.planned_write_areas.map((file) => checkedReference(projectRoot, undefined, file, "planned_write_areas", false));
|
|
78
78
|
const runHome = runHomePath(run);
|
|
79
79
|
const outputDir = path.join(runHome, stage.dir, "subagents", item.id);
|
|
80
80
|
assertWithin(runHome, outputDir, "output directory");
|
|
@@ -85,7 +85,7 @@ export function renderWorkerPrompt(context, input) {
|
|
|
85
85
|
workspaceRoot,
|
|
86
86
|
requiredRead,
|
|
87
87
|
discoveryBoundary,
|
|
88
|
-
|
|
88
|
+
plannedWriteAreas,
|
|
89
89
|
outputDir,
|
|
90
90
|
staticInputs,
|
|
91
91
|
...(genericTask ? { handoff: genericTask.handoff } : {})
|
|
@@ -107,7 +107,7 @@ export function renderWorkerPrompt(context, input) {
|
|
|
107
107
|
canon_version: readCanonVersion(projectRoot),
|
|
108
108
|
renderer_version: getCliVersionReport().cli.version,
|
|
109
109
|
static_inputs: staticInputs.map(({ path: inputPath, sha256 }) => ({ path: inputPath, sha256 })),
|
|
110
|
-
validation: { required_read: requiredRead, discovery_boundary: discoveryBoundary,
|
|
110
|
+
validation: { required_read: requiredRead, discovery_boundary: discoveryBoundary, planned_write_areas: plannedWriteAreas },
|
|
111
111
|
...(genericTask ? { task_handoff: genericTask.handoff } : {})
|
|
112
112
|
});
|
|
113
113
|
writeJson(reportPath, {
|
|
@@ -121,7 +121,7 @@ export function renderWorkerPrompt(context, input) {
|
|
|
121
121
|
stage: input.stage,
|
|
122
122
|
workspace_root: workspaceRoot,
|
|
123
123
|
output: { launch_prompt: promptPath, prompt_stack: stackPath },
|
|
124
|
-
validation: { required_read: requiredRead, discovery_boundary: discoveryBoundary,
|
|
124
|
+
validation: { required_read: requiredRead, discovery_boundary: discoveryBoundary, planned_write_areas: plannedWriteAreas },
|
|
125
125
|
...(genericTask ? { task_handoff: genericTask.handoff } : {})
|
|
126
126
|
});
|
|
127
127
|
return {
|
|
@@ -204,7 +204,7 @@ function protocolIdForRun(context, projectId, runId) {
|
|
|
204
204
|
return run.subject_id;
|
|
205
205
|
}
|
|
206
206
|
function requireRun(context, projectId, runId) {
|
|
207
|
-
const row = context.db.get("SELECT id, project_id, subject_type, subject_id, workspace_root, run_index_path, run_home_path, index_json FROM
|
|
207
|
+
const row = context.db.get("SELECT id, project_id, subject_type, subject_id, workspace_root, run_index_path, run_home_path, index_json FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [projectId, runId, runId]);
|
|
208
208
|
if (!row)
|
|
209
209
|
throw new AppError("not_found", `Run is not found: ${runId}`, 1);
|
|
210
210
|
return row;
|
|
@@ -290,9 +290,10 @@ function renderPrompt(input) {
|
|
|
290
290
|
`- Non-goals: ${(item.semantic_spine?.non_goals ?? []).join("; ") || "none"}`,
|
|
291
291
|
`- Acceptance contribution: ${item.semantic_spine?.acceptance_contribution ?? "not_applicable"}`,
|
|
292
292
|
"",
|
|
293
|
-
"##
|
|
294
|
-
`-
|
|
295
|
-
|
|
293
|
+
"## Hard Write Boundary",
|
|
294
|
+
`- All project writes must stay under: \`${input.workspaceRoot}\``,
|
|
295
|
+
"- Do not write outside this workspace, to another RUN, or into Git/worktree control data.",
|
|
296
|
+
"- Accepted requirements and non-goals remain binding; planned write areas do not.",
|
|
296
297
|
`- Checks: ${(item.execution_context?.checks ?? []).map((value) => `\`${value}\``).join(", ") || "none"}`,
|
|
297
298
|
`- Report directory: \`${input.outputDir}\``,
|
|
298
299
|
"",
|
|
@@ -307,10 +308,15 @@ function renderPrompt(input) {
|
|
|
307
308
|
""
|
|
308
309
|
]
|
|
309
310
|
: []),
|
|
310
|
-
"##
|
|
311
|
+
"## Discovery Hints",
|
|
311
312
|
...input.discoveryBoundary.map((value) => `- \`${value}\``),
|
|
312
313
|
"",
|
|
313
|
-
"
|
|
314
|
+
"## Planned Write Areas",
|
|
315
|
+
"These are SOFT coordination hints for parallel workers, not an allowlist or permission boundary.",
|
|
316
|
+
`- Areas: ${input.plannedWriteAreas.map((value) => `\`${value}\``).join(", ") || "none predicted"}`,
|
|
317
|
+
"- You may read or change any project-local file needed to complete the accepted task. Report every actual changed path.",
|
|
318
|
+
"",
|
|
319
|
+
"Read the static instructions below before acting. Read every required source first; discovery hints do not prohibit necessary additional project-local reads. Report material additional sources and changes.",
|
|
314
320
|
"",
|
|
315
321
|
...staticInputs.flatMap((entry) => [`## Static Input: ${entry.path}`, "", entry.content.trim(), ""])
|
|
316
322
|
].join("\n");
|
|
@@ -47,7 +47,7 @@ export function registerProtocol(context, input) {
|
|
|
47
47
|
JSON.stringify(state.blockers),
|
|
48
48
|
JSON.stringify(state.active_def),
|
|
49
49
|
runtimeStateJsonPath(context.ddFlowHome, project.id, protocolId),
|
|
50
|
-
planJsonPath(projectRoot, protocolId),
|
|
50
|
+
planJsonPath(workspacePath ?? projectRoot, protocolId),
|
|
51
51
|
existing?.created_at ?? now,
|
|
52
52
|
now
|
|
53
53
|
];
|
|
@@ -107,9 +107,9 @@ export function getProtocolStatus(context, input) {
|
|
|
107
107
|
worktree: context.db.get("SELECT * FROM worktree_records WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]),
|
|
108
108
|
hook_status: hookStatusForProject(context, protocol.project_id),
|
|
109
109
|
codex_home_profiles: codexHomeProfilesForProject(context, protocol.project_id),
|
|
110
|
-
|
|
110
|
+
sessions: activeFlowSessionBindingsForProject(context, protocol.project_id).filter((session) => session.protocol_id === protocol.id),
|
|
111
111
|
codex_session_bindings: activeCodexSessionBindingsForProject(context, protocol.project_id).filter((binding) => binding.protocol_id === protocol.id),
|
|
112
|
-
|
|
112
|
+
hook_events: codexHookEventsForProject(context, protocol.project_id).filter((event) => event.protocol_id === protocol.id),
|
|
113
113
|
audit: getAuditEvents(context, protocol.project_id, protocol.id)
|
|
114
114
|
};
|
|
115
115
|
}
|
|
@@ -502,7 +502,7 @@ export function cancelProtocol(context, input) {
|
|
|
502
502
|
}
|
|
503
503
|
let closedSessions = 0;
|
|
504
504
|
if (input.closeSessions) {
|
|
505
|
-
const result = context.db.run(`UPDATE
|
|
505
|
+
const result = context.db.run(`UPDATE sessions
|
|
506
506
|
SET status = 'stopped', stop_reason = ?, updated_at = ?, stopped_at = ?
|
|
507
507
|
WHERE project_id = ?
|
|
508
508
|
AND protocol_id = ?
|
|
@@ -705,7 +705,7 @@ function removeLocalFeatureBranch(projectRoot, branch, force) {
|
|
|
705
705
|
return { ok: true, skipped: false, reason: force ? "branch_force_deleted" : "branch_deleted", branch };
|
|
706
706
|
}
|
|
707
707
|
function releaseRelatedMergeLocks(context, protocol, reason) {
|
|
708
|
-
const workers = context.db.all(`SELECT DISTINCT worker_id FROM
|
|
708
|
+
const workers = context.db.all(`SELECT DISTINCT worker_id FROM sessions
|
|
709
709
|
WHERE project_id = ? AND protocol_id = ? AND worker_id IS NOT NULL`, [protocol.project_id, protocol.id]).map((row) => row.worker_id).filter((workerId) => Boolean(workerId));
|
|
710
710
|
const job = context.db.get("SELECT claimed_by_session_id FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
711
711
|
if (job?.claimed_by_session_id) {
|
|
@@ -866,7 +866,7 @@ export function persistProtocolState(context, protocol, state) {
|
|
|
866
866
|
]);
|
|
867
867
|
if (state.status === "closed" || state.stage === "closed") {
|
|
868
868
|
const now = context.now();
|
|
869
|
-
context.db.run(`UPDATE
|
|
869
|
+
context.db.run(`UPDATE sessions
|
|
870
870
|
SET status = 'stopped', stop_reason = 'protocol closed', updated_at = ?, stopped_at = ?
|
|
871
871
|
WHERE project_id = ?
|
|
872
872
|
AND protocol_id = ?
|
|
@@ -1216,14 +1216,14 @@ function readinessMissingFields(state) {
|
|
|
1216
1216
|
}
|
|
1217
1217
|
function linkedRunsForProtocol(context, protocol, limit) {
|
|
1218
1218
|
return context.db.all(`SELECT id, short_id, flow_kind, subject_type, subject_id, status, verdict, next_action, run_index_path, index_json, created_at, updated_at, completed_at
|
|
1219
|
-
FROM
|
|
1219
|
+
FROM runs
|
|
1220
1220
|
WHERE project_id = ? AND subject_type = 'protocol' AND subject_id = ?
|
|
1221
1221
|
ORDER BY updated_at DESC, id DESC
|
|
1222
1222
|
LIMIT ?`, [protocol.project_id, protocol.id, limit]);
|
|
1223
1223
|
}
|
|
1224
1224
|
function resolveLinkedRun(context, protocol, runIdOrAlias) {
|
|
1225
1225
|
const matches = context.db.all(`SELECT id, short_id, flow_kind, subject_type, subject_id, status, verdict, next_action, run_index_path, index_json, created_at, updated_at, completed_at
|
|
1226
|
-
FROM
|
|
1226
|
+
FROM runs
|
|
1227
1227
|
WHERE project_id = ? AND (id = ? OR short_id = ?)
|
|
1228
1228
|
ORDER BY updated_at DESC, id DESC`, [protocol.project_id, runIdOrAlias, runIdOrAlias]);
|
|
1229
1229
|
if (matches.length === 1)
|
|
@@ -2,16 +2,23 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { reconcileSessionCoverageRows } from "../domain/session-coverage.js";
|
|
4
4
|
export function refreshRunSessionProjection(context, projectId, runId) {
|
|
5
|
-
const run = context.db.get("SELECT id, status, project_root, subject_type, subject_id, runtime_path, run_index_path, index_json FROM
|
|
5
|
+
const run = context.db.get("SELECT id, flow_kind, status, project_root, subject_type, subject_id, runtime_path, run_index_path, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
6
6
|
if (!run)
|
|
7
7
|
return;
|
|
8
8
|
const index = JSON.parse(run.index_json);
|
|
9
|
-
const sessionRows = context.db.all(`SELECT session_id, parent_session_id, role, session_kind, worker_id, current_stage, status,
|
|
9
|
+
const sessionRows = context.db.all(`SELECT session_id, harness, provider_session_id, parent_session_id, role, session_kind, worker_id, current_stage, status,
|
|
10
10
|
created_at, updated_at, stopped_at, coverage_units_json
|
|
11
|
-
FROM
|
|
11
|
+
FROM sessions WHERE project_id = ? AND run_id = ? ORDER BY created_at, session_id`, [projectId, runId]);
|
|
12
12
|
const sessions = sessionRows.map(sessionProjection);
|
|
13
|
+
const workRows = context.db.all("SELECT work_id, parent_work_id, status, depends_on_json, launch_policy, started_at, completed_at FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]);
|
|
14
|
+
const works = workRows.map((work) => ({ work_id: work.work_id, parent_work_id: work.parent_work_id, status: work.status, depends_on: parseStringArray(work.depends_on_json), launch_policy: work.launch_policy, started_at: work.started_at, completed_at: work.completed_at }));
|
|
15
|
+
const rootWorkId = workRows.find((work) => work.parent_work_id === null)?.work_id ?? null;
|
|
13
16
|
const coverage = reconcileSessionCoverageRows(sessionRows);
|
|
14
|
-
const sessionCoverage = {
|
|
17
|
+
const sessionCoverage = run.flow_kind === "vnext_specify" && coverage.expected_unit_ids.length === 0 ? {
|
|
18
|
+
status: "not_applicable",
|
|
19
|
+
expected: [], observed: [], missing: [],
|
|
20
|
+
diagnostics: ["worker_coverage_not_required"]
|
|
21
|
+
} : {
|
|
15
22
|
status: coverage.status,
|
|
16
23
|
expected: coverage.expected_unit_ids,
|
|
17
24
|
observed: coverage.observed_unit_ids,
|
|
@@ -20,19 +27,35 @@ export function refreshRunSessionProjection(context, projectId, runId) {
|
|
|
20
27
|
};
|
|
21
28
|
const plan = run.subject_type === "protocol" ? planProjection(context, projectId, run.subject_id, run.project_root) : {};
|
|
22
29
|
const workers = workerProjection(context, projectId, runId);
|
|
30
|
+
const vnext = run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize";
|
|
31
|
+
const hadLegacyCoverage = vnext && ("session_coverage" in index || "usage_coverage" in index);
|
|
32
|
+
if (vnext) {
|
|
33
|
+
delete index.session_coverage;
|
|
34
|
+
delete index.usage_coverage;
|
|
35
|
+
delete index.workers;
|
|
36
|
+
delete index.current_stage;
|
|
37
|
+
delete index.attempts;
|
|
38
|
+
}
|
|
23
39
|
if (JSON.stringify(index.sessions ?? []) === JSON.stringify(sessions)
|
|
24
|
-
&& JSON.stringify(index.
|
|
40
|
+
&& JSON.stringify(index.works ?? []) === JSON.stringify(works)
|
|
41
|
+
&& index.root_work_id === rootWorkId
|
|
42
|
+
&& (vnext || JSON.stringify(index.session_coverage) === JSON.stringify(sessionCoverage))
|
|
25
43
|
&& JSON.stringify(index.plan_ref) === JSON.stringify(plan.plan_ref)
|
|
26
44
|
&& JSON.stringify(index.plan_progress) === JSON.stringify(plan.plan_progress)
|
|
27
|
-
&& JSON.stringify(index.workers ?? {}) === JSON.stringify(workers))
|
|
45
|
+
&& (vnext || JSON.stringify(index.workers ?? {}) === JSON.stringify(workers))
|
|
46
|
+
&& !hadLegacyCoverage)
|
|
28
47
|
return;
|
|
29
48
|
index.sessions = sessions;
|
|
30
|
-
index.
|
|
49
|
+
index.works = works;
|
|
50
|
+
index.root_work_id = rootWorkId;
|
|
51
|
+
if (!vnext)
|
|
52
|
+
index.session_coverage = sessionCoverage;
|
|
31
53
|
if (plan.plan_ref)
|
|
32
54
|
index.plan_ref = plan.plan_ref;
|
|
33
55
|
if (plan.plan_progress)
|
|
34
56
|
index.plan_progress = plan.plan_progress;
|
|
35
|
-
|
|
57
|
+
if (!vnext)
|
|
58
|
+
index.workers = workers;
|
|
36
59
|
index.updated_at = context.now();
|
|
37
60
|
const runtime = readJson(run.runtime_path);
|
|
38
61
|
const runtimeRevision = typeof runtime?.runtime_revision === "number"
|
|
@@ -41,19 +64,30 @@ export function refreshRunSessionProjection(context, projectId, runId) {
|
|
|
41
64
|
? index.runtime_revision + 1
|
|
42
65
|
: 1;
|
|
43
66
|
index.runtime_revision = runtimeRevision;
|
|
44
|
-
const authoritative = runtime ?? { ...index, schema_id: "dd-flow/flow-run@
|
|
67
|
+
const authoritative = { ...(runtime ?? {}), ...index, schema_id: String(index.schema_id ?? runtime?.schema_id ?? "dd-flow/flow-run@3") };
|
|
45
68
|
authoritative.sessions = sessions;
|
|
46
|
-
authoritative.
|
|
69
|
+
authoritative.works = works;
|
|
70
|
+
authoritative.root_work_id = rootWorkId;
|
|
71
|
+
if (vnext) {
|
|
72
|
+
delete authoritative.session_coverage;
|
|
73
|
+
delete authoritative.usage_coverage;
|
|
74
|
+
delete authoritative.workers;
|
|
75
|
+
delete authoritative.current_stage;
|
|
76
|
+
delete authoritative.attempts;
|
|
77
|
+
}
|
|
78
|
+
else
|
|
79
|
+
authoritative.session_coverage = sessionCoverage;
|
|
47
80
|
if (plan.plan_ref)
|
|
48
81
|
authoritative.plan_ref = plan.plan_ref;
|
|
49
82
|
if (plan.plan_progress)
|
|
50
83
|
authoritative.plan_progress = plan.plan_progress;
|
|
51
|
-
|
|
84
|
+
if (!vnext)
|
|
85
|
+
authoritative.workers = workers;
|
|
52
86
|
authoritative.updated_at = index.updated_at;
|
|
53
87
|
authoritative.runtime_revision = runtimeRevision;
|
|
54
88
|
if (run.status !== "discarded" || fs.existsSync(run.runtime_path))
|
|
55
89
|
writeJson(run.runtime_path, authoritative);
|
|
56
|
-
context.db.run("UPDATE
|
|
90
|
+
context.db.run("UPDATE runs SET index_json = ?, updated_at = ? WHERE project_id = ? AND id = ?", [JSON.stringify(index), index.updated_at, projectId, runId]);
|
|
57
91
|
}
|
|
58
92
|
function planProjection(context, projectId, protocolId, projectRoot) {
|
|
59
93
|
const binding = context.db.get("SELECT plan_path, plan_revision, plan_sha256 FROM plan_bindings WHERE project_id = ? AND protocol_id = ?", [projectId, protocolId]);
|
|
@@ -107,10 +141,12 @@ function parseStringArray(value) {
|
|
|
107
141
|
function sessionProjection(row) {
|
|
108
142
|
return {
|
|
109
143
|
session_id: row.session_id,
|
|
144
|
+
harness: row.harness,
|
|
145
|
+
provider_session_id: row.provider_session_id,
|
|
110
146
|
parent_session_id: row.parent_session_id,
|
|
111
147
|
role: row.role,
|
|
112
148
|
session_kind: row.session_kind,
|
|
113
|
-
|
|
149
|
+
work_id: row.worker_id,
|
|
114
150
|
current_stage: row.current_stage,
|
|
115
151
|
status: row.status,
|
|
116
152
|
created_at: row.created_at,
|