feat(lanes): drive runs from the pipeline template's own skill
A pipeline template may now declare the slash command that drives it (`skill`), so the choice of pipeline is the choice of workflow rather than just a picture on the dashboard: - `ccam ship-full "<req>"` runs the 16-node `ship-feature` template's skill (`/ccam:ship-feature-lane`) — through gates, QC and the senior gate to a published PR, watched until a human merges it. - `ccam ship-quick "<req>"` runs the 8-node `default` template's skill (`/ccam:ship-feature`) — in-repo feature work that stops at the gate. Each command puts the lane on its own template before spawning, so the dashboard renders the right node count from the first stage instead of after the skill self-corrects. `--print` shows the command, switches nothing and runs nothing. The prompt reaches `claude` as one argv element, never through a shell. The same field feeds the Workspace run console, which pre-fills its prompt with the lane's driving skill and never overwrites a prompt the user has already edited.
This commit is contained in:
@@ -542,3 +542,34 @@ describe("ccam lanes — inferred stage detection", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam ship-full / ccam ship-quick", () => {
|
||||
it("ship-quick prints the 8-stage pipeline's skill, ship-full the 16-stage one", async () => {
|
||||
const quick = await cli(["ship-quick", "add", "an", "export", "button", "--print"], LANE_DIR);
|
||||
assert.equal(quick.status, 0, quick.stderr);
|
||||
assert.match(quick.stdout, /claude "\/ccam:ship-feature add an export button"/);
|
||||
|
||||
const full = await cli(["ship-full", "add", "an", "export", "button", "--print"], LANE_DIR);
|
||||
assert.equal(full.status, 0, full.stderr);
|
||||
assert.match(full.stdout, /claude "\/ccam:ship-feature-lane add an export button"/);
|
||||
});
|
||||
|
||||
it("--print switches nothing: the lane keeps whatever template it was on", async () => {
|
||||
const before = await get(`/api/lanes`);
|
||||
const lane = before.body.lanes.find((l) => l.cwd === LANE_DIR);
|
||||
const r = await cli(["ship-full", "anything", "--print"], LANE_DIR);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
const after = await get(`/api/lanes`);
|
||||
assert.equal(after.body.lanes.find((l) => l.id === lane.id).pipeline, lane.pipeline);
|
||||
});
|
||||
|
||||
it("exits non-zero with the command's own usage when no requirement is given", async () => {
|
||||
const r = await cli(["ship-full"], LANE_DIR);
|
||||
assert.notEqual(r.status, 0);
|
||||
assert.match(r.stderr, /usage: ccam ship-full/);
|
||||
|
||||
const q = await cli(["ship-quick"], LANE_DIR);
|
||||
assert.notEqual(q.status, 0);
|
||||
assert.match(q.stderr, /usage: ccam ship-quick/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -231,6 +231,9 @@ describe("lanes lib", () => {
|
||||
assert.equal(p.pipeline_nodes.find((n) => n.id === "review").state, "current");
|
||||
assert.ok(p.progress > 0 && p.progress < 100);
|
||||
assert.equal(p.liveness, "idle");
|
||||
// The template's driving slash command rides along so the run console can
|
||||
// pre-fill its prompt with it.
|
||||
assert.equal(p.pipeline_skill, "/ccam:ship-feature");
|
||||
lanes.deleteLane(l.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Default feature pipeline",
|
||||
"skill": "/ccam:ship-feature",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "intake",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"id": "ship-feature",
|
||||
"name": "Ship feature (lane pipeline)",
|
||||
"skill": "/ccam:ship-feature-lane",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "intake",
|
||||
|
||||
@@ -490,6 +490,7 @@ function lanePayload(lane, ageSec = null) {
|
||||
return {
|
||||
...lane,
|
||||
pipeline_name: pipeline.name,
|
||||
pipeline_skill: pipeline.skill || "",
|
||||
pipeline_nodes: withDetected(nodeStates(pipeline, lane), pipeline, lane),
|
||||
progress: progressPct(pipeline, lane),
|
||||
stage_seconds: Number.isNaN(since)
|
||||
|
||||
@@ -30,6 +30,11 @@ function loadAll() {
|
||||
try {
|
||||
const doc = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
||||
if (!doc.id || !Array.isArray(doc.nodes) || !doc.nodes.length) continue;
|
||||
// The slash command that drives this pipeline. The Run console
|
||||
// pre-fills its prompt with it, so a lane on the 16-node template
|
||||
// starts the 16-step skill and one on the 8-node template the 8-step
|
||||
// one. Never executed by the dashboard — it is text in a prompt box.
|
||||
doc.skill = typeof doc.skill === "string" ? doc.skill : "";
|
||||
doc.nodes = doc.nodes.map((n) => ({
|
||||
id: n.id,
|
||||
label: n.label || n.id,
|
||||
|
||||
Reference in New Issue
Block a user