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
2 changes: 1 addition & 1 deletion .claude/fable/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ command — no restart.
subagent_model: opus # fable | opus | sonnet | haiku | inherit (= session model)
subagent_effort: medium # low | medium | high | xhigh | inherit
fleet: standard # light | standard | max — scales pools, votes, round caps
# votes: 3 # skeptics per review finding (uncomment to override the fleet preset)
# votes: 3 # skeptics per finding, review and exhaust (uncomment to override the fleet preset)
# max_rounds: 6 # fable-exhaust round cap (uncomment to override the fleet preset)
# max_leads: 30 # fable-research deep-read cap (uncomment to override the fleet preset)
```
Expand Down
2 changes: 1 addition & 1 deletion .claude/fable/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.0.0
2.0.1
4 changes: 3 additions & 1 deletion .claude/skills/fable-config/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ Template for a missing CONFIG.md — recreate the `## Subagents` yaml block exac
subagent_model: opus # fable | opus | sonnet | haiku | inherit (= session model)
subagent_effort: medium # low | medium | high | xhigh | inherit
fleet: standard # light | standard | max — scales pools, votes, round caps
# votes: 3 # skeptics per review finding (uncomment to override the fleet preset)
# votes: 3 # skeptics per finding (uncomment to override the fleet preset)
# max_rounds: 6 # fable-exhaust round cap (uncomment to override the fleet preset)
# max_leads: 30 # fable-research deep-read cap (uncomment to override the fleet preset)
```
4 changes: 2 additions & 2 deletions .claude/skills/fable-ship/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: fable-ship
description: Release-readiness gate — detects the project's build/test/release mechanics, runs the gates (read-only hygiene first, then checks and docs in parallel), then a skeptic attacks the "ready to ship" claim. Use before releasing, deploying, tagging, or publishing. It verifies readiness; it never deploys.
description: Release-readiness gate — detects the project's build/test/release mechanics, runs the read-only gates (detection, hygiene, docs) in parallel and the build/test gate after them alone, then a skeptic attacks the "ready to ship" claim. Use before releasing, deploying, tagging, or publishing. It verifies readiness; it never deploys.
argument-hint: "[scope]"
---

Expand All @@ -13,4 +13,4 @@ Before dispatching, read `.claude/fable/CONFIG.md` if it exists and pass its yam
3. Report the verdict first — ready or not — then every blocker and warning with its evidence, then the ship mechanics the workflow returns (`shipCommands` and `releaseNotes`). If no ship command was detected, say so rather than inventing one.
4. Never execute the deployment itself unless the user explicitly asks; deploying is theirs to trigger. If they ask, run their project's deploy command and report the outcome verbatim.

If the Workflow tool is unavailable, run the same structure with Agent-tool subagents: one `fable-scout` to detect the project checks (reading the project's own operating docs — CLAUDE.md and its imports, AGENTS.md — before scanning build files), three parallel gate agents (run checks / repo hygiene / docs), then one `fable-skeptic` attacking the readiness claim.
If the Workflow tool is unavailable, run the same structure with Agent-tool subagents: in parallel, one `fable-scout` to detect the project checks (reading the project's own operating docs — CLAUDE.md and its imports, AGENTS.md — before scanning build files) plus read-only hygiene and docs gate agents; then run the detected build/test checks alone (they may dirty the working tree, which would poison a concurrent audit); then one `fable-skeptic` attacking the readiness claim.
2 changes: 2 additions & 0 deletions .claude/workflows/fable-design.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ const judgments = (await parallel(rubrics.map((rubric, j) => () =>
)
))).filter(Boolean)

if (!judgments.length) throw new Error('no judge scores were produced — an unjudged winner would be arbitrary; re-run fable-design')

const totals = approaches.map((_, i) =>
judgments.reduce((sum, j) => {
const s = (j.scores || []).find(x => x.approach === i)
Expand Down
29 changes: 22 additions & 7 deletions .claude/workflows/fable-exhaust.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,17 @@ const STANCES = [
]

// Fleet scaling — every bound is announced up front, per the no-silent-caps rule.
// The skeptic panel is votes-sized (an explicit cfg.votes overrides the fleet),
// cycling the three lenses; majority uphold to survive.
const stances = fleet === 'light' ? STANCES.slice(0, 2) : STANCES
const panel = fleet === 'light' ? LENSES.slice(0, 1) : LENSES
const need = fleet === 'light' ? 1 : 2
const votes = Math.max(1, cfg.votes || { light: 1, standard: 3, max: 5 }[fleet])
const panel = Array.from({ length: votes }, (_, i) => LENSES[i % LENSES.length])
const need = Math.floor(votes / 2) + 1
const MAX_ROUNDS = cfg.max_rounds || { light: 3, standard: 6, max: 8 }[fleet]
log('fleet ' + fleet + ': ' + stances.length + ' finder stances per round, ' + panel.length + '-lens verification, round cap ' + MAX_ROUNDS)
log('fleet ' + fleet + ': ' + stances.length + ' finder stances per round, ' + votes + ' skeptic vote(s) per finding (' + need + ' to survive), round cap ' + MAX_ROUNDS)

const key = b => b.file + ':' + b.line + ':' + b.title.toLowerCase().slice(0, 60)
// Malformed structured output (missing file/title) is dropped and counted, never fatal.
const key = b => b.file + ':' + b.line + ':' + String(b.title || '').toLowerCase().slice(0, 60)
const seen = new Set()
// Locations only (no titles) — this set is re-broadcast to every finder each round,
// so it must stay small; the finders need "don't re-report here", not the details.
Expand All @@ -94,13 +98,22 @@ let dry = 0

while (dry < 2 && round < MAX_ROUNDS && (!budget.total || budget.remaining() > 40_000)) {
round++
const found = (await parallel(stances.map((stance, i) => () =>
if (seenLocs.size > 200) log('dedup hint truncated to the most recent 200 of ' + seenLocs.size + ' known locations — older ones may be re-reported and re-deduped')
const finderResults = (await parallel(stances.map((stance, i) => () =>
run(
'Hunt for ' + hunt + ' in ' + scope + '. Round ' + round + '. Your stance: ' + stance + '.' +
(seenLocs.size ? '\nAlready found — do NOT re-report findings at these locations: ' + Array.from(seenLocs).slice(-200).join('; ') : ''),
{ label: 'find:r' + round + 's' + (i + 1), phase: 'Find', schema: BUGS, agentType: 'fable-finder' }
)
))).filter(Boolean).flatMap(r => r.findings || [])
))).filter(Boolean)
if (!finderResults.length) {
// A round nobody searched is a failed round, not a dry one — dry rounds claim coverage.
log('round ' + round + ': every finder failed or was skipped — not counted toward the dry-round stop')
continue
}
const reported = finderResults.flatMap(r => r.findings || []).filter(Boolean)
const found = reported.filter(b => b.file)
if (found.length < reported.length) log('round ' + round + ': dropped ' + (reported.length - found.length) + ' malformed finding(s) missing a file')

const fresh = found.filter(b => !seen.has(key(b)))
if (!fresh.length) {
Expand All @@ -121,7 +134,9 @@ while (dry < 2 && round < MAX_ROUNDS && (!budget.total || budget.remaining() > 4
)))
.then(vs => ({ b, real: vs.filter(v => v && v.refuted === false).length >= need }))
))
const kept = judged.filter(j => j.real).map(j => j.b)
const kept = judged.filter(Boolean).filter(j => j.real).map(j => j.b)
const lost = judged.filter(j => !j).length
if (lost) log('round ' + round + ': ' + lost + ' finding(s) lost their skeptic panel — not confirmed, not silently dropped')
confirmed.push(...kept)
log('round ' + round + ': ' + found.length + ' reported, ' + fresh.length + ' fresh, ' + kept.length + ' confirmed (total ' + confirmed.length + ')')
}
Expand Down
5 changes: 3 additions & 2 deletions .claude/workflows/fable-migrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ if (files.length >= PILOT_MIN) {
(result, f) => run(checkPrompt(f, result), { label: 'pilot-check:' + f, phase: 'Pilot', schema: CHECK, agentType: 'fable-skeptic' })
.then(c => ({ file: f, ...(c || { ok: false, problems: 'checker did not complete' }) }))
)
const pilotBad = pilotResults.filter(r => r && !r.ok)
// A pilot entry that never completed is a failed pilot, not a passed one — the gate fails closed.
const pilotBad = pilotResults.map((r, i) => r || { file: pilotFiles[i], ok: false, problems: 'pilot transform/check did not complete' }).filter(r => !r.ok)
if (pilotBad.length) {
log('pilot failed on ' + pilotBad.length + ' of ' + pilotFiles.length + ' file(s) — aborting the fan-out, ' + rest.length + ' file(s) left untouched')
return {
Expand All @@ -128,7 +129,7 @@ const results = await pipeline(
.then(c => ({ file: f, ...(c || { ok: false, problems: 'checker did not complete' }) }))
)

const bad = results.filter(r => r && !r.ok)
const bad = results.map((r, i) => r || { file: rest[i], ok: false, problems: 'transform/check did not complete' }).filter(r => !r.ok)
if (bad.length) log(bad.length + ' file(s) failed their per-file check — see perFileProblems')

phase('Final verify')
Expand Down
4 changes: 3 additions & 1 deletion .claude/workflows/fable-review.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ const sub = opts => ({
})
if (cfg.subagent_model || cfg.subagent_effort || cfg.fleet) log('config: subagents on ' + (cfg.subagent_model || 'the session model') + ' at ' + (cfg.subagent_effort || 'session') + ' effort, fleet ' + fleet)

const votes = input.votes || cfg.votes || { light: 1, standard: 3, max: 5 }[fleet]
// Clamped to at least 1: a zero/negative vote count would spawn no skeptics and
// mark every raw finding "survived adversarial verification".
const votes = Math.max(1, input.votes || cfg.votes || { light: 1, standard: 3, max: 5 }[fleet])
const needed = Math.floor(votes / 2) + 1
log(votes + ' skeptic vote(s) per finding, ' + needed + ' uphold(s) to survive')

Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ jobs:
[ "$(cat "$T/.claude/agents/user-own.md")" = "user-owned" ]
grep -q 'fleet: light' "$T/.claude/fable/CONFIG.md" || { echo "update overwrote user CONFIG.md"; exit 1; }
[ "$(cat "$T/.claude/settings.json")" = '{"permissions":{"allow":["Workflow"]}}' ] || { echo "update overwrote user settings.json"; exit 1; }
- name: update never re-creates a deleted settings.json (the opt-out)
run: |
rm "$T/.claude/settings.json"
./install.sh --update "$T"
[ ! -e "$T/.claude/settings.json" ] || { echo "update re-created deleted settings.json"; exit 1; }

install-ps1:
runs-on: windows-latest
Expand Down Expand Up @@ -91,3 +96,7 @@ jobs:
if ((Get-Content (Join-Path $t '.claude\agents\user-own.md')) -ne 'user-owned') { throw 'update touched a user-owned file' }
if (-not (Select-String -Path (Join-Path $t '.claude\fable\CONFIG.md') -SimpleMatch 'fleet: light' -Quiet)) { throw 'update overwrote user CONFIG.md' }
if ((Get-Content -Raw (Join-Path $t '.claude\settings.json')).Trim() -ne '{"permissions":{"allow":["Workflow"]}}') { throw 'update overwrote user settings.json' }

Remove-Item (Join-Path $t '.claude\settings.json')
.\install.ps1 -Target $t -Update
if (Test-Path (Join-Path $t '.claude\settings.json')) { throw 'update re-created deleted settings.json' }
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

If a release ever removes a shipped file, the entry lists the stale path so `--update` users can delete it manually.

## 2.0.1 — 2026-08-08

Fixes from running the pack's own `fable-exhaust` over v2.0.0 (one fully verified round: 4 finders, 13 raw findings, three-lens skeptic panels; later finder rounds were cut off by a usage limit, so this was a bounded hunt, not a dry-out), plus fresh README graphics:

- **Installers**: `--update` (and any re-run on an existing install) no longer re-creates a deleted `.claude/settings.json` — deleting it is the documented opt-out, and it is now truly first-install-only (keyed on the pre-existing VERSION stamp). CI asserts the opt-out on both platforms.
- **fable-exhaust**: no longer crashes on a finding missing `title`/`file` or on a lost skeptic panel (guards added, drops announced); rounds where every finder failed no longer count as "ran dry" (a failed round is not evidence of coverage); the 200-location dedup-hint truncation is announced; the skeptic panel is now votes-sized (`fleet max` really means 5 votes — code now matches CONFIG.md's claim).
- **fable-review**: vote count clamped to at least 1 — a zero/negative `votes` would have spawned no skeptics and marked every raw finding as verified.
- **fable-migrate**: the pilot gate fails closed — a pilot whose transform/check never completed now aborts the fan-out instead of passing it.
- **fable-design**: an all-judges-failed panel now errors instead of silently crowning approach 0.
- **Docs**: fable-ship's description now matches its actual gate ordering (read-only gates in parallel, build/tests after, alone); the `/fable-config` recreation template carries the full shipped key set.
- **README graphics**: new SVG set (value-dial, review-pipeline, demo-run with the real 2.0 dogfood numbers); social preview regenerated; the stale 1.0 screenshot removed.

## 2.0.0 — 2026-08-07

The Claude 5 / value-maxxing release: pick your spend instead of prompting for it, tuned for the leaner Claude Code system prompts that shipped with the Claude 5 generation.
Expand Down
18 changes: 5 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ rm -f .claude/agents/fable-*.md .claude/workflows/fable-*.js

## The value setup

<img src="assets/value-dial.svg" alt="Two dials: the main agent is a harness setting (/model Fable 5 or Opus 5, /effort low, thinking via Tab); subagents are set in CONFIG.md (opus at medium effort, fleet light/standard/max). Every fable workflow reads both and announces every bound it applies." width="100%">

Two dials. The main agent is a harness setting; the subagents are the pack's config.

| | Set with | Recommended |
Expand Down Expand Up @@ -118,21 +120,11 @@ One folder, five layers, wired by string name, with a [checker](tools/check-work

The step most packs skip: **every raw finding goes on trial before it reaches you.** Skeptics judge each finding through different lenses (trace it line by line; is it reachable; is the impact real), majority verdict. A plausible-but-wrong finding dies in the pipeline instead of costing you twenty minutes.

```mermaid
flowchart LR
U["you: /fable-review"] --> W["fable-review.js"]
W --> F1["finder<br/>correctness"]
W --> F2["finder<br/>contracts"]
W --> F3["finder<br/>security"]
W --> F4["finder<br/>resources"]
F1 & F2 & F3 & F4 --> S{"skeptics vote<br/>per finding"}
S -->|"majority upholds"| OK["you see it"]
S -->|"majority refutes"| DEAD["it dies here"]
```
<img src="assets/review-pipeline.svg" alt="The fable-review pipeline: your diff fans out to four finders (correctness, contracts, security, resources); every raw finding faces a skeptic panel whose vote count comes from the fleet config; majority upholds and you see it, majority refutes and it dies in the pipeline" width="100%">

A real run of this pipeline over this repo's own 1.0 release — 4 finders, 8 raw findings, 24 skeptic votes, 3 confirmed and fixed before merge:
A real run of this pipeline over the 2.0.0 release branch itself — 4 finders, 5 raw findings, 15 skeptic votes, all 5 confirmed and fixed before merge:

<img src="assets/demo-review.png" alt="Real fable-review run: 4 finders, 8 raw findings, 24 skeptic votes; 3 confirmed, 5 refuted as stale because the fixes had already landed" width="100%">
<img src="assets/demo-run.svg" alt="Real fable-review run over the 2.0.0 release branch: config announced (fleet standard, 3 votes, 2 upholds to survive), 4 finders, 5 raw findings, 15 skeptic votes, 5 confirmed and all 5 fixed before merge in commit 83833c0" width="100%">

For long jobs, `/fable-marathon` keeps all state in `FABLE-RUN.md` at the project root — goal, walls (actions that always queue for you), backlog with machine-checkable `done-when:` commands, journal — committed at every verified milestone, so any session resumes from the file alone. Unattended: `/loop /fable-marathon` or a scheduled task.

Expand Down
Binary file removed assets/demo-review.png
Binary file not shown.
28 changes: 28 additions & 0 deletions assets/demo-run.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading