Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parallel-plan-executor",
"version": "0.6.22",
"version": "0.6.23",
"author": "Christian Bacilio",
"private": true,
"type": "module",
Expand Down
12 changes: 7 additions & 5 deletions skills/plan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 20 additions & 11 deletions src/plan-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
}
Expand All @@ -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*`([^`]+)`/);
Expand Down
55 changes: 55 additions & 0 deletions tests/plan-parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`',
'',
Expand All @@ -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'),
Expand All @@ -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',
'',
Expand All @@ -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`',
'',
Expand All @@ -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)',
Expand All @@ -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()`',
Expand All @@ -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`',
Expand All @@ -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()`',
Expand Down
Loading