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": "