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");
|
assert.equal(p.pipeline_skill, "/ccam:ship-feature");
|
||||||
lanes.deleteLane(l.id);
|
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");
|
const { withLaneLock } = require("../lib/lane-lock");
|
||||||
|
|||||||
@@ -486,6 +486,22 @@ try {
|
|||||||
db.prepare("ALTER TABLE lanes ADD COLUMN slug TEXT").run();
|
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
|
// 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 —
|
// 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.
|
// 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 (
|
INSERT OR REPLACE INTO dashboard_runs (
|
||||||
id, session_id, mode, cwd, model, permission_mode, effort,
|
id, session_id, mode, cwd, model, permission_mode, effort,
|
||||||
resume_session_id, prompt_preview, status, exit_code, started_at, ended_at,
|
resume_session_id, prompt_preview, status, exit_code, started_at, ended_at,
|
||||||
lane_id
|
lane_id, tool
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@id, @session_id, @mode, @cwd, @model, @permission_mode, @effort,
|
@id, @session_id, @mode, @cwd, @model, @permission_mode, @effort,
|
||||||
@resume_session_id, @prompt_preview, @status, @exit_code, @started_at, @ended_at,
|
@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,
|
const RUN_COLUMNS = `id, session_id, mode, cwd, model, permission_mode, effort,
|
||||||
resume_session_id, prompt_preview, status, exit_code,
|
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(`
|
const listStmt = db.prepare(`
|
||||||
SELECT ${RUN_COLUMNS}
|
SELECT ${RUN_COLUMNS}
|
||||||
@@ -89,6 +89,7 @@ function recordRun(handle) {
|
|||||||
started_at: startedAt,
|
started_at: startedAt,
|
||||||
ended_at: endedAt,
|
ended_at: endedAt,
|
||||||
lane_id: typeof handle.laneId === "number" ? handle.laneId : null,
|
lane_id: typeof handle.laneId === "number" ? handle.laneId : null,
|
||||||
|
tool: handle.tool === "codex" ? "codex" : "claude",
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
/* persistence is best-effort */
|
/* 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
|
* `getPipeline` falls back to the default template for an unknown id — correct
|
||||||
* when READING (a lane must always render something), wrong when WRITING: a
|
* when READING (a lane must always render something), wrong when WRITING: a
|
||||||
@@ -139,15 +147,17 @@ function createLane({
|
|||||||
source_repo = null,
|
source_repo = null,
|
||||||
base_branch = null,
|
base_branch = null,
|
||||||
slug = null,
|
slug = null,
|
||||||
|
tool = "claude",
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (!cwd || typeof cwd !== "string" || !cwd.startsWith("/")) {
|
if (!cwd || typeof cwd !== "string" || !cwd.startsWith("/")) {
|
||||||
throw Object.assign(new Error("cwd must be an absolute path"), { code: "EBADCWD" });
|
throw Object.assign(new Error("cwd must be an absolute path"), { code: "EBADCWD" });
|
||||||
}
|
}
|
||||||
validateKind(kind);
|
validateKind(kind);
|
||||||
validatePipeline(pipeline);
|
validatePipeline(pipeline);
|
||||||
|
validateTool(tool);
|
||||||
const info = db
|
const info = db
|
||||||
.prepare(
|
.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(
|
.run(
|
||||||
title,
|
title,
|
||||||
@@ -158,6 +168,7 @@ function createLane({
|
|||||||
source_repo,
|
source_repo,
|
||||||
base_branch,
|
base_branch,
|
||||||
slug,
|
slug,
|
||||||
|
tool,
|
||||||
nowIso()
|
nowIso()
|
||||||
);
|
);
|
||||||
return getLane(info.lastInsertRowid);
|
return getLane(info.lastInsertRowid);
|
||||||
@@ -599,6 +610,7 @@ function purgeLaneSessions(laneId) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
DEAD_SEC,
|
DEAD_SEC,
|
||||||
|
VALID_TOOLS,
|
||||||
createLane,
|
createLane,
|
||||||
listLanes,
|
listLanes,
|
||||||
getLane,
|
getLane,
|
||||||
|
|||||||
Reference in New Issue
Block a user