feat(lanes): record which CLI tool drives a lane

This commit is contained in:
2026-08-27 11:00:13 +07:00
parent 97839b1f97
commit a66331f008
4 changed files with 54 additions and 4 deletions
+21
View File
@@ -236,6 +236,27 @@ describe("lanes lib", () => {
assert.equal(p.pipeline_skill, "/ccam:ship-feature");
lanes.deleteLane(l.id);
});
it("createLane defaults tool to claude and accepts codex", () => {
const a = lanes.createLane({ cwd: "/tmp/tool-default" });
assert.equal(a.tool, "claude");
const b = lanes.createLane({ cwd: "/tmp/tool-codex", tool: "codex" });
assert.equal(b.tool, "codex");
});
it("createLane rejects an unknown tool", () => {
assert.throws(
() => lanes.createLane({ cwd: "/tmp/tool-bad", tool: "cursor" }),
(err) => err.code === "EBADTOOL"
);
});
it("updateLane never writes tool (immutable per lane)", () => {
const lane = lanes.createLane({ cwd: "/tmp/tool-immutable", tool: "codex" });
const after = lanes.updateLane(lane.id, { tool: "claude", title: "x" });
assert.equal(after.tool, "codex");
assert.equal(after.title, "x");
});
});
const { withLaneLock } = require("../lib/lane-lock");
+16
View File
@@ -486,6 +486,22 @@ try {
db.prepare("ALTER TABLE lanes ADD COLUMN slug TEXT").run();
}
// Migrate: which CLI drives this lane — `claude` (default) or `codex`. Additive
// with a default, so every existing lane keeps today's behavior. A lane's tool
// is immutable (never in PATCHABLE): the tmux pane, the run history and the
// binary probe all key off it, so flipping it under a live pane would describe
// a run that isn't what's actually attached.
try {
db.prepare("SELECT tool FROM lanes LIMIT 1").get();
} catch {
db.prepare("ALTER TABLE lanes ADD COLUMN tool TEXT NOT NULL DEFAULT 'claude'").run();
}
try {
db.prepare("SELECT tool FROM dashboard_runs LIMIT 1").get();
} catch {
db.prepare("ALTER TABLE dashboard_runs ADD COLUMN tool TEXT NOT NULL DEFAULT 'claude'").run();
}
// Migrate: label a dashboard run with the lane it was started through, so the
// Workspace page can show one lane's run history. Additive and nullable —
// POST /api/run keeps writing rows with no lane. One probe for the one column.
+4 -3
View File
@@ -25,11 +25,11 @@ const insertStmt = db.prepare(`
INSERT OR REPLACE INTO dashboard_runs (
id, session_id, mode, cwd, model, permission_mode, effort,
resume_session_id, prompt_preview, status, exit_code, started_at, ended_at,
lane_id
lane_id, tool
) VALUES (
@id, @session_id, @mode, @cwd, @model, @permission_mode, @effort,
@resume_session_id, @prompt_preview, @status, @exit_code, @started_at, @ended_at,
@lane_id
@lane_id, @tool
)
`);
@@ -44,7 +44,7 @@ const updateStmt = db.prepare(`
const RUN_COLUMNS = `id, session_id, mode, cwd, model, permission_mode, effort,
resume_session_id, prompt_preview, status, exit_code,
started_at, ended_at, lane_id`;
started_at, ended_at, lane_id, tool`;
const listStmt = db.prepare(`
SELECT ${RUN_COLUMNS}
@@ -89,6 +89,7 @@ function recordRun(handle) {
started_at: startedAt,
ended_at: endedAt,
lane_id: typeof handle.laneId === "number" ? handle.laneId : null,
tool: handle.tool === "codex" ? "codex" : "claude",
});
} catch {
/* persistence is best-effort */
+13 -1
View File
@@ -95,6 +95,14 @@ function validateKind(kind) {
}
}
const VALID_TOOLS = new Set(["claude", "codex"]);
function validateTool(tool) {
if (!VALID_TOOLS.has(tool)) {
throw Object.assign(new Error(`unknown tool: ${tool}`), { code: "EBADTOOL" });
}
}
/**
* `getPipeline` falls back to the default template for an unknown id — correct
* when READING (a lane must always render something), wrong when WRITING: a
@@ -139,15 +147,17 @@ function createLane({
source_repo = null,
base_branch = null,
slug = null,
tool = "claude",
} = {}) {
if (!cwd || typeof cwd !== "string" || !cwd.startsWith("/")) {
throw Object.assign(new Error("cwd must be an absolute path"), { code: "EBADCWD" });
}
validateKind(kind);
validatePipeline(pipeline);
validateTool(tool);
const info = db
.prepare(
"INSERT INTO lanes (title, cwd, branch, pipeline, kind, source_repo, base_branch, slug, stage_since) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
"INSERT INTO lanes (title, cwd, branch, pipeline, kind, source_repo, base_branch, slug, tool, stage_since) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
)
.run(
title,
@@ -158,6 +168,7 @@ function createLane({
source_repo,
base_branch,
slug,
tool,
nowIso()
);
return getLane(info.lastInsertRowid);
@@ -599,6 +610,7 @@ function purgeLaneSessions(laneId) {
module.exports = {
DEAD_SEC,
VALID_TOOLS,
createLane,
listLanes,
getLane,