Plain Healthcheck
Always use the skill
to retrieve the ***plain syntax rules — but only if you haven't done so yet.
When to run
Run this skill **whenever anything in the *plain project is finalized and the project is about to be left in a state the user (or another skill) might render from. That includes, but is not limited to:
- End of (Phase 4) — before presenting the render command.
- End of (Phase 3 final review) — before declaring the feature done.
- End of — after applying a fix, before telling the user to re-render.
- After finalizing any single edit that changes the renderable surface — e.g. after , , ,
add-implementation-requirement
, , , , , , , , , , , or any of the implement-*-testing-script
skills.
- After hand-editing a file, a , or anything under .
- On demand — whenever the user asks whether the project is in a renderable state.
The healthcheck is not a forge-plain-only step. Treat it as the default closing move for any workflow that finalizes something in the project.
Do
not skip this skill because "the dry-run passed earlier" —
s, scripts, and specs can all drift between runs. The healthcheck is cheap; rendering against stale specs is expensive.
Workflow
The skill is a detect → fix → re-run loop. It does not stop at the first failure; it surfaces everything wrong, fixes what it can, and only returns when either everything passes or a gap genuinely requires user input.
Step 1 — Inventory the project
- List every file in the repo root (and any subdirectories that contain files). Build the module graph from each file's YAML frontmatter (, ).
- Identify top modules — every module that is not -ed by any other module. A single-stack project has one top module; a multi-part project (e.g. backend + frontend) has one top module per part.
- List every in the repo (root and per-part directories such as , ).
- List every script under .
- Pair each top module with the that governs it. The pairing rule is: the config file in the same directory as the top module wins; failing that, the repo-root . A multi-part project must have one config per part — record any top module that has no governing config as a failure.
Print a one-line inventory summary so the rest of the run is easy to follow, e.g.
Top modules: backend/api.plain (config: backend/config.yaml), frontend/web.plain (config: frontend/config.yaml). Scripts in test_scripts/: 4.
Step 2 — Validate every
For each
in the inventory, check
all of the following. Collect every failure — do
not stop at the first.
- File parses. It is valid YAML.
- At minimum is present. Every project gets a unit-test runner.
- For every script field that is present (, ,
prepare-environment-script
):
- The path is a string ending in (macOS/Linux) or (Windows). The extension must match the rest of the project — do not mix and in a single config.
- The referenced file actually exists on disk under .
- On Unix, the script has the executable bit set (). If not, that is a fixable failure.
- No mixed stacks per config. Every script referenced from a single must target the same language/stack. For example, should not reference . If a config crosses stacks, that is a failure — the project should have been split into multiple configs per the rule in .
- No dangling fields. Any field whose target file does not exist is a failure.
prepare-environment-script
implies . A prepare-environment-script
only makes sense in service of conformance tests — the environment is what those tests run against. If a declares prepare-environment-script
but does not declare , that is a failure. Surface it to the user and offer to either (a) invoke implement-conformance-testing-script
to add the missing script, or (b) remove the prepare-environment-script
field if it was added in error. Do not auto-pick.
- No orphan scripts. Every script under should be referenced by some . If a script is never referenced, surface it as a warning (not a hard failure — the user may be in the middle of authoring).
For each failure, record the offending config path, the offending field, and the concrete problem (
,
,
, etc.).
Auto-fixes you may apply
- Missing executable bit on a script that otherwise looks fine → .
- Stale path that points at a renamed script that clearly exists under a different name in → only if there is exactly one obvious candidate (same language tag, same script kind). When in doubt, leave it for the user.
Anything else (missing script, mixed stacks, missing
) must be surfaced to the user — do not silently regenerate scripts here. Re-invoking
implement-unit-testing-script
,
implement-conformance-testing-script
, or
implement-prepare-environment-script
from inside the healthcheck is allowed
only if the user explicitly approves it after being shown the gap.
Step 3 — Dry-run every top module
For each
(top_module, config.yaml)
pair from Step 1, run a dry-run from the project root via the
tool:
bash
codeplain <top_module>.plain --dry-run
Match the dry-run to how the user will actually render. Pass the flags the user would pass for the real render so what you validate is what they will run:
- — required whenever the governing config file is not the default , or when the project has multiple s and the dry-run is being launched from somewhere that isn't the part's directory. takes a file name, not a path; if needed, into the part's directory before running so the right is found.
- — only when templates live outside and is not already set in the relevant config.
- — useful when an / chain is suspect.
- / — strongly recommended on a failed dry-run; the extra log output usually pinpoints the offending file and spec.
Treat the dry-run as a hard gate: the healthcheck only passes when every top module's dry-run exits successfully.
When a dry-run fails
Iterate until it passes:
- Read the error output. If the first run was not verbose, immediately re-run with . Identify the offending file, the line (if reported), and the kind of issue: missing concept, syntax error, cyclic definition, complexity violation (
Functional spec too complex!
), conflicting reqs, missing template, broken /, missing config field, etc.
- Fix only the files (or the relevant / template) using the appropriate edit skill — , , ,
add-implementation-requirement
, , , , or an inline edit. Never modify generated code under or .
- If you are uncertain about ***plain syntax for the failing construct, re-load before fixing.
- Re-run the same
codeplain <top_module>.plain --dry-run …
command with the same flags. Repeat until it exits successfully.
If the failure is something the healthcheck cannot reasonably fix on its own (e.g. the user has to choose between two contradictory specs and neither side was pre-approved, or a missing concept whose semantics aren't clear), stop and surface it to the user with the offending snippet and a concrete question. Do not invent behavior.
Environment failures
If
is not on PATH, or
is not set:
- Do not pretend the dry-run passed.
- Tell the user exactly what's missing and how to fix it (install the CLI, export the env var) and stop the healthcheck with a clearly-marked environment failure. This is the only kind of failure that may legitimately remain unresolved at the end of the skill.
Step 4 — Report
Emit one of:
- — followed by a short summary of what was checked:
N config.yaml(s) validated
, , . The caller (, , ) can then continue to its hand-off step.
- — followed by a numbered list of every unresolved problem. Each entry must include: the file (config / / script) it applies to, the concrete issue, and what the user needs to decide if the healthcheck couldn't resolve it.
The verdict goes on the first line so callers can pattern-match without parsing the whole report.
What this skill does NOT do
- It does not author new specs from scratch — use / for that.
- It does not run the real render — only .
- It does not execute the testing scripts (, , ). It only verifies that the scripts are wired up correctly via . The user runs the scripts themselves.
- It does not silently regenerate config files or scripts. The most it does is and (with user approval) re-invoke the relevant
implement-*-testing-script
skill.
Validation Checklist