feat(lanes): record which CLI tool drives a lane
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user