plain-forge 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -0
  3. package/bin/cli.mjs +143 -0
  4. package/forge/docs/.gitkeep +0 -0
  5. package/forge/rules/definitions.md +57 -0
  6. package/forge/rules/exported-concepts.md +39 -0
  7. package/forge/rules/func-specs.md +72 -0
  8. package/forge/rules/impl-reqs.md +50 -0
  9. package/forge/rules/import-modules.md +51 -0
  10. package/forge/rules/required-concepts.md +45 -0
  11. package/forge/rules/requires-modules.md +59 -0
  12. package/forge/rules/test-reqs.md +47 -0
  13. package/forge/skills/add-acceptance-test/SKILL.md +98 -0
  14. package/forge/skills/add-concept/SKILL.md +67 -0
  15. package/forge/skills/add-feature/SKILL.md +136 -0
  16. package/forge/skills/add-functional-spec/SKILL.md +81 -0
  17. package/forge/skills/add-functional-specs/SKILL.md +115 -0
  18. package/forge/skills/add-implementation-requirement/SKILL.md +73 -0
  19. package/forge/skills/add-resource/SKILL.md +108 -0
  20. package/forge/skills/add-template/SKILL.md +65 -0
  21. package/forge/skills/add-test-requirement/SKILL.md +68 -0
  22. package/forge/skills/analyze-2-func-specs/SKILL.md +102 -0
  23. package/forge/skills/analyze-func-specs/SKILL.md +124 -0
  24. package/forge/skills/analyze-if-func-spec-too-complex/SKILL.md +152 -0
  25. package/forge/skills/break-down-func-spec/SKILL.md +156 -0
  26. package/forge/skills/check-plain-env/SKILL.md +288 -0
  27. package/forge/skills/consolidate-concepts/SKILL.md +193 -0
  28. package/forge/skills/create-import-module/SKILL.md +98 -0
  29. package/forge/skills/create-requires-module/SKILL.md +104 -0
  30. package/forge/skills/debug-specs/SKILL.md +189 -0
  31. package/forge/skills/forge-integration/SKILL.md +443 -0
  32. package/forge/skills/forge-plain/SKILL.md +333 -0
  33. package/forge/skills/implement-conformance-testing-script/SKILL.md +247 -0
  34. package/forge/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +324 -0
  35. package/forge/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +100 -0
  36. package/forge/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +102 -0
  37. package/forge/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +92 -0
  38. package/forge/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +100 -0
  39. package/forge/skills/implement-prepare-environment-script/SKILL.md +242 -0
  40. package/forge/skills/implement-prepare-environment-script/assets/prepare_environment_java.sh +42 -0
  41. package/forge/skills/implement-prepare-environment-script/assets/prepare_environment_python.sh +81 -0
  42. package/forge/skills/implement-unit-testing-script/SKILL.md +133 -0
  43. package/forge/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +82 -0
  44. package/forge/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +68 -0
  45. package/forge/skills/implement-unit-testing-script/assets/run_unittests_java.sh +45 -0
  46. package/forge/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +76 -0
  47. package/forge/skills/implement-unit-testing-script/assets/run_unittests_python.sh +90 -0
  48. package/forge/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +83 -0
  49. package/forge/skills/init-config-file/SKILL.md +261 -0
  50. package/forge/skills/init-plain-project/SKILL.md +124 -0
  51. package/forge/skills/load-plain-reference/SKILL.md +646 -0
  52. package/forge/skills/plain-healthcheck/SKILL.md +132 -0
  53. package/forge/skills/refactor-module/SKILL.md +197 -0
  54. package/forge/skills/resolve-spec-conflict/SKILL.md +88 -0
  55. package/forge/skills/run-codeplain/SKILL.md +540 -0
  56. package/package.json +42 -0
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env pwsh
2
+
3
+ $ErrorActionPreference = 'Stop'
4
+
5
+ $UNRECOVERABLE_ERROR_EXIT_CODE = 69
6
+
7
+ # Check if subfolder name is provided
8
+ if (-not $args[0]) {
9
+ Write-Host "Error: No subfolder name provided."
10
+ Write-Host "Usage: $($MyInvocation.MyCommand.Name) <subfolder_name>"
11
+ exit $UNRECOVERABLE_ERROR_EXIT_CODE
12
+ }
13
+
14
+ $BuildFolder = $args[0]
15
+
16
+ $GO_BUILD_SUBFOLDER = "go_$BuildFolder"
17
+
18
+ if ($env:VERBOSE -eq "1") {
19
+ Write-Host "Preparing Go build subfolder: $GO_BUILD_SUBFOLDER"
20
+ }
21
+
22
+ # Check if the go build subfolder exists
23
+ if (Test-Path $GO_BUILD_SUBFOLDER) {
24
+ # Delete all files and folders inside
25
+ Get-ChildItem -Path $GO_BUILD_SUBFOLDER -Force | Remove-Item -Recurse -Force
26
+
27
+ if ($env:VERBOSE -eq "1") {
28
+ Write-Host "Cleanup completed."
29
+ }
30
+ } else {
31
+ if ($env:VERBOSE -eq "1") {
32
+ Write-Host "Subfolder does not exist. Creating it..."
33
+ }
34
+
35
+ New-Item -ItemType Directory -Path $GO_BUILD_SUBFOLDER -Force | Out-Null
36
+ }
37
+
38
+ Copy-Item -Path "$BuildFolder/*" -Destination $GO_BUILD_SUBFOLDER -Recurse -Force
39
+
40
+ # Move to the subfolder
41
+ if (-not (Test-Path $GO_BUILD_SUBFOLDER)) {
42
+ Write-Host "Error: Go build folder '$GO_BUILD_SUBFOLDER' does not exist."
43
+ exit $UNRECOVERABLE_ERROR_EXIT_CODE
44
+ }
45
+
46
+ Push-Location $GO_BUILD_SUBFOLDER
47
+
48
+ try {
49
+ Write-Host "Running go get..."
50
+ # Temporarily allow stderr output without throwing (Go tools write to stderr)
51
+ # ForEach-Object converts ErrorRecord objects (from stderr) to plain strings to avoid verbose error formatting
52
+ $ErrorActionPreference = 'Continue'
53
+ $output = go get 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } } | Out-String
54
+ $ErrorActionPreference = 'Stop'
55
+ if ($output.Trim()) { Write-Host $output }
56
+
57
+ # Execute all Golang unittests in the subfolder
58
+ Write-Host "Running Golang unittests in $BuildFolder..."
59
+ $ErrorActionPreference = 'Continue'
60
+ $output = go test 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } } | Out-String
61
+ $exit_code = $LASTEXITCODE
62
+ $ErrorActionPreference = 'Stop'
63
+
64
+ Write-Host $output
65
+ exit $exit_code
66
+ } finally {
67
+ Pop-Location
68
+ }
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+
3
+ # Check that Java 21 is installed
4
+ if ! /usr/libexec/java_home -v 21 >/dev/null 2>&1; then
5
+ echo "Error: Java 21 is not installed."
6
+ exit 69
7
+ fi
8
+
9
+ export JAVA_HOME=$(/usr/libexec/java_home -v 21)
10
+ java --version
11
+
12
+ # Check if subfolder name is provided
13
+ if [ -z "$1" ]; then
14
+ echo "Error: No subfolder name provided."
15
+ echo "Usage: $0 <subfolder_name>"
16
+ exit 1
17
+ fi
18
+
19
+ # Define the path to the java build subfolder
20
+ WORKING_FOLDER=.tmp/$1
21
+
22
+ # Check if the java subfolder exists
23
+ if [ -d "$WORKING_FOLDER" ]; then
24
+ # delete everything in the subfolder
25
+ rm -rf "$WORKING_FOLDER"/*
26
+ else
27
+ echo "Subfolder '$WORKING_FOLDER' does not exist. Creating it now..."
28
+ mkdir -p "$WORKING_FOLDER"
29
+ fi
30
+
31
+
32
+ # copy all folders and files from the build folder to the subfolder
33
+ cp -R $1/* $WORKING_FOLDER
34
+ printf "Copied from $1 to $WORKING_FOLDER...\n"
35
+ # Move to the subfolder
36
+ cd "$WORKING_FOLDER" 2>/dev/null
37
+ printf "Moved to $WORKING_FOLDER...\n"
38
+ if [ $? -ne 0 ]; then
39
+ echo "Error: Subfolder '$1' does not exist."
40
+ exit 2
41
+ fi
42
+
43
+ # Execute all Java unittests in the subfolder
44
+ echo "Running Java unittests in $(pwd)..."
45
+ mvn test
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env pwsh
2
+
3
+ $ErrorActionPreference = 'Stop'
4
+
5
+ $UNRECOVERABLE_ERROR_EXIT_CODE = 69
6
+
7
+ # Check if subfolder name is provided
8
+ if (-not $args[0]) {
9
+ Write-Host "Error: No subfolder name provided."
10
+ Write-Host "Usage: $($MyInvocation.MyCommand.Name) <subfolder_name>"
11
+ exit $UNRECOVERABLE_ERROR_EXIT_CODE
12
+ }
13
+
14
+ $BuildFolder = $args[0]
15
+
16
+ # Try to find Python interpreter (python3 first, then python)
17
+ if (Get-Command python3 -ErrorAction SilentlyContinue) {
18
+ $PYTHON_CMD = "python3"
19
+ } elseif (Get-Command python -ErrorAction SilentlyContinue) {
20
+ $PYTHON_CMD = "python"
21
+ } else {
22
+ Write-Host "Error: Python interpreter not found. Please install Python."
23
+ exit $UNRECOVERABLE_ERROR_EXIT_CODE
24
+ }
25
+
26
+ $PYTHON_BUILD_SUBFOLDER = "python_$BuildFolder"
27
+
28
+ if ($env:VERBOSE -eq "1") {
29
+ Write-Host "Preparing Python build subfolder: $PYTHON_BUILD_SUBFOLDER"
30
+ }
31
+
32
+ # Check if the Python build subfolder exists
33
+ if (Test-Path $PYTHON_BUILD_SUBFOLDER) {
34
+ # Delete all files and folders inside
35
+ Get-ChildItem -Path $PYTHON_BUILD_SUBFOLDER -Force | Remove-Item -Recurse -Force
36
+
37
+ if ($env:VERBOSE -eq "1") {
38
+ Write-Host "Cleanup completed."
39
+ }
40
+ } else {
41
+ if ($env:VERBOSE -eq "1") {
42
+ Write-Host "Subfolder does not exist. Creating it..."
43
+ }
44
+
45
+ New-Item -ItemType Directory -Path $PYTHON_BUILD_SUBFOLDER -Force | Out-Null
46
+ }
47
+
48
+ Copy-Item -Path "$BuildFolder/*" -Destination $PYTHON_BUILD_SUBFOLDER -Recurse -Force
49
+
50
+ # Move to the subfolder
51
+ if (-not (Test-Path $PYTHON_BUILD_SUBFOLDER)) {
52
+ Write-Host "Error: Python build folder '$PYTHON_BUILD_SUBFOLDER' does not exist."
53
+ exit $UNRECOVERABLE_ERROR_EXIT_CODE
54
+ }
55
+
56
+ Push-Location $PYTHON_BUILD_SUBFOLDER
57
+
58
+ try {
59
+ # Execute all Python unittests in the subfolder
60
+ Write-Host "Running Python unittests in $PYTHON_BUILD_SUBFOLDER..."
61
+
62
+ # Temporarily allow stderr output without throwing (Python unittest writes progress to stderr)
63
+ # ForEach-Object converts ErrorRecord objects (from stderr) to plain strings to avoid verbose error formatting
64
+ $ErrorActionPreference = 'Continue'
65
+ $output = & $PYTHON_CMD -m unittest discover -b 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } } | Out-String
66
+ $exit_code = $LASTEXITCODE
67
+ $ErrorActionPreference = 'Stop'
68
+
69
+ # Echo the original output
70
+ Write-Host $output
71
+
72
+ # Return the exit code of the unittest command
73
+ exit $exit_code
74
+ } finally {
75
+ Pop-Location
76
+ }
@@ -0,0 +1,90 @@
1
+ #!/bin/bash
2
+
3
+ UNRECOVERABLE_ERROR_EXIT_CODE=69
4
+
5
+ # Check if subfolder name is provided
6
+ if [ -z "$1" ]; then
7
+ echo "Error: No subfolder name provided."
8
+ echo "Usage: $0 <subfolder_name>"
9
+ exit $UNRECOVERABLE_ERROR_EXIT_CODE
10
+ fi
11
+
12
+ current_dir=$(pwd)
13
+ echo "Current directory: $current_dir"
14
+ echo "Build folder name: $1"
15
+ echo "--------------------------------"
16
+
17
+ PYTHON_BUILD_SUBFOLDER=.tmp/$1
18
+
19
+ if [ "${VERBOSE:-}" -eq 1 ] 2>/dev/null; then
20
+ printf "Preparing Python build subfolder: $PYTHON_BUILD_SUBFOLDER\n"
21
+ fi
22
+
23
+ rm -rf $PYTHON_BUILD_SUBFOLDER
24
+ mkdir -p $PYTHON_BUILD_SUBFOLDER
25
+
26
+ cp -R $1/* $PYTHON_BUILD_SUBFOLDER
27
+
28
+ # Move to the subfolder
29
+ cd "$PYTHON_BUILD_SUBFOLDER" 2>/dev/null
30
+
31
+ if [ $? -ne 0 ]; then
32
+ printf "Error: Python build folder '$PYTHON_BUILD_SUBFOLDER' does not exist.\n"
33
+ exit $UNRECOVERABLE_ERROR_EXIT_CODE
34
+ fi
35
+
36
+ printf "Creating and activating virtual environment...\n"
37
+
38
+ # Time the virtual environment creation and activation
39
+ start_time=$(date +%s.%N)
40
+
41
+ VENV_DIR=".venv"
42
+
43
+ if ! $PYTHON_CMD -m venv "$VENV_DIR"; then
44
+ printf "Error: Failed to create virtual environment in '$VENV_DIR'.\n"
45
+ exit $UNRECOVERABLE_ERROR_EXIT_CODE
46
+ fi
47
+
48
+ # shellcheck disable=SC1091
49
+ source "$VENV_DIR/bin/activate"
50
+
51
+ if [ $? -ne 0 ]; then
52
+ printf "Error: Failed to activate virtual environment at '$VENV_DIR/bin/activate'.\n"
53
+ exit $UNRECOVERABLE_ERROR_EXIT_CODE
54
+ fi
55
+
56
+ # Install requirements if requirements.txt exists
57
+ if [ -f "requirements.txt" ]; then
58
+ pip install --upgrade pip
59
+ pip install -r requirements.txt
60
+ else
61
+ echo "Error: requirements.txt not found. Cannot proceed with setting up requirements."
62
+ fi
63
+
64
+ end_time=$(date +%s.%N)
65
+
66
+ # Calculate and display the time taken
67
+ duration=$(echo "$end_time - $start_time" | bc)
68
+ printf "Requirements setup completed in %.2f seconds\n\n" "$duration"
69
+
70
+
71
+ # Execute all Python unittests in the subfolder
72
+ echo "Running Python unittests in $PYTHON_BUILD_SUBFOLDER..."
73
+
74
+ output=$(timeout 120s python -m unittest discover -b -v 2>&1)
75
+ exit_code=$?
76
+
77
+ # Check if the command timed out
78
+ if [ $exit_code -eq 124 ]; then
79
+ printf "\nError: Unittests timed out after 120 seconds.\n"
80
+ exit $exit_code
81
+ fi
82
+
83
+ # Echo the original output
84
+ echo "$output"
85
+
86
+ # Return the exit code of the unittest command
87
+ exit $exit_code
88
+
89
+ # Note: The 'discover' option automatically identifies and runs all unittests in the current directory and subdirectories
90
+ # Ensure that your Python files are named according to the unittest discovery pattern (test*.py by default)
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env pwsh
2
+
3
+ $ErrorActionPreference = 'Stop'
4
+
5
+ $UNRECOVERABLE_ERROR_EXIT_CODE = 69
6
+
7
+ # ANSI escape code pattern to remove color codes and formatting from output
8
+ $ANSI_ESCAPE_PATTERN = '\x1b\[[0-9;]*[mK]'
9
+
10
+ # Check if subfolder name is provided
11
+ if (-not $args[0]) {
12
+ Write-Host "Error: No subfolder name provided."
13
+ Write-Host "Usage: $($MyInvocation.MyCommand.Name) <subfolder_name>"
14
+ exit $UNRECOVERABLE_ERROR_EXIT_CODE
15
+ }
16
+
17
+ $BuildFolder = $args[0]
18
+
19
+ # Define the path to the subfolder
20
+ $NODE_SUBFOLDER = "node_$BuildFolder"
21
+
22
+ if ($env:VERBOSE -eq "1") {
23
+ Write-Host "Preparing Node subfolder: $NODE_SUBFOLDER"
24
+ }
25
+
26
+ # Check if the node subfolder exists
27
+ if (Test-Path $NODE_SUBFOLDER) {
28
+ # Delete all files and folders except "node_modules", "build", and "package-lock.json"
29
+ Get-ChildItem -Path $NODE_SUBFOLDER -Force |
30
+ Where-Object {
31
+ $_.Name -ne "node_modules" -and
32
+ $_.Name -ne "build" -and
33
+ $_.Name -ne "package-lock.json"
34
+ } | Remove-Item -Recurse -Force
35
+
36
+ if ($env:VERBOSE -eq "1") {
37
+ Write-Host "Cleanup completed, keeping 'node_modules' and 'package-lock.json'."
38
+ }
39
+ } else {
40
+ if ($env:VERBOSE -eq "1") {
41
+ Write-Host "Subfolder does not exist. Creating it..."
42
+ }
43
+
44
+ New-Item -ItemType Directory -Path $NODE_SUBFOLDER -Force | Out-Null
45
+ }
46
+
47
+ Copy-Item -Path "$BuildFolder/*" -Destination $NODE_SUBFOLDER -Recurse -Force
48
+
49
+ # Move to the subfolder
50
+ if (-not (Test-Path $NODE_SUBFOLDER)) {
51
+ Write-Host "Error: Subfolder '$BuildFolder' does not exist."
52
+ exit $UNRECOVERABLE_ERROR_EXIT_CODE
53
+ }
54
+
55
+ Push-Location $NODE_SUBFOLDER
56
+
57
+ try {
58
+ # Install libraries
59
+ npm install
60
+
61
+ # Execute all React unittests in the subfolder
62
+ Write-Host "Running React unittests in $BuildFolder..."
63
+ # Temporarily allow stderr output without throwing (npm/jest may write to stderr)
64
+ # ForEach-Object converts ErrorRecord objects (from stderr) to plain strings to avoid verbose error formatting
65
+ $ErrorActionPreference = 'Continue'
66
+ $output = npm test -- --runInBand --silent --detectOpenHandles 2>&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } } | Out-String
67
+ $TEST_EXIT_CODE = $LASTEXITCODE
68
+ $ErrorActionPreference = 'Stop'
69
+
70
+ # Strip ANSI escape codes
71
+ $output = $output -replace $ANSI_ESCAPE_PATTERN, ''
72
+ Write-Host $output
73
+
74
+ # Check if tests failed
75
+ if ($TEST_EXIT_CODE -ne 0) {
76
+ Write-Host "Error: Tests failed with exit code $TEST_EXIT_CODE"
77
+ exit $TEST_EXIT_CODE
78
+ }
79
+
80
+ exit $TEST_EXIT_CODE
81
+ } finally {
82
+ Pop-Location
83
+ }
@@ -0,0 +1,261 @@
1
+ ---
2
+ name: init-config-file
3
+ description: >-
4
+ Build / finalize the `config.yaml` file(s) that the `codeplain` renderer
5
+ consumes. Pulls together every decision made during Phase 3 of `forge-plain`
6
+ (script paths, template directory, build folders, copy/dest behavior, log
7
+ settings) and emits one canonical `config.yaml` per part of the project.
8
+ Run this at the **end of `forge-plain`** (just before `plain-healthcheck`),
9
+ at the end of `add-feature` whenever the testing surface or template
10
+ directory changed, and any time the user wants to regenerate / consolidate
11
+ a project's `config.yaml`.
12
+ ---
13
+
14
+ # Init Config File
15
+
16
+ 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.
17
+
18
+ ## When to run
19
+
20
+ - **End of `forge-plain` Phase 3 / start of Phase 4** — after every test-script decision is locked in (unit tests, conformance tests, prepare-environment), before delegating to `plain-healthcheck`.
21
+ - **End of `add-feature`** — only when Phase 3 of the feature touched the testing surface (new script generated, script removed, template directory introduced).
22
+ - **End of any single-skill workflow that finalizes a script or template** — e.g. after `implement-unit-testing-script`, `implement-conformance-testing-script`, `implement-prepare-environment-script`, `add-template`, or `create-import-module`.
23
+ - **On demand** — when the user asks "rebuild my config", "what valid keys are there", or you discover the config file is hand-edited / inconsistent.
24
+
25
+ If you only fixed a typo inside a `.plain` file and the testing surface didn't move, you do **not** need to re-run this skill — go straight to `plain-healthcheck`.
26
+
27
+ ## What this skill does
28
+
29
+ 1. Determines **how many** `config.yaml` files the project needs (one per part — see [Per-part split](#per-part-split)).
30
+ 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).
31
+ 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).
32
+ 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 lookup rule the renderer uses: absolute path → path relative to the config file's directory → path relative to the renderer's directory.
33
+ 5. Hands off to `plain-healthcheck` for the full validation pass.
34
+
35
+ ## What this skill does NOT do
36
+
37
+ - 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.
38
+ - 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.
39
+ - 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.
40
+ - It does **not** write secrets. `api-key` belongs in the `CODEPLAIN_API_KEY` environment variable, never in `config.yaml`.
41
+
42
+ ## Valid keys reference
43
+
44
+ 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}")`.
45
+
46
+ 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.
47
+
48
+ ### Keys you typically include
49
+
50
+ These keys reflect choices made in Phase 3 of `forge-plain` and are the bread and butter of a project's `config.yaml`:
51
+
52
+ | Key | Type | Default | When to include |
53
+ |---|---|---|---|
54
+ | `unittests-script` | path (string) | — | **Required.** Every project gets a unit-test runner. Path resolves relative to the config file's directory (preferred) or the renderer directory. |
55
+ | `conformance-tests-script` | path (string) | — | Include when the user opted into conformance testing in Phase 3. |
56
+ | `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. |
57
+ | `test-script-timeout` | int (seconds) | `120` | Include only when the user explicitly raised/lowered the default. |
58
+ | `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. |
59
+ | `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`). |
60
+ | `conformance-tests-folder` | string | `conformance_tests` | Include only when the user picked a non-default folder name. |
61
+ | `build-folder` | string | `plain_modules` | Include only when the user picked a non-default folder name. Must differ from `build-dest`. |
62
+ | `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`. |
63
+ | `base-folder` | string | — | Include when the user wants build output rooted somewhere other than the project root. |
64
+
65
+ ### Keys you occasionally include
66
+
67
+ These are useful but the defaults are almost always fine. Only include them when the user explicitly changed the default during Phase 3:
68
+
69
+ | Key | Type | Default | Notes |
70
+ |---|---|---|---|
71
+ | `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. |
72
+ | `copy-conformance-tests` | bool | `false` | Requires `conformance-tests-script` to also be set. |
73
+ | `conformance-tests-dest` | string | `dist_conformance_tests` | Target folder for the conformance-test copy. Must differ from `conformance-tests-folder`. |
74
+ | `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). |
75
+ | `log-file-name` | string | `codeplain.log` | If `log-to-file` is `false`, this key must be left out. Resolved relative to the `.plain` file directory. |
76
+ | `render-machine-graph` | bool | `false` | Include only when the user wants the state-machine graph rendered. |
77
+ | `headless` | bool | `false` | Include only when the project is meant to run in CI / non-interactive mode by default. |
78
+ | `force-render` | bool | `false` | Almost never belongs in `config.yaml`; prefer the CLI flag for one-off forced renders. |
79
+ | `verbose` | bool | `false` | Almost never belongs in `config.yaml`; prefer the CLI flag for one-off verbose runs. |
80
+ | `api` | URL (string) | `https://api.codeplain.ai` | Include only when the user is pointing at a non-default API endpoint. |
81
+
82
+ ### Keys you must NEVER include
83
+
84
+ These flags are **per-invocation** or **secret**. Putting them in `config.yaml` is always wrong:
85
+
86
+ | Key | Why it doesn't belong |
87
+ |---|---|
88
+ | `api-key` | Secret. Belongs in the `CODEPLAIN_API_KEY` environment variable. Never in a file the user might commit. |
89
+ | `dry-run` | Per-invocation. `plain-healthcheck` runs the dry-run explicitly; pinning it in the config would make a real render impossible. |
90
+ | `full-plain` | Per-invocation preview. Mutually exclusive with `dry-run`. |
91
+ | `render-range` | Per-invocation. Selects a slice of functionalities to (re)render. |
92
+ | `render-from` | Per-invocation. Mutually exclusive with `render-range`. |
93
+ | `replay-with` | Internal / debugging flag. |
94
+ | `config-name` | Refers to the config file itself — the renderer ignores it when reading the config. |
95
+ | `filename` | The `.plain` file to render is always passed positionally on the CLI. |
96
+
97
+ If the user asks to put any of these in `config.yaml`, refuse and explain why.
98
+
99
+ ## Logging configuration
100
+
101
+ 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.
102
+
103
+ ### How the renderer assembles logging
104
+
105
+ From `setup_logging` in the `codeplain` source:
106
+
107
+ 1. The renderer first installs a set of **baseline levels**:
108
+ - root logger → `INFO`
109
+ - `LOGGER_NAME` (the renderer's own logger) → `INFO`
110
+ - `git` → `WARNING`
111
+ - `transitions` → `ERROR`
112
+ - `transitions.extensions.diagrams` → `ERROR`
113
+ 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.
114
+ 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.
115
+
116
+ 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.
117
+
118
+ ### Default behavior
119
+
120
+ - The CLI default value for `logging-config-path` is `logging_config.yaml`. If a file by that exact name exists in the current working directory, it will be loaded automatically — even without `logging-config-path` being set in `config.yaml`.
121
+ - If the file does not exist, the renderer silently keeps the baseline levels from step 1 above (no warning).
122
+ - If the file exists but fails to parse / apply, the renderer warns (`Failed to load logging configuration from …`) and falls back to the baseline.
123
+
124
+ 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:
125
+
126
+ | Situation | What to do |
127
+ |---|---|
128
+ | 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`. |
129
+ | 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. |
130
+ | 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: <that path>` in `config.yaml`. |
131
+
132
+ 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.
133
+
134
+ ### Recommended logging config
135
+
136
+ 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):
137
+
138
+ ```/dev/null/logging_config.yaml.example#L1-15
139
+ version: 1
140
+ disable_existing_loggers: false
141
+ formatters:
142
+ default:
143
+ format: "%(levelname)s:%(name)s:%(message)s"
144
+ handlers:
145
+ console:
146
+ class: logging.StreamHandler
147
+ formatter: default
148
+ level: DEBUG
149
+ loggers:
150
+ codeplain:
151
+ level: DEBUG
152
+ urllib3:
153
+ level: WARNING
154
+ root:
155
+ level: INFO
156
+ handlers: [console]
157
+ ```
158
+
159
+ Guidelines for what to put in this file:
160
+
161
+ - Always set `version: 1` — `dictConfig` requires it.
162
+ - 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.
163
+ - Only override levels the user actually asked about. Don't preemptively add every logger the codebase touches — that creates ongoing maintenance for no benefit.
164
+ - Don't put the `LoggingHandler` / `CrashLogHandler` / `FileHandler` here — the renderer attaches those itself after `dictConfig` runs. Adding them here will cause duplicate log lines.
165
+ - This file is **not** validated by `plain-healthcheck`. If you change it, ask the user to confirm by reading it back to them.
166
+
167
+ ## Per-part split
168
+
169
+ The rule, which mirrors what `forge-plain` Phase 3 already establishes, is **one `config.yaml` per part of the system that has its own testing scripts**:
170
+
171
+ - **Single-stack project** (e.g. one Python service) → one `config.yaml` at the project root.
172
+ - **Multi-part project** (e.g. Python backend + React frontend) → one `config.yaml` per part, placed next to the part's top module (e.g. `backend/config.yaml`, `frontend/config.yaml`). Each config references only its own scripts; **never mix stacks in a single config**.
173
+ - A part's split should follow the module boundaries from Phase 1 / Phase 2: if a module has its own language, framework, and test scripts, it gets its own `config.yaml` next to that module.
174
+
175
+ Before emitting anything, state the planned split to the user (e.g. "I'll emit `backend/config.yaml` and `frontend/config.yaml`") if there is more than one part.
176
+
177
+ ## Workflow
178
+
179
+ ### Step 1 — Inventory
180
+
181
+ 1. List every `.plain` file in the repo and identify the top modules (modules not `requires`-ed by anything else) — same procedure as `plain-healthcheck` Step 1.
182
+ 2. For each top module, determine which part it belongs to (single-stack → one part; multi-part → one part per top module).
183
+ 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).
184
+ 4. Identify the template directory (typically `template/`) and any custom resource directories (typically `resources/`).
185
+ 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.
186
+
187
+ ### Step 2 — Assemble per-part values
188
+
189
+ For each part:
190
+
191
+ 1. Start from an empty key set.
192
+ 2. Add `unittests-script: test_scripts/run_unittests_<lang>.<sh|ps1>` — required. If the script doesn't exist yet, stop and tell the caller to run `implement-unit-testing-script` first.
193
+ 3. If the part has a conformance script on disk → add `conformance-tests-script: …`.
194
+ 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).
195
+ 5. If the project has shared templates → add `template-dir: template` (or whatever path the user used).
196
+ 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.
197
+ 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.
198
+ 8. Cross-validate the assembled key set:
199
+ - `build-dest` is set to `dist`.
200
+ - `build-folder` ≠ `build-dest` (in particular, `build-folder` is never `dist`).
201
+ - `conformance-tests-folder` ≠ `conformance-tests-dest`.
202
+ - `copy-conformance-tests: true` requires `conformance-tests-script`.
203
+ - `log-file-name` is set ⇒ `log-to-file` is not `false`.
204
+ - All `*-script` paths resolve on disk (absolute → relative to config dir → relative to renderer dir).
205
+ - No script path crosses stacks (e.g. `backend/config.yaml` must not reference `run_unittests_js.sh`).
206
+
207
+ ### Step 3 — Emit `config.yaml`
208
+
209
+ For each part, write a clean YAML file:
210
+
211
+ - 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).
212
+ - Use dashed key names. Quote string values only when YAML requires it.
213
+ - No comments inside the file — keep it machine-parseable. If the user needs a comment, put it in the surrounding spec or README.
214
+ - Idempotent: re-running this skill on an unchanged project produces a byte-for-byte identical file.
215
+
216
+ Example for a single-stack Python project with conformance testing and a prepare-environment script:
217
+
218
+ ```/dev/null/config.yaml.example#L1-5
219
+ unittests-script: test_scripts/run_unittests_python.sh
220
+ conformance-tests-script: test_scripts/run_conformance_tests_python.sh
221
+ prepare-environment-script: test_scripts/prepare_environment_python.sh
222
+ template-dir: template
223
+ build-dest: dist
224
+ ```
225
+
226
+ Example for a multi-part project (`backend/config.yaml`):
227
+
228
+ ```/dev/null/config.yaml.example#L1-4
229
+ unittests-script: test_scripts/run_unittests_python.sh
230
+ conformance-tests-script: test_scripts/run_conformance_tests_python.sh
231
+ template-dir: ../template
232
+ build-dest: dist
233
+ ```
234
+
235
+ ### Step 4 — Hand off
236
+
237
+ Tell the caller exactly which file(s) were written and invoke `plain-healthcheck` to validate the project end-to-end. Do **not** declare success on your own — `plain-healthcheck` is the source of truth for "is this project ready to render?".
238
+
239
+ ## Anti-patterns
240
+
241
+ - **Inventing values for keys the user never decided on.** Leave them out and let the renderer use its default.
242
+ - **Mixing stacks in one config.** `backend/config.yaml` referencing a JS script is always a bug — split into per-part configs instead.
243
+ - **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.
244
+ - **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.
245
+ - **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).
246
+ - **Reading the renderer's API key from a project file.** Always rely on `CODEPLAIN_API_KEY`.
247
+
248
+ ## Validation Checklist
249
+
250
+ - [ ] One `config.yaml` exists per part of the system (single-stack → root; multi-part → per part).
251
+ - [ ] Every `config.yaml` has at minimum `unittests-script`.
252
+ - [ ] Every `*-script` value points at a file that exists on disk.
253
+ - [ ] No `config.yaml` declares `prepare-environment-script` without also declaring `conformance-tests-script`.
254
+ - [ ] No `config.yaml` mixes stacks (every script in it targets the same language).
255
+ - [ ] `build-dest` is set to `dist` in every emitted `config.yaml`.
256
+ - [ ] `build-folder` ≠ `build-dest`; `conformance-tests-folder` ≠ `conformance-tests-dest`.
257
+ - [ ] `copy-conformance-tests: true` only when `conformance-tests-script` is set.
258
+ - [ ] `log-file-name` only when `log-to-file` is not `false`.
259
+ - [ ] No forbidden keys (`api-key`, `dry-run`, `full-plain`, `render-range`, `render-from`, `replay-with`, `config-name`, `filename`).
260
+ - [ ] `template-dir` set whenever the project has shared templates or import modules.
261
+ - [ ] `plain-healthcheck` returned `PASS` after the config(s) were written.