From 59aaffd938a82b48976cc6769da339be98b523e8 Mon Sep 17 00:00:00 2001 From: Danny Gillespie Date: Fri, 3 Jul 2026 15:02:47 +0100 Subject: [PATCH 1/4] feat(evolve): rename the evolution command to /ratchet:evolve Rename skills/ratchet-evolve -> skills/evolve so the plugin command is /ratchet:evolve (clean rename, no alias). Update the README command table, examples, standalone-usage note, and add a one-run demo transcript. The helper CLI binary stays 'ratchet-evolve'. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 36 +++++++++++++++++----- bin/ratchet-evolve | 4 +-- skills/{ratchet-evolve => evolve}/SKILL.md | 12 ++++++-- 3 files changed, 40 insertions(+), 12 deletions(-) rename skills/{ratchet-evolve => evolve}/SKILL.md (93%) diff --git a/README.md b/README.md index 1b859cd..af0687b 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ It bundles two things: - **The Ratchet command family** (`/ratchet:*`) — a consequence engine that turns ambiguity into shipped, falsifiable artifacts through adversarial execution loops. -- **`/ratchet-evolve`** — a narrower, bounded loop that mutates one artifact, tests it, and +- **`/ratchet:evolve`** — a narrower, bounded loop that mutates one artifact, tests it, and keeps only proven improvement. Every command produces *pressure*, not just insight. Each one forces a choice, creates an @@ -113,7 +113,7 @@ node bin/ratchet --help Verify: ```bash -ratchet --version # -> ratchet 0.1.0 +ratchet --version # -> ratchet 0.2.0 ratchet init ratchet status ``` @@ -175,15 +175,15 @@ artifact (code file, prompt, skill, README, spec, workflow): | Command | Purpose | | --- | --- | -| `/ratchet:ratchet-evolve` | Mutate → test → keep only proven improvement → serialize the next edge. | +| `/ratchet:evolve` | Mutate → test → keep only proven improvement → serialize the next edge. | ``` LOCK → SNAPSHOT → PRESSURE → MUTATE → JUDGE → APPLY → VERIFY → KEEP/REVERT/ASK → RECORD → NEXT EDGE ``` ```bash -/ratchet:ratchet-evolve src/auth/session.js --goal "reduce login-state race conditions" --test "npm test -- auth" --mode code -/ratchet:ratchet-evolve README.md --goal "make install impossible to misunderstand" --mode docs +/ratchet:evolve src/auth/session.js --goal "reduce login-state race conditions" --test "npm test -- auth" --mode code +/ratchet:evolve README.md --goal "make install impossible to misunderstand" --mode docs ``` It defaults to `--iterations 2` and **proposes** patches without `--write`. Its rule is @@ -192,8 +192,30 @@ this better" — it evolves along one chosen pressure vector and records every v `.ratchet/evolve-log.jsonl` via the `ratchet-evolve` helper CLI. > Note: because plugin skills are namespaced by the plugin, the command is invoked as -> `/ratchet:ratchet-evolve`. Drop the `SKILL.md` into `~/.claude/skills/ratchet-evolve/` to -> use it as a bare `/ratchet-evolve` outside the plugin. +> `/ratchet:evolve` (renamed from the older `ratchet-evolve` skill in v0.2.0 — no alias is +> kept). Drop the `SKILL.md` into `~/.claude/skills/evolve/` to use it as a bare `/evolve` +> outside the plugin. + +#### One run, end to end + +```text +Before: README install path is ambiguous — global vs. project vs. CLI-only blur together. + +Command: /ratchet:evolve README.md --goal "make install impossible to misunderstand" --mode docs + +Mutation: Split install into three labelled paths (global plugin, project-local, CLI-only), + each with its own verify step. No other section touched. + +Verify: Manual docs checks — first-use path unambiguous, no contradiction, no missing step + between install and first success. All passed. + +Verdict: KEEP ← allowed only because evidence exists; the proof gate rejects a bare KEEP + +Next edge: Add a 60-second GIF of the plugin install. (readable later via `ratchet-evolve next`) +``` + +Every verdict lands in `.ratchet/evolve-log.jsonl`. A `KEEP` without verification evidence is +refused at write time — the loop cannot record progress it did not prove. --- diff --git a/bin/ratchet-evolve b/bin/ratchet-evolve index 2b9142e..62b803f 100644 --- a/bin/ratchet-evolve +++ b/bin/ratchet-evolve @@ -1,8 +1,8 @@ #!/usr/bin/env node 'use strict'; -// Deterministic helpers for the /ratchet-evolve loop. The reasoning lives in -// skills/ratchet-evolve/SKILL.md; this binary provides snapshot, scoring, +// Deterministic helpers for the /ratchet:evolve loop. The reasoning lives in +// skills/evolve/SKILL.md; this binary provides snapshot, scoring, // verification, and the evolution journal. try { diff --git a/skills/ratchet-evolve/SKILL.md b/skills/evolve/SKILL.md similarity index 93% rename from skills/ratchet-evolve/SKILL.md rename to skills/evolve/SKILL.md index 05c2583..be78499 100644 --- a/skills/ratchet-evolve/SKILL.md +++ b/skills/evolve/SKILL.md @@ -1,5 +1,5 @@ --- -name: ratchet-evolve +name: evolve description: "Evolve one artifact through a bounded, evidence-gated mutation loop — lock target, snapshot baseline, apply one pressure, generate candidate deltas, judge, patch, verify, keep or revert, record state, and name the next edge. Not brainstorming and not a general 'make this better': every kept change must be proven. Use to harden a specific code file, prompt, skill, test suite, README, spec, or workflow along a chosen pressure vector." argument-hint: " --goal \"\" [--iterations 2] [--test \"\"] [--mode code|prompt|docs|workflow|auto] [--write]" allowed-tools: @@ -35,7 +35,7 @@ No state → no loop continuity. ## Invocation ``` -/ratchet-evolve --goal "" [--iterations 2] [--test ""] [--mode code|prompt|docs|workflow|auto] [--write] +/ratchet:evolve --goal "" [--iterations 2] [--test ""] [--mode code|prompt|docs|workflow|auto] [--write] ``` Parse from `$ARGUMENTS`: the target artifact, the `--goal`, `--iterations` (default **2**), @@ -114,7 +114,13 @@ ratchet-evolve log append '{"target":"...","goal":"...","iteration":1,"pressure" Appends to `/.ratchet/evolve-log.jsonl`. ### 10. NEXT EDGE -Name the next mutation: small, specific, testable, attached to the **same** artifact. +Name the next mutation: small, specific, testable, attached to the **same** artifact. It is +persisted with the event and readable next session via `ratchet-evolve next`. Check +`ratchet-evolve status` for the running kept/reverted/asked tally. + +Copy-paste shapes live in `templates/`: `evolve-event.json` (the log-append payload), +`evolve-manual-checks.md` (manual evidence when there is no test command), and +`evolve-report.md` (the output contract below). ## Stop rules From be4a5fd0a871caf1a72d5228ad93f0783010fc03 Mon Sep 17 00:00:00 2001 From: Danny Gillespie Date: Fri, 3 Jul 2026 15:02:56 +0100 Subject: [PATCH 2/4] feat(evolve): enforce the proof gate; enrich status and add next No proof -> no keep, in code: appendEvent now rejects a KEEP with failed verification or no command/manual evidence (REVERT and ASK exempt). status reports kept/reverted/asks/targets/last/next-edge; add 'ratchet-evolve next' and manual-evidence templates. Also fix --version printing help on the evolve CLI, and derive its version from package.json. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/evolve/index.js | 37 ++++++++++++++++++++---- src/evolve/journal.js | 10 +++++-- src/evolve/schema.js | 25 +++++++++++++++- templates/evolve-event.json | 12 ++++++++ templates/evolve-manual-checks.md | 32 +++++++++++++++++++++ templates/evolve-report.md | 25 ++++++++++++++++ test/evolve.test.js | 47 ++++++++++++++++++++++++++++++- 7 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 templates/evolve-event.json create mode 100644 templates/evolve-manual-checks.md create mode 100644 templates/evolve-report.md diff --git a/src/evolve/index.js b/src/evolve/index.js index 490a756..c5cdfe4 100644 --- a/src/evolve/index.js +++ b/src/evolve/index.js @@ -9,7 +9,7 @@ const journal = require('./journal'); const pressure = require('./pressure'); const { PRESSURES, MODES } = require('./schema'); -const VERSION = '0.1.0'; +const VERSION = require('../../package.json').version; function out(s) { process.stdout.write(String(s) + '\n'); @@ -102,15 +102,27 @@ function renderStatus(s) { if (!s.events) return '### Evolve log\n_empty — no evolution events recorded yet._'; const o = []; o.push('### Evolve log'); - o.push(`- Events: ${s.events} (kept ${s.kept}, reverted ${s.reverted})`); - o.push(`- Targets: ${s.targets.join(', ')}`); + o.push(`- Events: ${s.events}`); + o.push(`- Kept: ${s.kept}`); + o.push(`- Reverted: ${s.reverted}`); + o.push(`- Asks: ${s.asks}`); + o.push('- Active targets:'); + for (const t of s.targets) o.push(` - ${t}`); if (s.last) { - o.push(`- Last: ${s.last.id} — ${s.last.target} — verdict **${s.last.verdict}**`); + o.push(`- Last verdict: ${s.last.verdict} (${s.last.id})`); if (s.last.nextEdge) o.push(`- Next edge: ${s.last.nextEdge}`); } return o.join('\n'); } +function renderNext(s) { + const edge = s.last && s.last.nextEdge ? s.last.nextEdge : ''; + if (!edge) return '### Next edge\n_none recorded — run an evolve iteration first._'; + const o = ['### Next edge', edge]; + if (s.last && s.last.target) o.push('', `Target: ${s.last.target}`); + return o.join('\n'); +} + // --- dispatch -------------------------------------------------------------- function run(argv) { @@ -120,6 +132,11 @@ function run(argv) { const asJson = Boolean(opts.json); const [group, sub] = pos; + // `--version` / `--help` parse into opts, not pos, so handle them before the + // positional dispatch — otherwise `ratchet-evolve --version` prints help. + if (opts.version) return out(`ratchet-evolve ${VERSION}`); + if (opts.help && group == null) return help(); + switch (group) { case undefined: case 'help': @@ -167,6 +184,15 @@ function run(argv) { return out(asJson ? JSON.stringify(s, null, 2) : renderStatus(s)); } + case 'next': { + const s = journal.status(cwd); + if (asJson) { + const edge = s.last && s.last.nextEdge ? s.last.nextEdge : ''; + return out(JSON.stringify({ nextEdge: edge, from: s.last ? s.last.id : null, target: s.last ? s.last.target : null }, null, 2)); + } + return out(renderNext(s)); + } + case 'pressure': { const info = pressure.suggest(sub || 'code'); return out(asJson ? JSON.stringify(info, null, 2) : JSON.stringify(info, null, 2)); @@ -189,8 +215,9 @@ function help() { ' ratchet-evolve snapshot [--goal g] [--mode m] capture baseline (hash, git, mode)', ' ratchet-evolve score mutation rank candidates, pick one (--json)', ' ratchet-evolve verify [--test "cmd"] [--mode m] gather verification evidence (--json)', - ' ratchet-evolve log append write an evolution event', + ' ratchet-evolve log append write an evolution event (KEEP needs proof)', ' ratchet-evolve log | status read the evolve log (--json)', + ' ratchet-evolve next show the last recorded next edge (--json)', ' ratchet-evolve pressure [mode] suggested pressure vector for a mode', '', ` modes: ${MODES.join(', ')}`, diff --git a/src/evolve/journal.js b/src/evolve/journal.js index 0f082e6..7b2e1a6 100644 --- a/src/evolve/journal.js +++ b/src/evolve/journal.js @@ -3,7 +3,7 @@ const fs = require('fs'); const path = require('path'); -const { newEvent } = require('./schema'); +const { newEvent, validateKeepGate } = require('./schema'); // The evolution log lives in the project, not the shared plugin data dir: // evolution events are tied to specific artifacts in this repo, so the trail @@ -48,6 +48,9 @@ function nextId(cwd, iso) { function appendEvent(cwd, fields) { const event = newEvent(fields); + // Proof gate: refuse to persist a KEEP without verification evidence. Throws + // before any write, so the log never contains an unproven kept mutation. + validateKeepGate(event); if (!event.id) event.id = nextId(cwd, event.timestamp); const file = logPath(cwd); fs.mkdirSync(path.dirname(file), { recursive: true }); @@ -57,12 +60,13 @@ function appendEvent(cwd, fields) { function status(cwd = process.cwd()) { const events = readEvents(cwd); - if (!events.length) return { events: 0, targets: [], last: null }; + if (!events.length) return { events: 0, kept: 0, reverted: 0, asks: 0, targets: [], last: null }; const targets = [...new Set(events.map((e) => e.target))]; const last = events[events.length - 1]; const kept = events.filter((e) => e.verdict === 'KEEP').length; const reverted = events.filter((e) => e.verdict === 'REVERT').length; - return { events: events.length, kept, reverted, targets, last }; + const asks = events.filter((e) => e.verdict === 'ASK').length; + return { events: events.length, kept, reverted, asks, targets, last }; } module.exports = { logPath, readEvents, appendEvent, nextId, status }; diff --git a/src/evolve/schema.js b/src/evolve/schema.js index af37b76..b07c0cc 100644 --- a/src/evolve/schema.js +++ b/src/evolve/schema.js @@ -50,4 +50,27 @@ function newEvent(f = {}) { }; } -module.exports = { PRESSURES, MODES, VERDICTS, nowIso, newEvent }; +// The proof gate. "No proof → no keep": a KEEP verdict may only be recorded when +// verification actually produced evidence and did not fail. REVERT and ASK are +// exempt — they exist precisely to record the absence of a proven improvement. +// Throws (does not return a boolean) so a bad KEEP can never be silently written. +function validateKeepGate(event) { + if (!event || event.verdict !== 'KEEP') return event; + const v = event.verification || {}; + const result = v.result; + const commands = Array.isArray(v.commands) ? v.commands : []; + const manualChecks = Array.isArray(v.manualChecks) ? v.manualChecks : []; + + if (result === 'fail') { + throw new Error('cannot KEEP: verification failed'); + } + if (!commands.length && !manualChecks.length) { + throw new Error('cannot KEEP: no verification evidence'); + } + if (result === 'manual' && !manualChecks.length) { + throw new Error('cannot KEEP: manual verification requires explicit checks'); + } + return event; +} + +module.exports = { PRESSURES, MODES, VERDICTS, nowIso, newEvent, validateKeepGate }; diff --git a/templates/evolve-event.json b/templates/evolve-event.json new file mode 100644 index 0000000..4c69f4b --- /dev/null +++ b/templates/evolve-event.json @@ -0,0 +1,12 @@ +{ + "target": "", + "goal": "", + "iteration": 1, + "pressure": { "primary": "", "secondary": "" }, + "chosenMutation": "", + "filesTouched": [], + "verification": { "commands": [], "manualChecks": [], "result": "manual" }, + "verdict": "ASK", + "remainingRisks": [], + "nextEdge": "" +} diff --git a/templates/evolve-manual-checks.md b/templates/evolve-manual-checks.md new file mode 100644 index 0000000..2cfa163 --- /dev/null +++ b/templates/evolve-manual-checks.md @@ -0,0 +1,32 @@ +# Manual Evidence + +When a target has no runnable test command (prompts, docs, workflows), verification +is *manual* — but manual is evidence, not a loophole. Fill this in and attach the +result to the evolution event. A KEEP still requires that these checks were actually +performed and passed. + +``` +Target: +Goal: +Mode: code | prompt | docs | workflow +``` + +## Checks + +- [ ] A lazy model cannot escape the instruction. +- [ ] The artifact still forces a concrete output. +- [ ] It prevents self-praise. +- [ ] It leaves durable state. +- [ ] No unrelated scope was touched. +- [ ] The next action cannot be misread. + +## Verdict + +``` +KEEP | REVERT | ASK +``` + +## Evidence notes + + diff --git a/templates/evolve-report.md b/templates/evolve-report.md new file mode 100644 index 0000000..cccf50a --- /dev/null +++ b/templates/evolve-report.md @@ -0,0 +1,25 @@ +RATCHET EVOLVE REPORT + +TARGET: +GOAL: +BASELINE: + +PRESSURE +Primary:

+Secondary:

+Rejected:

+ +ITERATION 1 +Mutation candidates: +Chosen mutation: +Patch: +Verification: +Verdict: KEEP | REVERT | ASK + +FINAL STATE +Kept changes: +Rejected changes: +Tests/evals run: +Remaining defects: +Next edge: +State written: .ratchet/evolve-log.jsonl ( events) diff --git a/test/evolve.test.js b/test/evolve.test.js index 58cb222..25e2579 100644 --- a/test/evolve.test.js +++ b/test/evolve.test.js @@ -19,6 +19,7 @@ const score = require('../src/evolve/score'); const verify = require('../src/evolve/verify'); const journal = require('../src/evolve/journal'); const pressure = require('../src/evolve/pressure'); +const schema = require('../src/evolve/schema'); let passed = 0; function ok(name, fn) { @@ -82,7 +83,12 @@ ok('verify without a command returns manual checks', () => { }); ok('journal appends events with dated ids and status', () => { - const e1 = journal.appendEvent(process.cwd(), { target: 'a.md', verdict: 'KEEP', nextEdge: 'do x' }); + const e1 = journal.appendEvent(process.cwd(), { + target: 'a.md', + verdict: 'KEEP', + nextEdge: 'do x', + verification: { manualChecks: ['reviewed the delta'], result: 'manual' }, + }); assert.strictEqual(e1.id, 'evo_2026_07_03_001'); const e2 = journal.appendEvent(process.cwd(), { target: 'a.md', verdict: 'REVERT' }); assert.strictEqual(e2.id, 'evo_2026_07_03_002'); @@ -90,9 +96,48 @@ ok('journal appends events with dated ids and status', () => { assert.strictEqual(s.events, 2); assert.strictEqual(s.kept, 1); assert.strictEqual(s.reverted, 1); + assert.strictEqual(s.asks, 0); assert.strictEqual(s.last.verdict, 'REVERT'); }); +ok('proof gate: KEEP is allowed with command or manual evidence', () => { + const keep = (over) => schema.newEvent({ target: 't', verdict: 'KEEP', ...over }); + // passing command evidence + schema.validateKeepGate(keep({ verification: { commands: [{ pass: true }], result: 'pass' } })); + // explicit manual checks + schema.validateKeepGate(keep({ verification: { manualChecks: ['a lazy model cannot escape'], result: 'manual' } })); +}); + +ok('proof gate: KEEP is rejected without proof', () => { + const keep = (over) => schema.newEvent({ target: 't', verdict: 'KEEP', ...over }); + // failed verification + assert.throws( + () => schema.validateKeepGate(keep({ verification: { commands: [{ pass: false }], result: 'fail' } })), + /verification failed/ + ); + // no evidence at all + assert.throws( + () => schema.validateKeepGate(keep({ verification: { commands: [], manualChecks: [], result: 'manual' } })), + /no verification evidence/ + ); + // claims pass but carries no evidence + assert.throws( + () => schema.validateKeepGate(keep({ verification: { commands: [], manualChecks: [], result: 'pass' } })), + /no verification evidence/ + ); +}); + +ok('proof gate: REVERT and ASK bypass the gate', () => { + schema.validateKeepGate(schema.newEvent({ target: 't', verdict: 'REVERT', verification: { result: 'fail' } })); + schema.validateKeepGate(schema.newEvent({ target: 't', verdict: 'ASK' })); +}); + +ok('proof gate: appendEvent refuses to persist an unproven KEEP', () => { + assert.throws(() => journal.appendEvent(process.cwd(), { target: 'b.md', verdict: 'KEEP' }), /cannot KEEP/); + const s = journal.status(process.cwd()); + assert.ok(!s.targets.includes('b.md'), 'the rejected KEEP was never written'); +}); + ok('pressure suggests a vector and flags the rewrite trap', () => { const p = pressure.suggest('docs'); assert.ok(p.primaryCandidates.includes('clarity')); From 47f56a50d6ffd5fffd32228b588c2ada21fd9083 Mon Sep 17 00:00:00 2001 From: Danny Gillespie Date: Fri, 3 Jul 2026 15:03:06 +0100 Subject: [PATCH 3/4] feat(cli): add compile done + doctor; back up corrupt state; snapshot dot-dirs 'ratchet compile done' atomically clears dirty, stamps lastCompileAt, and records a compile.done event so the Stop hook stops nagging; the compile flow and scribe agent now use it. 'ratchet doctor' validates plugin shape and version alignment. Malformed state.json/ledger.json are backed up to *.corrupt..json before reinit instead of being silently lost. Repo snapshots now surface allowlisted dot-dirs (.claude-plugin, .github, .claude, .ratchet) while still ignoring .git and caches. Adds plugin-shape tests and fixes --version printing help on the state CLI. Co-Authored-By: Claude Opus 4.8 (1M context) --- agents/ratchet-scribe.md | 5 +- skills/compile/SKILL.md | 6 +- src/cli.js | 139 +++++++++++++++++++++++++++++++++++++- src/repoSnapshot.js | 17 ++++- src/state.js | 41 ++++++++++- test/cli.test.js | 46 +++++++++++++ test/plugin-shape.test.js | 115 +++++++++++++++++++++++++++++++ 7 files changed, 358 insertions(+), 11 deletions(-) create mode 100644 test/plugin-shape.test.js diff --git a/agents/ratchet-scribe.md b/agents/ratchet-scribe.md index 24c089c..27e83cc 100644 --- a/agents/ratchet-scribe.md +++ b/agents/ratchet-scribe.md @@ -24,8 +24,9 @@ re-deriving anything. You are precise and you never embellish. 4. **One-sentence memory.** Produce a single sentence that captures the session's state well enough to prime a cold start. This is the highest-value line; make it load-bearing. -5. **Stamp the compile.** After writing, set `lastCompileAt` and clear the dirty flag by - running `ratchet state set lastCompileAt ""` so the Stop hook stops nagging. +5. **Stamp the compile.** After writing everything else, run `ratchet compile done` — it + stamps `lastCompileAt`, clears the dirty flag, and records a `compile.done` event in one + atomic move so the Stop hook stops nagging. Run it last, or a later `state set` re-dirties. ## Output shape diff --git a/skills/compile/SKILL.md b/skills/compile/SKILL.md index bf9a4de..80b075b 100644 --- a/skills/compile/SKILL.md +++ b/skills/compile/SKILL.md @@ -46,10 +46,12 @@ ratchet artifact add '{"title":"...","kind":"...","status":"...","holes":["..."] ratchet defect add '{"severity":"...","summary":"...","status":"..."}' # if any new ratchet state append openLoops '{"text":"...","status":"open"}' # if any ratchet state set phase compile -ratchet state set lastCompileAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" +ratchet compile done ``` -Setting `lastCompileAt` clears the Stop-hook nag. Then emit the record: +`ratchet compile done` clears the dirty flag, stamps `lastCompileAt`, and records a +`compile.done` history event in one atomic move — which silences the Stop-hook nag. (Run it +last: a `state set` after it would re-dirty the session.) Then emit the record: ``` ratchet export markdown diff --git a/src/cli.js b/src/cli.js index 59573f9..fb63b4b 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,6 +1,7 @@ 'use strict'; const fs = require('fs'); +const path = require('path'); const state = require('./state'); const ledger = require('./ledger'); @@ -10,7 +11,8 @@ const repo = require('./repoSnapshot'); const md = require('./markdown'); const schemas = require('./schemas'); -const VERSION = '0.1.0'; +const VERSION = require('../package.json').version; +const PLUGIN_ROOT = path.resolve(__dirname, '..'); function out(s) { process.stdout.write(String(s) + '\n'); @@ -59,6 +61,11 @@ function run(argv) { const asJson = flags.has('--json'); const [group, sub, ...rest] = pos; + // `--version` / `--help` are flags, so they never land in `group`; handle them + // up front or `ratchet --version` silently falls through to the help text. + if (flags.has('--version')) return out(`ratchet ${VERSION}`); + if (flags.has('--help') && group == null) return help(); + switch (group) { case undefined: case 'help': @@ -111,6 +118,14 @@ function run(argv) { return out(asJson ? JSON.stringify(s, null, 2) : md.stateSummary(s)); } + case 'compile': { + if (sub !== 'done') throw new Error('usage: ratchet compile done'); + return cmdCompileDone(cwd); + } + + case 'doctor': + return cmdDoctor(cwd, asJson); + case 'touch': { const file = sub; if (!file) throw new Error('usage: ratchet touch '); @@ -279,6 +294,124 @@ function cmdHook(cwd, sub) { } } +// Atomically mark a session as compiled. Unlike `state set`, this CLEARS dirty +// (so the Stop hook stops nagging) and stamps lastCompileAt in one move. Compile +// completion is an event, not a scalar edit — it must never re-dirty the state. +function cmdCompileDone(cwd) { + const now = schemas.nowIso(); + const s = state.loadState(cwd); + s.lastCompileAt = now; + s.dirty = false; + s.history.push({ id: state.makeId('hist'), at: now, event: 'compile.done', note: 'state serialized' }); + state.saveState(cwd, s); + return out('compiled — dirty cleared, state serialized'); +} + +// Extract the raw YAML frontmatter block from a SKILL.md / agent .md file. +function frontmatter(text) { + const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text); + return m ? m[1] : null; +} + +// "Is my plugin healthy?" — validate the plugin shape against what Claude Code +// expects, plus version alignment and a live state/snapshot check. Exits non-zero +// on any failure so CI and `npm test` can gate a release on it. +function cmdDoctor(cwd, asJson) { + const root = PLUGIN_ROOT; + const checks = []; + const add = (name, ok, detail) => checks.push({ name, ok: Boolean(ok), detail: detail || '' }); + const readJsonFile = (rel) => { + try { + return { ok: true, data: JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8')) }; + } catch (e) { + return { ok: false, error: e.message }; + } + }; + + const major = Number(process.versions.node.split('.')[0]); + add('node >= 18', major >= 18, `found ${process.version}`); + + const pkg = readJsonFile('package.json'); + add('package.json parses', pkg.ok, pkg.ok ? '' : pkg.error); + const plugin = readJsonFile('.claude-plugin/plugin.json'); + add('.claude-plugin/plugin.json parses', plugin.ok, plugin.ok ? '' : plugin.error); + const market = readJsonFile('.claude-plugin/marketplace.json'); + add('.claude-plugin/marketplace.json parses', market.ok, market.ok ? '' : market.error); + + if (pkg.ok && plugin.ok && market.ok) { + const pv = pkg.data.version; + const others = [ + ['plugin.json', plugin.data.version], + ['marketplace.metadata', market.data.metadata && market.data.metadata.version], + ['marketplace.plugins[0]', market.data.plugins && market.data.plugins[0] && market.data.plugins[0].version], + ]; + const mismatch = others.filter(([, v]) => v !== pv).map(([k, v]) => `${k}=${v}`); + add('versions aligned', mismatch.length === 0, mismatch.length ? `package=${pv} but ${mismatch.join(', ')}` : `all ${pv}`); + } + + for (const d of ['skills', 'agents', 'hooks', 'bin', 'src']) { + const p = path.join(root, d); + add(`dir ${d}/ exists`, fs.existsSync(p) && fs.statSync(p).isDirectory()); + } + + const hooks = readJsonFile('hooks/hooks.json'); + add('hooks/hooks.json parses', hooks.ok, hooks.ok ? '' : hooks.error); + + add('bin/ratchet exists', fs.existsSync(path.join(root, 'bin', 'ratchet'))); + add('bin/ratchet-evolve exists', fs.existsSync(path.join(root, 'bin', 'ratchet-evolve'))); + + const skillProblems = []; + try { + for (const name of fs.readdirSync(path.join(root, 'skills'))) { + const skillMd = path.join(root, 'skills', name, 'SKILL.md'); + if (!fs.existsSync(skillMd)) { + skillProblems.push(`${name}: no SKILL.md`); + continue; + } + const fm = frontmatter(fs.readFileSync(skillMd, 'utf8')); + if (!fm) skillProblems.push(`${name}: no frontmatter`); + else if (!/(^|\n)description:/.test(fm)) skillProblems.push(`${name}: no description`); + } + } catch (e) { + skillProblems.push(e.message); + } + add('every skills/*/SKILL.md has frontmatter + description', skillProblems.length === 0, skillProblems.join('; ')); + + let stateDetail = ''; + let stateOk = true; + try { + state.initProject(cwd); + stateDetail = state.projectDir(cwd); + } catch (e) { + stateOk = false; + stateDetail = e.message; + } + add('state dir writable', stateOk, stateDetail); + + let snapOk = true; + let snapDetail = ''; + try { + const snap = repo.snapshot(cwd); + snapDetail = `${snap.fileCount} files`; + } catch (e) { + snapOk = false; + snapDetail = e.message; + } + add('repo snapshot works', snapOk, snapDetail); + + const failed = checks.filter((c) => !c.ok); + if (asJson) { + out(JSON.stringify({ ok: failed.length === 0, checks }, null, 2)); + } else { + out('ratchet doctor'); + out(''); + for (const c of checks) out(` ${c.ok ? 'ok ' : 'FAIL'} ${c.name}${c.detail ? ` — ${c.detail}` : ''}`); + out(''); + out(failed.length === 0 ? 'healthy — plugin shape looks good.' : `${failed.length} problem(s) found.`); + } + if (failed.length) process.exitCode = 1; +} + function help() { out( [ @@ -290,6 +423,7 @@ function help() { ' ratchet state get [key] [--json] read state (or one key)', ' ratchet state set set objective|title|bottleneck|phase|nextAction|nextCommand|...', ' ratchet state append append to decisions|artifacts|defects|assumptions|openLoops|history', + ' ratchet compile done mark session compiled: clear dirty + stamp lastCompileAt', ' ratchet state reset wipe session state', '', 'ARTIFACTS & DEFECTS', @@ -310,6 +444,9 @@ function help() { ' ratchet export markdown full session compile', ' ratchet export json raw state', '', + 'PLUGIN HEALTH', + ' ratchet doctor [--json] check plugin shape, version alignment, state dir', + '', 'json args accept a raw string, @file, or - for stdin.', ].join('\n') ); diff --git a/src/repoSnapshot.js b/src/repoSnapshot.js index 03a80ff..2ec10d3 100644 --- a/src/repoSnapshot.js +++ b/src/repoSnapshot.js @@ -27,6 +27,17 @@ const IGNORE_DIRS = new Set([ '.vscode', ]); +// Dot-directories that ARE meaningful to a plugin/repo and must not be hidden. +// Without this, a blanket dot-dir skip loses .claude-plugin, .github, etc. — the +// exact directories a Claude Code plugin lives in. +const DOT_DIR_ALLOWLIST = new Set(['.claude', '.claude-plugin', '.github', '.ratchet']); + +function shouldSkipDir(name) { + if (IGNORE_DIRS.has(name)) return true; + if (name.startsWith('.') && !DOT_DIR_ALLOWLIST.has(name)) return true; + return false; +} + const SIGNAL_FILES = [ 'package.json', 'pnpm-workspace.yaml', @@ -68,8 +79,8 @@ function walk(root, maxFiles = 4000) { } for (const e of entries) { if (e.isDirectory()) { - // Skip ignored dirs and dot-dirs (e.g. .git, .cache) entirely. - if (IGNORE_DIRS.has(e.name) || e.name.startsWith('.')) continue; + // Skip ignored dirs and non-allowlisted dot-dirs (e.g. .git, .cache). + if (shouldSkipDir(e.name)) continue; stack.push(path.join(dir, e.name)); } else if (e.isFile()) { fileCount++; @@ -89,7 +100,7 @@ function topDirs(root) { try { return fs .readdirSync(root, { withFileTypes: true }) - .filter((e) => e.isDirectory() && !IGNORE_DIRS.has(e.name)) + .filter((e) => e.isDirectory() && !shouldSkipDir(e.name)) .map((e) => e.name) .sort(); } catch (_e) { diff --git a/src/state.js b/src/state.js index f820fd5..95a0a4d 100644 --- a/src/state.js +++ b/src/state.js @@ -64,6 +64,40 @@ function writeJson(file, obj) { fs.writeFileSync(file, JSON.stringify(obj, null, 2) + '\n', 'utf8'); } +// Preserve, never silently lose. A tool whose promise is persistent state must +// not throw away a malformed file — a bad write or a manual edit could corrupt +// it. Missing/empty → fresh (quiet). Malformed → copy the bad bytes to +// .corrupt..json, warn, then let the caller reinitialize. +function backupCorrupt(file, raw) { + try { + const stamp = schemas.nowIso().replace(/[:.]/g, '-'); + const dest = `${file}.corrupt.${stamp}.json`; + fs.writeFileSync(dest, raw, 'utf8'); + process.stderr.write( + `[ratchet] ${path.basename(file)} was malformed — backed up to ${path.basename(dest)} and reinitialized.\n` + ); + return dest; + } catch (_e) { + return null; // best effort; never block the session over a backup + } +} + +function readJsonResilient(file) { + let raw; + try { + raw = fs.readFileSync(file, 'utf8'); + } catch (_e) { + return null; // missing / unreadable → caller creates fresh + } + if (!raw.trim()) return null; // empty file → fresh, no noisy backup + try { + return JSON.parse(raw); + } catch (_e) { + backupCorrupt(file, raw); + return null; + } +} + // --------------------------------------------------------------------------- // State lifecycle. // --------------------------------------------------------------------------- @@ -84,9 +118,10 @@ function initProject(cwd, { force = false } = {}) { } function loadState(cwd) { - const existing = readJson(statePath(cwd)); + const existing = readJsonResilient(statePath(cwd)); if (existing) return existing; - // Auto-init on first read so skills never hit a missing file. + // Auto-init on first read so skills never hit a missing file. A corrupt file + // has already been backed up by readJsonResilient before we overwrite it. const fresh = schemas.newState(); writeJson(statePath(cwd), fresh); return fresh; @@ -99,7 +134,7 @@ function saveState(cwd, state) { } function loadLedger(cwd) { - const existing = readJson(ledgerPath(cwd)); + const existing = readJsonResilient(ledgerPath(cwd)); if (existing) return existing; const fresh = schemas.newLedger(); writeJson(ledgerPath(cwd), fresh); diff --git a/test/cli.test.js b/test/cli.test.js index d9e4cbd..37e4a44 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -17,6 +17,8 @@ const scoring = require('../src/scoring'); const artifacts = require('../src/artifacts'); const ledger = require('../src/ledger'); const md = require('../src/markdown'); +const repo = require('../src/repoSnapshot'); +const cli = require('../src/cli'); let passed = 0; function ok(name, fn) { @@ -104,5 +106,49 @@ ok('markdown render does not throw on populated state', () => { assert.ok(exp.includes('Ratchet compile')); }); +ok('compile done clears dirty, stamps lastCompileAt, records history', () => { + cli.run(['node', 'ratchet', 'touch', 'README.md']); + assert.strictEqual(state.loadState(cwd).dirty, true, 'touch dirties state'); + cli.run(['node', 'ratchet', 'compile', 'done']); + const s = state.loadState(cwd); + assert.strictEqual(s.dirty, false, 'compile done clears dirty (Stop hook stays quiet)'); + assert.ok(s.lastCompileAt, 'lastCompileAt is stamped'); + assert.ok(s.history.some((h) => h.event === 'compile.done'), 'compile.done recorded in history'); +}); + +ok('corrupt state.json is backed up, not silently lost', () => { + const sp = state.statePath(cwd); + state.initProject(cwd, { force: true }); + fs.writeFileSync(sp, '{ this is not valid json', 'utf8'); + const loaded = state.loadState(cwd); // triggers backup + fresh + assert.ok(loaded && loaded.version, 'a fresh state is created'); + const dir = path.dirname(sp); + const backups = fs.readdirSync(dir).filter((f) => f.startsWith('state.json.corrupt.')); + assert.ok(backups.length >= 1, 'a corrupt backup exists'); + const raw = fs.readFileSync(path.join(dir, backups[0]), 'utf8'); + assert.ok(raw.includes('this is not valid json'), 'backup preserves the original bad bytes'); +}); + +ok('corrupt ledger.json is backed up, not silently lost', () => { + const lp = state.ledgerPath(cwd); + fs.writeFileSync(lp, 'garbage{', 'utf8'); + const loaded = state.loadLedger(cwd); + assert.ok(loaded && loaded.version, 'a fresh ledger is created'); + const backups = fs.readdirSync(path.dirname(lp)).filter((f) => f.startsWith('ledger.json.corrupt.')); + assert.ok(backups.length >= 1, 'a corrupt ledger backup exists'); +}); + +ok('repo snapshot sees allowlisted dot dirs, skips .git / node_modules', () => { + const proj = path.join(tmp, 'snap-fixture'); + for (const d of ['.claude-plugin', '.github', '.git', 'node_modules', 'src']) { + fs.mkdirSync(path.join(proj, d), { recursive: true }); + } + const snap = repo.snapshot(proj); + assert.ok(snap.dirs.includes('.claude-plugin'), '.claude-plugin is visible'); + assert.ok(snap.dirs.includes('.github'), '.github is visible'); + assert.ok(!snap.dirs.includes('.git'), '.git is skipped'); + assert.ok(!snap.dirs.includes('node_modules'), 'node_modules is skipped'); +}); + fs.rmSync(tmp, { recursive: true, force: true }); process.stdout.write(`\n${passed} passed\n`); diff --git a/test/plugin-shape.test.js b/test/plugin-shape.test.js new file mode 100644 index 0000000..fc314b1 --- /dev/null +++ b/test/plugin-shape.test.js @@ -0,0 +1,115 @@ +'use strict'; + +// Zero-dependency plugin-shape test. Run: node test/plugin-shape.test.js +// Validates the packaging surface Claude Code expects — so a broken plugin +// package (missing SKILL.md, drifted version, stale command name) fails CI +// before a user ever installs it. Reads the real repo, writes nothing. + +const path = require('path'); +const fs = require('fs'); +const assert = require('assert'); + +const root = path.resolve(__dirname, '..'); +const readJson = (rel) => JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8')); +const exists = (rel) => fs.existsSync(path.join(root, rel)); +const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8'); +const frontmatter = (text) => { + const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text); + return m ? m[1] : null; +}; + +let passed = 0; +function ok(name, fn) { + fn(); + passed++; + process.stdout.write(` ok ${name}\n`); +} + +const pkg = readJson('package.json'); +const plugin = readJson('.claude-plugin/plugin.json'); +const market = readJson('.claude-plugin/marketplace.json'); + +ok('manifests exist and parse', () => { + assert.ok(pkg.version, 'package.json has a version'); + assert.ok(plugin.version, 'plugin.json has a version'); + assert.ok(market.metadata && market.metadata.version, 'marketplace metadata has a version'); + assert.ok(Array.isArray(market.plugins) && market.plugins.length, 'marketplace lists plugins'); +}); + +ok('versions are aligned across every surface', () => { + const v = pkg.version; + assert.strictEqual(plugin.version, v, 'plugin.json version matches package.json'); + assert.strictEqual(market.metadata.version, v, 'marketplace metadata version matches package.json'); + assert.strictEqual(market.plugins[0].version, v, 'marketplace plugin version matches package.json'); +}); + +ok('CLI VERSION constants match package.json', () => { + const cli = require('../src/cli'); + const evolve = require('../src/evolve/index'); + assert.strictEqual(cli.VERSION, pkg.version, 'ratchet CLI version matches package.json'); + assert.strictEqual(evolve.VERSION, pkg.version, 'ratchet-evolve CLI version matches package.json'); +}); + +ok('hooks/hooks.json exists and parses', () => { + const hooks = readJson('hooks/hooks.json'); + assert.ok(hooks.hooks, 'hooks.json has a hooks map'); +}); + +ok('every bin target from package.json exists', () => { + for (const [name, rel] of Object.entries(pkg.bin || {})) { + assert.ok(exists(rel), `bin ${name} -> ${rel} exists`); + } + assert.ok(exists('bin/ratchet'), 'bin/ratchet exists'); + assert.ok(exists('bin/ratchet-evolve'), 'bin/ratchet-evolve exists'); +}); + +ok('required plugin directories exist', () => { + for (const d of ['skills', 'agents', 'hooks', 'bin', 'src']) { + assert.ok(fs.existsSync(path.join(root, d)), `${d}/ exists`); + } +}); + +const skillDirs = fs + .readdirSync(path.join(root, 'skills'), { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => e.name); + +ok('every skill has a SKILL.md with frontmatter + description', () => { + assert.ok(skillDirs.length > 0, 'at least one skill'); + for (const name of skillDirs) { + const rel = `skills/${name}/SKILL.md`; + assert.ok(exists(rel), `${rel} exists`); + const fm = frontmatter(read(rel)); + assert.ok(fm, `${name}: SKILL.md has YAML frontmatter`); + assert.ok(/(^|\n)description:/.test(fm), `${name}: SKILL.md frontmatter has a description`); + } +}); + +ok('every agent has a .md with frontmatter', () => { + const agents = fs.readdirSync(path.join(root, 'agents')).filter((f) => f.endsWith('.md')); + assert.ok(agents.length > 0, 'at least one agent'); + for (const f of agents) { + const fm = frontmatter(read(`agents/${f}`)); + assert.ok(fm, `agents/${f} has frontmatter`); + assert.ok(/(^|\n)description:/.test(fm), `agents/${f} frontmatter has a description`); + } +}); + +ok('the evolution command was renamed to /ratchet:evolve', () => { + assert.ok(skillDirs.includes('evolve'), 'skills/evolve exists'); + assert.ok(!skillDirs.includes('ratchet-evolve'), 'skills/ratchet-evolve was removed'); +}); + +ok('README command list matches the skill folders', () => { + const readme = read('README.md'); + for (const name of skillDirs) { + assert.ok(readme.includes(`/ratchet:${name}`), `README references /ratchet:${name}`); + } +}); + +ok('README does not mention removed command names', () => { + const readme = read('README.md'); + assert.ok(!readme.includes('/ratchet:ratchet-evolve'), 'no stale /ratchet:ratchet-evolve in README'); +}); + +process.stdout.write(`\n${passed} passed\n`); From c418bbebc751c024bc31128b0cafe2ebcd5a2963 Mon Sep 17 00:00:00 2001 From: Danny Gillespie Date: Fri, 3 Jul 2026 15:03:15 +0100 Subject: [PATCH 4/4] chore(release): v0.2.0 Proof Gate Bump package, plugin, and marketplace manifests to 0.2.0. Wire plugin-shape tests into npm test and add 'ratchet doctor' + 'ratchet-evolve status' to the CI smoke checks. Update manifest command references and the bug-report template. Document the release in CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/marketplace.json | 6 +-- .claude-plugin/plugin.json | 4 +- .github/ISSUE_TEMPLATE/bug_report.yml | 4 +- .github/workflows/ci.yml | 2 + CHANGELOG.md | 60 ++++++++++++++++++++++++++- package.json | 4 +- 6 files changed, 70 insertions(+), 10 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1a7d8b4..db053f2 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Torque Loop — a single-plugin marketplace.", - "version": "0.1.0" + "version": "0.2.0" }, "plugins": [ { "name": "ratchet", "source": "./", - "description": "Torque Loop: turns ambiguous work into shipped artifacts through target locking, adversarial validation, patching, and state serialization, plus a bounded /ratchet-evolve loop.", - "version": "0.1.0", + "description": "Torque Loop: turns ambiguous work into shipped artifacts through target locking, adversarial validation, patching, and state serialization, plus a bounded /ratchet:evolve loop.", + "version": "0.2.0", "author": { "name": "Danny Gillespie" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 1edf228..92f138a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,8 +1,8 @@ { "name": "ratchet", "displayName": "Torque Loop", - "version": "0.1.0", - "description": "Torque Loop turns ambiguous work into shipped artifacts through target locking, adversarial validation, patching, and state serialization — plus a bounded /ratchet-evolve loop. A stateful advancement engine, not a prompt library. Not affiliated with Anthropic.", + "version": "0.2.0", + "description": "Torque Loop turns ambiguous work into shipped artifacts through target locking, adversarial validation, patching, and state serialization — plus a bounded /ratchet:evolve loop. A stateful advancement engine, not a prompt library. Not affiliated with Anthropic.", "author": { "name": "Danny Gillespie" }, diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2a6902f..61c176e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -41,7 +41,7 @@ body: options: - A /ratchet:* slash command (plugin) - The `ratchet` CLI - - The `ratchet-evolve` CLI / loop + - The `ratchet-evolve` CLI / `/ratchet:evolve` loop - A hook (SessionStart / PostToolUse / Stop) - Install / marketplace - Docs / README @@ -53,7 +53,7 @@ body: attributes: label: Versions description: Output of `node --version` and `ratchet --version`, plus your OS. - placeholder: node v20.11.0 · ratchet 0.1.0 · Windows 11 + placeholder: node v20.11.0 · ratchet 0.2.0 · Windows 11 validations: required: true - type: textarea diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cf6cc7..4089389 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,4 +34,6 @@ jobs: run: | node bin/ratchet --version node bin/ratchet --help + node bin/ratchet doctor node bin/ratchet-evolve --help + node bin/ratchet-evolve status diff --git a/CHANGELOG.md b/CHANGELOG.md index b932287..b57ee82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.0] - 2026-07-03 — Proof Gate + +Hardening, not expansion. This release makes the loop's philosophy mechanically true: +a cleaner command surface, an enforced proof gate, trustworthy state, and a verified +plugin shape. **No proof → no keep** is now enforced by code, not just by convention. + +### Added + +- **Proof gate.** `ratchet-evolve log append` now rejects a `KEEP` verdict that lacks + verification evidence: a failed result, or no command/manual checks, or a `manual` + result with no explicit checks, all throw before anything is written. `REVERT` and + `ASK` are exempt by design. +- **`ratchet compile done`** — atomically stamps `lastCompileAt`, clears the dirty flag, + and records a `compile.done` history event, so the Stop hook stops nagging after a + compile. +- **`ratchet doctor`** — a plugin health check: Node version, manifest parsing, version + alignment, required directories, every `SKILL.md` frontmatter/description, `hooks.json`, + bin targets, state-dir writability, and a live repo-snapshot probe. Exits non-zero on + any failure. +- **`ratchet-evolve next`** — reads the last recorded next edge (`--json` supported). +- **Manual-evidence templates** — `templates/evolve-event.json`, + `templates/evolve-manual-checks.md`, and `templates/evolve-report.md`. +- **Plugin-shape test suite** (`test/plugin-shape.test.js`) — asserts manifests parse and + align, CLI version constants match `package.json`, every skill has a valid `SKILL.md`, + agents have frontmatter, bin targets exist, and the README command list matches the + skill folders (and mentions no removed command names). Wired into `npm test` and CI. + +### Changed + +- **Renamed the evolution command** from `/ratchet:ratchet-evolve` to `/ratchet:evolve` + (skill folder `skills/ratchet-evolve/` → `skills/evolve/`). The helper CLI binary stays + `ratchet-evolve`. No compatibility alias is kept. +- `ratchet-evolve status` now reports kept / reverted / **asks** / active targets / last + verdict / next edge. +- CLI version strings are now derived from `package.json`, making it the single source of + truth for the plugin, both CLIs, and the marketplace manifest. +- The compile flow (`/ratchet:compile`, `ratchet-scribe`) now calls `ratchet compile done` + instead of setting `lastCompileAt` by hand. +- Bumped package, plugin, and marketplace manifests to `0.2.0`. +- CI smoke-checks now include `ratchet doctor` and `ratchet-evolve status`. + +### Fixed + +- `--version` printed the full help text instead of the version, on both CLIs. +- The dirty-state Stop-hook reminder persisted after a compile; `ratchet compile done` + now clears it. +- The evolution journal accepted `KEEP` events with no verification evidence. +- Repo snapshots hid plugin-critical dot directories (`.claude-plugin`, `.github`, + `.claude`, `.ratchet`) via a blanket dot-dir skip; an allowlist now surfaces them while + still ignoring `.git`, `node_modules`, and caches. +- Malformed `state.json` / `ledger.json` were silently discarded; they are now backed up + to `.corrupt..json` before a fresh file is created. + +### Removed + +- `/ratchet:ratchet-evolve` (renamed to `/ratchet:evolve`). + ## [0.1.0] - 2026-07-03 Initial public release. @@ -31,5 +88,6 @@ Initial public release. - Single-plugin marketplace manifest so the repo installs directly as a Claude Code plugin. - Zero-dependency smoke test suites for the state engine and the evolution helpers. -[Unreleased]: https://github.com/TheLucidTech/torque-loop/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/TheLucidTech/torque-loop/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/TheLucidTech/torque-loop/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/TheLucidTech/torque-loop/releases/tag/v0.1.0 diff --git a/package.json b/package.json index 785a48b..a74c8ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@torque-loop/plugin", - "version": "0.1.0", + "version": "0.2.0", "description": "Torque Loop — a Claude Code plugin that evolves ambiguity into shipped, tested, serialized artifacts through evidence-gated loops. Not affiliated with Anthropic.", "license": "MIT", "author": "Danny Gillespie", @@ -14,7 +14,7 @@ "node": ">=18" }, "scripts": { - "test": "node test/cli.test.js && node test/evolve.test.js", + "test": "node test/cli.test.js && node test/evolve.test.js && node test/plugin-shape.test.js", "ratchet": "node bin/ratchet", "ratchet-evolve": "node bin/ratchet-evolve" },