diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 07b4b47..cbd3fad 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.3.0" + "version": "0.4.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.3.0", + "version": "0.4.0", "author": { "name": "Danny Gillespie" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index becf134..107cb7b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ratchet", "displayName": "Torque Loop", - "version": "0.3.0", + "version": "0.4.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/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 1fe8e28..05006f5 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "torque-loop", - "version": "0.3.0", + "version": "0.4.0", "description": "Torque Loop evolves ambiguous work into shipped, tested, serialized artifacts through evidence-gated loops.", "author": { "name": "Danny Gillespie", diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bcffa9..b6a50aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.0] - 2026-07-03 — Aperture + +0.2 gated proof; 0.3 gated the *seam* of that proof. 0.4 adds the dial that decides how +much of the loop to run at all: **spend the full ratchet only when uncertainty earns it, +and snap when it doesn't.** + +### Added + +- **Aperture dial** — `ratchet score aperture ` scores five uncertainty dimensions + (`ambiguity`, `terrain`, `taste`, `blastRadius`, `reversibility`, each 0–2), maps the + total to a level A0–A4, and returns the exact ratchet skill sequence to run at that + depth — from `build → verify` (A0 Snap) up to `lock → cut → decide` with **no build** + until constraints are locked (A4 Max). A missing dimension defaults to neutral (1), + never certain (0), so unknown uncertainty opens the aperture rather than closing it. +- **Aperture Read in `/ratchet:ignite`** — the master loop now meters itself: it scores + the task first and runs only the depth uncertainty earns, instead of always running all + seven steps. `/ratchet:lock` gained a matching "meter what follows" note. + +### Changed + +- `/ratchet:ignite` reframed from "run all seven; do not skip" to "run the aperture's + metered sequence" — the seven-step pipeline is now the A2 default, not the floor. +- Bumped package, both plugin manifests, and the marketplace manifest to `0.4.0`. + ## [0.3.0] - 2026-07-03 — Seam Gate Where 0.2 made the loop **require proof**, 0.3 asks whether the proof is about the @@ -157,7 +181,8 @@ 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.3.0...HEAD +[Unreleased]: https://github.com/TheLucidTech/torque-loop/compare/v0.4.0...HEAD +[0.4.0]: https://github.com/TheLucidTech/torque-loop/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/TheLucidTech/torque-loop/compare/v0.2.0...v0.3.0 [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/README.md b/README.md index 1d336c5..913723b 100644 --- a/README.md +++ b/README.md @@ -168,8 +168,10 @@ one shared data directory. ## Commands -In Claude Code, run `/ratchet:ignite` when you don't know which command to run — it drives -the full loop. In Codex, ask it to use the matching Torque Loop skill, for example +In Claude Code, run `/ratchet:ignite` when you don't know which command to run — it reads the +task's uncertainty with the **aperture dial** (`ratchet score aperture`) and runs only the loop +depth that earns it: snap to `build → verify` when the task is trivial, open to the full loop +when it isn't. In Codex, ask it to use the matching Torque Loop skill, for example `torque-loop:ignite` or `torque-loop:evolve`. ### Core loop diff --git a/package.json b/package.json index ca5d68f..5efa47d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@torque-loop/plugin", - "version": "0.3.0", + "version": "0.4.0", "description": "Torque Loop — a Claude Code and Codex plugin that evolves ambiguity into shipped, tested, serialized artifacts through evidence-gated loops.", "license": "MIT", "author": "Danny Gillespie", diff --git a/skills/ignite/SKILL.md b/skills/ignite/SKILL.md index 06d842b..6d1e053 100644 --- a/skills/ignite/SKILL.md +++ b/skills/ignite/SKILL.md @@ -27,7 +27,39 @@ If `ratchet` is not on PATH, call the plugin-local CLI as `$CLAUDE_PLUGIN_ROOT`; in Codex local development, use this repo root or the installed plugin path shown by `codex plugin list --json`. -## The pipeline (run all seven; do not skip) +## Step 1 — Aperture Read (meter the loop) + +Before running the pipeline, score how much of it this task earns. Rate five +uncertainty dimensions 0–2 and let the CLI set the depth: + +``` +ratchet score aperture '{"ambiguity":_,"terrain":_,"taste":_,"blastRadius":_,"reversibility":_}' +``` + +- **ambiguity** — goals / acceptance criteria / constraints unclear? +- **terrain** — codebase / domain / tooling unfamiliar? +- **taste** — would the user recognize the right answer only once shown? +- **blastRadius** — could it hit data, security, production, many files/users? +- **reversibility** — hard to undo or validate? + +The result names an aperture and the exact ratchet sequence to run: + +| Level | Score | Run | +|---|---|---| +| A0 Snap | 0–2 | `build → verify` — do it, prove it, stop. | +| A1 Narrow | 3–4 | `lock → build → verify → compile`. | +| A2 Working | 5–6 | the full seven-step pipeline below. | +| A3 Wide | 7–8 | add `auction` + `decide`; prototype / reference before build. | +| A4 Max | 9–10 | `lock → cut → decide`, then **STOP — produce options, do not build** until constraints are locked. | + +Widen one level after failed verification, a surprising repo constraint, or +conflicting requirements; narrow one after the user locks a decision or the work +goes mechanical. + +**Do not inflate ceremony.** Running the full loop on an A0 task is the same failure +as skipping it on an A4 task. Spend proof where uncertainty earns it. + +## The pipeline (the A2 default — run the aperture's metered sequence, not always all seven) 1. **Lock the target.** Apply the discipline of `/ratchet:lock`. Produce: literal object, operation, output shape, real outcome, proof-of-done, smallest artifact, @@ -64,6 +96,7 @@ plugin path shown by `codex plugin list --json`. Return exactly these blocks, in order: ``` +APERTURE: — ran: LOCKED TARGET: CHOSEN BOTTLENECK: KILL-ASSUMPTION: — test: diff --git a/skills/lock/SKILL.md b/skills/lock/SKILL.md index cebded4..517944e 100644 --- a/skills/lock/SKILL.md +++ b/skills/lock/SKILL.md @@ -56,5 +56,18 @@ ratchet state set objective " , proof: rank obstacles ([{name,leverage,certainty,speed,risk}])', ' ratchet score confidence compute session confidence + loop-clear', + ' ratchet score aperture meter loop depth from uncertainty ({ambiguity,terrain,taste,blastRadius,reversibility} 0-2)', '', 'CONTEXT', ' ratchet snapshot repo [path] cheap repo read (files, dirs, git)', diff --git a/src/markdown.js b/src/markdown.js index 1ae83ff..0849443 100644 --- a/src/markdown.js +++ b/src/markdown.js @@ -91,6 +91,24 @@ function confidence(state) { } // `○` = still draining confidence, `●` = terminal (resolved/waived/superseded). +// The aperture dial: how much of the loop this task earns, and the metered +// sequence of ratchet skills to run at that depth. +function aperture(a) { + const d = a.dimensions || {}; + const o = []; + o.push(`### Aperture: ${a.level} ${a.name} — score ${a.score}/10`); + o.push( + `Uncertainty: ambiguity ${d.ambiguity} · terrain ${d.terrain} · taste ${d.taste} · blast ${d.blastRadius} · reversibility ${d.reversibility}` + ); + o.push( + a.implement + ? '**Implement:** yes — run the loop at this depth.' + : '**Implement:** NO — lock constraints and produce options first; do not build yet.' + ); + o.push(`**Metered loop:** ${a.sequence.map((s) => `\`/ratchet:${s}\``).join(' → ')}`); + return o.join('\n'); +} + function defectList(defects) { if (!defects || !defects.length) return '_no defects recorded_'; const lines = ['### Defects']; @@ -205,4 +223,4 @@ function fullExport(state, ledger) { return out.join('\n'); } -module.exports = { stateSummary, friction, confidence, defectList, defectOne, gitStatusRefs, repoSnapshot, fullExport, dash, bullets }; +module.exports = { stateSummary, friction, confidence, aperture, defectList, defectOne, gitStatusRefs, repoSnapshot, fullExport, dash, bullets }; diff --git a/src/scoring.js b/src/scoring.js index 70f9c00..e06b2dd 100644 --- a/src/scoring.js +++ b/src/scoring.js @@ -123,4 +123,67 @@ function scoreConfidence(state) { return { score, band, loopClear, penalties, openDefects: openDefects.length }; } -module.exports = { scoreFriction, scoreConfidence, clamp, isDefectOpen, TERMINAL_DEFECT_STATUSES }; +// --------------------------------------------------------------------------- +// Aperture (0.4). The dial that meters how much of the loop to run. Score five +// uncertainty dimensions 0..2, sum to 0..10, map to A0..A4, and emit the ratchet +// skill sequence to run at that depth. The point is anti-ceremony: spend the +// full loop only when uncertainty earns it; snap when it doesn't. +// ambiguity : are goals / acceptance criteria / constraints unclear? +// terrain : is the codebase / domain / tooling unfamiliar? +// taste : would the user recognize the right answer only once shown? +// blastRadius : could it hit data, security, production, many files/users? +// reversibility : is the change hard to undo or validate? +// --------------------------------------------------------------------------- + +const APERTURE_DIMENSIONS = ['ambiguity', 'terrain', 'taste', 'blastRadius', 'reversibility']; + +const APERTURE_LEVELS = [ + { max: 2, level: 'A0', name: 'Snap', implement: true, sequence: ['build', 'verify'] }, + { max: 4, level: 'A1', name: 'Narrow', implement: true, sequence: ['lock', 'build', 'verify', 'compile'] }, + { max: 6, level: 'A2', name: 'Working', implement: true, sequence: ['lock', 'cut', 'build', 'attack', 'patch', 'verify', 'compile'] }, + { max: 8, level: 'A3', name: 'Wide', implement: true, sequence: ['lock', 'auction', 'cut', 'decide', 'build', 'attack', 'patch', 'compile'] }, + { max: 10, level: 'A4', name: 'Max', implement: false, sequence: ['lock', 'cut', 'decide'] }, +]; + +// A missing dimension defaults to 1 (neutral), never 0 — treating unknown +// uncertainty as "certain" would under-open the aperture, the exact failure the +// dial exists to prevent. +function apertureDim(dims, key) { + const v = dims ? dims[key] : undefined; + return v == null ? 1 : clamp(v, 0, 2); +} + +function scoreAperture(dims) { + if (dims == null || typeof dims !== 'object' || Array.isArray(dims)) { + throw new Error( + 'aperture expects a JSON object of dimensions (ambiguity, terrain, taste, blastRadius, reversibility), each 0-2' + ); + } + const scored = {}; + let total = 0; + for (const key of APERTURE_DIMENSIONS) { + const v = apertureDim(dims, key); + scored[key] = v; + total += v; + } + const band = APERTURE_LEVELS.find((b) => total <= b.max) || APERTURE_LEVELS[APERTURE_LEVELS.length - 1]; + return { + score: total, // 0..10 + level: band.level, // A0..A4 + name: band.name, + implement: band.implement, // A4: do NOT build until constraints are locked + sequence: band.sequence.slice(), // ratchet skills to run at this depth + dimensions: scored, + }; +} + +module.exports = { + scoreFriction, + scoreConfidence, + scoreAperture, + clamp, + isDefectOpen, + TERMINAL_DEFECT_STATUSES, + APERTURE_DIMENSIONS, + APERTURE_LEVELS, +}; diff --git a/test/cli.test.js b/test/cli.test.js index 1ede53c..699842f 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -191,6 +191,48 @@ ok('friction rejects non-array', () => { assert.throws(() => scoring.scoreFriction({ not: 'an array' }), /array/); }); +// --- aperture dial (0.4) ---------------------------------------------------- + +ok('aperture meters loop depth from uncertainty', () => { + const snap = scoring.scoreAperture({ ambiguity: 0, terrain: 0, taste: 0, blastRadius: 0, reversibility: 0 }); + assert.strictEqual(snap.score, 0); + assert.strictEqual(snap.level, 'A0'); + assert.strictEqual(snap.implement, true); + assert.deepStrictEqual(snap.sequence, ['build', 'verify']); + + const mid = scoring.scoreAperture({ ambiguity: 1, terrain: 1, taste: 1, blastRadius: 1, reversibility: 1 }); + assert.strictEqual(mid.score, 5); + assert.strictEqual(mid.level, 'A2'); + + const max = scoring.scoreAperture({ ambiguity: 2, terrain: 2, taste: 2, blastRadius: 2, reversibility: 2 }); + assert.strictEqual(max.score, 10); + assert.strictEqual(max.level, 'A4'); + assert.strictEqual(max.implement, false, 'A4 must not build before constraints are locked'); + assert.ok(!max.sequence.includes('build'), 'A4 produces options, not code'); +}); + +ok('aperture defaults a missing dimension to neutral, not certain', () => { + // 4 missing dims default to 1 each (=4) + blastRadius 2 = 6 → A2, not A0 + const a = scoring.scoreAperture({ blastRadius: 2 }); + assert.strictEqual(a.score, 6); + assert.strictEqual(a.level, 'A2'); +}); + +ok('aperture clamps out-of-range dimensions and rejects non-objects', () => { + const a = scoring.scoreAperture({ ambiguity: 9, terrain: -3, taste: 2, blastRadius: 0, reversibility: 0 }); + assert.strictEqual(a.dimensions.ambiguity, 2); + assert.strictEqual(a.dimensions.terrain, 0); + assert.throws(() => scoring.scoreAperture([1, 2, 3]), /object/); + assert.throws(() => scoring.scoreAperture('nope'), /object/); +}); + +ok('aperture renders with its metered ratchet loop', () => { + const out = md.aperture(scoring.scoreAperture({ ambiguity: 2, terrain: 1, taste: 2, blastRadius: 1, reversibility: 1 })); + assert.ok(/Aperture: A3 Wide/.test(out)); + assert.ok(/Metered loop/.test(out)); + assert.ok(/ratchet:build/.test(out)); +}); + ok('markdown render does not throw on populated state', () => { const out = md.stateSummary(state.loadState(cwd)); assert.ok(out.includes('Ratchet state'));