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 (/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. + 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"] -``` +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 -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: -Real fable-review run: 4 finders, 8 raw findings, 24 skeptic votes; 3 confirmed, 5 refuted as stale because the fixes had already landed +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 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 @@ + + + + + + + + + + + + fable-review · the 2.0.0 release branch, reviewed by its own fleet + + + $ /fable-review the 2.0 overhaul branch + ▸ config: subagents at medium effort, fleet standard + ▸ 3 skeptic vote(s) per finding, 2 uphold(s) to survive + Review find:correctness find:contracts find:security find:resources + Verify 5 raw findings → 15 independent skeptic votes + ✓ 5 of 5 findings survived cross-examination — most-severe first: + major CONFIG.md votes key silently pinned review verification … + major fleet presets unreachable through the documented command … + + 3 more (doc/code mismatches caught before any user hit them) + ✓ all 5 fixed and re-verified before merge — commit 83833c0 + + + Real run, real numbers — the pack gates its own releases with itself. + 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 @@ + + + + + + + + + Every finding goes on trial before it reaches you + + + + your diff + /fable-review + + + + finder · correctness + finder · contracts + finder · security + finder · resources + + + + + + + + + + + skeptic panel + per finding · three lenses: + trace it · can it trigger · is it real impact + votes: fleet light 1 · standard 3 · max 5 + + + + + + + + + + majority upholds + you see it + + majority refutes + it dies here + + + + + + A plausible-but-wrong finding costs the pipeline, not twenty minutes of your time. + 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 @@ + + + + + + + + + Two dials. You set the spend — nothing is inferred. + + + + 1 · Main agent + a harness setting + /model Fable 5 (or Opus 5) + /effort low + thinking Tab toggle / /config + low effort + structure beats high effort alone + + + + 2 · Subagents + .claude/fable/CONFIG.md — /fable-config, no restart + subagent_model: opus + subagent_effort: medium + fleet: light | standard | max + light ≈ 6 agents per review · standard ≈ 16 · max = audits + + + + + + + + every fable workflow reads both — and announces every bound it applies + 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"