Change Discipline
Applies whenever you edit something that already exists. The hard gate below is a lightweight always-on habit and runs on every such edit. The two facets — boundary and style — apply only when the change actually has that dimension, and live in bundled references so a routine edit does not carry them.
Before you change anything that already exists (hard gate)
Applies to EVERY edit of existing code, a config/contract value, a default, a test expectation, or any behavior — not only bug fixes. The mistake this prevents: treating a symptom (a failing test, a "wrong-looking" value/regex/field name, a reported defect) as the spec and flipping the code to match it, thereby re-introducing a bug a previous commit deliberately fixed.
- Establish WHY the current state is what it is, before changing it. When version-control history exists, run
git log -S "<symbol/value>",git log -L, orgit blameon the exact line/symbol/default you intend to change. Current behavior is often the deliberate result of an earlierfix:[#…]/feat:; the commit message + bug id tell you the intent. When the file or symbol is untracked or history is unavailable, state that limit and reconstruct intent from the primary evidence that remains — current callers and consumers, schemas or contracts, neighboring tests and docs, and observed behavior. Missing history is neither permission to guess nor an automatic blocker. A real near-miss: a removed dangerous-char;(its removal had fixed a log-download bug) — re-adding it to satisfy a stale test would have re-opened the bug. - A symptom is not a spec — decide DIRECTION from history + the real contract, not from the symptom. Current behavior is a deliberate later decision → the test/expectation is stale: update it and record which commit/bug voided the old one. The test/contract is right and the code is incomplete (e.g. committed together yet contradicting) → change the code. Never flip a value or loosen a check just to make a symptom disappear.
- Grep every caller/consumer of a shared symbol, signature, contract field, or config key before changing it, and run the neighboring tests. A change that satisfies one site can break a real caller (e.g. a guard added for one test that blocks an automatic background path). Widening/loosening can't break existing-valid inputs but can have a security blast radius — check that too. When removing a resource, path, mount, or name, also grep its literal string across manifests, scripts, command strings, config, and docs — structured references are not the only references. Case: a shared volume was removed from a manifest's mounts and volume list, but the init container's
chown -Rargument list still carried the path; the container failed on the missing directory and the service never started. - An empty search and a clean
git statuscan both be ignore-rule illusions. Before concluding "nothing matches" from a search run at a repo or hub root, check whether ignore rules silently excluded the directories you meant to search (nested sub-repos, dist dirs) and re-run with ignore rules off or with explicit paths. Symmetrically, a new file on an ignored path leavesgit statusclean without being tracked —git add -f(or fix the rule) and confirm it actually entered the commit. A real case: a hub-root search nearly concluded a whole codebase did not exist. - Proving the change works belongs to
verification. The narrowest failing test read from its real result, no new regression against a clean baseline, real-input comparison against the pre-change build, and the deployed-versus-verified distinction are that skill's ladder. When it is unavailable, run the narrowest test that exercises the change and read the real result, never a piped command's exit code. - Review fixes are new changes and get the same review. After applying a review round, re-review the fixed diff with the same rigor before declaring it done; a fix that changes a name, an identity, an ordering, or a concurrency property is a new design decision, not a patch. Case: across four review rounds nine severe findings surfaced and six of them were introduced by the previous round's fixes, including a content-hash file name that turned a rare overwrite into a routine one.
- Confirm the diagnosis from independent angles before changing code — a wrong root cause yields a confident wrong fix. Reason from the system's own observed behavior, not from the first hypothesis offered: a report that "it's X" — from the user, a teammate, or another agent — is a lead, not a verdict, so think it through from a blank slate. When two sources disagree about the cause, reconcile them with evidence rather than adopting the convenient one; the more consequential or non-obvious the cause, the more independent confirmation it needs (ideally two lines of evidence converging) before you act on it. If the fix that follows is large, cross-cutting, or otherwise high-risk, surface the diagnosis and the options and let the user choose the direction instead of proceeding silently. Choose the fix's scope on a ladder: never a special case for the reported sample; a generic rule inside the component you control; and never a contract pushed onto producers or upstreams you do not control. Case: a versioned-name filter was first patched for one sample name, rejected as a special case, then redesigned as a cross-producer identity contract, rejected as too large; the accepted fix was a field-level generic rule inside the one service that owned the comparison.
- If history shows the change sits on an unresolved design / security / contract decision, stop and escalate with the evidence instead of silently picking a side. Leaving it unchanged-and-flagged is a valid outcome; quietly satisfying a symptom you lack the authority to interpret is not.
- Escalation is for genuine uncertainty, not for a call the user has already made — and that call stays made when a review wants to reverse it. When the user has explicitly decided the direction or authorized the action, carry it out — do not re-confirm, re-litigate, or stall because a subagent or another agent disagrees; the stop-and-escalate above is reserved for an unconfirmed root cause, an unresolved design/security/contract decision, or a large, cross-cutting, or high-risk change. A verified finding — from a review pass, a subagent, or your own later re-read — about something the user deliberately removed, rejected, or chose is a recommendation to raise, not a defect to apply: check history for the deliberate decision (
git log -S,git log --diff-filter=D), surface it, and let the user rule. Naming it in your summary while applying it in the same pass is not consent. A real case: a review flagged a line the user had explicitly deleted as "missing", and the agent restored it — reversing the user's call and then defending the reversal in the file. - Working-tree changes you did not make belong to a human until proven otherwise. When files you never touched show up
deleted/modified, assume the user edited them by hand and ask before acting on them; nevergit restore ./git checkout ./git cleanover unexplained changes, and never attribute them to a sync service, an editor, or an environment glitch without first gathering evidence for that attribution. If you must clear the tree,git stashto preserve the state first. A real case: an agent blamed a batch of unexplained deletions on cloud-sync drift — on a machine with no such sync configured — and bulk-restored, silently undoing the user's own manual cleanup. - Commands that touch login state, auth files, network egress, or a process the user is running are irreversible-class.
login,logout, writing or replacing an auth file, changing a fail-closed or routing rule, restarting a router or browser, and exercising an agent the user is currently using each need the impact stated first and an explicit go-ahead; never run them as a side effect of exploration or testing. Case: a background probe of the CLI login commands invalidated the user's own session, and a proposed auto-login-and-poll flow was rejected for the same reason.
Bundled Resources
Load the facet the change actually has instead of carrying both on every edit:
- When the change touches a boundary — a shared symbol, a public interface, dependency direction, a new abstraction about to be introduced, a config/schema contract with a rollback path, a file that exists as copies across repos, a module that mixes responsibilities, a re-runnable setup/migration that must stay idempotent, or an audit or exploration delegated to subagents that must stay read-only — read references/boundaries-and-contracts.md.
- When the change has a style, naming, input-validation, or comments dimension — including serialized config where an unquoted value can change how the file parses, and the final diff re-read before declaring done — read references/style-and-validation.md.