wdi-method 0.6.6 → 0.6.8
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 +33 -5
- package/bin/wdi-method.js +389 -26
- package/kit/.constitution/method/constitution.md +4 -1
- package/kit/.constitution/method/document/bmad-guide.md +10 -6
- package/kit/.constitution/method/document/bmad-skill-register.md +36 -6
- package/kit/.constitution/method/scripts/validate.py +162 -18
- package/kit/.constitution/method/why/README.md +192 -186
- package/kit/.constitution/method/why/artifact-map.md +1 -1
- package/kit/skills/wdi-autopilot/SKILL.md +383 -375
- package/kit/skills/wdi-blueprint/SKILL.md +3 -3
- package/kit/skills/wdi-build/SKILL.md +393 -371
- package/kit/skills/wdi-help/SKILL.md +111 -110
- package/kit/skills/wdi-init/SKILL.md +19 -1
- package/kit/skills/wdi-upgrade/SKILL.md +194 -187
- package/kit-overlay/constitution.md +4 -1
- package/package.json +1 -1
- package/scaffold/docs/agents/issue-tracker.md +56 -43
package/README.md
CHANGED
|
@@ -42,16 +42,26 @@ cd /path/to/your/product-repo
|
|
|
42
42
|
npx bmad-method install
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
**2. mattpocock/skills** —
|
|
45
|
+
**2. mattpocock/skills** — **into this repo**, on every agent:
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
# Claude Code — a managed bundle that updates when its author ships
|
|
49
|
-
claude plugins install mattpocock-skills # or, inside a session: /plugin install mattpocock-skills
|
|
50
|
-
|
|
51
|
-
# Codex, Cursor, OpenCode, other agents — copies the skills into this repo; keep setup-matt-pocock-skills selected
|
|
52
48
|
npx skills@latest add mattpocock/skills
|
|
53
49
|
```
|
|
54
50
|
|
|
51
|
+
Take all six the method drives: `to-spec`, `to-tickets`, `implement`, `tdd`, `code-review`, and
|
|
52
|
+
`domain-modeling`. Either install mode works — "copy" or "symlink".
|
|
53
|
+
|
|
54
|
+
**The Claude Code plugin is not an alternative here, and the reason is mechanical.** `to-spec`,
|
|
55
|
+
`to-tickets` and `implement` ship with `disable-model-invocation: true`, so no skill can invoke them;
|
|
56
|
+
nothing outside the file lifts that flag, and a plugin's files are not this repo's to edit. WDI Method
|
|
57
|
+
strips it from the copies the repo owns — which is what lets `wdi-build` invoke an engine and
|
|
58
|
+
`wdi-autopilot` run an iteration with nobody watching — and re-applies that on every update, because
|
|
59
|
+
`npx skills update` restores the author's file. The installer refuses without the six, and
|
|
60
|
+
`--skip-engines-check` is the escape for CI and for a repo that will never reach G5.
|
|
61
|
+
|
|
62
|
+
If you also have the plugin installed for your user, the repo's copies are what run; removing the
|
|
63
|
+
plugin keeps `/to-spec` unambiguous.
|
|
64
|
+
|
|
55
65
|
**You do not need to run `/setup-matt-pocock-skills` to get started.** Step 3 seeds `docs/agents/` with
|
|
56
66
|
the two answers WDI Method actually has a requirement on, so the engines are aligned from the first
|
|
57
67
|
install. Run the setup skill only to *change* something — to point at GitHub or Jira instead of local
|
|
@@ -450,6 +460,24 @@ English, whatever the settings say — it travels to every repo through this pac
|
|
|
450
460
|
|
|
451
461
|
It prints the version it replaced, what it wrote, what it kept, and what to do next.
|
|
452
462
|
|
|
463
|
+
### Moving a repo from 0.6.7 or earlier to 0.6.8
|
|
464
|
+
|
|
465
|
+
Four things change for a repo already running the method. The first is the only one that can stop an
|
|
466
|
+
update, and all four are mechanical.
|
|
467
|
+
|
|
468
|
+
| What changed | What it means for your repo |
|
|
469
|
+
|---|---|
|
|
470
|
+
| **The engines must be in the repo** | `install` and `update` refuse until `to-spec`, `to-tickets`, `implement`, `tdd`, `code-review` and `domain-modeling` are here — `npx skills@latest add mattpocock/skills`. The Claude Code plugin no longer counts: three of the six ship locked against skill invocation, nothing outside the file unlocks them, and a plugin's files are not yours to edit. `--skip-engines-check` still installs without them |
|
|
471
|
+
| **The engines are invoked, not handed to you** | `wdi-build` calls `to-spec`, `to-tickets`, `implement`, `tdd` and `code-review` itself, so `wdi-autopilot` can finish a spec with nobody watching. Every `update` re-unlocks the repo's copies, because `npx skills update` puts the author's lock back — and `engines-invocable` in `validate.py` goes red when it has |
|
|
472
|
+
| **Thirteen BMad skills are retired at G5, and now enforced** | `bmad-spec`, `bmad-build`, `bmad-build-auto`, `bmad-code-review`, `bmad-retrospective`, `bmad-agent-dev`, `bmad-create-epics-and-stories`, `bmad-create-story`, `bmad-dev-story`, `bmad-dev-auto`, `bmad-quick-dev`, `bmad-sprint-planning`, `bmad-sprint-status`. Each is locked out of model invocation and denied in `.claude/settings.json`; typing the slash command yourself still works. `bmad-skill-register.md` carries the list and the criterion — retired only where this method has a named replacement, which is why `bmad-qa-generate-e2e-tests` and `bmad-checkpoint-preview` are not on it |
|
|
473
|
+
| **A spec has one predefined home** | `.scratch/<spec-id>-<slug>/`, with `SPEC.md` and `issues/<NN>-<slug>.md` inside it. Left free, that folder name gets written a different way in every repo and traces back to nothing. A row in `specs.yaml` is now what makes an effort a spec rather than ad hoc work — the path no longer says |
|
|
474
|
+
|
|
475
|
+
Run the `wdi-upgrade` skill after updating: it names what is still in the old shape, including a
|
|
476
|
+
`docs/agents/issue-tracker.md` that still carries `/setup-matt-pocock-skills`' own answer, and the spec
|
|
477
|
+
folders that need moving. `npx wdi-method engines` reports the engine state on its own, and
|
|
478
|
+
`npx wdi-method engines --fix` repairs what can be repaired without touching anything you wrote — the
|
|
479
|
+
previous config is kept as `.bak`.
|
|
480
|
+
|
|
453
481
|
---
|
|
454
482
|
|
|
455
483
|
## Changing the method
|
package/bin/wdi-method.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import * as p from "@clack/prompts";
|
|
8
9
|
import {
|
|
@@ -66,14 +67,65 @@ const GENERIC_FOLDER_PATTERNS = new Set([
|
|
|
66
67
|
]);
|
|
67
68
|
|
|
68
69
|
const BMAD_INSTALL = `npx bmad-method install`;
|
|
69
|
-
// The
|
|
70
|
-
//
|
|
71
|
-
//
|
|
70
|
+
// The engines G5 runs. BMad writes the documents; these cut the work.
|
|
71
|
+
//
|
|
72
|
+
// They are installed IN THE REPO, and a user-level plugin no longer counts. Three reasons, and the
|
|
73
|
+
// third is what forced it: a method whose G5 depends on what the operator happened to install on
|
|
74
|
+
// their laptop behaves differently per machine; `.control/wdi-method.yaml` cannot record a version
|
|
75
|
+
// it does not own; and `to-spec`, `to-tickets` and `implement` ship with
|
|
76
|
+
// `disable-model-invocation: true`, which nothing outside the file can lift — the gate reads the
|
|
77
|
+
// frontmatter and consults no setting, and `skillOverrides` only ever tightens. Owning the file is
|
|
78
|
+
// the only route to an engine a skill can invoke, so owning the file is now the requirement.
|
|
79
|
+
// Upstream ships that route deliberately: the plugin is "subscribe rather than fork", `skills.sh`
|
|
80
|
+
// "copies editable skill files into your project, so you can hack on them and make them your own".
|
|
72
81
|
const ENGINES_REPO = "https://github.com/mattpocock/skills";
|
|
73
82
|
const ENGINES_PLUGIN = "mattpocock-skills";
|
|
74
83
|
const ENGINES_INSTALL = `/plugin install ${ENGINES_PLUGIN}`;
|
|
75
84
|
const ENGINES_INSTALL_ANY = "npx skills@latest add mattpocock/skills";
|
|
76
85
|
const ENGINES_SETUP = "/setup-matt-pocock-skills";
|
|
86
|
+
// Six, not five. `domain-modeling` is G3's — `wdi-blueprint` invokes it — and it used to be reached
|
|
87
|
+
// by its plugin-namespaced name. With the plugin no longer required that name resolves to nothing,
|
|
88
|
+
// so the skill joins the local install and every reference to it dropped the prefix.
|
|
89
|
+
const ENGINE_SKILLS = ["to-spec", "to-tickets", "implement", "tdd", "code-review", "domain-modeling"];
|
|
90
|
+
// The three that arrive flagged. `tdd`, `code-review` and `domain-modeling` never carried the flag
|
|
91
|
+
// and MUST NOT gain one.
|
|
92
|
+
const ENGINE_FLAGGED = ["to-spec", "to-tickets", "implement"];
|
|
93
|
+
const ENGINE_LOCK = "skills-lock.json";
|
|
94
|
+
const GUARD_MARK = "Driven by `wdi-build` and `wdi-autopilot`";
|
|
95
|
+
const GUARD_LINE = `> **${GUARD_MARK}.** \`wdi-method\` unlocked model invocation for this `
|
|
96
|
+
+ "engine in this repo so those two can drive it unattended. Invoked from anywhere else — a stray "
|
|
97
|
+
+ "session, a subagent that thought this looked relevant — stop and say so: this engine publishes "
|
|
98
|
+
+ "to the tracker and writes code.";
|
|
99
|
+
|
|
100
|
+
// Every folder a platform reads skills from. One list, because a repo installs the engines wherever
|
|
101
|
+
// `npx skills add` was pointed, and that installer offers symlinks across several of them.
|
|
102
|
+
const SKILL_HOMES = [".claude", ".agents", ".agent", ".cursor", ".codex"];
|
|
103
|
+
|
|
104
|
+
// BMad skills RETIRED at G5. This array is the single home of that list: `bmad-skill-register.md`
|
|
105
|
+
// carries the same names for a reader, and a test fails when the two disagree.
|
|
106
|
+
//
|
|
107
|
+
// The criterion, and it is why the list is this long and not longer: a BMad skill is retired only
|
|
108
|
+
// where this method has a NAMED replacement for what it produces. `bmad-build` and `bmad-agent-dev`
|
|
109
|
+
// produce code that `implement` produces; `bmad-spec` a contract that `to-spec` produces;
|
|
110
|
+
// `bmad-create-epics-and-stories` an `epics` level this method REPEALED in code, not merely in
|
|
111
|
+
// prose. `bmad-qa-generate-e2e-tests` and `bmad-checkpoint-preview` have no replacement here, so
|
|
112
|
+
// they are NOT retired — banning a capability with nothing in its place is how a method gets
|
|
113
|
+
// worked around instead of followed.
|
|
114
|
+
const BMAD_RETIRED_G5 = [
|
|
115
|
+
"bmad-spec",
|
|
116
|
+
"bmad-build",
|
|
117
|
+
"bmad-build-auto",
|
|
118
|
+
"bmad-code-review",
|
|
119
|
+
"bmad-retrospective",
|
|
120
|
+
"bmad-agent-dev",
|
|
121
|
+
"bmad-create-epics-and-stories",
|
|
122
|
+
"bmad-create-story",
|
|
123
|
+
"bmad-dev-story",
|
|
124
|
+
"bmad-dev-auto",
|
|
125
|
+
"bmad-quick-dev",
|
|
126
|
+
"bmad-sprint-planning",
|
|
127
|
+
"bmad-sprint-status",
|
|
128
|
+
];
|
|
77
129
|
const REPO_URL = "https://github.com/wiradigitalid/wdi-method";
|
|
78
130
|
const HELP_SKILL = "wdi-help";
|
|
79
131
|
const INIT_SKILL = "wdi-init";
|
|
@@ -113,6 +165,7 @@ function usage() {
|
|
|
113
165
|
install [dir] first install (TUI unless --yes)
|
|
114
166
|
update [dir] update (TUI unless --yes)
|
|
115
167
|
verify [dir]
|
|
168
|
+
engines [dir] [--fix] report the six engines, their invocation state, and the BMad G5 ban
|
|
116
169
|
promote <live-dir> --rescue pull a method change back out of a consumer (not the normal flow)
|
|
117
170
|
|
|
118
171
|
--yes non-interactive
|
|
@@ -136,6 +189,7 @@ function parseArgs(argv) {
|
|
|
136
189
|
agents: null,
|
|
137
190
|
skipBmad: false,
|
|
138
191
|
rescue: false,
|
|
192
|
+
fix: false,
|
|
139
193
|
yes: false,
|
|
140
194
|
product: null,
|
|
141
195
|
client: null,
|
|
@@ -156,7 +210,7 @@ function parseArgs(argv) {
|
|
|
156
210
|
return args;
|
|
157
211
|
}
|
|
158
212
|
const first = rest[0];
|
|
159
|
-
if (["install", "update", "verify", "promote"].includes(first)) {
|
|
213
|
+
if (["install", "update", "verify", "promote", "engines"].includes(first)) {
|
|
160
214
|
args.cmd = rest.shift();
|
|
161
215
|
} else if (first.startsWith("-")) {
|
|
162
216
|
args.cmd = "wizard";
|
|
@@ -167,6 +221,7 @@ function parseArgs(argv) {
|
|
|
167
221
|
while (rest.length) {
|
|
168
222
|
const t = rest.shift();
|
|
169
223
|
if (t === "--skip-bmad-check") args.skipBmad = true;
|
|
224
|
+
else if (t === "--fix") args.fix = true;
|
|
170
225
|
else if (t === "--skip-engines-check") args.skipEngines = true;
|
|
171
226
|
else if (t === "--rescue") args.rescue = true;
|
|
172
227
|
else if (t === "--yes" || t === "-y") args.yes = true;
|
|
@@ -293,11 +348,45 @@ function requireTarget(dir) {
|
|
|
293
348
|
return target;
|
|
294
349
|
}
|
|
295
350
|
|
|
296
|
-
/**
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
351
|
+
/** Skill files in the repo, keyed by name, de-duplicated by the file each one REALLY is.
|
|
352
|
+
*
|
|
353
|
+
* The de-duplication is the point. `npx skills add` offers "symlink — single source of truth" when
|
|
354
|
+
* more than one agent is selected, so one SKILL.md is reachable through `.claude/skills/` and
|
|
355
|
+
* `.agents/skills/` at once. Walking directories would patch it twice — and where the link points
|
|
356
|
+
* into `node_modules`, patching it at all would edit a dependency.
|
|
357
|
+
*/
|
|
358
|
+
function repoSkillFiles(target, names) {
|
|
359
|
+
const out = new Map();
|
|
360
|
+
for (const home of SKILL_HOMES) {
|
|
361
|
+
for (const name of names) {
|
|
362
|
+
const file = path.join(target, home, "skills", name, "SKILL.md");
|
|
363
|
+
if (!fs.existsSync(file)) continue;
|
|
364
|
+
let real = file;
|
|
365
|
+
try {
|
|
366
|
+
real = fs.realpathSync(file);
|
|
367
|
+
} catch {}
|
|
368
|
+
if (!out.has(name)) out.set(name, new Map());
|
|
369
|
+
out.get(name).set(real, file);
|
|
370
|
+
}
|
|
300
371
|
}
|
|
372
|
+
return out;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** The six engines, in the REPO. A user-level plugin is not an answer here — see ENGINE_SKILLS. */
|
|
376
|
+
function enginesReport(target) {
|
|
377
|
+
const files = repoSkillFiles(target, ENGINE_SKILLS);
|
|
378
|
+
const missing = ENGINE_SKILLS.filter((n) => !files.has(n));
|
|
379
|
+
return { files, missing, present: missing.length === 0 };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function enginesPresent(target) {
|
|
383
|
+
return enginesReport(target).present;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Only ever a WARNING. The plugin's copies are namespaced and still flagged, so they can neither be
|
|
387
|
+
* invoked nor shadow the repo's — but `/to-spec` in the UI becomes ambiguous, and `npx skills
|
|
388
|
+
* update` run against a plugin-shaped install is one way the flag comes back. */
|
|
389
|
+
function pluginEnginesRegistered() {
|
|
301
390
|
const cfg = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
302
391
|
const registry = path.join(cfg, "plugins", "installed_plugins.json");
|
|
303
392
|
if (!fs.existsSync(registry)) return false;
|
|
@@ -309,6 +398,149 @@ function enginesPresent(target) {
|
|
|
309
398
|
}
|
|
310
399
|
}
|
|
311
400
|
|
|
401
|
+
/** Strip the author's flag, and write one guard line where it stood.
|
|
402
|
+
*
|
|
403
|
+
* The flag was the only thing stopping a stray session from publishing tickets. Removing it without
|
|
404
|
+
* naming who may drive the engine trades a hard gate for nothing, so the two arrive together.
|
|
405
|
+
* Idempotent by construction: no flag and a guard already present means the file is returned as-is,
|
|
406
|
+
* which is what keeps a second `update` from stacking a second line.
|
|
407
|
+
*/
|
|
408
|
+
function withInvocationEnabled(text) {
|
|
409
|
+
let out = text;
|
|
410
|
+
if (/^disable-model-invocation\s*:.*$/m.test(out)) {
|
|
411
|
+
out = out.replace(/^disable-model-invocation\s*:.*\r?\n/m, "");
|
|
412
|
+
}
|
|
413
|
+
if (!out.includes(GUARD_MARK)) {
|
|
414
|
+
out = out.replace(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)/, `$1\n${GUARD_LINE}\n`);
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function enableEngineInvocation(target) {
|
|
420
|
+
const { files } = enginesReport(target);
|
|
421
|
+
const patched = new Set();
|
|
422
|
+
for (const name of ENGINE_FLAGGED) {
|
|
423
|
+
const copies = files.get(name);
|
|
424
|
+
if (!copies) continue;
|
|
425
|
+
for (const real of copies.keys()) {
|
|
426
|
+
const before = fs.readFileSync(real, "utf8");
|
|
427
|
+
const after = withInvocationEnabled(before);
|
|
428
|
+
if (after === before) continue;
|
|
429
|
+
fs.writeFileSync(real, after, "utf8");
|
|
430
|
+
patched.add(name);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (patched.size) {
|
|
434
|
+
note(`engines invocable: ${[...patched].join(" · ")} — author's flag removed, guard line written`);
|
|
435
|
+
}
|
|
436
|
+
return [...patched];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** The mirror image, pointed at BMad's G5 wrappers: the flag ADDED rather than removed.
|
|
440
|
+
*
|
|
441
|
+
* A rule in a document lost this argument for three releases. `bmad-build` sits in the repo's own
|
|
442
|
+
* skill folder claiming it "implements any user intent, requirement, story, bug fix or change
|
|
443
|
+
* request", model-invocable, while the sanctioned engines sat in a plugin the model could not call.
|
|
444
|
+
* The harness rewarded the forbidden path. This is what stops rewarding it — and it leaves the
|
|
445
|
+
* human route open, because the gate only refuses the Skill tool: `/bmad-build` typed by a person
|
|
446
|
+
* still runs.
|
|
447
|
+
*/
|
|
448
|
+
function withModelInvocationDisabled(text) {
|
|
449
|
+
if (/^disable-model-invocation\s*:\s*true/m.test(text)) return text;
|
|
450
|
+
if (!/^---\r?\n/.test(text)) return text; // no frontmatter of its own: not ours to invent one
|
|
451
|
+
if (/^disable-model-invocation\s*:/m.test(text)) {
|
|
452
|
+
return text.replace(/^disable-model-invocation\s*:.*$/m, "disable-model-invocation: true");
|
|
453
|
+
}
|
|
454
|
+
return text.replace(/^---\r?\n/, "---\ndisable-model-invocation: true\n");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function retireBmadG5(target) {
|
|
458
|
+
const files = repoSkillFiles(target, BMAD_RETIRED_G5);
|
|
459
|
+
const patched = new Set();
|
|
460
|
+
for (const [name, copies] of files) {
|
|
461
|
+
for (const real of copies.keys()) {
|
|
462
|
+
const before = fs.readFileSync(real, "utf8");
|
|
463
|
+
const after = withModelInvocationDisabled(before);
|
|
464
|
+
if (after === before) continue;
|
|
465
|
+
fs.writeFileSync(real, after, "utf8");
|
|
466
|
+
patched.add(name);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (patched.size) {
|
|
470
|
+
note(`retired at G5: ${patched.size} BMad skill${patched.size === 1 ? "" : "s"} can no longer be `
|
|
471
|
+
+ `model-invoked (a person typing the slash command still can)`);
|
|
472
|
+
}
|
|
473
|
+
return [...patched];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Second layer, and the only one that survives BMad reinstalling its own wrappers mid-week.
|
|
477
|
+
*
|
|
478
|
+
* Merged, never replaced: a product's own permissions are its own. Invalid JSON is reported rather
|
|
479
|
+
* than repaired — rewriting a settings file nobody can parse is how a repo loses its allowlist.
|
|
480
|
+
*/
|
|
481
|
+
function writeDenyRules(target) {
|
|
482
|
+
const file = path.join(target, ".claude", "settings.json");
|
|
483
|
+
let settings = {};
|
|
484
|
+
if (fs.existsSync(file)) {
|
|
485
|
+
try {
|
|
486
|
+
settings = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
487
|
+
} catch {
|
|
488
|
+
note(".claude/settings.json is not valid JSON — deny rules NOT written; fix it and re-run");
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) return 0;
|
|
492
|
+
}
|
|
493
|
+
const perms = settings.permissions && typeof settings.permissions === "object"
|
|
494
|
+
&& !Array.isArray(settings.permissions) ? settings.permissions : {};
|
|
495
|
+
const deny = Array.isArray(perms.deny) ? perms.deny : [];
|
|
496
|
+
const want = BMAD_RETIRED_G5.map((n) => `Skill(${n})`);
|
|
497
|
+
const added = want.filter((rule) => !deny.includes(rule));
|
|
498
|
+
if (!added.length) return 0;
|
|
499
|
+
perms.deny = [...deny, ...added];
|
|
500
|
+
settings.permissions = perms;
|
|
501
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
502
|
+
fs.writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
503
|
+
note(`deny rules for ${added.length} retired BMad skill${added.length === 1 ? "" : "s"} `
|
|
504
|
+
+ `→ .claude/settings.json`);
|
|
505
|
+
return added.length;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** A trace of what the engines were when the method last looked — not a lockfile.
|
|
509
|
+
*
|
|
510
|
+
* `npx skills add` writes its own `skills-lock.json` with a folder hash per skill, and that hash
|
|
511
|
+
* stops matching the moment the flag is stripped. So the register records the hash of the file the
|
|
512
|
+
* method actually reads, AFTER the patch. It is informational: `engines-invocable` decides by
|
|
513
|
+
* looking for the flag, not by comparing hashes, because a legitimate content change MUST NOT read
|
|
514
|
+
* as a defect.
|
|
515
|
+
*/
|
|
516
|
+
function engineFingerprints(target) {
|
|
517
|
+
const { files } = enginesReport(target);
|
|
518
|
+
const out = {};
|
|
519
|
+
for (const name of ENGINE_SKILLS) {
|
|
520
|
+
const copies = files.get(name);
|
|
521
|
+
if (!copies) continue;
|
|
522
|
+
const [real] = [...copies.keys()].sort();
|
|
523
|
+
out[name] = createHash("sha256").update(fs.readFileSync(real)).digest("hex").slice(0, 12);
|
|
524
|
+
}
|
|
525
|
+
return out;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function engineInvocationState(target) {
|
|
529
|
+
const { files } = enginesReport(target);
|
|
530
|
+
const blocked = [];
|
|
531
|
+
for (const name of ENGINE_FLAGGED) {
|
|
532
|
+
const copies = files.get(name);
|
|
533
|
+
if (!copies) continue;
|
|
534
|
+
for (const real of copies.keys()) {
|
|
535
|
+
if (/^disable-model-invocation\s*:\s*true/m.test(fs.readFileSync(real, "utf8"))) {
|
|
536
|
+
blocked.push(name);
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return { blocked };
|
|
542
|
+
}
|
|
543
|
+
|
|
312
544
|
function bmadMissingMessage() {
|
|
313
545
|
return [
|
|
314
546
|
"BMad Method is not installed in this repo. Install it first, then run this installer again.",
|
|
@@ -328,12 +560,18 @@ function bmadMissingMessage() {
|
|
|
328
560
|
//
|
|
329
561
|
// So it blocks, and `--skip-engines-check` is the escape, exactly as `--skip-bmad-check` is for BMad. The
|
|
330
562
|
// escape matters: CI installs into a bare checkout, and a repo that will never reach G5 is a real case.
|
|
331
|
-
function enginesMissingMessage() {
|
|
563
|
+
function enginesMissingMessage(missing) {
|
|
564
|
+
const names = (missing && missing.length ? missing : ENGINE_SKILLS).join(" · ");
|
|
332
565
|
return [
|
|
333
|
-
|
|
566
|
+
`The engines are not in this repo. Missing: ${names}`,
|
|
567
|
+
"",
|
|
568
|
+
"They MUST be installed INTO the repo, not as a user-level plugin — the method strips",
|
|
569
|
+
"`disable-model-invocation` from its own copies so `wdi-build` and `wdi-autopilot` can drive",
|
|
570
|
+
"them, and a plugin's files are not the repo's to edit.",
|
|
571
|
+
"",
|
|
572
|
+
` ${ENGINES_INSTALL_ANY}`,
|
|
334
573
|
"",
|
|
335
|
-
`
|
|
336
|
-
` Other agents: ${ENGINES_INSTALL_ANY}`,
|
|
574
|
+
`Take all six: ${ENGINE_SKILLS.join(" · ")}. Choose "copy" or "symlink" — either is read.`,
|
|
337
575
|
"",
|
|
338
576
|
"You do NOT need to run the setup skill after this — the installer seeds docs/agents/ already",
|
|
339
577
|
`answered for this method. Run ${ENGINES_SETUP} only to change tracker.`,
|
|
@@ -343,6 +581,39 @@ function enginesMissingMessage() {
|
|
|
343
581
|
].join("\n");
|
|
344
582
|
}
|
|
345
583
|
|
|
584
|
+
/** `docs/agents/` is the engines' config, and its PATH is the author's: `to-spec`, `to-tickets`,
|
|
585
|
+
* `implement` and `triage` read `docs/agents/issue-tracker.md` and `docs/agents/domain.md` and
|
|
586
|
+
* nowhere else. What the files SAY is this method's, and that is the half that kept going wrong:
|
|
587
|
+
* a repo that ran `/setup-matt-pocock-skills` carries upstream's answer, which sends every engine to
|
|
588
|
+
* `.scratch/` with no registry behind it and never mentions `specs.yaml`. Two of four live repos
|
|
589
|
+
* still had it.
|
|
590
|
+
*
|
|
591
|
+
* The installer does not touch a product-owned file, and that rule stays. This is the repair, run
|
|
592
|
+
* from `wdi-method engines --fix` — by `wdi-init` or `wdi-upgrade`, knowingly — and it keeps the
|
|
593
|
+
* previous text beside it as `.bak` rather than deleting an answer somebody may have meant.
|
|
594
|
+
*/
|
|
595
|
+
function repairAgentDocs(target) {
|
|
596
|
+
const dir = path.join(target, "docs", "agents");
|
|
597
|
+
const fixed = [];
|
|
598
|
+
for (const name of ["issue-tracker.md", "domain.md"]) {
|
|
599
|
+
const seed = path.join(ROOT, "scaffold", "docs", "agents", name);
|
|
600
|
+
if (!fs.existsSync(seed)) continue;
|
|
601
|
+
const to = path.join(dir, name);
|
|
602
|
+
if (!fs.existsSync(to)) {
|
|
603
|
+
copyFile(seed, to);
|
|
604
|
+
fixed.push(`${name} (seeded)`);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const text = fs.readFileSync(to, "utf8");
|
|
608
|
+
if (text.includes("seeded by `wdi-method`")) continue;
|
|
609
|
+
fs.writeFileSync(`${to}.bak`, text, "utf8");
|
|
610
|
+
copyFile(seed, to);
|
|
611
|
+
fixed.push(`${name} (was upstream's — previous text kept as ${name}.bak)`);
|
|
612
|
+
}
|
|
613
|
+
for (const line of fixed) note(`repaired docs/agents/${line}`);
|
|
614
|
+
return fixed;
|
|
615
|
+
}
|
|
616
|
+
|
|
346
617
|
// The product's custom room. Three properties, and all three MUST hold together:
|
|
347
618
|
// install/update seeds its content ONLY when absent — never written again after that
|
|
348
619
|
// promote SKIPS it entirely, so a product's own rules can never reach the public repo
|
|
@@ -889,13 +1160,33 @@ function seedEmptyLayers(target, { first }) {
|
|
|
889
1160
|
function writeStamp(target) {
|
|
890
1161
|
const control = path.join(target, ".control");
|
|
891
1162
|
if (!fs.existsSync(control)) return;
|
|
892
|
-
const
|
|
1163
|
+
const eng = enginesReport(target);
|
|
1164
|
+
const fp = engineFingerprints(target);
|
|
1165
|
+
const names = Object.keys(fp);
|
|
1166
|
+
const lines = [
|
|
893
1167
|
"# Written by wdi-method install/update. A trace, not a lockfile.",
|
|
894
1168
|
`wdi_method: ${PKG.version}`,
|
|
895
1169
|
`bmad_method: ${readBmadVersion(target) || '""'}`,
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1170
|
+
];
|
|
1171
|
+
if (names.length) {
|
|
1172
|
+
const blocked = engineInvocationState(target).blocked;
|
|
1173
|
+
lines.push("engines:");
|
|
1174
|
+
lines.push(" source: local");
|
|
1175
|
+
lines.push(" package: mattpocock/skills");
|
|
1176
|
+
lines.push(` lock: ${fs.existsSync(path.join(target, ENGINE_LOCK)) ? ENGINE_LOCK : '""'}`);
|
|
1177
|
+
lines.push(` model_invocation: ${blocked.length ? "blocked" : "enabled"}`);
|
|
1178
|
+
if (eng.missing.length) lines.push(` missing: [${eng.missing.join(", ")}]`);
|
|
1179
|
+
lines.push(" # sha256 of each SKILL.md AFTER the flag was stripped, first 12. Informational:");
|
|
1180
|
+
lines.push(" # engines-invocable decides by looking for the flag, not by comparing these.");
|
|
1181
|
+
lines.push(" skills:");
|
|
1182
|
+
for (const name of names) lines.push(` ${name}: ${fp[name]}`);
|
|
1183
|
+
} else {
|
|
1184
|
+
lines.push("engines:");
|
|
1185
|
+
lines.push(" source: none # none in this repo — G5 cannot run until they are installed");
|
|
1186
|
+
}
|
|
1187
|
+
lines.push(`installed_at: ${today()}`);
|
|
1188
|
+
lines.push("");
|
|
1189
|
+
const stamp = lines.join("\n");
|
|
899
1190
|
fs.writeFileSync(path.join(control, "wdi-method.yaml"), stamp, "utf8");
|
|
900
1191
|
note("stamped .control/wdi-method.yaml");
|
|
901
1192
|
}
|
|
@@ -1111,12 +1402,25 @@ function printSummary(target, agents, { first, was, written, skipped, skills, to
|
|
|
1111
1402
|
`run the ${INIT_SKILL} skill, intent ${DIM}readers${RESET}, ` +
|
|
1112
1403
|
`to write it for this repo's stack`);
|
|
1113
1404
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
summaryLine("", `${DIM}·${RESET}
|
|
1405
|
+
const engReport = enginesReport(target);
|
|
1406
|
+
summaryLine("engines", engReport.present
|
|
1407
|
+
? `${ENGINE_SKILLS.join(" · ")} — found (in this repo)`
|
|
1408
|
+
: `NOT found: ${engReport.missing.join(" · ")}. G5 (wdi-build) and the Fast Path need them; G1–G4 run without them`);
|
|
1409
|
+
if (!engReport.present) {
|
|
1410
|
+
summaryLine("", `${DIM}·${RESET} into THIS repo: ${DIM}${ENGINES_INSTALL_ANY}${RESET} — a user-level plugin does not count`);
|
|
1411
|
+
summaryLine("", `${DIM}·${RESET} docs/agents/ is already seeded, so ${DIM}${ENGINES_SETUP}${RESET} is not needed · ${ENGINES_REPO}`);
|
|
1412
|
+
} else {
|
|
1413
|
+
const blocked = engineInvocationState(target).blocked;
|
|
1414
|
+
if (blocked.length) {
|
|
1415
|
+
summaryLine("", `${DIM}·${RESET} still flagged, so no skill can invoke ${blocked.join(" · ")} — run ${DIM}npx wdi-method engines --fix${RESET}`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
// Upstream's own warning: "installing both leaves you with every skill twice." It is survivable —
|
|
1419
|
+
// the plugin's copies are namespaced and still flagged, so they can neither be invoked nor shadow
|
|
1420
|
+
// the repo's — but `/to-spec` in the UI stops being one thing, so it is said out loud.
|
|
1421
|
+
if (pluginEnginesRegistered()) {
|
|
1422
|
+
summaryLine("", `${DIM}·${RESET} the ${ENGINES_PLUGIN} plugin is ALSO installed for this user — the repo's copies are what run;`);
|
|
1423
|
+
summaryLine("", `${DIM}·${RESET} remove the plugin to keep ${DIM}/to-spec${RESET} unambiguous`);
|
|
1120
1424
|
}
|
|
1121
1425
|
const pending = first ? [] : pendingUpgrades(target);
|
|
1122
1426
|
if (pending.length) {
|
|
@@ -1215,6 +1519,12 @@ function apply(target, agents,
|
|
|
1215
1519
|
setProductIdentity(target, { name: product, client });
|
|
1216
1520
|
setLanguagePolicy(target, { docLanguage, docFilenameLanguage, chosen: languageChosen });
|
|
1217
1521
|
upsertAgentFiles(target, agents, product);
|
|
1522
|
+
// Mechanical, idempotent, and re-run on EVERY update because both sides are restored behind our
|
|
1523
|
+
// back: `npx skills update` puts the author's flag back, and BMad's installer rewrites its own
|
|
1524
|
+
// wrappers. A one-time fix would hold for about a week.
|
|
1525
|
+
enableEngineInvocation(target);
|
|
1526
|
+
retireBmadG5(target);
|
|
1527
|
+
writeDenyRules(target);
|
|
1218
1528
|
writeStamp(target);
|
|
1219
1529
|
printSummary(target, agents, { first, was, written, skipped, skills, tomls, opencodeCmds });
|
|
1220
1530
|
printNextSteps({
|
|
@@ -1224,6 +1534,52 @@ function apply(target, agents,
|
|
|
1224
1534
|
});
|
|
1225
1535
|
}
|
|
1226
1536
|
|
|
1537
|
+
function enginesCommand(target, { fix }) {
|
|
1538
|
+
const before = enginesReport(target);
|
|
1539
|
+
console.log("");
|
|
1540
|
+
console.log(` engines ${before.present ? "all present" : `MISSING ${before.missing.join(" · ")}`}`);
|
|
1541
|
+
for (const name of ENGINE_SKILLS) {
|
|
1542
|
+
const copies = before.files.get(name);
|
|
1543
|
+
if (!copies) continue;
|
|
1544
|
+
const flagged = [...copies.keys()].some((f) =>
|
|
1545
|
+
/^disable-model-invocation\s*:\s*true/m.test(fs.readFileSync(f, "utf8")));
|
|
1546
|
+
const where = [...copies.values()].map((f) => posixRel(target, f)).join(", ");
|
|
1547
|
+
console.log(` ${name.padEnd(17)}${flagged ? "flagged — no skill can invoke it" : "invocable"} ${DIM}${where}${RESET}`);
|
|
1548
|
+
}
|
|
1549
|
+
const banned = repoSkillFiles(target, BMAD_RETIRED_G5);
|
|
1550
|
+
const open = [];
|
|
1551
|
+
for (const [name, copies] of banned) {
|
|
1552
|
+
const shut = [...copies.keys()].every((f) =>
|
|
1553
|
+
/^disable-model-invocation\s*:\s*true/m.test(fs.readFileSync(f, "utf8")));
|
|
1554
|
+
if (!shut) open.push(name);
|
|
1555
|
+
}
|
|
1556
|
+
console.log(` bmad G5 ${banned.size} installed, ${open.length ? `STILL model-invocable: ${open.join(" · ")}` : "all retired"}`);
|
|
1557
|
+
const tracker = path.join(target, "docs", "agents", "issue-tracker.md");
|
|
1558
|
+
const trackerOwn = fs.existsSync(tracker)
|
|
1559
|
+
&& fs.readFileSync(tracker, "utf8").includes("seeded by `wdi-method`");
|
|
1560
|
+
console.log(` config docs/agents/issue-tracker.md ${trackerOwn ? "is the method's" : "is NOT the method's — upstream's answer sends the engines to the wrong place"}`);
|
|
1561
|
+
console.log("");
|
|
1562
|
+
|
|
1563
|
+
if (!fix) {
|
|
1564
|
+
if (!before.present || open.length || !trackerOwn
|
|
1565
|
+
|| engineInvocationState(target).blocked.length) {
|
|
1566
|
+
console.log(` ${DIM}to repair what can be repaired:${RESET} npx wdi-method engines --fix`);
|
|
1567
|
+
console.log("");
|
|
1568
|
+
}
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
repairAgentDocs(target);
|
|
1572
|
+
enableEngineInvocation(target);
|
|
1573
|
+
retireBmadG5(target);
|
|
1574
|
+
writeDenyRules(target);
|
|
1575
|
+
writeStamp(target);
|
|
1576
|
+
ok("engines aligned");
|
|
1577
|
+
if (!before.present) {
|
|
1578
|
+
console.log("");
|
|
1579
|
+
console.log(enginesMissingMessage(before.missing));
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1227
1583
|
function verify(target, agents) {
|
|
1228
1584
|
requireKit();
|
|
1229
1585
|
const missing = [];
|
|
@@ -1436,8 +1792,11 @@ async function runWizard(pre) {
|
|
|
1436
1792
|
: "BMad Method: not installed",
|
|
1437
1793
|
hasWdi ? "WDI Method: already present — the installer will offer an update" : "WDI Method: not present",
|
|
1438
1794
|
enginesPresent(target)
|
|
1439
|
-
?
|
|
1440
|
-
: `
|
|
1795
|
+
? `Engines (mattpocock/skills, in this repo): all ${ENGINE_SKILLS.length} present`
|
|
1796
|
+
: `Engines: MISSING ${enginesReport(target).missing.join(" · ")} — ${ENGINES_INSTALL_ANY} (${ENGINES_REPO})`,
|
|
1797
|
+
engineInvocationState(target).blocked.length
|
|
1798
|
+
? `Engine invocation: BLOCKED for ${engineInvocationState(target).blocked.join(" · ")} — npx wdi-method engines --fix`
|
|
1799
|
+
: "Engine invocation: enabled (the author's disable-model-invocation is stripped from the repo's copies)",
|
|
1441
1800
|
nonempty ? "Folder is not empty (normal for a product repo already under way)" : "Folder is empty",
|
|
1442
1801
|
].join("\n");
|
|
1443
1802
|
p.note(facts, "Detected");
|
|
@@ -1582,7 +1941,7 @@ function runNonInteractive(args) {
|
|
|
1582
1941
|
die(bmadMissingMessage());
|
|
1583
1942
|
}
|
|
1584
1943
|
if (!args.skipEngines && !enginesPresent(target)) {
|
|
1585
|
-
die(enginesMissingMessage());
|
|
1944
|
+
die(enginesMissingMessage(enginesReport(target).missing));
|
|
1586
1945
|
}
|
|
1587
1946
|
const existing = readIndexIdentity(target);
|
|
1588
1947
|
const product = args.product || existing.name;
|
|
@@ -1600,7 +1959,7 @@ function runNonInteractive(args) {
|
|
|
1600
1959
|
|
|
1601
1960
|
async function main() {
|
|
1602
1961
|
const args = parseArgs(process.argv);
|
|
1603
|
-
if (!["wizard", "install", "update", "verify", "promote"].includes(args.cmd)) {
|
|
1962
|
+
if (!["wizard", "install", "update", "verify", "promote", "engines"].includes(args.cmd)) {
|
|
1604
1963
|
usage();
|
|
1605
1964
|
process.exit(2);
|
|
1606
1965
|
}
|
|
@@ -1623,6 +1982,10 @@ async function main() {
|
|
|
1623
1982
|
promote(args.dir);
|
|
1624
1983
|
return;
|
|
1625
1984
|
}
|
|
1985
|
+
if (args.cmd === "engines") {
|
|
1986
|
+
enginesCommand(requireTarget(args.dir), { fix: Boolean(args.fix) });
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1626
1989
|
const wantTui = !args.yes && args.cmd !== "verify" && process.stdin.isTTY && process.stdout.isTTY;
|
|
1627
1990
|
if (wantTui) {
|
|
1628
1991
|
await runWizard(args);
|
|
@@ -25,10 +25,13 @@ The repo layout is governed by `corpus-guide.md` and mapped by
|
|
|
25
25
|
| `.how/` | How it is built |
|
|
26
26
|
| `.what-rendered/` · `.how-rendered/` | The two above, assembled for a human to read — one page per gate, at the mirror path. Regenerated by `validate.py`, never edited, never read by a skill |
|
|
27
27
|
| `_bmad-output/` | Run workspace; MUST be in git, not curated |
|
|
28
|
+
| `.scratch/` | One directory per effort: a spec's `SPEC.md` and its ticket files, and ad hoc work that has no `FR` yet. MUST be in git — the corpus cites into it by path |
|
|
28
29
|
| `.work/` | Scratch; MUST be in git, emptied when a task closes |
|
|
29
30
|
| *(application roots)* | Application code — named and mapped in `.control/structure-codebase.md` |
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
**One exception, and it is not corpus.** `docs/agents/issue-tracker.md` and `docs/agents/domain.md` are the ENGINES' config, and their path belongs to the engines' author — `to-spec`, `to-tickets`, `implement` and `triage` read those two files and nowhere else. What they SAY is this method's, and `wdi-method` seeds both already answered. Neither is corpus, neither is a rule, and nothing else MAY be added beside them.
|
|
33
|
+
|
|
34
|
+
Otherwise the method does not use a `docs/` layer for corpus or rules. A leftover `docs/`
|
|
32
35
|
folder is inventory to sort, not a second home.
|
|
33
36
|
|
|
34
37
|
## Article 4 — Lifecycle
|
|
@@ -47,12 +47,16 @@ inventory, and putting somebody else's skill in it makes the register a lie.
|
|
|
47
47
|
|
|
48
48
|
No → **absorb the discipline and invoke nothing.** Prose we could have written is not an engine.
|
|
49
49
|
|
|
50
|
-
An engine whose output this corpus keeps is invoked even
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
An engine whose output this corpus keeps is invoked even though it is **not part of this package's
|
|
51
|
+
install** — `domain-modeling` is the case, and `wdi-blueprint` invokes it. It is one of the six the
|
|
52
|
+
installer requires IN the repo, so it is named without a plugin prefix: a repo may hold the engines as
|
|
53
|
+
its own files or not at all, and `mattpocock-skills:domain-modeling` resolves to nothing in the first
|
|
54
|
+
case. Two rules make the reference safe in a repo that does not have it:
|
|
55
|
+
|
|
56
|
+
- **A missing engine is a state, not a defect — at G1–G4.** Report it once, name the standard the work
|
|
57
|
+
is still held to, and do the work. You MUST NOT block one of those gates on it, and you MUST NOT
|
|
58
|
+
report its absence as a finding. G5 is the exception and always was: `wdi-build` cannot run without
|
|
59
|
+
the engines, and the installer refuses rather than let that be discovered with a spec already open.
|
|
56
60
|
- **The wrapper carries the standard, never the plugin.** What the engine is invoked *for* MUST be written
|
|
57
61
|
in the wrapping skill as behaviours to verify — so the same bar is met either way, and a run that
|
|
58
62
|
produced none of them is reported as a transcription rather than landed.
|