wdi-method 0.6.30 → 0.6.32
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/CHANGELOG.md +129 -0
- package/NOTICE +7 -2
- package/README.md +16 -11
- package/bin/wdi-method.js +396 -87
- package/kit/.constitution/method/document/architecture-guide.md +217 -209
- package/kit/.constitution/method/document/bmad-skill-register.md +107 -104
- package/kit/.constitution/method/document/corpus-guide.md +522 -517
- package/kit/.constitution/method/document/decision-guide.md +236 -216
- package/kit/.constitution/method/document/delivery-flow-guide.md +20 -0
- package/kit/.constitution/method/document/prd-guide.md +245 -245
- package/kit/.constitution/method/document/templates/design-system.md +96 -66
- package/kit/.constitution/method/document/templates/experience.md +62 -0
- package/kit/.constitution/method/document/templates/structure-codebase.md +131 -129
- package/kit/.constitution/method/document/templates/ux.md +78 -76
- package/kit/.constitution/method/document/ux-guide.md +161 -115
- package/kit/.constitution/method/method-glossary.md +3 -0
- package/kit/.constitution/method/scripts/validate.py +3375 -3200
- package/kit/.constitution/method/structure-guide.md +204 -202
- package/kit/.constitution/method/why/README.md +1 -1
- package/kit/.constitution/method/why/artifact-map.md +158 -157
- package/kit/.constitution/method/why/portability.md +19 -2
- package/kit/skills/wdi-autopilot/SKILL.md +32 -19
- package/kit/skills/wdi-blueprint/SKILL.md +271 -264
- package/kit/skills/wdi-build/SKILL.md +28 -19
- package/kit/skills/wdi-component/SKILL.md +179 -174
- package/kit/skills/wdi-daily-autopilot/SKILL.md +24 -13
- package/kit/skills/wdi-daily-what-to-build/SKILL.md +9 -6
- package/kit/skills/wdi-daily-what-to-test/SKILL.md +2 -0
- package/kit/skills/wdi-decision/SKILL.md +206 -203
- package/kit/skills/wdi-explain-to-me/SKILL.md +2 -0
- package/kit/skills/wdi-help/SKILL.md +130 -125
- package/kit/skills/wdi-init/SKILL.md +10 -5
- package/kit/skills/wdi-problem/SKILL.md +114 -108
- package/kit/skills/wdi-product/SKILL.md +167 -162
- package/kit/skills/wdi-prune-or-archive/SKILL.md +2 -0
- package/kit/skills/wdi-reconcile/SKILL.md +170 -169
- package/kit/skills/wdi-upgrade/SKILL.md +234 -215
- package/kit/skills/wdi-ux/SKILL.md +187 -169
- package/kit-overlay/AGENTS.md +15 -2
- package/kit-overlay/portability.md +19 -2
- package/lib/platforms.mjs +420 -248
- package/package.json +1 -1
- package/scaffold/.control/registry/index.yaml +2 -1
package/bin/wdi-method.js
CHANGED
|
@@ -20,13 +20,23 @@ import {
|
|
|
20
20
|
writeProductIdentity,
|
|
21
21
|
} from "../lib/identity.mjs";
|
|
22
22
|
import {
|
|
23
|
+
declaredPlatforms,
|
|
23
24
|
detectPlatforms,
|
|
24
25
|
formatPlatformList,
|
|
26
|
+
getPlatform,
|
|
27
|
+
hostsYaml,
|
|
25
28
|
isKnownPlatform,
|
|
29
|
+
isRetiredPlatform,
|
|
30
|
+
normalizePlatformId,
|
|
26
31
|
normalizePlatformIds,
|
|
32
|
+
PLATFORMS,
|
|
27
33
|
platformSelectOptions,
|
|
34
|
+
platformsWithManualOnly,
|
|
28
35
|
platformUsesHook,
|
|
29
36
|
PREFERRED_PLATFORM_IDS,
|
|
37
|
+
RETIRED_PLATFORMS,
|
|
38
|
+
retiredPlatformsIn,
|
|
39
|
+
ruleFiles,
|
|
30
40
|
skillDestinations,
|
|
31
41
|
} from "../lib/platforms.mjs";
|
|
32
42
|
import { opencodeCommandsDir, syncOpencodeCommands } from "../lib/opencode-commands.mjs";
|
|
@@ -101,9 +111,36 @@ const GUARD_LINE = `> **${GUARD_MARK}.** \`wdi-method\` unlocked model invocatio
|
|
|
101
111
|
+ "session, a subagent that thought this looked relevant — stop and say so: this engine publishes "
|
|
102
112
|
+ "to the tracker and writes code.";
|
|
103
113
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
114
|
+
// The method's own skills a person MUST type. Each carries `disable-model-invocation: true` (the hosts
|
|
115
|
+
// that honour it hold it there), a guard line at the top of its body (every host reads that), and a
|
|
116
|
+
// line in the AGENTS.md block (every host reads that too).
|
|
117
|
+
const WDI_MANUAL_ONLY = [
|
|
118
|
+
"wdi-daily-what-to-build",
|
|
119
|
+
"wdi-daily-autopilot",
|
|
120
|
+
"wdi-daily-what-to-test",
|
|
121
|
+
"wdi-prune-or-archive",
|
|
122
|
+
"wdi-explain-to-me",
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
/** Every folder in a repo that may hold skills.
|
|
126
|
+
*
|
|
127
|
+
* NOT a hand-kept list of hosts. The one this replaced named five folders while the installer
|
|
128
|
+
* offered forty hosts, so an engine installed for Kiro, Cline, or Trae was reported missing and never
|
|
129
|
+
* unlocked. Every `.<host>/skills` at the root counts, plus every directory a supported host is
|
|
130
|
+
* documented to read (`skills/` for OpenClaw, nested ones) — a repo installs the engines wherever
|
|
131
|
+
* `npx skills add` was pointed, and that installer symlinks across several of them.
|
|
132
|
+
*/
|
|
133
|
+
function skillRoots(target) {
|
|
134
|
+
const roots = new Set(PLATFORMS.flatMap((p) => p.reads));
|
|
135
|
+
if (fs.existsSync(target)) {
|
|
136
|
+
for (const e of fs.readdirSync(target, { withFileTypes: true })) {
|
|
137
|
+
if (!e.name.startsWith(".") || e.name === ".git") continue;
|
|
138
|
+
if (!e.isDirectory() && !e.isSymbolicLink()) continue;
|
|
139
|
+
if (fs.existsSync(path.join(target, e.name, "skills"))) roots.add(`${e.name}/skills`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [...roots];
|
|
143
|
+
}
|
|
107
144
|
|
|
108
145
|
// BMad skills RETIRED at G5. This array is the single home of that list: `bmad-skill-register.md`
|
|
109
146
|
// carries the same names for a reader, and a test fails when the two disagree.
|
|
@@ -169,7 +206,10 @@ function usage() {
|
|
|
169
206
|
install [dir] first install (TUI unless --yes)
|
|
170
207
|
update [dir] update (TUI unless --yes)
|
|
171
208
|
verify [dir]
|
|
172
|
-
engines [dir] [--fix]
|
|
209
|
+
engines [dir] [--fix] [--copy]
|
|
210
|
+
report the six engines, their invocation state, and the BMad G5 ban;
|
|
211
|
+
--copy puts the repo's own engine copies where each selected host reads
|
|
212
|
+
upgrade-check [dir] re-probe what wdi-upgrade still owes; rewrites upgrade_pending (exit 1 if any)
|
|
173
213
|
promote <live-dir> --rescue pull a method change back out of a consumer (not the normal flow)
|
|
174
214
|
|
|
175
215
|
--yes non-interactive
|
|
@@ -181,6 +221,7 @@ function usage() {
|
|
|
181
221
|
--doc-filename-language <text> slug part of document filenames; free text, default English
|
|
182
222
|
--skip-bmad-check
|
|
183
223
|
--skip-engines-check install without to-spec / to-tickets / implement
|
|
224
|
+
--copy-engines engines are in the repo but a selected host cannot read them: copy them there
|
|
184
225
|
|
|
185
226
|
BMad first, then this package. ${WDI_REPO}
|
|
186
227
|
`);
|
|
@@ -214,7 +255,7 @@ function parseArgs(argv) {
|
|
|
214
255
|
return args;
|
|
215
256
|
}
|
|
216
257
|
const first = rest[0];
|
|
217
|
-
if (["install", "update", "verify", "promote", "engines"].includes(first)) {
|
|
258
|
+
if (["install", "update", "verify", "promote", "engines", "upgrade-check"].includes(first)) {
|
|
218
259
|
args.cmd = rest.shift();
|
|
219
260
|
} else if (first.startsWith("-")) {
|
|
220
261
|
args.cmd = "wizard";
|
|
@@ -226,15 +267,21 @@ function parseArgs(argv) {
|
|
|
226
267
|
const t = rest.shift();
|
|
227
268
|
if (t === "--skip-bmad-check") args.skipBmad = true;
|
|
228
269
|
else if (t === "--fix") args.fix = true;
|
|
270
|
+
else if (t === "--copy" || t === "--copy-engines") args.copyEngines = true;
|
|
229
271
|
else if (t === "--skip-engines-check") args.skipEngines = true;
|
|
230
272
|
else if (t === "--rescue") args.rescue = true;
|
|
231
273
|
else if (t === "--yes" || t === "-y") args.yes = true;
|
|
232
274
|
else if (t === "--agents") {
|
|
233
275
|
const raw = rest.shift();
|
|
234
276
|
if (!raw) die("--agents needs a comma-separated list");
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
277
|
+
const asked = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
278
|
+
const retired = asked.filter((a) => isRetiredPlatform(a));
|
|
279
|
+
if (retired.length) {
|
|
280
|
+
die(retired.map((a) => `${a} is no longer supported — ${RETIRED_PLATFORMS[normalizePlatformId(a)]}`)
|
|
281
|
+
.join("\n "));
|
|
282
|
+
}
|
|
283
|
+
args.agents = normalizePlatformIds(asked);
|
|
284
|
+
const unknown = asked.filter((a) => !isKnownPlatform(a));
|
|
238
285
|
if (unknown.length) die(`unknown platform: ${unknown.join(", ")} (run --list-agents)`);
|
|
239
286
|
if (!args.agents.length) die("--agents needs at least one known platform");
|
|
240
287
|
} else if (t === "--product") args.product = rest.shift();
|
|
@@ -361,30 +408,73 @@ function requireTarget(dir) {
|
|
|
361
408
|
*/
|
|
362
409
|
function repoSkillFiles(target, names) {
|
|
363
410
|
const out = new Map();
|
|
364
|
-
for (const
|
|
411
|
+
for (const root of skillRoots(target)) {
|
|
365
412
|
for (const name of names) {
|
|
366
|
-
const file = path.join(target,
|
|
413
|
+
const file = path.join(target, root, name, "SKILL.md");
|
|
367
414
|
if (!fs.existsSync(file)) continue;
|
|
368
415
|
let real = file;
|
|
369
416
|
try {
|
|
370
417
|
real = fs.realpathSync(file);
|
|
371
418
|
} catch {}
|
|
372
419
|
if (!out.has(name)) out.set(name, new Map());
|
|
373
|
-
out.get(name)
|
|
420
|
+
const seen = out.get(name);
|
|
421
|
+
if (!seen.has(real)) seen.set(real, []);
|
|
422
|
+
seen.get(real).push(file);
|
|
374
423
|
}
|
|
375
424
|
}
|
|
376
425
|
return out;
|
|
377
426
|
}
|
|
378
427
|
|
|
379
|
-
/** The six engines, in the REPO
|
|
380
|
-
|
|
428
|
+
/** The six engines, in the REPO — and visible to EVERY selected host.
|
|
429
|
+
*
|
|
430
|
+
* Present somewhere is not enough. Kiro reads `.kiro/skills` and nothing else, so engines in
|
|
431
|
+
* `.agents/skills` are invisible to it and `wdi-build` fails there at G5. `missingByHost` names, per
|
|
432
|
+
* selected host, the engines none of its `reads` directories holds. With no hosts given it falls
|
|
433
|
+
* back to "anywhere in the repo", which is what the unlock and the lock work on.
|
|
434
|
+
* A user-level plugin is not an answer here — see ENGINE_SKILLS.
|
|
435
|
+
*/
|
|
436
|
+
function enginesReport(target, agents = []) {
|
|
381
437
|
const files = repoSkillFiles(target, ENGINE_SKILLS);
|
|
382
438
|
const missing = ENGINE_SKILLS.filter((n) => !files.has(n));
|
|
383
|
-
|
|
439
|
+
const missingByHost = new Map();
|
|
440
|
+
for (const id of normalizePlatformIds(agents)) {
|
|
441
|
+
const reads = getPlatform(id).reads.map((d) => path.join(target, d) + path.sep);
|
|
442
|
+
const visible = (name) => [...(files.get(name)?.values() || [])].flat()
|
|
443
|
+
.some((f) => reads.some((r) => f.startsWith(r)));
|
|
444
|
+
const lacking = ENGINE_SKILLS.filter((n) => !visible(n));
|
|
445
|
+
if (lacking.length) missingByHost.set(id, lacking);
|
|
446
|
+
}
|
|
447
|
+
const present = agents.length ? missingByHost.size === 0 : missing.length === 0;
|
|
448
|
+
return { files, missing, missingByHost, present };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function enginesPresent(target, agents = []) {
|
|
452
|
+
return enginesReport(target, agents).present;
|
|
384
453
|
}
|
|
385
454
|
|
|
386
|
-
|
|
387
|
-
|
|
455
|
+
/** Engines that ARE in the repo, copied into the skill folder of each selected host that cannot see
|
|
456
|
+
* them. The source is the repo's own copy — no network, no third-party installer — which is what
|
|
457
|
+
* `npx skills add` in "copy" mode would have produced. An existing folder is never overwritten.
|
|
458
|
+
* Returns the number of engine folders written; engines missing from the repo entirely are left to
|
|
459
|
+
* `npx skills add`, because there is nothing here to copy from.
|
|
460
|
+
*/
|
|
461
|
+
function copyEnginesToHosts(target, agents) {
|
|
462
|
+
const report = enginesReport(target, agents);
|
|
463
|
+
let written = 0;
|
|
464
|
+
for (const [id, names] of report.missingByHost) {
|
|
465
|
+
const host = getPlatform(id);
|
|
466
|
+
for (const name of names) {
|
|
467
|
+
const copies = report.files.get(name);
|
|
468
|
+
if (!copies) continue;
|
|
469
|
+
const source = path.dirname([...copies.keys()].sort()[0]);
|
|
470
|
+
const dest = path.join(target, ...host.skillDir.split("/"), name);
|
|
471
|
+
if (fs.existsSync(dest)) continue;
|
|
472
|
+
copyTree(source, dest);
|
|
473
|
+
written += 1;
|
|
474
|
+
}
|
|
475
|
+
note(`copied engines for ${host.name} → ${host.skillDir}/ (from the repo's own copy)`);
|
|
476
|
+
}
|
|
477
|
+
return written;
|
|
388
478
|
}
|
|
389
479
|
|
|
390
480
|
/** Only ever a WARNING. The plugin's copies are namespaced and still flagged, so they can neither be
|
|
@@ -482,7 +572,10 @@ function retireBmadG5(target) {
|
|
|
482
572
|
* Merged, never replaced: a product's own permissions are its own. Invalid JSON is reported rather
|
|
483
573
|
* than repaired — rewriting a settings file nobody can parse is how a repo loses its allowlist.
|
|
484
574
|
*/
|
|
485
|
-
function writeDenyRules(target) {
|
|
575
|
+
function writeDenyRules(target, agents) {
|
|
576
|
+
// Claude Code's own settings file, for Claude Code only. Writing it for a Kiro-only repo would
|
|
577
|
+
// create a `.claude/` folder nothing reads.
|
|
578
|
+
if (!platformsWithManualOnly(agents, "claude").length) return 0;
|
|
486
579
|
const file = path.join(target, ".claude", "settings.json");
|
|
487
580
|
let settings = {};
|
|
488
581
|
if (fs.existsSync(file)) {
|
|
@@ -509,6 +602,46 @@ function writeDenyRules(target) {
|
|
|
509
602
|
return added.length;
|
|
510
603
|
}
|
|
511
604
|
|
|
605
|
+
/** OpenCode's equivalent: `permission.skill: ask` in `opencode.json`. The model may still reach for
|
|
606
|
+
* the skill, and OpenCode then asks the person before loading it — a gate the model cannot pass
|
|
607
|
+
* alone, with the human route left open. `deny` would hide it from the person too.
|
|
608
|
+
*
|
|
609
|
+
* Merged, never replaced, under the same rules as `.claude/settings.json`: a value the product
|
|
610
|
+
* already set for a name is kept, and a file nobody can parse (`opencode.json` may carry comments) is
|
|
611
|
+
* reported rather than rewritten.
|
|
612
|
+
*/
|
|
613
|
+
function writeOpencodePermissions(target, agents) {
|
|
614
|
+
if (!platformsWithManualOnly(agents, "opencode").length) return 0;
|
|
615
|
+
const file = path.join(target, "opencode.json");
|
|
616
|
+
let cfg = {};
|
|
617
|
+
if (fs.existsSync(file)) {
|
|
618
|
+
try {
|
|
619
|
+
cfg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
620
|
+
} catch {
|
|
621
|
+
note("opencode.json is not plain JSON — skill permissions NOT written; add them by hand: "
|
|
622
|
+
+ `permission.skill "${WDI_MANUAL_ONLY[0]}": "ask" (and the rest in bmad-skill-register.md)`);
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
625
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) return 0;
|
|
626
|
+
}
|
|
627
|
+
const perm = cfg.permission && typeof cfg.permission === "object" && !Array.isArray(cfg.permission)
|
|
628
|
+
? cfg.permission : {};
|
|
629
|
+
if (typeof perm.skill === "string") {
|
|
630
|
+
note(`opencode.json sets permission.skill to "${perm.skill}" for every skill — left as the product wrote it`);
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
const skill = perm.skill && typeof perm.skill === "object" ? perm.skill : {};
|
|
634
|
+
const added = [...WDI_MANUAL_ONLY, ...BMAD_RETIRED_G5].filter((n) => !(n in skill));
|
|
635
|
+
if (!added.length) return 0;
|
|
636
|
+
for (const n of added) skill[n] = "ask";
|
|
637
|
+
perm.skill = skill;
|
|
638
|
+
cfg.permission = perm;
|
|
639
|
+
if (!cfg.$schema) cfg = { $schema: "https://opencode.ai/config.json", ...cfg };
|
|
640
|
+
fs.writeFileSync(file, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
|
|
641
|
+
note(`opencode.json: ${added.length} manual-only skill${added.length === 1 ? "" : "s"} set to "ask"`);
|
|
642
|
+
return added.length;
|
|
643
|
+
}
|
|
644
|
+
|
|
512
645
|
/** A trace of what the engines were when the method last looked — not a lockfile.
|
|
513
646
|
*
|
|
514
647
|
* `npx skills add` writes its own `skills-lock.json` with a folder hash per skill, and that hash
|
|
@@ -564,18 +697,37 @@ function bmadMissingMessage() {
|
|
|
564
697
|
//
|
|
565
698
|
// So it blocks, and `--skip-engines-check` is the escape, exactly as `--skip-bmad-check` is for BMad. The
|
|
566
699
|
// escape matters: CI installs into a bare checkout, and a repo that will never reach G5 is a real case.
|
|
567
|
-
function enginesMissingMessage(
|
|
568
|
-
const
|
|
700
|
+
function enginesMissingMessage(report) {
|
|
701
|
+
const missing = report?.missing?.length ? report.missing
|
|
702
|
+
: report?.missingByHost?.size ? [] : ENGINE_SKILLS;
|
|
703
|
+
const byHost = report?.missingByHost?.size ? [...report.missingByHost] : [];
|
|
704
|
+
const head = [];
|
|
705
|
+
if (missing.length) {
|
|
706
|
+
head.push(`The engines are not in this repo. Missing: ${missing.join(" · ")}`, "",
|
|
707
|
+
` ${ENGINES_INSTALL_ANY}`);
|
|
708
|
+
}
|
|
709
|
+
if (byHost.length) {
|
|
710
|
+
if (head.length) head.push("");
|
|
711
|
+
head.push("Not where every selected host can read them — install for each host named here:", "");
|
|
712
|
+
for (const [id, names] of byHost) {
|
|
713
|
+
const p = getPlatform(id);
|
|
714
|
+
head.push(` ${p.name} reads ${p.reads.join(", ")} — cannot see ${names.join(" · ")}`,
|
|
715
|
+
` ${ENGINES_INSTALL_ANY} --agent ${p.skillsCli} (lands them where ${p.name} reads)`);
|
|
716
|
+
}
|
|
717
|
+
head.push("",
|
|
718
|
+
"Or, without the network — the engines are already in this repo, just not where those hosts read:",
|
|
719
|
+
" npx wdi-method engines --copy (or add --copy-engines to install / update)",
|
|
720
|
+
"Or leave those hosts out if this repo does not use them: --agents <the hosts you use>");
|
|
721
|
+
}
|
|
569
722
|
return [
|
|
570
|
-
|
|
723
|
+
...head,
|
|
571
724
|
"",
|
|
572
725
|
"They MUST be installed INTO the repo, not as a user-level plugin — the method strips",
|
|
573
726
|
"`disable-model-invocation` from its own copies so `wdi-build` and `wdi-autopilot` can drive",
|
|
574
727
|
"them, and a plugin's files are not the repo's to edit.",
|
|
575
728
|
"",
|
|
576
|
-
`
|
|
577
|
-
"",
|
|
578
|
-
`Take all six: ${ENGINE_SKILLS.join(" · ")}. Choose "copy" or "symlink" — either is read.`,
|
|
729
|
+
`Take all six: ${ENGINE_SKILLS.join(" · ")}. Choose "copy" or "symlink" — either is read; on Windows`,
|
|
730
|
+
"without Developer Mode a symlink fails and the installer falls back to copy, which is fine.",
|
|
579
731
|
"",
|
|
580
732
|
"You do NOT need to run the setup skill after this — the installer seeds docs/agents/ already",
|
|
581
733
|
`answered for this method. Run ${ENGINES_SETUP} only to change tracker.`,
|
|
@@ -1266,9 +1418,47 @@ function seedEmptyLayers(target, { first }) {
|
|
|
1266
1418
|
}
|
|
1267
1419
|
}
|
|
1268
1420
|
|
|
1269
|
-
|
|
1421
|
+
// What `wdi-upgrade` still owes, written where it is committed. The summary line alone was the only
|
|
1422
|
+
// record, so a repo whose owner missed that one screen could not find out again — and `wdi-help` can
|
|
1423
|
+
// only route to `wdi-upgrade` from something it can read. Absent when nothing is pending: an empty
|
|
1424
|
+
// list would read as a question nobody has to answer.
|
|
1425
|
+
const PENDING_HEADER = "# What wdi-upgrade still has to move. Re-probed by `npx wdi-method upgrade-check`; absent = nothing.";
|
|
1426
|
+
|
|
1427
|
+
function pendingBlock(items) {
|
|
1428
|
+
if (!items.length) return "";
|
|
1429
|
+
return `${PENDING_HEADER}\nupgrade_pending:\n${items.map((i) => ` - ${JSON.stringify(i)}\n`).join("")}`;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Re-probe and rewrite ONLY the pending block, leaving the rest of the stamp as `update` wrote it.
|
|
1433
|
+
// This is how `wdi-upgrade` closes: the field disappears when the probes come back clean.
|
|
1434
|
+
function upgradeCheck(target) {
|
|
1435
|
+
const file = path.join(target, ".control", "wdi-method.yaml");
|
|
1436
|
+
if (!fs.existsSync(file)) die("no .control/wdi-method.yaml — the method is not installed here");
|
|
1437
|
+
const items = pendingUpgrades(target);
|
|
1438
|
+
let text = fs.readFileSync(file, "utf8")
|
|
1439
|
+
.replace(/^# What wdi-upgrade still has to move\..*\r?\n/m, "")
|
|
1440
|
+
.replace(/^upgrade_pending:\r?\n(?:[ \t]+- .*\r?\n)*/m, "");
|
|
1441
|
+
const block = pendingBlock(items);
|
|
1442
|
+
if (block) {
|
|
1443
|
+
text = /^installed_at:/m.test(text)
|
|
1444
|
+
? text.replace(/^installed_at:/m, `${block}installed_at:`)
|
|
1445
|
+
: `${text.replace(/\s*$/, "\n")}${block}`;
|
|
1446
|
+
}
|
|
1447
|
+
fs.writeFileSync(file, text, "utf8");
|
|
1448
|
+
console.log("");
|
|
1449
|
+
if (!items.length) {
|
|
1450
|
+
ok("nothing pending — wdi-upgrade owes nothing here");
|
|
1451
|
+
return 0;
|
|
1452
|
+
}
|
|
1453
|
+
console.log(` upgrade ${items.length} item${items.length === 1 ? "" : "s"} still in the OLD shape — run the wdi-upgrade skill`);
|
|
1454
|
+
for (const item of items) console.log(` ${DIM}·${RESET} ${item}`);
|
|
1455
|
+
return 1;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function writeStamp(target, agents) {
|
|
1270
1459
|
const control = path.join(target, ".control");
|
|
1271
1460
|
if (!fs.existsSync(control)) return;
|
|
1461
|
+
const hosts = normalizePlatformIds(agents && agents.length ? agents : declaredPlatforms(target, fs));
|
|
1272
1462
|
const eng = enginesReport(target);
|
|
1273
1463
|
const fp = engineFingerprints(target);
|
|
1274
1464
|
const names = Object.keys(fp);
|
|
@@ -1276,6 +1466,12 @@ function writeStamp(target) {
|
|
|
1276
1466
|
"# Written by wdi-method install/update. A trace, not a lockfile.",
|
|
1277
1467
|
`wdi_method: ${PKG.version}`,
|
|
1278
1468
|
`bmad_method: ${readBmadVersion(target) || '""'}`,
|
|
1469
|
+
"# The hosts this repo was installed for. `update` reuses this list; the skills and validate.py",
|
|
1470
|
+
"# read `hosts:` to learn where each host reads skills, how a person types one, and whether the",
|
|
1471
|
+
"# host has its own scheduler.",
|
|
1472
|
+
"platforms:",
|
|
1473
|
+
...hosts.map((id) => ` - ${id}`),
|
|
1474
|
+
hostsYaml(hosts),
|
|
1279
1475
|
];
|
|
1280
1476
|
if (names.length) {
|
|
1281
1477
|
const blocked = engineInvocationState(target).blocked;
|
|
@@ -1293,6 +1489,8 @@ function writeStamp(target) {
|
|
|
1293
1489
|
lines.push("engines:");
|
|
1294
1490
|
lines.push(" source: none # none in this repo — G5 cannot run until they are installed");
|
|
1295
1491
|
}
|
|
1492
|
+
const pending = pendingBlock(pendingUpgrades(target));
|
|
1493
|
+
if (pending) lines.push(pending.trimEnd());
|
|
1296
1494
|
lines.push(`installed_at: ${today()}`);
|
|
1297
1495
|
lines.push("");
|
|
1298
1496
|
const stamp = lines.join("\n");
|
|
@@ -1427,17 +1625,17 @@ function pendingUpgrades(target) {
|
|
|
1427
1625
|
});
|
|
1428
1626
|
};
|
|
1429
1627
|
const items = [];
|
|
1430
|
-
if (has(".control", "registry", "requirements.yaml")) items.push("requirements.yaml → goals.yaml + requirements-<slug>.yaml");
|
|
1628
|
+
if (has(".control", "registry", "requirements.yaml")) items.push("#1 requirements.yaml → goals.yaml + requirements-<slug>.yaml");
|
|
1431
1629
|
// The file the engines actually read. `/setup-matt-pocock-skills` writes its own answer here — no
|
|
1432
1630
|
// `specs.yaml`, no predefined path — and `seedAgentDocs` will not overwrite a file the product owns,
|
|
1433
1631
|
// so without this probe the repo never learns why its tickets scatter.
|
|
1434
1632
|
if (has("docs", "agents", "issue-tracker.md")
|
|
1435
1633
|
&& !read("docs", "agents", "issue-tracker.md").includes("seeded by `wdi-method`")) {
|
|
1436
|
-
items.push("docs/agents/issue-tracker.md is not the method's answer (npx wdi-method engines --fix)");
|
|
1634
|
+
items.push("#11 docs/agents/issue-tracker.md is not the method's answer (npx wdi-method engines --fix)");
|
|
1437
1635
|
}
|
|
1438
1636
|
const strays = specsOutsideScratch(read(".control", "registry", "specs.yaml"));
|
|
1439
1637
|
if (strays.length) {
|
|
1440
|
-
items.push(
|
|
1638
|
+
items.push(`#12 spec_folder outside .scratch/<spec-id>-<slug>/ on ${strays.join(", ")} `
|
|
1441
1639
|
+ `(the folder moves, then its cites)`);
|
|
1442
1640
|
}
|
|
1443
1641
|
// Reported only where it is still WORK. A closed pre-rename wave is read correctly (0.6.7 taught
|
|
@@ -1451,46 +1649,109 @@ function pendingUpgrades(target) {
|
|
|
1451
1649
|
// that would answer "not mine" and stop.
|
|
1452
1650
|
const legacyOpen = specsInLegacyShape(read(".control", "registry", "specs.yaml"));
|
|
1453
1651
|
if (legacyOpen.length) {
|
|
1454
|
-
items.push(
|
|
1652
|
+
items.push(`#2 ${legacyOpen.join(", ")} still in the W<n>/epics/stories shape and not closed `
|
|
1455
1653
|
+ `(flattened into tickets, id kept as its retired alias)`);
|
|
1456
1654
|
}
|
|
1457
|
-
if (/^## (Executive Summary|Vision|Assumptions|Prerequisites)\s*$/m.test(read(".what", "_product-brief", "brief.md"))) items.push("brief.md in the 14-section shape");
|
|
1655
|
+
if (/^## (Executive Summary|Vision|Assumptions|Prerequisites)\s*$/m.test(read(".what", "_product-brief", "brief.md"))) items.push("#3 brief.md in the 14-section shape");
|
|
1458
1656
|
// Sections by NAME: the numbers moved between kits (Non-Goals was §7 in one, §5 in the next).
|
|
1459
|
-
if (anyIn(".what/_prd", "prd.md", /^## (\d+\.\s*)?(Document Purpose|Glossary|Non-Goals|Open Questions|Assumptions Index)\b|\*\*Proof of done:\*\*/m)) items.push("a prd.md in the 12-section shape, or with FR blocks");
|
|
1657
|
+
if (anyIn(".what/_prd", "prd.md", /^## (\d+\.\s*)?(Document Purpose|Glossary|Non-Goals|Open Questions|Assumptions Index)\b|\*\*Proof of done:\*\*/m)) items.push("#4 a prd.md in the 12-section shape, or with FR blocks");
|
|
1460
1658
|
const whatDir = path.join(target, ".what");
|
|
1461
1659
|
if (fs.existsSync(whatDir)) {
|
|
1462
1660
|
for (const pc of fs.readdirSync(whatDir)) {
|
|
1463
1661
|
if (pc.startsWith("_")) continue;
|
|
1464
1662
|
const srs = read(".what", pc, `SRS-${pc}.md`);
|
|
1465
|
-
if (/^\|\s*UC-\d+\s*\|/m.test(srs)) { items.push("an SRS with a UC Catalogue table (now a pointer)"); break; }
|
|
1663
|
+
if (/^\|\s*UC-\d+\s*\|/m.test(srs)) { items.push("#5 an SRS with a UC Catalogue table (now a pointer)"); break; }
|
|
1466
1664
|
}
|
|
1467
1665
|
}
|
|
1468
1666
|
const howDir = path.join(target, ".how");
|
|
1469
1667
|
if (fs.existsSync(howDir)) {
|
|
1470
1668
|
for (const pc of fs.readdirSync(howDir)) {
|
|
1471
1669
|
if (pc.startsWith("_")) continue;
|
|
1472
|
-
if (/\|\s*Quoted rule\s*\||Quoted verbatim from/.test(read(".how", pc, `SDD-${pc}.md`))) { items.push("an SDD quoting AD-N text (now ids only)"); break; }
|
|
1670
|
+
if (/\|\s*Quoted rule\s*\||Quoted verbatim from/.test(read(".how", pc, `SDD-${pc}.md`))) { items.push("#6 an SDD quoting AD-N text (now ids only)"); break; }
|
|
1473
1671
|
}
|
|
1474
1672
|
}
|
|
1475
|
-
if (/\|\s*Container\s*\|\s*Product Components living in it\s*\|/.test(read(".how", "_platform", "c4-l2-containers.md"))) items.push("c4-l2 with a PC x container table (now a pointer)");
|
|
1476
|
-
if (has(".control", "generated", "brief.md") || has(".control", "generated", "blueprint.md")) items.push("human pages still in .control/generated/ (render clears them)");
|
|
1477
|
-
if (has(".what", "_product-brief", "brief.md") && !has(".what-rendered")) items.push("no .what-rendered/ yet (render creates it)");
|
|
1673
|
+
if (/\|\s*Container\s*\|\s*Product Components living in it\s*\|/.test(read(".how", "_platform", "c4-l2-containers.md"))) items.push("#7 c4-l2 with a PC x container table (now a pointer)");
|
|
1674
|
+
if (has(".control", "generated", "brief.md") || has(".control", "generated", "blueprint.md")) items.push("#8 human pages still in .control/generated/ (render clears them)");
|
|
1675
|
+
if (has(".what", "_product-brief", "brief.md") && !has(".what-rendered")) items.push("#9 no .what-rendered/ yet (render creates it)");
|
|
1478
1676
|
// Skipped: what the validator never reads (kit copies, rendered output, dependencies) and what it
|
|
1479
1677
|
// treats as a record of the PAST — memlog, decisions, reports, _bmad-output. A stale path in a log
|
|
1480
1678
|
// is history, not a finding, and repointing it would falsify the record.
|
|
1481
1679
|
const SKIP = new Set([".git", "node_modules", "target", ".constitution", ".claude", ".agents", ".agent",
|
|
1482
1680
|
".what-rendered", ".how-rendered", "dist", "build", "memlog", "decisions", "reports", "meetings", "_bmad-output", ".work"]);
|
|
1483
1681
|
const OLD_PAGE = /\.control\/generated\/(brief|blueprint|prd-[a-z0-9-]+)\.md/;
|
|
1682
|
+
// At the root, every dot-folder except the corpus layers is a tool's: an agent host's skill copies
|
|
1683
|
+
// (`.kiro/skills/wdi-upgrade` names the OLD paths on purpose, to probe for them), editor state, CI.
|
|
1684
|
+
// Listing hosts by name missed every one outside the first three, and each such repo was told to run
|
|
1685
|
+
// `wdi-upgrade` on every update with nothing to move.
|
|
1686
|
+
const CORPUS_DOT = new Set([".control", ".what", ".how"]);
|
|
1484
1687
|
const citesOldPage = (dir, depth) => {
|
|
1485
1688
|
if (depth > 8) return false;
|
|
1486
1689
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1690
|
+
if (e.isDirectory() && depth === 0 && e.name.startsWith(".") && !CORPUS_DOT.has(e.name)) continue;
|
|
1487
1691
|
if (e.isDirectory()) { if (!SKIP.has(e.name) && citesOldPage(path.join(dir, e.name), depth + 1)) return true; continue; }
|
|
1488
1692
|
if (e.name === "answered.md") continue;
|
|
1489
1693
|
if (e.name.endsWith(".md") && OLD_PAGE.test(fs.readFileSync(path.join(dir, e.name), "utf8"))) return true;
|
|
1490
1694
|
}
|
|
1491
1695
|
return false;
|
|
1492
1696
|
};
|
|
1493
|
-
if (citesOldPage(target, 0)) items.push("a document cites .control/generated/brief|blueprint|prd-*.md (pages moved to the rendered trees)");
|
|
1697
|
+
if (citesOldPage(target, 0)) items.push("#10 a document cites .control/generated/brief|blueprint|prd-*.md (pages moved to the rendered trees)");
|
|
1698
|
+
// Cross-component EXPERIENCE parked in the product's design system. Before `.what/experience.md`
|
|
1699
|
+
// existed it had no home, so a repo put it in the one product-level UX file there was — a promise
|
|
1700
|
+
// in the build layer. The section names are bmad-ux's own, which is where the content comes from.
|
|
1701
|
+
// The names are bmad-ux's own and the product-level template's, in either spelling — a probe narrower
|
|
1702
|
+
// than the template it migrates into reports "nothing owed" while the content is still in the wrong layer.
|
|
1703
|
+
if (/^## (Foundation|Information architecture|Voice and tone|Flow map|Onboarding|Journeys|Shared edge cases|Promises every surface keeps|Cross-component (behaviou?r|journeys?))\b/mi
|
|
1704
|
+
.test(read(".how", "_platform", "design-system.md"))) {
|
|
1705
|
+
items.push("#15 cross-component experience in design-system.md (moves to .what/experience.md)");
|
|
1706
|
+
}
|
|
1707
|
+
// #16 and #18 follow `ux-landed`: nothing about a UX run is owed before a Product Component exists.
|
|
1708
|
+
const components = read(".control", "registry", "components.yaml");
|
|
1709
|
+
const hasPcs = /^product_components:[ \t]*\r?\n[ \t]+-/m.test(components);
|
|
1710
|
+
const LANDED_FROM = /^landed_from:.*(?:\r?\n[ \t]+-.*)*/m; // provenance, not a citation
|
|
1711
|
+
const UX_RUN = /_bmad-output\/ux\/[A-Za-z0-9_./-]*?(DESIGN|EXPERIENCE|design-system)\.md/;
|
|
1712
|
+
const citesUxRun = (dir, depth) => {
|
|
1713
|
+
if (depth > 8 || !fs.existsSync(dir)) return false;
|
|
1714
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1715
|
+
const full = path.join(dir, e.name);
|
|
1716
|
+
if (e.isDirectory()) { if (citesUxRun(full, depth + 1)) return true; continue; }
|
|
1717
|
+
if (/\.(md|ya?ml)$/.test(e.name) && UX_RUN.test(fs.readFileSync(full, "utf8").replace(LANDED_FROM, ""))) return true;
|
|
1718
|
+
}
|
|
1719
|
+
return false;
|
|
1720
|
+
};
|
|
1721
|
+
if (hasPcs && (citesUxRun(path.join(target, ".what"), 0) || citesUxRun(path.join(target, ".how"), 0))) {
|
|
1722
|
+
items.push("#16 the corpus cites a UX run in _bmad-output/ux/ (repoint at what landed; provenance to landed_from)");
|
|
1723
|
+
}
|
|
1724
|
+
// #17 — a container whose `repo:` sends its code elsewhere but still has a heading here, or a `repo:` on
|
|
1725
|
+
// a container nobody writes. The self-naming case needs the origin remote and is left to `container-built`.
|
|
1726
|
+
const ctrSection = (components.match(/^containers:[^\n]*\n((?:[ \t].*(?:\r?\n|$)|\r?\n)*)/m) || [, ""])[1];
|
|
1727
|
+
const headings = new Set([...(read(".control", "structure-codebase.md").split(/^## Containers\s*$/m)[1] || "")
|
|
1728
|
+
.split(/^## (?!#)/m)[0].matchAll(/^###\s+(.+?)\s*$/gm)].map((m) => m[1]));
|
|
1729
|
+
const repoDebt = ctrSection.split(/^[ \t]*-[ \t]+id:[ \t]*/m).slice(1).some((block) => {
|
|
1730
|
+
const id = block.split(/\r?\n/)[0].trim().replace(/^["']|["']$/g, "");
|
|
1731
|
+
const repo = /^[ \t]+repo:[ \t]*["']?([^"'\r\n]+)/m.exec(block)?.[1].trim();
|
|
1732
|
+
if (!repo) return false;
|
|
1733
|
+
return /^[ \t]+built:[ \t]*false\b/m.test(block) || headings.has(id);
|
|
1734
|
+
});
|
|
1735
|
+
if (repoDebt) {
|
|
1736
|
+
items.push("#17 a container's repo: disagrees with the code map (heading for code kept elsewhere, or repo: on built: false)");
|
|
1737
|
+
}
|
|
1738
|
+
// #18 — landed UX documents written before `landed_from` existed. Without it `ux-landed` cannot tell
|
|
1739
|
+
// which run a landing discharged, so every run in `_bmad-output/ux/` reads as still owed.
|
|
1740
|
+
const uxRoot = path.join(target, "_bmad-output", "ux");
|
|
1741
|
+
const hasRun = fs.existsSync(uxRoot) && fs.readdirSync(uxRoot, { recursive: true })
|
|
1742
|
+
.some((f) => /(^|[\\/])(DESIGN|EXPERIENCE)\.md$/.test(String(f)));
|
|
1743
|
+
const landedDocs = [];
|
|
1744
|
+
for (const [layer, slot, name] of [[".how", "01-ux", "DESIGN.md"], [".what", "04-usecases", "EXPERIENCE.md"]]) {
|
|
1745
|
+
const base = path.join(target, layer);
|
|
1746
|
+
if (!fs.existsSync(base)) continue;
|
|
1747
|
+
for (const pc of fs.readdirSync(base)) {
|
|
1748
|
+
const f = path.join(base, pc, slot, name);
|
|
1749
|
+
if (fs.existsSync(f)) landedDocs.push(f);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (hasPcs && hasRun && landedDocs.some((f) => !/^landed_from:/m.test(fs.readFileSync(f, "utf8")))) {
|
|
1753
|
+
items.push("#18 a landed UX document has no landed_from (name the run file(s) it came from)");
|
|
1754
|
+
}
|
|
1494
1755
|
return items;
|
|
1495
1756
|
}
|
|
1496
1757
|
|
|
@@ -1529,34 +1790,20 @@ function upsertAgentFiles(target, platforms, productName) {
|
|
|
1529
1790
|
if (!next.endsWith("\n")) next += "\n";
|
|
1530
1791
|
fs.writeFileSync(agentsFile, next);
|
|
1531
1792
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
mirrors.push(path.join(target, ".agents", "AGENTS.md"));
|
|
1538
|
-
}
|
|
1539
|
-
for (const mirror of mirrors) {
|
|
1793
|
+
// Every rule file a selected host reads in place of, or beside, AGENTS.md — CLAUDE.md, GEMINI.md,
|
|
1794
|
+
// .junie/guidelines.md, … — carries the same block. A host that never sees the block never sees
|
|
1795
|
+
// the method policy, and on a host with no manual-only gate that policy is the lock.
|
|
1796
|
+
for (const rel of ruleFiles(platforms).slice(1)) {
|
|
1797
|
+
const mirror = path.join(target, ...rel.split("/"));
|
|
1540
1798
|
fs.mkdirSync(path.dirname(mirror), { recursive: true });
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
fs.writeFileSync(mirror, patched.endsWith("\n") ? patched : `${patched}\n`);
|
|
1544
|
-
note(`method block refreshed in ${posixRel(target, mirror)}`);
|
|
1545
|
-
} else {
|
|
1799
|
+
const current = fs.existsSync(mirror) ? fs.readFileSync(mirror, "utf8") : "";
|
|
1800
|
+
if (!current.trim() || current.trim() === "@AGENTS.md") {
|
|
1546
1801
|
fs.writeFileSync(mirror, next);
|
|
1547
|
-
note(`created ${
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
if (platformUsesHook(platforms, "claude-md")) {
|
|
1552
|
-
const claude = path.join(target, "CLAUDE.md");
|
|
1553
|
-
if (!fs.existsSync(claude) || fs.readFileSync(claude, "utf8").trim() === "@AGENTS.md") {
|
|
1554
|
-
fs.writeFileSync(claude, next);
|
|
1555
|
-
note("CLAUDE.md synchronized from AGENTS.md");
|
|
1802
|
+
note(current.trim() ? `${rel} synchronized from AGENTS.md` : `created ${rel}`);
|
|
1556
1803
|
} else {
|
|
1557
|
-
const patched = upsertMethodBlock(
|
|
1558
|
-
fs.writeFileSync(
|
|
1559
|
-
note(
|
|
1804
|
+
const patched = upsertMethodBlock(current, template);
|
|
1805
|
+
fs.writeFileSync(mirror, patched.endsWith("\n") ? patched : `${patched}\n`);
|
|
1806
|
+
note(`method block refreshed in ${rel}`);
|
|
1560
1807
|
}
|
|
1561
1808
|
}
|
|
1562
1809
|
}
|
|
@@ -1612,12 +1859,17 @@ function printSummary(target, agents, { first, was, written, skipped, skills, to
|
|
|
1612
1859
|
`run the ${INIT_SKILL} skill, intent ${DIM}readers${RESET}, ` +
|
|
1613
1860
|
`to write it for this repo's stack`);
|
|
1614
1861
|
}
|
|
1615
|
-
const engReport = enginesReport(target);
|
|
1862
|
+
const engReport = enginesReport(target, agents);
|
|
1616
1863
|
summaryLine("engines", engReport.present
|
|
1617
|
-
? `${ENGINE_SKILLS.join(" · ")} — found (in this repo)`
|
|
1618
|
-
:
|
|
1864
|
+
? `${ENGINE_SKILLS.join(" · ")} — found (in this repo), readable by every selected host`
|
|
1865
|
+
: engReport.missing.length
|
|
1866
|
+
? `NOT found: ${engReport.missing.join(" · ")}. G5 (wdi-build) and the Fast Path need them; G1–G4 run without them`
|
|
1867
|
+
: `NOT readable by ${[...engReport.missingByHost.keys()].join(", ")}. G5 (wdi-build) and the Fast Path need them; G1–G4 run without them`);
|
|
1619
1868
|
if (!engReport.present) {
|
|
1620
|
-
|
|
1869
|
+
for (const [id, names] of engReport.missingByHost) {
|
|
1870
|
+
summaryLine("", `${DIM}·${RESET} ${id}: ${names.join(" · ")} — ${DIM}${ENGINES_INSTALL_ANY} --agent ${getPlatform(id).skillsCli}${RESET}`);
|
|
1871
|
+
}
|
|
1872
|
+
summaryLine("", `${DIM}·${RESET} into THIS repo — a user-level plugin does not count`);
|
|
1621
1873
|
summaryLine("", `${DIM}·${RESET} docs/agents/ is already seeded, so ${DIM}${ENGINES_SETUP}${RESET} is not needed · ${ENGINES_REPO}`);
|
|
1622
1874
|
} else {
|
|
1623
1875
|
const blocked = engineInvocationState(target).blocked;
|
|
@@ -1735,8 +1987,10 @@ function apply(target, agents,
|
|
|
1735
1987
|
// wrappers. A one-time fix would hold for about a week.
|
|
1736
1988
|
enableEngineInvocation(target);
|
|
1737
1989
|
retireBmadG5(target);
|
|
1738
|
-
writeDenyRules(target);
|
|
1739
|
-
|
|
1990
|
+
writeDenyRules(target, agents);
|
|
1991
|
+
writeOpencodePermissions(target, agents);
|
|
1992
|
+
noteRetiredPlatforms(target);
|
|
1993
|
+
writeStamp(target, agents);
|
|
1740
1994
|
printSummary(target, agents, { first, was, written, skipped, skills, tomls, opencodeCmds });
|
|
1741
1995
|
printNextSteps({
|
|
1742
1996
|
first,
|
|
@@ -1745,16 +1999,34 @@ function apply(target, agents,
|
|
|
1745
1999
|
});
|
|
1746
2000
|
}
|
|
1747
2001
|
|
|
1748
|
-
|
|
1749
|
-
|
|
2002
|
+
/** A host the repo still names that the method dropped. Said, never acted on: its folder holds
|
|
2003
|
+
* whatever the product put there too, and on update absence is a decision the owner makes. */
|
|
2004
|
+
function noteRetiredPlatforms(target) {
|
|
2005
|
+
for (const id of retiredPlatformsIn(target, fs)) {
|
|
2006
|
+
note(`${id} is no longer supported — ${RETIRED_PLATFORMS[id]}. Its wdi-* skill folders were left `
|
|
2007
|
+
+ "in place; remove them by hand when you are ready");
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function enginesCommand(target, { fix, copy }) {
|
|
2012
|
+
const agents = declaredPlatforms(target, fs);
|
|
2013
|
+
if (copy) {
|
|
2014
|
+
const n = copyEnginesToHosts(target, agents);
|
|
2015
|
+
if (n) enableEngineInvocation(target);
|
|
2016
|
+
ok(n ? `copied ${n} engine folder${n === 1 ? "" : "s"}` : "nothing to copy — every selected host already reads the engines, or none are in the repo");
|
|
2017
|
+
}
|
|
2018
|
+
const before = enginesReport(target, agents);
|
|
1750
2019
|
console.log("");
|
|
1751
|
-
console.log(` engines ${before.present ? "all present" : `MISSING ${before.missing.join(" · ")}`}`);
|
|
2020
|
+
console.log(` engines ${before.present ? "all present" : before.missing.length ? `MISSING ${before.missing.join(" · ")}` : "present, but not where every host reads"}`);
|
|
2021
|
+
for (const [id, names] of before.missingByHost) {
|
|
2022
|
+
console.log(` ${id.padEnd(17)}cannot read ${names.join(" · ")} ${DIM}${ENGINES_INSTALL_ANY} --agent ${getPlatform(id).skillsCli}${RESET}`);
|
|
2023
|
+
}
|
|
1752
2024
|
for (const name of ENGINE_SKILLS) {
|
|
1753
2025
|
const copies = before.files.get(name);
|
|
1754
2026
|
if (!copies) continue;
|
|
1755
2027
|
const flagged = [...copies.keys()].some((f) =>
|
|
1756
2028
|
/^disable-model-invocation\s*:\s*true/m.test(fs.readFileSync(f, "utf8")));
|
|
1757
|
-
const where = [...copies.values()].map((f) => posixRel(target, f)).join(", ");
|
|
2029
|
+
const where = [...copies.values()].flat().map((f) => posixRel(target, f)).join(", ");
|
|
1758
2030
|
console.log(` ${name.padEnd(17)}${flagged ? "flagged — no skill can invoke it" : "invocable"} ${DIM}${where}${RESET}`);
|
|
1759
2031
|
}
|
|
1760
2032
|
const banned = repoSkillFiles(target, BMAD_RETIRED_G5);
|
|
@@ -1782,12 +2054,13 @@ function enginesCommand(target, { fix }) {
|
|
|
1782
2054
|
repairAgentDocs(target);
|
|
1783
2055
|
enableEngineInvocation(target);
|
|
1784
2056
|
retireBmadG5(target);
|
|
1785
|
-
writeDenyRules(target);
|
|
1786
|
-
|
|
2057
|
+
writeDenyRules(target, agents);
|
|
2058
|
+
writeOpencodePermissions(target, agents);
|
|
2059
|
+
writeStamp(target, agents);
|
|
1787
2060
|
ok("engines aligned");
|
|
1788
2061
|
if (!before.present) {
|
|
1789
2062
|
console.log("");
|
|
1790
|
-
console.log(enginesMissingMessage(before
|
|
2063
|
+
console.log(enginesMissingMessage(before));
|
|
1791
2064
|
}
|
|
1792
2065
|
}
|
|
1793
2066
|
|
|
@@ -2024,7 +2297,7 @@ async function runWizard(pre) {
|
|
|
2024
2297
|
// both is told about BMad first rather than sent to install the second thing.
|
|
2025
2298
|
const engineGate = enginesReport(target);
|
|
2026
2299
|
if (!engineGate.present && !pre.skipEngines) {
|
|
2027
|
-
p.note(enginesMissingMessage(engineGate
|
|
2300
|
+
p.note(enginesMissingMessage(engineGate), "Engines next");
|
|
2028
2301
|
p.outro("Install them into this repo, then run this again: npx wdi-method");
|
|
2029
2302
|
process.exit(1);
|
|
2030
2303
|
}
|
|
@@ -2101,17 +2374,48 @@ async function runWizard(pre) {
|
|
|
2101
2374
|
const detected = pre.agents
|
|
2102
2375
|
? normalizePlatformIds(pre.agents)
|
|
2103
2376
|
: detectPlatforms(target, fs);
|
|
2104
|
-
|
|
2377
|
+
let selected = cancelIf(
|
|
2105
2378
|
await p.autocompleteMultiselect({
|
|
2106
2379
|
message: "Which tools get the wdi-* skills? (⭐ = recommended)",
|
|
2107
2380
|
options: platformSelectOptions(detected),
|
|
2108
2381
|
initialValues: detected,
|
|
2109
2382
|
required: true,
|
|
2110
2383
|
maxItems: 8,
|
|
2111
|
-
placeholder:
|
|
2384
|
+
placeholder: `Type to search all ${PLATFORMS.length} hosts — Space selects, Enter confirms`,
|
|
2112
2385
|
}),
|
|
2113
2386
|
);
|
|
2114
2387
|
|
|
2388
|
+
// Step 2 again, now that the hosts are known. Present somewhere is not enough: Kiro reads
|
|
2389
|
+
// `.kiro/skills` only, so engines in `.agents/skills` leave it unable to run G5.
|
|
2390
|
+
let hostGate = enginesReport(target, selected);
|
|
2391
|
+
if (!hostGate.present && !pre.skipEngines && !hostGate.missing.length) {
|
|
2392
|
+
// The engines are in the repo, only not where these hosts read. That is fixable right here.
|
|
2393
|
+
const blind = [...hostGate.missingByHost.keys()];
|
|
2394
|
+
const names = blind.map((id) => getPlatform(id).name).join(", ");
|
|
2395
|
+
const choice = cancelIf(await p.select({
|
|
2396
|
+
message: `The engines are in this repo, but ${names} cannot read them. What now?`,
|
|
2397
|
+
options: [
|
|
2398
|
+
{ value: "copy", label: `Copy them where ${names} read`, hint: "from this repo's own copy, no network" },
|
|
2399
|
+
{ value: "drop", label: `Leave ${names} out of this install`, hint: "if this repo does not use them" },
|
|
2400
|
+
{ value: "stop", label: "Stop — I will run npx skills add myself" },
|
|
2401
|
+
],
|
|
2402
|
+
initialValue: "copy",
|
|
2403
|
+
}));
|
|
2404
|
+
if (choice === "copy") copyEnginesToHosts(target, selected);
|
|
2405
|
+
if (choice === "drop") selected = selected.filter((id) => !blind.includes(id));
|
|
2406
|
+
if (choice === "stop" || !selected.length) {
|
|
2407
|
+
p.note(enginesMissingMessage(hostGate), "Engines for the hosts you picked");
|
|
2408
|
+
p.outro("Install them where each host reads, then run this again: npx wdi-method");
|
|
2409
|
+
process.exit(1);
|
|
2410
|
+
}
|
|
2411
|
+
hostGate = enginesReport(target, selected);
|
|
2412
|
+
}
|
|
2413
|
+
if (!hostGate.present && !pre.skipEngines) {
|
|
2414
|
+
p.note(enginesMissingMessage(hostGate), "Engines for the hosts you picked");
|
|
2415
|
+
p.outro("Install them where each host reads, then run this again: npx wdi-method");
|
|
2416
|
+
process.exit(1);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2115
2419
|
p.note(
|
|
2116
2420
|
[
|
|
2117
2421
|
"The corpus folder names are fixed — they are not an install option:",
|
|
@@ -2119,9 +2423,9 @@ async function runWizard(pre) {
|
|
|
2119
2423
|
"",
|
|
2120
2424
|
"What gets written for the platforms you picked:",
|
|
2121
2425
|
" AGENTS.md (the BEGIN:wdi-method block — always)",
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2426
|
+
...ruleFiles(selected).slice(1).map((f) => ` ${f} (method block mirror)`),
|
|
2427
|
+
platformsWithManualOnly(selected, "claude").length ? " .claude/settings.json (deny rules, merged)" : "",
|
|
2428
|
+
platformsWithManualOnly(selected, "opencode").length ? " opencode.json (permission.skill: ask, merged)" : "",
|
|
2125
2429
|
platformUsesHook(selected, "opencode-commands")
|
|
2126
2430
|
? ` ${opencodeCommandsDir()}/wdi-*.md (slash commands → skills)`
|
|
2127
2431
|
: "",
|
|
@@ -2134,12 +2438,12 @@ async function runWizard(pre) {
|
|
|
2134
2438
|
|
|
2135
2439
|
const okGo = cancelIf(await p.confirm({ message: first ? "Run the install?" : "Run the update?", initialValue: true }));
|
|
2136
2440
|
if (!okGo) {
|
|
2137
|
-
p.cancel("
|
|
2441
|
+
p.cancel("Cancelled.");
|
|
2138
2442
|
process.exit(0);
|
|
2139
2443
|
}
|
|
2140
2444
|
|
|
2141
2445
|
const spinner = p.spinner();
|
|
2142
|
-
spinner.start(first ? "
|
|
2446
|
+
spinner.start(first ? "Installing…" : "Updating…");
|
|
2143
2447
|
apply(target, selected, {
|
|
2144
2448
|
docLanguage,
|
|
2145
2449
|
docFilenameLanguage,
|
|
@@ -2148,7 +2452,7 @@ async function runWizard(pre) {
|
|
|
2148
2452
|
product: String(product).trim(),
|
|
2149
2453
|
client: String(client).trim(),
|
|
2150
2454
|
});
|
|
2151
|
-
spinner.stop(first ? "
|
|
2455
|
+
spinner.stop(first ? "Installed" : "Updated");
|
|
2152
2456
|
p.outro(first ? "Done. Take the after-install steps above." : "Done. Read the method-block diff in AGENTS.md.");
|
|
2153
2457
|
}
|
|
2154
2458
|
|
|
@@ -2162,8 +2466,9 @@ function runNonInteractive(args) {
|
|
|
2162
2466
|
if (!args.skipBmad && !bmadPresent(target)) {
|
|
2163
2467
|
die(bmadMissingMessage());
|
|
2164
2468
|
}
|
|
2165
|
-
if (
|
|
2166
|
-
|
|
2469
|
+
if (args.copyEngines) copyEnginesToHosts(target, agents);
|
|
2470
|
+
if (!args.skipEngines && !enginesPresent(target, agents)) {
|
|
2471
|
+
die(enginesMissingMessage(enginesReport(target, agents)));
|
|
2167
2472
|
}
|
|
2168
2473
|
const existing = readIndexIdentity(target);
|
|
2169
2474
|
const product = args.product || existing.name;
|
|
@@ -2181,7 +2486,7 @@ function runNonInteractive(args) {
|
|
|
2181
2486
|
|
|
2182
2487
|
async function main() {
|
|
2183
2488
|
const args = parseArgs(process.argv);
|
|
2184
|
-
if (!["wizard", "install", "update", "verify", "promote", "engines"].includes(args.cmd)) {
|
|
2489
|
+
if (!["wizard", "install", "update", "verify", "promote", "engines", "upgrade-check"].includes(args.cmd)) {
|
|
2185
2490
|
usage();
|
|
2186
2491
|
process.exit(2);
|
|
2187
2492
|
}
|
|
@@ -2205,7 +2510,11 @@ async function main() {
|
|
|
2205
2510
|
return;
|
|
2206
2511
|
}
|
|
2207
2512
|
if (args.cmd === "engines") {
|
|
2208
|
-
enginesCommand(requireTarget(args.dir), { fix: Boolean(args.fix) });
|
|
2513
|
+
enginesCommand(requireTarget(args.dir), { fix: Boolean(args.fix), copy: Boolean(args.copyEngines) });
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
if (args.cmd === "upgrade-check") {
|
|
2517
|
+
process.exitCode = upgradeCheck(requireTarget(args.dir));
|
|
2209
2518
|
return;
|
|
2210
2519
|
}
|
|
2211
2520
|
const wantTui = !args.yes && args.cmd !== "verify" && process.stdin.isTTY && process.stdout.isTTY;
|