bmad-plus 0.16.0 → 0.17.1

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 (66) hide show
  1. package/CHANGELOG.md +114 -29
  2. package/README.md +45 -27
  3. package/SECURITY.md +9 -1
  4. package/osint-agent-package/README.md +13 -0
  5. package/osint-agent-package/SETUP_KEYS.md +13 -0
  6. package/package.json +32 -8
  7. package/readme-international/README.de.md +134 -537
  8. package/readme-international/README.es.md +136 -556
  9. package/readme-international/README.fr.md +135 -553
  10. package/src/bmad-plus/agents/agent-shadow/SKILL.md +6 -0
  11. package/src/bmad-plus/packs/pack-memory/README.md +1 -1
  12. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +1 -1
  13. package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +1 -1
  14. package/src/bmad-plus/packs/pack-shield/README.md +2 -0
  15. package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -0
  16. package/src/bmad-plus/packs/pack-shield/shared/audit-report-template.md +6 -0
  17. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +4 -0
  18. package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +18 -4
  19. package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +95 -19
  20. package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +90 -10
  21. package/src/bmad-plus/skills/bmad-plus-uat/templates/README.md +63 -0
  22. package/src/bmad-plus/skills/bmad-plus-uat/templates/example-uat-spec.json +95 -0
  23. package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-release-gate.mjs +127 -0
  24. package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-spec-present.mjs +77 -0
  25. package/tools/build/adapters.config.js +1 -1
  26. package/tools/build/generate-adapters.js +186 -60
  27. package/tools/build/generate.js +49 -13
  28. package/tools/cli/bmad-plus-cli.js +3 -6
  29. package/tools/cli/commands/autoconfig.js +66 -24
  30. package/tools/cli/commands/install.js +620 -356
  31. package/tools/cli/commands/memory.js +40 -13
  32. package/tools/cli/commands/scan.js +94 -30
  33. package/tools/cli/commands/uat.js +21 -5
  34. package/tools/cli/commands/uninstall.js +65 -16
  35. package/tools/cli/commands/update-check.js +8 -3
  36. package/tools/cli/commands/update-policy.js +14 -5
  37. package/tools/cli/commands/update.js +89 -20
  38. package/tools/cli/i18n.js +128 -66
  39. package/tools/cli/lib/ide-config.js +3 -1
  40. package/tools/cli/lib/install-manifest.js +13 -6
  41. package/tools/cli/lib/installation-health.js +33 -1
  42. package/tools/cli/lib/installed-adapters.js +29 -6
  43. package/tools/cli/lib/memory-init.js +19 -4
  44. package/tools/cli/lib/packs.js +17 -28
  45. package/tools/cli/lib/path-hash.js +5 -1
  46. package/tools/cli/lib/python-provision.js +51 -12
  47. package/tools/cli/lib/stack-detect.js +51 -25
  48. package/tools/cli/lib/uat.js +18 -0
  49. package/tools/cli/lib/update-check.js +132 -34
  50. package/tools/cli/lib/update-dispatch.js +2 -2
  51. package/tools/cli/lib/update-policy.js +57 -19
  52. package/tools/cli/lib/update-transaction.js +439 -85
  53. package/tools/build/README.md +0 -78
  54. package/tools/build/check-counts.js +0 -626
  55. package/tools/build/check-install-contract.js +0 -648
  56. package/tools/build/generated-adapters/.codex/AGENTS.md +0 -134
  57. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +0 -139
  58. package/tools/build/generated-adapters/.opencode/AGENTS.md +0 -134
  59. package/tools/build/generated-adapters/AGENTS.md +0 -132
  60. package/tools/build/generated-adapters/CLAUDE.md +0 -135
  61. package/tools/build/generated-adapters/CONVENTIONS.md +0 -134
  62. package/tools/build/generated-adapters/GEMINI.md +0 -139
  63. package/tools/build/generated-adapters/README.md +0 -79
  64. package/tools/maintain/upstream-candidate.js +0 -456
  65. package/tools/release/publication-content.js +0 -903
  66. package/tools/release/supply-chain.js +0 -282
@@ -61,6 +61,12 @@ or a public entity). This gate governs every direct menu path — there is no pa
61
61
  personal data that skips it. It does not remove any capability or restrict lawful,
62
62
  authorized use (CG-02); `QS` on a public handle, `DG`, and tooling paths are exempt.
63
63
 
64
+ **Output disclaimer.** Open every dossier or profile with: *"AI-generated OSINT output —
65
+ may contain errors; not legal advice. The operator is the data controller and is
66
+ responsible for the lawful basis (GDPR Art. 6/9), platform Terms of Service, and human
67
+ verification before any decision."* Never profile a private individual without a
68
+ recorded legal basis.
69
+
64
70
  ## Required Setup
65
71
 
66
72
  - Python 3.10+
@@ -97,7 +97,7 @@ After installation:
97
97
 
98
98
  ```
99
99
  # Scan your projects
100
- "Zecher, scan projects in D:\travail\DEV"
100
+ "Zecher, scan projects in ~/dev"
101
101
 
102
102
  # Start remembering
103
103
  "Atlas, create a PRD for..." ← decisions auto-logged
@@ -29,7 +29,7 @@ While executing the protocol, the host agent should:
29
29
  A session that ends without an agent turn has no guaranteed handoff.
30
30
 
31
31
  Manual activation via Zecher:
32
- - `"Zecher, scan projects in D:\travail\DEV"` — discover and index all projects
32
+ - `"Zecher, scan projects in ~/dev"` — discover and index all projects
33
33
  - `"Zecher, consolidate memory"` — deduplicate, archive stale, promote
34
34
  - `"Zecher, where were we?"` — reconstruct context from last session
35
35
  - `"Zecher, health check"` — verify memory integrity
@@ -165,7 +165,7 @@ AGENTS.md → Already agent-aware
165
165
  ### Project Metadata (per scan)
166
166
  ```yaml
167
167
  # ~/.bmad-plus/brain/projects/<hash>.yaml
168
- path: "D:\\travail\\DEV\\my-project"
168
+ path: "~/dev/my-project"
169
169
  name: "my-project"
170
170
  hash: "a1b2c3d4" # SHA256 of absolute path
171
171
  stack:
@@ -2,6 +2,8 @@
2
2
 
3
3
  > **27 compliance agents across 6 categories, plus 11 GDPR & AI Act workflows** + 1 orchestrator covering Data Privacy, Cybersecurity, Industry Compliance, Defense & Export, AI Governance, and Accessibility & ESG.
4
4
 
5
+ > ⚠️ **Not legal advice.** Shield output is AI-generated, may be incomplete or out of date, and does not replace a lawyer, DPO or certified auditor. Human review by a qualified person is required before any compliance decision, filing or attestation.
6
+
5
7
  ## Overview
6
8
 
7
9
  Pack Shield transforms BMAD+ into a comprehensive GRC (Governance, Risk & Compliance) assistant. Each agent is an expert system prompt for a specific regulatory framework, providing structured compliance guidance including gap analysis, policy drafting, control mapping, and audit support.
@@ -4,6 +4,8 @@
4
4
  > **Version:** 1.0.0
5
5
  > **Created by:** Laurent Rochetta — https://github.com/lrochetta/BMAD-PLUS
6
6
 
7
+ > ⚠️ **Not legal advice.** Shield output is AI-generated, may be incomplete or out of date, and does not replace a lawyer, DPO or certified auditor. Human review by a qualified person is required before any compliance decision, filing or attestation.
8
+
7
9
  ## Overview
8
10
 
9
11
  Shield transforms BMAD+ into a comprehensive GRC (Governance, Risk & Compliance) assistant. It serves as an intelligent orchestrator for 27 compliance agents across 6 categories, plus 11 GDPR & AI Act workflows, covering 25+ regulatory frameworks. Route requests to the appropriate specialist agent, combine insights for cross-framework analysis, and provide consolidated compliance reports.
@@ -4,10 +4,14 @@
4
4
  > **Purpose:** Standardized compliance audit report format
5
5
  > **Version:** 1.0.0
6
6
 
7
+ > ⚠️ **Not legal advice.** Shield output is AI-generated, may be incomplete or out of date, and does not replace a lawyer, DPO or certified auditor. Human review by a qualified person is required before any compliance decision, filing or attestation.
8
+
7
9
  ---
8
10
 
9
11
  ## Compliance Audit Report
10
12
 
13
+ > 🤖 **AI-GENERATED DRAFT — Not legal advice.** Produced with BMAD+ Shield (AI-assisted). Requires review and sign-off by a qualified person (lawyer, DPO or certified auditor) before use. Keep this banner on every copy.
14
+
11
15
  ### Cover Page
12
16
 
13
17
  | Field | Value |
@@ -21,6 +25,8 @@
21
25
  | **Report Date** | [DATE] |
22
26
  | **Prepared by** | [NAME] |
23
27
  | **Classification** | Confidential |
28
+ | **Generation** | AI-generated draft — human review required |
29
+ | **Reviewed by (qualified person)** | [NAME, ROLE, DATE] |
24
30
 
25
31
  ---
26
32
 
@@ -5,6 +5,8 @@
5
5
  > **Version:** 1.0.0
6
6
  > **Created by:** Laurent Rochetta — https://github.com/lrochetta/BMAD-PLUS
7
7
 
8
+ > ⚠️ **Not legal advice.** Shield output is AI-generated, may be incomplete or out of date, and does not replace a lawyer, DPO or certified auditor. Human review by a qualified person is required before any compliance decision, filing or attestation.
9
+
8
10
  ---
9
11
 
10
12
  ## Persona
@@ -216,6 +218,8 @@ Which area? (1-8, or describe your situation)
216
218
  3. Use cross-framework-mapper for overlaps
217
219
  4. Present consolidated report with `shared/audit-report-template.md`
218
220
 
221
+ Every report or assessment you produce MUST keep the template's "AI-generated" marking and the "Not legal advice" disclaimer above.
222
+
219
223
  ### Uncertainty
220
224
  If the framework is ambiguous:
221
225
  1. Ask 2-3 clarifying questions (jurisdiction, industry, data types)
@@ -29,10 +29,11 @@ delivery ─► spec JSON ─► build page ─► deploy to test env ─► a p
29
29
  - Someone asks for "the recette", "the UAT", "what should I check".
30
30
  - A results file appears, or the user says "read the run".
31
31
 
32
- Read `recette.mode` from `_bmad/config.yaml`: `advisory` (default — build and offer the page,
33
- never block), `gate` (the delivery waits for a passing gate), `off` (no recipe; say so in the
34
- delivery report). A delivery with no human-observable change states that fact rather than
35
- skipping in silence.
32
+ Read `uat.mode` from `_bmad/config.yaml`: `advisory` (build and offer the page, never block),
33
+ `gate` (the delivery waits for a passing gate), `off` (no recipe; say so in the delivery report).
34
+ **An absent `uat` block means `advisory`** — a project updated from an earlier version keeps its
35
+ configuration untouched, so the block is simply missing there. A delivery with no
36
+ human-observable change states that fact rather than skipping in silence.
36
37
 
37
38
  ## 1. Write the spec — `_bmad-output/uat/specs/<product>-<version>.json`
38
39
 
@@ -113,6 +114,19 @@ with the figures, the tester's name, and what stays open. The evidence is labell
113
114
  **human-observed**: the gate establishes that the run is complete, current and triaged, never
114
115
  that the tester looked at the right place.
115
116
 
117
+ ## Gating the project itself
118
+
119
+ `templates/` ships two scripts to copy into the project — they need nothing but Node,
120
+ and they are the only layer an agent cannot skip:
121
+
122
+ | Script | Where it belongs | What it refuses |
123
+ |---|---|---|
124
+ | `uat-spec-present.mjs` | next to the project's other checks, before the push | a version in `package.json` that no recipe names. A grouped recipe covers a version because it lists it in `versions`, never because its file name suggests a range |
125
+ | `uat-release-gate.mjs` | the production deploy step | a deployment whose recipe has no finished run, an outdated one, an unclassified failure, or a passed writing step nobody confirmed. Exit 4 means the gate could not run at all — which is not a pass |
126
+
127
+ `templates/README.md` carries the wiring. Tell the user plainly when a project has
128
+ neither: the recipe is then produced and offered, and nothing enforces it.
129
+
116
130
  ## Gating a delivery with Nexus
117
131
 
118
132
  ```bash
@@ -30,6 +30,11 @@
30
30
  --mono-bg: #24282c; --focus: #4fb3b8;
31
31
  }
32
32
  * { box-sizing: border-box; }
33
+ /* A class that sets display wins over the hidden attribute: without this rule the steps
34
+ showed before any run existed, and a tester's answers were silently discarded. */
35
+ [hidden] { display: none !important; }
36
+ code.cmd { cursor: copy; }
37
+ code.cmd.copied::after { content: " \2713 " attr(data-copied); font-family: var(--sans); color: var(--ok); }
33
38
  body { background: var(--bg); color: var(--ink); font-family: var(--sans); font-size: 15px; line-height: 1.5; margin: 0; padding-inline: 16px; padding-block: 0 64px; }
34
39
  h1, h2, h3 { font-family: var(--serif); text-wrap: balance; margin: 0; }
35
40
  h1 { font-size: 1.7rem; font-weight: 600; }
@@ -44,9 +49,15 @@
44
49
  .count { display: flex; gap: 14px; font-variant-numeric: tabular-nums; font-size: 0.9rem; }
45
50
  .count b { font-weight: 600; }
46
51
  .count .ok { color: var(--ok); } .count .ko { color: var(--ko); } .count .warn { color: var(--warn); } .count .todo { color: var(--muted); }
47
- .gauge { flex: 1 1 160px; height: 6px; background: var(--line); border-radius: 3px; overflow: hidden; display: flex; }
48
- .gauge i { display: block; height: 100%; }
49
- .gauge .ok { background: var(--ok); } .gauge .ko { background: var(--ko); } .gauge .warn { background: var(--warn); }
52
+ /* One segment per step: green seen, red not seen, orange blocked, grey skipped, empty to do. */
53
+ .strip { flex: 1 1 100%; display: flex; gap: 3px; min-height: 12px; }
54
+ .strip a { flex: 1 1 0; min-width: 10px; height: 12px; border-radius: 3px; background: var(--line); border: 1px solid transparent; }
55
+ .strip a:hover, .strip a:focus-visible { border-color: var(--ink); outline: none; }
56
+ .strip a[data-state="passed"] { background: var(--ok); }
57
+ .strip a[data-state="failed"] { background: var(--ko); }
58
+ .strip a[data-state="blocked"] { background: var(--warn); }
59
+ .strip a[data-state="skipped"] { background: var(--line-strong); }
60
+ .strip a[data-state="partial"] { background: linear-gradient(90deg, var(--ok) 50%, var(--line) 50%); }
50
61
  .state { font-size: 0.82rem; color: var(--muted); }
51
62
  .state.saved { color: var(--ok); }
52
63
  .head { padding-block: 26px 18px; display: grid; gap: 10px; }
@@ -117,7 +128,7 @@
117
128
  <span class="warn"><b id="c-blocked">0</b> <span id="l-blocked"></span></span>
118
129
  <span class="todo"><b id="c-todo">0</b> <span id="l-todo"></span></span>
119
130
  </div>
120
- <div class="gauge" aria-hidden="true"><i class="ok" id="g-passed" style="width:0%"></i><i class="ko" id="g-failed" style="width:0%"></i><i class="warn" id="g-blocked" style="width:0%"></i></div>
131
+ <nav class="strip" id="strip"></nav>
121
132
  <span class="state" id="save-state">—</span>
122
133
  </div>
123
134
  </div>
@@ -143,9 +154,9 @@
143
154
  <div id="closing" class="section"></div>
144
155
  <label class="note" for="overall"><span id="t-overall"></span><textarea id="overall"></textarea></label>
145
156
  <div class="row">
146
- <button class="btn primary" id="btn-finish" type="button"></button>
147
- <button class="btn" id="btn-save" type="button"></button>
148
- <button class="btn" id="btn-copy" type="button"></button>
157
+ <button class="btn primary" id="btn-finish" type="button" disabled></button>
158
+ <button class="btn" id="btn-save" type="button" disabled></button>
159
+ <button class="btn" id="btn-copy" type="button" disabled></button>
149
160
  <span class="state" id="end-state"></span>
150
161
  </div>
151
162
  <p class="info-box" id="mode-note"></p>
@@ -215,13 +226,14 @@
215
226
  $("btn-finish").textContent = run && run.finishedAt ? T.finished : T.finishRun;
216
227
  $("btn-save").textContent = T.saveJson;
217
228
  $("btn-copy").textContent = T.copyJson;
218
- $("l-passed").textContent = T.seen;
219
- $("l-failed").textContent = T.notSeen;
220
- $("l-blocked").textContent = T.blocked;
229
+ $("l-passed").textContent = T.countSeen;
230
+ $("l-failed").textContent = T.countNotSeen;
231
+ $("l-blocked").textContent = T.countBlocked;
221
232
  $("l-todo").textContent = T.todo;
222
233
  $("mode-note").textContent = dbDoc ? T.dbMode : serve ? T.serveMode : T.localMode;
223
234
  renderHead();
224
235
  renderSteps();
236
+ renderStrip();
225
237
  if (run) reflect();
226
238
  listResumable();
227
239
  }
@@ -321,10 +333,51 @@
321
333
  article.innerHTML = head + body;
322
334
  article.querySelector("h2").textContent = step.title;
323
335
  if (step.duration) article.querySelector(".duration").textContent = step.duration;
336
+ // A command read inside a sentence gets copied with the sentence's punctuation
337
+ // ("doctor." instead of "doctor"): each command in "Do" copies itself exactly.
338
+ Array.prototype.forEach.call(article.querySelectorAll(".section ol code"), function (code) {
339
+ code.classList.add("cmd");
340
+ code.setAttribute("role", "button");
341
+ code.setAttribute("tabindex", "0");
342
+ code.title = T.copyCommand;
343
+ });
324
344
  main.appendChild(article);
325
345
  });
326
346
  }
327
347
 
348
+ function renderStrip() {
349
+ var strip = $("strip");
350
+ strip.innerHTML = "";
351
+ STEPS.forEach(function (step, index) {
352
+ var segment = document.createElement("a");
353
+ segment.id = "s-" + step.id;
354
+ segment.href = "#step-" + step.id;
355
+ segment.title = index + " · " + step.title;
356
+ strip.appendChild(segment);
357
+ });
358
+ }
359
+
360
+ function copyCommand(code) {
361
+ var text = code.textContent.trim();
362
+ var done = function () {
363
+ code.setAttribute("data-copied", T.commandCopied);
364
+ code.classList.add("copied");
365
+ setTimeout(function () { code.classList.remove("copied"); }, 1500);
366
+ };
367
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done, function () {});
368
+ }
369
+
370
+ /** Nothing is recorded before a run exists: say so, instead of discarding answers in silence. */
371
+ function requireRun(event) {
372
+ if (run) return true;
373
+ if (event && event.target instanceof HTMLInputElement && event.target.type === "radio") event.target.checked = false;
374
+ $("startbox").hidden = false;
375
+ $("start-state").textContent = T.startFirst;
376
+ $("startbox").scrollIntoView({ block: "center" });
377
+ $("tester").focus();
378
+ return false;
379
+ }
380
+
328
381
  function reflect() {
329
382
  recount();
330
383
  STEPS.forEach(function (step) {
@@ -351,10 +404,20 @@
351
404
  $("c-failed").textContent = s.failed;
352
405
  $("c-blocked").textContent = s.blocked;
353
406
  $("c-todo").textContent = s.unanswered;
354
- $("g-passed").style.width = (total ? (s.passed / total) * 100 : 0) + "%";
355
- $("g-failed").style.width = (total ? (s.failed / total) * 100 : 0) + "%";
356
- $("g-blocked").style.width = (total ? (s.blocked / total) * 100 : 0) + "%";
357
- $("bar-tester").textContent = run.tester + (run.finishedAt ? " · " + T.finished : "");
407
+ STEPS.forEach(function (step, index) {
408
+ var segment = $("s-" + step.id);
409
+ if (!segment) return;
410
+ var observed = run.steps[step.id];
411
+ var states = Object.keys(observed.expect).map(function (k) { return observed.expect[k].state; });
412
+ var answered = states.filter(function (v) { return v !== null && v !== undefined; }).length;
413
+ var state = observed.state || (answered ? "partial" : "");
414
+ segment.setAttribute("data-state", state);
415
+ segment.title = index + " · " + step.title + " — " + (state === "passed" ? T.seen : state === "failed" ? T.notSeen : state === "blocked" ? T.blocked : state === "skipped" ? T.skip : T.todo) + " (" + answered + "/" + states.length + ")";
416
+ });
417
+ $("strip").setAttribute("aria-label", total + " · " + s.passed + " " + T.seen + " · " + s.failed + " " + T.notSeen + " · " + s.blocked + " " + T.blocked);
418
+ // "Run by laurent · finished on 2026-09-16", as the recipes played in claude.ai always read.
419
+ $("bar-tester").textContent = T.runBy.replace("{tester}", run.tester) +
420
+ (run.finishedAt ? " · " + T.finishedOn.replace("{date}", String(run.finishedAt).slice(0, 10)) : "");
358
421
  var overall = $("overall");
359
422
  if (document.activeElement !== overall) overall.value = run.overallNote || "";
360
423
  $("json").value = JSON.stringify(run, null, 2);
@@ -405,6 +468,8 @@
405
468
  $("steps").hidden = false;
406
469
  $("end").hidden = false;
407
470
  $("bar").hidden = false;
471
+ $("btn-save").disabled = false;
472
+ $("btn-copy").disabled = false;
408
473
  reflect();
409
474
  if (db) {
410
475
  dbDoc = db.doc("recettes/" + id);
@@ -422,7 +487,7 @@
422
487
 
423
488
  function start() {
424
489
  var tester = $("tester").value.trim();
425
- if (!tester) { $("start-state").textContent = T.tester; $("tester").focus(); return; }
490
+ if (!tester) { $("start-state").textContent = T.startFirst; $("tester").focus(); return; }
426
491
  var id = newRunId(tester);
427
492
  open(id, newRun(tester));
428
493
  persist(true);
@@ -457,24 +522,31 @@
457
522
  document.addEventListener("change", function (event) {
458
523
  var target = event.target;
459
524
  if (target === $("lang")) { applyLanguage(target.value); return; }
460
- if (!(target instanceof HTMLInputElement) || target.type !== "radio" || !run) return;
525
+ if (!(target instanceof HTMLInputElement) || target.type !== "radio") return;
461
526
  var m = /^e-(.+)-([a-z])$/.exec(target.name);
462
- if (!m) return;
527
+ if (!m || !requireRun(event)) return;
463
528
  run.steps[m[1]].expect[m[2]].state = target.value;
464
529
  persist();
465
530
  });
466
531
  document.addEventListener("input", function (event) {
467
532
  var target = event.target;
468
- if (!(target instanceof HTMLTextAreaElement) || !run) return;
533
+ if (!(target instanceof HTMLTextAreaElement)) return;
534
+ if (!requireRun(event)) return;
469
535
  if (target === $("overall")) { run.overallNote = target.value; persist(); return; }
470
536
  var step = /^note-(.+)$/.exec(target.id);
471
537
  if (step && run.steps[step[1]]) { run.steps[step[1]].note = target.value; persist(); return; }
472
538
  var expect = /^n-(.+)-([a-z])$/.exec(target.id);
473
539
  if (expect && run.steps[expect[1]]) { run.steps[expect[1]].expect[expect[2]].note = target.value; persist(); }
474
540
  });
541
+ document.addEventListener("keydown", function (event) {
542
+ var code = event.target && event.target.closest && event.target.closest("code.cmd");
543
+ if (code && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); copyCommand(code); }
544
+ });
475
545
  document.addEventListener("click", function (event) {
546
+ var code = event.target && event.target.closest && event.target.closest("code.cmd");
547
+ if (code) { copyCommand(code); return; }
476
548
  var id = event.target && event.target.getAttribute && event.target.getAttribute("data-skip");
477
- if (!id || !run) return;
549
+ if (!id || !requireRun(event)) return;
478
550
  Object.keys(run.steps[id].expect).forEach(function (k) { run.steps[id].expect[k].state = "skipped"; });
479
551
  persist();
480
552
  });
@@ -482,18 +554,22 @@
482
554
  $("btn-start").addEventListener("click", start);
483
555
  $("tester").addEventListener("keydown", function (event) { if (event.key === "Enter") start(); });
484
556
  $("btn-finish").addEventListener("click", function () {
557
+ if (!requireRun()) return;
485
558
  if (run.summary.unanswered > 0 && !window.confirm(T.confirmUnanswered.replace("{n}", run.summary.unanswered))) return;
486
559
  run.finishedAt = new Date().toISOString();
487
560
  persist(true);
488
561
  $("end-state").textContent = T.finished;
489
562
  });
490
563
  $("btn-copy").addEventListener("click", function () {
564
+ if (!requireRun()) return;
491
565
  var text = JSON.stringify(run, null, 2);
492
566
  var fallback = function () { $("json").parentElement.open = true; $("json").focus(); $("json").select(); };
493
567
  if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(function () { $("end-state").textContent = T.copied; }, fallback);
494
568
  else fallback();
495
569
  });
496
570
  $("btn-save").addEventListener("click", function () {
571
+ // Never export a run that does not exist: that is how a tester once saved "null.json".
572
+ if (!requireRun()) return;
497
573
  var name = runId + ".json";
498
574
  var text = JSON.stringify(run, null, 2);
499
575
  if (downloads) {
@@ -33,7 +33,15 @@
33
33
  "savedRemote": "Saved ·",
34
34
  "localMode": "This page is a local file: at the end, save the JSON and hand the file over — that is what the agent reads.",
35
35
  "dbMode": "Your answers are saved as you go; the agent reads them directly, you have nothing to send.",
36
- "serveMode": "Your answers are saved straight into the project as you tick; you have nothing to send."
36
+ "serveMode": "Your answers are saved straight into the project as you tick; you have nothing to send.",
37
+ "startFirst": "Enter your first name and start a run first — nothing is recorded before that.",
38
+ "copyCommand": "Click to copy",
39
+ "commandCopied": "Copied",
40
+ "runBy": "Run by {tester}",
41
+ "finishedOn": "finished on {date}",
42
+ "countSeen": "seen",
43
+ "countNotSeen": "not seen",
44
+ "countBlocked": "blocked"
37
45
  },
38
46
  "fr": {
39
47
  "name": "Français",
@@ -69,7 +77,15 @@
69
77
  "savedRemote": "Enregistré ·",
70
78
  "localMode": "Cette page est un fichier local : à la fin, enregistre le JSON et transmets le fichier — c'est lui que l'agent lit.",
71
79
  "dbMode": "Tes réponses sont enregistrées au fur et à mesure : l'agent les lit directement, tu n'as rien à envoyer.",
72
- "serveMode": "Tes réponses sont enregistrées directement dans le projet à chaque coche ; tu n'as rien à envoyer."
80
+ "serveMode": "Tes réponses sont enregistrées directement dans le projet à chaque coche ; tu n'as rien à envoyer.",
81
+ "startFirst": "Écris ton prénom et commence la recette d’abord — rien n’est enregistré avant.",
82
+ "copyCommand": "Cliquer pour copier",
83
+ "commandCopied": "Copié",
84
+ "runBy": "Recette de {tester}",
85
+ "finishedOn": "terminée le {date}",
86
+ "countSeen": "vus",
87
+ "countNotSeen": "pas vus",
88
+ "countBlocked": "bloqués"
73
89
  },
74
90
  "es": {
75
91
  "name": "Español",
@@ -105,7 +121,15 @@
105
121
  "savedRemote": "Guardado ·",
106
122
  "localMode": "Esta página es un archivo local: al final, guarda el JSON y entrega el archivo — es lo que lee el agente.",
107
123
  "dbMode": "Tus respuestas se guardan sobre la marcha; el agente las lee directamente, no tienes que enviar nada.",
108
- "serveMode": "Tus respuestas se guardan directamente en el proyecto con cada marca; no tienes que enviar nada."
124
+ "serveMode": "Tus respuestas se guardan directamente en el proyecto con cada marca; no tienes que enviar nada.",
125
+ "startFirst": "Escribe tu nombre y empieza la prueba primero: nada se guarda antes.",
126
+ "copyCommand": "Clic para copiar",
127
+ "commandCopied": "Copiado",
128
+ "runBy": "Prueba de {tester}",
129
+ "finishedOn": "terminada el {date}",
130
+ "countSeen": "vistos",
131
+ "countNotSeen": "no vistos",
132
+ "countBlocked": "bloqueados"
109
133
  },
110
134
  "de": {
111
135
  "name": "Deutsch",
@@ -141,7 +165,15 @@
141
165
  "savedRemote": "Gespeichert ·",
142
166
  "localMode": "Diese Seite ist eine lokale Datei: speichere am Ende das JSON und gib die Datei weiter — sie ist es, die der Agent liest.",
143
167
  "dbMode": "Deine Antworten werden laufend gespeichert; der Agent liest sie direkt, du musst nichts senden.",
144
- "serveMode": "Deine Antworten werden bei jedem Klick direkt im Projekt gespeichert; du musst nichts senden."
168
+ "serveMode": "Deine Antworten werden bei jedem Klick direkt im Projekt gespeichert; du musst nichts senden.",
169
+ "startFirst": "Gib zuerst deinen Vornamen ein und starte den Durchlauf — vorher wird nichts gespeichert.",
170
+ "copyCommand": "Zum Kopieren klicken",
171
+ "commandCopied": "Kopiert",
172
+ "runBy": "Durchlauf von {tester}",
173
+ "finishedOn": "abgeschlossen am {date}",
174
+ "countSeen": "gesehen",
175
+ "countNotSeen": "nicht gesehen",
176
+ "countBlocked": "blockiert"
145
177
  },
146
178
  "pt-br": {
147
179
  "name": "Português (Brasil)",
@@ -177,7 +209,15 @@
177
209
  "savedRemote": "Salvo ·",
178
210
  "localMode": "Esta página é um arquivo local: no final, salve o JSON e entregue o arquivo — é ele que o agente lê.",
179
211
  "dbMode": "Suas respostas são salvas conforme você avança; o agente as lê diretamente, você não precisa enviar nada.",
180
- "serveMode": "Suas respostas são salvas direto no projeto a cada marcação; você não precisa enviar nada."
212
+ "serveMode": "Suas respostas são salvas direto no projeto a cada marcação; você não precisa enviar nada.",
213
+ "startFirst": "Digite seu nome e comece a rodada primeiro — nada é salvo antes disso.",
214
+ "copyCommand": "Clique para copiar",
215
+ "commandCopied": "Copiado",
216
+ "runBy": "Rodada de {tester}",
217
+ "finishedOn": "encerrada em {date}",
218
+ "countSeen": "vistos",
219
+ "countNotSeen": "não vistos",
220
+ "countBlocked": "bloqueados"
181
221
  },
182
222
  "ru": {
183
223
  "name": "Русский",
@@ -213,7 +253,15 @@
213
253
  "savedRemote": "Сохранено ·",
214
254
  "localMode": "Эта страница — локальный файл: в конце сохраните JSON и передайте файл — именно его читает агент.",
215
255
  "dbMode": "Ваши ответы сохраняются по ходу; агент читает их напрямую, отправлять ничего не нужно.",
216
- "serveMode": "Ваши ответы сохраняются прямо в проект при каждой отметке; отправлять ничего не нужно."
256
+ "serveMode": "Ваши ответы сохраняются прямо в проект при каждой отметке; отправлять ничего не нужно.",
257
+ "startFirst": "Сначала введите имя и начните прогон — до этого ничего не сохраняется.",
258
+ "copyCommand": "Нажмите, чтобы скопировать",
259
+ "commandCopied": "Скопировано",
260
+ "runBy": "Прогон: {tester}",
261
+ "finishedOn": "завершён {date}",
262
+ "countSeen": "видно",
263
+ "countNotSeen": "не видно",
264
+ "countBlocked": "недоступно"
217
265
  },
218
266
  "zh": {
219
267
  "name": "中文 (简体)",
@@ -249,7 +297,15 @@
249
297
  "savedRemote": "已保存 ·",
250
298
  "localMode": "本页是本地文件:结束时请保存 JSON 并把文件交出去 — 代理读取的正是这个文件。",
251
299
  "dbMode": "你的回答会即时保存,代理直接读取,你无需发送任何东西。",
252
- "serveMode": "你的每次勾选都会直接保存到项目中,你无需发送任何东西。"
300
+ "serveMode": "你的每次勾选都会直接保存到项目中,你无需发送任何东西。",
301
+ "startFirst": "请先输入名字并开始验收——在此之前不会保存任何内容。",
302
+ "copyCommand": "点击复制",
303
+ "commandCopied": "已复制",
304
+ "runBy": "{tester} 的验收",
305
+ "finishedOn": "完成于 {date}",
306
+ "countSeen": "看到",
307
+ "countNotSeen": "没看到",
308
+ "countBlocked": "无法进行"
253
309
  },
254
310
  "he": {
255
311
  "name": "עברית",
@@ -285,7 +341,15 @@
285
341
  "savedRemote": "נשמר ·",
286
342
  "localMode": "הדף הזה הוא קובץ מקומי: בסוף שמור את ה‑JSON והעבר את הקובץ — זה מה שהסוכן קורא.",
287
343
  "dbMode": "התשובות נשמרות תוך כדי, והסוכן קורא אותן ישירות. אין מה לשלוח.",
288
- "serveMode": "כל סימון נשמר ישירות בפרויקט. אין מה לשלוח."
344
+ "serveMode": "כל סימון נשמר ישירות בפרויקט. אין מה לשלוח.",
345
+ "startFirst": "קודם כתוב את שמך והתחל הרצה — לפני כן שום דבר לא נשמר.",
346
+ "copyCommand": "לחץ להעתקה",
347
+ "commandCopied": "הועתק",
348
+ "runBy": "הרצה של {tester}",
349
+ "finishedOn": "הסתיימה ב-{date}",
350
+ "countSeen": "נראו",
351
+ "countNotSeen": "לא נראו",
352
+ "countBlocked": "חסומים"
289
353
  },
290
354
  "ja": {
291
355
  "name": "日本語",
@@ -321,7 +385,15 @@
321
385
  "savedRemote": "保存済み ·",
322
386
  "localMode": "このページはローカルファイルです。最後に JSON を保存してファイルを渡してください — エージェントが読むのはそれです。",
323
387
  "dbMode": "回答は随時保存され、エージェントが直接読みます。送信は不要です。",
324
- "serveMode": "チェックするたびに回答がプロジェクトへ直接保存されます。送信は不要です。"
388
+ "serveMode": "チェックするたびに回答がプロジェクトへ直接保存されます。送信は不要です。",
389
+ "startFirst": "まず名前を入力して実施を開始してください。それまでは何も保存されません。",
390
+ "copyCommand": "クリックしてコピー",
391
+ "commandCopied": "コピーしました",
392
+ "runBy": "{tester} の実施",
393
+ "finishedOn": "{date} に終了",
394
+ "countSeen": "見えた",
395
+ "countNotSeen": "見えない",
396
+ "countBlocked": "実施不可"
325
397
  },
326
398
  "it": {
327
399
  "name": "Italiano",
@@ -357,6 +429,14 @@
357
429
  "savedRemote": "Salvato ·",
358
430
  "localMode": "Questa pagina è un file locale: alla fine salva il JSON e consegna il file — è quello che legge l'agente.",
359
431
  "dbMode": "Le tue risposte vengono salvate man mano; l'agente le legge direttamente, non devi inviare nulla.",
360
- "serveMode": "Le tue risposte vengono salvate direttamente nel progetto a ogni spunta; non devi inviare nulla."
432
+ "serveMode": "Le tue risposte vengono salvate direttamente nel progetto a ogni spunta; non devi inviare nulla.",
433
+ "startFirst": "Scrivi il tuo nome e inizia prima la sessione: prima non viene salvato nulla.",
434
+ "copyCommand": "Clic per copiare",
435
+ "commandCopied": "Copiato",
436
+ "runBy": "Sessione di {tester}",
437
+ "finishedOn": "terminata il {date}",
438
+ "countSeen": "visti",
439
+ "countNotSeen": "non visti",
440
+ "countBlocked": "bloccati"
361
441
  }
362
442
  }
@@ -0,0 +1,63 @@
1
+ # Project gates for human acceptance
2
+
3
+ BMAD+ installs no lifecycle hook, so an instruction can be skipped and a Nexus task
4
+ can be bypassed. These two scripts are the layer that cannot: they run inside your
5
+ own checks and fail the command that calls them. Copy the ones you want into your
6
+ project — for example under `scripts/` — and wire them where they bite.
7
+
8
+ Both need nothing but Node 20 and are safe to read before you run them.
9
+
10
+ ## G1 — a version does not leave without its recipe
11
+
12
+ `uat-spec-present.mjs` fails when no recipe names the version in `package.json`.
13
+
14
+ ```jsonc
15
+ // package.json
16
+ {
17
+ "scripts": {
18
+ "uat:spec": "node scripts/uat-spec-present.mjs",
19
+ "pretest": "npm run uat:spec" // or call it from your CI step
20
+ }
21
+ }
22
+ ```
23
+
24
+ A grouped recipe covers a version because it **names** it in `versions`, never
25
+ because its file name suggests a range. That is deliberate: deducing coverage from
26
+ a range makes a version look checked when nobody wrote anything about it.
27
+
28
+ Where to run it: the same place your tests run, and before the push, not after the
29
+ deployment. A reminder printed once the version is live only records the miss.
30
+
31
+ ## G3 — production waits for a run that was read
32
+
33
+ `uat-release-gate.mjs` resolves the recipe covering the current version and runs
34
+ `bmad-plus uat gate` on it. It exits 0 only when the latest run is finished, answers
35
+ the current revision of the recipe, leaves no expectation unanswered, has every
36
+ failure classified with a decision, and has every passed writing step confirmed
37
+ read-only.
38
+
39
+ ```sh
40
+ # deploy-production.sh
41
+ node scripts/uat-release-gate.mjs || exit 1
42
+ # … deploy only below this line
43
+ ```
44
+
45
+ Exit codes: `0` passed · `1` failed · `2` no finished run yet · `3` the run answered
46
+ another revision of the recipe · `4` the gate itself could not run. The last one is
47
+ not a pass: a missing gate is a missing gate.
48
+
49
+ The script runs the CLI's own entry point with the Node that started it: `BMAD_PLUS_CLI`
50
+ if you set it, otherwise `node_modules/bmad-plus/` if the project installed it. No shell
51
+ and no `npx`: a Windows `.cmd` shim cannot be spawned without a shell, and a release gate
52
+ must not depend on a download. If neither exists it exits 4 and says so.
53
+
54
+ ## What these gates do and do not establish
55
+
56
+ They establish that a recipe exists for the version, and that a human run covering
57
+ it is complete, current and classified. They do not establish that the tester looked
58
+ at the right place, and they do not judge the product. Every report says
59
+ *human-observed* for that reason.
60
+
61
+ A failure classified as a defect of the recipe is not a product fix: amend the spec,
62
+ rebuild it, replay the affected steps, and say so. A failure nobody could classify
63
+ keeps the gate red on purpose — ask the tester what they saw instead of guessing.