Fable-Discipline
Coding and task discipline, distilled from reading thousands of edits by one
capable model and grading them against an independent review. It is additive: it
does not replace what your model already does well, it adds the habits that strong
engineers have and most code generation skips.
Most of it is judgment the skill teaches. One slice is enforced by a hook, because
a rule a model can talk itself out of is a suggestion, not a gate. The skill
teaches; the hook makes the one checkable habit non-optional.
Position: the execution-discipline layer of prd-os
This skill is not a sibling system to prd-os; it is prd-os's execution-discipline
layer (merged 2026-07-04, see the plugin CHANGELOG). Two load paths, one skill:
- PRD work:
/issue-start loads this skill before the first edit of every
DSSE issue. The issue's receipts (verified, reviewed, findings_triaged) are the
task-level half of the same contract this skill states per-edit.
- Non-PRD work: the quick-plan fast path and the fable-discipline auto-invoke
rule load it for any coding task bigger than a one-line change.
The public standalone export (github.com/assafkip/fable-discipline) mirrors this
copy; scripts/export-fable-mirror.sh --check is the drift blocker.
When NOT to use this
One-line changes, typo fixes, a task with one obvious approach that fits in a
single pass. Staging a trivial task buries the answer under ceremony. This earns
its cost only when a one-shot attempt would plausibly miss something.
Layer 1: running the task
How to move through complex work without shipping a confident wrong answer.
Stage before you act. Write the stage plan first. Number the stages, name
the one checkable artifact each produces. If a stage produces nothing
checkable, merge it into the next. The map is living, not a contract; update it
when what you learn invalidates it.
Verify each stage with a check that can fail. A test that runs, a file that
provably exists in the right shape, a source actually read, an output diffed
against the spec. "I reviewed it and it looks right" is not a check: a model
that would skip verification also passes its own introspection. If a stage has
no failable check, say so and mark its output unverified so the gap is visible.
Say, then batch. State the one-line intent, then fire the burst of actions
that executes it. The test is binary: a reader given only your intent lines
can reconstruct the plan, or the intent line failed; rewrite it. It keeps
you from drifting mid-burst.
Done is written, not felt. Define done-criteria up front. On a task that
spans sessions, keep a short work log (decisions, what was tried, what failed)
and re-read it before continuing, so you do not duplicate work or guess where
you left off.
Ground before you diagnose. Confirm a perceived error actually reproduces
before you act on it. Read where the data comes from before you trust what it
represents. The failure mode this prevents: seeing a likely error, diagnosing
it instinctively, and running with that explanation before checking it was real.
Layer 2: writing the code
Recon before edit. Read reality, do not assume it. Grep the real schema and
call-sites before changing anything. Never edit a file you have not read this
session, unless you created that file in this same session (your own fresh
write counts as read). If you already read a schema, re-read the exact field
names rather than guessing them.
Verify against a copy, with a negative self-test. A passing gate is not
trusted until it has been seen to fail. Run the reproducer against a copy of the
live resource, never the live one, unless the resource is disposable by design
(a regenerable fixture or sandbox environment). Then corrupt a valid input and
prove the check FAILS on the violation, so a green result is not a rubber stamp.
Single-writer chokepoint, guarded by a CENSUS TAKEN BY CODE. Route every
mutation of a shared resource through one helper, and make a gate enumerate the
consumers from the source, never from your recollection of them. Migrate existing
call-sites one small, independently revertible edit at a time, not one bulk
rewrite. For a module that declares an exclusion predicate, that gate is
q-system/.q-system/scripts/consumer-parity-check.py, a PostToolUse hook that
walks the AST and reports every walker that skips the predicate. Retired 2026-08-03
(ASK-315): this line used to say "write a test that greps the tree", which is
prose naming no executable. Under it, a commit whose message read "one predicate
for all three consumers" shipped with a fourth, and six instances of that shape
landed in one file in one night. A habit is not a census.
Why-comments anchored to a named scar. Comments encode the constraint and
the specific past bug that motivates it, not a restatement of the code. These
survive refactors because they encode an invariant.
Capture every out-of-scope finding; never just mention it. If you notice
a real issue that is out of scope for the current work, write it to a tracked
backlog (an issue, a ticket, a standing ledger your gate reads), not a bare
prose mention. A mention is a silent drop; a tracked item is one a gate can
keep failing until it is resolved. The paired lint blocks deferral language
written into code without such a capture. Override: capture first, then ack
the captured line with # spillover-skip; there is no skip-first path.
Build against the recurring gap classes. If the change scales or touches
sensitive data, walk the gap-class block in references/checklist.md: an
in-memory cap is not a disk bound; a UI hide is not access control; a gate
fails closed while a filter fails open; redact at the egress edge; a new flag
must reach every reader; check-then-mutate needs one lock; single-source the
version; a cross-cutting invariant needs a written scope + a self-enumerating
guard. Check only the classes the change touches.
Least-code bias. Prefer the smallest change that solves it. Reach for
delete-and-reuse before you write new code; the best fix is often a line
removed, not a line added. When you catch yourself writing the third
near-identical thing, stop and generalize the mechanism instead of adding a
third copy.
Consistency rules
Habits that are easy to do somewhere and forget elsewhere. Make them non-optional:
- Test isolation. A test uses a temp copy, a tempfile, or
:memory:, never a
real data path. (This is the slice the paired hook enforces.) Override: an
audit test that must NAME a live path may assert on it (assertion lines are
exempt); anything else carries # fable-discipline-lint-skip in that file
with a one-line reason.
- Declare and pin every new dependency the moment you import it, and keep a
test that proves the manifest covers every third-party import.
- Specify degenerate cases before implementing: empty, single-element,
disconnected, non-converging. Each gets defined behavior, not an implicit crash.
- Validate persisted external input. Never store arbitrary user or model JSON
that, if malformed, can permanently break a render or load path, unless every
read path runs a validating loader that tolerates the malformed record.
- Enumerate all call-sites when scoping a change. Grep for every site the
change must reach before declaring it done.
Anti-patterns to drop
- Guessing a schema or API you already read. Re-read it.
- Acting on an unconfirmed diagnosis. Confirm the error is real first.
- Re-attempting the same failing command. Change the approach, do not retry it.
- Applying the user's stated style rules only to shipped output, not your own
narration.
Communication while building
Terse mid-task, then decompress at the seam into a short "ran, not assumed" block
listing what you ran and what passed. When a real choice exists, name the options,
mark your pick, give the tradeoff, end with one action.
Enforcement
The deterministic slice (test isolation) is enforced by
scripts/fable-discipline-lint.py, wired in hooks/hooks.json as a PostToolUse hook.
Everything else is judgment and lives here. See EXAMPLE.md for a worked
before/after, and references/checklist.md for the copy-paste pre-done gate.
1---2name: fable-discipline3description: Engineering discipline distilled from a forensic read of one model's work. Use when building a feature, fixing a bug, writing tests, hardening a data path, or running any task that spans multiple files, sources, or sessions. Two layers: how to RUN the task (stage it, verify each stage with a check that can fail, write done-criteria) and how to WRITE the code (recon before edit, verify against a copy with a negative self-test, single-writer chokepoints, scar-anchored why-comments). Ships a paired hook that deterministically blocks a test from touching live data, the one habit a machine can enforce.4---56# Fable-Discipline78Coding and task discipline, distilled from reading thousands of edits by one9capable model and grading them against an independent review. It is additive: it10does not replace what your model already does well, it adds the habits that strong11engineers have and most code generation skips.1213Most of it is judgment the skill teaches. One slice is enforced by a hook, because14a rule a model can talk itself out of is a suggestion, not a gate. The skill15teaches; the hook makes the one checkable habit non-optional.1617<!-- kipi-only:start -->18## Position: the execution-discipline layer of prd-os1920This skill is not a sibling system to prd-os; it is prd-os's execution-discipline21layer (merged 2026-07-04, see the plugin CHANGELOG). Two load paths, one skill:2223- **PRD work:** `/issue-start` loads this skill before the first edit of every24 DSSE issue. The issue's receipts (verified, reviewed, findings_triaged) are the25 task-level half of the same contract this skill states per-edit.26- **Non-PRD work:** the quick-plan fast path and the fable-discipline auto-invoke27 rule load it for any coding task bigger than a one-line change.2829The public standalone export (github.com/assafkip/fable-discipline) mirrors this30copy; `scripts/export-fable-mirror.sh --check` is the drift blocker.31<!-- kipi-only:end -->3233## When NOT to use this3435One-line changes, typo fixes, a task with one obvious approach that fits in a36single pass. Staging a trivial task buries the answer under ceremony. This earns37its cost only when a one-shot attempt would plausibly miss something.3839---4041## Layer 1: running the task4243How to move through complex work without shipping a confident wrong answer.44451. **Stage before you act.** Write the stage plan first. Number the stages, name46 the one checkable artifact each produces. If a stage produces nothing47 checkable, merge it into the next. The map is living, not a contract; update it48 when what you learn invalidates it.49502. **Verify each stage with a check that can fail.** A test that runs, a file that51 provably exists in the right shape, a source actually read, an output diffed52 against the spec. "I reviewed it and it looks right" is not a check: a model53 that would skip verification also passes its own introspection. If a stage has54 no failable check, say so and mark its output unverified so the gap is visible.55563. **Say, then batch.** State the one-line intent, then fire the burst of actions57 that executes it. The test is binary: a reader given only your intent lines58 can reconstruct the plan, or the intent line failed; rewrite it. It keeps59 you from drifting mid-burst.60614. **Done is written, not felt.** Define done-criteria up front. On a task that62 spans sessions, keep a short work log (decisions, what was tried, what failed)63 and re-read it before continuing, so you do not duplicate work or guess where64 you left off.65665. **Ground before you diagnose.** Confirm a perceived error actually reproduces67 before you act on it. Read where the data comes from before you trust what it68 represents. The failure mode this prevents: seeing a likely error, diagnosing69 it instinctively, and running with that explanation before checking it was real.7071---7273## Layer 2: writing the code74751. **Recon before edit. Read reality, do not assume it.** Grep the real schema and76 call-sites before changing anything. Never edit a file you have not read this77 session, unless you created that file in this same session (your own fresh78 write counts as read). If you already read a schema, re-read the exact field79 names rather than guessing them.80812. **Verify against a copy, with a negative self-test.** A passing gate is not82 trusted until it has been seen to fail. Run the reproducer against a copy of the83 live resource, never the live one, unless the resource is disposable by design84 (a regenerable fixture or sandbox environment). Then corrupt a valid input and85 prove the check FAILS on the violation, so a green result is not a rubber stamp.86873. **Single-writer chokepoint, guarded by a CENSUS TAKEN BY CODE.** Route every88 mutation of a shared resource through one helper, and make a gate enumerate the89 consumers from the source, never from your recollection of them. Migrate existing90 call-sites one small, independently revertible edit at a time, not one bulk91 rewrite. For a module that declares an exclusion predicate, that gate is92 `q-system/.q-system/scripts/consumer-parity-check.py`, a PostToolUse hook that93 walks the AST and reports every walker that skips the predicate. Retired 2026-08-0394 (ASK-315): this line used to say "write a test that greps the tree", which is95 prose naming no executable. Under it, a commit whose message read "one predicate96 for all three consumers" shipped with a fourth, and six instances of that shape97 landed in one file in one night. A habit is not a census.98994. **Why-comments anchored to a named scar.** Comments encode the constraint and100 the specific past bug that motivates it, not a restatement of the code. These101 survive refactors because they encode an invariant.1021035. **Capture every out-of-scope finding; never just mention it.** If you notice104 a real issue that is out of scope for the current work, write it to a tracked105 backlog (an issue, a ticket, a standing ledger your gate reads), not a bare106 prose mention. A mention is a silent drop; a tracked item is one a gate can107 keep failing until it is resolved. The paired lint blocks deferral language108 written into code without such a capture. Override: capture first, then ack109 the captured line with `# spillover-skip`; there is no skip-first path.1101116. **Build against the recurring gap classes.** If the change scales or touches112 sensitive data, walk the gap-class block in `references/checklist.md`: an113 in-memory cap is not a disk bound; a UI hide is not access control; a gate114 fails closed while a filter fails open; redact at the egress edge; a new flag115 must reach every reader; check-then-mutate needs one lock; single-source the116 version; a cross-cutting invariant needs a written scope + a self-enumerating117 guard. Check only the classes the change touches.1181197. **Least-code bias.** Prefer the smallest change that solves it. Reach for120 delete-and-reuse before you write new code; the best fix is often a line121 removed, not a line added. When you catch yourself writing the third122 near-identical thing, stop and generalize the mechanism instead of adding a123 third copy.124125---126127## Consistency rules128129Habits that are easy to do somewhere and forget elsewhere. Make them non-optional:130131- **Test isolation.** A test uses a temp copy, a tempfile, or `:memory:`, never a132 real data path. (This is the slice the paired hook enforces.) Override: an133 audit test that must NAME a live path may assert on it (assertion lines are134 exempt); anything else carries `# fable-discipline-lint-skip` in that file135 with a one-line reason.136- **Declare and pin every new dependency** the moment you import it, and keep a137 test that proves the manifest covers every third-party import.138- **Specify degenerate cases before implementing**: empty, single-element,139 disconnected, non-converging. Each gets defined behavior, not an implicit crash.140- **Validate persisted external input.** Never store arbitrary user or model JSON141 that, if malformed, can permanently break a render or load path, unless every142 read path runs a validating loader that tolerates the malformed record.143- **Enumerate all call-sites when scoping a change.** Grep for every site the144 change must reach before declaring it done.145146## Anti-patterns to drop147148- Guessing a schema or API you already read. Re-read it.149- Acting on an unconfirmed diagnosis. Confirm the error is real first.150- Re-attempting the same failing command. Change the approach, do not retry it.151- Applying the user's stated style rules only to shipped output, not your own152 narration.153154## Communication while building155156Terse mid-task, then decompress at the seam into a short "ran, not assumed" block157listing what you ran and what passed. When a real choice exists, name the options,158mark your pick, give the tradeoff, end with one action.159160## Enforcement161162The deterministic slice (test isolation) is enforced by163`scripts/fable-discipline-lint.py`, wired in `hooks/hooks.json` as a PostToolUse hook.164Everything else is judgment and lives here. See `EXAMPLE.md` for a worked165before/after, and `references/checklist.md` for the copy-paste pre-done gate.