diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7892830..29c43ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,3 +31,44 @@ jobs: cache: npm - run: npm ci - run: npm test + + # `install` sets up uv and plain-parser (bin/python-tools.mjs). Run it for real + # on each OS, then check that the installed plain-parser validates a spec. + install-plain-parser: + name: Install plain-parser (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + defaults: + run: + shell: bash + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + - name: plain-forge install + working-directory: ${{ runner.temp }} + run: | + mkdir project && cd project + node "$GITHUB_WORKSPACE/bin/cli.mjs" install --agent claude --scope project | tee install.log + grep -E "plain-parser: (already installed|installed)" install.log + # uv installs tools into ~/.local/bin, which is not on the runner's PATH. + # On Windows, GITHUB_PATH needs the Windows form of Git Bash's $HOME. + - run: | + dir="$HOME/.local/bin" + if command -v cygpath >/dev/null; then dir=$(cygpath -w "$dir"); fi + echo "$dir" >> "$GITHUB_PATH" + - name: plain-parser check + working-directory: ${{ runner.temp }}/project + run: | + printf '%s\n' '***implementation reqs***' '' '- :Implementation: should be in Python.' '' \ + '***functional specs***' '' '- :Implementation: prints "hello".' > app.plain + plain-parser check app.plain + sed 's/:Implementation: prints/:Undefined: prints/' app.plain > bad.plain + if plain-parser check bad.plain; then echo "bad.plain should fail"; exit 1; fi diff --git a/.gitignore b/.gitignore index 26f059c..990283d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ conformance_tests .env.development.local .env.test.local .env.production.local -codeplain.log *.tgz # local release credentials (slack webhook) diff --git a/AGENTS.md b/AGENTS.md index 0cc093c..d1aa2a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co So a change here is almost always one of: (a) editing the installer CLI, or (b) editing the instructional content under `forge/skills/` and `forge/rules/`. These need different mindsets — see "Editing `forge/` content" below. -`plain-forge` only *authors* `.plain` specs. Rendering specs into code is done by a **separate** tool, the `codeplain` CLI (codeplain.ai), which this repo does not contain. +`plain-forge` only *authors* `.plain` specs. Rendering specs into code is done by a **separate** renderer, which this repo does not contain. ## Commands @@ -28,14 +28,14 @@ node --test test/cli.test.mjs # run a single test f ## The installer CLI (`bin/cli.mjs`) -A single self-contained ESM file with **zero runtime dependencies**; it exports its internals so the test suite can import them without running `main()` (guarded by `isInvokedDirectly()`, which realpath-compares `argv[1]` to `__filename` — needed because the global bin is a symlink). Key model: +An ESM file with **zero runtime dependencies**, plus `bin/python-tools.mjs` (see below); it exports its internals so the test suite can import them without running `main()` (guarded by `isInvokedDirectly()`, which realpath-compares `argv[1]` to `__filename` — needed because the global bin is a symlink). Key model: - `AGENTS` maps agent name → content dir: `claude→.claude`, `codex|copilot|universal→.agents`, `forgecode→.forge`, and `opencode→.opencode`. `SCOPES`: `project` (cwd) / `global` (`$HOME`). Global ForgeCode and OpenCode paths have explicit exceptions in `resolveBaseDir`. `CONTENT_DIRS = [skills, rules, docs]` (missing source dirs are silently skipped). -- **install** writes `forge/{skills,rules,docs}` into `/`, recording every written file in `/.plain-forge/manifest.json`. It **refuses** (exit 1) if a manifest or a "forge signature" already exists — install never overwrites in place; you use `update` for that. +- **install** writes `forge/{skills,rules,docs}` into `/`, recording every written file in `/.plain-forge/manifest.json`. It **refuses** (exit 1) if a manifest or a "forge signature" already exists — install never overwrites in place; you use `update` for that. It then runs `ensurePlainParser()` from `bin/python-tools.mjs`, which installs plain-parser with `uv tool install`, installing uv first with astral's `install.sh` (Linux/macOS) or `install.ps1` (Windows). It never throws: on failure it prints the manual commands and the install still succeeds. `--skip-plain-parser` skips it, and the integration tests always pass it. The file is temporary and goes away once plain-parser ships on npm. - **update** auto-detects every install across both scopes × all agents, re-copies the fresh tree, and **prunes** files that were in the old manifest but no longer ship (confirmed individually unless `--yes`). Only manifest-recorded files are ever prune candidates, so the user's own/third-party files are never touched. - **uninstall** deletes exactly `manifest.files` then the manifest; refuses (exit 1) on a manifest-less install rather than guessing which files are its own. - Legacy (manifest-less) installs are recognized only when **all** of `FORGE_SIGNATURE_SKILLS` (`forge-plain`, `add-feature`, `debug-specs`, `load-plain-reference`) are present, then refreshed and given a manifest going forward. @@ -74,8 +74,8 @@ Other constraints the rules enforce (see `forge/rules/`): functional specs are m ### The skill lifecycle (orchestration) -`forge-plain` is the top-level orchestrator: a short Phase 0 intent interview followed by four gated phases — (1) definitions + functional specs, (2) implementation reqs / tech stack, (3) testing (unit→impl reqs, conformance→test reqs, generate `test_scripts/`, build `config.yaml`, probe host via `check-plain-env`), (4) validate via `plain-healthcheck` (`codeplain … --dry-run` gate) then hand off the render command. Phases 1–3 are **one-question-at-a-time, write-to-disk-immediately**. `add-feature` is the same authoring loop scoped to one feature on an existing project; `init-plain-project` is a no-interview scaffold; `run-codeplain` supervises a live `codeplain --headless` render. The installer ships `forge/rules/*.md` beside the skills; native rule consumers load them directly, while other agents reach them through `load-plain-reference`. +`forge-plain` is the top-level orchestrator: a short Phase 0 intent interview followed by four gated phases — (1) definitions + functional specs, (2) implementation reqs / tech stack, (3) testing (unit→impl reqs, conformance→test reqs, generate `test_scripts/`, build `config.yaml`, probe host via `check-plain-env`), (4) validate via `plain-healthcheck` (config and script checks, `plain-parser check` on every top module, plus rule checks the parser can't do) then hand off the render target. Phases 1–3 are **one-question-at-a-time, write-to-disk-immediately**. `add-feature` is the same authoring loop scoped to one feature on an existing project; `init-plain-project` is a no-interview scaffold. The installer ships `forge/rules/*.md` beside the skills; native rule consumers load them directly, while other agents reach them through `load-plain-reference`. ## Memory / persistent context -- Generated artifacts and project scratch (`plain_modules/`, `test_scripts/`, `*.yaml`, `codeplain.log`, env files) are gitignored. The renderer writes everything it generates under `plain_modules//`: `code/` for implementation and unit tests, `tests/` for conformance tests (one folder per functional spec). +- Generated artifacts and project scratch (`plain_modules/`, `test_scripts/`, `*.yaml`, env files) are gitignored. The renderer writes everything it generates under `plain_modules//`: `code/` for implementation and unit tests, `tests/` for conformance tests (one folder per functional spec). diff --git a/CLAUDE.md b/CLAUDE.md index 0cc093c..d1aa2a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,7 +11,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co So a change here is almost always one of: (a) editing the installer CLI, or (b) editing the instructional content under `forge/skills/` and `forge/rules/`. These need different mindsets — see "Editing `forge/` content" below. -`plain-forge` only *authors* `.plain` specs. Rendering specs into code is done by a **separate** tool, the `codeplain` CLI (codeplain.ai), which this repo does not contain. +`plain-forge` only *authors* `.plain` specs. Rendering specs into code is done by a **separate** renderer, which this repo does not contain. ## Commands @@ -28,14 +28,14 @@ node --test test/cli.test.mjs # run a single test f ## The installer CLI (`bin/cli.mjs`) -A single self-contained ESM file with **zero runtime dependencies**; it exports its internals so the test suite can import them without running `main()` (guarded by `isInvokedDirectly()`, which realpath-compares `argv[1]` to `__filename` — needed because the global bin is a symlink). Key model: +An ESM file with **zero runtime dependencies**, plus `bin/python-tools.mjs` (see below); it exports its internals so the test suite can import them without running `main()` (guarded by `isInvokedDirectly()`, which realpath-compares `argv[1]` to `__filename` — needed because the global bin is a symlink). Key model: - `AGENTS` maps agent name → content dir: `claude→.claude`, `codex|copilot|universal→.agents`, `forgecode→.forge`, and `opencode→.opencode`. `SCOPES`: `project` (cwd) / `global` (`$HOME`). Global ForgeCode and OpenCode paths have explicit exceptions in `resolveBaseDir`. `CONTENT_DIRS = [skills, rules, docs]` (missing source dirs are silently skipped). -- **install** writes `forge/{skills,rules,docs}` into `/`, recording every written file in `/.plain-forge/manifest.json`. It **refuses** (exit 1) if a manifest or a "forge signature" already exists — install never overwrites in place; you use `update` for that. +- **install** writes `forge/{skills,rules,docs}` into `/`, recording every written file in `/.plain-forge/manifest.json`. It **refuses** (exit 1) if a manifest or a "forge signature" already exists — install never overwrites in place; you use `update` for that. It then runs `ensurePlainParser()` from `bin/python-tools.mjs`, which installs plain-parser with `uv tool install`, installing uv first with astral's `install.sh` (Linux/macOS) or `install.ps1` (Windows). It never throws: on failure it prints the manual commands and the install still succeeds. `--skip-plain-parser` skips it, and the integration tests always pass it. The file is temporary and goes away once plain-parser ships on npm. - **update** auto-detects every install across both scopes × all agents, re-copies the fresh tree, and **prunes** files that were in the old manifest but no longer ship (confirmed individually unless `--yes`). Only manifest-recorded files are ever prune candidates, so the user's own/third-party files are never touched. - **uninstall** deletes exactly `manifest.files` then the manifest; refuses (exit 1) on a manifest-less install rather than guessing which files are its own. - Legacy (manifest-less) installs are recognized only when **all** of `FORGE_SIGNATURE_SKILLS` (`forge-plain`, `add-feature`, `debug-specs`, `load-plain-reference`) are present, then refreshed and given a manifest going forward. @@ -74,8 +74,8 @@ Other constraints the rules enforce (see `forge/rules/`): functional specs are m ### The skill lifecycle (orchestration) -`forge-plain` is the top-level orchestrator: a short Phase 0 intent interview followed by four gated phases — (1) definitions + functional specs, (2) implementation reqs / tech stack, (3) testing (unit→impl reqs, conformance→test reqs, generate `test_scripts/`, build `config.yaml`, probe host via `check-plain-env`), (4) validate via `plain-healthcheck` (`codeplain … --dry-run` gate) then hand off the render command. Phases 1–3 are **one-question-at-a-time, write-to-disk-immediately**. `add-feature` is the same authoring loop scoped to one feature on an existing project; `init-plain-project` is a no-interview scaffold; `run-codeplain` supervises a live `codeplain --headless` render. The installer ships `forge/rules/*.md` beside the skills; native rule consumers load them directly, while other agents reach them through `load-plain-reference`. +`forge-plain` is the top-level orchestrator: a short Phase 0 intent interview followed by four gated phases — (1) definitions + functional specs, (2) implementation reqs / tech stack, (3) testing (unit→impl reqs, conformance→test reqs, generate `test_scripts/`, build `config.yaml`, probe host via `check-plain-env`), (4) validate via `plain-healthcheck` (config and script checks, `plain-parser check` on every top module, plus rule checks the parser can't do) then hand off the render target. Phases 1–3 are **one-question-at-a-time, write-to-disk-immediately**. `add-feature` is the same authoring loop scoped to one feature on an existing project; `init-plain-project` is a no-interview scaffold. The installer ships `forge/rules/*.md` beside the skills; native rule consumers load them directly, while other agents reach them through `load-plain-reference`. ## Memory / persistent context -- Generated artifacts and project scratch (`plain_modules/`, `test_scripts/`, `*.yaml`, `codeplain.log`, env files) are gitignored. The renderer writes everything it generates under `plain_modules//`: `code/` for implementation and unit tests, `tests/` for conformance tests (one folder per functional spec). +- Generated artifacts and project scratch (`plain_modules/`, `test_scripts/`, `*.yaml`, env files) are gitignored. The renderer writes everything it generates under `plain_modules//`: `code/` for implementation and unit tests, `tests/` for conformance tests (one folder per functional spec). diff --git a/README.md b/README.md index c0da90a..b80f6ff 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # plain-forge -A toolkit for working with [∗∗∗plain](https://plainlang.org) projects from inside your AI coding agent of choice — Claude Code, Codex, ForgeCode, OpenCode, and any other agent that reads from a standard skills directory. plain-forge ships skills, rules, and docs that turn a conversation into complete `.plain` spec files, then keeps maintaining them across the lifetime of the project. The specs are rendered into production-ready code by the [codeplain](https://codeplain.ai) renderer. +A toolkit for working with [∗∗∗plain](https://plainlang.org) projects from inside your AI coding agent of choice — Claude Code, Codex, ForgeCode, OpenCode, and any other agent that reads from a standard skills directory. plain-forge ships skills, rules, and docs that turn a conversation into complete `.plain` spec files, then keeps maintaining them across the lifetime of the project. The specs are then rendered into production-ready code by a ***plain renderer. ## What plain-forge does @@ -14,7 +14,7 @@ plain-forge is organized around four kinds of work, each with its own entry-poin Pick whichever entry point matches how much upfront design you want: -- **`forge-plain`** — full end-to-end interview. One question at a time, immediate writes to disk, covers product → tech stack → testing → validation in four phases. Produces a complete `.plain` project with `config.yaml`, test scripts, and a successful `codeplain --dry-run` before handing off. +- **`forge-plain`** — full end-to-end interview. One question at a time, immediate writes to disk, covers product → tech stack → testing → validation in four phases. Produces a complete `.plain` project with `config.yaml`, test scripts, and a passing `plain-healthcheck` before handing off. - **`init-plain-project`** — lightweight scaffold. Asks only about the base technology, project kind, and whether conformance testing is on; emits a template module, a stub top-level module, the testing scripts, and a `config.yaml`. No interview, no specs — pair it with `add-feature` to grow the project feature by feature. ### 2. Grow an existing project @@ -25,15 +25,14 @@ Pick whichever entry point matches how much upfront design you want: ### 3. Validate and maintain -- **`plain-healthcheck`** — the verification gate. Inventories every `.plain` module, validates every `config.yaml`, and dry-runs every top module with the right config. Returns `PASS` / `FAIL` with a numbered punch-list when something is broken. +- **`plain-healthcheck`** — the verification gate. Inventories every `.plain` module, validates every `config.yaml`, parses every top module with [`plain-parser`](https://github.com/plainlang/plain-parser), and checks every module against the ***plain rules. Returns `PASS` / `FAIL` with a numbered punch-list when something is broken. - **`init-config-file`** — assembles the canonical `config.yaml` per part of a project from the decisions made during interviewing. -- **`check-plain-env`** — probes the host machine for everything the project needs (language toolchains, external services, system binaries, drivers, `codeplain` itself) and emits a `PASS` / `WARN` / `FAIL` report with OS-specific install commands. +- **`check-plain-env`** — probes the host machine for everything the project needs (language toolchains, external services, system binaries, drivers) and emits a `PASS` / `WARN` / `FAIL` report with OS-specific install commands. - **`analyze-func-specs`** / **`analyze-if-func-spec-too-complex`** / **`break-down-func-spec`** / **`resolve-spec-conflict`** — the spec-quality toolchain that runs behind `add-feature` and `forge-plain` but can also be invoked directly when reviewing or refactoring. -### 4. Debug and render +### 4. Debug and test - **`debug-specs`** — when the rendered app misbehaves, this traces the generated code back to the spec that caused it and fixes the spec (never the generated code). -- **`run-codeplain`** — experimental supervised render. Launches `codeplain` for you, tails the log, watches generated code appear under `plain_modules/`, and detects pathologies (stuck conformance loops, complexity errors, missing concepts, render failures). On approval it stops the renderer, hands off to the right spec-edit skill, and resumes. - **`render-spec`** — [pyro](https://github.com/plainlang/pyro), the open-source ***plain renderer, bundled with plain-forge. Renders a `.plain` module (and the modules it requires or imports) into working, tested code directly inside your agent. See [Render with pyro](#render-with-pyro). - **`implement-unit-testing-script`** / **`implement-conformance-testing-script`** / **`implement-prepare-environment-script`** — generate the per-language testing scripts that the renderer and you both invoke. New languages can be added by these skills without touching any other part of the project. @@ -57,6 +56,8 @@ This prompts you to pick an agent and a scope using an arrow-key menu. You can a npx plain-forge install --agent claude --scope project ``` +`install` also installs [`plain-parser`](https://pypi.org/project/plain-parser/), the Python CLI that `plain-healthcheck` uses to validate `.plain` files. It installs it with [uv](https://docs.astral.sh/uv/), installing uv first if it is missing, on Linux, macOS and Windows alike. If plain-parser is already on your `PATH`, nothing is installed. Pass `--skip-plain-parser` to leave both alone. + **Agent options:** | `--agent` | Installs into (project · global) | Use when | @@ -148,6 +149,7 @@ If an install has **no manifest** (e.g. one that predates manifests), `uninstall 1. Open your project folder and start a session in your favorite AI coding agent (Claude Code, OpenCode, Codex, …). 2. Make sure the plain-forge skills are available in that session. +3. Make sure [`plain-parser`](https://pypi.org/project/plain-parser/) is installed. `plain-healthcheck` uses it to validate `.plain` files. `npx plain-forge install` installs it for you; otherwise run `uv tool install plain-parser` or `pipx install plain-parser` (Python 3.11 or newer). ### Starting a new project @@ -159,7 +161,7 @@ If an install has **no manifest** (e.g. one that predates manifests), `uninstall If you'd rather skip the full upfront interview and build the specs feature-by-feature, use this lighter loop: -1. Invoke `init-plain-project`. It asks just for the base technology, the project kind, and whether conformance testing is enabled, then scaffolds the project skeleton: `template/base.plain` with the base `***implementation reqs***` and `***test reqs***`, a stub top-level `.plain` (frontmatter only — no functional specs, no concepts), the unit-test script, an optional conformance-test script, an optional prepare-environment script, and a `config.yaml` wired to whichever scripts were generated. No `codeplain --dry-run` is run. +1. Invoke `init-plain-project`. It asks just for the base technology, the project kind, and whether conformance testing is enabled, then scaffolds the project skeleton: `template/base.plain` with the base `***implementation reqs***` and `***test reqs***`, a stub top-level `.plain` (frontmatter only — no functional specs, no concepts), the unit-test script, an optional conformance-test script, an optional prepare-environment script, and a `config.yaml` wired to whichever scripts were generated. The project is not validated. 2. From there, either: - **Converse with the agent.** Just describe the next feature in plain English; the agent will invoke `add-feature` for you and run its one-question-at-a-time loop until the feature is on disk. - **Invoke `add-feature` manually** whenever you want to drive the loop yourself. @@ -174,19 +176,7 @@ If you'd rather skip the full upfront interview and build the specs feature-by-f ### Rendering specs -Once your `.plain` files are ready (and `plain-healthcheck` is green), render the specs into code with the [Codeplain](https://codeplain.ai) renderer: - -```bash -codeplain .plain -``` - -plain-forge prints the exact command (with the right final module name) at the end of Phase 4. - -#### Supervised render (experimental) - -If you'd rather have plain-forge babysit the run from your AI coding agent, invoke `run-codeplain`. It launches the renderer for you, tails `codeplain.log`, watches generated code appear under `plain_modules/`, and surfaces what's happening in plain English. If it detects a pathology (stuck conformance loop, complexity error, missing concept, render failure), it asks for approval to stop the renderer, hands off to the right spec-edit skill (`debug-specs`, `resolve-spec-conflict`, `break-down-func-spec`, …), and resumes the render from the last completed functionality via `--render-from`. - -This is an **experimental** feature — the default and most reliable way to render is still the manual `codeplain .plain` invocation above. +Once your `.plain` files are ready (and `plain-healthcheck` is green), render the specs into code with a ***plain renderer, passing it the final module in the dependency chain. plain-forge names that module at the end of Phase 4. #### Render with pyro @@ -227,6 +217,7 @@ forge/ # canonical content, copied verbatim on install bin/ cli.mjs # the `plain-forge` CLI — `install` and `update` commands + python-tools.mjs # installs uv + plain-parser during `install` (until plain-parser ships on npm) vendor/ pyro/ # git submodule: plainlang/pyro, pinned to the release plain-forge ships @@ -237,8 +228,9 @@ scripts/ test/ cli.test.mjs # tests for the install / update CLI + python-tools.test.mjs # tests for the uv + plain-parser install step -package.json # ships only `bin/cli.mjs` and `forge/` to npm +package.json # ships only `bin/cli.mjs`, `bin/python-tools.mjs` and `forge/` to npm ``` ### Bundled pyro skill @@ -270,10 +262,9 @@ On `install`, the CLI reads `forge/skills` and `forge/rules` and writes them int | Skill | Description | |-------|-------------| | `forge-plain` | End-to-end QA interview that produces complete `.plain` spec files for a new project | -| `init-plain-project` | Lightweight project initializer — scaffolds `template/base.plain` (base impl + test reqs), a stub top-level module, the testing scripts, and `config.yaml`. No functional specs, no concepts, no dry-run. Pair with `add-feature` to grow the project feature-by-feature. | +| `init-plain-project` | Lightweight project initializer — scaffolds `template/base.plain` (base impl + test reqs), a stub top-level module, the testing scripts, and `config.yaml`. No functional specs, no concepts, no validation. Pair with `add-feature` to grow the project feature-by-feature. | | `add-feature` | Interview the user about a single feature, then write all the specs for it | | `render-spec` | [pyro](https://github.com/plainlang/pyro), the open-source renderer: renders a `.plain` module and the modules it requires or imports into working, tested code, from inside your agent. See [Render with pyro](#render-with-pyro). | -| `run-codeplain` | **Experimental.** Launch a `codeplain` render and supervise it end-to-end — tails `codeplain.log`, watches generated code appear, detects pathologies (stuck conformance loops, complexity errors, missing concepts, render failures), and on approval stops the renderer, hands off to the right spec-edit skill, and resumes with `--render-from`. The default render path is still the manual `codeplain .plain` command. | ### Spec Authoring @@ -301,9 +292,9 @@ On `install`, the CLI reads `forge/skills` and `forge/rules` and writes them int | Skill | Description | |-------|-------------| -| `init-config-file` | Build / finalize the project's `config.yaml` file(s) from the decisions made in Phase 3. Knows the full set of valid keys derived from the `codeplain` CLI, refuses to write secrets or per-invocation flags, and produces one config per part of the project. Run at the end of `forge-plain` (just before `plain-healthcheck`) and any time the testing surface changes. | -| `plain-healthcheck` | Verification gate: validates every `config.yaml`, confirms each `*-script` field points at a real file in `test_scripts/`, and dry-runs every top module. Run whenever anything in the project is finalized — at the end of `forge-plain`, at the end of `add-feature`, after `debug-specs`, and after any single-skill edit that touches the renderable surface. | -| `check-plain-env` | Read the project's `.plain` files, `test_scripts/`, `config.yaml`(s), and `resources/`, then probe the host for every requirement **the package manager can't install**: language toolchains (`python` + `pip`, `node` + `npm`, JDK + `mvn`, Go, Rust, .NET, etc.), external services (Postgres, Redis, Docker, ...), system binaries that language packages wrap (`ffmpeg`, `tesseract`, `pdftoppm`, browser binaries, ...), hardware / drivers / accelerators (NVIDIA driver → CUDA toolkit → cuDNN → framework-sees-GPU chain), `codeplain` itself, and credential env vars. Does **not** probe individual language packages — `pip install -r requirements.txt` (and equivalents) handle those when the test scripts run. Emits a `PASS` / `WARN` / `FAIL` report with OS-specific install commands for any gaps. Read-only — never installs anything. Run on first-time setup, before rendering on a new machine, after adding a new tech to a project, or any time `command not found` shows up in test output. | +| `init-config-file` | Build / finalize the project's `config.yaml` file(s) from the decisions made in Phase 3. Knows the full set of valid keys, refuses to write secrets or per-invocation options, and produces one config per part of the project. Run at the end of `forge-plain` (just before `plain-healthcheck`) and any time the testing surface changes. | +| `plain-healthcheck` | Verification gate: validates every `config.yaml`, confirms each `*-script` field points at a real file in `test_scripts/`, runs `plain-parser check` on every top module, and checks every module against the rules. Run whenever anything in the project is finalized — at the end of `forge-plain`, at the end of `add-feature`, after `debug-specs`, and after any single-skill edit that touches the renderable surface. | +| `check-plain-env` | Read the project's `.plain` files, `test_scripts/`, `config.yaml`(s), and `resources/`, then probe the host for every requirement **the package manager can't install**: language toolchains (`python` + `pip`, `node` + `npm`, JDK + `mvn`, Go, Rust, .NET, etc.), external services (Postgres, Redis, Docker, ...), system binaries that language packages wrap (`ffmpeg`, `tesseract`, `pdftoppm`, browser binaries, ...), hardware / drivers / accelerators (NVIDIA driver → CUDA toolkit → cuDNN → framework-sees-GPU chain), credential env vars, and `plain-parser`. Does **not** probe individual language packages — `pip install -r requirements.txt` (and equivalents) handle those when the test scripts run. Emits a `PASS` / `WARN` / `FAIL` report with OS-specific install commands for any gaps. Read-only — never installs anything. Run on first-time setup, before rendering on a new machine, after adding a new tech to a project, or any time `command not found` shows up in test output. | | `analyze-if-func-spec-too-complex` | Check if a spec exceeds the 200-line complexity limit | | `analyze-func-specs` | Check a batch of specs (2+) against each other in one call and return every conflicting pair | | `analyze-2-func-specs` | Legacy: check exactly two specs for conflicts (prefer `analyze-func-specs`) | diff --git a/RELEASING.md b/RELEASING.md index 33b88a4..bcf331f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -14,8 +14,8 @@ immediately before publishing. Nothing is committed back. This is deliberate: writing the version to `main` would mean a bookkeeping commit on a protected branch, which needs either a stored credential or a self-merging PR. The -tag avoids both — the same model the [codeplain](https://github.com/Codeplain-ai/codeplain) -repo uses, where hatch-vcs derives the version from the tag. +tag avoids both — the same model as hatch-vcs, which derives a Python package's version +from its tag. The consequence, so it doesn't surprise anyone: **`package.json` on `main` is stale by design.** At the time of writing it says `1.0.19` while npm serves `1.0.20`. The tag and @@ -102,7 +102,7 @@ Two consequences worth knowing: - **Every merge permanently adds a version to npm.** Unpublishing is only possible within 72 hours, so the registry accumulates one prerelease per merge. Add a `paths-ignore` to - the `push` trigger if that churn stops being worth it — only `bin/cli.mjs` and `forge/` + the `push` trigger if that churn stops being worth it — only `bin/` and `forge/` reach the tarball, so docs-only and test-only merges publish nothing of substance. - **Re-running a merge's workflow is a no-op**, not a failure. The version is derived from the sha, so the job detects the version is already on npm and skips. diff --git a/bin/cli.mjs b/bin/cli.mjs index 27addd4..40b9b70 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -5,6 +5,8 @@ import path from "node:path"; import readline from "node:readline"; import { fileURLToPath } from "node:url"; +import { ensurePlainParser } from "./python-tools.mjs"; + const __filename = fileURLToPath(import.meta.url); const pkgRoot = path.resolve(path.dirname(__filename), ".."); const forgeDir = path.join(pkgRoot, "forge"); @@ -117,6 +119,7 @@ Install options: --agent Target agent layout --scope Install into cwd or $HOME + --skip-plain-parser Don't install plain-parser (and uv) -h, --help Show this help Update options: @@ -139,6 +142,8 @@ Examples: "install" fails if plain-forge is already installed at the target — use "update" to refresh it. Missing install flags are prompted interactively. +"install" also installs plain-parser, which the healthcheck uses to validate +.plain files, with uv (installing uv first if it is missing). "update" auto-detects installs and prunes only files plain-forge wrote (confirming each removal), leaving your own and third-party skills untouched. "uninstall" deletes exactly the files recorded in the install manifest, then @@ -152,6 +157,7 @@ function parseArgs(argv) { if (a === "--agent") out.agent = argv[++i]; else if (a === "--scope") out.scope = argv[++i]; else if (a === "-y" || a === "--yes") out.yes = true; + else if (a === "--skip-plain-parser") out.skipPlainParser = true; else if (a === "-h" || a === "--help") out.help = true; else out._.push(a); } @@ -794,6 +800,7 @@ async function cmdInstall(args) { console.log(` rules: ${counts.rules}`); console.log(` docs: ${counts.docs}`); wireRules(agent, scope); + if (!args.skipPlainParser) ensurePlainParser(); console.log(); printNextSteps(agent); } @@ -988,7 +995,6 @@ function printNextSteps(agent) { const bold = (s) => `\x1b[1m${s}\x1b[0m`; const dim = (s) => `\x1b[2m${s}\x1b[0m`; const plain = (s) => color(palette.plain, s); - const codeplain = (s) => color(palette.brand, s); const link = (s) => color(palette.link, s, "4;"); console.log(`\x1b[1mnext steps:\x1b[0m`); @@ -1006,9 +1012,6 @@ function printNextSteps(agent) { ` ${bold("add-feature")} — add a feature to an existing ${plain("***plain")} project`, ); console.log(); - console.log( - `prerequisite: install the ${codeplain("codeplain")} CLI to render your specs into code — ${link("https://www.codeplain.ai/")}`, - ); console.log( `usage guide: ${link("https://github.com/plainlang/plain-forge#usage")}`, ); diff --git a/bin/python-tools.mjs b/bin/python-tools.mjs new file mode 100644 index 0000000..8323fee --- /dev/null +++ b/bin/python-tools.mjs @@ -0,0 +1,139 @@ +// Installs plain-parser, the Python CLI `plain-healthcheck` uses to validate +// `.plain` files, together with uv, the tool that installs it. +// +// Temporary: once plain-parser ships as an npm package it becomes a regular +// dependency in package.json and this file goes away. +// +// Nothing here throws. A failed step prints a warning with the manual +// commands, and the plain-forge install carries on. +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const PLAIN_PARSER = "plain-parser"; + +const UV_INSTALLERS = { + win32: { + cmd: "powershell.exe", + args: [ + "-NoProfile", + "-ExecutionPolicy", + "ByPass", + "-Command", + "irm https://astral.sh/uv/install.ps1 | iex", + ], + manual: + 'powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"', + }, + // Linux and macOS. Falls back to wget where curl is missing (minimal images). + posix: { + cmd: "sh", + args: [ + "-c", + "if command -v curl >/dev/null 2>&1; then curl -LsSf https://astral.sh/uv/install.sh | sh; " + + "elif command -v wget >/dev/null 2>&1; then wget -qO- https://astral.sh/uv/install.sh | sh; " + + 'else echo "the uv installer needs curl or wget, and neither is installed" >&2; exit 1; fi', + ], + manual: "curl -LsSf https://astral.sh/uv/install.sh | sh", + }, +}; + +function uvInstaller(platform) { + return platform === "win32" ? UV_INSTALLERS.win32 : UV_INSTALLERS.posix; +} + +// Run a command. `inherit` streams its output to the user; otherwise stdout is +// captured. `error` is set (ENOENT) when the command is not on PATH. +function runCommand(cmd, args, { inherit = false } = {}) { + const r = spawnSync(cmd, args, { + encoding: "utf8", + stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"], + }); + return { status: r.status, stdout: r.stdout ?? "", error: r.error }; +} + +function isOnPath(cmd, run) { + const r = run(cmd, ["--help"]); + return !r.error && r.status === 0; +} + +// Where the uv installer puts uv. A fresh install is not on this process's +// PATH, so look for it here before giving up. +function uvCandidates(platform, env, home) { + const exe = platform === "win32" ? "uv.exe" : "uv"; + const dirs = [ + env.UV_INSTALL_DIR, + env.XDG_BIN_HOME, + path.join(home, ".local", "bin"), + path.join(home, ".cargo", "bin"), + ].filter(Boolean); + return dirs.map((d) => path.join(d, exe)); +} + +function findUv({ platform, env, home, run, exists }) { + if (isOnPath("uv", run)) return "uv"; + return uvCandidates(platform, env, home).find(exists) ?? null; +} + +function warnOffPath(log, binDir) { + log(` plain-parser: installed${binDir ? ` into ${binDir}` : ""}, which is not on your PATH yet.`); + log(` run "uv tool update-shell" and open a new terminal.`); + return { status: "not-on-path" }; +} + +function warnManual(log, reason, platform) { + log(` plain-parser: not installed — ${reason}`); + log(` plain-forge's healthcheck needs it. Install it yourself with:`); + log(` ${uvInstaller(platform).manual}`); + log(` uv tool install ${PLAIN_PARSER}`); +} + +// Make sure `plain-parser` is installed, installing uv first if needed. +// Returns { status } — "present", "installed", "not-on-path", or "failed". +function ensurePlainParser({ + platform = process.platform, + env = process.env, + home = os.homedir(), + run = runCommand, + exists = fs.existsSync, + log = console.log, +} = {}) { + if (isOnPath(PLAIN_PARSER, run)) { + log(` plain-parser: already installed`); + return { status: "present" }; + } + + let uv = findUv({ platform, env, home, run, exists }); + if (!uv) { + log(` uv: not found — installing it (https://docs.astral.sh/uv/)`); + const { cmd, args } = uvInstaller(platform); + const r = run(cmd, args, { inherit: true }); + uv = !r.error && r.status === 0 ? findUv({ platform, env, home, run, exists }) : null; + if (!uv) { + warnManual(log, "installing uv failed", platform); + return { status: "failed" }; + } + } + + // Installed by an earlier run, but its bin dir is not on PATH. + const binDir = run(uv, ["tool", "dir", "--bin"]).stdout.trim(); + const exe = platform === "win32" ? `${PLAIN_PARSER}.exe` : PLAIN_PARSER; + if (binDir && exists(path.join(binDir, exe))) return warnOffPath(log, binDir); + + log(` plain-parser: not found — installing it with uv`); + // uv downloads a suitable Python (3.11+) itself when the host has none. + const r = run(uv, ["tool", "install", PLAIN_PARSER], { inherit: true }); + if (r.error || r.status !== 0) { + warnManual(log, "`uv tool install plain-parser` failed", platform); + return { status: "failed" }; + } + + if (isOnPath(PLAIN_PARSER, run)) { + log(` plain-parser: installed`); + return { status: "installed" }; + } + return warnOffPath(log, binDir); +} + +export { ensurePlainParser, uvInstaller, uvCandidates }; diff --git a/forge/rules/integration-embedded-testing.md b/forge/rules/integration-embedded-testing.md index 2cce46e..f24617c 100644 --- a/forge/rules/integration-embedded-testing.md +++ b/forge/rules/integration-embedded-testing.md @@ -120,7 +120,7 @@ output=$(mvn test --no-transfer-progress 2>&1) ## Common contract (all three scripts) -All three scripts are invoked by the `codeplain` renderer with positional arguments. They MUST: +All three scripts are invoked by the renderer with positional arguments. They MUST: 1. **Be POSIX-bash (`.sh`) on macOS / Linux, PowerShell (`.ps1`) on Windows.** Executable, idempotent — re-running with the same inputs produces the same result 2. **Validate every positional argument up front.** Exit with a clear `Usage:` line on bad args. Conventional exit codes (consistent with the shared testing-script rules): diff --git a/forge/rules/linked-resources.md b/forge/rules/linked-resources.md index 4398164..624a612 100644 --- a/forge/rules/linked-resources.md +++ b/forge/rules/linked-resources.md @@ -61,9 +61,9 @@ A linked resource **must not** be any of the following: ``` ## File location and path resolution -- Paths are resolved against the directory that contains the `.plain` file passed to `codeplain`, not the directory where `codeplain` is run +- Paths are resolved against the directory that contains the `.plain` file passed to the renderer, not the directory where the renderer is run - This includes links inside imported and required modules — they resolve against that same directory, not against the directory the imported module itself lives in -- A path that does not resolve there is then looked up in the `--template-dir` directory (if given), and last in the built-in `standard_template_library` +- A path that does not resolve there is then looked up in the `template-dir` directory (if configured), and last in the renderer's built-in template library - Because the `.plain` file's own directory is the anchor, a render gives the same result from any working directory - Relative paths may traverse into parent directories — `[resource](../../resources/resource.md)` is valid — as long as they resolve to a real text file on disk - The conventional location is a `resources/` directory next to the `.plain` file diff --git a/forge/skills/add-feature/SKILL.md b/forge/skills/add-feature/SKILL.md index e196f65..1ce3672 100644 --- a/forge/skills/add-feature/SKILL.md +++ b/forge/skills/add-feature/SKILL.md @@ -114,7 +114,8 @@ Most checks already happened in the loop; this is a slim consistency pass whose - Acceptance tests (if any) are consistent with their parent specs. 3. Present the final diff for the modified file(s) and get the user's approval. 4. If the user requests changes, drop **straight back into the one-question loop** — one question, one write, one fix at a time. Do not restart the loop from scratch. -5. **Run `plain-healthcheck`** — the last thing this skill does. It validates every `config.yaml` and dry-runs every top module, so a feature is never finished while the project would fail to render. If it returns `FAIL`, work through its numbered list (fixing only `.plain` files / `config.yaml` / scripts — never generated code) by dropping back into the loop, then re-run. Repeat until `PASS`. Only then tell the user the feature is ready and remind them to re-render with `codeplain .plain`. +5. **Run `plain-healthcheck`** — the last check this skill runs. It validates every `config.yaml`, runs `plain-parser check` on every top module, and checks every module against the rules, so a feature is never finished while the project would fail to render. If it returns `FAIL`, work through its numbered list (fixing only `.plain` files / `config.yaml` / scripts — never generated code) by dropping back into the loop, then re-run. Repeat until `PASS`. Only then tell the user the feature is ready to render as `.plain`. +6. **Default renderer: pyro.** Ask the user whether to re-render `` now with pyro's `render-spec` skill (`/render-spec .plain`, or `/pyro:render-spec .plain` when pyro is installed as a Claude Code plugin), and invoke it only if they agree. If no `render-spec` skill is available in this session, tell the user pyro is not installed and point them to https://github.com/plainlang/pyro#installation. The user may render with any other ***plain renderer instead. ## Next feature diff --git a/forge/skills/add-feature/references/checklist.md b/forge/skills/add-feature/references/checklist.md index 04b4c88..6608ae2 100644 --- a/forge/skills/add-feature/references/checklist.md +++ b/forge/skills/add-feature/references/checklist.md @@ -35,4 +35,4 @@ Use this to verify the workflow was followed — never as a substitute for it. R - [ ] Read the modified `.plain` file(s) in full. - [ ] Verified: concepts defined before use, no cycles, chronological ordering correct end-to-end, functional specs language-agnostic, every external interface explicit (endpoints, methods, CLI args, formats), acceptance tests consistent with their parent specs. - [ ] Presented the final diff and got the user's approval (change requests dropped back into the one-question loop, not a restart). -- [ ] Ran `plain-healthcheck`; worked its numbered list to `PASS` (fixing only `.plain`/`config.yaml`/scripts — never generated code) before declaring the feature ready and reminding the user to re-render with `codeplain .plain`. +- [ ] Ran `plain-healthcheck`; worked its numbered list to `PASS` (fixing only `.plain`/`config.yaml`/scripts — never generated code) before declaring the feature ready and reminding the user to re-render `.plain`. diff --git a/forge/skills/add-resource/SKILL.md b/forge/skills/add-resource/SKILL.md index 8545fac..6b7ea6c 100644 --- a/forge/skills/add-resource/SKILL.md +++ b/forge/skills/add-resource/SKILL.md @@ -40,9 +40,9 @@ Use standard markdown link syntax inside any spec section: ## Path Rules -- Paths are resolved **against the directory that contains the `.plain` file passed to `codeplain`**, not the directory where `codeplain` is run. +- Paths are resolved **against the directory that contains the `.plain` file passed to the renderer**, not the directory where the renderer is run. - This includes links inside imported and required modules — they resolve against that same directory, not against the directory the imported module itself lives in. -- A path that does not resolve there is then looked up in the `--template-dir` directory (if given), and last in the built-in `standard_template_library`. +- A path that does not resolve there is then looked up in the `template-dir` directory (if configured), and last in the renderer's built-in template library. - Relative paths may traverse into parent directories — `[resource](../../resources/resource.md)` is valid — as long as they resolve to a real text file on disk. - **No external URLs** — only local file references. - **No folder paths** — the target must be a file, not a directory. diff --git a/forge/skills/check-forge-version/SKILL.md b/forge/skills/check-forge-version/SKILL.md index 146beab..cf4c86b 100644 --- a/forge/skills/check-forge-version/SKILL.md +++ b/forge/skills/check-forge-version/SKILL.md @@ -104,7 +104,7 @@ Relay the verdict to the user, then act on it: ``` State plainly that **continuing on an outdated plain-forge is not recommended** — its skills and - rules have been superseded, specs authored against them may not match what the current `codeplain` + rules have been superseded, specs authored against them may not match what the current renderer expects, and fixes released since the installed version are not in effect. On `WARN`, where the version could not be confirmed, say the install should be treated as outdated until proven otherwise. diff --git a/forge/skills/check-forge-version/scripts/check-forge-version.mjs b/forge/skills/check-forge-version/scripts/check-forge-version.mjs index 5a1a1ee..fa4d88f 100644 --- a/forge/skills/check-forge-version/scripts/check-forge-version.mjs +++ b/forge/skills/check-forge-version/scripts/check-forge-version.mjs @@ -265,7 +265,7 @@ function report(installs, latest, statuses) { `${n} ${n === 1 ? "install is" : "installs are"} behind v${latest.version}. ` + "Continuing on an outdated plain-forge is not recommended: its skills and rules have " + "been superseded, so specs authored against them may not match what the current " + - "codeplain renderer expects, and fixes released since this version are not in effect.", + "renderer expects, and fixes released since this version are not in effect.", ), ); } diff --git a/forge/skills/check-plain-env/SKILL.md b/forge/skills/check-plain-env/SKILL.md index e4e6343..a12f725 100644 --- a/forge/skills/check-plain-env/SKILL.md +++ b/forge/skills/check-plain-env/SKILL.md @@ -18,7 +18,7 @@ This skill answers one question: **does the host machine have everything the *** ## When to run -- **First time you open a ***plain project on a new machine** — before the first `codeplain .plain` or `./test_scripts/run_unittests*.sh` invocation. This is the most common case. +- **First time you open a ***plain project on a new machine** — before the first render or `./test_scripts/run_unittests*.sh` invocation. This is the most common case. - **At the start of `forge-plain` Phase 3 environment verification** — `forge-plain` historically did this inline; delegate to this skill instead so the same check runs the same way everywhere. - **After `add-feature`** — when the new feature brought in a new dependency (a new framework, a new service, a new package manager) the project didn't need before. - **Before a real render** — alongside `plain-healthcheck`. `plain-healthcheck` answers "are the specs renderable?"; this skill answers "can this machine render them?". Both should pass. @@ -33,7 +33,7 @@ This skill is **read-only and observational**. It does not edit `.plain` files, - It does **not** install anything. It only suggests install commands and lets the user run them. - It does **not** generate scripts, modify `config.yaml`, or change project files. Use `implement-*-testing-script`, `init-config-file`, etc. for that. - It does **not** start services (databases, brokers, Docker daemons). It only checks whether the right binaries and (optionally) running endpoints are reachable. -- It does **not** validate the specs themselves (syntax, dry-run, complexity). Use `plain-healthcheck` for that — the two skills are complementary. +- It does **not** validate the specs themselves (syntax, concepts, module graph). Use `plain-healthcheck` for that — the two skills are complementary. It only checks that `plain-parser`, which `plain-healthcheck` runs, is installed. - It does **not** print secrets. Check whether an env var is *set* (e.g. `printenv FOO >/dev/null && echo set || echo missing`), never echo its value. ## Workflow @@ -147,10 +147,9 @@ Worked example — the GPU case: Add these to every requirement list regardless of project: -- `codeplain` CLI on `PATH` — `codeplain --version`. -- `CODEPLAIN_API_KEY` env var **set** — `printenv CODEPLAIN_API_KEY >/dev/null && echo set || echo missing` (never echo the value). - A shell matching the testing scripts' extension (Bash for `.sh`, PowerShell for `.ps1`). - `git` (the renderer uses it; almost every plain project tracks itself in git). +- `plain-parser` and **Python 3.11 or newer** — `plain-healthcheck` validates every module with `plain-parser check`. Probe `python3 --version` and `plain-parser --help`; install with `pipx install plain-parser` or `uv tool install plain-parser`. ### Step 3 — Probe the host @@ -163,7 +162,7 @@ For every requirement the project produced in Step 2, run a check using the `ter Probe in the same category order Step 2 produced them, so the report reads top-down: 1. **Category 1 — Language toolchains.** Run the toolchain + package-manager probes listed in Step 2's Category 1 for the languages this project actually uses. Do not probe individual language packages. -2. **Category 5 — Codeplain and `git`.** Always required, independent of the project. `codeplain --version`, `printenv CODEPLAIN_API_KEY >/dev/null`, `git --version`. Verify the shell flavor matches the scripts' extension; a `.sh`-only project on native Windows is a `FAIL` (suggest WSL). +2. **Category 5 — Shell, `git`, and `plain-parser`.** Always required, independent of the project. `git --version`, `python3 --version` (3.11 or newer), `plain-parser --help`. Verify the shell flavor matches the scripts' extension; a `.sh`-only project on native Windows is a `FAIL` (suggest WSL). 3. **Category 2 — External services.** For each service identified at runtime, probe the CLI's presence first, then — if the CLI exists — check whether the service itself is reachable. Service binary present but daemon down → `WARN`, not `FAIL`. 4. **Category 3 — System binaries that language packages wrap.** For each one identified in Step 2, check the system binary is on `PATH` (`which ` / ` --version`). The wrapper package itself isn't probed. 5. **Category 4 — Hardware, drivers, accelerators.** Walk the layered probe from Step 2 in order (driver → device visibility → SDK → acceleration libs → framework-sees-it → version match), reporting each layer's result separately. Never collapse a multi-layer failure into a single "GPU not available" message — each layer has a different fix. @@ -186,7 +185,7 @@ For every **FAIL** and **WARN** item, the report must include four columns: | **Status** | "missing", "outdated (found X, need Y)", "binary present but service not running", or "env var not set". | | **How to install** | OS-specific command(s). See [Install suggestions](#install-suggestions) below. | -Group the report into the same six probe groups used in Step 3 (toolchains, codeplain/git, services, system binaries, hardware, credentials), so the user can fix one whole layer at a time. +Group the report into the same six probe groups used in Step 3 (toolchains, shell/git/plain-parser, services, system binaries, hardware, credentials), so the user can fix one whole layer at a time. End the report with a one-liner reminder: `Re-run check-plain-env after installing missing items to confirm.` @@ -213,7 +212,6 @@ The tables below are a **starter set** of well-known install commands for the th | Docker | Install Docker Desktop from https://docker.com/products/docker-desktop | | `pg_config` (psycopg) | comes with `brew install postgresql@16` | | Xcode CLT | `xcode-select --install` | -| `codeplain` | follow the install instructions at https://codeplain.ai | ### Linux (Debian/Ubuntu) @@ -263,7 +261,7 @@ When the host OS detection in Step 1 was inconclusive (e.g. an unrecognized Linu - **Don't pre-bake a catalog.** Derive the requirement list at runtime from the project in front of you. A hard-coded "things to always check" table becomes wrong the moment any project deviates from the assumed shape — and every project does. - **Don't probe testing framework binaries as if they were independent.** `pytest`, `jest`, `vitest`, `phpunit`, `junit-console`, etc. are installed by the package manager via `requirements.txt` / `package.json` / `pom.xml`. The toolchain check (Category 1) is enough — the framework binary itself is out of scope. - **Don't probe in silence.** Use the `terminal` tool and capture the actual command output (version strings, exit codes). Telling the user "looks like Python is installed" without running `python3 --version` is guessing. -- **Don't print secret values.** Check whether `CODEPLAIN_API_KEY`, `DATABASE_URL`, etc. are set, not what they contain. Use `printenv VAR >/dev/null` not `printenv VAR`. +- **Don't print secret values.** Check whether `DATABASE_URL`, API keys, etc. are set, not what they contain. Use `printenv VAR >/dev/null` not `printenv VAR`. - **Don't install anything on the user's behalf.** Even when the install command is obvious. The user needs to opt in. - **Don't stop at the first FAIL.** Run the full sweep so the report is complete in one pass. - **Don't suggest `curl | sh` installs when a package manager works.** Reserve the upstream installer fallback for cases where no package-manager option exists (e.g. `rustup`, sometimes Go). @@ -280,6 +278,7 @@ When the host OS detection in Step 1 was inconclusive (e.g. an unrecognized Linu - [ ] Every hardware / accelerator signal ("GPU", "fine-tune", "local inference", "CUDA", "MPS", etc.) triggered the layered Category 4 probe. - [ ] Every requirement probed via the `terminal` tool with an actual version / availability command (no "looks installed" guesses). - [ ] GPU / accelerator chains probed layer-by-layer (driver → device visibility → toolkit → acceleration libs → framework-sees-it → version match), with each layer reported separately. +- [ ] `plain-parser` and Python 3.11 or newer probed (always required). - [ ] Each result classified as PASS / WARN / FAIL. - [ ] Report has the verdict on the first line. - [ ] Every FAIL and WARN row has all four columns: What / Why / Status / How to install. diff --git a/forge/skills/forge-plain/SKILL.md b/forge/skills/forge-plain/SKILL.md index a512d0f..8752949 100644 --- a/forge/skills/forge-plain/SKILL.md +++ b/forge/skills/forge-plain/SKILL.md @@ -4,7 +4,7 @@ description: >- End-to-end `***plain` spec authoring workflow: a short intent interview followed by a gated, one-question-at-a-time interview (product, tech stack, testing) that writes complete .plain specification files to disk incrementally, reviews each addition, and - validates the specs with a dry-run before handoff. Use when the user starts a + validates the specs with plain-healthcheck before handoff. Use when the user starts a new project or wants to build something new from scratch. Not for adding a feature to an existing project (use add-feature) or for editing generated code (the .plain specs are the source of truth). @@ -85,7 +85,7 @@ When entering a phase, read its reference file and walk its topics **in order** | 1 — What are we building? | `references/phase-1-product.md` | the new `***definitions***` and `***functional specs***`, plus any technology-free `***implementation reqs***`, are on disk and approved | | 2 — What tech should it use? | `references/phase-2-tech.md` | the new `***implementation reqs***` are on disk and approved | | 3 — How is testing done? | `references/phase-3-testing.md` | the `***test reqs***` (and `***acceptance tests***` if conformance is on) are on disk, the `test_scripts/` and `config.yaml`(s) exist, and `check-plain-env` passed or each gap was acknowledged | -| 4 — Validate and hand off | `references/phase-4-validate-handoff.md` | the agent ran `codeplain .plain --dry-run` successfully against the render target, and the user has the render command plus every side-channel command | +| 4 — Validate and hand off | `references/phase-4-validate-handoff.md` | `plain-healthcheck` returned `PASS`, and the user has the render target plus every side-channel command | Between phases, summarize and get explicit overall confirmation before continuing — the intent brief after Phase 0; the full feature list and module/concept layout after Phase 1; the tech stack and @@ -112,8 +112,7 @@ Once the initial specs exist, the user will return with new features. Use the `a - **A phase gate is not met** (a review agent returned unmet boxes) → do not advance; fix each gap through the interview → author → review loop, then spawn a fresh review agent and repeat until `APPROVED`. - **A review agent can't be spawned or returns nothing usable** → spawn a fresh one; do not self-certify the gate. Never `SendMessage` a running reviewer — capture its returned result. If spawning genuinely fails in this environment, walk the phase's checklist block inline as a fallback and tell the user the review was self-run. - **`check-plain-env` returns `FAIL`** (Phase 3) → walk each gap with the user; install, swap to an alternative, or explicitly acknowledge it before Phase 4. Re-invoke after any install. -- **`plain-healthcheck` returns `FAIL`** (Phase 4) → do not present the render command; work through its numbered list with the right edit skill and re-run until it passes. -- **Environment failure** (`codeplain` not on PATH, `CODEPLAIN_API_KEY` unset) → tell the user exactly what is missing and how to fix it; never pretend the check passed. +- **`plain-healthcheck` returns `FAIL`** (Phase 4) → do not hand off the render target; work through its numbered list with the right edit skill and re-run until it passes. ## Self-check checklist diff --git a/forge/skills/forge-plain/references/checklist.md b/forge/skills/forge-plain/references/checklist.md index f9d6e21..ce36afe 100644 --- a/forge/skills/forge-plain/references/checklist.md +++ b/forge/skills/forge-plain/references/checklist.md @@ -63,5 +63,5 @@ A box is met only when its source confirms it. Return `APPROVED` only when every - [ ] **[disk]** Identified the render target — the last module in the dependency chain (or the single module). - [ ] **[both]** Ran `init-config-file` to build the final `config.yaml`(s); resolved any precondition gap with the user before validating. -- [ ] **[record]** Ran `plain-healthcheck`; worked its numbered list to `PASS` — never presented the render command on a `FAIL`. -- [ ] **[record]** Presented the render command only after the dry-run passed, plus every side-channel script actually generated in Phase 3. +- [ ] **[record]** Ran `plain-healthcheck`; worked its numbered list to `PASS` — never handed off the render target on a `FAIL`. +- [ ] **[record]** Handed off the render target only after `plain-healthcheck` passed, plus every side-channel script actually generated in Phase 3. diff --git a/forge/skills/forge-plain/references/phase-3-testing.md b/forge/skills/forge-plain/references/phase-3-testing.md index 68abb2f..b627b2f 100644 --- a/forge/skills/forge-plain/references/phase-3-testing.md +++ b/forge/skills/forge-plain/references/phase-3-testing.md @@ -55,7 +55,7 @@ When all topics are complete, recap the full testing strategy: which `config.yam Delegate environment verification to the `check-plain-env` skill — do **not** probe the machine inline. It is the single source of truth for "can this machine render and test this project?" and derives the requirement list at runtime from the project's `.plain` files, `test_scripts/`, `config.yaml`(s), and `resources/`. -`check-plain-env` detects the host OS, builds the requirement list (language toolchains + their package managers, external services, system binaries that language packages wrap, hardware / drivers / accelerators, `codeplain` itself, credentials — the layers a package manager **cannot** install), probes each with a real version / availability command, and emits a `PASS` / `WARN` / `FAIL` report with OS-specific install commands for any gaps. It never probes individual language packages (`torch`, `numpy`, `FastAPI`, `react`, JARs, gems) — those are installed by the project's own `prepare_environment` / unit-test scripts the moment they run. It is read-only and installs nothing itself. +`check-plain-env` detects the host OS, builds the requirement list (language toolchains + their package managers, external services, system binaries that language packages wrap, hardware / drivers / accelerators, credentials — the layers a package manager **cannot** install), probes each with a real version / availability command, and emits a `PASS` / `WARN` / `FAIL` report with OS-specific install commands for any gaps. It never probes individual language packages (`torch`, `numpy`, `FastAPI`, `react`, JARs, gems) — those are installed by the project's own `prepare_environment` / unit-test scripts the moment they run. It is read-only and installs nothing itself. Act on the return value: diff --git a/forge/skills/forge-plain/references/phase-4-validate-handoff.md b/forge/skills/forge-plain/references/phase-4-validate-handoff.md index 058c3a0..1c881bb 100644 --- a/forge/skills/forge-plain/references/phase-4-validate-handoff.md +++ b/forge/skills/forge-plain/references/phase-4-validate-handoff.md @@ -1,6 +1,6 @@ # Phase 4 — Validate and hand off -Two halves. First **the agent** validates every spec end-to-end with a `codeplain` dry-run so the user never wastes a real render — or any debugging time — on a fixable static error. Only after that passes does the agent **hand off** the render command (plus any side-channel commands) to the user. +Two halves. First **the agent** validates every spec with `plain-healthcheck` so the user never wastes a real render — or any debugging time — on a fixable static error. Only after that passes does the agent **hand off** the render target (plus any side-channel commands) to the user. ## 4a. Identify the render target @@ -14,36 +14,34 @@ Find the **last module in the dependency chain** — the module that is not `req Finalize the project's `config.yaml` file(s) before validation. Phase 3 may have written provisional entries as scripts were generated; **this** is where they are consolidated into the canonical form the renderer expects. Invoke the `init-config-file` skill. It: - enumerates every part of the project (one `config.yaml` per part — single-stack → root config; multi-part → one config per part), -- assembles only the **valid** config keys derived from the `codeplain` CLI parser, -- emits a clean YAML file per part (script paths first, then template/build folders, then copy/log settings), +- assembles only the **valid** config keys, +- emits a clean YAML file per part (script paths first, then template/build folders), - verifies every `*-script` value resolves to a real file on disk, -- refuses to write secrets (`api-key`) or per-invocation flags (`dry-run`, `full-plain`, `render-range`, `render-from`, `replay-with`) into the config. +- refuses to write secrets or per-invocation options into the config. If `init-config-file` stops because a precondition isn't met (e.g. a `prepare-environment-script` exists but no conformance script does), resolve the gap with the user before continuing — do **not** hand a known-broken config to `plain-healthcheck`. ## 4c. Validate the project with `plain-healthcheck` -Run the `plain-healthcheck` skill — the single source of truth for "is this project ready to render?". Do **not** run the dry-run inline. It: +Run the `plain-healthcheck` skill — the single source of truth for "is this project ready to render?". Do **not** run its checks inline. It: - inventories every `.plain` module and identifies every top module, - validates every `config.yaml` (existence, parseability, script paths actually pointing at files in `test_scripts/`, no mixed stacks), and -- runs `codeplain .plain --dry-run` for **every** top module with the correct `--config-name` for multi-part projects. +- runs `plain-parser check` on every top module, which parses it with its whole `import` / `requires` chain (frontmatter, module graph, sections, concepts, linked resources, templates), and +- checks every module for what the parser can't judge (section ownership, bullets, linked-resource contents). -The skill runs the full detect → fix → re-run loop itself (syntax errors, undefined concepts, broken `import` / `requires` chains, cyclic definitions, missing templates, complexity violations, conflicting reqs, config drift, missing scripts) and returns only once everything passes or a gap genuinely needs the user. Then: +The skill runs the full detect → fix → re-run loop itself (syntax errors, undefined concepts, broken `import` / `requires` chains, cyclic definitions, missing templates, misplaced facts, config drift, missing scripts) and returns only once everything passes or a gap genuinely needs the user. Then: - **`PASS`** → move on to step 4d. - **`FAIL`** → do **not** ask the user to render. Work through the numbered list it produced (each item references a specific `.plain` file, `config.yaml`, or script), resolve each one with the appropriate edit skill, and re-run `plain-healthcheck` until it returns `PASS`. Any item the skill could not auto-resolve will name the concrete question to put to the user. -- **Environment failure** (e.g. `codeplain` not on PATH, `CODEPLAIN_API_KEY` not set) → `plain-healthcheck` surfaces this as a clearly-marked environment failure. Tell the user exactly what's missing and how to fix it before continuing. Do not pretend the healthcheck passed. -## 4d. Present the render command +## 4d. Hand off the render target -Only after the dry-run passes, tell the user their specs are ready and present the render command: +Only after `plain-healthcheck` passes, tell the user their specs are ready and that `` is the module to render: -``` -codeplain .plain -``` +- Chain `base.plain → features.plain → integrations.plain` → render `integrations.plain`. +- Single module `my_app.plain` → render `my_app.plain`. -- Chain `base.plain → features.plain → integrations.plain` → `codeplain integrations.plain`. -- Single module `my_app.plain` → `codeplain my_app.plain`. +**Default renderer: pyro.** Ask the user whether to render `` now with pyro's `render-spec` skill (`/render-spec .plain`, or `/pyro:render-spec .plain` when pyro is installed as a Claude Code plugin), and invoke it only if they agree. If no `render-spec` skill is available in this session, tell the user pyro is not installed and point them to https://github.com/plainlang/pyro#installation. The user may render with any other ***plain renderer instead. Also remind the user of any **side-channel commands** they may want to run themselves per the Phase 3 testing strategy — for example `./test_scripts/run_unittests.sh `, `./test_scripts/prepare_environment.sh `, or `./test_scripts/run_conformance_tests.sh `. Mention only the scripts that were actually generated in Phase 3. diff --git a/forge/skills/implement-conformance-testing-script/SKILL.md b/forge/skills/implement-conformance-testing-script/SKILL.md index e5424a4..8aab86c 100644 --- a/forge/skills/implement-conformance-testing-script/SKILL.md +++ b/forge/skills/implement-conformance-testing-script/SKILL.md @@ -150,7 +150,7 @@ Shared across both shell flavors **and** both variants: - Any other non-zero code — propagated from the underlying test command. - **Working folder naming:** `/_` — the system temp directory (`/tmp` in Bash, `[System.IO.Path]::GetTempPath()` in PowerShell) plus a short identifier for the language (`java`, `python`, `node`, `go`, `rust`, ...) and the **basename** of the *first* argument (the build folder — `$(basename "$1")` / `Split-Path $BuildFolder -Leaf`), never the conformance tests folder and never the raw argument (it may be absolute; concatenating it produces broken doubled paths). All dependency installs, build outputs, caches, test runner artifacts, and the test invocation itself live inside this folder. Nothing the script does should touch `$1` after step 5 (install-inline) / step 4 (activate-only), or `$2` at any point. - **Cleanup ownership:** the **install-inline** variant removes its working folder on exit, success and failure alike (`trap 'rm -rf "$WORKING_FOLDER"' EXIT` in Bash; `Remove-Item -Recurse -Force` in the PowerShell `finally` block, after `Pop-Location`). The **activate-only** variant **never deletes the working folder** — prepare owns its lifecycle and re-creates it once per render; deleting it here would break every subsequent per-spec invocation. -- **Logging — be as verbose as possible.** The script is the only thing the operator sees between a `codeplain` render and a green/red conformance result; when it fails, the only forensic evidence anyone has is its stdout/stderr. Treat the script like a production runbook, not a quiet helper. Concretely: +- **Logging — be as verbose as possible.** The script is the only thing the operator sees between a render and a green/red conformance result; when it fails, the only forensic evidence anyone has is its stdout/stderr. Treat the script like a production runbook, not a quiet helper. Concretely: - **Announce every step before doing it**, with the exact value of every variable involved — the resolved build folder, the resolved conformance-tests folder, the working folder, the language, the toolchain version detected, the isolation root being activated (install-inline) or verified (activate-only), the variant in use, and the test command about to run. "Running tests" alone is useless; "Running `python -m unittest discover -b -s /abs/path/to/tests` from working folder `/tmp/python_build/` with venv activated at `./.venv` (Python 3.11.6)" is triage-ready. - **Echo every non-trivial command before executing it.** In Bash, use `set -x` for the body of the script (or `echo "+ "` immediately before each call); in PowerShell, set `$VerbosePreference = 'Continue'` and use `Write-Verbose` / `Write-Host` to print each command line with its arguments. - **Print clear section banners.** Each pattern step gets a banner like `===== [6/8] Activate prepared environment =====` (activate-only) or `===== [7/8] Install dependencies =====` (install-inline) so a long log can be navigated by eye, **and** the banner names the variant so the operator immediately knows which branch is executing. @@ -234,7 +234,7 @@ Notes: - **Activate-only:** [Activating a prepared environment](#activating-a-prepared-environment-activate-only) table — use the matching verify, activate, and test-command columns in steps 4, 6, and 8. 6. Add the language-appropriate "no tests discovered" guard from [No tests discovered detection](#no-tests-discovered-detection). 7. Save the new script. For Bash, `chmod +x` it. -8. **Update `config.yaml` to reference the new script.** Add or update the `conformance-tests-script:` key with the path to the newly created script (e.g., `conformance-tests-script: test_scripts/run_conformance_tests_.sh`). This is mandatory — the `codeplain` renderer needs this reference to invoke the conformance test script after rendering each functional spec. +8. **Update `config.yaml` to reference the new script.** Add or update the `conformance-tests-script:` key with the path to the newly created script (e.g., `conformance-tests-script: test_scripts/run_conformance_tests_.sh`). This is mandatory — the renderer needs this reference to invoke the conformance test script after rendering each functional spec. 9. **Link the script into the base `.plain` files as a linked resource.** Once the script exists on disk, it must be referenced from the project's base `.plain` module(s) — typically the one carrying `***test reqs***` for the conformance-testing strategy — so the renderer sees the script's contents as ground truth for the conformance contract. Use the `add-resource` skill to add a standard markdown link (e.g. `[conformance test runner](test_scripts/run_conformance_tests_.sh)`) inside the relevant section of the spec, with the path resolved relative to the `.plain` file. The script is a single text file, so it satisfies the linked-resource contract directly — do not link the `test_scripts/` directory, do not paraphrase the script into the spec, and do not duplicate its commands inline. If multiple base `.plain` modules reference the conformance contract (e.g. a host module and an integration module that inherits from it), link the script from each module that needs it; the linked resource lives next to the spec that owns the test req, not at the top of the chain by default. After linking, read the spec back to confirm the link path resolves to the script you just wrote. If this is the activate-only variant, link the matching `prepare_environment_` script next to it from the same spec — the two halves of the conformance contract belong together. 10. **For activate-only scripts only**: smoke-test by running `prepare_environment_. && run_conformance_tests_. `. If the conformance script errors with "prepared environment missing" right after a successful prepare, the two scripts disagree on either the working-folder path or the isolation location — fix that before declaring done. @@ -255,6 +255,6 @@ Notes: - **Don't write a cross-shell hybrid** (e.g. a `.sh` that detects PowerShell, or vice versa). Ship one script per shell, named with the appropriate extension. - **Don't install dependencies into the user's global location** (`~/.m2`, system-wide `pip`, `~/.cargo`, etc.) in the install-inline variant. Always isolate inside `$WORKING_FOLDER` so concurrent runs and other projects can't interfere. - **Don't run the test command without first verifying the install / activation succeeded.** A failed install (or missing prepared env) followed by a "test" run produces misleading errors that look like test failures. -- **Don't forget to update `config.yaml`.** After creating the conformance test script, always add or update the `conformance-tests-script:` key in `config.yaml` to reference the new script. Without this entry, the `codeplain` renderer won't know where to find the conformance test script. +- **Don't forget to update `config.yaml`.** After creating the conformance test script, always add or update the `conformance-tests-script:` key in `config.yaml` to reference the new script. Without this entry, the renderer won't know where to find the conformance test script. - **Don't forget to link the script as a resource in the base `.plain` files.** A script that is referenced only from `config.yaml` is invisible to the spec contract — the renderer treats `conformance-tests-script:` as a build-system pointer, not as part of the test-req contract the model reads. Use the `add-resource` skill to add a markdown link to the script from the `.plain` module that owns the conformance test reqs (see Workflow step 9). Omitting the linked resource means the model authors and reviews conformance-test code without ever seeing the actual runner it has to satisfy — and for activate-only scripts, the model also loses sight of which isolation paths must already be in place by the time tests run. - **Don't write a terse script.** Silent steps, `>/dev/null` redirects on the install / activation output, missing `--version` prints, absent variant banners, an un-logged resolved `$2`, and a missing final summary all make the script harder to debug than the tests it is running. Follow the *Logging — be as verbose as possible* rule under [Conventions](#conventions) literally: every step announces itself, every command is echoed, the variant is named in its banner, the resolved `$CONFORMANCE_TESTS_FOLDER` is printed, every failure prints what was about to run and where, and the final summary names the variant, test command, exit code, and working folder. diff --git a/forge/skills/implement-prepare-environment-script/SKILL.md b/forge/skills/implement-prepare-environment-script/SKILL.md index 36c765a..6dd5de9 100644 --- a/forge/skills/implement-prepare-environment-script/SKILL.md +++ b/forge/skills/implement-prepare-environment-script/SKILL.md @@ -134,7 +134,7 @@ Shared across both shell flavors: - `0` — success. - **Working folder naming:** `/_` — the system temp directory (`/tmp` in Bash, `[System.IO.Path]::GetTempPath()` in PowerShell) plus a short identifier for the language (`java`, `python`, `node`, `go`, `rust`, ...) and the **basename** of the first (and only) argument (`$(basename "$1")` / `Split-Path -Leaf`). Never embed the raw argument in the path — it may be absolute, and concatenating it produces broken doubled paths. All dependency installs, build outputs, caches, and pre-built artifacts live inside this folder. Nothing the script does should touch `$1` after step 4. - **No cleanup on exit.** Prepare leaves the working folder populated — that folder is its deliverable. Do not add a `trap … EXIT` / `finally` deletion; the folder is reclaimed the next time prepare wipes-and-recreates it (and the OS eventually reclaims the temp dir). -- **Logging — be as verbose as possible.** The script is the only thing the operator sees between a `codeplain` render and a green/red conformance result; when prepare fails, the only forensic evidence anyone has is its stdout/stderr. Treat the script like a production runbook, not a quiet helper. Concretely: +- **Logging — be as verbose as possible.** The script is the only thing the operator sees between a render and a green/red conformance result; when prepare fails, the only forensic evidence anyone has is its stdout/stderr. Treat the script like a production runbook, not a quiet helper. Concretely: - **Announce every step before doing it**, with the exact value of every variable involved — the resolved source folder, the working folder, the language, the toolchain version detected, the isolation root, the dependency-manifest path, the install / pre-build command about to run. "Installing dependencies" alone is useless; "Installing dependencies from `./requirements.txt` into venv `./.venv` (Python 3.11.6 at `/usr/local/bin/python3`)" is triage-ready. - **Echo every non-trivial command before executing it.** In Bash, use `set -x` for the body of the script (or `echo "+ "` immediately before each call); in PowerShell, set `$VerbosePreference = 'Continue'` and use `Write-Verbose` / `Write-Host` to print each command line with its arguments. - **Print clear section banners.** Each pattern step gets a banner like `===== [4/6] Copy build into working folder =====` so a long log can be navigated by eye. @@ -192,7 +192,7 @@ Notes: 4. Translate each of the six pattern steps into the equivalent commands for the target language **and** shell. The toolchain check and dependency install/build are the language-specific parts; the rest is mechanical translation between Bash and PowerShell syntax. 5. Pick the dependency-isolation mechanism from the [Dependency isolation](#dependency-isolation) table. **Verify it matches the path used by the corresponding `run_conformance_tests_` script.** 6. Save the new script to the appropriate `test_scripts/` location (e.g. `test_scripts/prepare_environment_.sh` / `.ps1`). For Bash, `chmod +x` it. -7. **Update `config.yaml` to reference the new script.** Add or update the `prepare-environment-script:` key with the path to the newly created script (e.g., `prepare-environment-script: test_scripts/prepare_environment_.sh`). This is mandatory — the `codeplain` renderer needs this reference to invoke the prepare script before running conformance tests. +7. **Update `config.yaml` to reference the new script.** Add or update the `prepare-environment-script:` key with the path to the newly created script (e.g., `prepare-environment-script: test_scripts/prepare_environment_.sh`). This is mandatory — the renderer needs this reference to invoke the prepare script before running conformance tests. 8. **Link the script into the base `.plain` files as a linked resource.** Once the script exists on disk, it must be referenced from the project's base `.plain` module(s) — typically the one carrying `***test reqs***` for the conformance-testing strategy — so the renderer sees the script's contents as ground truth for the prepare contract. Use the `add-resource` skill to add a standard markdown link (e.g. `[prepare environment](test_scripts/prepare_environment_.sh)`) inside the relevant section of the spec, with the path resolved relative to the `.plain` file. The script is a single text file, so it satisfies the linked-resource contract directly — do not link the `test_scripts/` directory, do not paraphrase the script into the spec, and do not duplicate its commands inline. If multiple base `.plain` modules reference the prepare contract (e.g. a host module and an integration module that inherits from it), link the script from each module that needs it; the linked resource lives next to the spec that owns the test req, not at the top of the chain by default. After linking, read the spec back to confirm the link path resolves to the script you just wrote. 9. **Reconcile the conformance script (see next section).** This is mandatory whenever a matching `run_conformance_tests_` already exists in the project. 10. After both scripts are in place and `config.yaml` is updated, do a paired re-read: open prepare and conformance side by side and confirm they agree on working folder name, isolation path, and toolchain version. @@ -242,6 +242,6 @@ Adding a `prepare_environment_` script changes the contract for the corres - **Don't forget to time the install.** Without the duration log, there's no way to tell whether prepare is actually saving wall-clock time vs. doing the same work the conformance script would have done inline. - **Don't leave an existing `run_conformance_tests_` script untouched after generating prepare.** If the conformance script still does its own staging and install, prepare's work is wiped (by the `rm -rf`) or duplicated (by re-running install) on every run — defeating the purpose of this skill entirely. Always run the [Reconcile the existing conformance script](#reconcile-the-existing-conformance-script) step. - **Don't default to Bash without checking the host OS.** A `.sh` script on native Windows (outside Git Bash / WSL) won't even run, and a `.ps1` script on macOS/Linux is equally useless. Always run the detection in [Pick the Shell First](#pick-the-shell-first) before deciding the file extension. If the project supports both, produce both files — and remember to reconcile the matching conformance script in **both** flavors too. -- **Don't forget to update `config.yaml`.** After creating the prepare environment script, always add or update the `prepare-environment-script:` key in `config.yaml` to reference the new script. Without this entry, the `codeplain` renderer won't know to invoke the prepare script before running conformance tests, and the entire optimization will be bypassed. +- **Don't forget to update `config.yaml`.** After creating the prepare environment script, always add or update the `prepare-environment-script:` key in `config.yaml` to reference the new script. Without this entry, the renderer won't know to invoke the prepare script before running conformance tests, and the entire optimization will be bypassed. - **Don't forget to link the script as a resource in the base `.plain` files.** A script that is referenced only from `config.yaml` is invisible to the spec contract — the renderer treats `prepare-environment-script:` as a build-system pointer, not as part of the test-req contract the model reads. Use the `add-resource` skill to add a markdown link to the script from the `.plain` module that owns the conformance / prepare test reqs (see Workflow step 8). Omitting the linked resource means the model authors and reviews conformance-test code without ever seeing the actual environment-prep contract it has to satisfy — and it cannot reason about which isolation paths conformance must activate. - **Don't write a terse script.** Silent steps, `>/dev/null` redirects on the install / pre-build output, missing `--version` prints, absent failure banners, and a missing final summary line all make the script harder to debug than the code it is preparing. Follow the *Logging — be as verbose as possible* rule under [Conventions](#conventions) literally: every step announces itself, every command is echoed, every failure prints what was about to run and where, and the final summary names the language, working folder, isolation root, and duration. diff --git a/forge/skills/implement-unit-testing-script/SKILL.md b/forge/skills/implement-unit-testing-script/SKILL.md index 7b3f57c..04ddefb 100644 --- a/forge/skills/implement-unit-testing-script/SKILL.md +++ b/forge/skills/implement-unit-testing-script/SKILL.md @@ -62,7 +62,7 @@ Shared across both shell flavors: - Any other non-zero code — propagated from the underlying test command. - **Working folder naming:** `/_` — the system temp directory (`/tmp` in Bash, `[System.IO.Path]::GetTempPath()` in PowerShell) plus a short identifier for the language (`java`, `python`, `node`, `go`, `rust`, ...) and the **basename** of the build-folder argument (`$(basename "$1")` / `Split-Path $BuildFolder -Leaf`). Never embed the raw argument in the path — it may be absolute, and concatenating it produces broken doubled paths. All dependency installs, build outputs, caches, and the test run itself live inside this folder. Nothing the script does should touch the source build folder after step 4. - **Cleanup on exit:** the working folder is temporary — remove it when the script exits, on success and failure alike (`trap 'rm -rf "$WORKING_FOLDER"' EXIT` in Bash; `Remove-Item -Recurse -Force` in the PowerShell `finally` block, after `Pop-Location`). No leftover build folders may accumulate in the temp directory or the user's project. -- **Logging — be as verbose as possible.** The script is the only thing the operator sees between a `codeplain` render and a green/red test result; when it fails, the only forensic evidence anyone has is its stdout/stderr. Treat the script like a production runbook, not a quiet helper. Concretely: +- **Logging — be as verbose as possible.** The script is the only thing the operator sees between a render and a green/red test result; when it fails, the only forensic evidence anyone has is its stdout/stderr. Treat the script like a production runbook, not a quiet helper. Concretely: - **Announce every step before doing it**, with the exact value of every variable involved — the resolved source folder, the working folder, the language, the toolchain version detected, the isolation root, the dependency-manifest path, the test command about to run. "Installing dependencies" alone is useless; "Installing dependencies from `./requirements.txt` into venv `./.venv` (Python 3.11.6 at `/usr/local/bin/python3`)" is triage-ready. - **Echo every non-trivial command before executing it.** In Bash, use `set -x` for the body of the script (or `echo "+ "` immediately before each call); in PowerShell, set `$VerbosePreference = 'Continue'` and use `Write-Verbose` / `Write-Host` to print each command line with its arguments. - **Print clear section banners.** Each of the seven steps gets a banner like `===== [3/7] Working directory setup =====` so a long log can be navigated by eye. @@ -117,7 +117,7 @@ Notes: 3. Translate each of the seven steps above into the equivalent commands for the target language **and** shell. The toolchain check, dependency install, and test invocation are the language-specific parts; the rest is mechanical translation between Bash and PowerShell syntax. 4. Pick the dependency-isolation mechanism from the [Dependency isolation](#dependency-isolation) table and use it consistently in both step 6 and step 7. 5. Save the new script to `assets/run_unittests_.sh` or `assets/run_unittests_.ps1`. For Bash, `chmod +x` it. -6. **Update `config.yaml` to reference the new script.** Add or update the `unit-tests-script:` key with the path to the newly created script (e.g., `unit-tests-script: test_scripts/run_unittests_.sh`). This is mandatory — the `codeplain` renderer needs this reference to invoke the unit test script during the development workflow. +6. **Update `config.yaml` to reference the new script.** Add or update the `unit-tests-script:` key with the path to the newly created script (e.g., `unit-tests-script: test_scripts/run_unittests_.sh`). This is mandatory — the renderer needs this reference to invoke the unit test script during the development workflow. 7. **Link the script into the base `.plain` files as a linked resource.** Once the script exists on disk, it must be referenced from the project's base `.plain` module(s) into its ***implementation reqs*** section using the `add-resource` skill. After linking, read the spec back to confirm the link path resolves to the script you just wrote. ## Anti-Patterns @@ -131,6 +131,6 @@ Notes: - Don't write a cross-shell hybrid (e.g. a `.sh` that detects PowerShell, or vice versa). Ship one script per shell, named with the appropriate extension. - Don't install dependencies into the user's global location (`~/.m2`, system-wide `pip`, `~/.cargo`, etc.). Always isolate inside `$WORKING_FOLDER` so concurrent runs and other projects can't interfere. - Don't run the test command without first verifying the install step succeeded. A failed install followed by a "test" run produces misleading errors that look like test failures. -- **Don't forget to update `config.yaml`.** After creating the unit test script, always add or update the `unit-tests-script:` key in `config.yaml` to reference the new script. Without this entry, the `codeplain` renderer won't know where to find the unit test script. +- **Don't forget to update `config.yaml`.** After creating the unit test script, always add or update the `unit-tests-script:` key in `config.yaml` to reference the new script. Without this entry, the renderer won't know where to find the unit test script. - **Don't forget to link the script as a resource in the base `.plain` files.** A script that is referenced only from `config.yaml` is invisible to the spec contract — the renderer treats `unit-tests-script:` as a build-system pointer, not as part of the test-req contract the model reads. Use the `add-resource` skill to add a markdown link to the script from the `.plain` module that owns the unit-testing test reqs (see Workflow step 7). Omitting the linked resource means the model authors and reviews test-related code without ever seeing the actual runner it has to satisfy. - **Don't write a terse script.** Silent steps, `>/dev/null` redirects on the install output, missing `--version` prints, and absent failure banners all make the script harder to debug than the code it is testing. Follow the *Logging — be as verbose as possible* rule under [Conventions](#conventions) literally: every step announces itself, every command is echoed, every failure prints what was about to run and where. diff --git a/forge/skills/init-config-file/SKILL.md b/forge/skills/init-config-file/SKILL.md index d1d3e13..17d36b3 100644 --- a/forge/skills/init-config-file/SKILL.md +++ b/forge/skills/init-config-file/SKILL.md @@ -1,10 +1,10 @@ --- name: init-config-file description: >- - Build / finalize the `config.yaml` file(s) that the `codeplain` renderer - consumes. Pulls together every decision made during Phase 3 of `forge-plain` - (script paths, template directory, build folders, copy/dest behavior, log - settings) and emits one canonical `config.yaml` per part of the project. + Build / finalize the `config.yaml` file(s) that the renderer consumes. + Pulls together every decision made during Phase 3 of `forge-plain` (script + paths, template directory, build folders) and emits one canonical + `config.yaml` per part of the project. Run this at the **end of `forge-plain`** (just before `plain-healthcheck`), at the end of `add-feature` whenever the testing surface or template directory changed, and any time the user wants to regenerate / consolidate @@ -13,9 +13,7 @@ description: >- # Init Config File -Always use the skill `load-codeplain-reference` to retrieve the full `codeplain` CLI reference (every flag, the `config.yaml` mapping, and the secrets / per-invocation keys that must never be written to config) — but only if you haven't done so yet. This skill assembles `config.yaml` from the valid CLI-derived keys, so the CLI reference is required. - -This skill is the **single authoritative writer** of `config.yaml` for a ***plain project. Anything that ends up in `config.yaml` should go through this skill. The renderer (`codeplain`) reads each key listed below into the same argparse namespace it uses for CLI flags — so a value set in `config.yaml` is exactly equivalent to passing the corresponding `--flag` on the command line. +This skill is the **single authoritative writer** of `config.yaml` for a ***plain project. Anything that ends up in `config.yaml` should go through this skill. The renderer reads it to find the project's test scripts, templates, and output folders. ## When to run @@ -31,139 +29,35 @@ If you only fixed a typo inside a `.plain` file and the testing surface didn't m 1. Determines **how many** `config.yaml` files the project needs (one per part — see [Per-part split](#per-part-split)). 2. For each config, gathers the decided values from the current project state (existing scripts under `test_scripts/`, the template directory, the build/dest folder choices). 3. Emits a clean, alphabetically-grouped `config.yaml` containing **only** keys that are actually in use, using the canonical key names from the [Valid keys reference](#valid-keys-reference). -4. Verifies that every `*-script` value points at a file that exists on disk under `test_scripts/` (or wherever the user placed it), using the same resolution rule the renderer uses: an absolute path (or a `~` path) is used as-is; a relative path in `config.yaml` resolves against the **config file's directory**. There is no other fallback — the renderer fails fast with a `FileNotFoundError` when a script path doesn't resolve to an existing file. +4. Verifies that every `*-script` value points at a file that exists on disk under `test_scripts/` (or wherever the user placed it): an absolute path (or a `~` path) is used as-is; a relative path in `config.yaml` resolves against the **config file's directory**. There is no other fallback. 5. Hands off to `plain-healthcheck` for the full validation pass. ## What this skill does NOT do - It does **not** generate testing scripts. Use `implement-unit-testing-script`, `implement-conformance-testing-script`, or `implement-prepare-environment-script` first; this skill only wires them in. -- It does **not** decide *whether* the user wants conformance tests, a prepare-environment script, copy-build, etc. Those decisions belong to `forge-plain` Phase 3. This skill only **records** them. +- It does **not** decide *whether* the user wants conformance tests, a prepare-environment script, etc. Those decisions belong to `forge-plain` Phase 3. This skill only **records** them. - It does **not** invent values for keys whose decisions weren't made — it leaves them out (the renderer falls back to its default) rather than guessing. -- It does **not** write secrets. `api-key` belongs in the `CODEPLAIN_API_KEY` environment variable, never in `config.yaml`. +- It does **not** write secrets. API keys and tokens belong in environment variables, never in `config.yaml`. ## Valid keys reference -The canonical list of keys is derived from the `codeplain` CLI argparse parser. Every key below corresponds to exactly one `--flag` and is read by the renderer through `update_args_with_config`. **No other keys are valid** — the renderer rejects unknown keys with `parser.error(f"Invalid argument: {key}")`. - -YAML keys use the **dashed** form (e.g. `unittests-script`, not `unittests_script`) to mirror the CLI flag spelling. The only exception that has historically appeared with underscores is `template_dir`; prefer `template-dir` for new configs but accept either when reading an existing file. +Only the keys below are valid. -### Keys you typically include +YAML keys use the **dashed** form (e.g. `unittests-script`, not `unittests_script`). The only exception that has historically appeared with underscores is `template_dir`; prefer `template-dir` for new configs but accept either when reading an existing file. -These keys reflect choices made in Phase 3 of `forge-plain` and are the bread and butter of a project's `config.yaml`: +These keys reflect choices made in Phase 3 of `forge-plain`: | Key | Type | Default | When to include | |---|---|---|---| -| `unittests-script` | path (string) | — | **Required.** Every project gets a unit-test runner. A relative path resolves against the config file's directory; the renderer fails fast if the file doesn't exist. | +| `unittests-script` | path (string) | — | **Required.** Every project gets a unit-test runner. A relative path resolves against the config file's directory. | | `conformance-tests-script` | path (string) | — | Include when the user opted into conformance testing in Phase 3. | | `prepare-environment-script` | path (string) | — | Include only when both (a) the user opted into a prepare-environment script and (b) `conformance-tests-script` is also set. Setting prepare without conformance is a hard `plain-healthcheck` failure. | | `test-script-timeout` | int (seconds) | `120` | Include only when the user explicitly raised/lowered the default. | -| `template-dir` | path (string) | — | Include whenever the project has an `import` module or a custom template directory (e.g. `template/`). Required for projects with shared templates. | -| `logging-config-path` | path (string) | `logging_config.yaml` | Points at a **separate** YAML file consumed by Python's `logging.config.dictConfig`. This is the only knob that lets the user actually change log **levels** for the renderer and its dependencies. See [Logging configuration](#logging-configuration) below. Include the key explicitly whenever the project ships a non-default logging config; leave it out only when the user is happy with the renderer's defaults (`INFO` root, `WARNING` for `git`, `ERROR` for `transitions`). | +| `template-dir` | path (string) | — | Include whenever the project has an `import` module or a custom template directory (e.g. `template/`). Required for projects with shared templates. `plain-parser check` reads it too, so `plain-healthcheck` finds the templates. | | `build-folder` | string | `plain_modules` | Include only when the user picked a non-default folder name. Must differ from `build-dest`. The renderer writes `//code` (implementation + unit tests) and `//tests` (conformance tests, one folder per functional spec). | -| `build-dest` | string | `dist` | **Always include with the value `dist`.** This skill pins the copy destination explicitly so every project's `config.yaml` has the same, predictable target folder for the post-render copy. Even though `dist` matches the renderer's default, we still write it out so the choice is visible in the file and protected against future default changes. Must differ from `build-folder`. | -| `base-folder` | string | — | Include when the user wants build output rooted somewhere other than the project root. | - -### Keys you occasionally include - -These are useful but the defaults are almost always fine. Only include them when the user explicitly changed the default during Phase 3: - -| Key | Type | Default | Notes | -|---|---|---|---| -| `copy-build` | bool | `true` | The renderer copies the rendered code to `build-dest` after a successful render. Set to `false` only when the user doesn't want this. | -| `copy-conformance-tests` | bool | `false` | Requires `conformance-tests-script` to also be set. | -| `conformance-tests-dest` | string | `dist_conformance_tests` | Target folder for the post-render copy of `plain_modules//tests`. Must differ from `build-folder`. | -| `log-to-file` | bool | `true` | Disable only when the user explicitly does not want a log file. Controls whether logs are mirrored to disk — it does **not** set the log level (that's `logging-config-path`'s job). | -| `log-file-name` | string | `codeplain.log` | If `log-to-file` is `false`, this key must be left out. A relative value set here resolves against the config file's directory; when the key is left out entirely, the default resolves against the `.plain` file's directory. | -| `render-machine-graph` | bool | `false` | Include only when the user wants the state-machine graph rendered. | -| `headless` | bool | `false` | Include only when the project is meant to run in CI / non-interactive mode by default. | -| `force-render` | bool | `false` | Almost never belongs in `config.yaml`; prefer the CLI flag for one-off forced renders. | -| `verbose` | bool | `false` | Almost never belongs in `config.yaml`; prefer the CLI flag for one-off verbose runs. | -| `api` | URL (string) | `https://api.codeplain.ai` | Include only when the user is pointing at a non-default API endpoint. | - -### Keys you must NEVER include - -These flags are **per-invocation** or **secret**. Putting them in `config.yaml` is always wrong: - -| Key | Why it doesn't belong | -|---|---| -| `api-key` | Secret. Belongs in the `CODEPLAIN_API_KEY` environment variable. Never in a file the user might commit. | -| `dry-run` | Per-invocation. `plain-healthcheck` runs the dry-run explicitly; pinning it in the config would make a real render impossible. | -| `full-plain` | Per-invocation preview. Mutually exclusive with `dry-run`. | -| `render-range` | Per-invocation. Selects a slice of functionalities to (re)render. | -| `render-from` | Per-invocation. Mutually exclusive with `render-range`. | -| `replay-with` | Internal / debugging flag. | -| `config-name` | Refers to the config file itself — the renderer ignores it when reading the config. | -| `filename` | The `.plain` file to render is always passed positionally on the CLI. | - -If the user asks to put any of these in `config.yaml`, refuse and explain why. - -## Logging configuration - -Log **levels** are not controlled directly by `config.yaml` — they live in a separate YAML file that the renderer feeds to Python's `logging.config.dictConfig`. The `config.yaml` key `logging-config-path` is the pointer that wires the two together. - -### How the renderer assembles logging - -From `setup_logging` in the `codeplain` source: - -1. The renderer first installs a set of **baseline levels**: - - root logger → `INFO` - - `LOGGER_NAME` (the renderer's own logger) → `INFO` - - `git` → `WARNING` - - `transitions` → `ERROR` - - `transitions.extensions.diagrams` → `ERROR` -2. **If** `args.logging_config_path` resolves to an existing file on disk, the renderer loads that YAML and calls `logging.config.dictConfig(...)` on it. This overlays anything from step 1 — the user can raise, lower, or add levels for any logger they care about, add handlers, change formatters, etc. -3. The renderer then attaches its own handlers (TUI handler unless `headless`, file handler if `log-to-file`, crash buffer otherwise). Whatever **level** the root logger ended up at after step 2 is the level those handlers respect. - -In other words: `logging-config-path` is the **only** knob that changes the levels. `log-to-file` and `log-file-name` only control *whether and where* logs are written — not *what* gets written. - -### Default behavior - -- The CLI default value for `logging-config-path` is `logging_config.yaml`, resolved against the **`.plain` file's directory** (like every defaulted path parameter). If a file by that name exists next to the plain file, it will be loaded automatically — even without `logging-config-path` being set in `config.yaml`. A value set in `config.yaml` resolves against the config file's directory; a value passed on the CLI resolves against the invocation directory. -- If the file does not exist, the renderer silently keeps the baseline levels from step 1 above (no warning). -- If the file exists but fails to parse / apply, the renderer warns (`Failed to load logging configuration from …`) and falls back to the baseline. - -This means **the mere presence of a `logging_config.yaml` file is itself a config decision.** When you assemble a project's `config.yaml`, you have three cases: - -| Situation | What to do | -|---|---| -| The user is happy with the baseline levels and the project has no `logging_config.yaml` on disk. | Leave `logging-config-path` out of `config.yaml`. | -| The user wants custom levels and is fine with the file being named `logging_config.yaml` next to the `.plain` file. | Create that file (see [Recommended logging config](#recommended-logging-config) below). Leaving `logging-config-path` out of `config.yaml` is fine — the default points at it already — but explicitly setting `logging-config-path: logging_config.yaml` is also acceptable and makes the dependency visible to anyone reading the config. | -| The user wants the logging config file to live somewhere non-default (different filename or directory). | Create the file at the chosen path and set `logging-config-path: ` in `config.yaml`. | - -When in doubt, ask the user: "Do you want to change the default log levels (INFO for the renderer, WARNING for git, ERROR for transitions), or stick with the defaults?" Only generate / pin the file when they say yes. - -### Recommended logging config - -When the user does want custom levels, write a minimal `dictConfig`-style YAML file. Example with two common knobs (verbose renderer logs, plus suppression of a chatty third-party logger): - -```/dev/null/logging_config.yaml.example#L1-15 -version: 1 -disable_existing_loggers: false -formatters: - default: - format: "%(levelname)s:%(name)s:%(message)s" -handlers: - console: - class: logging.StreamHandler - formatter: default - level: DEBUG -loggers: - codeplain: - level: DEBUG - urllib3: - level: WARNING -root: - level: INFO - handlers: [console] -``` - -Guidelines for what to put in this file: +| `build-dest` | string | `dist` | **Always include with the value `dist`.** This skill pins the output destination explicitly so every project's `config.yaml` has the same, predictable target folder, visible in the file. Must differ from `build-folder`. | -- Always set `version: 1` — `dictConfig` requires it. -- Set `disable_existing_loggers: false` unless the user explicitly wants to silence loggers that were created before `dictConfig` ran. The renderer creates several before it loads this file, and disabling them by default leads to confusing dead silence. -- Only override levels the user actually asked about. Don't preemptively add every logger the codebase touches — that creates ongoing maintenance for no benefit. -- Don't put the `LoggingHandler` / `CrashLogHandler` / `FileHandler` here — the renderer attaches those itself after `dictConfig` runs. Adding them here will cause duplicate log lines. -- This file is **not** validated by `plain-healthcheck`. If you change it, ask the user to confirm by reading it back to them. +Never put secrets or per-invocation options in `config.yaml`. If the user asks to, refuse and explain why. ## Per-part split @@ -183,7 +77,7 @@ Before emitting anything, state the planned split to the user (e.g. "I'll emit ` 2. For each top module, determine which part it belongs to (single-stack → one part; multi-part → one part per top module). 3. List every script under `test_scripts/` and group them by part (e.g. `*_python.sh` belongs to the backend part, `*_js.sh` belongs to the frontend part). 4. Identify the template directory (typically `template/`) and any custom resource directories (typically `resources/`). -5. Read any **existing** `config.yaml` in each part's directory — preserve any user-set fields not listed in [Valid keys reference](#valid-keys-reference) only with the user's explicit approval, and warn that unknown keys will be rejected by the renderer. +5. Read any **existing** `config.yaml` in each part's directory — preserve any user-set fields not listed in [Valid keys reference](#valid-keys-reference) only with the user's explicit approval. ### Step 2 — Assemble per-part values @@ -194,22 +88,19 @@ For each part: 3. If the part has a conformance script on disk → add `conformance-tests-script: …`. 4. If the part has a prepare-environment script on disk → first verify `conformance-tests-script` is also being added; if not, stop and surface this to the user (offer to either generate the missing conformance script via `implement-conformance-testing-script` or drop the prepare-environment script). 5. If the project has shared templates → add `template-dir: template` (or whatever path the user used). -6. **Always add `build-dest: dist`.** This skill pins the copy destination on every config it writes, regardless of what Phase 3 said about it. If Phase 3 explicitly asked for a different `build-dest`, stop and surface the conflict to the user — do not silently honor the override. +6. **Always add `build-dest: dist`.** This skill pins the output destination on every config it writes, regardless of what Phase 3 said about it. If Phase 3 explicitly asked for a different `build-dest`, stop and surface the conflict to the user — do not silently honor the override. 7. For every other key in [Valid keys reference](#valid-keys-reference), include it **only** if Phase 3 produced a non-default decision for that key. 8. Cross-validate the assembled key set: - `build-dest` is set to `dist`. - `build-folder` ≠ `build-dest` (in particular, `build-folder` is never `dist`). - - `conformance-tests-folder` ≠ `conformance-tests-dest`. - - `copy-conformance-tests: true` requires `conformance-tests-script`. - - `log-file-name` is set ⇒ `log-to-file` is not `false`. - - All `*-script` paths resolve on disk (absolute / `~` paths as-is; relative paths against the config file's directory — no other fallback; the renderer fails fast on a missing script). + - All `*-script` paths resolve on disk (absolute / `~` paths as-is; relative paths against the config file's directory — no other fallback). - No script path crosses stacks (e.g. `backend/config.yaml` must not reference `run_unittests_js.sh`). ### Step 3 — Emit `config.yaml` For each part, write a clean YAML file: -- One key per line, in the order they appear in [Valid keys reference](#valid-keys-reference) (script paths first, then template/build folders, then copy/log settings). +- One key per line, in the order they appear in [Valid keys reference](#valid-keys-reference) (script paths first, then template/build folders). - Use dashed key names. Quote string values only when YAML requires it. - No comments inside the file — keep it machine-parseable. If the user needs a comment, put it in the surrounding spec or README. - Idempotent: re-running this skill on an unchanged project produces a byte-for-byte identical file. @@ -241,10 +132,9 @@ Tell the caller exactly which file(s) were written and invoke `plain-healthcheck - **Inventing values for keys the user never decided on.** Leave them out and let the renderer use its default. - **Mixing stacks in one config.** `backend/config.yaml` referencing a JS script is always a bug — split into per-part configs instead. -- **Putting `api-key`, `dry-run`, `full-plain`, `render-range`, `render-from`, `replay-with`, `config-name`, or `filename` in `config.yaml`.** All of these are per-invocation or secret; the renderer treats them as command-line concerns. +- **Putting secrets or per-invocation options in `config.yaml`.** - **Emitting `prepare-environment-script` without `conformance-tests-script`.** A prepare-environment script only makes sense in service of conformance tests; without one, `plain-healthcheck` will fail. - **Hand-merging into an existing config.yaml without re-running this skill.** If the user edited the config manually, re-run the skill to re-derive a clean canonical version (after confirming any custom fields with the user). -- **Reading the renderer's API key from a project file.** Always rely on `CODEPLAIN_API_KEY`. ## Validation Checklist @@ -254,9 +144,7 @@ Tell the caller exactly which file(s) were written and invoke `plain-healthcheck - [ ] No `config.yaml` declares `prepare-environment-script` without also declaring `conformance-tests-script`. - [ ] No `config.yaml` mixes stacks (every script in it targets the same language). - [ ] `build-dest` is set to `dist` in every emitted `config.yaml`. -- [ ] `build-folder` ≠ `build-dest`; `conformance-tests-folder` ≠ `conformance-tests-dest`. -- [ ] `copy-conformance-tests: true` only when `conformance-tests-script` is set. -- [ ] `log-file-name` only when `log-to-file` is not `false`. -- [ ] No forbidden keys (`api-key`, `dry-run`, `full-plain`, `render-range`, `render-from`, `replay-with`, `config-name`, `filename`). +- [ ] `build-folder` ≠ `build-dest`. +- [ ] No secrets or per-invocation options. - [ ] `template-dir` set whenever the project has shared templates or import modules. - [ ] `plain-healthcheck` returned `PASS` after the config(s) were written. diff --git a/forge/skills/init-plain-project/SKILL.md b/forge/skills/init-plain-project/SKILL.md index 56b55e4..f0ea5cb 100644 --- a/forge/skills/init-plain-project/SKILL.md +++ b/forge/skills/init-plain-project/SKILL.md @@ -6,7 +6,7 @@ description: >- then scaffolds a template import module (with base implementation reqs and test reqs), a stub top-level module, the unit testing script, and (optionally) the conformance and prepare-environment scripts plus a - config.yaml. Does NOT run `codeplain --dry-run`. Use when the user wants a + config.yaml. Does NOT run `plain-healthcheck`. Use when the user wants a quick project skeleton to start writing functional specs against, without the full forge-plain interview. --- @@ -17,7 +17,7 @@ Always use the skill `load-plain-reference` to retrieve the ***plain syntax rule ## Scope -This skill is intentionally **minimal**. It produces a runnable project skeleton — not a complete spec. **No `***definitions***` sections are written by this skill — not in the template module, not in the top module.** No functional specs, no concepts, no acceptance tests, no dry-run validation. The user will fill those in afterwards with `add-feature`, `add-concept`, `add-functional-spec`, etc. +This skill is intentionally **minimal**. It produces a runnable project skeleton — not a complete spec. **No `***definitions***` sections are written by this skill — not in the template module, not in the top module.** No functional specs, no concepts, no acceptance tests, no validation. The user will fill those in afterwards with `add-feature`, `add-concept`, `add-functional-spec`, etc. What this skill writes: @@ -30,7 +30,7 @@ What this skill writes: What this skill does **not** do: -- Run `codeplain --dry-run` or `plain-healthcheck`. +- Run `plain-healthcheck`. - Author concepts, functional specs, or acceptance tests. - Probe the host environment (`check-plain-env` is a separate step the user can run later). @@ -111,7 +111,7 @@ template_dir: template Always include `template_dir: template` because the project has a template import module from step 3. Use `.sh` on macOS/Linux and `.ps1` on Windows, matching whatever the implement-* skills produced. -Do **not** invoke `init-config-file` or `plain-healthcheck` here — keeping this skill light is the whole point. The user can run them later if they want a strict canonicalization or a dry-run. +Do **not** invoke `init-config-file` or `plain-healthcheck` here — keeping this skill light is the whole point. The user can run them later if they want a strict canonicalization or validation. ### 7. Recap @@ -119,7 +119,7 @@ Tell the user what was created and what's next: - Files written: `template/base.plain`, `.plain`, the test scripts, `config.yaml`. - Suggested next steps: add concepts with `add-concept`, add functional specs with `add-functional-spec` or `add-functional-specs`, or jump straight into `add-feature`. -- Mention that `codeplain .plain --dry-run` has not been run — they can run `plain-healthcheck` when they want validation. +- Mention that the project has not been validated — they can run `plain-healthcheck` when they want validation. ## Validation checklist diff --git a/forge/skills/init-plain-project/references/checklist.md b/forge/skills/init-plain-project/references/checklist.md index 1640ed8..7c875c1 100644 --- a/forge/skills/init-plain-project/references/checklist.md +++ b/forge/skills/init-plain-project/references/checklist.md @@ -24,8 +24,8 @@ Use this to verify the scaffold was produced correctly — never as a substitute - [ ] Generated `implement-conformance-testing-script` only if conformance was enabled (activate-only variant when a prepare-environment script will also exist, otherwise install-inline). - [ ] Generated `implement-prepare-environment-script` only if conformance was enabled **and** the user opted in. - [ ] Wrote `config.yaml` at the project root with only the keys for scripts actually generated, plus `template_dir: template`, using the correct `.sh`/`.ps1` extension for the host OS. -- [ ] Did **not** run `init-config-file`, `plain-healthcheck`, `check-plain-env`, or `codeplain --dry-run` (out of scope for this skill). +- [ ] Did **not** run `init-config-file`, `plain-healthcheck`, or `check-plain-env` (out of scope for this skill). ## Recap -- [ ] Told the user what was written, suggested next steps (`add-concept`, `add-functional-spec(s)`, `add-feature`), and noted that no dry-run was run (they can run `plain-healthcheck` later). +- [ ] Told the user what was written, suggested next steps (`add-concept`, `add-functional-spec(s)`, `add-feature`), and noted that the project was not validated (they can run `plain-healthcheck` later). diff --git a/forge/skills/load-codeplain-reference/SKILL.md b/forge/skills/load-codeplain-reference/SKILL.md deleted file mode 100644 index 7ce66ab..0000000 --- a/forge/skills/load-codeplain-reference/SKILL.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -name: load-codeplain-reference -description: >- - Loads the full `codeplain` CLI reference into context: the render command and - its positional plain-file argument, every flag (render control, config, - folders, test-script wiring, output copying, API, logging, headless mode), - path-resolution rules, the CODEPLAIN_API_KEY environment variable, the - config.yaml mapping, and the success/failure render banners. Use whenever - running, configuring, resuming, or reasoning about a `codeplain` render or its - config.yaml. Not for the ***plain language syntax itself (use - load-plain-reference) or for supervising a live render (use run-codeplain). ---- - -# CODEPLAIN_CLI_REFERENCE.md - -`codeplain` is the CLI that renders `***plain` specification files into production-ready code. It reads a `.plain` module (and everything it `import`s / `requires`), calls the codeplain API, and writes generated code under `plain_modules//code/` and conformance tests under `plain_modules//tests/`. The `.plain` specs are the source of truth; the generated code is a read-only artifact. - -This reference covers the CLI surface only. For the `***plain` language itself use `load-plain-reference`; to supervise a live render use `run-codeplain`; to assemble or validate `config.yaml` use `init-config-file` / `plain-healthcheck`. - -## Usage - -```text -codeplain [options] filename -``` - -```text -usage: codeplain [-h] [--verbose] [--base-folder BASE_FOLDER] - [--build-folder BUILD_FOLDER] - [--log-to-file | --no-log-to-file] - [--log-file-name LOG_FILE_NAME] [--config-name CONFIG_NAME] - [--render-range RENDER_RANGE | --render-from RENDER_FROM] - [--force-render] [--unittests-script UNITTESTS_SCRIPT] - [--conformance-tests-folder CONFORMANCE_TESTS_FOLDER] - [--conformance-tests-script CONFORMANCE_TESTS_SCRIPT] - [--prepare-environment-script PREPARE_ENVIRONMENT_SCRIPT] - [--test-script-timeout TEST_SCRIPT_TIMEOUT] [--api [API]] - [--api-key API_KEY] [--full-plain] [--dry-run] - [--replay-with REPLAY_WITH] [--template-dir TEMPLATE_DIR] - [--copy-build] [--build-dest BUILD_DEST] - [--copy-conformance-tests] - [--conformance-tests-dest CONFORMANCE_TESTS_DEST] - [--render-machine-graph] - [--logging-config-path LOGGING_CONFIG_PATH] [--headless] - filename -``` - -## Positional argument - -- **`filename`** — path to the `.plain` file to render. The directory containing this file has the **highest precedence for template loading**, so custom templates placed there override the defaults (see `--template-dir`). Render the **top module** of the dependency chain — the module not `requires`-ed by any other; its `requires`/`import` graph is pulled in automatically. - -## Path resolution (read this before setting any path flag) - -A path's meaning depends on **where it was written**: - -- Values given **on the command line** resolve against the **current working directory**. -- Values read from **`config.yaml`** resolve against the **config file's directory**. -- **Default** values resolve against the **directory containing the `.plain` file**. -- **Absolute paths** (and paths starting with `~`) are used as-is. - -## Options - -### Render control - -- **`--dry-run`** — preview code generation without making any changes. This is the static-validation gate; `plain-healthcheck` runs `codeplain .plain --dry-run` for every top module before a real render. **Per-invocation only — never store in `config.yaml`.** -- **`--full-plain`** — full preview of the assembled `***plain` specification before code generation. Use to inspect the context of all `***plain` primitives that will be included to render the given module. **Per-invocation only — never in `config.yaml`.** -- **`--render-range RENDER_RANGE`** — render a range of functionalities (e.g. `1`, or `2,3`). A comma separates the start and end IDs; the range is inclusive of both. A single ID renders only that one functionality. Mutually exclusive with `--render-from`. **Per-invocation only — never in `config.yaml`.** -- **`--render-from RENDER_FROM`** — continue generation starting from this functionality ID (inclusive). The ID must match a functionality in the `.plain` file. This is how a run resumes after a spec fix — pass the **first functionality to re-render**. Mutually exclusive with `--render-range`. **Per-invocation only — never in `config.yaml`.** -- **`--force-render`** — force a re-render of all required modules, invalidating cached module renders. Use only when a backward dependency genuinely changed; it costs more credits. -- **`--replay-with REPLAY_WITH`** — replay a previous render. **Per-invocation only — never in `config.yaml`.** - -### Configuration - -- **`--config-name CONFIG_NAME`** — name of the config file to look for. It is looked up in the `.plain` file's directory and in the current working directory. Defaults to `config.yaml`. Use it for multi-part projects (one config per part) or a non-default config file name. - -### Folders - -- **`--base-folder BASE_FOLDER`** — base folder for the build files. -- **`--build-folder BUILD_FOLDER`** — folder for build files (generated code lands under here, per module: `plain_modules//code/` and `plain_modules//tests/`). -- **`--template-dir TEMPLATE_DIR`** — path to a custom template directory. Templates are searched in this order: 1) the directory containing the `.plain` file, 2) this custom template directory, 3) the built-in `standard_template_library` directory. In `config.yaml` the equivalent key is `template_dir` — set it whenever the project has import modules or templates. - -### Test-script wiring - -- **`--unittests-script UNITTESTS_SCRIPT`** — shell script that runs unit tests on the generated code. Receives the build folder path as its first argument (default: `plain_modules`). `config.yaml` key: `unittests-script`. -- **`--conformance-tests-folder CONFORMANCE_TESTS_FOLDER`** — folder for conformance test files. -- **`--conformance-tests-script CONFORMANCE_TESTS_SCRIPT`** — path to the conformance-tests shell script. It must accept two arguments: 1) a folder containing generated source code (e.g. `plain_modules/module_name`), and 2) a subfolder of the conformance-tests folder containing the test files (e.g. `conformance_tests/subfoldername`). `config.yaml` key: `conformance-tests-script`. -- **`--prepare-environment-script PREPARE_ENVIRONMENT_SCRIPT`** — path to a shell script that prepares the testing environment. It must accept the source-code folder path as its first argument. It runs once per render, only to warm the environment the conformance runner attaches to. `config.yaml` key: `prepare-environment-script`. If this is declared, `conformance-tests-script` must be declared too. -- **`--test-script-timeout TEST_SCRIPT_TIMEOUT`** — timeout for test scripts, in seconds. Defaults to 120 seconds. Raise it if the project's test scripts are slow. - -### Output copying - -- **`--copy-build`** — after a successful render, copy the rendered code in `--base-folder` to `--build-dest`. -- **`--build-dest BUILD_DEST`** — target folder for `--copy-build` (used only when `--copy-build` is set). -- **`--copy-conformance-tests`** — after a successful render, copy the conformance tests in `--conformance-tests-folder` to `--conformance-tests-dest`. Requires `--conformance-tests-script`. -- **`--conformance-tests-dest CONFORMANCE_TESTS_DEST`** — target folder for `--copy-conformance-tests` (used only when it is set). - -### API - -- **`--api [API]`** — alternative base URL for the API. Default: `https://api.codeplain.ai`. -- **`--api-key API_KEY`** — API key used to access the API. If not provided, the `CODEPLAIN_API_KEY` environment variable is used. **Secret — never store it in `config.yaml`.** - -### Logging & output mode - -- **`--verbose`, `-v`** — enable verbose output. Verbose logging captures each test script's stdout/stderr into the log file between the renderer's own wrapper lines, which is what makes a live render inspectable. Keep logging verbose. -- **`--log-to-file` / `--no-log-to-file`** — enable or disable logging to a file. Defaults to `True`; pass `--no-log-to-file` to disable. -- **`--log-file-name LOG_FILE_NAME`** — name of the log file. Defaults to `codeplain.log`. If a file already exists at the resolved path, it is **overwritten** by the current run's logs — the log is therefore per-run, and its default location is the `.plain` file's directory. -- **`--logging-config-path LOGGING_CONFIG_PATH`** — path to a logging configuration file. -- **`--headless`** — run without the TUI: no terminal output except a single render-started message; all logs are written to the log file. **Required for agent-supervised runs** — a terminal tool cannot drive the interactive TUI, so supervision reads `codeplain.log` instead. -- **`--render-machine-graph`** — render the state-machine graph. Use only on explicit request. -- **`-h`, `--help`** — show the help message and exit. - -## Environment variables - -- **`CODEPLAIN_API_KEY`** — the API key used when `--api-key` is not passed on the command line. Export it before rendering (or pass `--api-key`). It is a secret and must never be written into `config.yaml`. - -## `config.yaml` relationship - -`codeplain` reads persistent options from a `config.yaml` (or the file named by `--config-name`), looked up in the `.plain` file's directory and the current working directory. This avoids retyping stable options on every run. - -- **One `config.yaml` per part of the system that has its own test scripts.** A single-stack project has one at the root; a multi-part project (e.g. a Python backend and a React frontend) has one per part, each referencing only its own scripts. -- **Established keys:** `unittests-script`, `conformance-tests-script`, `prepare-environment-script`, `template_dir`, `base-folder`, `build-folder`, plus copy/log settings. `init-config-file` is the canonical assembler and the source of truth for the full valid key set; treat it as authoritative. -- **Constraint:** if `prepare-environment-script` is declared, `conformance-tests-script` must be declared too — a prepare script only exists in service of conformance. -- **Never put in `config.yaml`:** secrets (`api-key`) and per-invocation flags (`dry-run`, `full-plain`, `render-range`, `render-from`, `replay-with`). Supply these on the command line only. -- Remember the path-resolution rule above: a script path written in `config.yaml` resolves against the config file's directory, not the working directory. - -## Render output - -Generated artifacts (gitignored, read-only — never edit): - -- `plain_modules//code/` — the generated project for each `.plain` spec (implementation + unit tests). -- `plain_modules//tests//` — generated conformance tests, one subfolder per functionality. -- `codeplain.log` — the per-run log (overwritten each run). - -When a run ends, `codeplain` writes a result banner (to the log, and to the terminal outside headless mode): - -- **Success:** `✓ rendering succeeded` (or `rendering complete`), followed by a metadata block: render id, input file, generated code folder, functionalities count, used credits, render time. -- **Failure:** `✗ rendering failed`, followed by the same metadata block. - -## Test-script exit-code contract - -The three wired scripts (`unittests-script`, `conformance-tests-script`, `prepare-environment-script`) share an exit-code contract that `codeplain`, `plain-healthcheck`, and `check-plain-env` all branch on: - -- **`69`** — unrecoverable error (missing toolchain, bad arguments, cannot enter the working folder, install failed). -- **`1`** — the "no tests discovered" guard in the conformance runner (and bad usage in the unit-test runner). -- **any other non-zero** — propagated verbatim from the underlying test command (a real test failure). - -Author or modify these scripts only through the matching `implement-{unit-testing,conformance-testing,prepare-environment}-script` skill — never by hand. - -## Common invocation patterns - -```bash -# Static-validation gate before a real render (what plain-healthcheck runs) -codeplain .plain --dry-run - -# Full render of the top module -codeplain .plain - -# Multi-part project: render one part with its own config file -codeplain backend/api.plain --config-name config.yaml - -# Agent-supervised render: background + headless, watch codeplain.log -nohup codeplain .plain --headless > /dev/null 2>&1 & - -# Resume after a spec fix: re-render from functionality N (inclusive) -codeplain .plain --render-from - -# Re-render exactly one functionality -codeplain .plain --render-range - -# Force a full re-render (backward dependency changed) — costs more credits -codeplain .plain --force-render -``` - -## Common errors and signals - -- **`codeplain: command not found`** — the CLI is not installed or not on `PATH`. Confirm with `command -v codeplain`. -- **`401` / `403` / `unauthorized`** — `CODEPLAIN_API_KEY` is unset or invalid; export it or pass `--api-key`. This is an API/auth problem, not a spec problem. -- **`429` / `rate limit` / `quota`** — API throttling or credit limit; not a spec problem. -- **`Functional spec too complex!`** — a single functional spec implies more than 200 lines of code; break it down (`break-down-func-spec`). -- **`missing concept` / `unknown definition` / `cyclic` / `not defined` / `cannot resolve`** — a spec-graph error (a concept used before it is defined, a cycle, or a broken `import`/`requires` chain). A `--dry-run` catches these before spending credits. -- **`Traceback (most recent call last):`** — the renderer itself crashed (a bug), not a spec error; capture the stack for a bug report. - -## See also - -- `load-plain-reference` — the `***plain` language syntax and authoring rules. -- `run-codeplain` — supervising a live render (monitor loop, pathologies, resume). -- `plain-healthcheck` — the pre-render dry-run gate across every top module. -- `init-config-file` — canonical `config.yaml` assembly and the full valid key set. -- `implement-{unit-testing,conformance-testing,prepare-environment}-script` — authoring the wired test scripts. diff --git a/forge/skills/load-plain-reference/SKILL.md b/forge/skills/load-plain-reference/SKILL.md index 3f50fbb..b7ba05b 100644 --- a/forge/skills/load-plain-reference/SKILL.md +++ b/forge/skills/load-plain-reference/SKILL.md @@ -2,8 +2,8 @@ name: load-plain-reference description: >- Loads the applicable ***plain authoring rules and operational references for the current task. - Covers section ownership, concepts, modules, resources, integrations, rendering, testing, and - codeplain CLI behavior without loading unrelated guidance. Use when authoring, editing, reviewing, + Covers section ownership, concepts, modules, resources, integrations, rendering, and testing + without loading unrelated guidance. Use when authoring, editing, reviewing, or debugging .plain files, or before invoking another skill that reads or writes .plain content. --- @@ -18,7 +18,7 @@ rules, so every routing decision below inherits its verdict. If that skill reports a stale or unconfirmed install, tell the user clearly that **authoring on an outdated plain-forge is not recommended** and ask them to run `npx plain-forge update` before -continuing — the rules routed to below may not match what the current `codeplain` renderer expects. +continuing — the rules routed to below may not match what the current renderer expects. Give them the choice rather than blocking: if they decline, continue with the rules on disk and treat the stale install as the first suspect for anything that later behaves unexpectedly. @@ -81,7 +81,6 @@ Authoring rules remain authoritative if a reference appears to conflict with the |---|---| | Project layout, source-of-truth model, or template inclusion | `references/project-model.md` | | Rendering order, generated artifacts, conformance workflow, or test scripts | `references/rendering-and-testing.md` | -| `codeplain` path resolution or CLI options | Invoke `load-codeplain-reference` | ## 5. Continue with the task diff --git a/forge/skills/load-plain-reference/references/project-model.md b/forge/skills/load-plain-reference/references/project-model.md index 95e540b..72b74c3 100644 --- a/forge/skills/load-plain-reference/references/project-model.md +++ b/forge/skills/load-plain-reference/references/project-model.md @@ -19,7 +19,7 @@ plain_modules/ # Generated output, one directory per module /code/ # implementation and unit tests /tests/ # conformance tests, one folder per functional spec test_scripts/ # Unit, environment-preparation, and conformance runners -config.yaml # codeplain CLI configuration +config.yaml # renderer configuration ``` The exact template directory can be configured. Follow the module and linked-resource rules for diff --git a/forge/skills/plain-healthcheck/SKILL.md b/forge/skills/plain-healthcheck/SKILL.md index a8622a9..4fb3925 100644 --- a/forge/skills/plain-healthcheck/SKILL.md +++ b/forge/skills/plain-healthcheck/SKILL.md @@ -2,9 +2,9 @@ name: plain-healthcheck description: >- Verification gate for a ***plain project. Verifies that every `config.yaml` - exists, points at scripts that actually live in `test_scripts/`, and that - `codeplain .plain --dry-run` passes for every top module in - the project. Run this whenever anything in the project is finalized — + exists and points at scripts that actually live in `test_scripts/`, parses + every top module with `plain-parser check`, and checks every module for what + the parser can't judge (section ownership, bullets, linked resources). Run this whenever anything in the project is finalized — including (but not limited to) the end of `forge-plain`, the end of `add-feature`, after `debug-specs`, after any single-skill edit that finalizes a concept, functional spec, requirement, template, or config — @@ -13,13 +13,13 @@ description: >- # Plain Healthcheck -Always use the skill `load-codeplain-reference` to retrieve the full `codeplain` CLI reference (every flag, path-resolution rules, `config.yaml` mapping, render banners, exit codes), and the skill `load-plain-reference` to retrieve the ***plain syntax rules — but only if you haven't done so yet. This skill runs `codeplain … --dry-run` and validates every `config.yaml`, so the CLI reference is required. +Always use the skill `load-plain-reference` to retrieve the ***plain syntax rules — but only if you haven't done so yet. Step 3b checks every module against those rules, and they are needed to fix what `plain-parser` reports in Step 3a. ## 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 `forge-plain`** (Phase 4) — before presenting the render command. +- **End of `forge-plain`** (Phase 4) — before handing off the render target. - **End of `add-feature`** (Phase 3 final review) — before declaring the feature done. - **End of `debug-specs`** — 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-concept`, `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `add-test-requirement`, `add-acceptance-test`, `add-template`, `add-resource`, `resolve-spec-conflict`, `break-down-func-spec`, `consolidate-concepts`, `refactor-module`, `create-import-module`, `create-requires-module`, or any of the `implement-*-testing-script` skills. @@ -28,7 +28,7 @@ Run this skill **whenever anything in the ***plain project is finalized** and th 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" — `config.yaml`s, scripts, and specs can all drift between runs. The healthcheck is cheap; rendering against stale specs is expensive. +Do **not** skip this skill because "the healthcheck passed earlier" — `config.yaml`s, scripts, and specs can all drift between runs. The healthcheck is cheap; rendering against stale specs is expensive. ## Workflow @@ -36,7 +36,7 @@ The skill is a **detect → fix → re-run** loop. It does not stop at the first ### Step 1 — Inventory the project -1. List every `.plain` file in the repo root (and any subdirectories that contain `.plain` files). Build the module graph from each file's YAML frontmatter (`requires`, `import`). Verify the `requires` graph forms a tree: every module's multiple `requires` entries must lie on one root-to-tip ancestor path. +1. List every `.plain` file in the repo root (and any subdirectories that contain `.plain` files). Build the module graph from each file's YAML frontmatter (`requires`, `import`); Step 3a validates it. 2. Identify **top modules** — every module that is not `requires`-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. 3. List every `config.yaml` in the repo (root and per-part directories such as `backend/`, `frontend/`). 4. List every script under `test_scripts/`. @@ -52,7 +52,7 @@ For each `config.yaml` in the inventory, check **all** of the following. Collect 2. **At minimum `unittests-script` is present.** Every project gets a unit-test runner. 3. **For every script field that is present** (`unittests-script`, `conformance-tests-script`, `prepare-environment-script`): - The path is a string ending in `.sh` (macOS/Linux) or `.ps1` (Windows). The extension must match the rest of the project — do not mix `.sh` and `.ps1` in a single config. - - The referenced file actually exists on disk, resolved the way the renderer resolves it: absolute / `~` paths as-is, relative paths against the **config file's directory** (typically landing under `test_scripts/`). There is no other fallback — the renderer fails fast with a `FileNotFoundError` on a script path that doesn't resolve, so a path that only "works" from some other directory is a failure here. + - The referenced file actually exists on disk: absolute / `~` paths as-is, relative paths against the **config file's directory** (typically landing under `test_scripts/`). There is no other fallback, so a path that only "works" from some other directory is a failure here. - On Unix, the script has the executable bit set (`-x`). If not, that is a fixable failure. 4. **No mixed stacks per config.** Every script referenced from a single `config.yaml` must target the same language/stack. For example, `backend/config.yaml` should not reference `run_unittests_js.sh`. If a config crosses stacks, that is a failure — the project should have been split into multiple configs per the applicable module rules loaded by `load-plain-reference`. 5. **No dangling fields.** Any `*-script` field whose target file does not exist is a failure. @@ -68,46 +68,54 @@ For each failure, record the offending config path, the offending field, and the Anything else (missing script, mixed stacks, missing `config.yaml`) 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 +### Step 3 — Validate every module -For each `(top_module, config.yaml)` pair from Step 1, run a dry-run from the project root via the `terminal` tool: +Step 3 has two halves: a mechanical parse by `plain-parser`, then the checks that need judgement. Collect every failure from both — do **not** stop at the first. -```bash -codeplain .plain --dry-run -``` +#### 3a. Parse every top module with `plain-parser` -**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: +[`plain-parser`](https://pypi.org/project/plain-parser/) is the ***plain parser. It parses a module together with its whole `import` / `requires` chain, so running it on every top module covers every module in the project. -- **`--config-name `** — required whenever the governing config file is not the default `config.yaml`, or when the project has multiple `config.yaml`s and the dry-run is being launched from somewhere that isn't the part's directory. `--config-name` takes a *file name*, not a path; if needed, `cd` into the part's directory before running so the right `config.yaml` is found. -- **`--template-dir `** — only when templates live outside `template/` **and** `template_dir` is not already set in the relevant config. -- **`--full-plain`** — useful when an `import`/`requires` chain is suspect. -- **`--verbose` / `-v`** — strongly recommended on a failed dry-run; the extra log output usually pinpoints the offending `.plain` file and spec. +1. **Check it is installed.** Run `plain-parser --help`. If the command is not found, stop and report `FAIL` with the install command (`pipx install plain-parser` or `uv tool install plain-parser`; it needs Python 3.11 or newer). Do **not** fall back to checking these rules by reading the files — without the parser there is no syntax gate. +2. **Run it from the top module's own directory**, once per top module: -Treat the dry-run as a hard gate: the healthcheck **only passes** when every top module's dry-run exits successfully. + ```bash + cd && plain-parser check .plain + ``` -#### When a dry-run fails + Add `--config-name ` when the governing config is not named `config.yaml`. `plain-parser` reads `template-dir` from the config next to the `.plain` file or in the working directory, and exits `2` when it finds one in both — running from the module's own directory avoids that in multi-part projects. +3. **Read the result.** Exit `0` prints `: OK`. Exit `1` prints `Error: …` on stderr — record that message verbatim as a failure against the top module. Exit `2` is a usage or config error (ambiguous or malformed config) — record it against the config. -Iterate until it passes: +`plain-parser` checks, among other things: frontmatter, that every `import` / `requires` entry exists and the graph has no cycles, that `requires` modules have one fixed order, that imported modules have no functional specs, section headings (allowed, not duplicated, `***definitions***` first, `***acceptance tests***` nested under a functional spec), concepts (defined, exported and required correctly), that a module with functional specs has implementation reqs, and that every linked resource and template resolves to an existing, relative, non-`.plain` file. It does not always report every error in one run, so after fixing what it reported, run it again until it prints `OK`. -1. Read the error output. If the first run was not verbose, immediately re-run with `--verbose`. Identify the offending `.plain` 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 `import`/`requires`, missing config field, etc. -2. Fix only the `.plain` files (or the relevant `config.yaml` / template) using the appropriate edit skill — `add-concept`, `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `resolve-spec-conflict`, `break-down-func-spec`, `consolidate-concepts`, or an inline edit. **Never** modify generated code under `plain_modules//code/` or `plain_modules//tests/`. -3. If you are uncertain about ***plain syntax for the failing construct, re-load `load-plain-reference` before fixing. -4. Re-run the same `codeplain .plain --dry-run …` command with the same flags. Repeat until it exits successfully. +#### 3b. Check what the parser can't judge -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. +Check every `.plain` module against the rules loaded by `load-plain-reference`: + +1. **Section ownership.** No fact sits in a section that doesn't own it — `:UnitTests:` facts only in `***implementation reqs***`, `:ConformanceTests:` facts only in `***test reqs***`, technology only in `***implementation reqs***`. +2. **Bullets.** Every line in every section is a valid `- ` list item or a correctly indented sub-bullet — no bare continuation lines. +3. **Linked resources.** Every linked resource is a single **text** file (not a binary), and no URL or folder path appears in the surrounding spec text unless it is a runtime value the generated software itself uses. + +Functional-spec complexity is checked when a spec is authored (`add-functional-spec(s)` → `analyze-if-func-spec-too-complex`). Re-run `analyze-if-func-spec-too-complex` here only for a spec that was hand-edited since it was authored. -#### Environment failures +Treat 3a and 3b together as a hard gate: the healthcheck **only passes** when `plain-parser check` prints `OK` for every top module and every module passes every check in 3b. -If `codeplain` is not on PATH, or `CODEPLAIN_API_KEY` is not set: +#### When a check fails -- 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. +Iterate until it passes: + +1. Identify the offending `.plain` file, the line, and the kind of issue: missing or duplicated concept, cyclic definition, broken `import`/`requires`, misplaced fact, bare continuation line, missing resource or template, etc. +2. Fix only the `.plain` files (or the relevant `config.yaml` / template) using the appropriate edit skill — `add-concept`, `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `resolve-section-ownership`, `resolve-spec-conflict`, `break-down-func-spec`, `consolidate-concepts`, or an inline edit. **Never** modify generated code under `plain_modules//code/` or `plain_modules//tests/`. +3. If you are uncertain about ***plain syntax for the failing construct, re-load `load-plain-reference` before fixing. +4. Re-run the failed checks — for a `plain-parser` failure, re-run `plain-parser check`. Repeat until they pass. + +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. ### Step 4 — Report Emit one of: -- **`PASS`** — followed by a short summary of what was checked: `N config.yaml(s) validated`, `M top module(s) dry-run`, `K scripts referenced`. The caller (`forge-plain`, `add-feature`, `debug-specs`) can then continue to its hand-off step. +- **`PASS`** — followed by a short summary of what was checked: `N config.yaml(s) validated`, `M module(s) checked (plain-parser OK for every top module)`, `K scripts referenced`. The caller (`forge-plain`, `add-feature`, `debug-specs`) can then continue to its hand-off step. - **`FAIL`** — followed by a numbered list of every unresolved problem. Each entry must include: the file (config / `.plain` / 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. @@ -115,7 +123,7 @@ The verdict goes on the first line so callers can pattern-match without parsing ## What this skill does NOT do - It does **not** author new specs from scratch — use `forge-plain` / `add-feature` for that. -- It does **not** run the real render — only `--dry-run`. +- It does **not** render anything. - It does **not** execute the testing scripts (`run_unittests`, `run_conformance_tests`, `prepare_environment`). It only verifies that the scripts are wired up correctly via `config.yaml`. The user runs the scripts themselves. - It does **not** silently regenerate config files or scripts. The most it does is `chmod +x` and (with user approval) re-invoke the relevant `implement-*-testing-script` skill. @@ -128,5 +136,6 @@ The verdict goes on the first line so callers can pattern-match without parsing - [ ] No `config.yaml` mixes stacks (e.g. Python + JS scripts in the same file) - [ ] No `config.yaml` declares `prepare-environment-script` without also declaring `conformance-tests-script` - [ ] Every `test_scripts/*` file is referenced by some `config.yaml` (or surfaced as a warning) -- [ ] `codeplain .plain --dry-run` exits successfully for **every** top module, with the right `--config-name` for multi-part projects +- [ ] `plain-parser check` printed `OK` for every top module, run from that module's directory +- [ ] Every module passes every check in Step 3b (section ownership, bullets, linked resources) - [ ] Verdict (`PASS` / `FAIL`) is on the first line, with a numbered list of remaining problems if it failed diff --git a/forge/skills/run-codeplain/SKILL.md b/forge/skills/run-codeplain/SKILL.md deleted file mode 100644 index face6c6..0000000 --- a/forge/skills/run-codeplain/SKILL.md +++ /dev/null @@ -1,483 +0,0 @@ ---- -name: run-codeplain -description: >- - Launch a `codeplain` render and supervise it end-to-end. Tails the log file, - watches generated code appear under `plain_modules/`, and inspects the live - TUI/process. Detects pathologies (stuck conformance loops, complexity errors, - missing concepts, render failures), and — with user approval — stops the - renderer, hands off to a spec-edit skill, and resumes with `--render-from`. - Use whenever the user wants to actually render specs (not just dry-run) and - wants supervision of the run. ---- - -# Run Codeplain - -Always use the skill `load-codeplain-reference` to retrieve the full `codeplain` CLI reference (every flag, path-resolution rules, `config.yaml` mapping, render banners, exit codes), and the skill `load-plain-reference` to retrieve the ***plain syntax rules — but only if you haven't done so yet. This skill drives the `codeplain` CLI, so the CLI reference is required before launching or resuming any render. - -## What this skill is - -A **supervisor** around a live `codeplain` render. The renderer itself is the codeplain CLI; this skill does not generate code. It: - -1. Launches `codeplain .plain` (or attaches to an in-flight run). -2. Watches three signal sources in a tight loop, in priority order: - - **`codeplain.log`** — the primary signal. Everything the renderer is doing, deciding, retrying, or failing at gets written here. This is the file you stare at. - - the generated **code outputs** under `plain_modules//` and `plain_modules//tests/` — a secondary signal, used to corroborate or contradict what the log says. - - the **process / TUI** itself (alive? exited? exit code?) — a tertiary signal, used only to know whether to keep monitoring. -3. Surfaces what is happening to the user in plain English, in near-real-time. -4. On pathology, stops the renderer (with user approval), hands off to the right edit skill, and resumes from the last completed functionality. - -This skill is **read-mostly**. It does not modify `.plain` files itself — it delegates to `debug-specs`, `resolve-spec-conflict`, `break-down-func-spec`, `add-implementation-requirement`, etc. The only files it writes are control files: it may `kill` a process, and it shells out to `codeplain` again to resume. - -**The log is the ground truth.** The TUI is opaque to the agent; the filesystem moves slowly and only on completed steps; but `codeplain.log` updates the moment the renderer takes any action — API call, retry, fix attempt, test run, abort. If the log and the filesystem disagree, trust the log. If the log and the TUI disagree (per the user's description), trust the log. - -## When to use - -- The user says "render it", "let's run codeplain", "kick off the render", "render `.plain`", or similar. -- The user already has a render going in another terminal and wants you to babysit it. -- After `forge-plain` / `add-feature` / `debug-specs` / `plain-healthcheck` finished and the next move is the real render. - -Do **not** use this skill for: - -- A dry-run gate — that is `plain-healthcheck`. -- Authoring or editing specs — that is `forge-plain`, `add-feature`, `debug-specs`, or the specific `add-*` / `resolve-*` / `break-down-*` skills. - -## Pre-flight (always do this first) - -Before launching anything: - -1. **Confirm the target.** Ask the user which top module they want rendered if it is not unambiguous from the conversation. The argument is the path to a `.plain` file (e.g. `agent_harness.plain`, `backend/api.plain`). -2. **Run `plain-healthcheck` if it hasn't been run since the last spec edit.** A real render that fails on something a dry-run would have caught burns credits. If healthcheck FAILs, stop here and surface the failures — do not start the render. -3. **Verify the environment.** - - `codeplain` is on `PATH` (`command -v codeplain`). - - `CODEPLAIN_API_KEY` is exported, **or** the user will pass `--api-key`. - - The governing `config.yaml` for the chosen module exists (use the pairing rule from `plain-healthcheck`: same-directory config wins; root `config.yaml` is the fallback). -4. **Decide on flags with the user.** Default to a minimal command line; mention these knobs only if they apply: - - `--config-name ` — multi-part project, or non-default config file name. - - `--render-range ` / `--render-from ` — partial render, or resume from a previous run. - - `--force-render` — invalidate cached module renders. - - `--template-dir ` — templates outside `template/` and not pinned in config. - - `--copy-build` / `--build-dest` — copy generated code somewhere outside `plain_modules/`. - - `--copy-conformance-tests` / `--conformance-tests-dest` — same for conformance tests. - - `--test-script-timeout ` — if test scripts are slow. - - `--api-key ` / `--api ` — only if the env var route is not in use. - - `--render-machine-graph` — only on explicit request. - - `--headless` — see "Launch mode" below. - - Do **not** prompt for flags the user did not signal interest in; the empty command line is the right default. - -5. **Locate and reset the log file.** By default it is `codeplain.log` in the same directory as the `.plain` file. If `--log-file-name` was overridden, the override resolves based on **where it was written**: a relative value on the CLI resolves against the invocation directory; a relative value in `config.yaml` resolves against the config file's directory; absolute and `~` paths are used as-is. **The file is overwritten** at the start of each run (per the `--log-file-name` help text). So: - - Before launch, note the **absolute path** of the log file and its current size (`wc -c `) — this lets you detect the moment the new run truncates and starts writing. - - After launch, every log read must be against this same path. Re-derive it (using the resolution rule above) if the user passes `--log-file-name`, sets `log-file-name` in `config.yaml`, or runs from a different working directory. - - The log is **per-run**: every byte you read during this run was written by this run. You don't have to worry about stale lines from prior runs. - -6. **Read every test script referenced by the governing `config.yaml`.** This is non-negotiable. The scripts are the contract between the renderer and the project; you cannot judge what the renderer is "fixing" without knowing what `pass` and `fail` actually mean in this project. For each of `unittests-script`, `prepare-environment-script`, and `conformance-tests-script` (those that are declared), read the script end-to-end **before** launching, and write down for yourself: - - - **Which framework is invoked** (e.g. `vitest`, `jest`, `pytest`, `go test`, `cargo test`, `phpunit`, etc.) and any framework flags (`--noEmit`, `--reporter=verbose`, custom configs). - - **What "pass" means**, exit-code-wise. Some scripts combine multiple checks (e.g. `unit_testing_typescript.sh` here runs `tsc --noEmit` *and* `vitest`, and its final exit code is the worst of the two — so a type error alone fails the unit-test gate). - - **What "fail" means**, including any failures that the script deliberately downgrades to warnings (e.g. `prepare_environment_typescript.sh` here treats a failed `npm run build` as a non-fatal warning). Failures the script swallows are failures the renderer will **never see**, so a spec defect that depends on them will silently slip through. - - **Where the scripts materialize their working environment** (current-convention scripts stage into the system temp directory, e.g. `/tmp/typescript_/`; older scripts used project-local folders like `.tmp/typescript_/`, `build/`, `target/`). This is where the renderer's iteration actually runs; if you need to look at what is breaking, this is the directory — not `plain_modules//` directly. - - **How conformance tests are discovered and staged** (glob, naming convention, alias setup, etc.). Some scripts stage tests into the source tree (this repo flattens `plain_modules//tests//` into `src//`); some run them in place; some require a specific file extension. This tells you exactly which conformance test file is in play for a given functionality. - - **Toolchain prerequisites** the script asserts (Node version, Python version, specific binaries). If any are missing on the user's machine, stop now and tell them — the renderer will burn credits on phantom failures otherwise. - - Keep this synthesis short (5–10 bullets, project-specific). It is the single most important reference for the classifier in [Spec-deviation classification](#spec-deviation-classification). The healthcheck only checks that the scripts *exist* and are *referenced correctly*; this step is the only place that reads what they actually *do*. - -## Launch always in `--headless` mode - -The agent must always launch the renderer in the background and supervise it via the log file and filesystem. The terminal tool **cannot** drive an interactive TUI, so TUI mode is incompatible with agent-launched rendering. Always use `--headless`: - -```bash -nohup codeplain .plain --headless > /dev/null 2>&1 & -echo $! # capture the PID -``` - -Record the PID. Confirm `codeplain.log` starts growing within a few seconds; if it doesn't, the launch failed — check the user's shell / API key and re-try. - -## Log monitoring contract - -The monitor loop in Phase 1 leans almost entirely on `codeplain.log`. Before describing the loop, here is exactly how to read this file efficiently and what to look for. Treat this section as the reference the loop calls into. - -### How to read the log incrementally - -The log grows monotonically during a run — only ever appended to, never rewritten — and can reach thousands of lines on a long render. Never re-read it whole on each pass. Track the **last byte offset** you read and read only the new bytes since then. - -Keep two pieces of state across iterations: - -- `LOG_PATH` — absolute path to `codeplain.log` (captured during pre-flight). -- `LOG_OFFSET` — number of bytes you have already consumed. Starts at `0` for a fresh run. - -Each monitor pass: - -```bash -SIZE=$(wc -c < "$LOG_PATH") -if [ "$SIZE" -gt "$LOG_OFFSET" ]; then - tail -c +$((LOG_OFFSET + 1)) "$LOG_PATH" # new bytes only - LOG_OFFSET=$SIZE -fi -``` - -If `SIZE < LOG_OFFSET`, the file was truncated — a new run has started or somebody rotated the log. Reset `LOG_OFFSET=0` and re-read from the top. - -If you cannot keep shell state between tool calls, the equivalent is: `wc -c` the file each pass to get the new size, remember the previous size in your own conversation state, and pass the byte range to `tail -c +` or `dd skip= bs=1`. Either way, **never** read the whole file on a routine pass — only when you need backstory (e.g. you just detected an error and want context from earlier). - -### Catalog of log patterns to recognize - -All patterns below are taken from the actual codeplain runtime. Treat them as the canonical signal vocabulary. - -| Pattern (regex / substring) | Meaning | Loop action | -| --------------------------- | ------- | ----------- | -| `^INFO:codeplain:[-]+$` followed by `Module: ` and `Rendering functionality :` | A new functionality is starting. The block ends at the next `-----` line. | Update `CURRENT_MODULE` and `CURRENT_FUNCTIONALITY`. Reset `ATTEMPT_COUNTER` to 0. Report to the user. | -| `Running unit tests script .* \(attempt: \)` | Unit tests starting, possibly a retry. | If `` climbs past 1, flag as warning; past 3, treat as Pathology G. | -| `\[#79FC96\]All Unit Tests scripts have passed successfully.\[/#79FC96\]` | Unit tests green. | Routine. Note the functionality is past the unit-test gate. | -| `Refactoring the generated code...` | Refactor pass between unit tests and conformance. | Routine. | -| `Implementing conformance tests...` / `Implementing test requirements:` | Conformance phase starting for the current functionality. | Routine. | -| `Running testing environment preparation script .* for build folder` | `prepare-environment-script` is running. | Routine. | -| `\[#79FC96\]All Testing Environment Preparation scripts have passed successfully.\[/#79FC96\]` | Env prep green. | Routine. | -| `Running conformance tests script .* for plain_modules//tests/ \(functionality in module \)` | Conformance tests starting for functionality ``. | Note `` — that is the on-disk folder where the latest conformance test lives. | -| `Running conformance tests attempt .` | Conformance retry loop. **Watch `` carefully.** | Increment `ATTEMPT_COUNTER = `. At `>=5`, trigger Pathology A. | -| `Fixing conformance test for functionality in module .` | Renderer is patching its own conformance test between attempts. | On its own, routine. Combined with climbing ``, smoking gun for Pathology A. | -| `Functional spec too complex!` | Single spec implies >200 LOC. Renderer aborts. | Pathology B. | -| Substrings: `missing concept`, `unknown definition`, `cyclic`, `not defined`, `cannot resolve` | Spec graph error. Renderer aborts. | Pathology C. | -| Substrings: `conflict`, `conflicting`, `contradicts` | Spec conflict surfaced. | Pathology D. | -| `Traceback (most recent call last):` followed by a Python stack | Renderer itself crashed (not a spec error). | Stop monitoring; surface the stack to the user verbatim; this is a bug report case, not a spec edit. | -| `\[#FF6B6B\]✗ rendering failed` | Renderer aborted. Followed by `render id`, `input file`, `generated code folder`, `functionalities`, `used credits`, `render time`. | Capture the whole failure block. Run is over — go to Phase 4 (failure branch). | -| `\[#79FC96\]✓ rendering succeeded` (or `rendering complete`) followed by the same metadata block | Renderer finished successfully. | Capture the success block. Run is over — go to Phase 4 (success branch). | -| `429` / `rate limit` / `quota` / `unauthorized` / `403` / `401` | API problem, not a spec problem. | Stop monitoring; tell the user the API rejected the request and surface the line. | -| **No new bytes for > 2 min while the process is alive** | API stall or wedged renderer. | Pathology E. | - -Where a column says "Pathology X", see Phase 2 for the response. - -### Three pieces of state to maintain across the loop - -The pathologies in Phase 2 can only be detected if you carry state between iterations. Keep these three variables in your working memory across passes: - -1. **`CURRENT_FUNCTIONALITY`** — the integer ID of the functionality currently being rendered. Updated whenever you see `Rendering functionality :`. -2. **`LAST_COMPLETED_FUNCTIONALITY`** — the highest functionality ID whose conformance tests passed cleanly (i.e. no `Fixing conformance test` immediately after, and either the next `Rendering functionality :` line appeared or the run succeeded). This is the value you'll pass to `--render-from ` on resume. -3. **`ATTEMPT_COUNTER`** — the most recent `Running conformance tests attempt ` value for `CURRENT_FUNCTIONALITY`. Reset to 0 when `CURRENT_FUNCTIONALITY` changes. - -A single-line summary of these three values is what you'll show the user on each pass. - -### Useful one-liners - -Use these as cheap, idempotent ways to interrogate the log. Each is safe to run on any pass. - -- Latest functionality the renderer started: - ```bash - grep -E "Rendering functionality [0-9]+:" codeplain.log | tail -n 1 - ``` -- Current conformance attempt counter: - ```bash - grep -E "Running conformance tests attempt [0-9]+\." codeplain.log | tail -n 1 - ``` -- All errors so far: - ```bash - grep -E "^(ERROR|CRITICAL|WARNING):codeplain:|✗ rendering failed|Functional spec too complex|Traceback" codeplain.log - ``` -- Time the last log line was written (proxy for liveness): - ```bash - stat -f %m codeplain.log # macOS — epoch mtime - ``` - -## Reading test-script output - -The renderer drives three external shell scripts during a run — `unittests-script`, `prepare-environment-script`, and `conformance-tests-script` (whichever are declared in the project's `config.yaml`). Each script wraps a real test framework (vitest, jest, pytest, tsc, etc.) and writes verbose output. It reads the output those scripts produce while the renderer is iterating. Three places to look, in priority order: - -### 1. Inline in `codeplain.log` (the primary source) - -With verbose logging on (the default — `--verbose` is `disabled`), codeplain captures each script's stdout/stderr and writes it into `codeplain.log` between its own wrapper lines: - -``` -INFO:codeplain:Running unit tests script .../unit_testing_typescript.sh. (attempt: 1) -... script output here: vitest summaries, FAIL lines, tsc diagnostics ... -INFO:codeplain:[#79FC96]All Unit Tests scripts have passed successfully.[/#79FC96] -``` - -So when you do the incremental log read in Phase 1, you are simultaneously reading the **renderer wrapper lines** ("running", "passed", "fixing") *and* the **raw framework output** sandwiched between them. The wrapper says *whether* the script passed; the framework output says *why* it failed and *what* the renderer is reacting to. Both are needed. - -What to extract from the framework output — stay framework-agnostic, look for these shapes: - -- **Counts**: ` passed`, ` failed`, ` skipped`. Climbing failure count between attempts means the renderer is making things worse. -- **Individual test names that failed**: lines beginning with `×`, `FAIL`, `✗`, `× should ...`, `FAIL src/.../foo.test.ts > `. The *same* test failing across attempts is the smoking gun for an under-specified spec. -- **Assertion bodies**: `expected X, got Y`. The values reveal exactly what the test expects vs what the implementation produced — the cleanest input to the classifier below. -- **Compile / type errors** (when the script wraps a type-checker): `error TS`, `cannot find module`, `is not assignable to`. A unit-test gate that keeps re-failing on the same type error means the implementation req or the type-level spec is wrong, not the code. -- **Stack traces**: which file under the prepared working folder threw, and at which line. Map the file back to `plain_modules//code/...` to find the code the spec is supposed to be governing. -- **"No test files found" / "No tests collected"**: the script ran but discovered nothing. Almost always a path/glob mismatch in the staged working folder, not a spec problem. Treat as Pathology F. - -Always set the logs to be verbose in `config.yaml`. - -### 2. Renderer memory under `plain_modules//.memory/` - -The renderer writes structured JSON notes about its own iteration there. The most useful is `conformance_test_memory/.json`, with fields like: - -```json -{ - "functionality": "Agent CLI", - "initial_issue_summary": "...what was failing at the start...", - "fix_attempt_summary": "...what the renderer changed...", - "current_issue_summary": "...what is still wrong now...", - "resolution_status": "UNRESOLVED", - "key_learnings": "...the renderer's own diagnosis of why..." -} -``` - -This is the renderer telling you, in its own words, what it tried and what is stuck. A `resolution_status: "UNRESOLVED"` is a hard signal that the loop is not converging — read the `key_learnings` field; it usually pinpoints the ambiguity in the spec or the limit of what the renderer can infer. - -Refresh these files on each pass while a retry loop is active — the renderer rewrites them between attempts. - -## Spec-deviation classification - -This is the most important active judgment the skill makes. Once a functionality enters a retry loop (unit-test or conformance), the renderer iterates — changing source code, and sometimes changing its own conformance tests — trying to satisfy the test-script output. **Not every iteration is honest.** Each iteration must be classified before deciding whether to let the renderer continue, stop and fix the spec, or stop and fix the test script. - -The inputs to the classifier are: - -1. The **spec** that governs the failing functionality — the functional spec and its acceptance test(s) in the `.plain` file. -2. The **test-script output** for the most recent attempt, from `codeplain.log` per the section above. -3. The renderer's own **`.memory/conformance_test_memory/.json`** for this functionality (when present). -4. The newest **conformance test file** the renderer just edited under `plain_modules//tests//`. -5. The newest **implementation file(s)** the renderer just edited under `plain_modules//code/`. - -With these in hand, place the current iteration into one of four buckets: - -### Bucket 1 — Honest convergence (let it continue) - -Signals: - -- Failure count is **going down** across attempts (e.g. 5 failed → 3 failed → 1 failed). -- The set of failing test names is shrinking and is a *subset* of the previous attempt's failures. -- The conformance test file is **unchanged** between attempts — only the implementation is changing. -- `key_learnings` in memory (if present) reads as legitimate technical reasoning about the framework / language / library, not about reinterpreting the spec. - -Action: let the renderer keep going, up to the threshold (5 conformance attempts / 3 unit-test attempts). Report progress to the user with the per-attempt failure count. - -### Bucket 2 — Spec under-specification (stop and fix the spec) - -Signals — any one of these is enough: - -- The **same test** fails every attempt with the **same assertion**, and the assertion checks something the functional spec **does not explicitly say** (e.g. exact exit code, exact error wording, specific status field). -- `current_issue_summary` in memory describes a question the spec does not answer (e.g. "unclear whether `--help` should exit 0 or 2"). -- The renderer's `fix_attempt_summary` keeps oscillating between two opposite interpretations of the same requirement (regex variant A this attempt, variant B next attempt, A again the attempt after). -- The conformance test file is being rewritten between attempts in a way that **changes which behavior is being asserted**, not just how it is asserted. - -Action: stop the renderer (SIGINT). Hand off to `debug-specs`. The spec must be tightened so there is only one valid behavior to converge on. Then resume with `--render-from ` for the offending functionality. - -### Bucket 3 — Renderer is drifting away from the spec (stop immediately) - -The dangerous case. Signals: - -- The renderer **rewrote a conformance test to weaken the assertion** — the test now accepts behavior the original spec does not allow. Compare the latest conformance test file to the previous attempt's; an assertion that loosened a `===` to a `>=`, removed an `expect`, or wrapped behavior in a `try/catch` to swallow failures is a drift signal. -- The renderer **deleted code that implemented a requirement** to make a test pass (a `try/catch` that now silently returns 0, a feature flag short-circuit, a hard-coded return value matching the test). Visible by diffing `plain_modules//src/...` across attempts. -- The renderer's `key_learnings` reads like rationalization rather than diagnosis (e.g. "adjusted assertion to match observed behavior", "removed strict check that was causing failures"). -- Failure count is going **up** between attempts. - -Action: stop the renderer **immediately**, don't wait for the threshold. The fix must be applied to the `.plain` file (the spec was probably not specific enough and the renderer chose the path of least resistance): - - If it happened during the **conformance testing phase**, the `***test reqs***` need to be tightened. - - If it happened during the rendering of a **functional requirement**, the functional specs, definitions, or `***impl reqs***` need to change. - - If it happened during **acceptance testing**, the acceptance test definitions need to be tightened. - -Do nothing on the generated-code side; the broken code under `plain_modules/` will be overwritten on resume. **Never** hand-edit it. - -Then resume with `--render-from ` (or `--force-render` if the drift contaminated earlier functionalities). Re-running `plain-healthcheck` first is mandatory. - -### Bucket 4 — The test script itself is the source of the failure (Pathology F) - -Signals: - -- Test-script output shows a **toolchain** error (`vitest: command not found`, `python: No module named pytest`, Node version too old). -- Output is `No test files found` / `no tests collected` while the conformance tests do exist on disk — a glob/path/staging bug in the script. -- The script is failing **before** running any framework command at all (`die: prepared environment missing`, missing argument, etc.). -- The failure mode is identical across functionalities, not specific to the spec being rendered. - -Action: This is **not** a spec problem and the spec should stay as is. Stop the renderer and investigate why the script fails: -- If it's a bug in the script (e.g., pathing issue, syntax error, missing environment variable load), attempt to fix the script directly, or hand off to the matching `implement-*-testing-script` skill. -- If it's a missing host dependency (e.g., Python or Node is not installed, or a required global package is missing), inform the user and ask them to install it. -- **Before resuming the renderer**, test the script by running it manually exactly as the renderer would. If the same infrastructure/script issue persists, the fix didn't work. If the script now runs (even if it reports legitimate test failures), the fix was successful. - -### Classifier discipline - -- Run the classifier **only** when a retry loop is actually in progress (`ATTEMPT_COUNTER >= 2`). For a first attempt that just failed, there isn't enough data yet — the renderer hasn't done a fix pass. -- Show your classification reasoning to the user before acting on Bucket 2 or Bucket 3. State the bucket, the concrete evidence ("test `should exit 0 on --help` has failed 3 attempts; spec text does not specify exit code"), and the proposed hand-off skill. Get their go-ahead before SIGINT. -- If two buckets seem to apply, prefer Bucket 4 → Bucket 3 → Bucket 2 → Bucket 1 in that order. A script bug shadows everything; a drift is worse than under-specification; under-specification is worse than honest iteration. - -## Phase 1 — Monitor loop - -Run the loop until one of these terminal conditions holds: the process exits successfully, the process exits with failure, or you (with the user's approval) kill it. - -On each iteration, in this order: - -### 1a. Read the new log bytes (primary) - -Using the incremental read recipe above, pull only the bytes appended since the last pass. Walk the new text against the pattern catalog and update `CURRENT_FUNCTIONALITY`, `LAST_COMPLETED_FUNCTIONALITY`, and `ATTEMPT_COUNTER`. Because verbose mode is on by default, this chunk also contains the **raw test-script output** (vitest summaries, tsc errors, stack traces) sandwiched between the renderer's wrapper lines — capture failure counts, failing test names, and key assertion text. Note any pathology hits, but don't act on them yet; finish the pass first so the report is consistent. - -If there are **no new bytes**, note the elapsed time since the last log activity (`stat` mtime works). Hitting 2 minutes of silence with the process alive triggers Pathology E. - -### 1b. Is the process alive? - -- Mode A: `ps -p ` (or `kill -0 `). If gone, capture the exit code via `wait`, or fall back to the log's final block (success / failure banner). -- Mode B: `pgrep -fl codeplain` should still return the run. If not, the user already stopped it (or it finished). - -If the process is gone but you haven't yet seen a `✓` or `✗` banner in the log, do one more incremental read — the banner is the very last thing codeplain writes, and the process exits immediately after. - -### 1c. Inspect the filesystem (corroboration only) - -This is a secondary signal — it confirms what the log already said. Don't go on a fishing trip here. - -For the current module, the renderer drops generated code into `plain_modules//` and conformance tests into `plain_modules//tests//`. Each pass, inspect what is new: - -- New files under `plain_modules//code/` confirm the functionality is being implemented. -- New folders under `plain_modules//tests/` confirm the functionality has reached the conformance phase. -- Files that were rewritten between passes are the renderer's "fix" attempts — diffing them across iterations is what reveals whether the renderer is converging or thrashing. - -Read **only the newest** file(s) (the ones that changed since last pass), and read them to understand *what the renderer chose to do*, not just *that* it did something. This is the first line of defense against "the spec is ambiguous and the renderer guessed wrong" — you can often spot the wrong guess in the generated code long before tests fail. - -Generated code under `plain_modules//code/` and `plain_modules//tests/` is **read-only**. Never edit it. Edits go in the `.plain` files. - -### 1d. Cadence - -Poll every 10–30 seconds. Faster than 10s is noisy and wastes tool calls; slower than 30s makes the user feel ignored. Tighten the cadence (down to 5s) when something looks off — climbing `ATTEMPT_COUNTER`, a new error keyword in the latest log chunk, an unexpectedly quiet log. Loosen the cadence (out to 60s) during long, healthy "rendering functionality N" stretches where the log is moving steadily and nothing alarming has appeared. - -The log read is cheap (incremental), so the bottleneck is the user-facing report, not the I/O. Prefer to err on the tighter side. - -### 1e. Report to the user - -After each pass, give the user a **short** status line — one or two sentences, built from the three state variables. Examples: - -- `Functionality 3/? in flight; unit tests just passed, conformance prep starting.` (after a `Implementing conformance tests...` line) -- `Functionality 4 is on conformance attempt 4 of an open loop; unit tests still green. One more attempt and I'll flag it.` (`ATTEMPT_COUNTER = 4`) -- `No new log lines in 95s; PID 12345 still alive. Likely an API call in flight.` (silent log, alive process) -- `Last completed: functionality 2. Currently rendering functionality 3.` (resume-readiness summary) - -Do not dump the raw log unless the user asks for it. When you do dump it, paste only the relevant block (e.g. the failure banner with the surrounding 10 lines), not the entire file. - -## Phase 2 — Pathologies and intervention - -These are the patterns that justify stopping the renderer. For each, the workflow is the same: **detect → confirm with the user → stop → diagnose → fix the specs → resume**. - -### Pathology A — Conformance loop that won't converge - -Symptom: `Running conformance tests attempt ` keeps climbing for the **same** functionality, with `Fixing conformance test for functionality ` between attempts. Unit tests stay green; only the conformance step fails. - -Threshold: **5 attempts on the same functionality** is the heuristic. The renderer will keep going (the real `codeplain.log` in this repo shows it climbing to 10), but each attempt costs credits and rarely converges if it hasn't by 5. - -Do **not** treat hitting the threshold as the diagnosis. From `ATTEMPT_COUNTER >= 2` onwards, run the [Spec-deviation classification](#spec-deviation-classification) classifier each pass. The bucket it returns is the diagnosis: - -- **Bucket 1** — keep going until the threshold; report the per-attempt failure count. -- **Bucket 2** — stop now (regardless of how close to the threshold you are); hand off to `debug-specs` / `add-implementation-requirement` / inline spec edit. The spec is under-specified. -- **Bucket 3** — stop **immediately**, do not wait for the threshold; the renderer is drifting. Tighten the spec or acceptance test before resuming. -- **Bucket 4** — the test script is the problem; route to Pathology E instead. - -### Pathology B — `Functional spec too complex!` - -The renderer rejected a spec because it would imply >200 LOC. Stop is automatic — the renderer aborts. Hand off to `break-down-func-spec`. - -### Pathology C — Missing concept / unknown definition / import error - -The renderer can't resolve a reference. Stop is automatic. Hand off to `add-concept` (or fix the `import` / `requires` chain). Re-run `plain-healthcheck` before resuming — a dry-run will catch this faster next time. - -### Pathology D — Conflicting specs - -Renderer surfaces a conflict, or you can see two specs in the same module that contradict each other in the generated code. Hand off to `resolve-spec-conflict`. - -### Pathology E — Test scripts themselves are broken - -Symptom: log shows the unit-test or conformance-test **script** itself failing (non-zero exit, syntax error, missing dependency) on every attempt, rather than the renderer's code failing the tests. The fix is **not** a spec change — it is a `test_scripts/` change. Stop and hand off to the matching `implement-*-testing-script` skill, or have the user repair the script directly. - -**Special case — stale scripts after a renderer upgrade (doubled paths).** The renderer now passes the build folder and conformance-tests folder to the scripts as **absolute paths**; older test scripts built paths by concatenating those arguments (`.tmp/$1`, `python_$1`, `$current_dir/$2`). The tell is a test-log path that contains the project root **twice**, e.g.: - -``` -ImportError: Start directory is not importable -/Users/x/project//Users/x/project/plain_modules/module/tests/spec -``` - -The render then gets stuck "fixing" tests that can never pass. The fix is never a spec edit: stop the render and regenerate the affected scripts via the matching `implement-*-testing-script` skill (current convention: system-temp working folder from `basename "$1"`, `$2` resolved to absolute before any `cd`). - -### Pathology F — Unit-test retry loop - -Less common than the conformance loop, but the same shape: `Running unit tests script ... (attempt: )` with `` climbing for the same functionality. Threshold: **3 attempts**. From `ATTEMPT_COUNTER >= 2` onwards, run the same [Spec-deviation classification](#spec-deviation-classification) classifier and act on the bucket it returns. The most common root causes for unit-test loops are an implementation req that doesn't match the unit-test scaffolding (Bucket 2), or test reqs / framework usage that the renderer is satisfying by weakening assertions (Bucket 3). Hand-off is typically `add-implementation-requirement` or `debug-specs`. - -### How to actually stop the renderer - -Always **SIGINT first**, never SIGKILL unless SIGINT fails: - -```bash -kill -INT -``` - -SIGINT lets codeplain flush the log and close out the run cleanly. Wait ~5s, then check `ps -p `. If still alive, repeat once. Only after a second SIGINT has failed should you escalate to `kill -TERM ` and, last resort, `kill -9 `. - -After the process is gone, do **one** final incremental read of the log — the renderer often writes a closing block on shutdown (`✗ rendering failed` with render id, input file, used credits, render time). Capture it; it's the most useful single artifact for the user. - -The value of `LAST_COMPLETED_FUNCTIONALITY` you've been tracking is what you'll feed `--render-from` on resume (as ``). If you weren't tracking it carefully, recover it now with: - -```bash -grep -E "Rendering functionality [0-9]+:" codeplain.log | tail -n 1 -``` - -and subtract 1 if the last functionality didn't reach a clean conformance pass. - -## Phase 3 — Fix + resume - -1. **Hand off to the right edit skill.** Don't do the spec edit inside this skill — invoke the dedicated one (`debug-specs`, `resolve-spec-conflict`, `break-down-func-spec`, `add-implementation-requirement`, `add-concept`, `add-functional-spec`, etc.). Pass it concrete evidence: the offending log lines, the file paths under `plain_modules/` you read, and the spec the user agreed is wrong. -2. **Re-run `plain-healthcheck`** after the fix. If it FAILs, do not resume — fix the next thing first. -3. **Resume with `--render-from`.** If the last fully-completed functionality was ``, resume with the next one: - - ```bash - codeplain .plain --headless --render-from - ``` - - `--render-from` includes its argument, so the value passed is the **first functionality you want re-rendered**, not the last one that already passed. If nothing completed, omit `--render-from` and start over from the beginning. If only a single functionality needs re-rendering and the rest are fine, prefer `--render-range ` instead. - -4. **Re-enter the monitor loop** in Phase 1. - -If the fix required regenerating a module's earlier output (e.g. a definition change that ripples backwards), use `--force-render` instead of `--render-from`. Confirm this trade-off with the user — it costs more credits. - -## Phase 4 — Wrap up - -When the renderer exits successfully: - -1. Read the success banner from the log: render id, generated code folder, functionalities count, used credits, render time. Report it to the user verbatim — these are the numbers they actually care about. -2. List the top-level files/folders that appeared (or changed) under `plain_modules//` and `plain_modules//tests/` so the user knows where to look. -3. Remind the user of the **side-channel commands** they may want to run themselves, based on the config: - - `./test_scripts/` for unit tests, - - `./test_scripts/` to set up the test env, - - `./test_scripts/` for conformance tests. - Only mention the scripts the project's `config.yaml` actually declares. -4. If `--copy-build` was set, confirm the build was copied to `--build-dest`. If `--copy-conformance-tests` was set, same for `--conformance-tests-dest`. - -When the renderer exits with a failure that wasn't intercepted by Phase 2 (e.g. transient API error, auth failure), report the failure block from the log and ask the user whether to retry (`--render-from` from the last completed ID) or to investigate. - -## Anti-patterns - -- **Editing generated code to "fix" a render.** Never. Generated files under `plain_modules//code/` and `plain_modules//tests/` exist as evidence only; they are overwritten every render. All fixes go in `.plain` files. -- **Reading the test scripts' source code to understand what's failing.** Don't. The scripts run real frameworks (vitest, pytest, etc.) and the *output* of those frameworks — captured in `codeplain.log` and in `.memory/conformance_test_memory/*.json` — is what tells you what is failing and why. Reading the script source tells you nothing the log doesn't already say. -- **Treating every conformance loop as under-specification.** Run the classifier. The renderer sometimes drifts (Bucket 3) by silently weakening its own tests; that is the opposite problem and the fix is different. -- **Re-reading the whole log on every pass.** It grows. Track the byte offset and read only what was appended since the last pass. Full reads are reserved for "I just saw an error and need backstory." -- **Trusting the filesystem over the log.** The filesystem only updates when a step completes; the log updates the moment any step *starts*, retries, or fails. When they disagree, the log is right. -- **Letting a conformance loop run indefinitely.** Each attempt costs credits and the renderer will keep going past any reasonable point (this repo's `codeplain.log` climbed to attempt 10 before bailing). The threshold is 5; surface it to the user immediately. -- **Re-running the whole module after a small fix.** Use `--render-from ` or `--render-range `. Only fall back to `--force-render` when a backward dependency genuinely changed. -- **Trying to read the TUI.** You can't see it. Use the log file and filesystem; ask the user to paste the TUI's test panel if the log is missing verbose output. -- **Dumping raw log into chat on every pass.** Summarize from the three state variables (`CURRENT_FUNCTIONALITY`, `LAST_COMPLETED_FUNCTIONALITY`, `ATTEMPT_COUNTER`) plus a classifier verdict when a loop is in progress. The user wants signal, not transcript. -- **Skipping `plain-healthcheck` because "dry-run passed earlier".** Specs drift; configs drift. Re-run it before every real render and after every fix. -- **`kill -9` as a first move.** Always SIGINT first — codeplain needs to flush its log and clean up. - -## Validation checklist - -- [ ] Target `.plain` file and governing `config.yaml` were confirmed before launch -- [ ] `plain-healthcheck` is PASS as of the latest spec state -- [ ] `CODEPLAIN_API_KEY` is set, or `--api-key` was supplied -- [ ] Launch mode (`--headless` agent-launched vs. user-launched TUI) was chosen with the user -- [ ] Renderer PID is known (or, in Mode B, confirmed alive via `pgrep`) -- [ ] Log file path is known and is observed to be growing -- [ ] Log path was captured during pre-flight and a byte offset is being tracked across passes -- [ ] Monitor loop reads only the new bytes of `codeplain.log` each pass (no full re-reads on routine passes) -- [ ] `CURRENT_FUNCTIONALITY`, `LAST_COMPLETED_FUNCTIONALITY`, and `ATTEMPT_COUNTER` are maintained across passes -- [ ] Filesystem inspection is used only to corroborate log signals, not as the primary signal -- [ ] Status reports to the user are short (≤2 sentences) and signal-driven, not transcript dumps -- [ ] Test-script output (failure counts, failing test names, assertion bodies, stack traces) is being extracted from the verbose log chunks each pass -- [ ] When `ATTEMPT_COUNTER >= 2`, the spec-deviation classifier ran and returned a bucket (1–4) before any stop/continue decision -- [ ] If a classifier verdict caused a stop, the bucket and concrete evidence (failing test name + spec gap, or the diff that showed drift) was shown to the user before SIGINT -- [ ] `.memory/conformance_test_memory/.json` was read (if present) to capture `current_issue_summary` and `resolution_status` -- [ ] Pathologies (loop / complexity / missing concept / conflict / stall / broken test script / drift) are recognized and acted on -- [ ] Renderer was stopped with SIGINT (escalating only on failure) — not SIGKILL by default -- [ ] Spec fixes were delegated to the right edit skill, not done inside this skill -- [ ] Resumes use `--render-from ` (or `--render-range`) — `--force-render` only with user approval -- [ ] On success, final render id / credits / time were reported and side-channel test commands were surfaced -- [ ] No generated code under `plain_modules//code/` or `plain_modules//tests/` was modified diff --git a/package.json b/package.json index 1626a16..28c87db 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "license": "MIT", "keywords": [ "plain", - "codeplain", "claude-code", "skills", "spec", @@ -28,6 +27,7 @@ }, "files": [ "bin/cli.mjs", + "bin/python-tools.mjs", "forge" ], "scripts": { diff --git a/presentations/2026-08-06-plain-forge.html b/presentations/2026-08-06-plain-forge.html deleted file mode 100644 index be683ce..0000000 --- a/presentations/2026-08-06-plain-forge.html +++ /dev/null @@ -1,816 +0,0 @@ - - - - - -plain-forge - - - - -
-
- - -
-
-
-

plain-forge

-

- turn ideas into ***plain specs -

-
-
-

agenda

-
    -
  • 01the problem and the solution
  • -
  • 02what actually ships
  • -
  • 03rules and skills
  • -
  • 04the skill catalog
  • -
  • 05forge-plain, phase by phase
  • -
  • 06how the interview runs
  • -
  • 07install, update, uninstall
  • -
  • 08challenges ahead
  • -
-
-
-
- - -
-
01 · the problem and the solution
-

codeplain renders specs. plain-forge helps you write and maintain them.

-
-
-
-

what an unaided author struggles with

- - - - - - - - - - - - -
getting startedhow do i start a new ***plain project?✓✓
writing a technical specificationhow do i best write this?✓✓
dividing the software into chunkswhat is one unit of functional spec?✓✓
conflicting requirementsdoes this spec conflict with anything?✓✓
depth of informationwhat details should i include?✓✓
debuggingwhy isn't the software working as intended?✓✓
saas expertwhat does the api look like?✓
live probinghow does the api actually behave?✓
integration forge
-
-
-

what the product does about it

-

the product empowers your agent with the understanding of the rules of writing - ***plain specs.

-

the user has the idea, the agent helps clarify it, writes the ***plain specs, and - reviews them with the user.

-

you work with the agent exactly as you would when writing code. the procedures are there to clarify intent — - and that intent is what gets baked into the specs.

-
-
-
- - - - - idea - - - on your own - - - - - - - - - - ***plain - - - codeplain - - - code - - - - maintenance - - - - - - - - - - - - - idea - - - - plain-forge - - - - - - - - - - ***plain - - - - codeplain - - - - code - - - - maintenance - - - - - - - - - PRD - - - - integration-forge - - - - - - - - - - - - - - ***plain - - - - codeplain - - - - code - - - - maintenance - - -
-
-
- - -
-
02 · shape
-

two halves: a content library and a copy tool

-

the whole product is markdown files. the only code that runs is the installer that copies them into place.

-
-
-

forge/ — the product

-
forge/
-  skills/<name>/SKILL.md   # 30 skills
-              references/  # loaded on demand
-              assets/      # 3 script generators only
-  rules/*.md               # 14 rule files
-

a skill is frontmatter — name, and a description ending in a "use when…" trigger the host - agent matches on — plus a prose body. operational bulk moves into references/ so it loads only when relevant.

-
-
-

editing the two halves

-
-
the cli
behaviour change → change test/cli.test.mjs. node's built-in node:test, ~880 lines.
-
the content
no tests. the review discipline is the rules-ownership model on the next slide.
-
-
-
-
- - -
-
03 · content architecture
-

rules hold the knowledge. skills hold the procedure.

-

this separation is the main design decision in the product.

-
-
-
-

forge/rules/ — 14 files, sole source of truth

-

every syntax rule, ownership rule, constraint and canonical example lives here.

-
-

sections

definitions.md
func-specs.md
impl-reqs.md
test-reqs.md

-

module wiring

import-modules.md
requires-modules.md
exported-concepts.md
required-concepts.md

-

syntax

bullet-continuation.md
linked-resources.md

-
-

integration forge

-

integrations.md
integration-embedded.md
integration-standalone.md
integration-embedded-testing.md
integration work only

-
-
-
-
-

why rules?

-

rules are autoloaded into the agent's context at startup, before any spec work begins. claude - code reads rules/*.md natively, each file scoped to **/*.plain.

-

the alternative is relying on the model to decide, mid-task, that now would be a good moment to go and read the - authoring guidance. that is exactly the unpredictability we want out of the loop.

-
agents without native rules wiring reach the same files through - load-plain-reference, which loads only the ones the current task needs.
-
-
-
-
- - -
-
04 · surface
-

30 skills, narrow and chained

-

four entry points a user invokes; everything else is called by them.

-
- - - - - - - - - -
entry pointsforge-plain · init-plain-project · add-feature · run-codeplain
authoringadd-functional-spec · add-functional-specs · add-implementation-requirement · add-test-requirement · add-concept · add-acceptance-test · add-resource · add-template
modulescreate-import-module · create-requires-module · refactor-module · consolidate-concepts
qualityanalyze-if-func-spec-too-complex · break-down-func-spec · analyze-func-specs · resolve-spec-conflict
gatesplain-healthcheck · check-plain-env · init-config-file
debug & scriptsdebug-specs · implement-{unit-testing, conformance-testing, prepare-environment}-script
-
-

forge-plain

starts a new project: a short intent interview, then four gated phases, ending in a passing dry-run and the render command.

-

add-feature

grows an existing project: the same one-question-at-a-time loop and the same gates, scoped to a single feature.

-

debug-specs

traces a failure back to the responsible spec and names the cause: ambiguous, missing, conflicting, incorrect, or a missing implementation req. edits the .plain file only.

-
-
-
- - -
-
05 · the flagship workflow
-

forge-plain: an intent interview, then four gated phases

-

finish a phase — on disk and approved — before the next one opens. no drafting ahead.

-
-
-
PHASE 0

intent

five questions: problem, user, outcome, success criterion, boundary. nothing written to disk.

gate · user confirms brief
-
PHASE 1

product

definitions and functional specs on disk and approved.

gate · review agent
-
PHASE 2

tech stack

implementation reqs: language, architecture, libraries, unit-test strategy.

gate · review agent
-
PHASE 3

testing

test reqs, generated test_scripts/, config.yaml(s), host probed.

gate · review agent
-
PHASE 4

validate

plain-healthcheck, a passing dry-run, then the render command is handed over.

gate · dry-run green
-
-
-

maintaining with plain-forge

-
-

add-feature

the same loop and the same gates, scoped to one feature on an existing project. its own short intent step; ends on plain-healthcheck.

-

debug-specs

something in the rendered app is wrong: it traces the failure back to the responsible spec and fixes the spec, never the code.

-

run-codeplain

supervises a live render, watches for stuck conformance loops and complexity errors, hands off to a spec-edit skill and resumes. experimental.

-
-
-
-
- - -
-
06 · inside a phase
-

the loop runs per topic. the gate runs once.

-

this is what phase 1, 2 and 3 from the previous slide actually do.

-
- - - - - - - - - - - - - - agent asks user - one question, never bundled - - - - - agent authors - written to disk immediately - - - - - review with user - only what just changed - - - rejected, or more to add — and on to the next topic - - - - - - every topic done - - - phase gate - a fresh agent as a judge - - - - approved - next phase - - - - technical gaps only - - - anything the user has to decide — back to the question - - - -
-

depth is allowed

one question per call, not per topic — a vague answer gets another pass at the same topic.

-

staleness is not

an answer that contradicts something already on disk is applied before the next question.

-

talk is not output

everything lands on disk — .plain files, test_scripts/, config.yaml.

-
-
-
- - -
-
07 · distribution
-

six agent layouts, and a manifest that makes updates safe

-

the install target is somebody else's directory, so the cli's whole design is about never touching what isn't ours.

-
-
- - - - - - - - -
agentprojectglobal
claude.claude/~/.claude/
codex · copilot · universal.agents/~/.agents/
forgecode.forge/~/forge/
opencode.opencode/~/.config/opencode/
-

the last two global paths are hardcoded exceptions. three agents share - .agents/; the manifest records which label was chosen so update reports it correctly.

-

rules wiring, per agent

-
-
claude code
native — auto-loads rules/*.md, each scoped with paths: "**/*.plain"
-
opencode
a glob merged into opencode.json, absolute when global
-
forgecode
a fenced managed block in AGENTS.md
-
the rest
nothing generated — the router skill reads the directory on demand
-
-
-
-

manifest.json

-

every file written, plus the agent label and package version. nothing outside that list is ever a candidate for - deletion, so a user's own and third-party skills in the same directory are structurally unreachable.

-
-
install
copies and records. refuses with exit 1 if a manifest or a legacy install is already there — it never overwrites in place.
-
update
finds every install across both scopes and all agents with no prompts, skips any whose recorded version didn't increase, re-copies, then prunes what no longer ships — each deletion confirmed individually, or all of them under --yes.
-
uninstall
deletes exactly the manifest's files, then the manifest, then the rules wiring, then any directory left empty.
-
-
a manifest-less install is detected by four signature skills and refreshed overwrite-only. - uninstall on one refuses rather than guessing: it lists the directories to clean by hand and exits non-zero.
-
-
-
- - -
-
08 · challenges
-

challenges ahead

-
-
- -
-
syncing with codeplain and ***plain — a maintenance problem
-
    -
  • a new ***plain feature has to reach the rules before an agent can use it
  • -
  • a codeplain cli change — flags, config keys — has to reach the skills
  • -
  • rules that lag behind the language teach the wrong thing
  • -
-
- -
-
evals — a research problem
-
    -
  • how well specs written by plain-forge render
  • -
  • how often a first render passes conformance
  • -
  • comparing two revisions of a skill on the same input
  • -
-
- -
-
observability — we don't own the agent
-
    -
  • where is a rule failing
  • -
  • which skill is failing, and at which step
  • -
  • how often a phase gate is rejected before it passes
  • -
-
- -
-
analytics — we don't own the agent
-
    -
  • which skills users reach for most
  • -
  • how a given skill performs over time
  • -
  • testing out new skills before they ship
  • -
-
- -
- -
-
-

should we own the agent harness?

-
    -
  • implementing our own agent
  • -
  • integrating plain-forge into codeplain
  • -
-

it gives observability and analytics a fighting chance, maybe also at evals

-

and how does any of that work when we ship open source?

-
-
-
-
- -
-
-
-
-
-
-
-
-

the end

-

please hesitate to ask questions

-
- -
- -
-
-
1 / 10  ·  ← → move
-
- - - - diff --git a/test/cli.test.mjs b/test/cli.test.mjs index 4bbc60c..bab0e81 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -84,6 +84,11 @@ describe("parseArgs", () => { assert.equal(parseArgs(["--help"]).help, true); assert.equal(parseArgs(["update"]).yes, undefined); }); + + test("recognizes --skip-plain-parser", () => { + assert.equal(parseArgs(["install", "--skip-plain-parser"]).skipPlainParser, true); + assert.equal(parseArgs(["install"]).skipPlainParser, undefined); + }); }); describe("toPosix", () => { @@ -555,7 +560,10 @@ describe("detectInstalls", () => { // ── Black-box integration tests: spawn the real CLI in an isolated HOME/cwd ── +// `install` would otherwise download uv and plain-parser; python-tools.test.mjs +// covers that step without the network. function runCli(args, { cwd, home, input } = {}) { + if (args[0] === "install") args = [...args, "--skip-plain-parser"]; return spawnSync("node", [CLI, ...args], { cwd, input, diff --git a/test/python-tools.test.mjs b/test/python-tools.test.mjs new file mode 100644 index 0000000..3696f51 --- /dev/null +++ b/test/python-tools.test.mjs @@ -0,0 +1,154 @@ +import assert from "node:assert/strict"; +import path from "node:path"; +import { describe, test } from "node:test"; + +import { ensurePlainParser, uvCandidates, uvInstaller } from "../bin/python-tools.mjs"; + +const HOME = path.join(path.sep, "home", "user"); +const ENOENT = Object.assign(new Error("spawn ENOENT"), { code: "ENOENT" }); + +// A fake host. `onPath` is the set of commands that resolve; `files` the set of +// paths that exist. Handlers can change both, e.g. when an installer runs. +function fakeHost({ onPath = [], files = [], handlers = {} } = {}) { + const host = { onPath: new Set(onPath), files: new Set(files), calls: [], logs: [] }; + host.run = (cmd, args, opts = {}) => { + host.calls.push({ cmd, args, inherit: Boolean(opts.inherit) }); + const key = [cmd, ...args].join(" "); + for (const [prefix, handler] of Object.entries(handlers)) { + if (key.startsWith(prefix)) return { status: 0, stdout: "", ...handler(host) }; + } + if (args[0] === "--help") { + return host.onPath.has(cmd) ? { status: 0, stdout: "" } : { status: null, stdout: "", error: ENOENT }; + } + return { status: 0, stdout: "" }; + }; + host.opts = (extra = {}) => ({ + platform: "linux", + env: {}, + home: HOME, + run: host.run, + exists: (p) => host.files.has(p), + log: (m) => host.logs.push(m), + ...extra, + }); + return host; +} + +const localUv = (exe = "uv") => path.join(HOME, ".local", "bin", exe); +const installs = (host) => host.calls.filter((c) => c.inherit).map((c) => [c.cmd, ...c.args].join(" ")); + +describe("ensurePlainParser", () => { + test("does nothing when plain-parser is already on PATH", () => { + const host = fakeHost({ onPath: ["plain-parser"] }); + assert.deepEqual(ensurePlainParser(host.opts()), { status: "present" }); + assert.deepEqual(installs(host), []); + }); + + test("installs plain-parser with uv from PATH", () => { + const host = fakeHost({ + onPath: ["uv"], + handlers: { "uv tool install": (h) => (h.onPath.add("plain-parser"), {}) }, + }); + assert.deepEqual(ensurePlainParser(host.opts()), { status: "installed" }); + assert.deepEqual(installs(host), ["uv tool install plain-parser"]); + }); + + test("uses uv from ~/.local/bin when it is not on PATH", () => { + const host = fakeHost({ + files: [localUv()], + handlers: { [`${localUv()} tool install`]: (h) => (h.onPath.add("plain-parser"), {}) }, + }); + assert.deepEqual(ensurePlainParser(host.opts()), { status: "installed" }); + assert.deepEqual(installs(host), [`${localUv()} tool install plain-parser`]); + }); + + test("installs uv with the shell installer on Linux, then plain-parser", () => { + const host = fakeHost({ + handlers: { + "sh -c": (h) => (h.files.add(localUv()), {}), + [`${localUv()} tool install`]: (h) => (h.onPath.add("plain-parser"), {}), + }, + }); + assert.deepEqual(ensurePlainParser(host.opts()), { status: "installed" }); + const [uvInstall, ppInstall] = installs(host); + assert.match(uvInstall, /^sh -c .*curl -LsSf https:\/\/astral\.sh\/uv\/install\.sh \| sh/); + assert.match(uvInstall, /wget -qO- https:\/\/astral\.sh\/uv\/install\.sh \| sh/); + assert.match(uvInstall, /needs curl or wget/); + assert.equal(ppInstall, `${localUv()} tool install plain-parser`); + }); + + test("installs uv with the PowerShell installer on Windows", () => { + const uvExe = localUv("uv.exe"); + const host = fakeHost({ + handlers: { + "powershell.exe": (h) => (h.files.add(uvExe), {}), + [`${uvExe} tool install`]: (h) => (h.onPath.add("plain-parser"), {}), + }, + }); + assert.deepEqual(ensurePlainParser(host.opts({ platform: "win32" })), { status: "installed" }); + const [uvInstall, ppInstall] = installs(host); + assert.equal(uvInstall.split(" ")[0], "powershell.exe"); + assert.match(uvInstall, /irm https:\/\/astral\.sh\/uv\/install\.ps1 \| iex/); + assert.equal(ppInstall, `${uvExe} tool install plain-parser`); + }); + + test("warns with the manual commands when installing uv fails", () => { + const host = fakeHost({ handlers: { "sh -c": () => ({ status: 1 }) } }); + assert.deepEqual(ensurePlainParser(host.opts()), { status: "failed" }); + assert.equal(installs(host).length, 1, "does not try uv tool install"); + const out = host.logs.join("\n"); + assert.match(out, /installing uv failed/); + assert.match(out, /curl -LsSf https:\/\/astral\.sh\/uv\/install\.sh \| sh/); + assert.match(out, /uv tool install plain-parser/); + }); + + test("warns when uv tool install fails", () => { + const host = fakeHost({ onPath: ["uv"], handlers: { "uv tool install": () => ({ status: 2 }) } }); + assert.deepEqual(ensurePlainParser(host.opts({ platform: "win32" })), { status: "failed" }); + const out = host.logs.join("\n"); + assert.match(out, /uv tool install plain-parser` failed/); + assert.match(out, /install\.ps1/); + }); + + test("tells the user to fix PATH when plain-parser lands off PATH", () => { + const host = fakeHost({ + onPath: ["uv"], + handlers: { "uv tool dir --bin": () => ({ stdout: "/home/user/.local/bin\n" }) }, + }); + assert.deepEqual(ensurePlainParser(host.opts()), { status: "not-on-path" }); + const out = host.logs.join("\n"); + assert.match(out, /installed into \/home\/user\/\.local\/bin, which is not on your PATH/); + assert.match(out, /uv tool update-shell/); + }); + + test("does not reinstall a plain-parser that is installed off PATH", () => { + const binDir = path.join(HOME, ".local", "bin"); + const host = fakeHost({ + onPath: ["uv"], + files: [path.join(binDir, "plain-parser")], + handlers: { "uv tool dir --bin": () => ({ stdout: `${binDir}\n` }) }, + }); + assert.deepEqual(ensurePlainParser(host.opts()), { status: "not-on-path" }); + assert.deepEqual(installs(host), []); + assert.match(host.logs.join("\n"), /uv tool update-shell/); + }); +}); + +describe("uv helpers", () => { + test("uvInstaller picks PowerShell on Windows and sh elsewhere", () => { + assert.equal(uvInstaller("win32").cmd, "powershell.exe"); + assert.equal(uvInstaller("linux").cmd, "sh"); + assert.equal(uvInstaller("darwin").cmd, "sh"); + }); + + test("uvCandidates honors UV_INSTALL_DIR and XDG_BIN_HOME first", () => { + const env = { UV_INSTALL_DIR: "/opt/uv", XDG_BIN_HOME: "/xdg/bin" }; + assert.deepEqual(uvCandidates("linux", env, HOME), [ + path.join("/opt/uv", "uv"), + path.join("/xdg/bin", "uv"), + path.join(HOME, ".local", "bin", "uv"), + path.join(HOME, ".cargo", "bin", "uv"), + ]); + assert.equal(path.basename(uvCandidates("win32", {}, HOME)[0]), "uv.exe"); + }); +});