From 651e79beb3e9f333901b88242c3723079669e7ff Mon Sep 17 00:00:00 2001 From: Bacsystem Solutions EIRL Date: Wed, 22 Jul 2026 16:33:42 -0500 Subject: [PATCH 1/2] fix(parser): warn when a task has no **Files:** section at all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Twin of the 0.6.22 **Interfaces:** fix, same root cause: extractSection flattened "missing" and "empty" to ''. A header typo (**File:**) silently removed the task from file-based serialization — overlapping tasks ran in parallel and hit avoidable merge conflicts, with a misleading symptom (the missing dependency looked like a Consumes/Produces problem). parseFiles now uses extractOptionalSection and warns. extractSection is now unused (both parseFiles and parseInterfaces use the optional variant) and has been removed. --- skills/plan/SKILL.md | 12 +++++---- src/plan-parser.js | 31 ++++++++++++++-------- tests/plan-parser.test.js | 55 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/skills/plan/SKILL.md b/skills/plan/SKILL.md index 91c6e88..02d0f8c 100644 --- a/skills/plan/SKILL.md +++ b/skills/plan/SKILL.md @@ -62,11 +62,13 @@ 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. 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. + independent. Both the `**Files:**` and `**Interfaces:**` headers + themselves must always be present, even for a task with an empty list + — omitting either entirely (e.g. a typo like `**Interface:**` or + `**File:**`) silently drops that task from the corresponding + inference (symbol-based dependencies, or file-based serialization); + the parser warns when a header is missing, but write the section + 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 01c77cb..4056c18 100644 --- a/src/plan-parser.js +++ b/src/plan-parser.js @@ -15,7 +15,7 @@ export function parsePlanWithDiagnostics(planText) { tasks.push({ id, title, - files: parseFiles(body), + files: parseFiles(body, id, warnings), interfaces: parseInterfaces(body, id, warnings), }); } @@ -35,21 +35,30 @@ const SECTION_END = '(?=\\n\\*\\*[A-Z][^*\\n]*:\\*\\*(?:\\n|$)|\\n- \\[ \\]|$)'; const FILES_SECTION_RE = new RegExp(`\\*\\*Files:\\*\\*\\n([\\s\\S]*?)${SECTION_END}`); const INTERFACES_SECTION_RE = new RegExp(`\\*\\*Interfaces:\\*\\*\\n([\\s\\S]*?)${SECTION_END}`); -function extractSection(body, sectionRe) { - const match = body.match(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". +// Devuelve null cuando la sección falta por completo (a diferencia de un simple ''), +// para que parseFiles/parseInterfaces puedan distinguir "no hay sección" de "la +// sección está pero vacía" y avisar solo en el primer caso. function extractOptionalSection(body, sectionRe) { const match = body.match(sectionRe); return match ? match[1] : null; } -function parseFiles(body) { - const section = extractSection(body, FILES_SECTION_RE); +function parseFiles(body, taskId, warnings) { + const section = extractOptionalSection(body, FILES_SECTION_RE); + if (section === null) { + // Mismo mecanismo que el fix de **Interfaces:** (0.6.22): un typo en el header + // (**File:**, **Archivos:**) dejaba la tarea sin archivos declarados en silencio, + // volviéndola invisible para la serialización por archivos (fileOwner) — dos + // tareas compartiendo un archivo corrían en paralelo y chocaban en el merge. + // Warning, no error: una tarea sin archivos puede ser legítima (p. ej. solo docs + // fuera del repo), aunque en planes de cys:plan la sección siempre existe. + warnings.push( + `Task ${taskId}: no **Files:** section found — file-based serialization ` + + `cannot see this task; if it truly touches no files, that's fine, but a ` + + `header typo here means overlapping tasks will run in parallel and conflict` + ); + return { create: [], modify: [], test: [] }; + } const files = { create: [], modify: [], test: [] }; for (const line of section.split('\n')) { const m = line.match(/^-\s*(Create|Modify|Test):\s*`([^`]+)`/); diff --git a/tests/plan-parser.test.js b/tests/plan-parser.test.js index 242ee1b..2d9ef08 100644 --- a/tests/plan-parser.test.js +++ b/tests/plan-parser.test.js @@ -184,6 +184,9 @@ test('una tarea sin sección **Interfaces:** genera un warning (hallazgo de revi const text = [ '### Task 1: Producer fine', '', + '**Files:**', + '- Create: `a.js`', + '', '**Interfaces:**', '- Produces: `foo`', '', @@ -207,6 +210,40 @@ test('una tarea sin sección **Interfaces:** genera un warning (hallazgo de revi ); }); +test('una tarea sin sección **Files:** genera un warning (mismo mecanismo que el fix de **Interfaces:** en 0.6.22: extractSection aplanaba ausente y vacío a "", así que un typo como **File:** borraba la serialización por archivos de la tarea sin aviso)', () => { + const text = [ + '### Task 1: Header typo', + '**File:**', + '- Create: `a.js`', + '**Interfaces:**', + '- Consumes: None', + '- Produces: None', + ].join('\n'); + + const { tasks, warnings } = parsePlanWithDiagnostics(text); + + assert.deepEqual(tasks[0].files, { create: [], modify: [], test: [] }); + assert.match( + warnings.join('\n'), + /Task 1.*no .*Files:.*section/i, + 'la ausencia de **Files:** debe avisar — sin ella la tarea es invisible para la serialización por archivos' + ); +}); + +test('una tarea CON sección **Files:** correcta no gana warnings nuevos (regresión)', () => { + const text = [ + '### Task 1: Fine', + '**Files:**', + '- Create: `a.js`', + '**Interfaces:**', + '- Consumes: None', + '- Produces: None', + ].join('\n'); + + const { warnings } = parsePlanWithDiagnostics(text); + assert.equal(warnings.filter((w) => /Files/.test(w)).length, 0); +}); + 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'), @@ -220,6 +257,9 @@ test('warns cuando una línea Consumes/Produces con contenido no tiene ningún b const text = [ '### Task 1: A', '', + '**Files:**', + '- Create: `a.js`', + '', '**Interfaces:**', '- Produces: makeWidget() factory', '', @@ -236,6 +276,9 @@ test('un símbolo de 1 carácter se descarta con un warning que dice la causa RE const text = [ '### Task 1: Uses short symbol', '', + '**Files:**', + '- Create: `a.js`', + '', '**Interfaces:**', '- Consumes: `x`', '', @@ -257,6 +300,9 @@ test('no warnea por "None" ni por líneas correctamente backtickeadas', () => { const text = [ '### Task 1: A', '', + '**Files:**', + '- Create: `a.js`', + '', '**Interfaces:**', '- Consumes: None', '- Produces: None (pure scaffolding)', @@ -265,6 +311,9 @@ test('no warnea por "None" ni por líneas correctamente backtickeadas', () => { '', '### Task 2: B', '', + '**Files:**', + '- Create: `b.js`', + '', '**Interfaces:**', '- Consumes: `makeA()`', '- Produces: `makeB()`', @@ -283,6 +332,9 @@ test('warns cuando Consumes/Produces queda vacío después de los dos puntos, co const text = [ '### Task 1: A', '', + '**Files:**', + '- Create: `a.js`', + '', '**Interfaces:**', '- Consumes:', ' - `some.Symbol`', @@ -302,6 +354,9 @@ test('warns (sin el hint de nested-list) cuando queda vacío sin un sub-bullet s const text = [ '### Task 1: A', '', + '**Files:**', + '- Create: `a.js`', + '', '**Interfaces:**', '- Consumes:', '- Produces: `makeB()`', From 6034dc191d8b388efe73626ed88772612195afc9 Mon Sep 17 00:00:00 2001 From: Bacsystem Solutions EIRL Date: Wed, 22 Jul 2026 16:37:54 -0500 Subject: [PATCH 2/2] chore(release): bump version to 0.6.23 --- .claude-plugin/plugin.json | 2 +- .cursor-plugin/plugin.json | 2 +- CHANGELOG.md | 17 +++++++++++++++++ gemini-extension.json | 2 +- package.json | 2 +- 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2c247ad..9d2c8f0 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cys", - "version": "0.6.22", + "version": "0.6.23", "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 9f989ee..615f034 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cys", - "version": "0.6.22", + "version": "0.6.23", "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 de5c73b..56286eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ 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.23 — 2026-07-22 + +Fixed: + +- `src/plan-parser.js`: twin of the 0.6.22 `**Interfaces:**` fix, same + root cause, other section — `parseFiles` still used the old + `extractSection`, which flattened "section missing" and "section + present but empty" to `''`. A header typo (`**File:**`, `**Archivos:**`) + silently removed the task from file-based serialization, so two tasks + sharing a file could run in parallel and hit an avoidable merge + conflict, with a misleading symptom (the missing dependency looked + like a Consumes/Produces problem, not a Files one). `parseFiles` now + uses `extractOptionalSection` (same as `parseInterfaces`) and warns + when the section is missing entirely. `extractSection` is now unused + and has been removed. `skills/plan/SKILL.md` extended to cover both + headers. + ## 0.6.22 — 2026-07-22 Fixed: diff --git a/gemini-extension.json b/gemini-extension.json index 4457a1b..285139d 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "cys", - "version": "0.6.22", + "version": "0.6.23", "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 90acf7f..d1a7c7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parallel-plan-executor", - "version": "0.6.22", + "version": "0.6.23", "author": "Christian Bacilio", "private": true, "type": "module",