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.6.0"
"version": "0.7.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.6.0",
"version": "0.7.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.6.0",
"version": "0.7.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.6.0",
"version": "0.7.0",
"description": "Torque Loop evolves ambiguous work into shipped, tested, serialized artifacts through evidence-gated loops.",
"author": {
"name": "Danny Gillespie",
Expand Down
60 changes: 59 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.7.0] - 2026-07-06 — Probe Gate

0.6 gated the fog: *no map → no confident build*. It left two holes: fog the dial named
could live only on stdout (skipped maps leave no trace, so unrecorded uncertainty never
drains confidence), and some fog cannot be mapped by reading or asking at all — it only
appears by touching. The probe closes both: **a probe is a build whose proof-of-done is
knowledge, not code.**

### Added

- **The probe — build-for-learn as a first-class map closure.** Known unknowns now close
by `user | territory | probe | parked | OPEN`, and `/ratchet:map` commissions a probe
(`templates/probe-card.md`: unknown · hypothesis · smallest reversible touch · allowed
surfaces · proof of learning · disposal rule · durable output · promotion rule · stop
condition) when only touching the repo can answer. No new schema: a probe is an
artifact (`kind:"probe"`, hole `disposal: pending`) whose hole drains confidence until
the code is disposed via the existing gated verb (`ratchet retract --reason`) or
explicitly promoted through a fresh `/ratchet:build` under the full proof/seam gates.
Both invariants are CLI-enforced, not conventions: `artifact add` injects the
`disposal: pending` hole on any probe that omits it, and a probe retraction must state
its outcome (`--reason` starting `disposed:` or `promoted:`; a promotion requires
`--superseded-by <the keep-build>`).
**Probe code dies; probe findings live** — as a decision, assumption, open loop,
defect, or map delta. `/ratchet:build` gained the build-for-learn mode (probe code is
never implementation progress); `/ratchet:handoff` surfaces probe outcomes so a
receiver cannot mistake residue for kept work.
- **The undrained-fog fix: scored fog can no longer live only on stdout.**
`ratchet score aperture` with `mapRequired` now serializes the fog as an open loop
(`fog: pre-build map required …`) for writer callers on **both output modes** — text
and `--json` alike, so programmatic consumers cannot bypass the write (the JSON result
carries `recordedFog`); propose-only agents still get a footprint-free read — and
`artifact add kind:"unknown-map"` closes that loop when the map lands. Both ends live
at the CLI boundary, so fog the dial named drains confidence, warns cold starts, and
survives handoff even if the session never runs the map.
- **Cold-start fog checks.** The control-plane scan now warns on live probe artifacts
(residue the next session could mistake for kept work) and FAILs when steering says
build while a `fog:` loop is open. A retracted probe is a *completed* probe, not dead
steering — the live-steering check exempts `kind:"probe"`.
- **Receipt fog card.** STATE now carries `fog` (live unknown-maps with OPEN item counts,
unmapped fog loops, probes live/disposed) and renders it — emptiness stated
(`Fog: none recorded`), never omitted.
- **Map convergence rule.** Every OPEN item leaving handover names its route out
(ask-user · probe · park owner+reason · assumption+killTest · defect). An OPEN item
with no route is a stall with a receipt, not mapped fog.
- **Surprise tripwires for fog the dial never saw.** `/ratchet:attack` flags
wrong-premise findings as fog to serialize (not merely defects) and recommends
re-entering `/ratchet:map` at two or more; `/ratchet:build` re-runs the aperture after
two deviations (or one that reshapes the locked target) and stops when it says map.
These are **prompt-level** guidance, unlike the CLI-enforced pieces above (fog-loop
write/close, probe invariants, cold-start checks) — a session that skips the skills
skips the tripwires; only the dial's own reads are boundary-recorded.

### Changed

- Session confidence names its epistemics out loud: the scope now reads **recorded loop
pressure, not correctness** — unrecorded fog is invisible to it.

## [0.6.0] - 2026-07-06 — Fog Gate

0.2 gated proof (*no proof → no keep*); 0.3 gated the *seam* of that proof
Expand Down Expand Up @@ -249,7 +306,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.6.0...HEAD
[Unreleased]: https://github.com/TheLucidTech/torque-loop/compare/v0.7.0...HEAD
[0.7.0]: https://github.com/TheLucidTech/torque-loop/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/TheLucidTech/torque-loop/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/TheLucidTech/torque-loop/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/TheLucidTech/torque-loop/compare/v0.3.0...v0.4.0
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,13 @@ when it isn't. In Codex, ask it to use the matching Torque Loop skill, for examp
> 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.
> own, and it records that fog as an open loop so it drains confidence until the map lands.
> 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. When an unknown can only be answered by
> touching the repo, the map commissions a **probe**: a time-boxed, reversible,
> build-for-learn spike whose code dies and whose finding lives as a map delta
> (`templates/probe-card.md`). Probe code never ships by inertia — keeping it requires an
> explicit promotion through the normal proof/seam gates.

### Specialized

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.6.0",
"version": "0.7.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
5 changes: 4 additions & 1 deletion reference/PROMPTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ then walk four quadrants in order, naming the current one:
assumption separately and say you will treat it as true until I correct it.
2. Known unknowns — ask ONE question at a time, widest-impact first. Give lettered options
with a recommended answer so I react in a few characters. Close each by:
user answer | territory (you researched it, show question + finding) | OPEN (+ what unblocks it).
user answer | territory (you researched it, show question + finding) | probe (a time-boxed
throwaway build whose only durable output is the finding — probe code dies, probe findings
live) | OPEN (+ what unblocks it AND the route that will close it: ask-user, probe,
park with owner, assumption + kill test, or defect).
3. Unknown knowns — surface my tacit taste and context. Don't make me imagine it; put
something concrete in front of me to react to (sample data, a throwaway mock, competing
design directions). Record what each reaction reveals.
Expand Down
6 changes: 6 additions & 0 deletions skills/attack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ Speak in each voice distinctly. Do not blur them into generic "concerns".
the contradicting case or mark the claim unproven.
- **Name the smallest patch** each finding needs — REMOVE / ADD / CHANGE. You specify the
delta; you do not apply it here.
- **Flag fog, not just defects.** A finding that says the *premise* was wrong — a hidden
convention, wrong-by-default data, a claim that contradicts the locked target or the
unknowns-map — is fog the aperture missed, not merely a bug. Serialize it
(`ratchet state append assumptions ...` with a kill test, or an `openLoops` entry
prefixed `fog:`), and at two or more such findings recommend `/ratchet:map` before any
further patching: you are patching inside unmapped terrain.

## Output contract

Expand Down
17 changes: 17 additions & 0 deletions skills/build/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ assumed, do not silently absorb it. Record a deviation (`templates/deviation-not
`decision`, `openLoop`, or `defect`, and add it to the map's *Deviations during build*
section. The map is a living record through the build, not a pre-build formality.

**Re-score on surprise.** Two deviations — or one that reshapes the locked target — mean
the aperture was scored too low. Re-run `ratchet score aperture`; if it now says
`Pre-build map: required`, stop building and run `/ratchet:map`. Patching deeper into
unmapped terrain is the confident-build-into-fog the dial exists to refuse.

**Build-for-learn (probe mode).** If the active task is a probe (a live `kind:"probe"`
artifact — see `templates/probe-card.md`), you are building to learn, not to keep:

- Build only the smallest artifact that produces the card's proof-of-learning; stay
inside its allowed surfaces; stop at its stop condition.
- The probe's completion is a map/state delta — a decision, assumption, open loop, or
defect — never a code diff. Probe code existing is not implementation progress; do not
claim it as such.
- Then dispose: revert the code and `ratchet retract <probe-id> --reason "disposed: …"`.
Keeping probe code is a **promotion** — a fresh build-for-keep under the full proof/seam
gates, never a default. Undisposed probes drain confidence and flag the cold-start scan.

For large or code-heavy artifacts, delegate to the `ratchet-builder` subagent.

## Output contract
Expand Down
5 changes: 5 additions & 0 deletions skills/handoff/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ to skim past:
9. **Deviations from the map** — if `/ratchet:map` ran, where the build diverged from its
plan (map said X → code revealed Y → call made) and which calls still need the user's
judgment. This is what stops the receiver from re-litigating what you already discovered.
10. **Probe outcomes** — for each probe: the unknown it attacked, the finding, and whether
its code was **disposed** or explicitly **promoted**. An undisposed probe is a
watch-out, not an asset — the receiver must not mistake build-for-learn residue for
kept work.

## Rules

Expand All @@ -61,6 +65,7 @@ NEXT ACTION: <the single concrete first move>
CONTEXT TO ACT: <files / commands / facts, by location>
WATCH-OUTS: <traps>
DEVIATIONS: <map said X → code revealed Y → call made> (each, if a map ran)
PROBES: <unknown → finding → disposed | promoted> (each, if any ran)
NEXT COMMAND: /ratchet:<command>
```

Expand Down
59 changes: 53 additions & 6 deletions skills/map/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ the moment you hit them; never close a quadrant off-screen.
question first (the answer that reshapes the most), not a wall of them. Give lettered
options with a **recommended** answer. Close each question exactly one way, in front of
the user: **user answer**, **territory** (you researched it, then show question +
finding), or **OPEN** (deferred, with what would unblock it).
finding), **probe** (only touching the repo can answer it — commission a probe card,
below), or **OPEN** (deferred, with what would unblock it *and the route that will close
it*: ask-user, probe, park with owner+reason, promote to an assumption with a kill test,
or a defect).

3. **Unknown knowns — extract the tacit.** The user cannot articulate what they don't know
they know. Do not ask abstractly. Put something concrete in their hands — sample data, a
Expand All @@ -70,10 +73,46 @@ the moment you hit them; never close a quadrant off-screen.

5. **Hand over the map.** One page, all four quadrants — shape it like
`templates/unknowns-map.md` and write it to `.ratchet/unknowns-map.md`. OPEN items live
**on the map**, not buried in chat. Add a tweakable build plan ordered by *what might
change* (judgment calls first, routine work compressed at the bottom) and a copy-paste
implementation prompt. Then stop — implementation is a separate engagement. The map stays
live through the build: `/ratchet:build` records deviations back onto it.
**on the map**, not buried in chat — and every OPEN item names its route out (ask-user,
probe, park with owner+reason, assumption+killTest, or defect). An OPEN item with no
route is a stall with a receipt, not mapped fog. Add a tweakable build plan ordered by
*what might change* (judgment calls first, routine work compressed at the bottom) and a
copy-paste implementation prompt. Then stop — implementation is a separate engagement.
The map stays live through the build: `/ratchet:build` records deviations back onto it.

## Probes — when the map must touch (build-for-learn)

Some fog only appears by touching: an unknown unknown no read can surface, a taste no
question can extract. For those, do not ask harder — commission a **probe**: a time-boxed,
reversible, throwaway build whose proof-of-done is knowledge, not code. Shape it like
`templates/probe-card.md`:

```
PROBE: <the unknown this closes>
- Hypothesis: <what touching should show>
- Smallest reversible touch: <mock | spike | failing test | fixture | instrumentation>
- Allowed surfaces: <files it may touch — nothing else>
- Proof of learning: <the observation that closes the unknown, either way>
- Stop condition: <time-box / attempt cap>
```

**Probe code dies; probe findings live.** Serialize the lifecycle:

```
ratchet artifact add '{"kind":"probe","title":"probe: <unknown>","status":"v0","holes":["disposal: pending"]}'
# run it via /ratchet:build in build-for-learn mode, then — findings first, then dispose:
ratchet state append decisions '{"choice":"<what it settled>","rejected":"<losing hypothesis>","tripwire":"<what reopens it>"}'
ratchet retract <probe-id> --reason "disposed: code reverted; finding recorded as <id>"
```

The `disposal: pending` hole drains confidence until the probe is retracted, and the
cold-start scan flags a live probe as residue — undisposed probe code cannot read clean.
If probe code turns out worth keeping, that is a **promotion**: a fresh `/ratchet:build`
under the full proof/seam gates, then `ratchet retract <probe-id> --reason "promoted: <the
keep-build>" --superseded-by <new-artifact-id>`. The CLI enforces both: a probe retraction
must start `disposed:` or `promoted:`, and a promotion must name its superseder. Nothing
ships because a probe happened to leave it behind. An unfinished probe (stop condition
hit) closes its item as OPEN with what it *did* learn.

## Output contract

Expand All @@ -85,7 +124,10 @@ KNOWN KNOWNS:
- <assumption> — treated as true until challenged

KNOWN UNKNOWNS:
- Q: <question> | recommend: <lettered answer> | closed by: user | territory | OPEN | unblocks: <what>
- Q: <question> | recommend: <lettered answer> | closed by: user | territory | probe | parked(owner) | OPEN + route | unblocks: <what>

PROBES (if any commissioned):
- probe: <unknown> | hypothesis: <expected> | touch: <smallest reversible> | stop: <time-box>

UNKNOWN KNOWNS:
- tacit constraint: <extracted> | reshaped the target: <how>
Expand Down Expand Up @@ -120,6 +162,11 @@ The map's OPEN loops, untested assumptions, and artifact holes all drain
`ratchet score confidence` on purpose — an unclosed unknown is tracked pressure, not a
forgotten one. The score cannot read ship-ready while the fog is still on the board.

The CLI holds both ends of the fog loop itself: `ratchet score aperture` records
`fog: pre-build map required` as an open loop the moment `mapRequired` fires (writer
callers), and `ratchet artifact add` with `kind:"unknown-map"` closes it when the map
lands — scored fog can never live only on stdout.

## Meter — when to reach for this

Run `/ratchet:map` when the aperture earns it, not on a reversible one-liner:
Expand Down
31 changes: 31 additions & 0 deletions src/artifacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,27 @@ function addArtifact(cwd, item) {
status: item.status || 'v0',
holes: Array.isArray(item.holes) ? item.holes : item.holes ? [item.holes] : [],
};
// A probe's drain is an invariant, not a convention: build-for-learn code
// must cost confidence until disposed or promoted, even when the caller
// omits the hole.
if (record.kind === 'probe' && !record.holes.some((h) => /disposal:\s*pending/i.test(String(h)))) {
record.holes.push('disposal: pending');
}
s.artifacts.push(record);
s.dirty = true;
s.history.push({ id: state.makeId('hist'), at: record.at, event: 'artifact.add', note: record.title });
// The map landing is the fog entering durable state: close the fog loop that
// `score aperture` opened, so the drain tracks reality instead of nagging past
// the point the map answered it. (The map's own OPEN items keep draining as
// this artifact's holes.)
if (record.kind === 'unknown-map') {
for (const l of s.openLoops || []) {
if (l.status !== 'closed' && String(l.text || '').startsWith(schemas.FOG_LOOP_PREFIX)) {
l.status = 'closed';
l.closedBy = record.id;
}
}
}
state.saveState(cwd, s);
return record;
}
Expand Down Expand Up @@ -122,6 +140,19 @@ function retractArtifact(cwd, id, { reason = '', supersededBy = '' } = {}) {
const s = state.loadState(cwd);
const a = (s.artifacts || []).find((x) => x.id === id);
if (!a) throw new Error(`no artifact with id "${id}"`);
// A probe retraction is its lifecycle exit and must state which one: the
// code died (disposed) or was explicitly rebuilt for keep (promoted). A
// vague reason would let residue stop draining without either outcome.
if (a.kind === 'probe') {
if (!/^(disposed|promoted):/i.test(reason)) {
throw new Error(
'a probe retraction must state its outcome: --reason must start with "disposed:" (code reverted, finding recorded) or "promoted:" (rebuilt for keep)'
);
}
if (/^promoted:/i.test(reason) && !supersededBy) {
throw new Error('a promoted probe requires --superseded-by <artifact-id> — the build-for-keep that replaced it');
}
}
const now = schemas.nowIso();
a.status = 'retracted';
a.retracted = { at: now, reason, supersededBy, keptForProvenance: true };
Expand Down
Loading
Loading