diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 7674b1f..2c247ad 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cys", - "version": "0.6.21", + "version": "0.6.22", "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 7a2820b..9f989ee 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cys", - "version": "0.6.21", + "version": "0.6.22", "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 b01efb9..de5c73b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ 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.22 — 2026-07-22 + +Fixed: + +- `src/plan-parser.js`: two diagnostic gaps found by an external review + with empirical reproduction (not static reading) — both let a + symbol-based dependency vanish from the plan's DAG without a useful + warning, so a task that should have been serialized could instead run + in parallel with its producer. + - A `- Consumes:`/`- Produces:` value with only a single-character + symbol between backticks (e.g. `` `x` ``) hit the anti-prose + `length > 1` filter and got the generic "no backtick-quoted symbols" + warning — false, the line has one. The warning now names the real + cause and cites the dropped symbol. + - A task missing the `**Interfaces:**` section entirely (typically a + header typo like `**Interface:**`) silently produced empty + `consumes`/`produces` with no warning at all — the single most + destructive parse miss and, until now, the only silent one. The + parser now warns and suggests writing `None` explicitly. + + Neither fix changes the graph a well-formed plan produces — both only + make the diagnostics tell the truth. `skills/plan/SKILL.md` now also + documents that the `**Interfaces:**` header itself must always be + present. + ## 0.6.21 — 2026-07-21 Added: diff --git a/gemini-extension.json b/gemini-extension.json index 0c2ae1d..4457a1b 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "cys", - "version": "0.6.21", + "version": "0.6.22", "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 8dde7c6..90acf7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parallel-plan-executor", - "version": "0.6.21", + "version": "0.6.22", "author": "Christian Bacilio", "private": true, "type": "module", diff --git a/skills/plan/SKILL.md b/skills/plan/SKILL.md index 50045f5..91c6e88 100644 --- a/skills/plan/SKILL.md +++ b/skills/plan/SKILL.md @@ -62,7 +62,11 @@ Hard rules (the executor's parser depends on them): - **One entry per line** in Consumes/Produces — a value wrapping onto a second line is silently lost by the parser. - `Consumes: None` means an empty list; write it when a task is - independent. + independent. The `**Interfaces:**` header itself must always be present + — omitting it entirely (e.g. a typo like `**Interface:**`) silently + drops every symbol-based dependency for that task; the parser warns + when the header is missing, but write `None` explicitly rather than + relying on that warning. - Two tasks touching the same file are automatically serialized by the executor; design task boundaries so files are disjoint whenever possible — disjoint tasks run in parallel. diff --git a/src/plan-parser.js b/src/plan-parser.js index 26b01ca..01c77cb 100644 --- a/src/plan-parser.js +++ b/src/plan-parser.js @@ -40,6 +40,14 @@ function extractSection(body, sectionRe) { return match ? match[1] : ''; } +// A diferencia de extractSection (que aplana "sin match" y "match vacío" a ''), esta +// devuelve null cuando la sección falta por completo, para que parseInterfaces pueda +// distinguir "no hay sección" de "la sección está pero vacía". +function extractOptionalSection(body, sectionRe) { + const match = body.match(sectionRe); + return match ? match[1] : null; +} + function parseFiles(body) { const section = extractSection(body, FILES_SECTION_RE); const files = { create: [], modify: [], test: [] }; @@ -67,6 +75,7 @@ const IDENTIFIER_RE = /[A-Za-z_][A-Za-z0-9_.]*/g; function extractSymbols(line) { const symbols = []; + const droppedShort = []; for (const [, rawSpan] of line.matchAll(BACKTICK_SPAN_RE)) { // Drop parenthesized call-argument lists first (e.g. the "name" in // `createWidget(name)`) so parameter names aren't mistaken for separate @@ -80,16 +89,31 @@ function extractSymbols(line) { } for (const [identifier] of span.matchAll(IDENTIFIER_RE)) { if (identifier.length > 1) symbols.push(identifier); + else droppedShort.push(identifier); } } - return symbols; + return { symbols, droppedShort }; } // "None"/"N/A"/"nothing" al comienzo del valor significa deliberadamente vacío. const NO_SYMBOLS_RE = /^(none|n\/a|nothing)\b/i; function parseInterfaces(body, taskId, warnings) { - const section = extractSection(body, INTERFACES_SECTION_RE); + const section = extractOptionalSection(body, INTERFACES_SECTION_RE); + if (section === null) { + // La sección entera falta — típicamente un typo en el header (**Interface:**, + // **Interfaz:**) o una tarea escrita a mano sin ella. Es el caso más destructivo + // (borra TODAS las dependencias por símbolo de la tarea) y era el único que no + // avisaba: un valor vacío sí warns, una línea sin backticks sí warns. Puede ser + // legítimo (tarea sin interfaces) — por eso es warning, no error, igual que el + // consumidor huérfano. Hallazgo de revisión externa 2026-07-22. + warnings.push( + `Task ${taskId}: no **Interfaces:** section found — symbol-based dependencies ` + + `for this task cannot be inferred; if the task has none, write ` + + `"- Consumes: None" / "- Produces: None" explicitly` + ); + return { consumes: [], produces: [] }; + } const lines = section.split('\n'); const interfaces = { consumes: [], produces: [] }; for (let i = 0; i < lines.length; i++) { @@ -116,14 +140,26 @@ function parseInterfaces(body, taskId, warnings) { ); continue; } - const symbols = extractSymbols(value); + const { symbols, droppedShort } = extractSymbols(value); if (symbols.length === 0) { - // La línea tiene contenido pero ningún backtick: se ignora, pero avisando — una - // dependencia perdida en silencio es justo lo que este parser debe evitar. - warnings.push( - `Task ${taskId}: ${consumes ? 'Consumes' : 'Produces'} line has no backtick-quoted ` + - `symbols and was ignored: "${value}"` - ); + const kind = consumes ? 'Consumes' : 'Produces'; + if (droppedShort.length > 0) { + // La causa real: había símbolos entre backticks, pero de 1 carácter — el + // filtro anti-prosa (length > 1) los descarta a propósito. Decir "no + // backtick-quoted symbols" mandaba a quien nombró su símbolo `x` a buscar + // el problema equivocado. Hallazgo de revisión externa 2026-07-22. + warnings.push( + `Task ${taskId}: ${kind} single-character symbol(s) ` + + `${droppedShort.map((s) => `\`${s}\``).join(', ')} were ignored ` + + `(too likely to be prose) — rename to 2+ characters if it's a real symbol: "${value}"` + ); + } else { + // La línea tiene contenido pero ningún backtick: se ignora, pero avisando — una + // dependencia perdida en silencio es justo lo que este parser debe evitar. + warnings.push( + `Task ${taskId}: ${kind} line has no backtick-quoted symbols and was ignored: "${value}"` + ); + } continue; } (consumes ? interfaces.consumes : interfaces.produces).push(...symbols); diff --git a/tests/plan-parser.test.js b/tests/plan-parser.test.js index fc5e046..242ee1b 100644 --- a/tests/plan-parser.test.js +++ b/tests/plan-parser.test.js @@ -180,6 +180,42 @@ test('una anotación bold con texto detrás NO termina la sección; un header so 'la anotación inline no debe cortar la sección (b.js se perdía en silencio)'); }); +test('una tarea sin sección **Interfaces:** genera un warning (hallazgo de revisión externa 2026-07-22: un typo en el header — p. ej. **Interface:** — borraba todas las dependencias por símbolo de la tarea sin un solo aviso, el único caso del parser que callaba)', () => { + const text = [ + '### Task 1: Producer fine', + '', + '**Interfaces:**', + '- Produces: `foo`', + '', + '- [ ] **Step 1: x**', + '', + '### Task 2: Section missing entirely', + '', + '**Files:**', + '- Create: `b.js`', + '', + '- [ ] **Step 1: x**', + ].join('\n'); + const { tasks, warnings } = parsePlanWithDiagnostics(text); + assert.deepEqual(tasks[1].interfaces, { consumes: [], produces: [] }); + assert.equal(warnings.length, 1); + assert.match(warnings[0], /Task 2/); + assert.match( + warnings[0], + /no .*Interfaces:.*section/i, + 'la ausencia total de la sección debe avisar — es el caso más destructivo y era el único silencioso' + ); +}); + +test('el plan de ejemplo hello-parallel no gana warnings nuevos (regresión: es un plan bien formado)', () => { + const examplePlan = readFileSync( + path.join(here, '../examples/hello-parallel/plan.md'), + 'utf8' + ); + const { warnings } = parsePlanWithDiagnostics(examplePlan); + assert.deepEqual(warnings, []); +}); + test('warns cuando una línea Consumes/Produces con contenido no tiene ningún backtick', () => { const text = [ '### Task 1: A', @@ -196,6 +232,27 @@ test('warns cuando una línea Consumes/Produces con contenido no tiene ningún b assert.match(warnings[0], /no backtick/); }); +test('un símbolo de 1 carácter se descarta con un warning que dice la causa REAL (hallazgo de revisión externa 2026-07-22: el warning genérico "no backtick-quoted symbols" mentía — la línea sí los tenía)', () => { + const text = [ + '### Task 1: Uses short symbol', + '', + '**Interfaces:**', + '- Consumes: `x`', + '', + '- [ ] **Step 1: x**', + ].join('\n'); + const { tasks, warnings } = parsePlanWithDiagnostics(text); + assert.deepEqual(tasks[0].interfaces.consumes, [], 'el filtro se mantiene: 1 char no es símbolo'); + assert.equal(warnings.length, 1); + assert.match(warnings[0], /Task 1/); + assert.match( + warnings[0], + /single-character/i, + 'el warning debe nombrar la causa real (símbolo de 1 carácter descartado), no "no backtick-quoted symbols"' + ); + assert.match(warnings[0], /`x`/, 'debe citar el símbolo descartado para que sea encontrable'); +}); + test('no warnea por "None" ni por líneas correctamente backtickeadas', () => { const text = [ '### Task 1: A',