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.
- package/README.md +1 -0
- package/bin/cli.mjs +1 -1
- package/forge/rules/bullet-continuation.md +16 -6
- package/forge/rules/concise-specs-examples.md +431 -0
- package/forge/rules/concise-specs-integration-examples.md +272 -0
- package/forge/rules/concise-specs.md +80 -0
- package/forge/rules/definitions.md +7 -4
- package/forge/rules/exported-concepts.md +3 -2
- package/forge/rules/func-specs.md +10 -3
- package/forge/rules/impl-reqs.md +8 -4
- package/forge/rules/import-modules.md +4 -6
- package/forge/rules/integration-embedded.md +0 -1
- package/forge/rules/integration-standalone.md +1 -1
- package/forge/rules/integrations.md +6 -7
- package/forge/rules/linked-resources.md +9 -8
- package/forge/rules/module-structure.md +105 -0
- package/forge/rules/required-concepts.md +5 -5
- package/forge/rules/requires-modules.md +37 -21
- package/forge/rules/test-reqs.md +11 -7
- package/forge/skills/add-acceptance-test/SKILL.md +1 -0
- package/forge/skills/add-concept/SKILL.md +2 -1
- package/forge/skills/add-feature/SKILL.md +2 -2
- package/forge/skills/add-functional-spec/SKILL.md +2 -1
- package/forge/skills/add-functional-specs/SKILL.md +2 -1
- package/forge/skills/add-implementation-requirement/SKILL.md +2 -1
- package/forge/skills/add-test-requirement/SKILL.md +1 -0
- package/forge/skills/break-down-func-spec/SKILL.md +1 -0
- package/forge/skills/check-forge-version/SKILL.md +154 -0
- package/forge/skills/check-forge-version/scripts/check-forge-version.mjs +368 -0
- package/forge/skills/create-import-module/SKILL.md +3 -3
- package/forge/skills/create-requires-module/SKILL.md +24 -19
- package/forge/skills/forge-plain/SKILL.md +1 -1
- package/forge/skills/forge-plain/references/checklist.md +2 -1
- package/forge/skills/forge-plain/references/phase-1-product.md +3 -2
- package/forge/skills/forge-plain/references/phase-2-tech.md +1 -1
- package/forge/skills/load-plain-reference/SKILL.md +20 -2
- package/forge/skills/load-plain-reference/references/project-model.md +0 -11
- package/forge/skills/plain-healthcheck/SKILL.md +1 -1
- package/forge/skills/refactor-module/SKILL.md +3 -3
- package/forge/skills/resolve-section-ownership/SKILL.md +65 -0
- package/forge/skills/resolve-spec-conflict/SKILL.md +1 -0
- 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 |
|
|
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`
|
|
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
|
|
21
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
35
|
-
4. **Review
|
|
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
|
|
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`
|
|
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
|
|
82
|
-
- The renderer sees the
|
|
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
|
|
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 |
|
|
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
|
-
- [ ]
|
|
102
|
-
- [ ]
|
|
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
|
|
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 `***
|
|
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 `***
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|