plain-forge 1.0.18 → 1.0.19

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 (42) hide show
  1. package/README.md +15 -8
  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/integration-embedded-testing.md +4 -1
  7. package/forge/rules/linked-resources.md +7 -6
  8. package/forge/skills/add-acceptance-test/SKILL.md +3 -25
  9. package/forge/skills/add-concept/SKILL.md +3 -22
  10. package/forge/skills/add-feature/SKILL.md +71 -67
  11. package/forge/skills/add-feature/references/checklist.md +38 -0
  12. package/forge/skills/add-feature/references/phase-0-intent.md +29 -0
  13. package/forge/skills/add-functional-spec/SKILL.md +3 -23
  14. package/forge/skills/add-functional-specs/SKILL.md +3 -24
  15. package/forge/skills/add-implementation-requirement/SKILL.md +4 -23
  16. package/forge/skills/add-resource/SKILL.md +7 -8
  17. package/forge/skills/add-test-requirement/SKILL.md +4 -21
  18. package/forge/skills/analyze-2-func-specs/SKILL.md +10 -10
  19. package/forge/skills/analyze-func-specs/SKILL.md +10 -10
  20. package/forge/skills/analyze-if-func-spec-too-complex/SKILL.md +93 -31
  21. package/forge/skills/break-down-func-spec/SKILL.md +78 -29
  22. package/forge/skills/create-import-module/SKILL.md +7 -0
  23. package/forge/skills/create-requires-module/SKILL.md +2 -1
  24. package/forge/skills/forge-plain/SKILL.md +80 -299
  25. package/forge/skills/forge-plain/references/checklist.md +66 -0
  26. package/forge/skills/forge-plain/references/phase-0-intent.md +29 -0
  27. package/forge/skills/forge-plain/references/phase-1-product.md +40 -0
  28. package/forge/skills/forge-plain/references/phase-2-tech.md +24 -0
  29. package/forge/skills/forge-plain/references/phase-3-testing.md +66 -0
  30. package/forge/skills/forge-plain/references/phase-4-validate-handoff.md +49 -0
  31. package/forge/skills/init-config-file/SKILL.md +2 -0
  32. package/forge/skills/init-plain-project/SKILL.md +4 -0
  33. package/forge/skills/init-plain-project/references/checklist.md +31 -0
  34. package/forge/skills/load-codeplain-reference/SKILL.md +188 -0
  35. package/forge/skills/load-plain-reference/SKILL.md +48 -678
  36. package/forge/skills/load-plain-reference/references/project-model.md +47 -0
  37. package/forge/skills/load-plain-reference/references/rendering-and-testing.md +57 -0
  38. package/forge/skills/plain-healthcheck/SKILL.md +2 -2
  39. package/forge/skills/resolve-spec-conflict/SKILL.md +13 -5
  40. package/forge/skills/run-codeplain/SKILL.md +1 -1
  41. package/package.json +1 -1
  42. package/forge/rules/line-length.md +0 -45
package/README.md CHANGED
@@ -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`.
@@ -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:.
@@ -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
 
@@ -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
@@ -32,10 +32,11 @@ Acceptance tests are nested under the functional spec they verify, using a `***a
32
32
  ```plain
33
33
  ***functional specs***
34
34
 
35
- - The system should process :Task: items in batches of 100.
35
+ - :Task: items are processed in batches of 100.
36
36
 
37
37
  ***acceptance tests***
38
38
  - Processing 250 :Task: items should result in 3 batches.
39
+
39
40
  - Each batch should contain at most 100 items.
40
41
  ```
41
42
 
@@ -44,29 +45,6 @@ Key formatting rules:
44
45
  - Each test bullet is indented under the `***acceptance tests***` header.
45
46
  - Multiple acceptance tests can be listed under a single functional spec.
46
47
 
47
- ### Line syntax (hard rule)
48
-
49
- **Every line inside `***acceptance tests***` must be its own list item starting with `- `.** ***plain has no concept of bare continuation lines — indented prose without a leading `- ` is **invalid syntax** and the renderer will reject it.
50
-
51
- - Hard limit: 120 characters per line. If a sentence is too long, **split it at a natural clause boundary into nested `- ` bullets** — never wrap onto an unprefixed line.
52
- - Nested clarifications are also `- ` items, indented under the parent. The indentation alone is not enough; the leading `- ` is required.
53
-
54
- BAD — bare continuation lines (invalid ***plain syntax, will not render):
55
-
56
- ```plain
57
- ***acceptance tests***
58
- - Processing 250 :Task: items should result in 3 batches with the
59
- last batch containing the remaining 50 items.
60
- ```
61
-
62
- GOOD — every line starts with `- `:
63
-
64
- ```plain
65
- ***acceptance tests***
66
- - Processing 250 :Task: items should result in 3 batches.
67
- - The first two batches each contain 100 items.
68
- - The last batch contains the remaining 50 items.
69
- ```
70
48
 
71
49
  ## Rules
72
50
 
@@ -76,7 +54,7 @@ An acceptance test is essentially an **example that illustrates** the functional
76
54
 
77
55
  ```
78
56
  Functional spec:
79
- - The system should return :Resource: items sorted by creation date in descending order.
57
+ - :Resource: items are returned sorted by creation date in descending order.
80
58
 
81
59
  Good (consistent):
82
60
  - The first :Resource: in the response should have the most recent creation date.
@@ -44,40 +44,21 @@ A concept definition is a bullet in `***definitions***` that starts with the con
44
44
 
45
45
  ```plain
46
46
  ***definitions***
47
+
47
48
  - :ConceptName: is a description of what it represents.
48
49
  ```
49
50
 
50
51
  Attributes and constraints are nested sub-bullets:
51
52
 
52
53
  ```plain
54
+ ***definitions***
55
+
53
56
  - :Task: describes an activity that needs to be done by :User:. :Task: has:
54
57
  - Name - a short description (required)
55
58
  - Notes - additional details (optional)
56
59
  - Due Date - completion deadline (optional)
57
60
  ```
58
61
 
59
- ## Line syntax (hard rule)
60
-
61
- **Every line inside `***definitions***` must be its own list item starting with `- `.** ***plain has no concept of bare continuation lines — indented prose without a leading `- ` is **invalid syntax** and the renderer will reject it.
62
-
63
- - Hard limit: 120 characters per line. If a sentence is too long, **split it at a natural clause boundary into nested `- ` bullets** — never wrap onto an unprefixed line.
64
- - Nested attributes are also `- ` items, indented under the parent. The indentation alone is not enough; the leading `- ` is required.
65
-
66
- BAD — bare continuation lines (invalid ***plain syntax, will not render):
67
-
68
- ```plain
69
- - :Task: describes an activity that needs to be done by :User:.
70
- - Name is a short description that the user provides when creating
71
- the task and is shown in the task list.
72
- ```
73
-
74
- GOOD — every line starts with `- `:
75
-
76
- ```plain
77
- - :Task: describes an activity that needs to be done by :User:.
78
- - Name is a short description provided when creating the task.
79
- - The name is shown in the task list.
80
- ```
81
62
 
82
63
  ## Validation Checklist
83
64