wdi-method 0.6.30 → 0.6.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +3 -0
  3. package/bin/wdi-method.js +121 -14
  4. package/kit/.constitution/method/document/architecture-guide.md +217 -209
  5. package/kit/.constitution/method/document/corpus-guide.md +522 -517
  6. package/kit/.constitution/method/document/decision-guide.md +236 -216
  7. package/kit/.constitution/method/document/delivery-flow-guide.md +20 -0
  8. package/kit/.constitution/method/document/prd-guide.md +245 -245
  9. package/kit/.constitution/method/document/templates/design-system.md +96 -66
  10. package/kit/.constitution/method/document/templates/experience.md +62 -0
  11. package/kit/.constitution/method/document/templates/structure-codebase.md +131 -129
  12. package/kit/.constitution/method/document/templates/ux.md +78 -76
  13. package/kit/.constitution/method/document/ux-guide.md +161 -115
  14. package/kit/.constitution/method/method-glossary.md +3 -0
  15. package/kit/.constitution/method/scripts/validate.py +3310 -3200
  16. package/kit/.constitution/method/structure-guide.md +204 -202
  17. package/kit/.constitution/method/why/artifact-map.md +158 -157
  18. package/kit/skills/wdi-blueprint/SKILL.md +271 -264
  19. package/kit/skills/wdi-component/SKILL.md +179 -174
  20. package/kit/skills/wdi-decision/SKILL.md +206 -203
  21. package/kit/skills/wdi-help/SKILL.md +127 -125
  22. package/kit/skills/wdi-init/SKILL.md +9 -4
  23. package/kit/skills/wdi-problem/SKILL.md +114 -108
  24. package/kit/skills/wdi-product/SKILL.md +167 -162
  25. package/kit/skills/wdi-reconcile/SKILL.md +170 -169
  26. package/kit/skills/wdi-upgrade/SKILL.md +234 -215
  27. package/kit/skills/wdi-ux/SKILL.md +187 -169
  28. package/kit-overlay/AGENTS.md +3 -1
  29. package/package.json +1 -1
  30. package/scaffold/.control/registry/index.yaml +2 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,74 @@ version contains every fix below it.
10
10
 
11
11
  ---
12
12
 
13
+ ## [0.6.31] - 2026-09-28
14
+
15
+ A patch by the owner's choice, though it carries behaviour changes — read the whole entry.
16
+
17
+ **Read this before you update.** Two validator rules are new or stricter, so a repo that is green today
18
+ can come back red. Most of it is something that was already wrong and went unchecked; two parts are new
19
+ obligations — the ban on citing a UX run from the corpus, and `landed_from` on every landed UX document.
20
+
21
+ ### Changed: behaviour
22
+
23
+ - **New validator `ux-landed`, checked per run.** Every landed UX document names the run file(s) it came
24
+ from in a new frontmatter field, `landed_from`. Once Product Components exist, every active run
25
+ `DESIGN.md` / `EXPERIENCE.md` in `_bmad-output/ux/` MUST be named by some landed document — a
26
+ component's, or `.what/experience.md` — and `.what/` and `.how/` MUST NOT cite the run anywhere else.
27
+ `landed_from` is provenance: the run may be deleted later and it stays valid. `.control/decisions/` may
28
+ still cite the run. A run file without frontmatter is recognised by its filename. UX stays optional: the rule
29
+ is silent with no run, silent before any component exists, silent on a run marked `superseded` or
30
+ `withdrawn`, and no `mode` or `risk_accepted` value changes it.
31
+ - **`container-built` understands a product split across repositories.** A new optional field `repo:` on
32
+ a container names the repository its code lives in when that is NOT this one. Such a container keeps
33
+ `built: true` and its C4 place, but gets no heading in this repo's code map — the template, the
34
+ structure guide, and the validator now say the same thing. Absent `repo:` means this repository, so a
35
+ single-repo product sees no change. `repo:` naming this repository — compared with the `origin`
36
+ remote; without one the comparison is reported as skipped — or `repo:` on a `built: false` container,
37
+ is red.
38
+ - **`cites-resolve` recognises installed skill copies under every host.** `.<host>/skills/bmad-*` and
39
+ `.<host>/skills/wdi-*` are skipped by pattern instead of a three-host list, so a repo installed for
40
+ Kiro, Cline, Trae, or any other supported host no longer carries permanent findings on files it may not
41
+ edit. `.work/` is skipped too — scratch is not authority, so a pasted validator line is not a claim.
42
+ - **`update` records what `wdi-upgrade` still owes.** The probes now write `upgrade_pending` into
43
+ `.control/wdi-method.yaml`, numbered by the rows of `wdi-upgrade`'s checklist, and the field is absent
44
+ when nothing is owed. `npx wdi-method upgrade-check` re-probes at any time and rewrites it (exit 1 while
45
+ anything is pending); `wdi-upgrade` ends by running it, and `wdi-help` routes on the field instead of on
46
+ whether `update` "just ran". The probes no longer report the method's own skill text under a non-Claude
47
+ host, or a scratch paper, as stale product content.
48
+
49
+ ### Changed: method
50
+
51
+ - **UX has a product level.** New `.what/experience.md` (template `templates/experience.md`) holds the
52
+ experience every component keeps — foundation, information architecture, voice and tone, the flow map,
53
+ journeys that cross components, shared edge cases. `design-system.md` grows the build side: state
54
+ patterns, interaction primitives, how accessibility is met, surfaces that are not screens.
55
+ `ux-guide.md` § *Product level* maps each `bmad-ux` section to one of the two, and both land at G2
56
+ because neither path contains a `<pc>`. A flow zoom-in lands with the component owning its screens,
57
+ never with the owner of a shared composite drawn inside it.
58
+ - **Completing `touches` on an applied decision is allowed**, append-only, for a file the applying commit
59
+ really changed within what the Decision says. A change outside it is a finding, not a trace.
60
+ - **Gates get recorded.** `wdi-problem`, `wdi-product`, `wdi-blueprint`, and `wdi-component` end by asking
61
+ the owner whether their gate passed and write `gates_passed` / `g4_passed` only on an explicit yes.
62
+ Skills that need a passed gate read the record and ask when it is missing; `wdi-reconcile` reports
63
+ downstream work whose gate was never recorded. `delivery-flow-guide.md` § *Recording a gate that passed*.
64
+ - **Session papers from outside tools live in `.work/<tool>/<slug>/`**, distilled into `DEC-` / memlog /
65
+ `.control/reports/` and deleted when the session closes. `corpus-guide.md` no longer asks for a `DEC-`
66
+ before scratch is deleted — the two guides disagreed.
67
+
68
+ **What a repo that already has the method installed does about it.** Run
69
+ `npx wdi-method@latest update --yes`, then read the `upgrade` line or `upgrade_pending`. Expect, where
70
+ they apply: cross-component UX sections parked in `design-system.md` (item 15), SRS or landed UX
71
+ documents citing the UX run (item 16), containers whose code lives in another repository (item 17 —
72
+ fill `repo:` there, drop it where it names this repo, and re-run `wdi-init` intent `structure`), and
73
+ landed UX documents with no `landed_from` (item 18). Gate skills now record `gates_passed`; a repo whose
74
+ gates passed before that is asked once per gate by `wdi-upgrade`, and nothing is written without a yes.
75
+
76
+ **`wdi-upgrade`:** needed when `upgrade_pending` lists anything — items 15–18 are new in this version —
77
+ or when `wdi-help` shows a passed gate that was never recorded. Not needed otherwise.
78
+
79
+ ---
80
+
13
81
  ## [0.6.30] - 2026-09-27
14
82
 
15
83
  A patch. No behaviour changes.
package/README.md CHANGED
@@ -73,6 +73,9 @@ Inside your coding agent, run:
73
73
  ```
74
74
  `wdi-help` reads `.control/registry/` and tells you the gate your project is at, the open specs, and the next skill, without guessing from the conversation.
75
75
 
76
+ ### Updating Later: Do I Need `/wdi-upgrade`?
77
+ You never have to work it out from the version number. `npx wdi-method@latest update` checks your repo's content for anything still in an older shape and writes what it found into `upgrade_pending` in `.control/wdi-method.yaml` — absent means nothing is owed. `/wdi-help` reads that field and tells you to run `/wdi-upgrade` first when it is there. `npx wdi-method upgrade-check` re-checks at any time. Every [`CHANGELOG.md`](CHANGELOG.md) entry also ends with a `wdi-upgrade: needed / not needed` line.
78
+
76
79
  ---
77
80
 
78
81
  ## Three Workflow Options
package/bin/wdi-method.js CHANGED
@@ -170,6 +170,7 @@ function usage() {
170
170
  update [dir] update (TUI unless --yes)
171
171
  verify [dir]
172
172
  engines [dir] [--fix] report the six engines, their invocation state, and the BMad G5 ban
173
+ upgrade-check [dir] re-probe what wdi-upgrade still owes; rewrites upgrade_pending (exit 1 if any)
173
174
  promote <live-dir> --rescue pull a method change back out of a consumer (not the normal flow)
174
175
 
175
176
  --yes non-interactive
@@ -214,7 +215,7 @@ function parseArgs(argv) {
214
215
  return args;
215
216
  }
216
217
  const first = rest[0];
217
- if (["install", "update", "verify", "promote", "engines"].includes(first)) {
218
+ if (["install", "update", "verify", "promote", "engines", "upgrade-check"].includes(first)) {
218
219
  args.cmd = rest.shift();
219
220
  } else if (first.startsWith("-")) {
220
221
  args.cmd = "wizard";
@@ -1266,6 +1267,43 @@ function seedEmptyLayers(target, { first }) {
1266
1267
  }
1267
1268
  }
1268
1269
 
1270
+ // What `wdi-upgrade` still owes, written where it is committed. The summary line alone was the only
1271
+ // record, so a repo whose owner missed that one screen could not find out again — and `wdi-help` can
1272
+ // only route to `wdi-upgrade` from something it can read. Absent when nothing is pending: an empty
1273
+ // list would read as a question nobody has to answer.
1274
+ const PENDING_HEADER = "# What wdi-upgrade still has to move. Re-probed by `npx wdi-method upgrade-check`; absent = nothing.";
1275
+
1276
+ function pendingBlock(items) {
1277
+ if (!items.length) return "";
1278
+ return `${PENDING_HEADER}\nupgrade_pending:\n${items.map((i) => ` - ${JSON.stringify(i)}\n`).join("")}`;
1279
+ }
1280
+
1281
+ // Re-probe and rewrite ONLY the pending block, leaving the rest of the stamp as `update` wrote it.
1282
+ // This is how `wdi-upgrade` closes: the field disappears when the probes come back clean.
1283
+ function upgradeCheck(target) {
1284
+ const file = path.join(target, ".control", "wdi-method.yaml");
1285
+ if (!fs.existsSync(file)) die("no .control/wdi-method.yaml — the method is not installed here");
1286
+ const items = pendingUpgrades(target);
1287
+ let text = fs.readFileSync(file, "utf8")
1288
+ .replace(/^# What wdi-upgrade still has to move\..*\r?\n/m, "")
1289
+ .replace(/^upgrade_pending:\r?\n(?:[ \t]+- .*\r?\n)*/m, "");
1290
+ const block = pendingBlock(items);
1291
+ if (block) {
1292
+ text = /^installed_at:/m.test(text)
1293
+ ? text.replace(/^installed_at:/m, `${block}installed_at:`)
1294
+ : `${text.replace(/\s*$/, "\n")}${block}`;
1295
+ }
1296
+ fs.writeFileSync(file, text, "utf8");
1297
+ console.log("");
1298
+ if (!items.length) {
1299
+ ok("nothing pending — wdi-upgrade owes nothing here");
1300
+ return 0;
1301
+ }
1302
+ console.log(` upgrade ${items.length} item${items.length === 1 ? "" : "s"} still in the OLD shape — run the wdi-upgrade skill`);
1303
+ for (const item of items) console.log(` ${DIM}·${RESET} ${item}`);
1304
+ return 1;
1305
+ }
1306
+
1269
1307
  function writeStamp(target) {
1270
1308
  const control = path.join(target, ".control");
1271
1309
  if (!fs.existsSync(control)) return;
@@ -1293,6 +1331,8 @@ function writeStamp(target) {
1293
1331
  lines.push("engines:");
1294
1332
  lines.push(" source: none # none in this repo — G5 cannot run until they are installed");
1295
1333
  }
1334
+ const pending = pendingBlock(pendingUpgrades(target));
1335
+ if (pending) lines.push(pending.trimEnd());
1296
1336
  lines.push(`installed_at: ${today()}`);
1297
1337
  lines.push("");
1298
1338
  const stamp = lines.join("\n");
@@ -1427,17 +1467,17 @@ function pendingUpgrades(target) {
1427
1467
  });
1428
1468
  };
1429
1469
  const items = [];
1430
- if (has(".control", "registry", "requirements.yaml")) items.push("requirements.yaml → goals.yaml + requirements-<slug>.yaml");
1470
+ if (has(".control", "registry", "requirements.yaml")) items.push("#1 requirements.yaml → goals.yaml + requirements-<slug>.yaml");
1431
1471
  // The file the engines actually read. `/setup-matt-pocock-skills` writes its own answer here — no
1432
1472
  // `specs.yaml`, no predefined path — and `seedAgentDocs` will not overwrite a file the product owns,
1433
1473
  // so without this probe the repo never learns why its tickets scatter.
1434
1474
  if (has("docs", "agents", "issue-tracker.md")
1435
1475
  && !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)");
1476
+ items.push("#11 docs/agents/issue-tracker.md is not the method's answer (npx wdi-method engines --fix)");
1437
1477
  }
1438
1478
  const strays = specsOutsideScratch(read(".control", "registry", "specs.yaml"));
1439
1479
  if (strays.length) {
1440
- items.push(`spec_folder outside .scratch/<spec-id>-<slug>/ on ${strays.join(", ")} `
1480
+ items.push(`#12 spec_folder outside .scratch/<spec-id>-<slug>/ on ${strays.join(", ")} `
1441
1481
  + `(the folder moves, then its cites)`);
1442
1482
  }
1443
1483
  // Reported only where it is still WORK. A closed pre-rename wave is read correctly (0.6.7 taught
@@ -1451,46 +1491,109 @@ function pendingUpgrades(target) {
1451
1491
  // that would answer "not mine" and stop.
1452
1492
  const legacyOpen = specsInLegacyShape(read(".control", "registry", "specs.yaml"));
1453
1493
  if (legacyOpen.length) {
1454
- items.push(`${legacyOpen.join(", ")} still in the W<n>/epics/stories shape and not closed `
1494
+ items.push(`#2 ${legacyOpen.join(", ")} still in the W<n>/epics/stories shape and not closed `
1455
1495
  + `(flattened into tickets, id kept as its retired alias)`);
1456
1496
  }
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");
1497
+ 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
1498
  // 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");
1499
+ 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
1500
  const whatDir = path.join(target, ".what");
1461
1501
  if (fs.existsSync(whatDir)) {
1462
1502
  for (const pc of fs.readdirSync(whatDir)) {
1463
1503
  if (pc.startsWith("_")) continue;
1464
1504
  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; }
1505
+ if (/^\|\s*UC-\d+\s*\|/m.test(srs)) { items.push("#5 an SRS with a UC Catalogue table (now a pointer)"); break; }
1466
1506
  }
1467
1507
  }
1468
1508
  const howDir = path.join(target, ".how");
1469
1509
  if (fs.existsSync(howDir)) {
1470
1510
  for (const pc of fs.readdirSync(howDir)) {
1471
1511
  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; }
1512
+ 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
1513
  }
1474
1514
  }
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)");
1515
+ 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)");
1516
+ if (has(".control", "generated", "brief.md") || has(".control", "generated", "blueprint.md")) items.push("#8 human pages still in .control/generated/ (render clears them)");
1517
+ if (has(".what", "_product-brief", "brief.md") && !has(".what-rendered")) items.push("#9 no .what-rendered/ yet (render creates it)");
1478
1518
  // Skipped: what the validator never reads (kit copies, rendered output, dependencies) and what it
1479
1519
  // treats as a record of the PAST — memlog, decisions, reports, _bmad-output. A stale path in a log
1480
1520
  // is history, not a finding, and repointing it would falsify the record.
1481
1521
  const SKIP = new Set([".git", "node_modules", "target", ".constitution", ".claude", ".agents", ".agent",
1482
1522
  ".what-rendered", ".how-rendered", "dist", "build", "memlog", "decisions", "reports", "meetings", "_bmad-output", ".work"]);
1483
1523
  const OLD_PAGE = /\.control\/generated\/(brief|blueprint|prd-[a-z0-9-]+)\.md/;
1524
+ // At the root, every dot-folder except the corpus layers is a tool's: an agent host's skill copies
1525
+ // (`.kiro/skills/wdi-upgrade` names the OLD paths on purpose, to probe for them), editor state, CI.
1526
+ // Listing hosts by name missed every one outside the first three, and each such repo was told to run
1527
+ // `wdi-upgrade` on every update with nothing to move.
1528
+ const CORPUS_DOT = new Set([".control", ".what", ".how"]);
1484
1529
  const citesOldPage = (dir, depth) => {
1485
1530
  if (depth > 8) return false;
1486
1531
  for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
1532
+ if (e.isDirectory() && depth === 0 && e.name.startsWith(".") && !CORPUS_DOT.has(e.name)) continue;
1487
1533
  if (e.isDirectory()) { if (!SKIP.has(e.name) && citesOldPage(path.join(dir, e.name), depth + 1)) return true; continue; }
1488
1534
  if (e.name === "answered.md") continue;
1489
1535
  if (e.name.endsWith(".md") && OLD_PAGE.test(fs.readFileSync(path.join(dir, e.name), "utf8"))) return true;
1490
1536
  }
1491
1537
  return false;
1492
1538
  };
1493
- if (citesOldPage(target, 0)) items.push("a document cites .control/generated/brief|blueprint|prd-*.md (pages moved to the rendered trees)");
1539
+ if (citesOldPage(target, 0)) items.push("#10 a document cites .control/generated/brief|blueprint|prd-*.md (pages moved to the rendered trees)");
1540
+ // Cross-component EXPERIENCE parked in the product's design system. Before `.what/experience.md`
1541
+ // existed it had no home, so a repo put it in the one product-level UX file there was — a promise
1542
+ // in the build layer. The section names are bmad-ux's own, which is where the content comes from.
1543
+ // The names are bmad-ux's own and the product-level template's, in either spelling — a probe narrower
1544
+ // than the template it migrates into reports "nothing owed" while the content is still in the wrong layer.
1545
+ 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
1546
+ .test(read(".how", "_platform", "design-system.md"))) {
1547
+ items.push("#15 cross-component experience in design-system.md (moves to .what/experience.md)");
1548
+ }
1549
+ // #16 and #18 follow `ux-landed`: nothing about a UX run is owed before a Product Component exists.
1550
+ const components = read(".control", "registry", "components.yaml");
1551
+ const hasPcs = /^product_components:[ \t]*\r?\n[ \t]+-/m.test(components);
1552
+ const LANDED_FROM = /^landed_from:.*(?:\r?\n[ \t]+-.*)*/m; // provenance, not a citation
1553
+ const UX_RUN = /_bmad-output\/ux\/[A-Za-z0-9_./-]*?(DESIGN|EXPERIENCE|design-system)\.md/;
1554
+ const citesUxRun = (dir, depth) => {
1555
+ if (depth > 8 || !fs.existsSync(dir)) return false;
1556
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
1557
+ const full = path.join(dir, e.name);
1558
+ if (e.isDirectory()) { if (citesUxRun(full, depth + 1)) return true; continue; }
1559
+ if (/\.(md|ya?ml)$/.test(e.name) && UX_RUN.test(fs.readFileSync(full, "utf8").replace(LANDED_FROM, ""))) return true;
1560
+ }
1561
+ return false;
1562
+ };
1563
+ if (hasPcs && (citesUxRun(path.join(target, ".what"), 0) || citesUxRun(path.join(target, ".how"), 0))) {
1564
+ items.push("#16 the corpus cites a UX run in _bmad-output/ux/ (repoint at what landed; provenance to landed_from)");
1565
+ }
1566
+ // #17 — a container whose `repo:` sends its code elsewhere but still has a heading here, or a `repo:` on
1567
+ // a container nobody writes. The self-naming case needs the origin remote and is left to `container-built`.
1568
+ const ctrSection = (components.match(/^containers:[^\n]*\n((?:[ \t].*(?:\r?\n|$)|\r?\n)*)/m) || [, ""])[1];
1569
+ const headings = new Set([...(read(".control", "structure-codebase.md").split(/^## Containers\s*$/m)[1] || "")
1570
+ .split(/^## (?!#)/m)[0].matchAll(/^###\s+(.+?)\s*$/gm)].map((m) => m[1]));
1571
+ const repoDebt = ctrSection.split(/^[ \t]*-[ \t]+id:[ \t]*/m).slice(1).some((block) => {
1572
+ const id = block.split(/\r?\n/)[0].trim().replace(/^["']|["']$/g, "");
1573
+ const repo = /^[ \t]+repo:[ \t]*["']?([^"'\r\n]+)/m.exec(block)?.[1].trim();
1574
+ if (!repo) return false;
1575
+ return /^[ \t]+built:[ \t]*false\b/m.test(block) || headings.has(id);
1576
+ });
1577
+ if (repoDebt) {
1578
+ items.push("#17 a container's repo: disagrees with the code map (heading for code kept elsewhere, or repo: on built: false)");
1579
+ }
1580
+ // #18 — landed UX documents written before `landed_from` existed. Without it `ux-landed` cannot tell
1581
+ // which run a landing discharged, so every run in `_bmad-output/ux/` reads as still owed.
1582
+ const uxRoot = path.join(target, "_bmad-output", "ux");
1583
+ const hasRun = fs.existsSync(uxRoot) && fs.readdirSync(uxRoot, { recursive: true })
1584
+ .some((f) => /(^|[\\/])(DESIGN|EXPERIENCE)\.md$/.test(String(f)));
1585
+ const landedDocs = [];
1586
+ for (const [layer, slot, name] of [[".how", "01-ux", "DESIGN.md"], [".what", "04-usecases", "EXPERIENCE.md"]]) {
1587
+ const base = path.join(target, layer);
1588
+ if (!fs.existsSync(base)) continue;
1589
+ for (const pc of fs.readdirSync(base)) {
1590
+ const f = path.join(base, pc, slot, name);
1591
+ if (fs.existsSync(f)) landedDocs.push(f);
1592
+ }
1593
+ }
1594
+ if (hasPcs && hasRun && landedDocs.some((f) => !/^landed_from:/m.test(fs.readFileSync(f, "utf8")))) {
1595
+ items.push("#18 a landed UX document has no landed_from (name the run file(s) it came from)");
1596
+ }
1494
1597
  return items;
1495
1598
  }
1496
1599
 
@@ -2181,7 +2284,7 @@ function runNonInteractive(args) {
2181
2284
 
2182
2285
  async function main() {
2183
2286
  const args = parseArgs(process.argv);
2184
- if (!["wizard", "install", "update", "verify", "promote", "engines"].includes(args.cmd)) {
2287
+ if (!["wizard", "install", "update", "verify", "promote", "engines", "upgrade-check"].includes(args.cmd)) {
2185
2288
  usage();
2186
2289
  process.exit(2);
2187
2290
  }
@@ -2208,6 +2311,10 @@ async function main() {
2208
2311
  enginesCommand(requireTarget(args.dir), { fix: Boolean(args.fix) });
2209
2312
  return;
2210
2313
  }
2314
+ if (args.cmd === "upgrade-check") {
2315
+ process.exitCode = upgradeCheck(requireTarget(args.dir));
2316
+ return;
2317
+ }
2211
2318
  const wantTui = !args.yes && args.cmd !== "verify" && process.stdin.isTTY && process.stdout.isTTY;
2212
2319
  if (wantTui) {
2213
2320
  await runWizard(args);