plain-forge 1.0.20 → 1.0.21-next.55f13d7

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 +1 -0
  2. package/bin/cli.mjs +1 -1
  3. package/forge/rules/bullet-continuation.md +16 -6
  4. package/forge/rules/concise-specs-examples.md +431 -0
  5. package/forge/rules/concise-specs-integration-examples.md +272 -0
  6. package/forge/rules/concise-specs.md +80 -0
  7. package/forge/rules/definitions.md +7 -4
  8. package/forge/rules/exported-concepts.md +3 -2
  9. package/forge/rules/func-specs.md +10 -3
  10. package/forge/rules/impl-reqs.md +8 -4
  11. package/forge/rules/import-modules.md +4 -6
  12. package/forge/rules/integration-embedded.md +0 -1
  13. package/forge/rules/integration-standalone.md +1 -1
  14. package/forge/rules/integrations.md +6 -7
  15. package/forge/rules/linked-resources.md +9 -8
  16. package/forge/rules/module-structure.md +105 -0
  17. package/forge/rules/required-concepts.md +5 -5
  18. package/forge/rules/requires-modules.md +37 -21
  19. package/forge/rules/test-reqs.md +11 -7
  20. package/forge/skills/add-acceptance-test/SKILL.md +1 -0
  21. package/forge/skills/add-concept/SKILL.md +2 -1
  22. package/forge/skills/add-feature/SKILL.md +2 -2
  23. package/forge/skills/add-functional-spec/SKILL.md +2 -1
  24. package/forge/skills/add-functional-specs/SKILL.md +2 -1
  25. package/forge/skills/add-implementation-requirement/SKILL.md +2 -1
  26. package/forge/skills/add-test-requirement/SKILL.md +1 -0
  27. package/forge/skills/break-down-func-spec/SKILL.md +1 -0
  28. package/forge/skills/check-forge-version/SKILL.md +154 -0
  29. package/forge/skills/check-forge-version/scripts/check-forge-version.mjs +368 -0
  30. package/forge/skills/create-import-module/SKILL.md +3 -3
  31. package/forge/skills/create-requires-module/SKILL.md +24 -19
  32. package/forge/skills/forge-plain/SKILL.md +1 -1
  33. package/forge/skills/forge-plain/references/checklist.md +2 -1
  34. package/forge/skills/forge-plain/references/phase-1-product.md +3 -2
  35. package/forge/skills/forge-plain/references/phase-2-tech.md +1 -1
  36. package/forge/skills/load-plain-reference/SKILL.md +20 -2
  37. package/forge/skills/load-plain-reference/references/project-model.md +0 -11
  38. package/forge/skills/plain-healthcheck/SKILL.md +1 -1
  39. package/forge/skills/refactor-module/SKILL.md +3 -3
  40. package/forge/skills/resolve-section-ownership/SKILL.md +65 -0
  41. package/forge/skills/resolve-spec-conflict/SKILL.md +1 -0
  42. package/package.json +5 -5
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env node
2
+ //
3
+ // Report whether the plain-forge installed on this machine is the latest
4
+ // published version.
5
+ //
6
+ // node check-forge-version.mjs # human-readable report
7
+ // node check-forge-version.mjs --json # machine-readable report
8
+ // node check-forge-version.mjs --offline # skip the registry, list installs only
9
+ //
10
+ // Exit codes (the verdict is also the first line of stdout):
11
+ // 0 PASS — every install found is at the latest version
12
+ // 1 FAIL — at least one install is behind the latest version
13
+ // 2 WARN — installs found, but at least one version is indeterminate
14
+ // 3 ERROR — the latest version could not be resolved from npm
15
+ // 4 NONE — no plain-forge install found in any candidate directory
16
+ //
17
+ // Zero dependencies, Node >= 18 (global fetch). The install-path model and the
18
+ // version comparison mirror bin/cli.mjs; keep them in sync with that file.
19
+
20
+ import fs from "node:fs";
21
+ import os from "node:os";
22
+ import path from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import { execFileSync } from "node:child_process";
25
+
26
+ const PKG = "plain-forge";
27
+ const REGISTRY = `https://registry.npmjs.org/${PKG}/latest`;
28
+ const MANIFEST_REL = path.join(".plain-forge", "manifest.json");
29
+
30
+ // Flagship skills every install ships. All four present means plain-forge is
31
+ // installed even when no manifest is (a legacy, pre-manifest install).
32
+ const SIGNATURE_SKILLS = [
33
+ "forge-plain",
34
+ "add-feature",
35
+ "debug-specs",
36
+ "load-plain-reference",
37
+ ];
38
+
39
+ // The install this script is executing from, found by walking up its own path
40
+ // rather than trusting cwd:
41
+ // <baseDir>/skills/check-forge-version/scripts/check-forge-version.mjs
42
+ // Without this, running the script from its own directory (the natural reading
43
+ // of "node scripts/check-forge-version.mjs") would scan the skill folder as if
44
+ // it were a project root and report NONE while sitting inside a live install.
45
+ // Returns null when running from the source repo, where forge/ is not an
46
+ // install.
47
+ function selfInstall() {
48
+ const here = path.dirname(fileURLToPath(import.meta.url));
49
+ if (path.basename(path.resolve(here, "..", "..")) !== "skills") return null;
50
+ const dir = path.resolve(here, "..", "..", "..");
51
+ const byDir = {
52
+ ".claude": "claude",
53
+ ".agents": "codex/copilot/universal",
54
+ ".forge": "forgecode",
55
+ forge: "forgecode",
56
+ ".opencode": "opencode",
57
+ opencode: "opencode",
58
+ };
59
+ const agent = byDir[path.basename(dir)];
60
+ if (!agent) return null;
61
+ // The source repo's forge/ dir also holds skills/, and ForgeCode's global
62
+ // install is ~/forge — same basename. Only the source tree sits next to the
63
+ // package's own package.json, so that is what separates them. Without this
64
+ // guard, running from a checkout reports the repo as an unmanaged install.
65
+ try {
66
+ const pkg = JSON.parse(
67
+ fs.readFileSync(path.join(dir, "..", "package.json"), "utf8"),
68
+ );
69
+ if (pkg?.name === PKG) return null;
70
+ } catch {
71
+ /* no sibling package.json -> a real install, not the source tree */
72
+ }
73
+ return { agent, scope: "self", dir };
74
+ }
75
+
76
+ // Every directory plain-forge can install into, mirroring resolveBaseDir in
77
+ // bin/cli.mjs — including the two global paths that break the usual pattern
78
+ // (ForgeCode uses ~/forge, OpenCode uses ~/.config/opencode). The install this
79
+ // script lives in comes first so it survives realpath deduplication.
80
+ function candidateInstalls() {
81
+ const cwd = process.cwd();
82
+ const home = os.homedir();
83
+ const self = selfInstall();
84
+ return [
85
+ ...(self ? [self] : []),
86
+ { agent: "claude", scope: "project", dir: path.join(cwd, ".claude") },
87
+ { agent: "codex/copilot/universal", scope: "project", dir: path.join(cwd, ".agents") },
88
+ { agent: "forgecode", scope: "project", dir: path.join(cwd, ".forge") },
89
+ { agent: "opencode", scope: "project", dir: path.join(cwd, ".opencode") },
90
+ { agent: "claude", scope: "global", dir: path.join(home, ".claude") },
91
+ { agent: "codex/copilot/universal", scope: "global", dir: path.join(home, ".agents") },
92
+ { agent: "forgecode", scope: "global", dir: path.join(home, "forge") },
93
+ { agent: "opencode", scope: "global", dir: path.join(home, ".config", "opencode") },
94
+ ];
95
+ }
96
+
97
+ function readManifest(baseDir) {
98
+ try {
99
+ const data = JSON.parse(
100
+ fs.readFileSync(path.join(baseDir, MANIFEST_REL), "utf8"),
101
+ );
102
+ if (data && Array.isArray(data.files)) return data;
103
+ } catch {
104
+ /* missing or malformed manifest -> treat as absent */
105
+ }
106
+ return null;
107
+ }
108
+
109
+ function hasForgeSignature(baseDir) {
110
+ return SIGNATURE_SKILLS.every((skill) =>
111
+ fs.existsSync(path.join(baseDir, "skills", skill)),
112
+ );
113
+ }
114
+
115
+ // Same semantics as compareVersions in bin/cli.mjs: 1 if a > b, -1 if a < b,
116
+ // 0 if equal, null when either side is not purely dotted-numeric (a
117
+ // prerelease such as "1.0.21-rc.1", or "unknown").
118
+ function compareVersions(a, b) {
119
+ const parse = (v) => String(v).split(".").map(Number);
120
+ const pa = parse(a);
121
+ const pb = parse(b);
122
+ if (pa.some(Number.isNaN) || pb.some(Number.isNaN)) return null;
123
+ const len = Math.max(pa.length, pb.length);
124
+ for (let i = 0; i < len; i++) {
125
+ const x = pa[i] ?? 0;
126
+ const y = pb[i] ?? 0;
127
+ if (x > y) return 1;
128
+ if (x < y) return -1;
129
+ }
130
+ return 0;
131
+ }
132
+
133
+ // Resolve the `latest` dist-tag. Prereleases are published under `next`, so
134
+ // `latest` is the right tag to compare a normal install against.
135
+ async function resolveLatest() {
136
+ try {
137
+ const res = await fetch(REGISTRY, {
138
+ headers: { accept: "application/json" },
139
+ signal: AbortSignal.timeout(10000),
140
+ });
141
+ if (!res.ok) throw new Error(`registry returned HTTP ${res.status}`);
142
+ const version = (await res.json())?.version;
143
+ if (!version) throw new Error("registry response had no version field");
144
+ return { version, source: "registry" };
145
+ } catch (registryErr) {
146
+ // Offline, proxied, or air-gapped hosts often still have a working npm
147
+ // configured against an internal mirror.
148
+ try {
149
+ const out = execFileSync("npm", ["view", PKG, "version"], {
150
+ encoding: "utf8",
151
+ timeout: 20000,
152
+ stdio: ["ignore", "pipe", "ignore"],
153
+ });
154
+ const version = out.trim();
155
+ if (!version) throw new Error("npm view printed nothing");
156
+ return { version, source: "npm view" };
157
+ } catch (npmErr) {
158
+ return {
159
+ version: null,
160
+ source: null,
161
+ error: `${registryErr.message}; npm fallback: ${npmErr.message}`,
162
+ };
163
+ }
164
+ }
165
+ }
166
+
167
+ function findInstalls() {
168
+ const installs = [];
169
+ const seen = new Set();
170
+ for (const cand of candidateInstalls()) {
171
+ if (!fs.existsSync(cand.dir)) continue;
172
+ const key = fs.realpathSync(cand.dir);
173
+ if (seen.has(key)) continue; // .agents is shared by three agent labels
174
+ const manifest = readManifest(cand.dir);
175
+ if (manifest) {
176
+ seen.add(key);
177
+ installs.push({
178
+ ...cand,
179
+ // The manifest records which agent layout produced the install; prefer
180
+ // it over the label the candidate list guessed.
181
+ agent: manifest.agent ?? cand.agent,
182
+ installed: manifest.version ?? null,
183
+ managed: true,
184
+ files: manifest.files.length,
185
+ });
186
+ } else if (hasForgeSignature(cand.dir)) {
187
+ seen.add(key);
188
+ installs.push({
189
+ ...cand,
190
+ installed: null,
191
+ managed: false,
192
+ files: null,
193
+ });
194
+ }
195
+ }
196
+ return installs;
197
+ }
198
+
199
+ function classify(install, latest) {
200
+ if (!install.managed) return "UNMANAGED";
201
+ if (!install.installed) return "INDETERMINATE";
202
+ const cmp = compareVersions(latest, install.installed);
203
+ if (cmp === null) return "INDETERMINATE";
204
+ if (cmp > 0) return "STALE";
205
+ return "CURRENT"; // equal, or ahead of the registry (a local dev build)
206
+ }
207
+
208
+ // Greedy wrap so the report reads as prose in a terminal instead of breaking
209
+ // mid-sentence at hardcoded string boundaries.
210
+ function wrap(text, width = 84) {
211
+ const out = [];
212
+ let line = "";
213
+ for (const word of text.split(/\s+/)) {
214
+ if (line && line.length + 1 + word.length > width) {
215
+ out.push(line);
216
+ line = word;
217
+ } else {
218
+ line = line ? `${line} ${word}` : word;
219
+ }
220
+ }
221
+ if (line) out.push(line);
222
+ return out;
223
+ }
224
+
225
+ function report(installs, latest, statuses) {
226
+ const lines = [];
227
+ const stale = statuses.filter((s) => s.status === "STALE");
228
+ const unclear = statuses.filter(
229
+ (s) => s.status === "UNMANAGED" || s.status === "INDETERMINATE",
230
+ );
231
+
232
+ let verdict;
233
+ if (stale.length > 0) verdict = "FAIL";
234
+ else if (unclear.length > 0) verdict = "WARN";
235
+ else verdict = "PASS";
236
+
237
+ lines.push(
238
+ `${verdict} — latest ${PKG} on npm is v${latest.version} (via ${latest.source})`,
239
+ );
240
+ lines.push("");
241
+ lines.push(`Found ${installs.length} install(s):`);
242
+ for (const s of statuses) {
243
+ const where = `${s.install.agent} (${s.install.scope})`;
244
+ const version = s.install.installed ? `v${s.install.installed}` : "version unknown";
245
+ lines.push(` [${s.status}] ${where} — ${version}`);
246
+ lines.push(` ${s.install.dir}`);
247
+ if (s.status === "UNMANAGED") {
248
+ lines.push(
249
+ " no .plain-forge/manifest.json — a pre-manifest install; update adopts it",
250
+ );
251
+ }
252
+ }
253
+ lines.push("");
254
+ if (verdict === "PASS") {
255
+ lines.push("Every install is at the latest published version. Nothing to do.");
256
+ return { text: lines.join("\n"), verdict };
257
+ }
258
+
259
+ lines.push("ACTION REQUIRED — please update plain-forge before continuing.");
260
+ lines.push("");
261
+ if (stale.length > 0) {
262
+ const n = stale.length;
263
+ lines.push(
264
+ ...wrap(
265
+ `${n} ${n === 1 ? "install is" : "installs are"} behind v${latest.version}. ` +
266
+ "Continuing on an outdated plain-forge is not recommended: its skills and rules have " +
267
+ "been superseded, so specs authored against them may not match what the current " +
268
+ "codeplain renderer expects, and fixes released since this version are not in effect.",
269
+ ),
270
+ );
271
+ }
272
+ if (unclear.length > 0) {
273
+ const n = unclear.length;
274
+ if (stale.length > 0) lines.push("");
275
+ lines.push(
276
+ ...wrap(
277
+ `${n} ${n === 1 ? "install could" : "installs could"} not be confirmed as current. ` +
278
+ "Treat it as outdated until proven otherwise — updating is the cheapest way to " +
279
+ "remove the doubt.",
280
+ ),
281
+ );
282
+ }
283
+ lines.push("");
284
+ lines.push(" npx plain-forge update");
285
+ lines.push("");
286
+ lines.push(
287
+ ...wrap(
288
+ "One run covers everything: update auto-detects every install across both scopes, prunes " +
289
+ "files that no longer ship, and never touches your own or third-party skills. Re-run " +
290
+ "this check afterwards to confirm.",
291
+ ),
292
+ );
293
+ return { text: lines.join("\n"), verdict };
294
+ }
295
+
296
+ async function main() {
297
+ const argv = process.argv.slice(2);
298
+ const asJson = argv.includes("--json");
299
+ const offline = argv.includes("--offline");
300
+
301
+ const installs = findInstalls();
302
+
303
+ if (installs.length === 0) {
304
+ const msg =
305
+ `NONE — no ${PKG} install found.\n\n` +
306
+ "Checked these directories (none held a .plain-forge/manifest.json or a\n" +
307
+ "recognizable forge skill tree):\n" +
308
+ candidateInstalls().map((c) => ` ${c.dir}`).join("\n") +
309
+ `\n\nInstall it with:\n\n npx ${PKG} install\n`;
310
+ console.log(asJson ? JSON.stringify({ verdict: "NONE", installs: [] }, null, 2) : msg);
311
+ process.exit(4);
312
+ }
313
+
314
+ if (offline) {
315
+ const out = installs
316
+ .map(
317
+ (i) =>
318
+ ` ${i.agent} (${i.scope}) — ${i.installed ? `v${i.installed}` : "version unknown"}\n ${i.dir}`,
319
+ )
320
+ .join("\n");
321
+ console.log(
322
+ asJson
323
+ ? JSON.stringify({ verdict: "OFFLINE", installs }, null, 2)
324
+ : `OFFLINE — registry not queried.\n\nFound ${installs.length} install(s):\n${out}\n`,
325
+ );
326
+ process.exit(0);
327
+ }
328
+
329
+ const latest = await resolveLatest();
330
+ if (!latest.version) {
331
+ const out = installs
332
+ .map(
333
+ (i) =>
334
+ ` ${i.agent} (${i.scope}) — ${i.installed ? `v${i.installed}` : "version unknown"}`,
335
+ )
336
+ .join("\n");
337
+ const msg =
338
+ `ERROR — could not resolve the latest ${PKG} version from npm.\n\n` +
339
+ `Reason: ${latest.error}\n\n` +
340
+ `Installed versions found (not compared against anything):\n${out}\n\n` +
341
+ "Re-run when the network is available, or check https://www.npmjs.com/package/" +
342
+ `${PKG} manually.\n`;
343
+ console.error(
344
+ asJson
345
+ ? JSON.stringify({ verdict: "ERROR", error: latest.error, installs }, null, 2)
346
+ : msg,
347
+ );
348
+ process.exit(3);
349
+ }
350
+
351
+ const statuses = installs.map((install) => ({
352
+ install,
353
+ status: classify(install, latest.version),
354
+ }));
355
+
356
+ const { text, verdict } = report(installs, latest, statuses);
357
+ console.log(
358
+ asJson
359
+ ? JSON.stringify({ verdict, latest: latest.version, source: latest.source, installs: statuses }, null, 2)
360
+ : text,
361
+ );
362
+ process.exit(verdict === "FAIL" ? 1 : verdict === "WARN" ? 2 : 0);
363
+ }
364
+
365
+ main().catch((err) => {
366
+ console.error(`ERROR — check-forge-version failed unexpectedly: ${err.message}`);
367
+ process.exit(3);
368
+ });
@@ -89,9 +89,9 @@ import:
89
89
  | Pulls in definitions | Yes | No (only `exported_concepts`) |
90
90
  | Pulls in implementation reqs | Yes | No |
91
91
  | Pulls in test reqs | Yes | No |
92
- | Pulls in functional specs | No | Yes (as previous requirements) |
93
- | Copies generated code | No | Yes |
94
- | Typical use | Templates, shared definitions | Build dependency chain |
92
+ | Pulls in functional specs | No | Yes (as previous requirements, transitively) |
93
+ | Copies generated code | No | Yes (accumulated ancestor chain) |
94
+ | Typical use | Templates, shared definitions | Attaching a module to the build chain |
95
95
 
96
96
  ## Validation Checklist
97
97
 
@@ -13,28 +13,29 @@ Always use the skill `load-plain-reference` to retrieve the ***plain syntax rule
13
13
 
14
14
  ## What Requires Does
15
15
 
16
- `requires` establishes a build ordering between modules. The required module is built **before** the current one. This does not necessarily mean the current module extends or depends on the required module's code — it may be completely independent. The `requires` relationship simply ensures the build order is correct.
16
+ `requires` attaches the current module to the project's build chain. The required module is built **before** the current one, and its generated code — the accumulated output of that module and all its ancestors — is copied as the starting point. The current module does not need to functionally extend the required module, but their outputs are never independent: the current module's code physically contains the required module's.
17
17
 
18
18
  When this module is rendered:
19
19
  - The required module's generated code (`plain_modules/<required_module>/code`) is copied as the starting point.
20
- - The required module's `***functional specs***` become visible as **previous functional specs**.
21
- - Only `exported_concepts` from the required module are available (not its full definitions).
20
+ - The `***functional specs***` of the required module **and all its ancestors** become visible as **previous functional specs**.
21
+ - Of the required module's definitions, only the concepts in its `exported_concepts` are visible, each with its full definition text. Export visibility is **not** transitive — to see an ancestor's exports, list that ancestor in `requires` too.
22
+
23
+ All modules connected by `requires` form a single tree with a common root. **Diamond joins are prohibited**: multiple `requires` entries must lie on one root-to-tip ancestor path. The deepest entry is the attachment point; the other entries exist only to make those ancestors' exports visible.
22
24
 
23
25
  Use `requires` for:
24
- - Ensuring a module is built after another in the build chain
25
26
  - Building on top of an existing module's functionality
26
- - Extending a base module with additional features
27
+ - Attaching a module at the right point in the build chain, even when it addresses a different concern
27
28
 
28
29
  If you only need shared definitions and reqs (no functional specs, no generated code), use `import` instead — see the `create-import-module` skill.
29
30
 
30
31
  ## Workflow
31
32
 
32
- 1. **Identify the dependency.** Determine which module this new module builds on. That module must already exist and be renderable.
33
+ 1. **Identify the attachment point.** Determine which existing module this new module attaches after in the build chain. That module must already exist and be renderable. If listing additional modules, verify each is an ancestor of the attachment point — no diamond joins.
33
34
  2. **Create the `.plain` file at the repository root** with YAML frontmatter containing the `requires` field. Modules with functional specs live at the root, not in `template/`.
34
- 3. **Review the required module's functional specs** — they will be treated as previous requirements. Your new functional specs must not conflict with them.
35
- 4. **Review the required module's `exported_concepts`** — only those concepts are available to reference from the required module.
35
+ 3. **Review the functional specs of the whole ancestor chain** — they will be treated as previous requirements. Your new functional specs must not conflict with any of them.
36
+ 4. **Review each listed module's `exported_concepts`** — only those concepts are available to reference; an ancestor's exports require listing that ancestor.
36
37
  5. **Add module-specific content** — definitions, implementation reqs, test reqs, and functional specs unique to this module.
37
- 6. **Check for conflicts** between your new functional specs and the required module's specs.
38
+ 6. **Check for conflicts** between your new functional specs and any spec in the ancestor chain.
38
39
 
39
40
  ## Format
40
41
 
@@ -60,6 +61,8 @@ description: Extended module that builds on base_module
60
61
 
61
62
  A module can use both `requires` and `import` together. `requires` points to other root-level modules; `import` resolves from the default `template/` directory (no prefix needed).
62
63
 
64
+ Multiple `requires` entries are legal only when they lie on one ancestor path (e.g., `requires: [auth, messaging]` where `messaging` itself requires `auth`). The deepest entry (`messaging`) is the attachment point; `auth` is listed only to make its exports visible.
65
+
63
66
  ## Exported Concepts
64
67
 
65
68
  The required module controls what concepts are visible via `exported_concepts`:
@@ -71,17 +74,17 @@ exported_concepts: [":StorageClient:", ":BackupResult:"]
71
74
  ---
72
75
  ```
73
76
 
74
- Only `:StorageClient:` and `:BackupResult:` would be available to modules that `require` this one. All other concepts from the required module are internal.
77
+ Only `:StorageClient:` and `:BackupResult:` would be available to modules that `require` this one, each with its full definition text. All other concepts from the required module are internal.
75
78
 
76
79
  ## Chronological Ordering with Requires
77
80
 
78
- Functional specs from `requires` modules are considered **previous functional specs**. This means:
81
+ Functional specs from the whole `requires` ancestor chain are considered **previous functional specs**. This means:
79
82
  - They are already rendered and their code exists.
80
83
  - Your new specs are rendered after them, with full awareness of what they defined.
81
- - Your new specs must not conflict with the required module's specs.
82
- - The renderer sees the required module's functional specs as context when rendering yours.
84
+ - Your new specs must not conflict with any spec in the ancestor chain.
85
+ - The renderer sees the chain's functional specs as context when rendering yours.
83
86
 
84
- The current module may or may not be functionally related to the required module. In some cases `requires` simply enforces build order — the two modules may be independent pieces of the same project that need to be built in sequence.
87
+ The current module may or may not be functionally related to the required module — `requires` places it at the right point in the accumulating build chain either way.
85
88
 
86
89
  ## Import vs Requires
87
90
 
@@ -90,16 +93,18 @@ The current module may or may not be functionally related to the required module
90
93
  | Pulls in definitions | Yes | No (only `exported_concepts`) |
91
94
  | Pulls in implementation reqs | Yes | No |
92
95
  | Pulls in test reqs | Yes | No |
93
- | Pulls in functional specs | No | Yes (as previous requirements) |
94
- | Copies generated code | No | Yes |
95
- | Typical use | Templates, shared definitions | Build dependency chain |
96
+ | Pulls in functional specs | No | Yes (as previous requirements, transitively) |
97
+ | Copies generated code | No | Yes (accumulated ancestor chain) |
98
+ | Typical use | Templates, shared definitions | Attaching a module to the build chain |
96
99
 
97
100
  ## Validation Checklist
98
101
 
99
102
  - [ ] Module file is at the repository root (not in `template/`)
100
103
  - [ ] Required module exists and is renderable
101
- - [ ] Required module's `exported_concepts` provide the concepts you need
102
- - [ ] New functional specs do not conflict with the required module's specs
104
+ - [ ] All `requires` entries lie on one ancestor path — no diamond joins
105
+ - [ ] The deepest `requires` entry is the intended attachment point
106
+ - [ ] Every listed module's `exported_concepts` provide the concepts you need
107
+ - [ ] New functional specs do not conflict with any spec in the ancestor chain
103
108
  - [ ] Module has at least one functional spec and one implementation req
104
109
  - [ ] Both `requires` and `import` are used correctly (not mixed up)
105
110
  - [ ] YAML frontmatter is correctly formatted between `---` markers
@@ -82,7 +82,7 @@ When entering a phase, read its reference file and walk its topics **in order**
82
82
  | Phase | Reference | Finished when |
83
83
  |---|---|---|
84
84
  | 0 — What is the intent? | `references/phase-0-intent.md` | a concise intent brief covering the problem, primary user and outcome, and initial boundary is explicitly confirmed; nothing is written to disk |
85
- | 1 — What are we building? | `references/phase-1-product.md` | the new `***definitions***` and `***functional specs***` are on disk and approved |
85
+ | 1 — What are we building? | `references/phase-1-product.md` | the new `***definitions***` and `***functional specs***`, plus any technology-free `***implementation reqs***`, are on disk and approved |
86
86
  | 2 — What tech should it use? | `references/phase-2-tech.md` | the new `***implementation reqs***` are on disk and approved |
87
87
  | 3 — How is testing done? | `references/phase-3-testing.md` | the `***test reqs***` (and `***acceptance tests***` if conformance is on) are on disk, the `test_scripts/` and `config.yaml`(s) exist, and `check-plain-env` passed or each gap was acknowledged |
88
88
  | 4 — Validate and hand off | `references/phase-4-validate-handoff.md` | the agent ran `codeplain <module>.plain --dry-run` successfully against the render target, and the user has the render command plus every side-channel command |
@@ -37,7 +37,8 @@ A box is met only when its source confirms it. Return `APPROVED` only when every
37
37
  - [ ] **[disk]** `.plain` module structure created with YAML frontmatter; template (if any) has no `***functional specs***`.
38
38
  - [ ] **[both]** Every concept authored in `***definitions***` via `add-concept`, defined before use.
39
39
  - [ ] **[both]** Every feature authored as functional specs via `add-functional-spec(s)`, each ≤200 LOC, in chronological build order.
40
- - [ ] **[disk]** No `***implementation reqs***`, `***test reqs***`, or `***acceptance tests***` written in this phase.
40
+ - [ ] **[disk]** No `***test reqs***` or `***acceptance tests***` written in this phase.
41
+ - [ ] **[disk]** Every `***implementation reqs***` bullet written this phase is technology-free policy — none names a language, framework, library, storage engine, or deployment target.
41
42
  - [ ] **[record]** Summarized the full feature list and module/concept layout; got explicit overall confirmation.
42
43
 
43
44
  ## Phase 2 — What tech should it use?
@@ -11,8 +11,9 @@ The moment the user answers, write the resulting snippet to disk using the right
11
11
  - **Module structure** — create or update the `.plain` file(s) (single module, template + modules, or chained modules). Set up YAML frontmatter (`import`, `requires`, `description`) and the proposed module name. If a template is used, create it in `template/` **without** a `***functional specs***` section. Use the `create-import-module` skill where applicable.
12
12
  - **`***definitions***`** — add or refine concepts (entities, attributes, relationships). Define every concept before it is referenced. Use the `add-concept` skill.
13
13
  - **`***functional specs***`** — translate each answer into a single chronological, incremental spec (≤200 lines of code change, language-agnostic, no conflicts). Use `add-functional-spec` for one new spec; use `add-functional-specs` only when a single answer naturally decomposes into a tight cluster of specs flowing from the same answer (e.g. list / create / delete CRUD on one entity). **Never hand-author the `***functional specs***` section directly.**
14
+ - **`***implementation reqs***`** — only a requirement that can be stated without naming a technology: an error-handling or retry policy, an idempotency or ordering guarantee, a data-format or precision rule. Use the `add-implementation-requirement` skill, on the module whose behavior it constrains. The moment stating it takes a language, framework, library, storage engine, or deployment target, it is a Phase 2 answer — note it and steer back.
14
15
 
15
- Do **not** add `***implementation reqs***`, `***test reqs***`, or `***acceptance tests***` in this phase — they belong to later phases.
16
+ Do **not** add `***test reqs***` or `***acceptance tests***` in this phase — they belong to later phases.
16
17
 
17
18
  ## Topics (in order)
18
19
 
@@ -25,7 +26,7 @@ Do **not** add `***implementation reqs***`, `***test reqs***`, or `***acceptance
25
26
  4. **Core entities** — the main "things" in the system (Users, Tasks, Orders, Messages, …), their attributes, and relationships. Author one concept per entity in `***definitions***`. Review each concept snippet.
26
27
  5. **Key features** — every distinct thing the app should do. For each feature capture the trigger, the expected outcome, and edge cases / validation rules. Author one or more functional specs per feature, in chronological build order, each ≤200 LOC. Break large features into smaller specs together with the user. Review each new functional spec (or tight group of related specs).
27
28
  6. **User flows** — walk through the app from the user's perspective: what happens first, what happens next, and at each decision point. Author the ordering and any missing intermediate functional specs. Review the affected sequence of specs.
28
- 7. **Constraints and rules** — business rules, validation, permissions, error-handling behavior. Fold these into the relevant functional specs, and add concepts where they are first-class entities (e.g. roles). Review the updated specs.
29
+ 7. **Constraints and rules** — business rules, validation, permissions, error-handling behavior. Fold the observable part into the relevant functional specs, and add concepts where they are first-class entities (e.g. roles); route the part that shapes how the behavior is built — retries, backoff, error taxonomy — to `***implementation reqs***`. Review the updated specs.
29
30
  8. **User interface (optional)** — skip entirely if the project has no UI. Otherwise ask, one question per iteration:
30
31
  - How does the UI look and feel?
31
32
  - Where are the key UI elements located?
@@ -1,6 +1,6 @@
1
1
  # Phase 2 — What technologies should it use?
2
2
 
3
- Gather the technical stack **and** the project's structure/architecture. This phase affects only `***implementation reqs***` — testing concerns come later. Run the core loop (ask → author → review) from `SKILL.md` for each topic below, in order. When the user has no preference, propose a sensible default that fits earlier answers and ask them to confirm.
3
+ Gather the technical stack **and** the project's structure/architecture. This phase affects only `***implementation reqs***` — testing concerns come later. Phase 1 may already have left technology-free policy reqs there (retries, error handling, data formats); read them before topic 1, and revisit one only where the chosen stack contradicts it or supplies it natively. Run the core loop (ask → author → review) from `SKILL.md` for each topic below, in order. When the user has no preference, propose a sensible default that fits earlier answers and ask them to confirm.
4
4
 
5
5
  ## Author target for this phase
6
6
 
@@ -12,6 +12,18 @@ description: >-
12
12
  Load only the rules and references required for the current task. Files under `../../rules/` are
13
13
  the source of truth for `.plain` syntax and authoring constraints. Do not restate or override them.
14
14
 
15
+ **First, confirm the rules about to be loaded are current.** On the first invocation of this skill in
16
+ a session, invoke `check-forge-version` before loading any rule file. A stale plain-forge ships stale
17
+ rules, so every routing decision below inherits its verdict.
18
+
19
+ If that skill reports a stale or unconfirmed install, tell the user clearly that **authoring on an
20
+ outdated plain-forge is not recommended** and ask them to run `npx plain-forge update` before
21
+ continuing — the rules routed to below may not match what the current `codeplain` renderer expects.
22
+ Give them the choice rather than blocking: if they decline, continue with the rules on disk and treat
23
+ the stale install as the first suspect for anything that later behaves unexpectedly.
24
+
25
+ Skip the check on later invocations in the same session, and skip it when the network is unavailable.
26
+
15
27
  ## 1. Account for rules already loaded
16
28
 
17
29
  - In Claude Code, applicable `.claude/rules/*.md` files are loaded natively. Do not read those rule
@@ -27,6 +39,8 @@ All paths are relative to this `SKILL.md`.
27
39
 
28
40
  | Task or content | Rule files |
29
41
  |---|---|
42
+ | New module, file skeleton, section placement, or comments | `../../rules/module-structure.md` |
43
+ | Moving facts between sections (see `resolve-section-ownership`) | `../../rules/module-structure.md`, plus the rule file of each section involved |
30
44
  | Definitions or concept usage | `../../rules/definitions.md` |
31
45
  | Functional specs or acceptance tests | `../../rules/func-specs.md` |
32
46
  | Implementation requirements or unit tests | `../../rules/impl-reqs.md` |
@@ -36,7 +50,8 @@ All paths are relative to this `SKILL.md`.
36
50
  | `required_concepts` | `../../rules/required-concepts.md` |
37
51
  | `exported_concepts` | `../../rules/exported-concepts.md` |
38
52
  | Linked files or `resources/` | `../../rules/linked-resources.md` |
39
- | Any `.plain` text or example | `../../rules/bullet-continuation.md` |
53
+ | Any `.plain` text or example | `../../rules/bullet-continuation.md`, `../../rules/concise-specs.md` |
54
+ | Drafting or trimming spec wording, or reviewing a spec for verbosity | `../../rules/concise-specs-examples.md` |
40
55
 
41
56
  For a whole-file review or a task spanning several sections, load the union of the applicable rule
42
57
  files. Do not load every rule by default.
@@ -53,6 +68,9 @@ Then read the rule matching the integration shape:
53
68
  - Standalone integration: `../../rules/integration-standalone.md`
54
69
  - Embedded integration test scripts: `../../rules/integration-embedded-testing.md`
55
70
 
71
+ When drafting or trimming the wording of an integration spec, also read
72
+ `../../rules/concise-specs-integration-examples.md`.
73
+
56
74
  Do not load integration rules for ordinary non-integration specs.
57
75
 
58
76
  ## 4. Load operational references only when needed
@@ -61,7 +79,7 @@ Authoring rules remain authoritative if a reference appears to conflict with the
61
79
 
62
80
  | Need | Reference |
63
81
  |---|---|
64
- | Project layout, source-of-truth model, templates, or comments | `references/project-model.md` |
82
+ | Project layout, source-of-truth model, or template inclusion | `references/project-model.md` |
65
83
  | Rendering order, generated artifacts, conformance workflow, or test scripts | `references/rendering-and-testing.md` |
66
84
  | `codeplain` path resolution or CLI options | Invoke `load-codeplain-reference` |
67
85
 
@@ -35,14 +35,3 @@ In addition to frontmatter `import`, ***plain supports parameterized template in
35
35
 
36
36
  Parameters are key-value pairs. Templates access them with `{{ variable_name }}`. Only variable
37
37
  substitution is supported; conditionals, loops, and other Liquid features are not available.
38
-
39
- ## Comments
40
-
41
- Lines beginning with `>` are ignored during rendering:
42
-
43
- ```plain
44
- > This is a comment in ***plain.
45
- ```
46
-
47
- Comments explain the specification to human authors. Do not use comments to carry requirements the
48
- renderer must implement.
@@ -36,7 +36,7 @@ The skill is a **detect → fix → re-run** loop. It does not stop at the first
36
36
 
37
37
  ### Step 1 — Inventory the project
38
38
 
39
- 1. List every `.plain` file in the repo root (and any subdirectories that contain `.plain` files). Build the module graph from each file's YAML frontmatter (`requires`, `import`).
39
+ 1. List every `.plain` file in the repo root (and any subdirectories that contain `.plain` files). Build the module graph from each file's YAML frontmatter (`requires`, `import`). Verify the `requires` graph forms a tree: every module's multiple `requires` entries must lie on one root-to-tip ancestor path.
40
40
  2. Identify **top modules** — every module that is not `requires`-ed by any other module. A single-stack project has one top module; a multi-part project (e.g. backend + frontend) has one top module per part.
41
41
  3. List every `config.yaml` in the repo (root and per-part directories such as `backend/`, `frontend/`).
42
42
  4. List every script under `test_scripts/`.
@@ -125,7 +125,7 @@ For each subsequent module in the chain:
125
125
 
126
126
  After all new modules are created:
127
127
  - If the original module is being fully replaced (all specs moved out), delete it or rename it to avoid confusion.
128
- - If the original module was required by other modules, update those modules to `require` the last module in the new chain instead (since it transitively includes all prior modules' specs and generated code).
128
+ - If the original module was required by other modules, update those modules to `require` the last module in the new chain instead — its accumulated code and previous functional specs cover the whole chain. Exported concepts are **not** transitive, though: if a downstream module references a concept now exported by an earlier module in the new chain, it must list that earlier module in its `requires` as well.
129
129
 
130
130
  ## Phase 4 — Verify
131
131
 
@@ -145,7 +145,7 @@ Verify that the chronological order of all functional specs — when read across
145
145
  ### 4c. Concept availability check
146
146
 
147
147
  For each module, verify:
148
- - [ ] Every `:Concept:` referenced in its specs is either defined locally, available via `import`, or available via `exported_concepts` from the `requires` chain
148
+ - [ ] Every `:Concept:` referenced in its specs is either defined locally, available via `import`, or available via `exported_concepts` from a module listed in `requires`
149
149
  - [ ] `exported_concepts` includes every concept that downstream modules need
150
150
  - [ ] No concept name collisions between modules
151
151
 
@@ -193,5 +193,5 @@ Acceptance tests are nested under their parent functional spec. When moving a sp
193
193
  - [ ] Each module has at least one functional spec and one implementation req
194
194
  - [ ] `requires` chain is correctly ordered (base → leaf)
195
195
  - [ ] Original module removed or updated
196
- - [ ] Downstream modules (if any) updated to require the correct module
196
+ - [ ] Downstream modules (if any) updated to require the chain tip, plus any earlier chain modules whose exported concepts they reference
197
197
  - [ ] User approved the final result