diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 9d2c8f0..4f423e5 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cys", - "version": "0.6.23", + "version": "0.6.24", "description": "Development methodology skills with parallel plan execution: design, plan, run, check, ship. Named after the author's twin daughters, Cielo y Sophia.", "author": { "name": "Christian Bacilio" }, "repository": "https://github.com/bacsystem/parallel-plan-executor", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 615f034..a9982e9 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cys", - "version": "0.6.23", + "version": "0.6.24", "description": "Development methodology skills for design, plan, check, and ship — parallel plan execution (cys:run) is Claude Code only for now.", "author": { "name": "Christian Bacilio" }, "repository": "https://github.com/bacsystem/parallel-plan-executor", diff --git a/CHANGELOG.md b/CHANGELOG.md index 56286eb..08ce6d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.6.24 — 2026-07-24 + +Added: + +- `bin/parse-plan.js` and `bin/plan-remainder.js` now report + `parallelWidth` (via the existing `computeParallelWidth`, 0.6.18) in + their JSON output. +- `/cys:run-plan` and `/cys:flow` now offer to set `maxConcurrency` + automatically when the parsed plan's `parallelWidth` exceeds 6, + explaining that the plan can run that many tasks at once (worktrees + + subagents + merges in parallel). Narrower plans are never asked. The + chosen value flows through to the `Workflow` launch args in both + commands. + + `runDag`'s `maxConcurrency` cap has existed since 0.6.15, but neither + command ever asked about it or passed it through — using it required + bypassing the command and invoking the `Workflow` tool by hand. Both + READMEs updated. + ## 0.6.23 — 2026-07-22 Fixed: diff --git a/README.es.md b/README.es.md index 2b1e925..26580bd 100644 --- a/README.es.md +++ b/README.es.md @@ -491,7 +491,9 @@ node bin/parse-plan.js /ruta/a/tu-plan.md > /tmp/plan-graph.json DAG. La tool `Workflow` de Claude Code ya encola las llamadas a `agent()` que exceden su propio tope de `min(16, cores-2)`, así que esto sirve sobre todo para ir *más abajo* de ese default — por ejemplo, para evitar muchos worktrees locales simultáneos en tu propia -máquina cuando un plan tiene una capa ancha de tareas independientes. +máquina cuando un plan tiene una capa ancha de tareas independientes. `/run-plan` y +`/cys:flow` te ofrecen configurarlo automáticamente cuando el ancho de paralelismo +inferido del plan supera 6 — no hace falta calcularlo a mano. ### El comando `/run-plan` diff --git a/README.md b/README.md index c5e9635..cd334ea 100644 --- a/README.md +++ b/README.md @@ -457,7 +457,8 @@ node bin/parse-plan.js /path/to/your-plan.md > /tmp/plan-graph.json Claude Code `Workflow` tool already queues excess `agent()` calls beyond its own `min(16, cores-2)` cap, so this is mainly useful to go *lower* than that — e.g. to avoid many simultaneous local git worktrees on your own machine for a plan with a wide layer of -independent tasks. +independent tasks. `/run-plan` and `/cys:flow` offer to set it for you when the parsed +plan's inferred parallel width exceeds 6 — you don't need to compute this by hand. ### The `/run-plan` slash command diff --git a/bin/parse-plan.js b/bin/parse-plan.js index 0fa3d72..eea5d9e 100644 --- a/bin/parse-plan.js +++ b/bin/parse-plan.js @@ -1,7 +1,7 @@ #!/usr/bin/env node import { readFileSync } from 'node:fs'; import { parsePlanWithDiagnostics } from '../src/plan-parser.js'; -import { buildGraphWithDiagnostics } from '../src/graph-builder.js'; +import { buildGraphWithDiagnostics, computeParallelWidth } from '../src/graph-builder.js'; const [, , planPath] = process.argv; if (!planPath) { @@ -19,4 +19,4 @@ for (const warning of warnings) { console.error(`WARNING: ${warning}`); } -console.log(JSON.stringify({ tasks, graph, warnings }, null, 2)); +console.log(JSON.stringify({ tasks, graph, warnings, parallelWidth: computeParallelWidth(graph) }, null, 2)); diff --git a/bin/plan-remainder.js b/bin/plan-remainder.js index f40222a..9e57517 100644 --- a/bin/plan-remainder.js +++ b/bin/plan-remainder.js @@ -2,7 +2,7 @@ import { readFileSync, realpathSync } from 'node:fs'; import { resolve } from 'node:path'; import { parsePlanWithDiagnostics } from '../src/plan-parser.js'; -import { buildGraphWithDiagnostics } from '../src/graph-builder.js'; +import { buildGraphWithDiagnostics, computeParallelWidth } from '../src/graph-builder.js'; const [, , planPath, stateJsonPath] = process.argv; if (!planPath || !stateJsonPath) { @@ -63,4 +63,8 @@ const warnings = [...parseWarnings, ...graphWarnings]; for (const warning of warnings) { console.error(`WARNING: ${warning}`); } -console.log(JSON.stringify({ tasks: remainingTasks, graph: remainingGraph, warnings, allDone }, null, 2)); +console.log(JSON.stringify( + { tasks: remainingTasks, graph: remainingGraph, warnings, allDone, parallelWidth: computeParallelWidth(remainingGraph) }, + null, + 2 +)); diff --git a/commands/flow.md b/commands/flow.md index e11262f..d0f1b73 100644 --- a/commands/flow.md +++ b/commands/flow.md @@ -75,11 +75,18 @@ REPO = `${CLAUDE_PLUGIN_ROOT}` "I authorize merging task-1 through task-N into feature/x"). Never fabricate it; a bare "yes" is not enough — they name the branches. Pass their words verbatim as `args.mergeAuthorization`. + - **Only if `parallelWidth` (from step 6's JSON) is greater than 6**: mention + that this plan can run up to that many tasks at once (worktrees + + subagents + merges all in parallel), and ask whether to cap it with + `maxConcurrency` — a positive integer, or leave unlimited (the + default). Don't ask for narrower plans; a low `parallelWidth` never + benefits from capping. 8. **Summarize and confirm**: plan path, repo, task count, parallelism - the graph shows, integration branch, PR settings, authorization text. - Re-check the working tree is still clean; if the integration branch - already exists, ask whether to continue on it or pick another name. + the graph shows, integration branch, PR settings, `maxConcurrency` if + set, authorization text. Re-check the working tree is still clean; if + the integration branch already exists, ask whether to continue on it + or pick another name. 9. **Create the integration branch if it doesn't exist**: run `git -C show-ref --verify --quiet @@ -107,8 +114,8 @@ REPO = `${CLAUDE_PLUGIN_ROOT}` 11. **Launch** the `Workflow` tool with: - `scriptPath`: `REPO/workflows/parallel-plan-executor.js` - `args`: `{ tasks, graph, planPath, repoPath, integrationBranch, - executorPath: REPO, openPr, pr, mergeAuthorization }` (omit the - optional ones not provided). + executorPath: REPO, openPr, pr, mergeAuthorization, maxConcurrency }` + (omit the optional ones not provided). 12. **After launching**: tell the user it runs in the background, that they can ask "how's the workflow going?" or open `/workflows`, and diff --git a/commands/run-plan.md b/commands/run-plan.md index 6c4b835..f80ba85 100644 --- a/commands/run-plan.md +++ b/commands/run-plan.md @@ -76,10 +76,15 @@ REPO = `${CLAUDE_PLUGIN_ROOT}` the branches themselves. Pass their words verbatim as `args.mergeAuthorization`. If they decline to give one, proceed without it and mention that some merges may then need authorizing individually mid-run. + - **Only if `parallelWidth` (from step 4's JSON) is greater than 6**: mention that + this plan can run up to that many tasks at once (worktrees + subagents + merges + all in parallel), and ask whether to cap it with `maxConcurrency` — a positive + integer, or leave unlimited (the default). Don't ask for narrower plans; a low + `parallelWidth` never benefits from capping. 6. **Summarize before launching**: plan path, repo, task count, integration branch, - openPr/PR settings, and confirm the authorization text with the user. This is a real - run against their repo — don't skip the confirmation. + openPr/PR settings, `maxConcurrency` if set, and confirm the authorization text with + the user. This is a real run against their repo — don't skip the confirmation. 7. **Create the integration branch if it doesn't exist** (skip if `allDone` was `true` — the branch already has everything merged on it): run @@ -107,10 +112,10 @@ REPO = `${CLAUDE_PLUGIN_ROOT}` 9. **Launch**: invoke the `Workflow` tool with: - `scriptPath`: `/workflows/parallel-plan-executor.js` - - `args`: if `allDone` was `true`, `{ tasks: [], graph: {}, planPath, repoPath, integrationBranch, executorPath: , finishOnly: true, openPr, pr }` (no `mergeAuthorization` — nothing merges in this mode). Otherwise, - `{ tasks, graph, planPath, repoPath, integrationBranch, executorPath: , openPr, pr, mergeAuthorization }` + - `args`: if `allDone` was `true`, `{ tasks: [], graph: {}, planPath, repoPath, integrationBranch, executorPath: , finishOnly: true, openPr, pr }` (no `mergeAuthorization`/`maxConcurrency` — nothing runs in this mode). Otherwise, + `{ tasks, graph, planPath, repoPath, integrationBranch, executorPath: , openPr, pr, mergeAuthorization, maxConcurrency }` (executorPath is REPO — the workflow invokes REPO/bin scripts by exact path; - omit `openPr`/`pr`/`mergeAuthorization` if not provided) + omit `openPr`/`pr`/`mergeAuthorization`/`maxConcurrency` if not provided) 10. **After launching**: tell the user it's running in the background, mention they can ask "how's the workflow going?" any time or open `/workflows`, and that you'll report diff --git a/gemini-extension.json b/gemini-extension.json index 285139d..3d7fc08 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "cys", - "version": "0.6.23", + "version": "0.6.24", "description": "Development methodology skills for design, plan, check, and ship — parallel plan execution (cys:run) is Claude Code only for now.", "repository": "https://github.com/bacsystem/parallel-plan-executor", "license": "MIT" diff --git a/package.json b/package.json index d1a7c7f..724e369 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parallel-plan-executor", - "version": "0.6.23", + "version": "0.6.24", "author": "Christian Bacilio", "private": true, "type": "module", diff --git a/tests/parse-plan-cli.test.js b/tests/parse-plan-cli.test.js index 27888b4..7b003b2 100644 --- a/tests/parse-plan-cli.test.js +++ b/tests/parse-plan-cli.test.js @@ -31,6 +31,15 @@ test('CLI exits non-zero with a usage message when no path is given', () => { assert.throws(() => execFileSync('node', [cliPath], { encoding: 'utf8' })); }); +test('el JSON incluye parallelWidth, para que los comandos decidan si vale la pena ofrecer maxConcurrency', () => { + const output = execFileSync('node', [cliPath, fixturePath], { encoding: 'utf8' }); + const parsed = JSON.parse(output); + + // fixtures/sample-plan.md: tasks 1 y 2 son independientes (capa 0, ancho 2), la tarea + // 3 depende de ambas (capa 1, ancho 1) — el ancho máximo del plan es 2. + assert.equal(parsed.parallelWidth, 2); +}); + test('el comando publicado en examples/README.md imprime el grafo que ese README promete', () => { // examples/README.md muestra `node bin/parse-plan.js examples/hello-parallel/plan.md` // como el primer contacto de un dev nuevo con cys — si el CLI cambia de forma diff --git a/tests/plan-remainder.test.js b/tests/plan-remainder.test.js index 23b9dde..1b2a27c 100644 --- a/tests/plan-remainder.test.js +++ b/tests/plan-remainder.test.js @@ -185,6 +185,22 @@ test('allDone es false mientras quede algo pendiente o fallido', () => { assert.equal(result.allDone, false); }); +test('el JSON incluye parallelWidth del grafo remanente, para que los comandos decidan si vale la pena ofrecer maxConcurrency al resumir', () => { + const { dir, planPath } = makeFixtures(); + const statePath = writeState(dir, planPath, { + 1: { status: 'done' }, + 2: { status: 'failed' }, + 3: { status: 'pending' }, + }); + + const stdout = execFileSync('node', [cli, planPath, statePath], { encoding: 'utf8' }); + const result = JSON.parse(stdout); + + // Remanente: { 2: [], 3: [2] } — 2 no tiene dependencias pendientes, 3 depende de 2: + // ancho máximo 1 (nunca corren al mismo tiempo). + assert.equal(result.parallelWidth, 1); +}); + test('falla ruidosamente sin args', () => { assert.throws(() => execFileSync('node', [cli], { encoding: 'utf8', stdio: 'pipe' })); }); diff --git a/tests/skills.test.js b/tests/skills.test.js index 303a67b..cfe43cc 100644 --- a/tests/skills.test.js +++ b/tests/skills.test.js @@ -100,6 +100,21 @@ test('los comandos detectan .cys/state.json de una corrida interrumpida (Fase 4b ); }); +test('los comandos ofrecen maxConcurrency cuando el plan es ancho, y lo pasan en el launch (hallazgo: runDag lo soporta desde 0.6.15 pero ningún comando lo preguntaba ni lo pasaba)', () => { + const flow = readFileSync(path.join(root, 'commands', 'flow.md'), 'utf8'); + const runPlan = readFileSync(path.join(root, 'commands', 'run-plan.md'), 'utf8'); + for (const [name, content] of [['flow.md', flow], ['run-plan.md', runPlan]]) { + assert.ok( + content.includes('parallelWidth') && content.includes('maxConcurrency'), + `commands/${name}: debe leer parallelWidth del JSON parseado y decidir si ofrecer maxConcurrency` + ); + assert.ok( + content.includes('mergeAuthorization, maxConcurrency') || content.includes('mergeAuthorization,\n maxConcurrency'), + `commands/${name}: maxConcurrency debe llegar hasta los args del launch, no quedar solo preguntado` + ); + } +}); + test('run-plan.md maneja allDone lanzando con finishOnly en vez de fallar por tasks vacío (final review, hallazgo Important #2)', () => { const runPlan = readFileSync(path.join(root, 'commands', 'run-plan.md'), 'utf8'); assert.ok(