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.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",
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.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",
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
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.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"
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.21",
"version": "0.6.22",
"author": "Christian Bacilio",
"private": true,
"type": "module",
Expand Down
6 changes: 5 additions & 1 deletion skills/plan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 45 additions & 9 deletions src/plan-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] };
Expand Down Expand Up @@ -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
Expand All @@ -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++) {
Expand All @@ -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);
Expand Down
57 changes: 57 additions & 0 deletions tests/plan-parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
Loading