Skip to content

feat(conventions): define .config/ concerns and wrap each tool in a task - #121

Merged
justinmerrell merged 15 commits into
mainfrom
feat/config-intent-tool-layer
Oct 24, 2026
Merged

justinmerrell merged 15 commits into
mainfrom
feat/config-intent-tool-layer

Conversation

@justinmerrell

Copy link
Copy Markdown
Contributor

Summary

  • .config/ gets a defined key. A concern names the files a tool reads (actions, markdown, yaml, …).
    A tool that reads every file is named for what it looks for (prose, security, spelling).
    CONF-14 checks that a subdirectory is a registered concern. CONF-15 checks that a known tool sits in its own
    concern. EC-0011 now says what .config/ is for: repository-wide quality configuration. A product's language
    toolchain configuration stays with the product. Vale moves from markdown/ to prose/.

  • Tool strictness floors (new EC-0044, CONF-16 – CONF-23). Each tool starts from its maintained preset:

    • yamllint extends default;
    • markdownlint keeps default;
    • pyright/basedpyright run at strict, recommended or all;
    • tsconfig sets strict;
    • Biome keeps recommended;
    • gitleaks keeps its default rules.

    Two review requirements add that warnings fail the gate, and that every departure from a preset carries its
    reason.

  • A tool-wrapper task layer (TASK-25 – TASK-28, HOOKS-15, TASK-20 relaxed).

    • .config/ holds the rules.
    • taskfiles/tools/<tool>.Taskfile.yml is the one internal place a binary, its --config and its severity
      flags appear.
    • Area fragments compose wrappers, and the root holds the verbs.
    • Hooks and CI run tasks, not binaries.
    • TASK-28 reports a sources glob that matches nothing. Such a task stays "up to date" forever.
  • This repository adopts all of it.

    • 18 wrappers.
    • The area fragments match their task names:
      • secrets, schemas, generate and prose split out;
      • checks:check → checks:compile;
      • invariants/classify → cli:*;
      • every fixer ends in :fix.
    • Deterministic gates are cached with go-task sources (checksum). The sources include the tool's config,
      mise.lock and the Taskfiles. A second task check with nothing changed takes about 20 s instead of about
      6 min. task --force check reruns everything.
  • A CI pass-marker pilot (decision 0040),
    off by default.

    • A Validate job whose inputs are byte-identical to a proven passing run of the same PR, or of main, can skip.
    • Only CI writes markers, and detect re-proves each one: key, ref, run, and that the workflow and action
      trees are unchanged.
    • Markers are never read on push, merge_group or forks.
    • It is gated by the repository variable CI_PASS_MARKERS, which merges unset, so CI behaves exactly as
      before.
  • CODEOWNERS selective gate (feat: a shared CODEOWNERS check, and drop this repo's * catch-all #118).

    • This repository owns only /.github/, /.repo/, /engineering-conventions/definitions/ and two .claude
      trust paths.
    • COMM-09 forbids a catch-all; a waiver is the allowlist.
    • COMM-10 reports a root-anchored path that doesn't exist.

Fixes #118 in part: the offline checks and this repository's file. Owner resolution and write access need the
GitHub API, so they stay on the issue.

Decisions 0038 (concerns and strictness),
0039 (tool wrappers and caching) and
0040 (pass markers) record the reasoning.

Consumer impact

  • Requirement IDs touched:
    • new: CONF-14 – CONF-23, TASK-25 – TASK-28, HOOKS-15, COMM-09, COMM-10, EC-0044
    • changed: TASK-20 (relaxed for tool wrappers), COMM-03, COMM-04 and COMM-08 prose
    • changed message: COPY's Vale path is now .config/prose/vale.ini
  • Change class: new requirements at warning → feat (a patch in 0.x). TASK-20 relaxed → fix.
  • Breaking? No. Everything is proposed at warning.
    • Expect new warnings for a .config/ bucket outside the vocabulary, a Vale config in markdown/, a *
      catch-all in CODEOWNERS, and a fragment task named outside its area.
    • The runner now preparses tsconfig.json, pyrightconfig.json, biome.json(c) and markdownlint JSON. A file
      jsonnet cannot parse gets a PARSE finding, as devcontainer.json already does.
  • since is 0.9.2, because release PR chore(release): release 0.9.2 #116 is still open. If chore(release): release 0.9.2 #116 merges first, these move to 0.9.3.

AI usage

  • This PR contains code generated by AI coding agents
  • I have manually reviewed and tested all AI-generated logic

Test plan

  • task --force check passes locally:
    • conventions:self reports "No findings." from both runners;
    • 1112 Rego tests pass;
    • 983 pytest tests pass, fixture repos included.
  • Each new conftest requirement ships a fixture repo that fires plus a near-miss, and the findings snapshot is
    updated.
  • Generated files were regenerated with task generate; generate:check is clean.
  • Caching:
    • a second task check skips unchanged gates;
    • editing one .md reruns only markdownlint, typos and Vale;
    • editing mise.lock invalidates every cached gate;
    • task tool:yamllint is refused as internal.
  • lefthook run pre-commit with staged files whose names need quoting.
  • ci-pass.sh:
    • the key mode tests pass;
    • prove passes 12 scenarios against a mock gh, including an edit-then-revert forgery, which it refuses.
  • Pass-marker trial (needs an admin): set CI_PASS_MARKERS=on and follow decision 0040's "Trying it" steps.

Follow-on issues

  • Grouped Dependabot titles exceed the 72-column subject limit #105 (Dependabot titles over 72 columns) is not included: the fix it proposes needs a pull_request_target
    job, which the zizmor gate flags.
  • After release: migration issues where the new warnings land:
    • platform's comments/, positioning/ and python/ buckets;
    • infra's flat .config/;
    • the SDK and CLI repositories.

BEGIN_COMMIT_OVERRIDE
feat(conventions): key .config/ concerns on the files a tool reads
feat(conventions): set a strictness floor for tool configuration
feat(conventions): add TASK-25 to TASK-28 and HOOKS-15 for tool wrappers
fix(checks): let a tool wrapper's include set internal under TASK-20
feat(checks): read the Taskfiles in taskfiles/tools/
feat(conventions): forbid a CODEOWNERS catch-all and dead paths
fix(conventions): name .config/prose/vale.ini in the COPY message
END_COMMIT_OVERRIDE

🤖 Generated with Claude Code

justinmerrell and others added 15 commits October 24, 2026 13:39
Replace the `*` catch-all with the selective gate musher-dev/platform
set for every repository: /.github/, /.repo/, the definitions every
consumer inherits, and the agent merge guard. Pin, lock and vendored
files stay unowned so bot pull requests merge on green CI.

Refs #118

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add pass markers to validate.yml behind the CI_PASS_MARKERS kill switch,
which merges unset. Each eligible job (repository_lint, checks, content,
conventions, bundle) ends with .github/actions/sync-pass-marker, which
saves a ci-pass-<key>-<run_id>-<attempt> cache entry on a same-repository
pull_request or a push to main. On a same-repository pull_request, detect
lists the markers on the PR ref and on main, re-proves each one against
the API with .github/scripts/ci-pass.sh, and outputs passed_jobs. An
eligible job skips when it is listed, and Validate / Required accepts
that skip only when detect succeeded and listed the job.

The key hashes the git object IDs of every path the job reads, from one
table in the script. actionlint declares the variable, and Dependabot
updates the new action's pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decision 0040 states the pass-marker design: the key, the record, the
five checks detect proves, the excluded jobs and why, the security
model (CI writes and reads, a local result never counts, the PR head is
the gate with no merge queue), the kill switch and how to trial it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A repository that keeps one wrapper Taskfile per tool under
taskfiles/tools/ had those files ignored: the launcher did not pass them
to conftest, the Taskfile library did not count them, and CONF-04 did not
take them as callers of a .config/ file. Each now reads that one extra
directory level, so a config named only by its tool's wrapper has a
caller, and a task call into a wrapper resolves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every tool the Taskfiles and git hooks run now has a wrapper in
taskfiles/tools/<tool>.Taskfile.yml, included flattened and internal as
tool:<tool>[:<detail>]: the only place its binary, config path, severity
flags and default file set are written. Area fragments name every public
task under their file (secrets:scan, schemas:*, generate*, prose:lint,
checks:compile, cli:invariants, cli:classify); content.Taskfile.yml is
split into schemas, generate and prose. `lint` now runs every linter and
format check, and `lint:fix` every fixer; a fixer ends in `:fix`, so
checks:fmt becomes checks:fmt:fix and checks:fmt:check stays the check.

Deterministic wrappers fingerprint their sources, so a second `task
check` skips unchanged gates (6m05s, then 19s here); `task --force check`
reruns them all, and CI starts from a fresh checkout. A run given files
goes to an uncached tool:<tool>:files task, so the hooks always run and
never record a fingerprint. The secret scan, online zizmor, Spectral,
invariants and the bundle stay uncached.

Lefthook jobs run tasks, passing staged files after `--`, which Task
hands on shell-quoted. The commit-msg job still runs committed directly,
since COMMIT-03 reads that command. CI, the rules, CONTRIBUTING and
AGENTS.md use the new task names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A repository that keeps one wrapper Taskfile per tool under
taskfiles/tools/ had those files ignored: the launcher did not pass them
to conftest, the Taskfile library did not count them, and CONF-04 did not
take them as callers of a .config/ file. Each now reads that one extra
directory level, so a config named only by its tool's wrapper has a
caller, and a task call into a wrapper resolves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Define a .config/ concern in EC-0011: it names the files its tool reads
(actions, commits, docker, json, markdown, openapi, opentofu, python,
rego, shell, sql, toml, typescript, yaml), or, for a tool that reads
every file, what it looks for (prose, security, spelling); mise keeps
mise/. State the boundary: .config/ is repository-wide quality
configuration, and a product's language toolchain stays with it.

- CONF-14: a .config/ subdirectory is a registered concern.
- CONF-15: a known tool's configuration sits in its tool's concern.

Move this repository's Vale config to .config/prose/vale.ini and point
the editor, docs, COPY examples and COPY-01's message at it. Index the
editor as a caller in .config/README.md, and drop two settings that
only restated or loosened the preset: typos' "musher" entry and
yamllint's comments overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add EC-0044 Tool strictness: a tool's configuration starts from the
tool's own preset and states every departure with a reason.

- CONF-16: yamllint extends default.
- CONF-17: markdownlint keeps its default rules on.
- CONF-18: pyright and basedpyright check at strict or above.
- CONF-19: a tsconfig.json turns strict on.
- CONF-20: Biome keeps its recommended rules on.
- CONF-21: a gitleaks [extend] keeps the default rules.
- CONF-22 (review): a tool's warnings fail the gate that runs it.
- CONF-23 (review): every departure from a preset has its reason.

The hadolint and zizmor floors stay in IMAGE-07 and GHA-33. The runner
now passes yamllint, markdownlint YAML, gitleaks and pyproject.toml to
conftest, and parses markdownlint JSON, tsconfig.json, Biome and
pyrightconfig.json with jsonnet first (decision 0015). Decision 0038
records the concern vocabulary and the floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add COMM-09 (no `*`, `**` or `/**` line with an owner; a waiver in
.repo/conventions.toml is the allowlist) and COMM-10 (a root-anchored
literal path names a file or directory that exists) to EC-0036, proposed
at warning since 0.9.2. Owner resolution and write access need GitHub's
API and are left to `gh api repos/{o}/{r}/codeowners/errors`.

Fixtures whose layout drops platform-api/ get a CODEOWNERS that matches
it, and COMM-08's example no longer shows a catch-all.

Refs #118

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/decisions/README.md
#	docs/repository.md
EC-0015 now describes four layers: rules in .config/<concern>/, one
internal wrapper per tool in taskfiles/tools/<tool>.Taskfile.yml, area
fragments in taskfiles/<area>.Taskfile.yml, and the verbs at the root.
It also says what a cached task lists as sources, and splits a wrapper
into a cached tool:<tool>:all and an uncached tool:<tool>:files run.

- TASK-25: a wrapper is included with flatten and internal, and its
  tasks are named tool:<tool> or tool:<tool>:<detail>.
- TASK-26: a wrapped tool's binary runs only in its wrapper.
- TASK-27: a flattened fragment's public tasks are named under its area.
- TASK-28: every sources glob matches something the repository holds.
- HOOKS-15: a lefthook job runs a task, not a wrapped tool; committed in
  commit-msg stays COMMIT-03's.
- TASK-20 (fix): no longer forbids internal on a tool wrapper's include;
  its internal message says so.
- Wrappers are fragments for TASK-18, TASK-19, TASK-21 and TASK-22.

Decision 0038 records why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/decisions/README.md
@justinmerrell
justinmerrell merged commit 4265dfe into main Oct 24, 2026
12 checks passed
@justinmerrell
justinmerrell deleted the feat/config-intent-tool-layer branch October 24, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: a shared CODEOWNERS check, and drop this repo's * catch-all

1 participant