plain-forge 1.0.18 → 1.0.20

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 (50) hide show
  1. package/README.md +17 -10
  2. package/bin/cli.mjs +53 -24
  3. package/forge/rules/bullet-continuation.md +59 -0
  4. package/forge/rules/definitions.md +15 -0
  5. package/forge/rules/func-specs.md +52 -7
  6. package/forge/rules/impl-reqs.md +1 -1
  7. package/forge/rules/integration-embedded-testing.md +7 -4
  8. package/forge/rules/integration-embedded.md +4 -1
  9. package/forge/rules/linked-resources.md +7 -6
  10. package/forge/rules/requires-modules.md +1 -1
  11. package/forge/rules/test-reqs.md +1 -1
  12. package/forge/skills/add-acceptance-test/SKILL.md +3 -25
  13. package/forge/skills/add-concept/SKILL.md +3 -22
  14. package/forge/skills/add-feature/SKILL.md +71 -67
  15. package/forge/skills/add-feature/references/checklist.md +38 -0
  16. package/forge/skills/add-feature/references/phase-0-intent.md +29 -0
  17. package/forge/skills/add-functional-spec/SKILL.md +3 -23
  18. package/forge/skills/add-functional-specs/SKILL.md +3 -24
  19. package/forge/skills/add-implementation-requirement/SKILL.md +4 -23
  20. package/forge/skills/add-resource/SKILL.md +7 -8
  21. package/forge/skills/add-test-requirement/SKILL.md +4 -21
  22. package/forge/skills/analyze-2-func-specs/SKILL.md +10 -10
  23. package/forge/skills/analyze-func-specs/SKILL.md +10 -10
  24. package/forge/skills/analyze-if-func-spec-too-complex/SKILL.md +93 -31
  25. package/forge/skills/break-down-func-spec/SKILL.md +78 -29
  26. package/forge/skills/create-import-module/SKILL.md +7 -0
  27. package/forge/skills/create-requires-module/SKILL.md +3 -2
  28. package/forge/skills/debug-specs/SKILL.md +5 -5
  29. package/forge/skills/forge-plain/SKILL.md +80 -299
  30. package/forge/skills/forge-plain/references/checklist.md +66 -0
  31. package/forge/skills/forge-plain/references/phase-0-intent.md +29 -0
  32. package/forge/skills/forge-plain/references/phase-1-product.md +40 -0
  33. package/forge/skills/forge-plain/references/phase-2-tech.md +24 -0
  34. package/forge/skills/forge-plain/references/phase-3-testing.md +66 -0
  35. package/forge/skills/forge-plain/references/phase-4-validate-handoff.md +49 -0
  36. package/forge/skills/implement-conformance-testing-script/SKILL.md +2 -2
  37. package/forge/skills/implement-prepare-environment-script/SKILL.md +2 -2
  38. package/forge/skills/implement-unit-testing-script/SKILL.md +1 -1
  39. package/forge/skills/init-config-file/SKILL.md +4 -3
  40. package/forge/skills/init-plain-project/SKILL.md +4 -0
  41. package/forge/skills/init-plain-project/references/checklist.md +31 -0
  42. package/forge/skills/load-codeplain-reference/SKILL.md +188 -0
  43. package/forge/skills/load-plain-reference/SKILL.md +48 -678
  44. package/forge/skills/load-plain-reference/references/project-model.md +48 -0
  45. package/forge/skills/load-plain-reference/references/rendering-and-testing.md +57 -0
  46. package/forge/skills/plain-healthcheck/SKILL.md +3 -3
  47. package/forge/skills/resolve-spec-conflict/SKILL.md +13 -5
  48. package/forge/skills/run-codeplain/SKILL.md +15 -15
  49. package/package.json +2 -2
  50. package/forge/rules/line-length.md +0 -45
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # plain-forge
6
6
 
7
- 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.
7
+ 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.
8
8
 
9
9
  ## What plain-forge does
10
10
 
@@ -20,7 +20,7 @@ Pick whichever entry point matches how much upfront design you want:
20
20
  ### 2. Grow an existing project
21
21
 
22
22
  - **`add-feature`** — takes a feature request in plain English and runs the same one-question-at-a-time loop that `forge-plain` uses, scoped to a single feature against an existing `.plain` file.
23
- - **Per-section authoring skills** — `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `add-test-requirement`, `add-concept`, `add-acceptance-test`, `add-resource`, `add-template`. Each enforces the relevant ***plain syntax rules (concept uniqueness, complexity limits, line-length, linked-resource constraints, …) so hand-authoring doesn't drift from the language.
23
+ - **Per-section authoring skills** — `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `add-test-requirement`, `add-concept`, `add-acceptance-test`, `add-resource`, `add-template`. Each enforces the relevant ***plain syntax rules (concept uniqueness, complexity limits, bullet syntax, linked-resource constraints, …) so hand-authoring doesn't drift from the language.
24
24
  - **Module-management skills** — `create-import-module`, `create-requires-module`, `refactor-module`, `consolidate-concepts` for restructuring as the project grows.
25
25
 
26
26
  ### 3. Validate and maintain
@@ -62,11 +62,15 @@ npx plain-forge install --agent claude --scope project
62
62
  |-----------|---------------|----------|
63
63
  | `claude` | `.claude/` · `~/.claude/` | You use Claude Code |
64
64
  | `codex` | `.agents/` · `~/.agents/` | You use the OpenAI Codex CLI |
65
+ | `copilot` | `.agents/` · `~/.agents/` | You use GitHub Copilot |
65
66
  | `forgecode` | `.forge/` · `~/forge/` | You use ForgeCode |
66
67
  | `opencode` | `.opencode/` · `~/.config/opencode/` | You use OpenCode |
67
68
  | `universal` | `.agents/` · `~/.agents/` | You want a runtime-neutral layout that any agent reading from `.agents/` can pick up |
68
69
 
69
- > **Why `codex` and `forgecode` don't install into `.codex/` or `.forgecode/`.** Those tools load *skills* from specific directories that are **not** named after the tool. Per the current (2026) docs, Codex reads skills from `.agents/skills` (`~/.agents/skills` globally) — `.codex/` is config-only — and ForgeCode reads them from `.forge/skills` (`~/forge/skills` globally). plain-forge installs where the skills will actually load. As a result `codex` and `universal` share the `.agents/` layout; the install manifest records which agent you chose so `update`/`uninstall` behave correctly.
70
+ > **Why several agents share `.agents/`.** Codex and GitHub Copilot both discover skills from
71
+ > `.agents/skills` (`~/.agents/skills` globally), which is also the runtime-neutral `universal`
72
+ > layout. ForgeCode instead reads `.forge/skills` (`~/forge/skills` globally). The manifest records
73
+ > which agent label you chose so `update` and `uninstall` report it correctly.
70
74
 
71
75
  **Scope options:**
72
76
 
@@ -88,16 +92,19 @@ Each install writes three subfolders under the chosen directory:
88
92
 
89
93
  #### How the rules get applied per agent
90
94
 
91
- The `skills/` are discovered natively by every supported agent. The `rules/` (the `.plain`-authoring guidance) are applied differently because each agent loads instruction files its own way — so plain-forge does a bit of extra, agent-specific wiring:
95
+ The `skills/` are discovered natively by every supported agent. The `load-plain-reference` skill
96
+ loads only the rules relevant to the current task and skips rules already present in context. Some
97
+ agents additionally support native rules wiring:
92
98
 
93
99
  | Agent | How rules are applied |
94
100
  |-------|-----------------------|
95
101
  | **Claude Code** | Native — Claude Code auto-loads `.claude/rules/*.md` at launch. Each rule carries `paths: "**/*.plain"` frontmatter so Claude scopes it to `.plain` files. No extra wiring. |
96
102
  | **OpenCode** | OpenCode doesn't auto-load a `rules/` dir; it reads globs from the `instructions` array in `opencode.json`. plain-forge **merges** a rules glob there — `.opencode/rules/*.md` (project, written to `./opencode.json`) or the absolute `~/.config/opencode/rules/*.md` (global, written to `~/.config/opencode/opencode.json`; absolute because OpenCode doesn't expand `~`). |
97
- | **Codex / ForgeCode** | Neither auto-loads a `rules/` dir; both read custom instructions only from `AGENTS.md`. plain-forge appends a fenced, **managed pointer block** to `AGENTS.md` telling the agent to read the installed rule files. Project → repo-root `./AGENTS.md`; global → `~/.codex/AGENTS.md` (Codex) or `~/forge/AGENTS.md` (ForgeCode). |
98
- | **Universal** | Verbatim copy only — the rules land in `.agents/rules/`, and it's up to whichever agent reads `.agents/` to consume them. No config is generated. |
103
+ | **ForgeCode** | plain-forge also appends a fenced managed pointer block to `AGENTS.md`: repo-root `./AGENTS.md` for project installs or `~/forge/AGENTS.md` globally. |
104
+ | **Codex / GitHub Copilot / Universal** | No extra instruction file is generated. Their discovered `load-plain-reference` skill loads applicable files from `.agents/rules/` when needed. |
99
105
 
100
- In every case where plain-forge touches a config or `AGENTS.md` file, it **merges** into what's already there — adding only its own glob/block and leaving all your other content intact — and it never writes into a malformed config (it warns instead). `uninstall` reverses the wiring: it removes only plain-forge's glob/block, deleting the file only if it contained nothing else.
106
+ When plain-forge touches `opencode.json` or ForgeCode's `AGENTS.md`, it merges into existing content
107
+ and leaves unrelated configuration intact. `uninstall` removes only plain-forge's glob or block.
101
108
 
102
109
  `install` refuses to run if plain-forge is already present at the target directory — it prints a message pointing you at `update` and exits non-zero. Use `update` (below) to refresh an existing install.
103
110
 
@@ -129,7 +136,7 @@ npx plain-forge uninstall --agent claude --scope global
129
136
 
130
137
  | Flag | Default | Values |
131
138
  |------|---------|--------|
132
- | `--agent` | `*` (all agents) | `claude`, `codex`, `forgecode`, `opencode`, `universal`, or `*` |
139
+ | `--agent` | `*` (all agents) | `claude`, `codex`, `copilot`, `forgecode`, `opencode`, `universal`, or `*` |
133
140
  | `--scope` | `project` | `project` (cwd) or `global` (home directory) |
134
141
 
135
142
  If an install has **no manifest** (e.g. one that predates manifests), `uninstall` cannot tell which files are plain-forge's, so it refuses to delete anything: it prints an error, lists the directories to clean up by hand, and exits non-zero. Refresh such an install with `update` first (which writes a manifest going forward), then `uninstall`.
@@ -188,7 +195,7 @@ Hit a bug in the rendered app, a failing test, or behavior that doesn't match wh
188
195
  2. plain-forge applies the fix in the `.plain` file(s) only and summarizes what changed.
189
196
  3. Re-render to regenerate the code (see [Rendering specs](#rendering-specs)).
190
197
 
191
- > **Important:** Never edit generated code under `plain_modules/` or `conformance_tests/` directly — your changes will be overwritten on the next render. Always fix the spec and re-render.
198
+ > **Important:** Never edit generated output under `plain_modules/` directly — neither the code in `plain_modules/<module>/code/` nor the conformance tests in `plain_modules/<module>/tests/`. Your changes will be overwritten on the next render. Always fix the spec and re-render.
192
199
 
193
200
 
194
201
  ## Repository Structure
@@ -209,7 +216,7 @@ test/
209
216
  package.json # ships only `bin/cli.mjs` and `forge/` to npm
210
217
  ```
211
218
 
212
- On `install`, the CLI reads `forge/skills` and `forge/rules` and writes them into the chosen agent directory (`.claude/`, `.agents/` for Codex/universal, `.forge/`, or `.opencode/` — see the per-agent table above for global-scope paths), recording every file it wrote in `<agent-dir>/.plain-forge/manifest.json` so `update` can later refresh and prune precisely. For agents that need it, it also wires the rules into `opencode.json` or `AGENTS.md` (see [How the rules get applied per agent](#how-the-rules-get-applied-per-agent)).
219
+ On `install`, the CLI reads `forge/skills` and `forge/rules` and writes them into the chosen agent directory (`.claude/`, `.agents/` for Codex/Copilot/universal, `.forge/`, or `.opencode/` — see the per-agent table above for global-scope paths), recording every file it wrote in `<agent-dir>/.plain-forge/manifest.json` so `update` can later refresh and prune precisely. For agents that need it, it also wires the rules into `opencode.json` or ForgeCode's `AGENTS.md` (see [How the rules get applied per agent](#how-the-rules-get-applied-per-agent)).
213
220
 
214
221
  ## Available Skills
215
222
 
package/bin/cli.mjs CHANGED
@@ -13,7 +13,8 @@ const forgeDir = path.join(pkgRoot, "forge");
13
13
  // These are the directories each tool actually loads skills from (verified
14
14
  // against the 2026 docs), NOT necessarily a dir named after the tool:
15
15
  // - Codex reads skills from .agents/skills (never .codex/skills); .codex is
16
- // config-only. So codex shares the .agents layout with `universal`.
16
+ // config-only. So codex shares the .agents layout with `copilot` and
17
+ // `universal`.
17
18
  // - ForgeCode reads skills from .forge/skills (never .forgecode/skills); its
18
19
  // global dir is ~/forge (no dot) — see resolveBaseDir.
19
20
  // - OpenCode reads project skills from .opencode; global is ~/.config/opencode
@@ -23,6 +24,7 @@ const forgeDir = path.join(pkgRoot, "forge");
23
24
  const AGENTS = {
24
25
  claude: ".claude",
25
26
  codex: ".agents",
27
+ copilot: ".agents",
26
28
  forgecode: ".forge",
27
29
  opencode: ".opencode",
28
30
  universal: ".agents",
@@ -53,16 +55,32 @@ function hasForgeSignature(baseDir) {
53
55
  );
54
56
  }
55
57
 
56
- const BANNER = `\x1b[38;2;224;255;110m██████╗ ██╗ █████╗ ██╗███╗ ██╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
58
+ const BANNER = `██████╗ ██╗ █████╗ ██╗███╗ ██╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
57
59
  ██╔══██╗██║ ██╔══██╗██║████╗ ██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝
58
60
  ██████╔╝██║ ███████║██║██╔██╗ ██║█████╗█████╗ ██║ ██║██████╔╝██║ ███╗█████╗
59
61
  ██╔═══╝ ██║ ██╔══██║██║██║╚██╗██║╚════╝██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝
60
62
  ██║ ███████╗██║ ██║██║██║ ╚████║ ██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗
61
- ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝\x1b[0m
63
+ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝
62
64
  `;
63
65
 
64
66
  const TAGLINE = "turn ideas into ***plain specs.";
65
67
 
68
+ function terminalHasLightBackground(env = process.env) {
69
+ const parts = String(env.COLORFGBG ?? "").split(";");
70
+ const background = Number(parts.at(-1));
71
+ if (!Number.isInteger(background)) return false;
72
+ return background === 7 || background >= 9;
73
+ }
74
+
75
+ function terminalPalette(env = process.env) {
76
+ return terminalHasLightBackground(env)
77
+ ? { brand: "76;92;0", plain: "0;105;55", link: "0;85;170" }
78
+ : { brand: "224;255;110", plain: "121;252;150", link: "95;175;255" };
79
+ }
80
+
81
+ const color = (rgb, text, extra = "") =>
82
+ `\x1b[${extra}38;2;${rgb}m${text}\x1b[0m`;
83
+
66
84
  function stripAnsi(s) {
67
85
  return s.replace(/\x1b\[[0-9;]*m/g, "");
68
86
  }
@@ -75,13 +93,14 @@ function bannerWidth() {
75
93
 
76
94
  function printBanner() {
77
95
  if (!process.stdout.isTTY) return;
78
- process.stdout.write("\n" + BANNER + "\n");
96
+ const palette = terminalPalette();
97
+ process.stdout.write("\n" + color(palette.brand, BANNER) + "\n");
79
98
 
80
99
  const width = bannerWidth();
81
100
  const pad = Math.max(0, Math.floor((width - TAGLINE.length) / 2));
82
101
  const tag = TAGLINE.replace(
83
102
  "***plain",
84
- "\x1b[38;2;121;252;150m***plain\x1b[0m",
103
+ color(palette.plain, "***plain"),
85
104
  );
86
105
  process.stdout.write(" ".repeat(pad) + tag + "\n\n");
87
106
  }
@@ -95,7 +114,7 @@ Commands:
95
114
  uninstall Remove a plain-forge install using its manifest
96
115
 
97
116
  Install options:
98
- --agent <claude|codex|forgecode|opencode|universal>
117
+ --agent <claude|codex|copilot|forgecode|opencode|universal>
99
118
  Target agent layout
100
119
  --scope <project|global> Install into cwd or $HOME
101
120
  -h, --help Show this help
@@ -105,7 +124,7 @@ Update options:
105
124
  confirming each one
106
125
 
107
126
  Uninstall options:
108
- --agent <claude|codex|forgecode|opencode|universal|*>
127
+ --agent <claude|codex|copilot|forgecode|opencode|universal|*>
109
128
  Which install to remove
110
129
  (default: * — every agent)
111
130
  --scope <project|global> Where to look (default: project)
@@ -462,10 +481,10 @@ function reportOpencodeUnmerge(result) {
462
481
  }
463
482
  }
464
483
 
465
- // --- Codex / ForgeCode instructions wiring ----------------------------------
466
- // Neither tool auto-reads a rules/ directory; both load custom instructions
467
- // only from AGENTS.md (Codex: repo AGENTS.md or ~/.codex/AGENTS.md; ForgeCode:
468
- // repo AGENTS.md or ~/forge/AGENTS.md). AGENTS.md has no include/glob mechanism,
484
+ // --- ForgeCode instructions wiring ------------------------------------------
485
+ // ForgeCode does not auto-read a rules/ directory; it loads custom instructions
486
+ // from AGENTS.md (repo AGENTS.md or ~/forge/AGENTS.md). AGENTS.md has no
487
+ // include/glob mechanism,
469
488
  // so we append a managed pointer block that tells the agent to read our rules
470
489
  // dir when touching .plain files. The block is fenced by markers so it can be
471
490
  // refreshed or removed idempotently without disturbing the user's own content.
@@ -476,7 +495,7 @@ const AGENTS_MD_END = "<!-- END plain-forge (managed) -->";
476
495
  // Agents whose rules are delivered through AGENTS.md rather than a natively
477
496
  // auto-loaded rules/ dir (claude) or a config glob (opencode).
478
497
  function usesAgentsMd(agent) {
479
- return agent === "codex" || agent === "forgecode";
498
+ return agent === "forgecode";
480
499
  }
481
500
 
482
501
  const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -485,8 +504,10 @@ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
485
504
  // root (cwd) for both tools; global scope differs per tool.
486
505
  function agentsMdPath(agent, scope) {
487
506
  if (scope === "project") return path.join(process.cwd(), "AGENTS.md");
488
- if (agent === "forgecode") return path.join(os.homedir(), "forge", "AGENTS.md");
489
- return path.join(os.homedir(), ".codex", "AGENTS.md"); // codex global
507
+ // Retained only so update/uninstall can remove blocks created by older Codex
508
+ // installs. New Codex installs do not create this file.
509
+ if (agent === "codex") return path.join(os.homedir(), ".codex", "AGENTS.md");
510
+ return path.join(os.homedir(), "forge", "AGENTS.md");
490
511
  }
491
512
 
492
513
  // Glob the pointer block references — the installed rules dir. Project scope is
@@ -603,6 +624,10 @@ function wireRules(agent, scope) {
603
624
  reportOpencodeMerge(mergeOpencodeInstructions(scope));
604
625
  } else if (usesAgentsMd(agent)) {
605
626
  reportAgentsMdMerge(mergeAgentsMd(agent, scope));
627
+ } else if (agent === "codex") {
628
+ // Migrate installs from the former Codex AGENTS.md wiring. Rules are now
629
+ // loaded by load-plain-reference itself.
630
+ reportAgentsMdUnmerge(unmergeAgentsMd(agent, scope));
606
631
  }
607
632
  } catch (err) {
608
633
  warnRulesWiring("wire", agent, err);
@@ -616,7 +641,7 @@ function unwireRules(agent, scope) {
616
641
  try {
617
642
  if (agent === "opencode") {
618
643
  reportOpencodeUnmerge(unmergeOpencodeInstructions(scope));
619
- } else if (usesAgentsMd(agent)) {
644
+ } else if (usesAgentsMd(agent) || agent === "codex") {
620
645
  reportAgentsMdUnmerge(unmergeAgentsMd(agent, scope));
621
646
  }
622
647
  } catch (err) {
@@ -655,8 +680,8 @@ function writeManifest(baseDir, files, agent) {
655
680
  const target = manifestPathFor(baseDir);
656
681
  fs.mkdirSync(path.dirname(target), { recursive: true });
657
682
  // `agent` records which agent layout produced this install. It matters when
658
- // two agents resolve to the same dir (codex and universal both use .agents):
659
- // it lets detect/update/uninstall know whether AGENTS.md rule-wiring applies.
683
+ // Multiple agents resolve to the same .agents dir, so the manifest preserves
684
+ // the selected label for detection, updates, and uninstall output.
660
685
  const manifest = {
661
686
  name: "plain-forge",
662
687
  version: readPkgVersion(),
@@ -714,7 +739,7 @@ function detectInstalls() {
714
739
  for (const scope of SCOPES) {
715
740
  for (const agent of Object.keys(AGENTS)) {
716
741
  const baseDir = resolveBaseDir(agent, scope);
717
- // Some agents share a directory (codex and universal both use .agents).
742
+ // Some agents share the .agents directory.
718
743
  // Only inspect each physical dir once so a single install isn't detected
719
744
  // — and later updated/pruned — twice.
720
745
  if (seen.has(baseDir)) continue;
@@ -929,8 +954,7 @@ async function cmdUninstall(args) {
929
954
  );
930
955
 
931
956
  // Undo the rules wiring (opencode.json / AGENTS.md). Keyed off the agent
932
- // recorded at install time, not the requested name — codex and universal
933
- // share the .agents dir, and only the codex install wired AGENTS.md. Done
957
+ // recorded at install time, not the requested name. Done
934
958
  // before the dir cleanup so a now-orphaned config file living inside baseDir
935
959
  // (e.g. forgecode-global ~/forge/AGENTS.md) can be removed and let baseDir
936
960
  // itself be pruned.
@@ -960,11 +984,12 @@ async function cmdUninstall(args) {
960
984
  }
961
985
 
962
986
  function printNextSteps(agent) {
963
- const bold = (s) => `\x1b[1;97m${s}\x1b[0m`;
987
+ const palette = terminalPalette();
988
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
964
989
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
965
- const plain = (s) => `\x1b[38;2;121;252;150m${s}\x1b[0m`;
966
- const codeplain = (s) => `\x1b[38;2;224;255;110m${s}\x1b[0m`;
967
- const link = (s) => `\x1b[4;38;2;95;175;255m${s}\x1b[0m`;
990
+ const plain = (s) => color(palette.plain, s);
991
+ const codeplain = (s) => color(palette.brand, s);
992
+ const link = (s) => color(palette.link, s, "4;");
968
993
 
969
994
  console.log(`\x1b[1mnext steps:\x1b[0m`);
970
995
  console.log(
@@ -996,6 +1021,8 @@ function agentLabel(agent) {
996
1021
  return "Claude Code";
997
1022
  case "codex":
998
1023
  return "Codex";
1024
+ case "copilot":
1025
+ return "GitHub Copilot";
999
1026
  case "forgecode":
1000
1027
  return "ForgeCode";
1001
1028
  case "opencode":
@@ -1090,4 +1117,6 @@ export {
1090
1117
  deleteForgeFile,
1091
1118
  detectInstalls,
1092
1119
  promptConfirm,
1120
+ terminalHasLightBackground,
1121
+ terminalPalette,
1093
1122
  };
@@ -0,0 +1,59 @@
1
+ ---
2
+ description: Bullet and continuation syntax for every section in .plain files
3
+ ---
4
+
5
+ # Rules for bullets and continuations in `.plain` files
6
+
7
+ These rules apply to every section and to concept explanations.
8
+
9
+ ## No line-length limit
10
+
11
+ - `.plain` does not impose a maximum number of characters per line
12
+ - Do not split a valid line merely to satisfy an arbitrary formatting width
13
+ - Prefer clear, precise wording; concision must never remove required detail
14
+
15
+ ## Never use bare continuation lines
16
+
17
+ - Every line inside a section must be a list item beginning with `- `
18
+ - An indented continuation without `- ` is invalid syntax
19
+ - If content is intentionally separated across lines, express the additional lines as nested
20
+ bullet items so each line remains syntactically valid
21
+
22
+ WRONG — bare continuation lines:
23
+
24
+ ```plain
25
+ ***functional specs***
26
+
27
+ - :GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle(),
28
+ which returns a list of :EventEnvelope: dicts conforming to the gateway contract.
29
+ ```
30
+
31
+ GOOD — one valid list item:
32
+
33
+ ```plain
34
+ ***functional specs***
35
+
36
+ - :GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle(), which returns a list of :EventEnvelope: dicts conforming to the gateway contract.
37
+ ```
38
+
39
+ BEST — separate clarifications expressed as nested bullets:
40
+
41
+ ```plain
42
+ ***functional specs***
43
+
44
+ - :GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle().
45
+ - The method returns a list of :EventEnvelope: dicts.
46
+ - The dicts conform to the gateway's :EventEnvelope: contract.
47
+ ```
48
+
49
+ ## Presenting `.plain` examples
50
+
51
+ - Show every example under its owning section header
52
+ - Separate top-level list items with one blank line
53
+ - Keep nested clarifications directly under their parent without a blank line
54
+ - BAD, WRONG, `Before:`, and `Too complex:` examples may intentionally demonstrate invalid syntax
55
+
56
+ ## Content that belongs in resources
57
+
58
+ Long URLs, schema fragments, and example payloads belong in `resources/` because they are external
59
+ artifacts, not because of their character count. Follow `linked-resources.md`.
@@ -42,14 +42,20 @@ When writing or editing a `***definitions***` section in a `.plain` file, always
42
42
  Bad — circular:
43
43
 
44
44
  ```plain
45
+ ***definitions***
46
+
45
47
  - :Customer: is a user who has placed at least one :Order:.
48
+
46
49
  - :Order: is placed by :Customer: and contains :OrderItem: entries.
47
50
  ```
48
51
 
49
52
  `:Order:` references `:Customer:`, and `:Customer:` references `:Order:`. Fix by removing the back-reference:
50
53
 
51
54
  ```plain
55
+ ***definitions***
56
+
52
57
  - :Customer: is a user of the system.
58
+
53
59
  - :Order: is placed by :Customer: and contains :OrderItem: entries.
54
60
  ```
55
61
 
@@ -69,13 +75,21 @@ Bad — circular:
69
75
  Example of defining technical components alongside their behavior:
70
76
 
71
77
  ```plain
78
+ ***definitions***
79
+
72
80
  - :CsvToJsonConverter: bidirectionally converts csv and json strings.
81
+
73
82
  - :CsvToJson: of :CsvToJsonConverter: converts a list of csv strings (e.g., "value1,value2,value3") to a list of json strings (e.g., "{\"key1\":\"value1\"}").
83
+
74
84
  - :JsonToCsv: of :CsvToJsonConverter: converts a list of json strings (e.g., "{\"key1\":\"value1\"}") to a list of csv strings (e.g., "value1,value2").
75
85
 
76
86
  - :StorageService: is an abstraction for a remote blob storage backend.
87
+
77
88
  - :CsvFileWriter: writes CSV rows to local files, automatically rotates to new files when size limits are reached, and flushes completed files to :StorageService:.
89
+
78
90
  - :CsvFileReader: reads CSV rows in a batched manner.
91
+
92
+
79
93
  - :CsvJoiner: joins csvs and uses :CsvFileWriter: to create the merged CSV.
80
94
  ```
81
95
 
@@ -83,6 +97,7 @@ Example of defining technical components alongside their behavior:
83
97
 
84
98
  ```plain
85
99
  ***definitions***
100
+
86
101
  - :ConceptName: is a description of what it represents.
87
102
  - Attribute one (required)
88
103
  - Attribute two (optional)
@@ -6,12 +6,53 @@ description: Rules for writing ***functional specs*** and ***acceptance tests***
6
6
 
7
7
  When writing or editing a `***functional specs***` section in a `.plain` file, always follow these rules:
8
8
 
9
+ ## Voice
10
+ - Write each functional spec as a **statement of fact** — present tense, indicative mood — describing what the software *is* or *does* once built, not as a command telling a developer what to build
11
+ - **Name a concrete subject**: an actor (`A :User: can …`), a named component or concept (`:PdfRenderer: lays out …`, `:Order: processing depends on …`), or the affected data in passive voice (`All :Resource: items are returned`)
12
+ - **Avoid the vague subject "The system …"** — it says nothing about *which* part does the work. Prefer the concrete component/concept, or passive voice with the affected concept as the subject
13
+ - **Never open a spec with an imperative** like "Implement …", "Add …", "Create …", "Show …", "Display …", "Build …" — those read as instructions, not specifications
14
+ - Do **not** use "should" / "should be able to" in a functional spec — that is requirement-modal voice; state the fact directly
15
+
16
+ BAD — imperative commands, or the vague "The system":
17
+
18
+ ```plain
19
+ ***functional specs***
20
+
21
+ - Implement the entry point for :App:.
22
+
23
+ - The system shows :TaskList:.
24
+
25
+ - Add :Task:.
26
+ ```
27
+
28
+ GOOD — statements of fact with concrete subjects:
29
+
30
+ ```plain
31
+ ***functional specs***
32
+
33
+ - :App: has an entry point.
34
+
35
+ - The :TaskList: is displayed.
36
+
37
+ - A :User: can add a :Task:. Only valid :Task: items are added.
38
+ ```
39
+
9
40
  ## Complexity limit
10
41
  - Each functional spec must imply a **maximum of 200 changed lines of code**
11
42
  - If a spec is too large, use `break-down-func-spec` to split it into multiple smaller, independent specs
12
43
  - Use `analyze-if-func-spec-too-complex` to verify before inserting
13
44
  - Use `analyze-func-specs` to check a spec (or a batch of specs) against all relevant existing specs in a single batched call; use `resolve-spec-conflict` for each conflicting pair it reports
14
45
 
46
+ ## First-spec bootstrap cost
47
+ - The first functional spec in a new root module carries implicit implementation cost even when its text describes little behavior
48
+ - The renderer may need to create the project structure, package metadata, framework configuration, entry point, initial source files, and unit-test infrastructure before implementing the behavior
49
+ - Count that bootstrap work toward the 200-changed-line limit; it is not free setup outside the spec
50
+ - Make the first spec the module's simplest useful, independently observable capability
51
+ - The first spec must still imply a testable outcome; setup alone is not a functional spec
52
+ - Prefer one minimal entry point or public operation with a deterministic result that can be invoked and verified
53
+ - Defer persistence, integrations, multiple interfaces, branching workflows, and cross-cutting behavior until later specs can build on the generated baseline
54
+ - This extra bootstrap cost applies when no previous implementation baseline exists; a module that continues from a `requires` module may already inherit the necessary scaffold
55
+
15
56
  ## Chronological ordering
16
57
  - Specs are rendered incrementally, top to bottom
17
58
  - The renderer has **no knowledge of future specs** — only previously rendered specs are in context
@@ -36,6 +77,14 @@ When writing or editing a `***functional specs***` section in a `.plain` file, a
36
77
  - Nested lines clarify the parent spec — they do not introduce separate functionality
37
78
  - Even with nested detail, the spec must still imply ≤ 200 lines of code
38
79
 
80
+ ## Testability
81
+ - Every functional spec must describe behavior that can be verified through an observable interface
82
+ - State the triggering input or action and the deterministic outcome a test can observe
83
+ - Include relevant outputs, state changes, errors, or externally visible side effects
84
+ - Do not rely on generated implementation details as the only way to verify the behavior
85
+ - If a black-box conformance test cannot be derived from the spec, make the behavior more explicit
86
+ - Testability does not require an acceptance test; acceptance tests are reserved for full workflows
87
+
39
88
  ## Deterministic interface
40
89
  - Specs must be detailed enough that a developer can use the built software without reading the generated code
41
90
  - All external interfaces must be explicit: REST endpoint paths and HTTP methods, CLI command names and arguments, file formats, message schemas, etc.
@@ -46,10 +95,6 @@ When writing or editing a `***functional specs***` section in a `.plain` file, a
46
95
  - `requires` modules only receive functional specs — do not rely on implementation reqs to convey behavior
47
96
  - Behavior that downstream modules need must be expressed in functional specs, not elsewhere
48
97
 
49
- ## Line length
50
- - See [`line-length.md`](line-length.md) — applies to every section, but bites hardest here because functional specs trend long
51
- - Hard limit: 120 characters. When a line gets too long, split at a natural clause boundary into nested `- ` bullets — **never** use bare indented continuation lines (invalid ***plain syntax)
52
-
53
98
  ## Acceptance tests
54
99
  - Nest `***acceptance tests***` under a functional spec when verification criteria are needed
55
100
  - Each acceptance test must be a **full workflow test** — a specific scenario that exercises the functional spec end-to-end, not a unit-level check of a single field or condition
@@ -63,11 +108,11 @@ When writing or editing a `***functional specs***` section in a `.plain` file, a
63
108
  ```plain
64
109
  ***functional specs***
65
110
 
66
- - Implement the entry point for :App:.
111
+ - :App: has an entry point.
67
112
 
68
- - :User: should be able to add :Task:. Only valid :Task: items can be added.
113
+ - A :User: can add a :Task:. Only valid :Task: items are added.
69
114
 
70
- - :User: should be able to send a :Message: to a :Conversation:.
115
+ - A :User: can send a :Message: to a :Conversation:.
71
116
  - A :Message: must have non-empty content.
72
117
  - The :Message: is appended to the end of the :Conversation:.
73
118
  - All :Participant: members of the :Conversation: can see the new :Message:.
@@ -13,7 +13,7 @@ When writing or editing an `***implementation reqs***` section in a `.plain` fil
13
13
 
14
14
  ## `:UnitTests:` lives here (hard rule)
15
15
  - **Everything** about `:UnitTests:` goes in `***implementation reqs***` — paths, approach, packages, framework, conventions, fixtures, mocking policy, file layout, naming, lint / static-analysis gates
16
- - `:UnitTests:` are part of the generated codebase (they sit inside `plain_modules/<module>/` alongside the implementation), so requirements that shape them are implementation reqs by definition
16
+ - `:UnitTests:` are part of the generated codebase (they sit inside `plain_modules/<module>/code/` alongside the implementation), so requirements that shape them are implementation reqs by definition
17
17
  - The unit-test generator reads **only** `***implementation reqs***` — anything about `:UnitTests:` placed elsewhere (e.g. `***test reqs***`) is silently ignored
18
18
  - Author each `:UnitTests:` requirement via `add-implementation-requirement` and phrase it in terms of `:UnitTests:` so the partition stays visible at a glance
19
19
 
@@ -4,7 +4,10 @@ description: Rules for authoring the three test scripts that an embedded ***plai
4
4
 
5
5
  # Rules for embedded-integration test scripts
6
6
 
7
- When an embedded integration ships its three `test_scripts/` (prepare-environment, unit, conformance) — whether you author them by hand or via the `implement-*-testing-script` skills — these rules apply on top of the shared testing-script rules in PLAIN_REFERENCE.md (exit-code conventions, the activate-only vs install-inline conformance distinction, `VERBOSE=1`, etc.).
7
+ When an embedded integration ships its three `test_scripts/` (prepare-environment, unit,
8
+ conformance), these rules apply on top of the contracts in the corresponding
9
+ `implement-*-testing-script` skills. Use `load-plain-reference` for the rendering and testing
10
+ workflow overview when needed.
8
11
 
9
12
  ## Staging model (read this first)
10
13
 
@@ -72,7 +75,7 @@ mvn test -Dtest='<:UnitTests: package>.**.*Test' checkstyle:check
72
75
 
73
76
  These reqs feed `run_conformance_tests_<lang>`. At minimum, declare:
74
77
 
75
- 1. **`:ConformanceTests:` source location** — where the conformance suite lives in the project (typically a sibling folder, e.g. `conformance_tests/<module>/`); the renderer passes the resolved path as `$2`
78
+ 1. **`:ConformanceTests:` source location** — the generated suite lives under `plain_modules/<module>/tests/`, one folder per functional spec; the renderer passes the resolved path of the folder under test as `$2`
76
79
  2. **`:ConformanceTests:` framework and execution command** — `mvn test --no-transfer-progress`, `pytest`, `npm test`, `go test ./...`, etc., with any flags / profiles the project requires
77
80
  3. **Fully qualified `:ConformanceTests:` package** (or path / pattern) used to scope discovery, if the runner needs one
78
81
  4. **`:ConformanceTests:` network and secrets policy** — by default the suite runs against the **live provider** (see [`integrations.md`](integrations.md) → *`:ConformanceTests:` always run against the live integration*). Declare the env-var names the script reads (e.g. `<PROVIDER>_API_KEY`), whether the script loads a `.env` file before running, and any specific endpoints that are mocked because they can't be exercised live safely (429, forced 5xx)
@@ -82,7 +85,7 @@ These reqs feed `run_conformance_tests_<lang>`. At minimum, declare:
82
85
  Author the conformance facts as one or more entries, phrased in terms of `:ConformanceTests:`:
83
86
 
84
87
  ```plain
85
- - :ConformanceTests: of :Implementation: live in `conformance_tests/foo/` and are implemented with JUnit 5 + Maven.
88
+ - :ConformanceTests: of :Implementation: live in `plain_modules/foo/tests/` and are implemented with JUnit 5 + Maven.
86
89
  - The fully qualified package used for :ConformanceTests: discovery is `com.example.integrations.foo.conformance`.
87
90
  - :ConformanceTests: are run via `mvn test --no-transfer-progress`; the host's Surefire plugin must be installed.
88
91
  - :ConformanceTests: run against the live :ProviderName: sandbox — no mocking of provider calls.
@@ -136,7 +139,7 @@ All three scripts are invoked by the `codeplain` renderer with positional argume
136
139
 
137
140
  ## 1. `prepare_environment_<lang>` — copy into the host, then compile
138
141
 
139
- Receives one positional argument: the renderer's build output folder (e.g. `plain_modules/<module>/`).
142
+ Receives one positional argument: the renderer's build output folder (e.g. `plain_modules/<module>/code/`).
140
143
 
141
144
  Purpose: stage the generated code **into the host codebase at the module's package path**, then run the host's install / build so that downstream test projects (specifically the conformance suite) can depend on it from the local dependency cache.
142
145
 
@@ -30,6 +30,9 @@ The embedded integration's `.plain` specs (and the rest of the ***plain project
30
30
  │ ├── resources/
31
31
  │ ├── test_scripts/
32
32
  │ ├── plain_modules/ # generated; gitignored
33
+ │ │ └── <module>/
34
+ │ │ ├── code/ # implementation + unit tests
35
+ │ │ └── tests/ # conformance tests, per functional spec
33
36
  │ └── config.yaml
34
37
  └── src/ # host source tree (Java, Python, etc.)
35
38
  ```
@@ -117,7 +120,7 @@ The renderer reads the directives from the spec and the shapes from the linked s
117
120
 
118
121
  ## Test-script wiring — copy into the host, run tests there
119
122
 
120
- Embedded integrations are tested **inside the host codebase itself**. The prepare and unit-test scripts copy the renderer's output (`$1`, i.e. `plain_modules/<module>/`) into the host's source tree at the module's package path, then compile / test the host project in place. Only the conformance script uses a scratch folder in the system temp directory (`/tmp/<lang>_conformance/`), because the conformance suite is a separate project that consumes the host build as a dependency.
123
+ Embedded integrations are tested **inside the host codebase itself**. The prepare and unit-test scripts copy the renderer's output (`$1`, i.e. `plain_modules/<module>/code/`) into the host's source tree at the module's package path, then compile / test the host project in place. Only the conformance script uses a scratch folder in the system temp directory (`/tmp/<lang>_conformance/`), because the conformance suite is a separate project that consumes the host build as a dependency.
121
124
 
122
125
  This matters because the integration's generated code references host symbols by their full import path (e.g. `from host_project.integrations.base import IntegrationContract`). Those imports only resolve cleanly when the test process is rooted in the host's package layout — anything else creates path edge cases that bite later in conformance failures.
123
126
 
@@ -23,7 +23,7 @@ A linked resource **must not** be any of the following:
23
23
  - Litmus test: "Would the renderer benefit from reading the bytes at this URL / folder?" If yes, save it to a file and link the file. If no (it's a runtime value the code carries forward), it can stay as plain text
24
24
 
25
25
  ## Structured protocol artifacts must be linked, never transcribed
26
- - JSON Schema, OpenAPI / Swagger, GraphQL SDL, Protobuf `.proto`, Avro / Thrift schemas, XML XSDs, AsyncAPI specs, JSON-RPC method definitions, wire-protocol descriptions, payload examples — anything with a formal machine-readable shape — belongs in a file under `resources/` (or a subfolder of the `.plain` file's directory)
26
+ - JSON Schema, OpenAPI / Swagger, GraphQL SDL, Protobuf `.proto`, Avro / Thrift schemas, XML XSDs, AsyncAPI specs, JSON-RPC method definitions, wire-protocol descriptions, payload examples — anything with a formal machine-readable shape — belongs in a file under `resources/` (or another directory within the project)
27
27
  - The spec line should describe the *role* of the artifact ("the request body conforms to ...", "the public API surface is defined in ...") rather than its contents
28
28
  - Reasons: one source of truth (no drift between prose and schema); the renderer and the generated code can both consume the file directly; schema changes show up cleanly as diffs
29
29
 
@@ -37,7 +37,7 @@ A linked resource **must not** be any of the following:
37
37
 
38
38
  ***functional specs***
39
39
 
40
- - :User: should be able to add :Task: by POSTing :TaskCreateRequest: to the `POST /tasks` endpoint of :TasksAPI:.
40
+ - A :User: can add a :Task: by POSTing :TaskCreateRequest: to the `POST /tasks` endpoint of :TasksAPI:.
41
41
  - The endpoint responds per :TasksAPI:.
42
42
  ```
43
43
 
@@ -54,11 +54,12 @@ A linked resource **must not** be any of the following:
54
54
 
55
55
  ***functional specs***
56
56
 
57
- - :User: should be able to add :Task: using :TaskModalSpec:.
57
+ - A :User: can add a :Task: using :TaskModalSpec:.
58
58
 
59
- - :User: should be able to edit :Task: using :TaskModalSpec:.
59
+ - A :User: can edit a :Task: using :TaskModalSpec:.
60
60
  ```
61
61
 
62
62
  ## File location and path resolution
63
- - Paths are resolved relative to the `.plain` file's directory
64
- - The conventional location is `resources/` under the `.plain` file's directory
63
+ - Paths are resolved relative to the directory where `codeplain` is run (the current working directory)
64
+ - 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
65
+ - The conventional location is a `resources/` directory; keep the resource inside the project so the path stays stable from wherever `codeplain` is run
@@ -8,7 +8,7 @@ When creating or editing a `.plain` file that uses `requires`, always follow the
8
8
 
9
9
  ## What requires does
10
10
  - `requires` establishes a **build ordering** — the required module is built before the current one
11
- - The required module's generated code (`plain_modules/<required_module>`) is copied as the starting point
11
+ - The required module's generated code (`plain_modules/<required_module>/code`) is copied as the starting point
12
12
  - The required module's `***functional specs***` become visible as **previous functional specs** — this property **is transitive**
13
13
  - Only `exported_concepts` from the required module are available — not its full definitions — and this property **is not transitive**
14
14
 
@@ -14,7 +14,7 @@ When writing or editing a `***test reqs***` section in a `.plain` file, always f
14
14
 
15
15
  ## `:ConformanceTests:` lives here (hard rule)
16
16
  - **Everything** about `:ConformanceTests:` goes in `***test reqs***` — paths, approach, packages, framework, execution command, mocking / network policy, fixtures, pass criteria, environment prerequisites
17
- - `:ConformanceTests:` live outside the generated codebase (typically in a separate project under `conformance_tests/<module>/`), so requirements that shape them belong in test reqs by definition
17
+ - `:ConformanceTests:` live outside the generated codebase, in their own tree under `plain_modules/<module>/tests/` (one folder per functional spec), so requirements that shape them belong in test reqs by definition
18
18
  - The conformance-test generator reads **only** `***test reqs***` — anything about `:ConformanceTests:` placed elsewhere (e.g. `***implementation reqs***`) is silently ignored
19
19
  - Author each `:ConformanceTests:` requirement via `add-test-requirement` and phrase it in terms of `:ConformanceTests:` so the partition stays visible at a glance
20
20
  - Conformance testing run scripts should be linked here as a linked resource