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.
- package/CHANGELOG.md +114 -29
- package/README.md +45 -27
- package/SECURITY.md +9 -1
- package/osint-agent-package/README.md +13 -0
- package/osint-agent-package/SETUP_KEYS.md +13 -0
- package/package.json +32 -8
- package/readme-international/README.de.md +134 -537
- package/readme-international/README.es.md +136 -556
- package/readme-international/README.fr.md +135 -553
- package/src/bmad-plus/agents/agent-shadow/SKILL.md +6 -0
- package/src/bmad-plus/packs/pack-memory/README.md +1 -1
- package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +1 -1
- package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +1 -1
- package/src/bmad-plus/packs/pack-shield/README.md +2 -0
- package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -0
- package/src/bmad-plus/packs/pack-shield/shared/audit-report-template.md +6 -0
- package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +4 -0
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +18 -4
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +95 -19
- package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +90 -10
- package/src/bmad-plus/skills/bmad-plus-uat/templates/README.md +63 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/example-uat-spec.json +95 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-release-gate.mjs +127 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-spec-present.mjs +77 -0
- package/tools/build/adapters.config.js +1 -1
- package/tools/build/generate-adapters.js +186 -60
- package/tools/build/generate.js +49 -13
- package/tools/cli/bmad-plus-cli.js +3 -6
- package/tools/cli/commands/autoconfig.js +66 -24
- package/tools/cli/commands/install.js +620 -356
- package/tools/cli/commands/memory.js +40 -13
- package/tools/cli/commands/scan.js +94 -30
- package/tools/cli/commands/uat.js +21 -5
- package/tools/cli/commands/uninstall.js +65 -16
- package/tools/cli/commands/update-check.js +8 -3
- package/tools/cli/commands/update-policy.js +14 -5
- package/tools/cli/commands/update.js +89 -20
- package/tools/cli/i18n.js +128 -66
- package/tools/cli/lib/ide-config.js +3 -1
- package/tools/cli/lib/install-manifest.js +13 -6
- package/tools/cli/lib/installation-health.js +33 -1
- package/tools/cli/lib/installed-adapters.js +29 -6
- package/tools/cli/lib/memory-init.js +19 -4
- package/tools/cli/lib/packs.js +17 -28
- package/tools/cli/lib/path-hash.js +5 -1
- package/tools/cli/lib/python-provision.js +51 -12
- package/tools/cli/lib/stack-detect.js +51 -25
- package/tools/cli/lib/uat.js +18 -0
- package/tools/cli/lib/update-check.js +132 -34
- package/tools/cli/lib/update-dispatch.js +2 -2
- package/tools/cli/lib/update-policy.js +57 -19
- package/tools/cli/lib/update-transaction.js +439 -85
- package/tools/build/README.md +0 -78
- package/tools/build/check-counts.js +0 -626
- package/tools/build/check-install-contract.js +0 -648
- package/tools/build/generated-adapters/.codex/AGENTS.md +0 -134
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +0 -139
- package/tools/build/generated-adapters/.opencode/AGENTS.md +0 -134
- package/tools/build/generated-adapters/AGENTS.md +0 -132
- package/tools/build/generated-adapters/CLAUDE.md +0 -135
- package/tools/build/generated-adapters/CONVENTIONS.md +0 -134
- package/tools/build/generated-adapters/GEMINI.md +0 -139
- package/tools/build/generated-adapters/README.md +0 -79
- package/tools/maintain/upstream-candidate.js +0 -456
- package/tools/release/publication-content.js +0 -903
- 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+
|
|
@@ -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
|
|
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: "
|
|
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 `
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
48
|
-
.
|
|
49
|
-
.
|
|
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
|
-
<
|
|
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.
|
|
219
|
-
$("l-failed").textContent = T.
|
|
220
|
-
$("l-blocked").textContent = T.
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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.
|
|
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"
|
|
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)
|
|
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 || !
|
|
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.
|