Skip to content

Releases: orieg/discipline

v0.14.4

Choose a tag to compare

@github-actions github-actions released this 28 Sep 03:33
v0.14.4
410daf2

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; an allow-assertion-drop added 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

Choose a tag to compare

@github-actions github-actions released this 28 Sep 00:20
v0.14.3
52a0565

Upgrading to v0.14.3

Behaviour changes

  • hook install (additive): New --upgrade: rewrites a file an earlier release generated (it carries the Written 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 by hook run), a hook file identical to what hook install of this release writes is a note, not agent-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-script that cannot fail, or a name starting with a reporting verb such as Comment) is no longer a verification step because its name mentions gate / test / check, so narrowing or removing it is not reported; continue-on-error in 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 (a version-lockstep group or a manifest-sync rule) is counted in the summary: the text output says how many changes skipped such a check and names the gate and group or manifest; --json adds skipped_by_gate (gate → changes) and a per-case skipped_checks list. Migration: None.
  • doctor (looser (an intended shadow step is not a FAIL)): An action step with advisory: true marked # discipline:advisory <reason> (on the line above the step or on its uses: line) is reported as non-blocking info 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

Choose a tag to compare

@github-actions github-actions released this 27 Sep 17:16
v0.14.2
f251807

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, not version-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)): A pyproject.toml requirement 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, and Q<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 with if: always() that reads its needs results is not verification-job-masked-by-condition; a set +e whose $? is saved and tested is not exit-code-masked; verification-step-masked-by-condition is reported only when an existing verification step now runs only after a failure (a new if: failure() diagnostic, or a bare always(), 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, not verification-job-removed / verification-workflow-deleted. always() && <condition> on a verification step is now verification-step-narrowed (warning), not masked-by-condition (error). Migration: None.
  • --format agent-prompt, hook run, MCP check_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-warnings keeps 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 install warns when git ignores a file it wrote; the Claude Code bootstrap puts ~/.local/bin on PATH through CLAUDE_ENV_FILE when 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)): The push-trigger finding is Pass, not Info, when merged-pr-body is enabled and every discipline job that runs on push is granted pull-requests: read or write (or read-all / write-all) by its job's permissions: or, when the job declares none, the workflow's: that grant is the fix the Info finding asked for. A job-level permissions: without it still reports Info, since it replaces the workflow's. Migration: None.
  • replay --json (additive): Each entry of cases_detail gains findings: the code, severity, file and line of every error and warning of that change's report, so what blocked a change is read from the replay instead of a check per commit. The message is never included, since a finding can echo secret material. discipline.replay.schema.json gains the field; its schema_version stays 1. Migration: None. A consumer that validates replay output against a copy of the old schema with additionalProperties: false updates its copy.
  • check (additive): New environment variable DISCIPLINE_PR_BODY_FILE names the file --pr-body-file would: the flag wins over it, and it wins over PR_BODY. Before, it was silently ignored. The agent hooks (hook run) and replay clear it for the checks they run, as they clear PR_BODY. Migration: None, unless a pipeline set DISCIPLINE_PR_BODY_FILE for another purpose: its file is now read as the pull-request body.
  • baseline --migrate (looser): With no baseline file, baseline --migrate prints 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 with install_only: true (as hook install --agent copilot --cloud-agent writes) is no longer counted as a discipline job: its triggers, token and push runs are not reported, so doctor --strict no longer fails on the Copilot setup steps. A step whose install_only is 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

Choose a tag to compare

@github-actions github-actions released this 27 Sep 03:05
v0.14.1
712239d

Upgrading to v0.14.1

Behaviour changes

  • hook run, hook install, MCP (additive): New hook run --observe (and hook 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 MCP check_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

Choose a tag to compare

@github-actions github-actions released this 26 Sep 20:12
v0.14.0
4ec3bd3

Upgrading to v0.14.0

Default changes

  • agent-scratch: exempt_paths without the bootstrap → exempt_paths gains .claude/hooks/discipline-bootstrap.sh (looser). The script hook install --agent claude-code writes is shared project configuration, like .claude/settings.json; instruction-smuggling still 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 lets discipline replay skip 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 set DISCIPLINE_REPLAY_CASE: remove it.
  • dependency-delta (stricter): uv.lock, composer.lock and Gemfile.lock are 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 directive allow-vacuous-test: <test> <reason> (also discipline:allow(vacuous-tests)) lifts every finding on that one new test; before, no directive could, and only exempt_paths, a helper declaration or the baseline did. Migration: None.
  • replay (changed (replay --json shape)): --json: gates whose overrides fail_on_overrides refused are listed in a case's own refused_overrides field (with the actor the case was checked as) and in the summary's refused_overrides_by_gate, instead of in blocking_gates with the reason in detail. 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 from blocking_gates or detail reads refused_overrides.
  • msrv (changed (exit 1 becomes 2)): A command that cannot run (not found, cannot start, over the timeout) and a Cargo.toml that 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 says MSRV declared, not verified. 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): An extra_assert_macros entry 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: @SuppressWarnings named 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_ALIASES is read as the new one with a note in the report's new deprecations list (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 json report and the replay --json summary ship as discipline.report.schema.json and discipline.replay.schema.json, generated by discipline docs and checked by docs --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 new code field on each JSON report violation, the [gate/code] heading in the agent-prompt report (hooks, discipline mcp), and a Findings: list in discipline 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 --write writes version = 2 with rule = "gate/code". A version-1 file still matches, with a deprecated: line, until discipline baseline --migrate rewrites it; config-integrity accepts that migration alone without a directive. Each finding in --format json gains fingerprint; SARIF ruleId is the finding code (one rule per code, gate as a tag) with partialFingerprints; the GitLab Code Quality check_name is the code and its fingerprint is the baseline fingerprint. Source-parse findings are coded under assertion-reduction whichever AST gate reports them. Migration: Code scanning and GitLab re-open their alerts once under the new ids; run discipline baseline --migrate in a change of its own.
  • agent surfaces (changed (agent-prompt text)): The Repair: line of the agent-prompt report (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 an execution, toolchain or nightly waiver 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-out and a JSON --output-file: no outcomes, and a could_not_check object with the reason (configuration, baseline, repository, tool-missing, tool-timeout, toolchain-unavailable, forge, gate, internal), the gate that could not run and the error. Before, --json-out held a single engine outcome with the error as its violation; JUnit, SARIF and GitLab reports still do. Every JSON report and replay summary starts with schema_version (1). The error on stderr is unchanged. Migration: A consumer that read the error from outcomes[0] of an exit-2 --json-out report reads could_not_check.detail. One that treated any stdout under --format json as a completed check reads the exit code first.
  • replay (changed): could_not_check_by_reason is keyed by the reason code the child check reported (forge for 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 carries reason, and the text summary lists each change with its error under its reason. Migration: A consumer that matched error text in the keys reads cases_detail[].detail.
  • MCP check_diff (additive): A check that could not run carries reason and gate in structuredContent, beside status: could_not_check. Migration: None.
  • MCP check_diff (additive): structuredContent carries schema_version and, when the check ran, findings: each finding's code, severity, title, location, message, repair and fingerprint, without the remediation. The tool declares it as its outputSchema. 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 words removes and deletes are 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...
Read more

v0.13.1

Choose a tag to compare

@github-actions github-actions released this 25 Sep 20:03
v0.13.1
ac158b1

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 span on record for, a gap with no event. 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). The in N / within N weeks / takes N form and plan vocabulary still fire, and scoped now counts as plan vocabulary. Migration: A sentence with scoped and 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. Under fail_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 with allowed_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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 15:59
v0.13.0
2410e5d

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] macros applies, and a .h header 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 Parsed is 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-integrity requires) or a branch runs the binary of the release in the action's own Cargo.toml, as @v0 already did, instead of the latest release. An explicit version: still wins. Migration: None; set version: 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=1 also skips the CI-only git fetch that deepens a shallow clone when the base does not resolve, and says so in the diagnostics. Migration: Unset DISCIPLINE_NO_NETWORK to 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 old discipline: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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 05:34
v0.12.3
5fa2b71

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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 00:49
v0.12.2
98492b7

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_test is 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

Choose a tag to compare

@github-actions github-actions released this 24 Sep 22:51
v0.12.1
802fe29

Upgrading to v0.12.1

Behaviour changes

  • error-swallowing (looser): A Python handler that catches only parse errors and does nothing but continue (a loop skipping lines that do not parse) is Unparseable Input Skipped, a warning at most, instead of a blocking Empty Error Handler Added. A bare return in a handler followed by a failing statement after the try is 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::terminate and __builtin_trap count as fatal assertions; extern "C" guards under #ifdef __cplusplus no longer leave error regions in C headers; a .h header 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 whose try body ends in a statement that always fails (assert False, raise, pytest.fail(...), JUnit fail(...)) is the expect-this-to-raise idiom and is no longer reported. Migration: None.
  • error-swallowing (looser): A Python except for KeyboardInterrupt, SystemExit or GeneratorExit alone is no longer Empty Error Handler Added: these are stop and exit requests, not errors. Mixed with an error type, or as BaseException, it is still reported. Migration: None.
  • config-integrity, command, test-floor, --policy-from base (looser): A --config path outside the repository no longer exits 2 (repo path ... should be relative): the base side falls back to the base discipline.toml, and config-integrity notes 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 --config outside 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