wdi-method 0.6.31 → 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 +61 -0
- package/NOTICE +7 -2
- package/README.md +13 -11
- package/bin/wdi-method.js +275 -73
- package/kit/.constitution/method/document/bmad-skill-register.md +107 -104
- package/kit/.constitution/method/scripts/validate.py +68 -3
- package/kit/.constitution/method/why/README.md +1 -1
- package/kit/.constitution/method/why/portability.md +19 -2
- package/kit/skills/wdi-autopilot/SKILL.md +32 -19
- package/kit/skills/wdi-build/SKILL.md +28 -19
- 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-explain-to-me/SKILL.md +2 -0
- package/kit/skills/wdi-help/SKILL.md +4 -1
- package/kit/skills/wdi-init/SKILL.md +1 -1
- package/kit/skills/wdi-prune-or-archive/SKILL.md +2 -0
- package/kit-overlay/AGENTS.md +12 -1
- package/kit-overlay/portability.md +19 -2
- package/lib/platforms.mjs +420 -248
- package/package.json +1 -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,9 @@ 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
|
|
173
212
|
upgrade-check [dir] re-probe what wdi-upgrade still owes; rewrites upgrade_pending (exit 1 if any)
|
|
174
213
|
promote <live-dir> --rescue pull a method change back out of a consumer (not the normal flow)
|
|
175
214
|
|
|
@@ -182,6 +221,7 @@ function usage() {
|
|
|
182
221
|
--doc-filename-language <text> slug part of document filenames; free text, default English
|
|
183
222
|
--skip-bmad-check
|
|
184
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
|
|
185
225
|
|
|
186
226
|
BMad first, then this package. ${WDI_REPO}
|
|
187
227
|
`);
|
|
@@ -227,15 +267,21 @@ function parseArgs(argv) {
|
|
|
227
267
|
const t = rest.shift();
|
|
228
268
|
if (t === "--skip-bmad-check") args.skipBmad = true;
|
|
229
269
|
else if (t === "--fix") args.fix = true;
|
|
270
|
+
else if (t === "--copy" || t === "--copy-engines") args.copyEngines = true;
|
|
230
271
|
else if (t === "--skip-engines-check") args.skipEngines = true;
|
|
231
272
|
else if (t === "--rescue") args.rescue = true;
|
|
232
273
|
else if (t === "--yes" || t === "-y") args.yes = true;
|
|
233
274
|
else if (t === "--agents") {
|
|
234
275
|
const raw = rest.shift();
|
|
235
276
|
if (!raw) die("--agents needs a comma-separated list");
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
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));
|
|
239
285
|
if (unknown.length) die(`unknown platform: ${unknown.join(", ")} (run --list-agents)`);
|
|
240
286
|
if (!args.agents.length) die("--agents needs at least one known platform");
|
|
241
287
|
} else if (t === "--product") args.product = rest.shift();
|
|
@@ -362,30 +408,73 @@ function requireTarget(dir) {
|
|
|
362
408
|
*/
|
|
363
409
|
function repoSkillFiles(target, names) {
|
|
364
410
|
const out = new Map();
|
|
365
|
-
for (const
|
|
411
|
+
for (const root of skillRoots(target)) {
|
|
366
412
|
for (const name of names) {
|
|
367
|
-
const file = path.join(target,
|
|
413
|
+
const file = path.join(target, root, name, "SKILL.md");
|
|
368
414
|
if (!fs.existsSync(file)) continue;
|
|
369
415
|
let real = file;
|
|
370
416
|
try {
|
|
371
417
|
real = fs.realpathSync(file);
|
|
372
418
|
} catch {}
|
|
373
419
|
if (!out.has(name)) out.set(name, new Map());
|
|
374
|
-
out.get(name)
|
|
420
|
+
const seen = out.get(name);
|
|
421
|
+
if (!seen.has(real)) seen.set(real, []);
|
|
422
|
+
seen.get(real).push(file);
|
|
375
423
|
}
|
|
376
424
|
}
|
|
377
425
|
return out;
|
|
378
426
|
}
|
|
379
427
|
|
|
380
|
-
/** The six engines, in the REPO
|
|
381
|
-
|
|
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 = []) {
|
|
382
437
|
const files = repoSkillFiles(target, ENGINE_SKILLS);
|
|
383
438
|
const missing = ENGINE_SKILLS.filter((n) => !files.has(n));
|
|
384
|
-
|
|
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 };
|
|
385
449
|
}
|
|
386
450
|
|
|
387
|
-
function enginesPresent(target) {
|
|
388
|
-
return enginesReport(target).present;
|
|
451
|
+
function enginesPresent(target, agents = []) {
|
|
452
|
+
return enginesReport(target, agents).present;
|
|
453
|
+
}
|
|
454
|
+
|
|
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;
|
|
389
478
|
}
|
|
390
479
|
|
|
391
480
|
/** Only ever a WARNING. The plugin's copies are namespaced and still flagged, so they can neither be
|
|
@@ -483,7 +572,10 @@ function retireBmadG5(target) {
|
|
|
483
572
|
* Merged, never replaced: a product's own permissions are its own. Invalid JSON is reported rather
|
|
484
573
|
* than repaired — rewriting a settings file nobody can parse is how a repo loses its allowlist.
|
|
485
574
|
*/
|
|
486
|
-
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;
|
|
487
579
|
const file = path.join(target, ".claude", "settings.json");
|
|
488
580
|
let settings = {};
|
|
489
581
|
if (fs.existsSync(file)) {
|
|
@@ -510,6 +602,46 @@ function writeDenyRules(target) {
|
|
|
510
602
|
return added.length;
|
|
511
603
|
}
|
|
512
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
|
+
|
|
513
645
|
/** A trace of what the engines were when the method last looked — not a lockfile.
|
|
514
646
|
*
|
|
515
647
|
* `npx skills add` writes its own `skills-lock.json` with a folder hash per skill, and that hash
|
|
@@ -565,18 +697,37 @@ function bmadMissingMessage() {
|
|
|
565
697
|
//
|
|
566
698
|
// So it blocks, and `--skip-engines-check` is the escape, exactly as `--skip-bmad-check` is for BMad. The
|
|
567
699
|
// escape matters: CI installs into a bare checkout, and a repo that will never reach G5 is a real case.
|
|
568
|
-
function enginesMissingMessage(
|
|
569
|
-
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
|
+
}
|
|
570
722
|
return [
|
|
571
|
-
|
|
723
|
+
...head,
|
|
572
724
|
"",
|
|
573
725
|
"They MUST be installed INTO the repo, not as a user-level plugin — the method strips",
|
|
574
726
|
"`disable-model-invocation` from its own copies so `wdi-build` and `wdi-autopilot` can drive",
|
|
575
727
|
"them, and a plugin's files are not the repo's to edit.",
|
|
576
728
|
"",
|
|
577
|
-
`
|
|
578
|
-
"",
|
|
579
|
-
`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.",
|
|
580
731
|
"",
|
|
581
732
|
"You do NOT need to run the setup skill after this — the installer seeds docs/agents/ already",
|
|
582
733
|
`answered for this method. Run ${ENGINES_SETUP} only to change tracker.`,
|
|
@@ -1304,9 +1455,10 @@ function upgradeCheck(target) {
|
|
|
1304
1455
|
return 1;
|
|
1305
1456
|
}
|
|
1306
1457
|
|
|
1307
|
-
function writeStamp(target) {
|
|
1458
|
+
function writeStamp(target, agents) {
|
|
1308
1459
|
const control = path.join(target, ".control");
|
|
1309
1460
|
if (!fs.existsSync(control)) return;
|
|
1461
|
+
const hosts = normalizePlatformIds(agents && agents.length ? agents : declaredPlatforms(target, fs));
|
|
1310
1462
|
const eng = enginesReport(target);
|
|
1311
1463
|
const fp = engineFingerprints(target);
|
|
1312
1464
|
const names = Object.keys(fp);
|
|
@@ -1314,6 +1466,12 @@ function writeStamp(target) {
|
|
|
1314
1466
|
"# Written by wdi-method install/update. A trace, not a lockfile.",
|
|
1315
1467
|
`wdi_method: ${PKG.version}`,
|
|
1316
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),
|
|
1317
1475
|
];
|
|
1318
1476
|
if (names.length) {
|
|
1319
1477
|
const blocked = engineInvocationState(target).blocked;
|
|
@@ -1632,34 +1790,20 @@ function upsertAgentFiles(target, platforms, productName) {
|
|
|
1632
1790
|
if (!next.endsWith("\n")) next += "\n";
|
|
1633
1791
|
fs.writeFileSync(agentsFile, next);
|
|
1634
1792
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
mirrors.push(path.join(target, ".agents", "AGENTS.md"));
|
|
1641
|
-
}
|
|
1642
|
-
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("/"));
|
|
1643
1798
|
fs.mkdirSync(path.dirname(mirror), { recursive: true });
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
fs.writeFileSync(mirror, patched.endsWith("\n") ? patched : `${patched}\n`);
|
|
1647
|
-
note(`method block refreshed in ${posixRel(target, mirror)}`);
|
|
1648
|
-
} else {
|
|
1799
|
+
const current = fs.existsSync(mirror) ? fs.readFileSync(mirror, "utf8") : "";
|
|
1800
|
+
if (!current.trim() || current.trim() === "@AGENTS.md") {
|
|
1649
1801
|
fs.writeFileSync(mirror, next);
|
|
1650
|
-
note(`created ${
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
if (platformUsesHook(platforms, "claude-md")) {
|
|
1655
|
-
const claude = path.join(target, "CLAUDE.md");
|
|
1656
|
-
if (!fs.existsSync(claude) || fs.readFileSync(claude, "utf8").trim() === "@AGENTS.md") {
|
|
1657
|
-
fs.writeFileSync(claude, next);
|
|
1658
|
-
note("CLAUDE.md synchronized from AGENTS.md");
|
|
1802
|
+
note(current.trim() ? `${rel} synchronized from AGENTS.md` : `created ${rel}`);
|
|
1659
1803
|
} else {
|
|
1660
|
-
const patched = upsertMethodBlock(
|
|
1661
|
-
fs.writeFileSync(
|
|
1662
|
-
note(
|
|
1804
|
+
const patched = upsertMethodBlock(current, template);
|
|
1805
|
+
fs.writeFileSync(mirror, patched.endsWith("\n") ? patched : `${patched}\n`);
|
|
1806
|
+
note(`method block refreshed in ${rel}`);
|
|
1663
1807
|
}
|
|
1664
1808
|
}
|
|
1665
1809
|
}
|
|
@@ -1715,12 +1859,17 @@ function printSummary(target, agents, { first, was, written, skipped, skills, to
|
|
|
1715
1859
|
`run the ${INIT_SKILL} skill, intent ${DIM}readers${RESET}, ` +
|
|
1716
1860
|
`to write it for this repo's stack`);
|
|
1717
1861
|
}
|
|
1718
|
-
const engReport = enginesReport(target);
|
|
1862
|
+
const engReport = enginesReport(target, agents);
|
|
1719
1863
|
summaryLine("engines", engReport.present
|
|
1720
|
-
? `${ENGINE_SKILLS.join(" · ")} — found (in this repo)`
|
|
1721
|
-
:
|
|
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`);
|
|
1722
1868
|
if (!engReport.present) {
|
|
1723
|
-
|
|
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`);
|
|
1724
1873
|
summaryLine("", `${DIM}·${RESET} docs/agents/ is already seeded, so ${DIM}${ENGINES_SETUP}${RESET} is not needed · ${ENGINES_REPO}`);
|
|
1725
1874
|
} else {
|
|
1726
1875
|
const blocked = engineInvocationState(target).blocked;
|
|
@@ -1838,8 +1987,10 @@ function apply(target, agents,
|
|
|
1838
1987
|
// wrappers. A one-time fix would hold for about a week.
|
|
1839
1988
|
enableEngineInvocation(target);
|
|
1840
1989
|
retireBmadG5(target);
|
|
1841
|
-
writeDenyRules(target);
|
|
1842
|
-
|
|
1990
|
+
writeDenyRules(target, agents);
|
|
1991
|
+
writeOpencodePermissions(target, agents);
|
|
1992
|
+
noteRetiredPlatforms(target);
|
|
1993
|
+
writeStamp(target, agents);
|
|
1843
1994
|
printSummary(target, agents, { first, was, written, skipped, skills, tomls, opencodeCmds });
|
|
1844
1995
|
printNextSteps({
|
|
1845
1996
|
first,
|
|
@@ -1848,16 +1999,34 @@ function apply(target, agents,
|
|
|
1848
1999
|
});
|
|
1849
2000
|
}
|
|
1850
2001
|
|
|
1851
|
-
|
|
1852
|
-
|
|
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);
|
|
1853
2019
|
console.log("");
|
|
1854
|
-
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
|
+
}
|
|
1855
2024
|
for (const name of ENGINE_SKILLS) {
|
|
1856
2025
|
const copies = before.files.get(name);
|
|
1857
2026
|
if (!copies) continue;
|
|
1858
2027
|
const flagged = [...copies.keys()].some((f) =>
|
|
1859
2028
|
/^disable-model-invocation\s*:\s*true/m.test(fs.readFileSync(f, "utf8")));
|
|
1860
|
-
const where = [...copies.values()].map((f) => posixRel(target, f)).join(", ");
|
|
2029
|
+
const where = [...copies.values()].flat().map((f) => posixRel(target, f)).join(", ");
|
|
1861
2030
|
console.log(` ${name.padEnd(17)}${flagged ? "flagged — no skill can invoke it" : "invocable"} ${DIM}${where}${RESET}`);
|
|
1862
2031
|
}
|
|
1863
2032
|
const banned = repoSkillFiles(target, BMAD_RETIRED_G5);
|
|
@@ -1885,12 +2054,13 @@ function enginesCommand(target, { fix }) {
|
|
|
1885
2054
|
repairAgentDocs(target);
|
|
1886
2055
|
enableEngineInvocation(target);
|
|
1887
2056
|
retireBmadG5(target);
|
|
1888
|
-
writeDenyRules(target);
|
|
1889
|
-
|
|
2057
|
+
writeDenyRules(target, agents);
|
|
2058
|
+
writeOpencodePermissions(target, agents);
|
|
2059
|
+
writeStamp(target, agents);
|
|
1890
2060
|
ok("engines aligned");
|
|
1891
2061
|
if (!before.present) {
|
|
1892
2062
|
console.log("");
|
|
1893
|
-
console.log(enginesMissingMessage(before
|
|
2063
|
+
console.log(enginesMissingMessage(before));
|
|
1894
2064
|
}
|
|
1895
2065
|
}
|
|
1896
2066
|
|
|
@@ -2127,7 +2297,7 @@ async function runWizard(pre) {
|
|
|
2127
2297
|
// both is told about BMad first rather than sent to install the second thing.
|
|
2128
2298
|
const engineGate = enginesReport(target);
|
|
2129
2299
|
if (!engineGate.present && !pre.skipEngines) {
|
|
2130
|
-
p.note(enginesMissingMessage(engineGate
|
|
2300
|
+
p.note(enginesMissingMessage(engineGate), "Engines next");
|
|
2131
2301
|
p.outro("Install them into this repo, then run this again: npx wdi-method");
|
|
2132
2302
|
process.exit(1);
|
|
2133
2303
|
}
|
|
@@ -2204,17 +2374,48 @@ async function runWizard(pre) {
|
|
|
2204
2374
|
const detected = pre.agents
|
|
2205
2375
|
? normalizePlatformIds(pre.agents)
|
|
2206
2376
|
: detectPlatforms(target, fs);
|
|
2207
|
-
|
|
2377
|
+
let selected = cancelIf(
|
|
2208
2378
|
await p.autocompleteMultiselect({
|
|
2209
2379
|
message: "Which tools get the wdi-* skills? (⭐ = recommended)",
|
|
2210
2380
|
options: platformSelectOptions(detected),
|
|
2211
2381
|
initialValues: detected,
|
|
2212
2382
|
required: true,
|
|
2213
2383
|
maxItems: 8,
|
|
2214
|
-
placeholder:
|
|
2384
|
+
placeholder: `Type to search all ${PLATFORMS.length} hosts — Space selects, Enter confirms`,
|
|
2215
2385
|
}),
|
|
2216
2386
|
);
|
|
2217
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
|
+
|
|
2218
2419
|
p.note(
|
|
2219
2420
|
[
|
|
2220
2421
|
"The corpus folder names are fixed — they are not an install option:",
|
|
@@ -2222,9 +2423,9 @@ async function runWizard(pre) {
|
|
|
2222
2423
|
"",
|
|
2223
2424
|
"What gets written for the platforms you picked:",
|
|
2224
2425
|
" AGENTS.md (the BEGIN:wdi-method block — always)",
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
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)" : "",
|
|
2228
2429
|
platformUsesHook(selected, "opencode-commands")
|
|
2229
2430
|
? ` ${opencodeCommandsDir()}/wdi-*.md (slash commands → skills)`
|
|
2230
2431
|
: "",
|
|
@@ -2237,12 +2438,12 @@ async function runWizard(pre) {
|
|
|
2237
2438
|
|
|
2238
2439
|
const okGo = cancelIf(await p.confirm({ message: first ? "Run the install?" : "Run the update?", initialValue: true }));
|
|
2239
2440
|
if (!okGo) {
|
|
2240
|
-
p.cancel("
|
|
2441
|
+
p.cancel("Cancelled.");
|
|
2241
2442
|
process.exit(0);
|
|
2242
2443
|
}
|
|
2243
2444
|
|
|
2244
2445
|
const spinner = p.spinner();
|
|
2245
|
-
spinner.start(first ? "
|
|
2446
|
+
spinner.start(first ? "Installing…" : "Updating…");
|
|
2246
2447
|
apply(target, selected, {
|
|
2247
2448
|
docLanguage,
|
|
2248
2449
|
docFilenameLanguage,
|
|
@@ -2251,7 +2452,7 @@ async function runWizard(pre) {
|
|
|
2251
2452
|
product: String(product).trim(),
|
|
2252
2453
|
client: String(client).trim(),
|
|
2253
2454
|
});
|
|
2254
|
-
spinner.stop(first ? "
|
|
2455
|
+
spinner.stop(first ? "Installed" : "Updated");
|
|
2255
2456
|
p.outro(first ? "Done. Take the after-install steps above." : "Done. Read the method-block diff in AGENTS.md.");
|
|
2256
2457
|
}
|
|
2257
2458
|
|
|
@@ -2265,8 +2466,9 @@ function runNonInteractive(args) {
|
|
|
2265
2466
|
if (!args.skipBmad && !bmadPresent(target)) {
|
|
2266
2467
|
die(bmadMissingMessage());
|
|
2267
2468
|
}
|
|
2268
|
-
if (
|
|
2269
|
-
|
|
2469
|
+
if (args.copyEngines) copyEnginesToHosts(target, agents);
|
|
2470
|
+
if (!args.skipEngines && !enginesPresent(target, agents)) {
|
|
2471
|
+
die(enginesMissingMessage(enginesReport(target, agents)));
|
|
2270
2472
|
}
|
|
2271
2473
|
const existing = readIndexIdentity(target);
|
|
2272
2474
|
const product = args.product || existing.name;
|
|
@@ -2308,7 +2510,7 @@ async function main() {
|
|
|
2308
2510
|
return;
|
|
2309
2511
|
}
|
|
2310
2512
|
if (args.cmd === "engines") {
|
|
2311
|
-
enginesCommand(requireTarget(args.dir), { fix: Boolean(args.fix) });
|
|
2513
|
+
enginesCommand(requireTarget(args.dir), { fix: Boolean(args.fix), copy: Boolean(args.copyEngines) });
|
|
2312
2514
|
return;
|
|
2313
2515
|
}
|
|
2314
2516
|
if (args.cmd === "upgrade-check") {
|