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.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",
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.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",
Expand Down
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion bin/review-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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.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"
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.20",
"version": "0.6.21",
"author": "Christian Bacilio",
"private": true,
"type": "module",
Expand Down
18 changes: 18 additions & 0 deletions skills/plan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 79 additions & 6 deletions tests/build-workflow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>${line}</line>'),
'la línea del ledger debe ir delimitada, no incrustada entre comillas'
output.includes('<line>${l}</line>') || output.includes('<line>${line}</line>'),
'cada línea del ledger debe ir delimitada con <line></line>, no incrustada entre comillas — ' +
'sigue siendo cierto tras el coalescing de appendLedger(), que arma el XML por línea vía .map()'
);
});

Expand Down Expand Up @@ -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'
Expand Down
Loading
Loading