Releases: orieg/discipline
Releases · orieg/discipline
Release list
v0.14.4
Upgrading to v0.14.4
Behaviour changes
assertion-reduction(looser (false positive removed)): A same-file helper whose whole body is one call to another same-file function, passing names and literals through (a thin wrapper:install(a) { install_with(a, false) }), counts that function's checks as its own, in every pack that resolves helpers; up to three wrappers are followed and a cycle stops. A test calling a helper that became such a wrapper keeps its effective-assertion count instead of being reported. In C/C++ and Python a wrapper hop no longer spends one of the three call levels, so checks reached through wrappers past the third level now count. Migration: None; anallow-assertion-dropadded for a helper refactored into a wrapper can be dropped.
The full ledger is in docs/ROADMAP.md.
What's Changed
- chore(hooks): move this repository's hook files to v0.14.3 by @orieg in #243
- fix(assertion-reduction): resolve same-file helpers through thin wrappers by @orieg in #244
- chore(release): bump version to 0.14.4 with its ledger rows by @orieg in #245
Full Changelog: v0.14.3...v0.14.4
v0.14.3
Upgrading to v0.14.3
Behaviour changes
hook install(additive): New--upgrade: rewrites a file an earlier release generated (it carries theWritten by \discipline hook install`header: the Claude Code bootstrap, the Copilot setup step, the OpenCode plugin) to this release. Without it, such a file that differs is reported as outdated instead of as current. A file without the header is never rewritten. Migration: After a release,hook install --agent ... --upgrade`.hook run,instruction-smuggling(looser (hook runs only)): In a hook's own check (DISCIPLINE_HOOK_RUN, set byhook run), a hook file identical to whathook installof this release writes is a note, notagent-instructions-changed: the hook reads no PR body, so on the branch that adds its own files it could never lift the finding. A hand-edited hook file is still reported; CI is unchanged. Migration: None.ci-integrity(looser (false positives removed; one error becomes a warning)): A step that reports (a reporting action,github-scriptthat cannot fail, or a name starting with a reporting verb such asComment) is no longer a verification step because its name mentionsgate/test/check, so narrowing or removing it is not reported;continue-on-errorin a job that verifies nothing is a warning, not an error. Migration: None.replay(additive (new summary line and JSON fields)): A change that skipped part of a gate because the configuration names a file the change does not have yet (aversion-lockstepgroup or amanifest-syncrule) is counted in the summary: the text output says how many changes skipped such a check and names the gate and group or manifest;--jsonaddsskipped_by_gate(gate → changes) and a per-caseskipped_checkslist. Migration: None.doctor(looser (an intended shadow step is not a FAIL)): An action step withadvisory: truemarked# discipline:advisory <reason>(on the line above the step or on itsuses:line) is reported asnon-blockinginfo naming the reason, not a FAIL. A marker without a reason or with a placeholder reason is ignored. The step still enforces nothing: a job whose only discipline steps are marked advisory still does not count as a blocking check. Migration: None; mark a deliberate shadow step to clear its FAIL.
The full ledger is in docs/ROADMAP.md.
What's Changed
- chore(hooks): move this repository's hook files to v0.14.2 by @orieg in #240
- fix: adoption round 2 (reporting steps, hook install --upgrade, hook runs skip their own files, doctor advisory marker, replay skips) by @orieg in #241
- chore(release): bump version to 0.14.3 with its ledger rows by @orieg in #242
Full Changelog: v0.14.2...v0.14.3
v0.14.2
Upgrading to v0.14.2
Behaviour changes
version-lockstep(looser (a change is not blamed for drift it did not cause)): Drift the base already had is a note, notversion-mismatch, when the change touches none of the group's sources; the next change that touches one must resolve it. Migration: None.dependency-delta(looser (false positive removed)): Apyproject.tomlrequirement on the project's own extras (all = ["<project>[a,b]"]) is not reported as a new or wildcard dependency. Migration: None.time-estimates(looser (false positives removed)): A lookback (the last 24 hours, also across a soft wrap),this <period>'s, andQ<n>followed by a reporting noun (the Q3 invoice) are not reported. Migration: None.ci-integrity(looser (false positives removed); one error becomes a warning): Fewer false positives, found by consumer adoption runs: a rollup job withif: always()that reads itsneedsresults is notverification-job-masked-by-condition; aset +ewhose$?is saved and tested is notexit-code-masked;verification-step-masked-by-conditionis reported only when an existing verification step now runs only after a failure (a newif: failure()diagnostic, or a barealways(), is not); a verification job or workflow whose steps pair by body with a job this change added (a rename, split, or move to another file) is a note, notverification-job-removed/verification-workflow-deleted.always() && <condition>on a verification step is nowverification-step-narrowed(warning), not masked-by-condition (error). Migration: None.--format agent-prompt,hook run, MCPcheck_diff(output (agent text)): Warnings are listed after the blocking findings under## Warnings (not blocking), with the instruction to fix one only if the change caused it; a report with only warnings says they do not block. Observe mode's note and log name only the blocking codes.--fail-on-warningskeeps every finding an issue. Migration: None.hook install(additive): A release binary writes the Copilot setup step pinned to its own commit (uses: orieg/discipline@<sha> # v<version>) instead of the tag;hook installwarns when git ignores a file it wrote; the Claude Code bootstrap puts~/.local/binonPATHthroughCLAUDE_ENV_FILEwhen it is not there. Migration: A setup-steps workflow written by an earlier release keeps its tag ref; pin it by hand or rewrite it.doctor(looser (a satisfied advisory no longer reported)): Thepush-triggerfinding is Pass, not Info, whenmerged-pr-bodyis enabled and every discipline job that runs on push is grantedpull-requests: readorwrite(orread-all/write-all) by its job'spermissions:or, when the job declares none, the workflow's: that grant is the fix the Info finding asked for. A job-levelpermissions:without it still reports Info, since it replaces the workflow's. Migration: None.replay --json(additive): Each entry ofcases_detailgainsfindings: thecode,severity,fileandlineof every error and warning of that change's report, so what blocked a change is read from the replay instead of acheckper commit. The message is never included, since a finding can echo secret material.discipline.replay.schema.jsongains the field; itsschema_versionstays 1. Migration: None. A consumer that validates replay output against a copy of the old schema withadditionalProperties: falseupdates its copy.check(additive): New environment variableDISCIPLINE_PR_BODY_FILEnames the file--pr-body-filewould: the flag wins over it, and it wins overPR_BODY. Before, it was silently ignored. The agent hooks (hook run) andreplayclear it for the checks they run, as they clearPR_BODY. Migration: None, unless a pipeline setDISCIPLINE_PR_BODY_FILEfor another purpose: its file is now read as the pull-request body.baseline --migrate(looser): With no baseline file,baseline --migrateprints a note that there is nothing to migrate and exits 0, so an adoption script can run it unconditionally; before, it was an error (exit 2). A baseline file that cannot be read still exits 2. Migration: None.doctor(looser (a false warning removed)): A workflow step that uses the action withinstall_only: true(ashook install --agent copilot --cloud-agentwrites) is no longer counted as a discipline job: its triggers, token and push runs are not reported, sodoctor --strictno longer fails on the Copilot setup steps. A step whoseinstall_onlyis an expression still counts. Migration: None.
The full ledger is in docs/ROADMAP.md.
What's Changed
- fix(doctor): an install_only action step is not a discipline job by @orieg in #236
- chore(hooks): dogfood discipline's agent hooks in observe mode by @orieg in #235
- fix(gates): false positives from v0.14.1 adoption runs by @orieg in #237
- fix(hook): warnings apart from blocking issues, a pinned cloud-agent step, and CLI adoption fixes by @orieg in #238
- chore(release): bump version to 0.14.2 with its ledger rows by @orieg in #239
Full Changelog: v0.14.1...v0.14.2
v0.14.1
Upgrading to v0.14.1
Behaviour changes
hook run,hook install, MCP (additive): Newhook run --observe(andhook install --observe, which writes it): the check runs but never blocks; what would have blocked is said on stderr, marked as observe mode, and appended to<git dir>/discipline/hook-observe.log. The text an agent reads when a check cannot run now names the reason and gate (discipline could not check this change (reason: tool-missing, gate miri)), in the hook and in MCPcheck_diff. Migration: None.
The full ledger is in docs/ROADMAP.md.
What's Changed
- feat(hook): observe mode, and could-not-check reasons named to the agent by @orieg in #233
- chore(release): bump version to 0.14.1 with its ledger rows by @orieg in #234
Full Changelog: v0.14.0...v0.14.1
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 versio...
v0.13.1
Upgrading to v0.13.1
Behaviour changes
time-estimates(looser; stricter (scoped)): A duration narrated as observed is not an estimate: one in the present perfect or a past passive, a spanon record for, a gapwith noevent. A frequency, an age or a sub-hour bound written with a number word is exempt as its digit form already was, and so is a test run length (N-hour run). Thein N/within Nweeks /takes Nform and plan vocabulary still fire, andscopednow counts as plan vocabulary. Migration: A sentence withscopedand a duration can receive a finding it did not before.replay(looser (replay only)): Each replayed change is checked with its merged pull request's author as--actor, as the action does, and an actor in the replaying shell is ignored. Underfail_on_overrides = true, an override by an allowed author is no longer refused, and a change blocked only by refused overrides names those gates and the reason instead of listing no gate. On a Gitea repository withallowed_override_actors: 8 of 19 blocked changes were such refusals. Migration: None.
The full ledger is in docs/ROADMAP.md.
What's Changed
- fix(replay): judge overrides against the pull request author by @orieg in #203
- fix(time-estimates): observed durations and number-word intervals are not estimates by @orieg in #204
- chore(release): bump version to 0.13.1 with its ledger rows by @orieg in #205
Full Changelog: v0.13.0...v0.13.1
v0.13.0
Upgrading to v0.13.0
Behaviour changes
- Objective-C (looser (parse errors no longer block); stricter (code in those files is now read)): Apple enum heads (
typedef NS_ENUM(...),NS_OPTIONS,CF_ENUM) and annotation / availability macros are rewritten byte for byte before the parse,[languages.c] macrosapplies, and a.hheader is read with the C, C++ or Objective-C grammar that leaves the fewest error regions; an Objective-C parse error is a warning, as in C. On a public Objective-C library: 156 files with 5,114 error regions become 6 with 26. Migration: An Objective-C project can receive findings in code that was unread. assertion-reduction(looser):Source File Could Not Be Fully Parsedis a warning, not an error, for a file that could hide no test: outside a test path, no tests on either side, and not Rust. In a test file, a file with tests, or Rust it still blocks. Migration: None.- Swift (looser): Waiting on XCTest expectations (
fulfillment(of:),wait(for:timeout:),waitForExpectations(timeout:)) counts as an assertion. Migration: None. - action (changed): A workflow that pins the action by commit SHA (as
ci-integrityrequires) or a branch runs the binary of the release in the action's ownCargo.toml, as@v0already did, instead of the latest release. An explicitversion:still wins. Migration: None; setversion:to choose another release. check(looser):--config ../candidate.toml, a relative path that escapes the repository, no longer exits 2; it is treated like an absolute path outside the repository (v0.12.1). Migration: None.check(stricter):DISCIPLINE_NO_NETWORK=1also skips the CI-onlygit fetchthat deepens a shallow clone when the base does not resolve, and says so in the diagnostics. Migration: UnsetDISCIPLINE_NO_NETWORKto let a shallow CI clone fetch its base.assertion-reduction(looser): A newly added NUL byte is also lifted by the marker of the gate that reports it (discipline:allow(assertion-reduction): <path> <reason>), as its remediation says; the olddiscipline:allow(vacuous-tests)form still works. Migration: None.
The full ledger is in docs/ROADMAP.md.
What's Changed
- fix: a SHA-pinned action runs its own release; audit follow-ups by @orieg in #201
- fix(objc,swift): Apple macros and ObjC headers parse, parse errors block only where a test could hide by @orieg in #200
- docs(architecture): the 1.0 stability policy (what 1.0 freezes) by @orieg in #197
- docs: align gate, configuration and architecture docs with the code by @orieg in #199
- chore(release): bump version to 0.13.0 with its ledger rows by @orieg in #202
Full Changelog: v0.12.3...v0.13.0
v0.12.3
Upgrading to v0.12.3
Behaviour changes
- Python (looser (assertion counts)): Same-file helpers are followed up to three calls deep (was one), as in C/C++, a recursive helper counted once: a check that moves from the function a test drives into a validator that function calls is no longer an assertion drop. Migration: None.
The full ledger is in docs/ROADMAP.md.
What's Changed
- fix(python): same-file helpers followed three calls deep, as in C/C++ by @orieg in #196
- chore(release): bump version to 0.12.3 with its ledger row by @orieg in #198
Full Changelog: v0.12.2...v0.12.3
v0.12.2
Upgrading to v0.12.2
Behaviour changes
- C/C++ (looser): A test (or helper) that calls same-file functions through a table it builds (
std::vector<std::pair<std::string, void (*)()>> tests = {{"get", TestGet}},{check_a, &check_b}) counts their checks; the three-call helper depth of v0.12.1 had turned such a refactor into an assertion drop. Migration: None. [tests] functions(looser): A declared Python name that starts with_(_self_test) is a test and its body test scope; the private-name rule used to override the declaration. Migration: None.
The full ledger is in docs/ROADMAP.md.
What's Changed
- fix(c,python): tests run from a table count, a declared
_self_testis a test by @orieg in #194 - chore(release): bump version to 0.12.2 with its ledger rows by @orieg in #195
Full Changelog: v0.12.1...v0.12.2
v0.12.1
Upgrading to v0.12.1
Behaviour changes
error-swallowing(looser): A Python handler that catches only parse errors and does nothing butcontinue(a loop skipping lines that do not parse) isUnparseable Input Skipped, a warning at most, instead of a blockingEmpty Error Handler Added. A barereturnin a handler followed by a failing statement after thetryis the expect-this-to-raise idiom and is no longer reported. Migration: None.- C/C++ (looser (assertion counts); stricter (code in C++ headers and guarded C headers is now read)): Same-file helpers are followed up to three calls deep (was one), a recursive helper counted once;
abort,exit,std::terminateand__builtin_trapcount as fatal assertions;extern "C"guards under#ifdef __cplusplusno longer leave error regions in C headers; a.hheader the C grammar cannot read is re-read as C++ when that leaves fewer error regions. Migration: A C or C++ header can receive findings it did not before. error-swallowing(looser): A handler whosetrybody ends in a statement that always fails (assert False,raise,pytest.fail(...), JUnitfail(...)) is the expect-this-to-raise idiom and is no longer reported. Migration: None.error-swallowing(looser): A PythonexceptforKeyboardInterrupt,SystemExitorGeneratorExitalone is no longerEmpty Error Handler Added: these are stop and exit requests, not errors. Mixed with an error type, or asBaseException, it is still reported. Migration: None.config-integrity,command,test-floor,--policy-from base(looser): A--configpath outside the repository no longer exits 2 (repo path ... should be relative): the base side falls back to the basediscipline.toml, andconfig-integritynotes that it compares nothing for an operator's file the change cannot edit. Migration: None.
The full ledger is in docs/ROADMAP.md.
What's Changed
- fix(config): a
--configoutside the repository no longer exits 2 by @orieg in #189 - fix(error-swallowing): a Python handler for a stop or exit signal alone is not reported by @orieg in #190
- fix(c,python): helpers three calls deep, C header guards, C++ headers, expect-raise try bodies by @orieg in #191
- feat(error-swallowing): a loop skipping unparseable input is a warning; return-then-fail is expect-raise by @orieg in #192
- chore(release): bump version to 0.12.1 with its ledger rows by @orieg in #193
Full Changelog: v0.12.0...v0.12.1