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
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
27 changes: 26 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <json>` 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
Expand Down Expand Up @@ -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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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": "@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",
Expand Down
35 changes: 34 additions & 1 deletion skills/ignite/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -64,6 +96,7 @@ plugin path shown by `codex plugin list --json`.
Return exactly these blocks, in order:

```
APERTURE: <A0–A4 level, score/10> — ran: <the metered sequence>
LOCKED TARGET: <one line>
CHOSEN BOTTLENECK: <one line + why it beats the runner-up>
KILL-ASSUMPTION: <the one that most needs a test> — test: <cheapest falsification>
Expand Down
13 changes: 13 additions & 0 deletions skills/lock/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,18 @@ ratchet state set objective "<operation> <object> → <output shape>, proof: <pr
ratchet state set phase lock
```

## Meter what follows

Once the target is locked, score its uncertainty so you spend only the loop it
earns — don't reflexively run the full ratchet on a reversible one-liner:

```
ratchet score aperture '{"ambiguity":_,"terrain":_,"taste":_,"blastRadius":_,"reversibility":_}'
```

- **A0–A1** → go straight to `/ratchet:build`, then `/ratchet:verify`.
- **A2–A3** → run `/ratchet:ignite`'s metered pipeline.
- **A4** → do not build yet; produce options and lock constraints first.

Then point forward: the locked target's smallest artifact is now the target of
`/ratchet:build`, or run `/ratchet:auction` first if the path to it is blocked.
7 changes: 6 additions & 1 deletion src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,12 @@ function cmdScore(cwd, sub, rest, asJson) {
state.saveState(cwd, s);
return out(md.confidence(s));
}
case 'aperture': {
const result = scoring.scoreAperture(readPayload(rest[0]));
return out(asJson ? JSON.stringify(result, null, 2) : md.aperture(result));
}
default:
throw new Error(`unknown score subcommand: ${sub} (friction | confidence)`);
throw new Error(`unknown score subcommand: ${sub} (friction | confidence | aperture)`);
}
}

Expand Down Expand Up @@ -632,6 +636,7 @@ function help() {
'SCORING',
' ratchet score friction <json> rank obstacles ([{name,leverage,certainty,speed,risk}])',
' ratchet score confidence compute session confidence + loop-clear',
' ratchet score aperture <json> meter loop depth from uncertainty ({ambiguity,terrain,taste,blastRadius,reversibility} 0-2)',
'',
'CONTEXT',
' ratchet snapshot repo [path] cheap repo read (files, dirs, git)',
Expand Down
20 changes: 19 additions & 1 deletion src/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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 };
65 changes: 64 additions & 1 deletion src/scoring.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
42 changes: 42 additions & 0 deletions test/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
Loading