diff --git a/.claude/fable/CONFIG.md b/.claude/fable/CONFIG.md
index fd74651..e88e49d 100644
--- a/.claude/fable/CONFIG.md
+++ b/.claude/fable/CONFIG.md
@@ -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)
```
diff --git a/.claude/fable/VERSION b/.claude/fable/VERSION
index 227cea2..38f77a6 100644
--- a/.claude/fable/VERSION
+++ b/.claude/fable/VERSION
@@ -1 +1 @@
-2.0.0
+2.0.1
diff --git a/.claude/skills/fable-config/SKILL.md b/.claude/skills/fable-config/SKILL.md
index f59b5da..a1b6aad 100644
--- a/.claude/skills/fable-config/SKILL.md
+++ b/.claude/skills/fable-config/SKILL.md
@@ -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)
```
diff --git a/.claude/skills/fable-ship/SKILL.md b/.claude/skills/fable-ship/SKILL.md
index 7046976..bca19f2 100644
--- a/.claude/skills/fable-ship/SKILL.md
+++ b/.claude/skills/fable-ship/SKILL.md
@@ -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]"
---
@@ -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.
diff --git a/.claude/workflows/fable-design.js b/.claude/workflows/fable-design.js
index c4aadc4..3ff4d1d 100644
--- a/.claude/workflows/fable-design.js
+++ b/.claude/workflows/fable-design.js
@@ -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)
diff --git a/.claude/workflows/fable-exhaust.js b/.claude/workflows/fable-exhaust.js
index 56506fc..8a9b73c 100644
--- a/.claude/workflows/fable-exhaust.js
+++ b/.claude/workflows/fable-exhaust.js
@@ -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.
@@ -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) {
@@ -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 + ')')
}
diff --git a/.claude/workflows/fable-migrate.js b/.claude/workflows/fable-migrate.js
index 450ba5f..be15731 100644
--- a/.claude/workflows/fable-migrate.js
+++ b/.claude/workflows/fable-migrate.js
@@ -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 {
@@ -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')
diff --git a/.claude/workflows/fable-review.js b/.claude/workflows/fable-review.js
index ab34755..413bd72 100644
--- a/.claude/workflows/fable-review.js
+++ b/.claude/workflows/fable-review.js
@@ -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')
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7b3fd4a..89e256c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
@@ -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' }
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f70fcfc..727a814 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/README.md b/README.md
index 96f37ae..12c4323 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,8 @@ rm -f .claude/agents/fable-*.md .claude/workflows/fable-*.js
## The value setup
+
+
Two dials. The main agent is a harness setting; the subagents are the pack's config.
| | Set with | Recommended |
@@ -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 correctness"]
- W --> F2["finder contracts"]
- W --> F3["finder security"]
- W --> F4["finder resources"]
- F1 & F2 & F3 & F4 --> S{"skeptics vote per finding"}
- S -->|"majority upholds"| OK["you see it"]
- S -->|"majority refutes"| DEAD["it dies here"]
-```
+
-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:
-
+
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.
diff --git a/assets/demo-review.png b/assets/demo-review.png
deleted file mode 100644
index d61f5db..0000000
Binary files a/assets/demo-review.png and /dev/null differ
diff --git a/assets/demo-run.svg b/assets/demo-run.svg
new file mode 100644
index 0000000..be24f4c
--- /dev/null
+++ b/assets/demo-run.svg
@@ -0,0 +1,28 @@
+
diff --git a/assets/review-pipeline.svg b/assets/review-pipeline.svg
new file mode 100644
index 0000000..d64174c
--- /dev/null
+++ b/assets/review-pipeline.svg
@@ -0,0 +1,56 @@
+
diff --git a/assets/social-preview.png b/assets/social-preview.png
index 14863ce..648b0b2 100644
Binary files a/assets/social-preview.png and b/assets/social-preview.png differ
diff --git a/assets/value-dial.svg b/assets/value-dial.svg
new file mode 100644
index 0000000..d8539f1
--- /dev/null
+++ b/assets/value-dial.svg
@@ -0,0 +1,36 @@
+
diff --git a/install.ps1 b/install.ps1
index e5645a9..afcc7e9 100644
--- a/install.ps1
+++ b/install.ps1
@@ -63,13 +63,15 @@ foreach ($f in Get-ChildItem -Path $src -Recurse -File) {
}
# First-run settings: materialize the template so Workflow/Agent launches don't
-# prompt (the pack's standing authorization). Never touches an existing file.
+# prompt (the pack's standing authorization). First install only — never touches
+# an existing file, and -Update never re-creates a deleted one (deleting it is
+# the documented opt-out).
$settings = Join-Path $dest 'settings.json'
-if (-not (Test-Path -LiteralPath $settings)) {
+if (-not (Test-Path -LiteralPath $settings) -and -not $Update -and -not $installedVersion) {
Copy-Item -LiteralPath (Join-Path $src 'fable\settings.template.json') -Destination $settings
Write-Host 'created .claude/settings.json (pre-approves the Workflow and Agent tools; delete it to opt out)'
}
-elseif (-not (Select-String -LiteralPath $settings -SimpleMatch '"Workflow"' -Quiet)) {
+elseif ((Test-Path -LiteralPath $settings) -and -not (Select-String -LiteralPath $settings -SimpleMatch '"Workflow"' -Quiet)) {
Write-Host 'note: .claude/settings.json exists - to skip permission prompts on fleet launches, add "Workflow" and "Agent" to permissions.allow'
}
diff --git a/install.sh b/install.sh
index e9b8389..8c82fbe 100755
--- a/install.sh
+++ b/install.sh
@@ -66,15 +66,15 @@ while IFS= read -r -d '' f; do
done < <(find "$src" -type f -print0)
# First-run settings: materialize the template so Workflow/Agent launches don't
-# prompt (the pack's standing authorization). Never touches an existing file.
+# prompt (the pack's standing authorization). First install only — never touches
+# an existing file, and --update never re-creates a deleted one (deleting it is
+# the documented opt-out).
settings="$dest/settings.json"
-if [ ! -e "$settings" ]; then
+if [ ! -e "$settings" ] && [ "$update" -ne 1 ] && [ -z "$installed_version" ]; then
cp "$src/fable/settings.template.json" "$settings"
echo "created .claude/settings.json (pre-approves the Workflow and Agent tools; delete it to opt out)"
-else
- if ! grep -q '"Workflow"' "$settings"; then
- echo 'note: .claude/settings.json exists — to skip permission prompts on fleet launches, add "Workflow" and "Agent" to permissions.allow'
- fi
+elif [ -e "$settings" ] && ! grep -q '"Workflow"' "$settings"; then
+ echo 'note: .claude/settings.json exists — to skip permission prompts on fleet launches, add "Workflow" and "Agent" to permissions.allow'
fi
claude_md="$target/CLAUDE.md"