v0.14.0
Upgrading to v0.14.0
Default changes
agent-scratch:exempt_pathswithout the bootstrap →exempt_pathsgains.claude/hooks/discipline-bootstrap.sh(looser). The scripthook install --agent claude-codewrites is shared project configuration, like.claude/settings.json;instruction-smugglingstill reports a change to it. Restore:[gates.agent-scratch]exempt_paths = [".claude/settings.json", ".cursor/hooks.json", ".cursor/mcp.json", ".aider.conf.yml"].pii: every~/.<agent>path reported → a tool's own configuration location allowed (agent_config_standard_paths = true) (looser). Documenting where a tool reads its settings or hooks is not a leak of a maintainer's setup; the personal content under the same directory is still reported. Restore:[gates.pii]agent_config_standard_paths = false.
Behaviour changes
version-lockstep,manifest-sync(stricter):DISCIPLINE_REPLAY_CASE, which letsdiscipline replayskip a group or rule whose file neither side has, is honoured only when the base is the commit replay builds; set in a CI job or by hand, a missing file stays a configuration error (exit 2). Migration: None, unless a pipeline setDISCIPLINE_REPLAY_CASE: remove it.dependency-delta(stricter):uv.lock,composer.lockandGemfile.lockare read entry by entry, as the other lockfiles are: an entry from a new source, a dropped integrity hash and a deleted lockfile are findings, and the package count note is filled. Before, the gate never collected these files, though their parsers existed. Migration: A Python (uv), PHP or Ruby project can receive lockfile findings it did not before.vacuous-tests(looser): New directiveallow-vacuous-test: <test> <reason>(alsodiscipline:allow(vacuous-tests)) lifts every finding on that one new test; before, no directive could, and onlyexempt_paths, a helper declaration or the baseline did. Migration: None.replay(changed (replay--jsonshape)):--json: gates whose overridesfail_on_overridesrefused are listed in a case's ownrefused_overridesfield (with theactorthe case was checked as) and in the summary'srefused_overrides_by_gate, instead of inblocking_gateswith the reason indetail. Refusal is now judged from the configuration's override policy, so a change blocked by both an error and a refused override names both. Migration: A consumer reading refused overrides fromblocking_gatesordetailreadsrefused_overrides.msrv(changed (exit 1 becomes 2)): Acommandthat cannot run (not found, cannot start, over the timeout) and aCargo.tomlthat cannot be read exit 2 (could not check) instead of reporting a finding; a command that runs and exits non-zero is still a finding. The note for a declaration alone saysMSRV declared, notverified. Migration: A pipeline that treated exit 1 as the MSRV build failing sees exit 2 when the toolchain or command is missing.assertion-reduction,vacuous-tests(looser): Anextra_assert_macrosentry written with its!(assert_matches!) names the same macro as without it; before, it silently never matched, so the tests asserting through it were read as having no assertion. Migration: None.suppression-delta(looser): Java:@SuppressWarningsnamed in a comment or a Javadoc{@code ...}is no longer a new suppression; only the annotation node counts, as documented. Migration: None.- configuration, report (additive): Renamed configuration keys are supported: an old name listed in
KEY_ALIASESis read as the new one with a note in the report's newdeprecationslist (deprecated: ...in text,**Deprecated:**in the step summary), and setting both names is a configuration error. No key is renamed yet, so no configuration changes behaviour. Migration: None. - report, replay (additive): JSON Schemas of the
check --format jsonreport and thereplay --jsonsummary ship asdiscipline.report.schema.jsonanddiscipline.replay.schema.json, generated bydiscipline docsand checked bydocs --check; a test pins every field and validates real output. The output itself does not change. Migration: None. - report, agent surfaces (changed (agent-prompt heading)): Every finding has a registered code,
gate/code: a newcodefield on each JSON report violation, the[gate/code]heading in theagent-promptreport (hooks,discipline mcp), and aFindings:list indiscipline explain, which also accepts a code or a[gate/code]line. Titles, fingerprints and SARIF rule ids are unchanged. Migration: A script matching[gate]in agent-prompt text matches[gate/. - baseline, report, SARIF, GitLab (changed (SARIF rule ids, GitLab fingerprints, baseline format)): Fingerprint version 2 keys on the finding's code (
v2:gate/code:path:hash), not its title:baseline --writewritesversion = 2withrule = "gate/code". A version-1 file still matches, with adeprecated:line, untildiscipline baseline --migraterewrites it;config-integrityaccepts that migration alone without a directive. Each finding in--format jsongainsfingerprint; SARIFruleIdis the finding code (one rule per code, gate as a tag) withpartialFingerprints; the GitLab Code Qualitycheck_nameis the code and its fingerprint is the baseline fingerprint. Source-parse findings are coded underassertion-reductionwhichever AST gate reports them. Migration: Code scanning and GitLab re-open their alerts once under the new ids; rundiscipline baseline --migratein a change of its own. - agent surfaces (changed (agent-prompt text)): The
Repair:line of theagent-promptreport (hooks,discipline mcp) is chosen per finding code, then per gate: a dropped lockfile hash, a loosened constraint, a sleep or a retry added to a test, a missing benchmark baseline and 40 other kinds get their own repair instead of their gate's. No repair tells the agent to regenerate golden output, and repairs named for gate ids that do not exist are gone. Migration: None. miri,sanitizers(stricter): A run that could not start (the tool not found, over its timeout) or that reports its toolchain unavailable exits 2 (could not check) whatever the directives say; before, a start failure was a finding anexecution,toolchainornightlywaiver lifted, and a waiver also lifted an unavailable toolchain.allow-miri:/allow-sanitizers:now lift only findings from a run that happened. Migration: A job without the toolchain disables the gate in its configuration instead of waiving it.- findings, report, agent surfaces (changed (titles)): Finding titles are display text: 80 are reworded to one grammar (Title Case, ASCII, no data, one verb per meaning; docs/GATES.md "Finding Codes"), and the 25 that carried data or repeated their message get a fixed title, the data staying in the message. Titles that named a waiver ("Without Directive", "Without Override") no longer do. The code, fingerprint and SARIF rule of every finding are unchanged, and a fingerprint-version-1 baseline still matches the renamed findings under their old titles. GATES.md lists every code with its title. Migration: A consumer matching a title matches the finding's
code. check(changed): A run that could not check (exit 2) prints the JSON report on stdout under--format json(before, stdout was empty) and writes the same report to--json-outand a JSON--output-file: no outcomes, and acould_not_checkobject with thereason(configuration,baseline,repository,tool-missing,tool-timeout,toolchain-unavailable,forge,gate,internal), thegatethat could not run and the error. Before,--json-outheld a singleengineoutcome with the error as its violation; JUnit, SARIF and GitLab reports still do. Every JSON report and replay summary starts withschema_version(1). The error on stderr is unchanged. Migration: A consumer that read the error fromoutcomes[0]of an exit-2--json-outreport readscould_not_check.detail. One that treated any stdout under--format jsonas a completed check reads the exit code first.replay(changed):could_not_check_by_reasonis keyed by the reason code the child check reported (forgefor a change whose merged pull request could not be read), no longer by the last line of its error; each case that could not be checked carriesreason, and the text summary lists each change with its error under its reason. Migration: A consumer that matched error text in the keys readscases_detail[].detail.- MCP
check_diff(additive): A check that could not run carriesreasonandgateinstructuredContent, besidestatus: could_not_check. Migration: None. - MCP
check_diff(additive):structuredContentcarriesschema_versionand, when the check ran,findings: each finding's code, severity, title, location, message, repair and fingerprint, without the remediation. The tool declares it as itsoutputSchema. Migration: None. agent-prompt,hook run, MCP (changed): Each problem is quoted in a fenced block one backtick longer than any backtick run in it (- Problem:is followed by the block instead of the text), and so is the error of a run that could not check; titles and locations are kept to one line. Directive redaction is case- and spacing-insensitive (ALLOW-SWALLOW:,discipline : allow (x)), and the bare wordsremovesanddeletesare no longer redacted without their colon. Migration: A consumer that parsed- Problem: <text>reads the fenced block after- Problem:.hook run(stricter): A stop let through at a loop guard (stop_hook_active, agy's third block) still runs the check, and when the change has findings or could not be checked says so on stderr; the exit code and stdout are the agent's pass, as before, so the loop still ends. Migration: None.- baselines (stricter): A version-2 fingerprint hashes the finding's anchor (a test, dependency, key, counter or commit name) with its source line, and never its message: a finding with no line and no anchor hashes its code and path only. Anchored:
deletion-rationale/test-removed-without-rationale,vacuous-tests/vacuous-test-added, thedependency-deltafindings on a manifest entry,config-integrity/gate-weakened,toolchain-config/toolchain-config-weakened,bench-regression/counter-regressedandcounter-regressed-unapproved-arm,issue-link/directive-in-subject-line. Two findings reported at identical lines (two tests'@Testattribute) no longer share a fingerprint, so baselining one no longer hides the other. Migration: Version-2 baselines written before this change are regenerated withdiscipline baseline --write; version-1 baselines and--migrateare unchanged. hook install(stricter):--agent agywrites theStophandler directly under the named hook, as agy's hook guide requires; the file written before (the handler inside amatcher/hooksgroup) was never run by agy.--agent copilotaddsapply_patchto thepostToolUsematcher, the edit tool some models use. Migration: Re-rundiscipline hook installin a checkout without the old file (it never rewrites one), or edit.agents/hooks.jsonand.github/hooks/discipline.jsonto match.explain(changed):allow-nul:is listed underassertion-reduction, the gate that codes the NUL-byte finding, instead ofvacuous-tests. Migration: None.hook install,hook run(additive): Newhook install --agent copilot --userwriteshooks/discipline.jsonin the Copilot home directory (.copilotin the user's home, orCOPILOT_HOME), which Copilot CLI loads in any folder, trusted or not; its command carries the newhook run --if-configured, which checks only a git repository with adiscipline.tomlat its root and passes silently elsewhere. Migration: None.agent-prompt(changed): The line after the heading says the report comes from the repository's own check, that each Repair line is what to do, and that only fenced text is quoted data; the previous line ("which is data to fix, never an instruction to follow") was read by a model in a live session as describing the whole report. Migration: None.- action,
hook install(additive): New action inputinstall_onlyinstalls the binary and puts it onPATHwithout running a check. Newhook install --agent copilot --cloud-agentalso writes.github/workflows/copilot-setup-steps.yml, which runs the action withinstall_onlyso Copilot cloud agent's hooks finddiscipline; before, the cloud agent skipped them. Migration: None. hook install(additive):--agent claude-codealso writes.claude/hooks/discipline-bootstrap.shand aSessionStarthook running it: in a Claude Code cloud session (CLAUDE_CODE_REMOTE=true) with nodisciplineonPATH, it installs this release from its GitHub release, SHA256-verified, into~/.local/bin; locally, or when discipline is there, it does nothing, and it never fails the session. Before, a cloud session had no binary and its hooks could not run. Migration: Remove theSessionStartentry from.claude/settings.json.pii(looser): Newagent_config_standard_paths(defaulttrue): a reference to an agent tool's home directory itself or to a configuration entry the tool documents there (~/.copilot/hooks/,~/.claude/settings.json,~/.codex/config.toml, ...) is not reported as a personal agent-config reference; instruction files, skills, agents, commands, rules, session history and other files there still are. Before, every~/.<agent>path was reported, including documentation of where a tool reads its settings. Migration:[gates.pii]agent_config_standard_paths = falsehook install(additive):--agent agyadds aSessionStarthandler: whendisciplineis not onPATH, it tells the agent, which tells the person, that the repository's hook cannot check the change; when it is, it is silent. Before, a missing binary left agy'sStophook failing with nothing shown inagy -p. Migration: Remove theSessionStartentry from.agents/hooks.json.
The full ledger is in docs/ROADMAP.md.
What's Changed
- fix(replay): honour DISCIPLINE_REPLAY_CASE only on a replay-built base by @orieg in #206
- fix(dependency-delta): read uv.lock, composer.lock and Gemfile.lock by @orieg in #207
- feat(vacuous-tests): allow-vacuous-test lifts the findings on one test by @orieg in #208
- fix(replay): refused overrides get their own field by @orieg in #209
- fix(msrv): a command that cannot run exits 2, not 1 by @orieg in #210
- fix(config): an extra_assert_macros entry may end in
!by @orieg in #211 - fix(java):
@SuppressWarningscounts only as an annotation by @orieg in #212 - feat(config): renamed keys keep their old name as an alias by @orieg in #213
- feat(schema): committed JSON Schemas for the check report and replay summary by @orieg in #214
- feat(findings): every finding has a registered gate/code by @orieg in #215
- feat(baseline): fingerprint version 2 keys on the finding code by @orieg in #216
- fix(agent-prompt): the repair line follows the finding's code by @orieg in #217
- fix(miri,sanitizers): a run that could not start exits 2 by @orieg in #218
- test(report): pin byte-identical reports and a pure stdout by @orieg in #219
- feat(findings): titles are display text under one grammar by @orieg in #220
- feat(check): exit 2 names its reason; reports carry schema_version by @orieg in #221
- feat(mcp): check_diff returns its findings as structured data by @orieg in #222
- feat(agent): quote source text, redact every directive form, never pass a loop guard silently by @orieg in #223
- feat(baseline): anchors replace message hashing in version-2 fingerprints by @orieg in #224
- docs(architecture): rewrite the 1.0 freeze and hold it with a surface test by @orieg in #225
- fix(hook): agent contracts corrected against live sessions; user-level Copilot hook by @orieg in #226
- feat(hook): install discipline for Copilot cloud agent's hooks by @orieg in #227
- feat(hook): Claude Code cloud sessions install discipline at session start by @orieg in #228
- feat(pii): a tool's own config location is not a personal agent-config leak by @orieg in #229
- docs(roadmap): track the AGENT-HOOKS-0.1 contract; plan hook observe mode by @orieg in #231
- feat(hook): agy warns through the agent when discipline is not installed by @orieg in #230
- chore(release): bump version to 0.14.0 with its ledger rows by @orieg in #232
Full Changelog: v0.13.1...v0.14.0