diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 63f7942..7674b1f 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "cys",
- "version": "0.6.20",
+ "version": "0.6.21",
"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 b404b09..7a2820b 100644
--- a/.cursor-plugin/plugin.json
+++ b/.cursor-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "cys",
- "version": "0.6.20",
+ "version": "0.6.21",
"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 fe19d83..b01efb9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,59 @@ 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.21 — 2026-07-21
+
+Added:
+
+- `requestWriteState()`/`appendLedger()` now log how many pending
+ writes/lines got coalesced into a single agent, so batching is visible
+ in the run's own log output instead of only inferable from agent counts.
+- `cys:plan`'s self-review checklist gained two checks caught only by the
+ executor's adversarial reviewer in a real `bs-inventory` pilot
+ (2026-07-20/21), not by writing the plan itself: caller-cardinality
+ (trace every real call site's multiplicity, not just your own task's
+ test case) and spec-field literalness (re-read and grep the spec's
+ exact field mapping instead of writing it from memory).
+
+Fixed:
+
+- `bin/review-package.js`: raised `execFileSync`'s `maxBuffer` to 1GB —
+ the 1MB Node default threw `ENOBUFS`/maxBuffer-exceeded on a real diff
+ package spanning many files (207-file MUI removal in the
+ `bs-inventory` pilot's Task 13).
+- `writeState()`/`deleteState()`/`appendLedger()` now instruct the agent
+ to verify content against real repo state (git log, git merge-base,
+ task reports) before writing, with a "report the discrepancy instead"
+ escape hatch — the previous "frame it as already-verified" wording
+ kept getting flagged as Instruction Poisoning by the safety classifier
+ across two real pilot runs.
+- The state-write agent now self-corrects stale-but-genuine entries
+ (queued-then-overtaken-by-real-progress) instead of only refusing and
+ leaving `state.json` frozen at its first snapshot for the rest of the
+ run; refusal is reserved for entries that look fabricated outright.
+- `requestWriteState()`/`appendLedger()` now coalesce concurrent calls
+ into one agent instead of dispatching one `[state]`/`[ledger]` agent
+ per transition/line — real pilot data showed bookkeeping agents alone
+ accounting for 35 of 77 dispatched agents in one run.
+- `runTask` now fast-exits tasks whose branch is already merged into the
+ integration branch (two read-only git checks, no re-implementation,
+ review, or merge) instead of re-dispatching already-finished work on
+ every resume — one pilot run re-invoked three tasks 3-5 times each,
+ costing 63 review agents and 57 merge agents on a 10-task plan. The
+ fast-exit also requires the branch to carry real commits of its own
+ (not just be a trivial ancestor), so an empty stub branch from a
+ session cut short between branch creation and first commit still
+ implements normally instead of being silently skipped.
+
+Changed:
+
+- Bookkeeping agents (`state`/`ledger`/`state-clear`) now run at
+ `effort: 'low'` instead of inheriting the session's reasoning effort,
+ since real pilot data showed they're a plurality of all dispatched
+ agents doing purely mechanical work (verify git, write JSON, append
+ log lines) — quality-bearing agents (implement/review/fix/merge/
+ final-review) are unaffected.
+
## 0.6.20 — 2026-07-19
Added:
diff --git a/bin/review-package.js b/bin/review-package.js
index a0dc66f..5719d75 100644
--- a/bin/review-package.js
+++ b/bin/review-package.js
@@ -12,7 +12,7 @@ if (!repoPath || !baseSha || !headSha || !outDir) {
// execFile (sin shell) con -C: los SHAs y rutas llegan de otros agentes — nada se
// interpola en una línea de shell.
function git(...args) {
- return execFileSync('git', ['-C', repoPath, ...args], { encoding: 'utf8' });
+ return execFileSync('git', ['-C', repoPath, ...args], { encoding: 'utf8', maxBuffer: 1024 * 1024 * 1024 });
}
let commits, stat, diff;
diff --git a/gemini-extension.json b/gemini-extension.json
index 8cfe1cc..0c2ae1d 100644
--- a/gemini-extension.json
+++ b/gemini-extension.json
@@ -1,6 +1,6 @@
{
"name": "cys",
- "version": "0.6.20",
+ "version": "0.6.21",
"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 faa4a7f..8dde7c6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "parallel-plan-executor",
- "version": "0.6.20",
+ "version": "0.6.21",
"author": "Christian Bacilio",
"private": true,
"type": "module",
diff --git a/skills/plan/SKILL.md b/skills/plan/SKILL.md
index 5c4048b..50045f5 100644
--- a/skills/plan/SKILL.md
+++ b/skills/plan/SKILL.md
@@ -93,6 +93,24 @@ Each step is one 2-5 minute action, with complete content — never
An empty warnings array is not proof the graph is right, only that the
parser didn't flag anything; a plan-authoring mistake can still produce
a graph that's silently wrong.
+- **Caller-cardinality check:** for any function whose sample code you
+ write once and other tasks' sample code then calls, find every one of
+ those call sites and check the multiplicity they actually feed it — not
+ just the single trivial case your own task's test happens to cover. A
+ `bs-inventory` pilot (2026-07-20) shipped a compliance ledger function
+ with a single shared running-balance accumulator, tested with one
+ product at one warehouse; two *other* tasks' sample code fed it a whole
+ tenant's movements across many products/warehouses, silently mixing
+ unrelated balances — invisible until a reviewer traced the real callers,
+ not caught by the function's own task's test.
+- **Spec-field literalness check:** for any sample code that maps a
+ specific field to a specific spec-mandated value (compliance/legal
+ fields especially), re-read that exact line of the design spec at
+ write time and grep for it — don't write the field from memory of the
+ general idea. Same pilot: the design spec said field 4 must be
+ `Warehouse.rucEstablishmentCode`; the plan's sample code used the
+ warehouse's internal ID instead, because it was written from a general
+ sense of "the warehouse" rather than the spec's literal mapping.
- **Exhaustive-coverage claims:** if the spec states a test suite "covers
every case in this table" (or similar), the plan's test steps must
enumerate each row as its own explicit test step — a summary claim
diff --git a/tests/build-workflow.test.js b/tests/build-workflow.test.js
index fa62159..7a9d145 100644
--- a/tests/build-workflow.test.js
+++ b/tests/build-workflow.test.js
@@ -131,8 +131,9 @@ test('built workflow serializes every main-repo working-tree operation through o
test('built workflow frames ledger content so free-form agent text cannot break the prompt', () => {
assert.ok(
- output.includes('${line}'),
- 'la línea del ledger debe ir delimitada, no incrustada entre comillas'
+ output.includes('${l}') || output.includes('${line}'),
+ 'cada línea del ledger debe ir delimitada con , no incrustada entre comillas — ' +
+ 'sigue siendo cierto tras el coalescing de appendLedger(), que arma el XML por línea vía .map()'
);
});
@@ -315,17 +316,89 @@ test('built workflow tags .cys/state.json writes with their own phase, not Merge
);
});
-test('built workflow frames the state-write prompt as verified bookkeeping, not new claims (safety-classifier block, pending.md bug)', () => {
+test('built workflow instructs the state-write agent to verify against real repo state, not just assert the content is true (safety-classifier block, pending.md bug reopened 2026-07-21)', () => {
+ const writeStateIndex = output.indexOf('function writeState()');
+ assert.ok(writeStateIndex >= 0, 'debe existir writeState()');
+ const writeStateBody = output.slice(writeStateIndex, writeStateIndex + 2600);
+ assert.ok(
+ writeStateBody.includes('independently verify') && writeStateBody.includes('git log') && writeStateBody.includes('git merge-base'),
+ 'la primera corrección (afirmarle al agente "esto ya es verdad, escríbelo") seguía siendo bloqueada por el clasificador en corridas reales — el prompt debe pedirle al agente que verifique el contenido contra el repo real antes de escribir, no solo confiar en la afirmación'
+ );
+ assert.ok(
+ writeStateBody.includes("don't refuse to write") && writeStateBody.includes('correct that task'),
+ 'una foto desactualizada por la cola de encolado es esperable, no un bloqueo — el agente debe autocorregir la entrada de la tarea con lo que verificó, no negarse a escribir sin más (el primer intento de este arreglo solo pedía "no escribas, reportá", lo que dejaba state.json congelado indefinidamente)'
+ );
+ assert.ok(
+ writeStateBody.includes('"status": "in_progress"') && writeStateBody.includes('"phase"'),
+ 'la corrección debe incluir tanto status como phase para una tarea en curso — un status sin phase es una corrección incompleta (pedido explícito del usuario tras ver state.json sin fase)'
+ );
+});
+
+test('built workflow instructs the ledger-append agent to spot-check the line against real evidence before appending (safety-classifier block, pending.md bug reopened 2026-07-21)', () => {
+ const appendLedgerBatchIndex = output.indexOf('function appendLedgerBatch(');
+ assert.ok(appendLedgerBatchIndex >= 0, 'debe existir appendLedgerBatch()');
+ const appendLedgerBatchBody = output.slice(appendLedgerBatchIndex, appendLedgerBatchIndex + 1200);
+ assert.ok(
+ appendLedgerBatchBody.includes('spot-check') && appendLedgerBatchBody.includes('git log'),
+ 'el agente de ledger debe verificar las afirmaciones de la línea contra evidencia real (git log, reportes) antes de agregarla, no solo confiar en el texto que le pasaron'
+ );
+});
+
+test('built workflow coalesces concurrent appendLedger() calls into one batched [ledger] agent', () => {
+ const appendLedgerIndex = output.indexOf('function appendLedger(line)');
+ assert.ok(appendLedgerIndex >= 0, 'debe existir appendLedger(line), el wrapper público de coalescing');
+ const appendLedgerBody = output.slice(appendLedgerIndex, appendLedgerIndex + 500);
+ assert.ok(
+ appendLedgerBody.includes('ledgerPending') && appendLedgerBody.includes('while'),
+ 'appendLedger() debe encolar en ledgerPending y reintentar con un loop mientras haya líneas nuevas, no lanzar un agente por cada llamada'
+ );
+ assert.ok(
+ output.includes('appendLedgerBatch(batch)'),
+ 'el batch acumulado debe pasarse completo a appendLedgerBatch(), no una línea a la vez'
+ );
+ assert.ok(
+ output.includes('agrupó') && output.includes('batch.length'),
+ 'debe loguear cuántas líneas se agruparon en un solo agente, para que se pueda confirmar en vivo que el coalescing está funcionando (pedido explícito del usuario)'
+ );
+});
+
+test('built workflow coalesces concurrent state-write requests instead of dispatching one [state] agent per settle()/markInProgress() call', () => {
+ const requestIndex = output.indexOf('function requestWriteState()');
+ assert.ok(requestIndex >= 0, 'debe existir requestWriteState(), el wrapper de coalescing sobre writeState()');
+ const requestBody = output.slice(requestIndex, requestIndex + 500);
+ assert.ok(
+ requestBody.includes('stateWriteDirty') && requestBody.includes('while'),
+ 'el coalescing debe usar una bandera "dirty" y un loop que reintente mientras haya pedidos nuevos, no lanzar un agente por cada llamada'
+ );
+ assert.ok(
+ output.includes('await requestWriteState()') && output.includes('return requestWriteState()'),
+ 'settle() y markInProgress() deben pasar por requestWriteState(), no llamar a writeState() directo (si no, el coalescing no tiene efecto)'
+ );
+ assert.ok(
+ output.includes('agrupó') && output.includes('stateWriteRequests'),
+ 'debe loguear cuántos pedidos de escritura se agruparon en un solo agente, para que se pueda confirmar en vivo que el coalescing está funcionando (pedido explícito del usuario)'
+ );
+});
+
+test('built workflow fast-exits re-dispatched already-merged tasks without re-review or re-merge (pilot bs-inventory 2026-07-21)', () => {
+ assert.ok(
+ output.includes('FAST-EXIT CHECK') && output.includes('alreadyMerged: true'),
+ 'el prompt del implementador debe chequear primero (branch existe + es ancestro de la rama de integración) y reportar alreadyMerged sin re-correr tests — cada re-despacho pagaba una re-verificación completa con testcontainers'
+ );
+ assert.ok(
+ output.includes('if (impl.alreadyMerged)') && output.includes('skipping review and merge'),
+ 'runTask debe cortocircuitar a settle(done) cuando el implementador confirma alreadyMerged — sin esto, cada re-despacho pagaba revisión + merge completos sobre trabajo ya integrado (63 reviews / 57 merges para 10 tareas en el piloto)'
+ );
assert.ok(
- output.includes('bookkeeping snapshot') && output.includes('already completed and verified earlier in this same run'),
- 'sin este framing, el clasificador de seguridad puede leer un JSON con status "done" y SHAs reales como una fabricación en vez de un registro legítimo de resultados ya verificados'
+ output.indexOf('if (impl.alreadyMerged)') < output.indexOf("await markInProgress(taskId, 'Review')"),
+ 'el cortocircuito debe evaluarse ANTES de entrar a la fase de Review'
);
});
test('built workflow adds updatedAt to .cys/state.json via the write agent\'s own date command (pending.md gap, Fase 4b design)', () => {
const writeStateIndex = output.indexOf('function writeState()');
assert.ok(writeStateIndex >= 0, 'debe existir writeState()');
- const writeStateBody = output.slice(writeStateIndex, writeStateIndex + 1400);
+ const writeStateBody = output.slice(writeStateIndex, writeStateIndex + 2600);
assert.ok(
writeStateBody.includes('date +%H:%M:%S'),
'el timestamp no puede venir de Date.now()/new Date() (prohibido en el sandbox de Workflow) — debe pedirle al agente que corra date'
diff --git a/workflows-src/parallel-plan-executor.template.js b/workflows-src/parallel-plan-executor.template.js
index 2902501..f164b54 100644
--- a/workflows-src/parallel-plan-executor.template.js
+++ b/workflows-src/parallel-plan-executor.template.js
@@ -39,24 +39,75 @@ function stateJson() {
function writeState() {
return enqueueMainRepo(() => agent(
- `Run \`date +%H:%M:%S\` first to get the current time. In repo ${repoPath}, write to ` +
- `.cys/state.json (create the file and its directory if missing, overwriting anything ` +
- `already there) the content between the tags below, but with ` +
- `"updatedAt": "