@deksden-com/dd-flow-cli 0.3.1 → 0.4.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 +12 -0
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +14 -4
- package/dist/cli/run-cli.js +54 -2
- package/dist/services/branch-context.js +254 -0
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/merge-queue.js +213 -0
- package/dist/services/merge-worker.js +10 -3
- package/dist/services/protocols.js +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add branch/workspace protocol context for agents: `protocol branch-status`, `branch_context` in protocol and merge JSON outputs, and branch bundle status with eligible, not-ready and claimed protocol lists.
|
|
8
|
+
|
|
9
|
+
Add `merge bundle status|claim|complete|fail` commands so a merge worker can integrate one feature branch containing multiple ready protocols under the existing merge lane lock.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- cfdcf1d: Fix compatibility status reporting so selected compatible engines advertise `normal_write` in `allowed_modes`; blocked operations are now reported only when a write is actually blocked by missing or incompatible engine resolution.
|
|
14
|
+
|
|
3
15
|
## 0.3.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-07-
|
|
3
|
+
"cli_version": "0.4.0",
|
|
4
|
+
"cli_commit": "3ab5ed2a5efcd2d1aa8a13a530b94995abe2bd3b",
|
|
5
|
+
"built_at": "2026-07-05T17:44:47.134Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
|
-
"version": "2.
|
|
8
|
-
"commit": "
|
|
7
|
+
"version": "2.10.0",
|
|
8
|
+
"commit": "228a069278ead592d2bee435a33e0da90536227b",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-06",
|
|
10
10
|
"repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
|
|
11
11
|
"memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
|
package/dist/cli/help.js
CHANGED
|
@@ -24,7 +24,7 @@ Core commands:
|
|
|
24
24
|
id next Preview next typed ids for protocol or run allocation.
|
|
25
25
|
plan set/status/item Attach and update protocol plans.
|
|
26
26
|
lane workspace/lock/status Manage shared workspace lanes and leases.
|
|
27
|
-
merge status/one-shot
|
|
27
|
+
merge status/one-shot/bundle Inspect, claim, or complete merge work from the current session.
|
|
28
28
|
merge-worker status/start/stop Manage long-lived project merge workers.
|
|
29
29
|
merge-queue status/next Claim, wait for, complete, or fail merge jobs.
|
|
30
30
|
session register/status/stop Register flow sessions and stop workers.
|
|
@@ -173,6 +173,8 @@ Examples:
|
|
|
173
173
|
Usage:
|
|
174
174
|
dd-flow protocol register <handshake-id> --project-root <root> [--workspace-path <checkout>] --json
|
|
175
175
|
dd-flow protocol status <protocol-id> --json
|
|
176
|
+
dd-flow protocol branch-status <protocol-id> --json
|
|
177
|
+
dd-flow protocol branch-status --project-root <root> --path <workspace> --json
|
|
176
178
|
dd-flow protocol ready --project-root <root> --json
|
|
177
179
|
dd-flow protocol blockers <protocol-id> --project-root <root> --json
|
|
178
180
|
dd-flow protocol implement <protocol-id> --project-root <root> [--force --reason <text>] --json
|
|
@@ -181,7 +183,7 @@ Usage:
|
|
|
181
183
|
dd-flow protocol ready-for-merge <protocol-id> --json
|
|
182
184
|
dd-flow protocol cancel <protocol-id> --reason <text> [--close-sessions true|false] [--cancel-queue true|false] [--release-locks true|false] [--worktree keep|remove] [--force] --json
|
|
183
185
|
|
|
184
|
-
ready, blockers and implement read protocol markdown frontmatter plus runtime state. implement is a preflight/guidance command: it never performs implementation, refuses unresolved blocked_by_protocols unless --force --reason is supplied, refuses terminal protocols and reports the expected next prompt/stage. transition validates the snapshotted flow contract stored in protocol runtime state, writes an audit event and updates lifecycle fields from the payload. --json-file is accepted as a compatibility alias for --payload-file. sync-from-run repairs legacy protocol/run mismatch from concrete RUN evidence instead of requiring manual runtime JSON edits.
|
|
186
|
+
ready, blockers and implement read protocol markdown frontmatter plus runtime state. branch-status is read-only and reports the current protocol/workspace branch context: sibling protocols in the same branch/worktree, queue status, active sessions and merge bundle eligibility. implement is a preflight/guidance command: it never performs implementation, refuses unresolved blocked_by_protocols unless --force --reason is supplied, refuses terminal protocols and reports the expected next prompt/stage. transition validates the snapshotted flow contract stored in protocol runtime state, writes an audit event and updates lifecycle fields from the payload. --json-file is accepted as a compatibility alias for --payload-file. sync-from-run repairs legacy protocol/run mismatch from concrete RUN evidence instead of requiring manual runtime JSON edits.
|
|
185
187
|
|
|
186
188
|
The project_root is the stable project identity used for queues, sessions, lanes, and dashboards. Runtime state is stored under DD_FLOW_HOME/projects/<PRJ-ID-slug>/runtime so feature worktree removal does not break status, cancel, or merge finalization. The optional workspace_path records the concrete checkout where the agent works. With --worktree remove, cancel removes the disposable git worktree recorded in runtime state or worktree records; with --force it also force-deletes the local feature branch when present.`
|
|
187
189
|
],
|
|
@@ -299,10 +301,14 @@ Related lane commands:
|
|
|
299
301
|
`dd-flow merge - current-session merge entrypoint helpers
|
|
300
302
|
|
|
301
303
|
Usage:
|
|
302
|
-
dd-flow merge status --project-root <root> --json
|
|
304
|
+
dd-flow merge status --project-root <root> [--path <workspace>] --json
|
|
303
305
|
dd-flow merge one-shot --project-root <root> --worker-id <id> [--path <workspace>] --json
|
|
306
|
+
dd-flow merge bundle status --project-root <root> --path <workspace> --json
|
|
307
|
+
dd-flow merge bundle claim --project-root <root> --worker-id <id> --path <workspace> --json
|
|
308
|
+
dd-flow merge bundle complete --project-root <root> --worker-id <id> --path <workspace> --summary <text> --json
|
|
309
|
+
dd-flow merge bundle fail --project-root <root> --worker-id <id> --path <workspace> --reason <text> --requeue true|false --json
|
|
304
310
|
|
|
305
|
-
merge status reports active merge worker, claimed protocols, active merge lane lock and
|
|
311
|
+
merge status reports active merge worker, claimed protocols, active merge lane lock, queue state and branch_context when --path is provided. merge one-shot keeps legacy single-protocol behavior and also returns branch_context. merge bundle commands operate on all ready protocols in the current branch/worktree: status is read-only, claim requires the merge lane lock owner, complete closes all claimed protocols after branch integration, and fail requeues or fails the claimed bundle.`
|
|
306
312
|
],
|
|
307
313
|
[
|
|
308
314
|
"merge-worker",
|
|
@@ -330,6 +336,10 @@ Usage:
|
|
|
330
336
|
|
|
331
337
|
The worker must own the merge lane from the registered merge workspace before claiming, completing, or failing queued protocols. Outputs use queue_item/protocol/claim as the primary contract and retain job as a compatibility alias. note updates the final completion summary after post-complete cleanup and does not require a lane lock. --path defaults to the current working directory. Use --worker-id as the owner identity.
|
|
332
338
|
|
|
339
|
+
For branch-level integration prefer:
|
|
340
|
+
dd-flow merge bundle status --project-root "$PWD" --path "$PWD" --json
|
|
341
|
+
dd-flow merge bundle claim --project-root "$PWD" --worker-id merge-worker --path "$PWD" --json
|
|
342
|
+
|
|
333
343
|
Examples:
|
|
334
344
|
dd-flow merge-queue wait-next --project-root "$PWD" --worker-id merge-worker --path "$PWD" --timeout 1200 --poll-interval 10 --acquire-lock true --json
|
|
335
345
|
dd-flow merge-queue complete PRT-123 --worker-id merge-worker --path "$PWD" --summary "merged locally; cleanup follows" --json
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { writeJson } from "../shared/json.js";
|
|
|
5
5
|
import { archiveProject, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
|
|
6
6
|
import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, requireProtocol, syncProtocolFromRun, transitionProtocol } from "../services/protocols.js";
|
|
7
7
|
import { blockPlanItem, completePlanItem, getPlanStatus, setProtocolPlan, skipPlanItem, startPlanItem } from "../services/plans.js";
|
|
8
|
-
import { cancelMergeQueueJob, claimNextMergeJob, completeMergeJob, failMergeJob, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
|
|
8
|
+
import { cancelMergeQueueJob, claimNextMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
|
|
9
9
|
import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
|
|
10
10
|
import { cleanupApply, cleanupScan } from "../services/cleanup.js";
|
|
11
11
|
import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
|
|
@@ -18,6 +18,7 @@ import { getCliVersionReport } from "../services/build-info.js";
|
|
|
18
18
|
import { autoRefreshDashboards, dashboardData, getCmuxStatus, openDashboard, openGlobalDashboard, openProtocolDashboard, refreshAllDashboards, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
|
|
19
19
|
import { dashboardTargetSummary, preferredDashboardCommand, relatedDashboardCommands, resolveDashboardTarget } from "../services/dashboard-targets.js";
|
|
20
20
|
import { getFlowSessionStatus, registerFlowSession, stopFlowSession, stopMergeWorker } from "../services/sessions.js";
|
|
21
|
+
import { getProtocolBranchContext } from "../services/branch-context.js";
|
|
21
22
|
import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, listFlowRuns, startFlowRun } from "../services/runs.js";
|
|
22
23
|
import { previewNextEntityId } from "../services/ids.js";
|
|
23
24
|
import { validateSchema } from "../services/schema-validation.js";
|
|
@@ -223,6 +224,18 @@ function renderHumanResult(args, result) {
|
|
|
223
224
|
const job = record.job;
|
|
224
225
|
lines.push(`job: ${String(job.protocol_id ?? "none")} ${String(job.status ?? "")}`.trim());
|
|
225
226
|
}
|
|
227
|
+
const branchContext = recordObject(record.branch_context);
|
|
228
|
+
const mergeBundle = recordObject(branchContext?.merge_bundle);
|
|
229
|
+
if (branchContext) {
|
|
230
|
+
lines.push(`branch: ${String(branchContext.feature_branch ?? branchContext.grouping_key ?? "unknown")}`);
|
|
231
|
+
const protocols = Array.isArray(branchContext.protocols) ? branchContext.protocols.length : 0;
|
|
232
|
+
lines.push(`branch_protocols: ${protocols}`);
|
|
233
|
+
if (mergeBundle) {
|
|
234
|
+
const eligible = Array.isArray(mergeBundle.eligible_protocols) ? mergeBundle.eligible_protocols.length : 0;
|
|
235
|
+
const notReady = Array.isArray(mergeBundle.not_ready_protocols) ? mergeBundle.not_ready_protocols.length : 0;
|
|
236
|
+
lines.push(`bundle: ${String(mergeBundle.reason ?? "unknown")} ready=${eligible} not_ready=${notReady}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
226
239
|
if (record.stopped)
|
|
227
240
|
lines.push(`stopped: ${String(record.reason ?? "true")}`);
|
|
228
241
|
if (record.timed_out)
|
|
@@ -422,6 +435,14 @@ async function dispatch(args, context, io) {
|
|
|
422
435
|
if (family === "protocol" && command === "status") {
|
|
423
436
|
return getProtocolStatus(context, { protocolId: requiredPosition(parsed, 0, "protocol-id") });
|
|
424
437
|
}
|
|
438
|
+
if (family === "protocol" && command === "branch-status") {
|
|
439
|
+
const protocolId = parsed.positional[0];
|
|
440
|
+
return getProtocolBranchContext(context, {
|
|
441
|
+
...(protocolId ? { protocolId } : {}),
|
|
442
|
+
projectRoot: optionalOption(parsed, "project-root"),
|
|
443
|
+
workspacePath: optionalOption(parsed, "path")
|
|
444
|
+
});
|
|
445
|
+
}
|
|
425
446
|
if (family === "protocol" && command === "ready") {
|
|
426
447
|
return getReadyProtocols(context, { projectRoot: requiredOption(parsed, "project-root") });
|
|
427
448
|
}
|
|
@@ -655,7 +676,38 @@ function dispatchMergeQueue(context, command, parsed) {
|
|
|
655
676
|
}
|
|
656
677
|
function dispatchMerge(context, command, parsed) {
|
|
657
678
|
if (command === "status") {
|
|
658
|
-
return getMergeWorkerStatus(context, { projectRoot: requiredOption(parsed, "project-root") });
|
|
679
|
+
return getMergeWorkerStatus(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: optionalOption(parsed, "path") });
|
|
680
|
+
}
|
|
681
|
+
if (command === "bundle") {
|
|
682
|
+
const action = requiredPosition(parsed, 0, "bundle action");
|
|
683
|
+
if (action === "status") {
|
|
684
|
+
return getMergeBundleStatus(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed) });
|
|
685
|
+
}
|
|
686
|
+
if (action === "claim") {
|
|
687
|
+
return claimMergeBundle(context, {
|
|
688
|
+
projectRoot: requiredOption(parsed, "project-root"),
|
|
689
|
+
workerId: requiredWorkerId(parsed),
|
|
690
|
+
workspacePath: workspacePathOption(parsed)
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
if (action === "complete") {
|
|
694
|
+
return completeMergeBundle(context, {
|
|
695
|
+
projectRoot: requiredOption(parsed, "project-root"),
|
|
696
|
+
workerId: requiredWorkerId(parsed),
|
|
697
|
+
workspacePath: workspacePathOption(parsed),
|
|
698
|
+
summary: requiredOption(parsed, "summary")
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
if (action === "fail") {
|
|
702
|
+
return failMergeBundle(context, {
|
|
703
|
+
projectRoot: requiredOption(parsed, "project-root"),
|
|
704
|
+
workerId: requiredWorkerId(parsed),
|
|
705
|
+
workspacePath: workspacePathOption(parsed),
|
|
706
|
+
reason: requiredOption(parsed, "reason"),
|
|
707
|
+
requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue")
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
throw new AppError("usage", `Unknown merge bundle action: ${action}`, 2);
|
|
659
711
|
}
|
|
660
712
|
if (command === "one-shot") {
|
|
661
713
|
return oneShotMergeClaim(context, {
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
6
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
7
|
+
import { normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
|
|
8
|
+
export function getProtocolBranchContext(context, input) {
|
|
9
|
+
const resolved = resolveBranchContextInput(context, input);
|
|
10
|
+
const rows = context.db.all(`SELECT id, project_id, project_root, status, stage, next_action, workspace_json, route_json, active_def_json, updated_at
|
|
11
|
+
FROM protocols
|
|
12
|
+
WHERE project_id = ?
|
|
13
|
+
ORDER BY updated_at ASC, id ASC`, [resolved.project.id]);
|
|
14
|
+
const queues = new Map(context.db
|
|
15
|
+
.all(`SELECT protocol_id, status, claimed_by_session_id, claimed_at, attempts_count, updated_at
|
|
16
|
+
FROM merge_queue
|
|
17
|
+
WHERE project_id = ?`, [resolved.project.id])
|
|
18
|
+
.map((row) => [row.protocol_id, row]));
|
|
19
|
+
const worktrees = new Map(context.db
|
|
20
|
+
.all(`SELECT protocol_id, integration_branch, feature_branch, base_ref, worktree_path, status
|
|
21
|
+
FROM worktree_records
|
|
22
|
+
WHERE project_id = ?`, [resolved.project.id])
|
|
23
|
+
.map((row) => [row.protocol_id, row]));
|
|
24
|
+
const sessions = context.db.all(`SELECT protocol_id, worker_id, flow_kind, status, current_stage, next_action
|
|
25
|
+
FROM flow_sessions
|
|
26
|
+
WHERE project_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [resolved.project.id]);
|
|
27
|
+
const sessionsByProtocol = new Map();
|
|
28
|
+
for (const session of sessions) {
|
|
29
|
+
if (!session.protocol_id)
|
|
30
|
+
continue;
|
|
31
|
+
sessionsByProtocol.set(session.protocol_id, [...(sessionsByProtocol.get(session.protocol_id) ?? []), session]);
|
|
32
|
+
}
|
|
33
|
+
const candidates = rows.map((row) => summarizeProtocol(row, queues.get(row.id), worktrees.get(row.id), sessionsByProtocol.get(row.id) ?? []));
|
|
34
|
+
const protocols = candidates.filter((candidate) => protocolMatchesBranch(candidate, resolved));
|
|
35
|
+
const eligible = protocols.filter((item) => item.bundle_eligible).map((item) => item.id);
|
|
36
|
+
const notReady = protocols.filter((item) => !item.bundle_eligible && !terminalStage(item.stage) && item.queue_status !== "claimed").map((item) => item.id);
|
|
37
|
+
const claimed = protocols.filter((item) => item.queue_status === "claimed").map((item) => item.id);
|
|
38
|
+
const blocked = protocols.filter((item) => item.stage === "blocked" || item.bundle_blockers.length > 0).map((item) => item.id);
|
|
39
|
+
const diagnostics = branchDiagnostics(resolved);
|
|
40
|
+
const diagnosticBlockers = diagnostics.filter((item) => item.severity === "error").map((item) => String(item.code));
|
|
41
|
+
const claimable = eligible.length > 0 && notReady.length === 0 && claimed.length === 0 && blocked.length === 0 && diagnosticBlockers.length === 0;
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
branch_context: {
|
|
45
|
+
current_protocol_id: resolved.protocol?.id ?? null,
|
|
46
|
+
project_id: resolved.project.id,
|
|
47
|
+
project_root: resolved.project.root,
|
|
48
|
+
integration_branch: resolved.integrationBranch,
|
|
49
|
+
feature_branch: resolved.featureBranch,
|
|
50
|
+
worktree_path: resolved.workspacePath,
|
|
51
|
+
grouping_key: resolved.groupingKey,
|
|
52
|
+
git: resolved.git,
|
|
53
|
+
diagnostics,
|
|
54
|
+
protocols,
|
|
55
|
+
merge_bundle: {
|
|
56
|
+
eligible_protocols: eligible,
|
|
57
|
+
not_ready_protocols: notReady,
|
|
58
|
+
claimed_protocols: claimed,
|
|
59
|
+
blocked_protocols: blocked,
|
|
60
|
+
claimable,
|
|
61
|
+
reason: claimable ? "claimable" : bundleReason(eligible, notReady, claimed, blocked, diagnosticBlockers)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function requireClaimableBranchBundle(context, input) {
|
|
67
|
+
const branchContext = getProtocolBranchContext(context, { projectRoot: input.projectRoot, workspacePath: input.workspacePath });
|
|
68
|
+
const contextBody = branchContext.branch_context;
|
|
69
|
+
const bundle = contextBody.merge_bundle;
|
|
70
|
+
if (bundle.claimable !== true) {
|
|
71
|
+
throw new AppError("merge_bundle_not_claimable", "Branch bundle is not claimable", 1, { branch_context: contextBody });
|
|
72
|
+
}
|
|
73
|
+
const protocolIds = Array.isArray(bundle.eligible_protocols) ? bundle.eligible_protocols.map(String) : [];
|
|
74
|
+
if (protocolIds.length === 0) {
|
|
75
|
+
throw new AppError("merge_bundle_empty", "Branch bundle has no ready protocols", 1, { branch_context: contextBody });
|
|
76
|
+
}
|
|
77
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
78
|
+
return { project, branchContext, protocolIds };
|
|
79
|
+
}
|
|
80
|
+
function resolveBranchContextInput(context, input) {
|
|
81
|
+
if (input.protocolId) {
|
|
82
|
+
const protocol = context.db.get(`SELECT id, project_id, project_root, status, stage, next_action, workspace_json, route_json, active_def_json, updated_at
|
|
83
|
+
FROM protocols
|
|
84
|
+
WHERE id = ?`, [input.protocolId]);
|
|
85
|
+
if (!protocol) {
|
|
86
|
+
throw new AppError("not_found", `Protocol is not registered: ${input.protocolId}`, 1, { protocol_id: input.protocolId });
|
|
87
|
+
}
|
|
88
|
+
const project = requireProjectByRoot(context, protocol.project_root);
|
|
89
|
+
const workspace = parseRecord(protocol.workspace_json);
|
|
90
|
+
const worktree = context.db.get("SELECT * FROM worktree_records WHERE protocol_id = ?", [protocol.id]);
|
|
91
|
+
const workspacePath = normalizePath(input.workspacePath ?? stringValue(worktree?.worktree_path) ?? stringValue(workspace.worktree_path));
|
|
92
|
+
const git = gitFacts(workspacePath ?? project.root);
|
|
93
|
+
const featureBranch = stringValue(worktree?.feature_branch) ?? stringValue(workspace.feature_branch) ?? stringValue(git.actual_branch);
|
|
94
|
+
return {
|
|
95
|
+
project,
|
|
96
|
+
protocol,
|
|
97
|
+
featureBranch,
|
|
98
|
+
workspacePath,
|
|
99
|
+
integrationBranch: stringValue(worktree?.integration_branch) ?? stringValue(workspace.integration_branch),
|
|
100
|
+
groupingKey: groupingKey(project.id, featureBranch, workspacePath),
|
|
101
|
+
git: { ...git, matches_runtime: branchMatches(featureBranch, stringValue(git.actual_branch)) }
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (!input.projectRoot) {
|
|
105
|
+
throw new AppError("usage", "branch context requires <protocol-id> or --project-root", 2);
|
|
106
|
+
}
|
|
107
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
108
|
+
const workspacePath = normalizePath(input.workspacePath ?? project.root);
|
|
109
|
+
const git = gitFacts(workspacePath ?? project.root);
|
|
110
|
+
const featureBranch = stringValue(git.actual_branch);
|
|
111
|
+
return {
|
|
112
|
+
project,
|
|
113
|
+
protocol: null,
|
|
114
|
+
featureBranch,
|
|
115
|
+
workspacePath,
|
|
116
|
+
integrationBranch: null,
|
|
117
|
+
groupingKey: groupingKey(project.id, featureBranch, workspacePath),
|
|
118
|
+
git: { ...git, matches_runtime: true }
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function summarizeProtocol(row, queue, worktree, activeSessions) {
|
|
122
|
+
const workspace = parseRecord(row.workspace_json);
|
|
123
|
+
const featureBranch = stringValue(worktree?.feature_branch) ?? stringValue(workspace.feature_branch);
|
|
124
|
+
const worktreePath = normalizePath(stringValue(worktree?.worktree_path) ?? stringValue(workspace.worktree_path));
|
|
125
|
+
const activeDef = parseJsonArray(row.active_def_json);
|
|
126
|
+
const queueStatus = queue?.status ?? null;
|
|
127
|
+
const bundleBlockers = bundleBlockersFor(row.stage, queueStatus, activeDef.length);
|
|
128
|
+
return {
|
|
129
|
+
id: row.id,
|
|
130
|
+
stage: row.stage,
|
|
131
|
+
status: row.status,
|
|
132
|
+
lifecycle: normalizeProtocolLifecycle({ rawStage: row.stage, rawStatus: row.status, queueStatus }),
|
|
133
|
+
queue_status: queueStatus,
|
|
134
|
+
queue_claimed_by: queue?.claimed_by_session_id ?? null,
|
|
135
|
+
active_sessions: activeSessions,
|
|
136
|
+
integration_branch: stringValue(worktree?.integration_branch) ?? stringValue(workspace.integration_branch),
|
|
137
|
+
feature_branch: featureBranch,
|
|
138
|
+
worktree_path: worktreePath,
|
|
139
|
+
bundle_eligible: ["ready_for_merge", "queued_for_merge"].includes(row.stage) && ["ready", "requeued"].includes(queueStatus ?? "") && activeDef.length === 0,
|
|
140
|
+
bundle_blockers: bundleBlockers,
|
|
141
|
+
updated_at: row.updated_at
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function protocolMatchesBranch(item, resolved) {
|
|
145
|
+
if (resolved.featureBranch && item.feature_branch) {
|
|
146
|
+
return item.feature_branch === resolved.featureBranch;
|
|
147
|
+
}
|
|
148
|
+
if (resolved.workspacePath && item.worktree_path) {
|
|
149
|
+
return item.worktree_path === resolved.workspacePath;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
function gitFacts(workspacePath) {
|
|
154
|
+
if (!fs.existsSync(workspacePath)) {
|
|
155
|
+
return { status: "missing_workspace", actual_branch: null, dirty: null, path: workspacePath };
|
|
156
|
+
}
|
|
157
|
+
const branch = spawnSync("git", ["-C", workspacePath, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8" });
|
|
158
|
+
if (branch.status !== 0) {
|
|
159
|
+
return { status: "not_git_checkout", actual_branch: null, dirty: null, path: workspacePath };
|
|
160
|
+
}
|
|
161
|
+
const status = spawnSync("git", ["-C", workspacePath, "status", "--porcelain"], { encoding: "utf8" });
|
|
162
|
+
return {
|
|
163
|
+
status: "ok",
|
|
164
|
+
actual_branch: branch.stdout.trim() || null,
|
|
165
|
+
dirty: status.status === 0 ? status.stdout.trim().length > 0 : null,
|
|
166
|
+
path: workspacePath
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function branchDiagnostics(resolved) {
|
|
170
|
+
const diagnostics = [];
|
|
171
|
+
if (resolved.git.status !== "ok") {
|
|
172
|
+
diagnostics.push({ code: "git_workspace_degraded", severity: "warning", summary: `Git workspace status is ${String(resolved.git.status)}` });
|
|
173
|
+
}
|
|
174
|
+
if (resolved.featureBranch && resolved.git.actual_branch && resolved.featureBranch !== resolved.git.actual_branch) {
|
|
175
|
+
diagnostics.push({
|
|
176
|
+
code: "branch_runtime_mismatch",
|
|
177
|
+
severity: "error",
|
|
178
|
+
summary: `Runtime branch ${resolved.featureBranch} does not match Git branch ${String(resolved.git.actual_branch)}.`
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return diagnostics;
|
|
182
|
+
}
|
|
183
|
+
function bundleBlockersFor(stage, queueStatus, activeDefCount) {
|
|
184
|
+
const blockers = [];
|
|
185
|
+
if (activeDefCount > 0)
|
|
186
|
+
blockers.push("active_def");
|
|
187
|
+
if (stage === "blocked")
|
|
188
|
+
blockers.push("protocol_blocked");
|
|
189
|
+
if (queueStatus === "claimed")
|
|
190
|
+
blockers.push("already_claimed");
|
|
191
|
+
return blockers;
|
|
192
|
+
}
|
|
193
|
+
function bundleReason(eligible, notReady, claimed, blocked, diagnostics) {
|
|
194
|
+
if (diagnostics.length > 0)
|
|
195
|
+
return diagnostics[0] ?? "diagnostic_blocker";
|
|
196
|
+
if (claimed.length > 0)
|
|
197
|
+
return "branch_has_claimed_protocols";
|
|
198
|
+
if (blocked.length > 0)
|
|
199
|
+
return "branch_has_blocked_protocols";
|
|
200
|
+
if (notReady.length > 0)
|
|
201
|
+
return "branch_has_not_ready_protocols";
|
|
202
|
+
if (eligible.length === 0)
|
|
203
|
+
return "no_ready_protocols";
|
|
204
|
+
return "unknown";
|
|
205
|
+
}
|
|
206
|
+
function terminalStage(stage) {
|
|
207
|
+
return ["closed", "cancelled"].includes(stage);
|
|
208
|
+
}
|
|
209
|
+
function groupingKey(projectId, featureBranch, workspacePath) {
|
|
210
|
+
if (featureBranch)
|
|
211
|
+
return `${projectId}:branch:${featureBranch}`;
|
|
212
|
+
if (workspacePath)
|
|
213
|
+
return `${projectId}:worktree:${workspacePath}`;
|
|
214
|
+
return `${projectId}:unknown`;
|
|
215
|
+
}
|
|
216
|
+
function branchMatches(expected, actual) {
|
|
217
|
+
if (!expected || !actual)
|
|
218
|
+
return null;
|
|
219
|
+
return expected === actual;
|
|
220
|
+
}
|
|
221
|
+
function normalizePath(value) {
|
|
222
|
+
if (!value)
|
|
223
|
+
return null;
|
|
224
|
+
const absolute = path.resolve(value);
|
|
225
|
+
if (fs.existsSync(absolute))
|
|
226
|
+
return fs.realpathSync(absolute);
|
|
227
|
+
const parent = path.dirname(absolute);
|
|
228
|
+
if (fs.existsSync(parent))
|
|
229
|
+
return path.join(fs.realpathSync(parent), path.basename(absolute));
|
|
230
|
+
return absolute;
|
|
231
|
+
}
|
|
232
|
+
function parseRecord(value) {
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(value);
|
|
235
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return {};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function parseJsonArray(value) {
|
|
242
|
+
if (typeof value !== "string")
|
|
243
|
+
return [];
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(value);
|
|
246
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function stringValue(value) {
|
|
253
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
254
|
+
}
|
|
@@ -27,6 +27,7 @@ export function preflightCliCompatibility(context, args, env = process.env) {
|
|
|
27
27
|
}
|
|
28
28
|
export function compatibilityReport(selection, classification) {
|
|
29
29
|
const build = getCliBuildInfo();
|
|
30
|
+
const normalWriteAllowed = selection.status === "selected" && Boolean(selection.selected);
|
|
30
31
|
return {
|
|
31
32
|
verdict: selection.status === "selected" ? "ok" : "incompatible",
|
|
32
33
|
memorybank_version: selection.memory_bank_version,
|
|
@@ -36,9 +37,11 @@ export function compatibilityReport(selection, classification) {
|
|
|
36
37
|
engine_resolution: selection.status,
|
|
37
38
|
required_engine_range: selection.required_range,
|
|
38
39
|
recommended_engine_version: selection.recommended_version,
|
|
39
|
-
allowed_modes:
|
|
40
|
+
allowed_modes: normalWriteAllowed
|
|
41
|
+
? ["read_only_diagnostics", "normal_write", "mb_upgrade"]
|
|
42
|
+
: ["read_only_diagnostics", "mb_upgrade"],
|
|
40
43
|
operation_mode: classification.mode,
|
|
41
|
-
blocked_operation: classification.mode === "normal_write" ? classification.operation : null,
|
|
44
|
+
blocked_operation: classification.mode === "normal_write" && !normalWriteAllowed ? classification.operation : null,
|
|
42
45
|
install_hint: selection.install_hint,
|
|
43
46
|
project_root: selection.project_root,
|
|
44
47
|
diagnostics: selection.diagnostics
|
|
@@ -6,6 +6,7 @@ import { persistProtocolState, protocolRunDiagnostics, readProtocolRuntimeState,
|
|
|
6
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
7
|
import { ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, requireLaneLockOwner, waitAcquireLaneLock } from "./lanes.js";
|
|
8
8
|
import { stopMergeWorker, stoppedMergeWorkerState } from "./sessions.js";
|
|
9
|
+
import { getProtocolBranchContext, requireClaimableBranchBundle } from "./branch-context.js";
|
|
9
10
|
const mergeLockTtlSeconds = 300;
|
|
10
11
|
export function getMergeQueueStatus(context, input) {
|
|
11
12
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -14,6 +15,9 @@ export function getMergeQueueStatus(context, input) {
|
|
|
14
15
|
queue: queueForProject(context, project.id)
|
|
15
16
|
};
|
|
16
17
|
}
|
|
18
|
+
export function getMergeBundleStatus(context, input) {
|
|
19
|
+
return getProtocolBranchContext(context, { projectRoot: input.projectRoot, workspacePath: input.workspacePath });
|
|
20
|
+
}
|
|
17
21
|
export function claimNextMergeJob(context, input) {
|
|
18
22
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
19
23
|
const stopState = stoppedMergeWorkerState(context, project.id, input.workerId);
|
|
@@ -69,6 +73,67 @@ export function claimNextMergeJob(context, input) {
|
|
|
69
73
|
claimed: Boolean(claimedProtocolId)
|
|
70
74
|
});
|
|
71
75
|
}
|
|
76
|
+
export function claimMergeBundle(context, input) {
|
|
77
|
+
const { project, protocolIds } = requireClaimableBranchBundle(context, {
|
|
78
|
+
projectRoot: input.projectRoot,
|
|
79
|
+
workspacePath: input.workspacePath
|
|
80
|
+
});
|
|
81
|
+
const stopState = stoppedMergeWorkerState(context, project.id, input.workerId);
|
|
82
|
+
if (stopState.stopped) {
|
|
83
|
+
throw new AppError("merge_worker_stopped", "Stopped or stopping merge worker cannot claim a merge bundle", 1, {
|
|
84
|
+
worker_id: input.workerId,
|
|
85
|
+
status: stopState.status,
|
|
86
|
+
reason: stopState.reason
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
requireLaneLockOwner(context, {
|
|
90
|
+
projectRoot: project.root,
|
|
91
|
+
lane: "merge",
|
|
92
|
+
workerId: input.workerId,
|
|
93
|
+
workspacePath: input.workspacePath
|
|
94
|
+
});
|
|
95
|
+
const now = context.now();
|
|
96
|
+
context.db.exec("BEGIN IMMEDIATE");
|
|
97
|
+
try {
|
|
98
|
+
for (const protocolId of protocolIds) {
|
|
99
|
+
const update = context.db.run(`UPDATE merge_queue
|
|
100
|
+
SET status = 'claimed', claimed_by_session_id = ?, claimed_at = ?, updated_at = ?
|
|
101
|
+
WHERE protocol_id = ? AND project_id = ? AND status IN ('ready', 'requeued')`, [input.workerId, now, now, protocolId, project.id]);
|
|
102
|
+
if (update.changes !== 1) {
|
|
103
|
+
throw new AppError("merge_bundle_claim_race", "Branch bundle changed while claiming", 1, {
|
|
104
|
+
protocol_id: protocolId,
|
|
105
|
+
protocol_ids: protocolIds
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
transitionClaimedProtocolToIntegration(context, protocolId, input.workerId, now);
|
|
109
|
+
appendAudit(context, {
|
|
110
|
+
protocolId,
|
|
111
|
+
projectId: project.id,
|
|
112
|
+
eventType: "merge_bundle.claimed_protocol",
|
|
113
|
+
payload: { protocol_id: protocolId, worker_id: input.workerId, protocol_ids: protocolIds }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
appendAudit(context, {
|
|
117
|
+
projectId: project.id,
|
|
118
|
+
eventType: "merge_bundle.claimed",
|
|
119
|
+
payload: { project_id: project.id, worker_id: input.workerId, protocol_ids: protocolIds, workspace_path: input.workspacePath }
|
|
120
|
+
});
|
|
121
|
+
context.db.exec("COMMIT");
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
context.db.exec("ROLLBACK");
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return mergeBundleResult(context, {
|
|
128
|
+
ok: true,
|
|
129
|
+
outcome: "claimed",
|
|
130
|
+
claimed: true,
|
|
131
|
+
projectRoot: project.root,
|
|
132
|
+
workspacePath: input.workspacePath,
|
|
133
|
+
workerId: input.workerId,
|
|
134
|
+
protocolIds
|
|
135
|
+
});
|
|
136
|
+
}
|
|
72
137
|
export async function waitNextMergeJob(context, input) {
|
|
73
138
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
74
139
|
const timeoutSeconds = input.timeoutSeconds ?? 0;
|
|
@@ -251,6 +316,71 @@ export function completeMergeJob(context, input) {
|
|
|
251
316
|
stop_after_current
|
|
252
317
|
});
|
|
253
318
|
}
|
|
319
|
+
export function completeMergeBundle(context, input) {
|
|
320
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
321
|
+
requireLaneLockOwner(context, {
|
|
322
|
+
projectRoot: project.root,
|
|
323
|
+
lane: "merge",
|
|
324
|
+
workerId: input.workerId,
|
|
325
|
+
workspacePath: input.workspacePath
|
|
326
|
+
});
|
|
327
|
+
const protocolIds = claimedBundleProtocolIds(context, { projectRoot: project.root, workspacePath: input.workspacePath, workerId: input.workerId });
|
|
328
|
+
if (protocolIds.length === 0) {
|
|
329
|
+
throw new AppError("merge_bundle_not_claimed", "No claimed protocols found for this branch bundle and worker", 1, {
|
|
330
|
+
worker_id: input.workerId,
|
|
331
|
+
workspace_path: input.workspacePath
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const now = context.now();
|
|
335
|
+
context.db.exec("BEGIN IMMEDIATE");
|
|
336
|
+
try {
|
|
337
|
+
for (const protocolId of protocolIds) {
|
|
338
|
+
const protocol = requireProtocol(context, protocolId);
|
|
339
|
+
const job = requireClaimedJob(context, protocolId, input.workerId);
|
|
340
|
+
context.db.run(`UPDATE merge_queue
|
|
341
|
+
SET status = 'merged', last_reason = ?, completed_at = ?, updated_at = ?
|
|
342
|
+
WHERE id = ?`, [input.summary, now, now, job.id]);
|
|
343
|
+
const state = readProtocolRuntimeState(context, protocol).state;
|
|
344
|
+
const flowContract = loadProjectFlowContract(protocol.project_root);
|
|
345
|
+
const targetStage = flowContract.merge_queue.complete.target_stage;
|
|
346
|
+
const targetStatus = flowContract.stages[targetStage]?.terminal ? targetStage : "running";
|
|
347
|
+
persistProtocolState(context, protocol, {
|
|
348
|
+
...state,
|
|
349
|
+
stage: targetStage,
|
|
350
|
+
status: targetStatus,
|
|
351
|
+
next_action: flowContract.merge_queue.complete.next_action,
|
|
352
|
+
updated_at: now
|
|
353
|
+
});
|
|
354
|
+
appendAudit(context, {
|
|
355
|
+
protocolId,
|
|
356
|
+
projectId: project.id,
|
|
357
|
+
eventType: "merge_bundle.completed_protocol",
|
|
358
|
+
payload: { protocol_id: protocolId, worker_id: input.workerId, summary: input.summary, protocol_ids: protocolIds }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
appendAudit(context, {
|
|
362
|
+
projectId: project.id,
|
|
363
|
+
eventType: "merge_bundle.completed",
|
|
364
|
+
payload: { project_id: project.id, worker_id: input.workerId, summary: input.summary, protocol_ids: protocolIds }
|
|
365
|
+
});
|
|
366
|
+
context.db.exec("COMMIT");
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
context.db.exec("ROLLBACK");
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
const stop_after_current = stopAfterCurrentIfRequested(context, project.id, project.root, input.workerId, "merge bundle complete after stop request");
|
|
373
|
+
return mergeBundleResult(context, {
|
|
374
|
+
ok: true,
|
|
375
|
+
outcome: "merged",
|
|
376
|
+
merged: true,
|
|
377
|
+
projectRoot: project.root,
|
|
378
|
+
workspacePath: input.workspacePath,
|
|
379
|
+
workerId: input.workerId,
|
|
380
|
+
protocolIds,
|
|
381
|
+
stop_after_current
|
|
382
|
+
});
|
|
383
|
+
}
|
|
254
384
|
export function noteMergeJob(context, input) {
|
|
255
385
|
const protocol = requireProtocol(context, input.protocolId);
|
|
256
386
|
const job = queueJobByProtocol(context, protocol.id);
|
|
@@ -319,6 +449,72 @@ export function failMergeJob(context, input) {
|
|
|
319
449
|
stop_after_current
|
|
320
450
|
});
|
|
321
451
|
}
|
|
452
|
+
export function failMergeBundle(context, input) {
|
|
453
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
454
|
+
requireLaneLockOwner(context, {
|
|
455
|
+
projectRoot: project.root,
|
|
456
|
+
lane: "merge",
|
|
457
|
+
workerId: input.workerId,
|
|
458
|
+
workspacePath: input.workspacePath
|
|
459
|
+
});
|
|
460
|
+
const protocolIds = claimedBundleProtocolIds(context, { projectRoot: project.root, workspacePath: input.workspacePath, workerId: input.workerId });
|
|
461
|
+
if (protocolIds.length === 0) {
|
|
462
|
+
throw new AppError("merge_bundle_not_claimed", "No claimed protocols found for this branch bundle and worker", 1, {
|
|
463
|
+
worker_id: input.workerId,
|
|
464
|
+
workspace_path: input.workspacePath
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
const now = context.now();
|
|
468
|
+
const status = input.requeue ? "requeued" : "failed";
|
|
469
|
+
context.db.exec("BEGIN IMMEDIATE");
|
|
470
|
+
try {
|
|
471
|
+
for (const protocolId of protocolIds) {
|
|
472
|
+
const protocol = requireProtocol(context, protocolId);
|
|
473
|
+
const job = requireClaimedJob(context, protocolId, input.workerId);
|
|
474
|
+
context.db.run(`UPDATE merge_queue
|
|
475
|
+
SET status = ?, attempts_count = attempts_count + 1, last_reason = ?, updated_at = ?
|
|
476
|
+
WHERE id = ?`, [status, input.reason, now, job.id]);
|
|
477
|
+
if (input.requeue) {
|
|
478
|
+
const state = readProtocolRuntimeState(context, protocol).state;
|
|
479
|
+
persistProtocolState(context, protocol, {
|
|
480
|
+
...state,
|
|
481
|
+
stage: "ready_for_merge",
|
|
482
|
+
status: "running",
|
|
483
|
+
next_action: "merge_requeued",
|
|
484
|
+
updated_at: now
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
appendAudit(context, {
|
|
488
|
+
protocolId,
|
|
489
|
+
projectId: project.id,
|
|
490
|
+
eventType: "merge_bundle.failed_protocol",
|
|
491
|
+
reason: input.reason,
|
|
492
|
+
payload: { protocol_id: protocolId, worker_id: input.workerId, requeue: input.requeue, protocol_ids: protocolIds }
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
appendAudit(context, {
|
|
496
|
+
projectId: project.id,
|
|
497
|
+
eventType: "merge_bundle.failed",
|
|
498
|
+
reason: input.reason,
|
|
499
|
+
payload: { project_id: project.id, worker_id: input.workerId, requeue: input.requeue, protocol_ids: protocolIds }
|
|
500
|
+
});
|
|
501
|
+
context.db.exec("COMMIT");
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
context.db.exec("ROLLBACK");
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
const stop_after_current = stopAfterCurrentIfRequested(context, project.id, project.root, input.workerId, "merge bundle fail after stop request");
|
|
508
|
+
return mergeBundleResult(context, {
|
|
509
|
+
ok: true,
|
|
510
|
+
outcome: input.requeue ? "requeued" : "failed",
|
|
511
|
+
projectRoot: project.root,
|
|
512
|
+
workspacePath: input.workspacePath,
|
|
513
|
+
workerId: input.workerId,
|
|
514
|
+
protocolIds,
|
|
515
|
+
stop_after_current
|
|
516
|
+
});
|
|
517
|
+
}
|
|
322
518
|
export function cancelMergeQueueJob(context, input) {
|
|
323
519
|
const protocol = requireProtocol(context, input.protocolId);
|
|
324
520
|
const reason = input.reason.trim();
|
|
@@ -407,6 +603,23 @@ function mergeQueueResult(job, extra) {
|
|
|
407
603
|
job: queueItem
|
|
408
604
|
};
|
|
409
605
|
}
|
|
606
|
+
function mergeBundleResult(context, input) {
|
|
607
|
+
const branchContext = getProtocolBranchContext(context, { projectRoot: input.projectRoot, workspacePath: input.workspacePath }).branch_context;
|
|
608
|
+
return {
|
|
609
|
+
...input,
|
|
610
|
+
protocol_ids: input.protocolIds,
|
|
611
|
+
branch_context: branchContext,
|
|
612
|
+
queue_items: input.protocolIds.map((protocolId) => queueJobByProtocol(context, protocolId) ?? null)
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function claimedBundleProtocolIds(context, input) {
|
|
616
|
+
const branchContext = getProtocolBranchContext(context, { projectRoot: input.projectRoot, workspacePath: input.workspacePath }).branch_context;
|
|
617
|
+
const protocols = Array.isArray(branchContext.protocols) ? branchContext.protocols : [];
|
|
618
|
+
return protocols
|
|
619
|
+
.filter((protocol) => protocol.queue_status === "claimed" && protocol.queue_claimed_by === input.workerId)
|
|
620
|
+
.map((protocol) => String(protocol.id))
|
|
621
|
+
.filter(Boolean);
|
|
622
|
+
}
|
|
410
623
|
function requireClaimedJob(context, protocolId, sessionId) {
|
|
411
624
|
const job = queueJobByProtocol(context, protocolId);
|
|
412
625
|
if (!job || job.status !== "claimed") {
|
|
@@ -7,13 +7,17 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
7
7
|
import { buildStaticFlowGuidance } from "./flow-guidance.js";
|
|
8
8
|
import { readProtocolRuntimeState, requireProtocol } from "./protocols.js";
|
|
9
9
|
import { registerFlowSession, stopMergeWorker } from "./sessions.js";
|
|
10
|
+
import { getProtocolBranchContext } from "./branch-context.js";
|
|
10
11
|
export function getMergeWorkerStatus(context, input) {
|
|
11
12
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
12
13
|
return {
|
|
13
14
|
ok: true,
|
|
14
15
|
project: { id: project.id, root: project.root },
|
|
15
16
|
merge_worker: detectMergeWorkerState(context, project.id),
|
|
16
|
-
queue: queueForProject(context, project.id).map((job) => withJobGuidance(context, { ...job }))
|
|
17
|
+
queue: queueForProject(context, project.id).map((job) => withJobGuidance(context, { ...job })),
|
|
18
|
+
branch_context: input.workspacePath
|
|
19
|
+
? getProtocolBranchContext(context, { projectRoot: project.root, workspacePath: input.workspacePath }).branch_context
|
|
20
|
+
: null
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
23
|
export function startMergeWorker(context, input) {
|
|
@@ -29,7 +33,8 @@ export function startMergeWorker(context, input) {
|
|
|
29
33
|
outcome: "status_only",
|
|
30
34
|
reason: "merge_worker_already_active",
|
|
31
35
|
merge_worker: detection,
|
|
32
|
-
queue: queueForProject(context, project.id)
|
|
36
|
+
queue: queueForProject(context, project.id),
|
|
37
|
+
branch_context: getProtocolBranchContext(context, { projectRoot: project.root, workspacePath }).branch_context
|
|
33
38
|
};
|
|
34
39
|
}
|
|
35
40
|
ensureLaneWorkspace(context, { projectRoot: project.root, lane: "merge", workspacePath, branch: input.branch });
|
|
@@ -98,7 +103,8 @@ export function oneShotMergeClaim(context, input) {
|
|
|
98
103
|
outcome: "status_only",
|
|
99
104
|
reason: "active_merge_worker_or_lock",
|
|
100
105
|
merge_worker: detection,
|
|
101
|
-
queue: queueForProject(context, project.id)
|
|
106
|
+
queue: queueForProject(context, project.id),
|
|
107
|
+
branch_context: getProtocolBranchContext(context, { projectRoot: project.root, workspacePath }).branch_context
|
|
102
108
|
};
|
|
103
109
|
}
|
|
104
110
|
ensureLaneWorkspace(context, { projectRoot: project.root, lane: "merge", workspacePath });
|
|
@@ -143,6 +149,7 @@ export function oneShotMergeClaim(context, input) {
|
|
|
143
149
|
: null,
|
|
144
150
|
job: claimed.job ? withJobGuidance(context, { ...claimed.job }) : null,
|
|
145
151
|
lock: claimed.job ? lock.lock : null,
|
|
152
|
+
branch_context: getProtocolBranchContext(context, { projectRoot: project.root, workspacePath }).branch_context,
|
|
146
153
|
flow_guidance: claimed.job ? withJobGuidance(context, { ...claimed.job }).flow_guidance : undefined
|
|
147
154
|
};
|
|
148
155
|
}
|
|
@@ -15,6 +15,7 @@ import { ensureRuntimeProtocolFiles, readPlanFile, readStateFile, writeState } f
|
|
|
15
15
|
import { activeCodexSessionBindingsForProject, activeFlowSessionBindingsForProject, codexHomeProfilesForProject, codexHookEventsForProject, hookStatusForProject } from "./hooks.js";
|
|
16
16
|
import { buildProtocolFlowGuidance } from "./flow-guidance.js";
|
|
17
17
|
import { lifecycleIsTerminalFailure, lifecycleIsTerminalSuccess, normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
|
|
18
|
+
import { getProtocolBranchContext } from "./branch-context.js";
|
|
18
19
|
export function registerProtocol(context, input) {
|
|
19
20
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
20
21
|
const project = requireProjectByRoot(context, projectRoot);
|
|
@@ -99,6 +100,7 @@ export function getProtocolStatus(context, input) {
|
|
|
99
100
|
state,
|
|
100
101
|
plan: plan ? summarizePlan(plan) : state.plan,
|
|
101
102
|
merge_queue: queue,
|
|
103
|
+
branch_context: getProtocolBranchContext(context, { protocolId: protocol.id }).branch_context,
|
|
102
104
|
flow_guidance: buildProtocolFlowGuidance({ state, latestRun: runDiagnostics.latest_run, queueStatus: queue?.status ?? null }),
|
|
103
105
|
worktree: context.db.get("SELECT * FROM worktree_records WHERE protocol_id = ?", [protocol.id]),
|
|
104
106
|
hook_status: hookStatusForProject(context, protocol.project_id),
|
|
@@ -203,6 +205,7 @@ export function implementProtocol(context, input) {
|
|
|
203
205
|
blockers,
|
|
204
206
|
active_sessions: activeSessions,
|
|
205
207
|
merge_queue: queue ?? null,
|
|
208
|
+
branch_context: protocol ? getProtocolBranchContext(context, { protocolId: protocol.id }).branch_context : null,
|
|
206
209
|
lifecycle,
|
|
207
210
|
terminal,
|
|
208
211
|
related_context: document.related_context,
|
|
@@ -306,6 +309,7 @@ export function readyForMerge(context, input) {
|
|
|
306
309
|
protocol_id: protocol.id,
|
|
307
310
|
queue_status: existingQueueJob.status,
|
|
308
311
|
state,
|
|
312
|
+
branch_context: getProtocolBranchContext(context, { protocolId: protocol.id }).branch_context,
|
|
309
313
|
lifecycle: normalizeProtocolLifecycle({ state, queueStatus: existingQueueJob.status }),
|
|
310
314
|
flow_guidance: buildProtocolFlowGuidance({ state, latestRun: runDiagnostics.latest_run, queueStatus: existingQueueJob.status })
|
|
311
315
|
};
|
|
@@ -358,6 +362,7 @@ export function readyForMerge(context, input) {
|
|
|
358
362
|
protocol_id: protocol.id,
|
|
359
363
|
queue_status: "ready",
|
|
360
364
|
state: nextState,
|
|
365
|
+
branch_context: getProtocolBranchContext(context, { protocolId: protocol.id }).branch_context,
|
|
361
366
|
lifecycle: normalizeProtocolLifecycle({ state: nextState, queueStatus: "ready" }),
|
|
362
367
|
flow_guidance: buildProtocolFlowGuidance({ state: nextState, latestRun: runDiagnostics.latest_run, queueStatus: "ready" })
|
|
363
368
|
};
|