From 846949aee530dfe0cf48cf4a802f7ef2fee03411 Mon Sep 17 00:00:00 2001 From: Danny Gillespie Date: Mon, 6 Jul 2026 04:53:36 +0100 Subject: [PATCH] feat(aperture): route A3-A4 through /ratchet:map before build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The aperture dial named the loop depth but not the pre-build fog gate, so /ratchet:ignite relied on the operator to remember /ratchet:map. Fold it into the metered sequence: A3 = lock -> map -> auction -> cut -> decide -> build ..., A4 = lock -> map -> cut -> decide (still builds nothing). Add a derived mapRequired flag — true at A3+, or when a single dimension the summed score under-weights demands it (taste=2, or terrain=2 with any ambiguity) — so a high-taste "know it when I see it" task earns a map even at a low overall score. md.aperture() renders "Pre-build map: required" and /ratchet:ignite routes into the map on its own. Guarded by a new cli.test assertion: sequence routing, the flag, the render line, the two low-score overrides, and the negative case. npm test green (cli 49 · evolve 19 · plugin-shape 14); ratchet doctor healthy. No schema change; A0-A2 sequences untouched; A4 still produces options, not code. Author-model: claude-opus-4-8[1m] Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 9 +++++++-- README.md | 7 +++++-- skills/ignite/SKILL.md | 10 ++++++++-- src/markdown.js | 3 +++ src/scoring.js | 13 +++++++++++-- test/cli.test.js | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 66 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff397ee..6e529d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 copy-paste implementation prompt before any code is written. CLI-backed (`artifact kind:"unknown-map"`, decisions, assumptions, open loops), so its open items drain `ratchet score confidence` until closed; no schema change. Method grafted from - dzhng/skills `explore-unknowns`, expressed in ratchet's own vocabulary. Aperture - auto-routing (scoring the task straight into the map) is deferred to a follow-up. + dzhng/skills `explore-unknowns`, expressed in ratchet's own vocabulary. +- **Aperture routes through the map.** `ratchet score aperture` now folds `/ratchet:map` + into the A3–A4 metered sequences (before `build`) and returns a `mapRequired` flag — + true at A3+, or when a single dimension the summed score under-weights demands it + (`taste = 2`, or `terrain = 2` with any `ambiguity`). `md.aperture()` renders + **`Pre-build map: required`** so `/ratchet:ignite` routes high-uncertainty work through + the fog gate instead of relying on the operator to remember. A4 still builds nothing. ## [0.5.0] - 2026-07-04 — Receipt diff --git a/README.md b/README.md index 2d66d15..6a8e7a7 100644 --- a/README.md +++ b/README.md @@ -206,8 +206,11 @@ when it isn't. In Codex, ask it to use the matching Torque Loop skill, for examp > unfamiliar terrain, reference-implementation ports, or "I'll know it when I see it" > taste. Walk the four unknown-quadrants (known knowns · known unknowns · unknown knowns · > unknown unknowns) and hand over the map *before* `/ratchet:build`; at **A4**, stop after -> the map until constraints are locked. What you deliver is the map, not a build — a wrong -> assumption caught on the map is a one-line fix; caught mid-build it is a rewrite. +> the map until constraints are locked. The aperture read raises **`Pre-build map: +> required`** for exactly these cases — including a high-`taste` or unfamiliar-`terrain` +> task the summed score would under-rate — so `/ratchet:ignite` routes into the map on its +> own. What you deliver is the map, not a build — a wrong assumption caught on the map is a +> one-line fix; caught mid-build it is a rewrite. ### Specialized diff --git a/skills/ignite/SKILL.md b/skills/ignite/SKILL.md index 6d1e053..05f7589 100644 --- a/skills/ignite/SKILL.md +++ b/skills/ignite/SKILL.md @@ -49,8 +49,14 @@ The result names an aperture and the exact ratchet sequence to 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. | +| A3 Wide | 7–8 | `lock → map → auction → cut → decide → build …` — map the fog before you build. | +| A4 Max | 9–10 | `lock → map → cut → decide`, then **STOP — produce options, do not build** until constraints are locked. | + +When the read reports **`Pre-build map: required`**, run `/ratchet:map` before any +build step and hand over the four-quadrant map first. The dial raises it at A3–A4, and +also on a single dominant dimension the score under-weights — "I'll know it when I see +it" taste, or unfamiliar terrain with any goal ambiguity — so a high-taste task can earn +a map even at a low overall score. At A4, stop after the map 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 diff --git a/src/markdown.js b/src/markdown.js index a89fbec..62fe795 100644 --- a/src/markdown.js +++ b/src/markdown.js @@ -143,6 +143,9 @@ function aperture(a) { : '**Implement:** NO — lock constraints and produce options first; do not build yet.' ); o.push(`**Metered loop:** ${a.sequence.map((s) => `\`/ratchet:${s}\``).join(' → ')}`); + if (a.mapRequired) { + o.push('**Pre-build map:** required — run `/ratchet:map` before `/ratchet:build`; map the fog first.'); + } if (a.scope) o.push(`_Scope: ${a.scope}._`); return o.join('\n'); } diff --git a/src/scoring.js b/src/scoring.js index 9ce8c2e..08aea84 100644 --- a/src/scoring.js +++ b/src/scoring.js @@ -284,8 +284,8 @@ 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'] }, + { max: 8, level: 'A3', name: 'Wide', implement: true, sequence: ['lock', 'map', 'auction', 'cut', 'decide', 'build', 'attack', 'patch', 'compile'] }, + { max: 10, level: 'A4', name: 'Max', implement: false, sequence: ['lock', 'map', 'cut', 'decide'] }, ]; // A missing dimension defaults to 1 (neutral), never 0 — treating unknown @@ -310,12 +310,21 @@ function scoreAperture(dims) { total += v; } const band = APERTURE_LEVELS.find((b) => total <= b.max) || APERTURE_LEVELS[APERTURE_LEVELS.length - 1]; + // The pre-build fog gate (/ratchet:map) is earned by high overall uncertainty + // (A3+), OR by a single dimension the summed score under-weights: "know it when + // I see it" taste, or unfamiliar terrain paired with any goal ambiguity. Those + // can sit below the A3 band yet still make a confident build the wrong move — so + // the flag can fire even when the metered sequence for this band does not + // include `map`, signalling "map first anyway." + const mapRequired = + total >= 7 || scored.taste === 2 || (scored.terrain === 2 && scored.ambiguity >= 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 + mapRequired, // route through /ratchet:map before building (see above) dimensions: scored, // This reading is only valid for the task as scored, at scoring time. scope: 'the one task scored, at scoring time — re-score if the task or its constraints change', diff --git a/test/cli.test.js b/test/cli.test.js index 76312da..0f67b68 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -238,6 +238,38 @@ ok('aperture renders with its metered ratchet loop', () => { assert.ok(/ratchet:build/.test(out)); }); +ok('aperture routes high-uncertainty work through /ratchet:map', () => { + // A3/A4 route through the pre-build fog gate, before build. + const wide = scoring.scoreAperture({ ambiguity: 2, terrain: 2, taste: 1, blastRadius: 1, reversibility: 1 }); // 7 → A3 + assert.strictEqual(wide.level, 'A3'); + assert.ok(wide.sequence.includes('map'), 'A3 sequence routes through map'); + assert.ok(wide.sequence.indexOf('map') < wide.sequence.indexOf('build'), 'map comes before build'); + assert.ok(wide.mapRequired, 'A3 requires a pre-build map'); + + const max = scoring.scoreAperture({ ambiguity: 2, terrain: 2, taste: 2, blastRadius: 2, reversibility: 2 }); // 10 → A4 + assert.ok(max.sequence.includes('map'), 'A4 routes through map'); + assert.ok(!max.sequence.includes('build'), 'A4 still produces options, not code'); + assert.ok(max.mapRequired); + + // The single-dimension override: "know it when I see it" taste warrants a map + // even when the summed score sits at A0. + const taste = scoring.scoreAperture({ ambiguity: 0, terrain: 0, taste: 2, blastRadius: 0, reversibility: 0 }); // 2 → A0 + assert.strictEqual(taste.level, 'A0'); + assert.ok(taste.mapRequired, 'high taste requires a map even at a low score'); + + // Unfamiliar terrain + any goal ambiguity, below the A3 band, still earns it. + const fog = scoring.scoreAperture({ ambiguity: 1, terrain: 2, taste: 0, blastRadius: 0, reversibility: 0 }); // 3 → A1 + assert.ok(fog.mapRequired, 'unfamiliar terrain with ambiguity earns a map'); + + // Plain low-uncertainty work does not — the flag must not fire on everything. + const narrow = scoring.scoreAperture({ ambiguity: 1, terrain: 0, taste: 0, blastRadius: 1, reversibility: 1 }); // 3 → A1 + assert.ok(!narrow.mapRequired, 'low-uncertainty work skips the map'); + + // The render surfaces the requirement, and stays quiet when it does not apply. + assert.ok(/Pre-build map:.*required/.test(md.aperture(wide)), 'render names the map requirement'); + assert.ok(!/Pre-build map/.test(md.aperture(narrow)), 'render stays quiet when a map is not required'); +}); + ok('markdown render does not throw on populated state', () => { const out = md.stateSummary(state.loadState(cwd)); assert.ok(out.includes('Ratchet state'));