AGENTS.md ยท diff

git:20261107.7bd306f to git:20261108.fe4cbb4

2 added, 0 removed. Audit A to A.

# AGENTS.md
Coding agents write the code and docs in this repository. Read this file before you change anything.
## Public repository: no sensitive details (READ FIRST)
- Everything committed here is public: files, code comments, commit messages, branch names, tags, pull request and issue text, review comments and release notes. Never add names of private repositories, tracker ids, local paths, customer names, account ids or internal planning notes. Describe the generic concept instead.
- Task tracking, research notes, plans and the measured review fixtures live in a private companion repository of the maintainers. If it is available in your session, follow its `AGENTS.md`. Never copy its content, names or ids here, and keep no issue-tracker state here.
- Never add that repository as a remote of a clone of this one, and never fetch from it or push to it.
- Changes land by pull request from maintainers only. Users open issues.
- A pull request title is a [Conventional Commit](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, ...); `pr-title-lint` checks it. The squash merge makes it the commit subject, and `automated-release` turns a `feat`, `fix`, `perf` or `revert` commit or a breaking change into the next version, a `CHANGELOG.md` section, a tag and a GitHub release. Never edit `CHANGELOG.md` or `version.json` by hand. `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json` make the repository a plugin of the [antonbabenko/agent-plugins](https://github.com/antonbabenko/agent-plugins) marketplace, which pins each release.
## What this repo is
modulestf reviews and maintains reusable Terraform modules. The first module family is terraform-aws-modules.
- `.agents/skills/`: the agent skills. `.claude/skills` is a symlink to it.
- `docs/`: design and decision records.
- `tests/`: the shell tests, listed below.
## Tests
The runner package test also needs ripgrep, and the record cap test needs Python 3 as well as Bash.
These tests make no network calls.
- `tests/plan-runner-packages-test.sh` checks that every direct Debian package
install has an explicit version, no extra unpinned install is added and apt's
authentication is retained. Negative mutations check each refusal. It does not
establish archive availability; both architecture builds and native smoke do.
- `tests/plan-runner-record-cap-test.sh` executes the production capture and
record assembly helpers with local Docker stubs: repeated and shared output,
remaining and exhausted capacity, command failure status, non-regular or
oversized destinations, and complete or oversized multi-example records.
The facts engine oracle and merge fixtures, and the checks that hold them to the facts
schemas, to this repository's `rule-facts.md` and to their own trees, are in the facts
repository's Go tests; its CI passes them the pinned `rule-facts.md` through
`MTF_FACTS_REGISTRY`. The layout is in [the oracle README](https://github.com/modulestf/facts/blob/master/testdata/oracle/README.md)
and [the merge README](https://github.com/modulestf/facts/blob/master/testdata/merge/README.md).
The suites below run with `bash` from a clean checkout, with no setup step and no network calls. They need the base toolchain: `bash`, `git`, `awk` and `grep`, plus `jq` for the plan pass, verify pass and render comment tests, `tar` and `gzip` for the terraform-docs pass test, `zsh` and `perl` for the plan pass and verify pass tests, and `perl` for the render comment test.
- `tests/ladder-pass-test.sh` runs the pull request review skill's `references/ladder-pass.sh` with `terraform`, `mtf-facts`, `curl` and the sandbox tool (`sandbox-exec` on macOS, `bwrap` on Linux) stubbed, and the canary replaced by lines the test sets: the pull request 556 case (a `compact()` subject wholly unknown at the base, a `concat()` subject of known length 1 at the head) and the record's shape, fixture-check before any plan, read-only fixtures, private raw plans, the lease and the sweep of dead runs; every fail-closed path (a canary leak, a missing class, an unfinished canary, a fixture-check violation or error, a plan error, a stand-in that plans known, a sensitive subject, a reference or type L1 cannot model, a category without a template); the runner taken from the script (exit status, timeout, output cap, the process group, closed descriptors); acquisition (a matching pin, the curl flags, a hash mismatch, a host outside the pin list, the cache mode, a schema that names other providers); and `probe`; the lease owner and the sweep, the budget clip, the manifest passed to `fixture-check --expect`, the classifier's cases as one var file and one observation per case id, a plan error in `subject.tf` as an error observation and any other as unverifiable, `terraform validate` before any plan, and a default change's omitted case; L3 with a stubbed aws provider (the provider tree moved out of scratch with its manifest and its CLI configuration, the pull request 556 replay with no change planned at the base and the attribute known after apply at the head, the control case and its baseline, fixture-check at L3 with the pins, the prior state copy and `-refresh=false`, the provider profile, and each refusal: no schema, a control that does not match, a schema type that differs, an added attribute, no all-known case, an attribute outside the targets, an uncurated type, L3 cases chosen whatever the number of L1 cases, a provider canary without the plugin socket or with a link that reaches a socket, a provider tree changed or holding a link); the runner's kill of a descendant that called setsid and of a process run from the provider tree; L2 with the stubbed `terraform test` (a for_each key change and a count change observed as instances at each revision, unknown keys and a null set element as error observations, fixture-check at L2 with the manifest of the .tf files, the one test file whose only run plans, verbose JSON per case and side, and each refusal: a validate error before any test, an error outside subject.tf, no plan, output that is not JSON, a stand-in that plans known, a fixture-check violation, an attribute other than count or for_each, over 32 cases, an unproven sandbox, and a template whose run applies, refused before any test, with the plan-only guard taken from the script on every refused shape); an added attribute with its null base (`base_absent`) run at L1 on both sides and controlled at L3 by that null, and a data source refused at L3 with its fixed reason, with or without a schema; a dynamic block not marked for_each_only, refused, one whose for_each is unchanged, refused, and one whose for_each alone changed, at L1; the aws_db_instance fixture at L3; and every entry of `ladder-fixtures/` against its schema extract, prior state, baseline and template. It needs `jq` and `perl`. When the real `terraform`, an `mtf-facts` with `fixture-check --expect` and the sandbox tool are installed, it also runs the pull request 556 case live, plus an expression that closes its block; the only connections are the canary's, inside the sandbox it proves. Otherwise the live case is skipped, and with `LADDER_LIVE_REQUIRED=1` a skip fails. With `LADDER_LIVE_CACHE` set to a provider cache directory, or with `LADDER_LIVE_REQUIRED=1`, it also acquires hashicorp/aws 6.70.0 from the pin file, the one download, outside the sandbox and hash-checked, and replays pull request 556 at L3 live: no change planned at the base, the attribute known after apply at the head, a valid control and no provider process left, and the same for the aws_db_instance fixture. The live run also takes an L2 for_each key change: base keys against head keys, no instance for an empty input, and the unknown keys error at both revisions; with an mtf-facts that has no `fixture-check --level L2` it is skipped. End to end, `mtf-facts behaviour` must select L1 and L2 for a for_each change of a small module, the ladder must run L2 on every case it requires, and `claims-eval` must list the L2 key change, unexplained. Live, `mtf-facts behaviour` must give an attribute a change adds its null base when the schema marks it a plain attribute, L1 must observe the null at the base in every case, and `claims-eval` must show no difference in the null case; at L3 live, the null base must keep the prior value with a valid control. The `Ladder live` workflow runs it that way on a Linux runner with bubblewrap, after `probe` reports `sandbox proven bwrap`.
- `tests/depth-test.sh` runs the pull request review skill's `references/depth.sh` against a stub `mtf-facts`: the binding (head, base, merge base, diff hash of `git diff --binary`), verify on a moved head or a changed hash or merge base, a failed diff, escalate, the owner raise, hostile file names and run directory names kept as data, the abort on two or no merge bases, `mtf-facts` missing, failing, late, refused or under the clone, a missing classifier or empty affected output, and that `depth.md` lists the reason tokens of `render-comment.jq`; `DEPTH_TEST_SHELL` picks the shell. The negative self-check mutates temp copies and confirms each one is caught. It needs `git`, `jq` and `perl`.
- `tests/review-run-test.sh` runs the pull request review skill's `references/review-run.sh` with `gh`, `terraform` and `mtf-facts` stubbed and a local bare repository: byte-for-byte handover and render from a synthetic fixture, the full command chain with hostile title, branch and file names, the closing issues' text (an issue item by origin, the 4000 byte cut and its entry, an issue the body names twice read once without GraphQL, a closed pull request's closing issue read, and a cross-repository reference, a third issue or a failed read adding no item), the module profile (the snapshot's tree and gate list in the task, an unknown id or a missing snapshot refused, and a reviewer profile line that differs from the handover or is missing refused before any render), light, focused and escalated runs rendered end to end, the depth escalations and the refusals that stop a run (an escalation after the handover, an edited depth file, focused without fact sheets, a failed escalation), the schema source lines, the host-pass defects, and the pinned sentence multiset of the moved workflow text.
- `tests/reviewer-index-test.sh` checks the reviewer's run summaries under `references/run/`: every `rule_id` a check owns has one row with a condition, the provider facts of `rule-facts.md` and section links that resolve and name it; each summary's Read First links its check file's first section; every level-2 section of the check and engine files is linked; `references/workflow.md` links each summary. It checks that Reading by Depth makes a row hold when the `mtf-facts` report gives its rule a finding or candidate, that `facts-engine.md` pairs `row not applied` only with clean or not_applicable, and that the Budget and `run/facts.md` name the fact sheet layout (`same_as`, `versions:` and `missing.json`). Headings aside, SKILL.md with `inputs.md`, `output.md` and `workflow.md`, and each check and engine file, keep the sentence multiset of the base commit `tests/reviewer-index-sentences.txt` names, changed only by the sentences it lists. It checks that `references/section.sh` prints exactly each heading's section, prints every anchored link from the summaries, check files and engine files, sibling skills included, and refuses paths outside the skills directory, symbolic links, non-markdown files, bad anchors and shared slugs. A negative self-check on mutated temp copies proves each of these checks catches breakage.
- `tests/schema-pass-test.sh` runs the pull request review skill's `references/schema-pass.sh` and `schema-facts.jq` under `/bin/bash` with the system tools only and curl stubbed: the depth modes' tokens from depth.json, the versions per type (latest, declared minimum, head lock, and the old floor at deep) deduplicated, one sheet for versions with the same body, the focused caps (16 sheets, 49152 bytes) and overflow, the trusted mirror and the source lines, each escalate reason, deep's partial results and `missing.json`, normal mode unchanged, and a negative self-check on mutated copies. It needs `jq`.
- `tests/behaviour-pass-test.sh` runs the pull request review skill's `references/behaviour-pass.sh` with `mtf-facts` stubbed: the classify and eval steps, the empty ladder record it hands `claims-eval` when no ladder ran, a ladder record and a claims file passed through, the head and base SHAs passed to `claims-eval` and its `capped` count held to the classifier's, the record lines with each `change` line, its `explained_by` claim ids and no input, and the run file object, an inert change and capped triggers, the module root prefix stripped from the files list and a rename covered at both paths, and each fail-closed refusal with its fixed reason: no tool, a tool that fails or passes its deadline, output over 1 MiB, not JSON or outside its contract, a trigger path with a space or a colon, a required case set that is not up to 32 cases with distinct ids, a path of the files list with no entry, a ladder record for another schema, head or base, and a claims file over its caps; then the usage errors, a unified diff among them. `complete`, `outstanding` and `evidence_digest` cross from `claims-eval` to the run object as they are and to the trigger's record line, and each refusal of their grammar is `output refused`; `also_categories` crosses from the classifier the same way. A trigger with a null side (`base_absent`, `head_absent`) classifies and evaluates, and the flags cross to no run trigger and no record line. It ends with the replay of a pull request that drops `compact()` around a list, one wrapper change and one unknown propagation candidate with no ladder run, rendered by `render-comment.sh`: inconclusive at ladder rule 4, never approve with nits, with every unverified trigger and claim in section 5 and the evidence line. It needs `jq`.
- `tests/plan-pass-test.sh` runs the pull request review skill's `references/plan-pass.sh` under bash and under zsh, with `terraform` and `aws` stubbed: the opt-in and tool notes, the review role session, its write probe and the session policy, the gates, the laptop relaxations of G3 (the AWS managed policy ARN, the local-only data sources, the event bus read through the module calls of the example root with the null and default rules of Terraform, the provider skip flags and comment lines, each refused shape, the file ban turning each off, and a negative self-check on mutated copies), the record lines, the evidence redaction, the merge base re-run and the cleanup, no cap on the number of examples, the saved plan read back with `terraform show -json` and no credentials into an addresses file of configuration addresses, attribute names, sensitive attribute names and the head commit, runner mode (refused outside a container, the budget and phase arguments it alone takes, and the exceptions it records, such as the Docker provider, a fixed role and `build_in_docker = true`), and the plan runner's `hosted-record.sh` on fixture record directories: a record reduced to classes, `Plan:` counts, cleaned addresses and a refused host count, with no host name, kept line or file name in the output, and `host-pass.sh`'s rules for that file taken from the script, which refuse an address or attribute outside its pattern, and each refusal: a class or base result outside the list, a block with no plan line, a duplicate or unsafe path, an example under a module root prefix, lines out of order, no summary, a record not in runner mode, a symbolic link, an input or output over its size cap, and an `--out` that exists or whose directory does not. It also checks the byte caps of the untrusted block in `host-records.jq`: the body cut to 8000 bytes, the title and a thread comment to 4000, 16000 in total, a closing issue to 4000 outside that total, and conversation comments not cut; an issue item that starts with its number and title, takes its origin by the author id and adds no blank line for an empty body; the `prose-truncated` entries of `host.json`, one per cut item class with its bytes, and `valid_host`'s refusal of zero or negative bytes, an unknown item, an item without bytes or two entries for one item.
- `tests/render-comment-test.sh` runs the pull request review skill's `references/render-comment.sh` and `render-comment.jq`, the host render of a review from merged findings with no model: the worked examples of `comment-format.md` byte for byte (the worked example, the Fix lines, and pull request 410 grouped and collapsed, with the Fix lines, Fix blocks and What to do section those two predate removed), the What to do section of every ladder rule, the action line in its place at rule 4 when only Optional items and the gaps line would remain, and the four facts the ladder makes hold for it, and the fixed item of a group at the module root `.`, the Fix block of a group of three or more entries and when a group stays unfolded, the verdict and proposed event of every ladder rule and the orderings and clamps `verdict.md` calls out, the disputed note, each collapse rule with a collapsed block and its Fix lines byte for byte, every fixed line of sections 5 and 6 in one whole body, the lines of sections 7 and 8 from `host.json`, the checks file and the run file, the plan coverage of the run file: an unplanned example is a rule 4 unknown listed after the verification gaps, golden, an inert change lists them as information and counts none as a gap, and a run file without coverage or with a class that does not match its list is refused, the module root prefix, that the script's fixed strings equal the Host Render Strings table of `comment-format.md`, that the findings schema and the handover state the length targets word for word, the leak scan, the at-mention step, and each refused input: findings outside the merged findings schema, a host file for another head, a bad run file, an assessment with a line that starts like a heading, bold line, HTML or list item or with more than two paragraphs, and an output directory that is missing, holds a file or lies in the run directory; null counts render the one line `Counts: not reported by the review run.` and zero counts do not, a run file's `profile` renders the Profile section, or an evidence note in COMPACT, and a malformed one is refused, and a run file with a count missing or empty counts is refused; more than 100 verification gaps all count for the verdict, while section 5 lists the first 100 and one line with the count of the rest. Behaviour evidence in the run file: verified, covered and inert pass, with no evidence line when every trigger and claim passed and the evidence line under the rationale otherwise, each unverified trigger or claim, the capped count and a step that did not run are rule 4 unknowns listed in section 5 byte for byte, a `review.verification-incomplete` finding is a rule 4 unknown with its own rationale, a `claim.*` finding blocks, a run file with no behaviour object is a rule 4 unknown outside a triage run, a refuted state blocks at rule 2 by itself, with its own rationale and checklist item when no `claim.*` finding words it, and disarms rule 0, while a CRITICAL finding keeps rule 1 and its rationale with the refuted item added, byte for byte, a `claim.*` finding with no refuted state is refused, and a behaviour object outside its grammar is refused. The not-planned examples of section 5 are one clause per reason with the names under it, golden. The tiers of `comment-format.md`: pull request 556 rendered COMPACT byte for byte, with `--standard` forcing STANDARD on the same inputs; the reported state, covered by one finding or two jointly or by a superset, left unverifiable by a stale digest, a partial union, a finding mixing two triggers, one at another line or a trigger not complete, with a complete empty set verified, a finding with only out-of-set ids ignored, a refuted claim still at rule 2 and a finding below LOW refused; each condition that forces STANDARD (a MEDIUM finding, an unverifiable trigger, a classifier that did not run, a version floor change, four findings, a group of three, a disputed note, a cause outside the table, an unresolved thread, no example counts, a title, thread or closing issue cut, a version floor among `also_categories`, a gate hit after a runner-mode pass or with no plan mode); a routed pre-existing LOW, uncounted, and a model finding that claims it refused; the G0 clause apart from the runner clause; coverage none and the approve verdict lines byte for byte; the name allowlist, sorting and `name withheld`; the caps of each tier refused by name, a `needs decision:` fix up to 600 characters in COMPACT, every offender in one message with its length and its limit; and the host.json cut entries. The negative self-check mutates temp copies of the script and confirms each weakened copy is caught, the tier rules included. It needs `jq`.
- `tests/reviewer-git-safety-test.sh` checks the reviewer's git safety form in `references/git-safety.md`, taking the functions and the table of queries from that file, and that every `diff`, `show` and `log` row carries both flags: for each setting a hostile workspace can put in `.git/config` or `.git/info/attributes` (external diff, diff driver command, textconv, smudge filter, fsmonitor, signature program, pager, lazy fetch over ssh and `ext::`), it shows the plain command runs a marker program and every query of the form's table runs none. It also shows that replace refs and grafts change what the plain queries read and not what the form reads, that `safe_git_refuse` refuses each partial clone key alone and a shallow clone but not a full clone, that `safe_git_partial` stops the head revision query for a partial clone but not for a shallow or full one, that `-c protocol.allow=never` alone does not stop an `ext::` lazy fetch, that `git status` still runs a clean filter in the form, and that a form without the lazy fetch, pager or replace overrides, or a refusal without the shallow test, is caught. It uses throwaway repositories only; the pager case needs `script(1)` and the lazy fetch case git 2.44 or newer, and each skips with a note without them.
- `tests/skills-test.sh` checks every markdown file and template git knows about, tracked or newly added but not yet committed, and the shell scripts under `.agents/skills/`, which get only the ASCII check: every relative link resolves to a path in the index, every anchor matches a real heading, no two headings in a file produce the same slug, headings are ATX, and the text is plain ASCII except where an exception records the character. A gitignored file is out of scope, and a link to one fails, because the forge cannot see it either. It also checks each shipped profile's `review-guide.md`, where it has one, against that profile: every level-2 profile section is cited or listed as Not covered, each cited or Not covered section still matches the hash the guide records, and every ID the guide lists exists in the reviewer skill. It checks, for each shipped profile under the maintainer's `profiles/`, that every profile constants block in its `PROFILE.md` parses, that the blocks and their keys are format 1 of `references/profile-format.md`, that only the keys that may be empty are, that the `profile-options` values are in their closed sets and `examples: none` holds no example entry, that the first heading names the directory, and that each value equals the rule text it restates, with a negative self-check per profile that reports each mutation under the block it changed. It checks that every rule id the Gate table and Downgrade sections of `profile-format.md` name is a row of `rule-facts.md`. When an `mtf-facts` with `profile-check` is installed (`MTF_FACTS` names another binary; `profile-check --help` detects it), every shipped profile must pass it, its `--gates` rows must equal the Gate table row by row (option, value and rules), and its `--downgrade-ids` must equal the Downgrade list; otherwise that parity is skipped with a note, and with `MTF_FACTS_REQUIRED=1` the skip fails. No workflow installs `mtf-facts` for this test yet: once a release with `profile-check` is pinned, the `Test` workflow should install it and set `MTF_FACTS_REQUIRED=1`. It also checks that the reviewer's `references/rule-facts.md` has one row for every `rule_id` the skill names and no other row, that each row's carry value is `file` or `never`, that every `file` rule, and no other, has one line in its carry audit, and that the registry columns hold allowed values: engine, class, scope, cross_area, area_kinds, bundle_sections and severities, a confirm class only on a candidate rule, never small on a rule that can be CRITICAL, and every candidate at lead until its evaluation, each with a mutation that must be caught. It also checks the reviewer's `references/engine/` files against that table: every `tool` and `candidate` rule has exactly one level-2 section, every level-2 section is such a rule, no `llm` rule has one, each section's `Engine` line matches its row and it holds its own Rule text, Confirm when and Host templates headings once each, and its `summary` and `suggested_fix` templates sit under Host templates, outside fenced code, and are one code span each, hold `{subject}` and are at most 200 characters; a negative self-check mutates temp copies and confirms each of these fails. It also runs the reviewer's `references/judgment/assemble.sh` twice and checks that the runs agree, equal the committed `prefix.md`, which is at most 90 KB (92160 bytes), and change no file of the skill, with a negative self-check on temp copies for each of these.
- `tests/skip-coverage-test.sh` runs the pull request review skill's `references/skip-coverage.sh` under bash and zsh on a synthetic module in a git repository: the allow-list of changes and each refused change, the base and head gate records, hits in downloaded modules, the witnesses and each reason, refused inputs, and a negative self-check on mutated temp copies.
- `tests/terraform-docs-pass-test.sh` runs the maintainer skill's `references/terraform-docs-pass.sh` with `curl` and terraform-docs stubbed: the profile constants it reads, a regenerated root and submodule in one run with the text outside the region and the file mode kept, the curl flags, a corrupt cache entry installed again once, and one negative fixture per check, each ending stale with the workspace unchanged: a wrapper or other entry, a path outside the shape or through a symbolic link, the eleventh directory, a terraform-docs configuration name in the directory or above it as a file, a dangling link or a fifo, an OpenTofu, JSON or override file, a symbolic link, hard link or fifo among the inputs, a name starting with a dash, a file over 1 MiB, 201 files, no `README.md`, each marker count failure including one in a code fence and the older pair, a hash mismatch, over 8 MiB, a time-out and the overall deadline, a failing tool, a file the run creates or deletes, a `README.md` the run turns into a directory, an empty region, marker text the run puts in the region, text before or after the region the run changes, and a workspace changed or added to during the run; an unreadable profile, a platform with no hash, a profile, download program or cache directory inside the workspace, a cache directory that is a link or has the wrong mode, and a second hash failure that ends the run's use of the tool; a run of 51 files that finishes fast, clean up on INT and TERM, a file not owned by the user when run as root; and that no run directory is left.
- `tests/verify-pass-test.sh` runs the pull request review skill's `references/verify-pass.sh` under bash and under zsh, with `terraform` and `mtf-facts` stubbed: each record, the version bump skip, a symbolic link example, the merge base re-run, an example the change adds, a merge base that cannot be fetched, a missing terraform, the environment it runs terraform in, the SHA inputs, the merge of a run outside the sandbox, the check of a host's results, the example selection (touched and caller first, an inert change that keeps every example, the tool's own fallback, an absent, failing or invalid tool and one inside the workspace, each selecting every example), the budget and the output cap with the stopped process group gone, a merge base fetch the budget stops, and the plan coverage from no plan, a laptop record, a runner record whose forged lines claim nothing, a hosted record, an inert change, and a module with no example directory.
+ - `tests/wip-skills-test.sh` checks every work in progress skill: a `SKILL.md` under `.agents/skills/` whose description says "Work in progress" or whose body holds the head of the Status template. Its description must open, unquoted, with "Work in progress, not usable yet.", its name is its directory, its body opens with one title and then the Status section, which is the fixed refusal paragraph with only the skill name slot varying, and the slot names an existing skill directory that is not itself work in progress. Its other headings are level-2 sections at column 0 from the allow-list (Use cases, Responsibilities, Non-goals, Open questions) in that order, an indented heading fails, no line is an HTML heading tag, and it holds no backtick and no fenced code. At least one such skill must exist. A negative self-check mutates temp copies and confirms each check catches its breakage.
## Skills
Each skill is a directory under `.agents/skills/<name>/`, where Codex and Antigravity load it. `.claude/skills` is a relative symlink to `../.agents/skills` so Claude Code loads the same files. Hooks and settings stay in `.claude/`: each tool has its own hook format.
- `SKILL.md`: entry point. Keep it short: triggers, modes, mandatory rules, workflow.
- `references/`: provider-neutral guidance that `SKILL.md` links to and loads on demand.
- `profiles/<profile>/`: conventions of one module family (templates, workflows, release config). Only in `terraform-module-maintainer`. Default profile: `terraform-aws-modules`; the other shipped profile is `in-repo-modules`.
Rules for editing skills:
- Keep `references/` provider-neutral. Anything specific to one module family goes into its profile.
- The profile format is the maintainer's `references/profile-format.md`. A profile is self-contained: no link to another profile, no symbolic link. A change to the format is a breaking change (`feat!`).
- Do not duplicate general Terraform guidance from `antonbabenko/terraform-skill`. Link to it instead.
- Provider schema facts (arguments, nested blocks, versions) come from the Terraform MCP tools, not from memory.
- Templates are copied into modules as-is. Keep them valid and consistent with the profile's `templates-map.md`.
- The reviewer links to the maintainer's `references/` and `profiles/` with relative paths. Do not copy them.
- The maintainer may link to the reviewer's `references/findings-schema.md` the same way, so the two skills agree on the shape of a finding. Both directions are relative paths between sibling skill directories, so the pair travels together: installing one without the other leaves a dead link.
- Skills must not assume GitHub, PRs, or comments. They work on a workspace and a task. That is the default, and it is required of every skill that makes a judgement. The one exception is a skill whose stated job is to adapt a single surface to the neutral ones, such as reviewing a GitHub pull request. Such a skill may know about its surface, but it keeps the host concerns to itself and hands a neutral skill only a workspace and a task. Finding a surface convenient is not the exception; adapting it is. Such a skill produces no finding of
its own. It may compute a decision from a neutral skill's findings plus surface state, provided the
computation is a documented total function of those inputs, stated in the skill, adding no judgement
of the code. Anything beyond that is a judging skill, however its job is stated.
- Add new detail to `references/` or a profile, not to `SKILL.md`.
+ - A skill whose description opens with "Work in progress, not usable yet." is a refusing skeleton: it does nothing and names an existing skill to use instead. Its body is a fixed Status paragraph and prose sections only, and `tests/wip-skills-test.sh` checks that shape. The layering of the skills, shipped and planned, is in [docs/skills-architecture.md](docs/skills-architecture.md).
## Authoring and review
Content here is written by a team of agents with cross review. This is how the skills in this repo were written, and it caught defects, so it is the process, not a suggestion.
- One agent takes one issue and writes to its acceptance criteria.
- A different agent, which did not write the change, reviews it against those criteria one by one and
returns a verdict: approve, or request changes with findings in the shape the reviewer skill's
`references/findings-schema.md` defines.
- Findings go back to the original author to fix, and the same reviewer re-checks the ones it raised.
The reviewer reviews; it does not edit.
- Mechanical checks are part of the review and are run, not asserted: every relative link resolves, every in-document anchor matches a real heading, no two headings in a file produce the same slug, cited tool names and parameters exist, and added lines are plain ASCII (the rules in [Writing style](#writing-style)). `bash tests/skills-test.sh` runs all of these but the tool names, which need the tool list and stay a manual step.
- A review of a skill also checks it against the rules in [Skills](#skills). A review of any text also runs the sensitivity scan in [Public repository](#public-repository-no-sensitive-details-read-first).
- Work that touches the same file is serialized. Only work on disjoint files runs in parallel.
- On a hard decision, such as a rule's severity or scope, a report or comment format, or a change to the findings schema, ask several independent models for a consensus review before you choose. Give them the options and the constraints, weigh their objections one by one, and record why you dismissed any. The owner does not always have the best idea, and a consensus review is how a weak one gets caught before it ships.
Why the second agent is worth its cost: a verification step listed existing tests as an unconditional check with the trust qualifier attached only to adding tests, and `terraform test` runs `apply` by default, so an untrusted workspace would have reached real credentials. The author did not see it; a reviewer reading the acceptance criteria did.
Content rules the review enforces:
- Write names out in full. `terraform-aws-modules` and `compliance.tf` are the two that come up. No internal shorthand and no initialism for either, anywhere a reader outside the project could see it, which per [Public repository](#public-repository-no-sensitive-details-read-first) is essentially everywhere: files, commit messages, branch names, tracker issues, and pull request text.
- Never write a bare at-mention of the assistant. Write it inside a code span if it has to appear at
all, the way this file already writes `@modulestf`. On an issue, a pull request, a comment or a commit
message a bare mention notifies the account or fires a trigger phrase. In a file it does not, but
file text gets quoted into those surfaces, so keep it in a code span everywhere. In prose, name the
assistant without the at-sign.
- No vendor or model attribution anywhere. The rule is in [Git](#git).
## Writing style
- Plain ASCII typography. No em or en dashes, no curly quotes, no ellipsis character, no
arrows, no box-drawing characters. Write ` - ` for a dash, `->` for an arrow, and draw a
directory tree with `|--`, `` `-- `` and `|`.
- The check is `LC_ALL=C grep -rnP '[^\x20-\x7e\t]' <path>`. It should return nothing. Do not write
the class as `'[^ -~\t]'`: in a POSIX bracket expression that `\t` is a backslash and a `t`, both
already in range, so the check flags every tab in any file that uses them. A file that
has to keep a character records why, next to the file it applies to: today the only one is
the lock workflow template, which is upstream text that generated modules must match byte
for byte.
- The record is a line beginning with the words `Typography exception` and a colon, then the
file in backticks and each kept codepoint written as `U+23F3`. `tests/skills-test.sh` reads
it, so the name must match exactly one file in the directory holding the record or below it,
and a codepoint the record does not name still fails, even in that same file.
- Short, direct sentences.
## Git
- Do not commit `.claude/settings.local.json` or `.history/` (gitignored).
- No vendor or model attribution in anything pushed to this repository. No `Co-authored-by` trailer
naming an assistant, no "Generated with" line, no model name or identifier, no session URL.
The carriers, in the order they are easiest to miss: the commit author and committer identity,
branch names, commit
messages, pull request titles and bodies, tracker issue text, review comments, and code comments.
Naming a tool, action, SDK or directory the project depends on is not attribution
(`anthropics/claude-code-action`, `.claude/`); attribution is a claim about who produced the change.
- Before committing, set `user.name` and `user.email` to a person. The author field is the most
durable attribution a repository has, and no trailer is needed to carry it.
- Attribution already pushed is history. Flag it and ask before any rewrite, as
[Public repository](#public-repository-no-sensitive-details-read-first) requires.