Add Bmad-Method files and some brand-design artifacts!

This commit is contained in:
2026-07-06 18:00:53 +01:00
parent 87bff9c251
commit 2b4a6a413c
1806 changed files with 417144 additions and 0 deletions
@@ -0,0 +1,58 @@
---
name: {skill-name}
description: {one-line summary plus the trigger phrases that should route here, e.g. "Use when the user says X or wants to Y"}
---
<!-- BUILDER SCAFFOLD GUIDANCE — DELETE THIS WHOLE COMMENT BLOCK BEFORE SHIPPING.
This is a starting point, not a shape to fill in mechanically. Keep the role
paragraph, the activation block, and whatever the skill actually needs. Cut the
rest. Every surviving line should beat its own absence.
Pick the archetype that matches what you are building and keep only its parts:
- One-shot action. The skill does a single thing and returns. Keep the role
paragraph and a short outcome statement. Drop multi-stage routing, memlog, and
resume. Most skills are this; resist adding more.
- Producer of a durable artifact (brief, PRD, report, deck). Keep memlog as the
process memory, a finalize beat that distills the memlog into the artifact, and
the output-path handling. This is the archetype that earns memlog.
- Multi-intent router. The skill handles a few related jobs behind one entry.
Keep an intent table that routes to references, and name the stages with
descriptive words, never numbered prefixes.
Customization: only add the resolver activation step and reference
{workflow.<name>} values if the author accepted customize.toml. If they declined,
use hardcoded paths and drop the resolver step entirely.
-->
# {skill-name}
{One paragraph stating the destination: the stance the skill acts from, the
outcome it produces, who consumes that output, and the bar that consumer sets.
Write it once; do not restate it lower down.}
## Resolution rules
- Bare paths and `{skill-root}` (e.g. `references/guide.md`) resolve from this skill's installed directory.
- `{project-root}` → the project working directory.
- `{skill-name}` → the skill directory's basename.
## On Activation
1. Load config from `{project-root}/_bmad/config.yaml` (and `.user.yaml` if present). Use sensible defaults for anything missing rather than requiring configuration.
<!-- Keep step 2 only for artifact-producing skills that carry process memory. -->
2. Resume check. Look for an existing `.memlog.md` in the run folder. If one is found, read it once to rebuild state and continue append-only; otherwise initialize a new memlog with `uv run {project-root}/_bmad/scripts/memlog.py init --path <run-folder>/.memlog.md`.
<!-- Keep step 3 only if the author accepted customize.toml. -->
3. Resolve the `workflow` block: run `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. If the script fails, merge these three files yourself in base → team → user order — `{skill-root}/customize.toml`, `{project-root}/_bmad/custom/{skill-name}.toml`, `{project-root}/_bmad/custom/{skill-name}.user.toml` — where scalars override, tables deep-merge, arrays of tables keyed by `code`/`id` replace matching entries and append new ones, and all other arrays append. Reference resolved values as `{workflow.<name>}` everywhere below; never hardcode a path beside a declared scalar.
## {Body}
{The body is whatever the skill needs and nothing more. State each beat as the
outcome you want, reserving exact procedure for the few places a wrong move costs
something. Name stages with descriptive words, never numbered prefixes.}
@@ -0,0 +1,54 @@
# DO NOT EDIT -- overwritten on every update.
#
# Workflow customization surface for {skill-name}.
# Team overrides: {project-root}/_bmad/custom/{skill-name}.toml
# Personal overrides: {project-root}/_bmad/custom/{skill-name}.user.toml
[workflow]
# --- Configurable below. Overrides merge per BMad structural rules: ---
# scalars: override wins • arrays (persistent_facts, activation_steps_*): append
# arrays-of-tables with `code`/`id`: replace matching items, append new ones.
# Steps to run before the standard activation (config load, greet).
# Overrides append. Use for pre-flight loads, compliance checks, etc.
activation_steps_prepend = []
# Steps to run after greet but before Stage 1 of the workflow.
# Overrides append. Use for context-heavy setup that should happen
# once the user has been acknowledged.
activation_steps_append = []
# Persistent facts the workflow keeps in mind for the whole run
# (standards, compliance constraints, stylistic guardrails).
# Distinct from the runtime memory sidecar -- these are static context
# loaded on activation. Overrides append.
#
# Each entry is either:
# - a literal sentence, e.g. "All briefs must include a regulatory-risk section."
# - a file reference prefixed with `file:`, e.g. "file:{project-root}/docs/standards.md"
# (glob patterns are supported; the file's contents are loaded and treated as facts).
persistent_facts = []
# Items that are executed when the workflow reaches its terminal stage, after
# the main output has been delivered. Override wins. Leave empty for
# no custom post-completion behavior.
on_complete = []
# --- Workflow-specific configurables (lifted during Configurability Discovery) ---
#
# Templates, output paths, and hooks the builder surfaced with the author.
# Bare paths resolve from the skill root; use `{project-root}/...` to point
# at an org-owned resource elsewhere in the repo. Override wins.
#
# Naming conventions:
# *_template -- file paths for templates the workflow loads
# *_output_path -- writable destinations
# on_<event> -- additional hook scalars beyond on_complete
#
# Example (from bmad-product-brief):
# brief_template = "resources/brief-template.md"
@@ -0,0 +1,860 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Skill Analysis Report</title>
<style>
:root {
--bg: #0f1b2d;
--panel: #16263d;
--panel-2: #1d3250;
--ink: #e9eef6;
--ink-dim: #9fb0c7;
--line: #294366;
--accent: #b66d46;
--accent-ink: #f4d9c8;
--critical: #e05656;
--high: #e0904a;
--medium: #d8c24a;
--low: #5aa0d0;
--ok: #4caf72;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 28px 20px 80px; }
header h1 { font-size: 22px; margin: 0 0 4px; }
header .meta { color: var(--ink-dim); font-size: 13px; }
header .meta b { color: var(--ink); font-weight: 600; }
.banner {
display: none;
background: #3a1414;
border: 1px solid var(--critical);
color: #ffd9d9;
padding: 14px 16px;
border-radius: 8px;
margin: 16px 0;
white-space: pre-wrap;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 13px;
}
.banner.show { display: block; }
.overview {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 10px;
padding: 18px;
margin: 18px 0;
}
.grade {
font-size: 34px;
font-weight: 800;
margin: 0 0 8px;
text-transform: capitalize;
}
.grade.g-excellent { color: var(--ok); }
.grade.g-good { color: var(--low); }
.grade.g-fair { color: var(--medium); }
.grade.g-poor { color: var(--critical); }
.verdict { font-size: 16px; font-weight: 600; margin: 0 0 14px; }
.summary { color: var(--ink-dim); margin: 0 0 14px; }
.counts { display: flex; flex-wrap: wrap; gap: 10px; }
.pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
background: var(--panel-2);
border: 1px solid var(--line);
font-size: 13px;
}
.pill .dot { width: 10px; height: 10px; border-radius: 50%; }
.pill .n { font-weight: 700; }
.dot.critical { background: var(--critical); }
.dot.high { background: var(--high); }
.dot.medium { background: var(--medium); }
.dot.low { background: var(--low); }
/* Generic synthesis/agent-block panel */
.block {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 10px;
padding: 18px;
margin: 18px 0;
}
.block > h2 {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ink-dim);
margin: 0 0 12px;
}
.block .mono, .block code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 13px;
background: var(--panel-2);
padding: 1px 5px;
border-radius: 4px;
}
.kv { margin: 0; display: grid; grid-template-columns: 150px 1fr; gap: 6px 14px; }
.kv dt { color: var(--ink-dim); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
.kv dd { margin: 0; }
/* Themes */
.theme { padding: 12px 0; border-top: 1px solid var(--line); }
.theme:first-of-type { border-top: none; padding-top: 0; }
.theme .t-head { display: flex; align-items: center; gap: 10px; }
.theme .t-title { font-weight: 600; flex: 1 1 auto; min-width: 0; }
.theme .t-cause { color: var(--ink-dim); margin-top: 4px; }
.theme .t-action { margin-top: 4px; }
.theme .t-findings { margin-top: 8px; padding-left: 12px; border-left: 2px solid var(--line); }
.theme .t-finding { font-size: 13px; color: var(--ink-dim); padding: 2px 0; }
/* Strengths */
.strength-list { margin: 0; padding-left: 20px; }
.strength-list li { padding: 2px 0; }
/* Recommendations */
.rec { padding: 8px 0; border-top: 1px solid var(--line); }
.rec:first-of-type { border-top: none; padding-top: 0; }
.rec .rank { font-weight: 700; color: var(--accent-ink); margin-right: 8px; }
.rec .resolves { color: var(--ink-dim); font-size: 12px; margin-left: 8px; }
/* Experience journeys */
.block .journey { padding: 8px 0; border-top: 1px solid var(--line); }
.block .journey:first-of-type { border-top: none; padding-top: 0; }
.block .journey .j-name { font-weight: 600; }
.block .journey .j-steps { color: var(--ink-dim); margin-top: 2px; }
.toolbar {
display: flex;
align-items: center;
gap: 12px;
margin: 18px 0 10px;
flex-wrap: wrap;
}
.toolbar .sel-count { color: var(--ink-dim); font-size: 13px; }
button {
font: inherit;
cursor: pointer;
border-radius: 8px;
border: 1px solid var(--line);
background: var(--panel-2);
color: var(--ink);
padding: 8px 14px;
}
button.primary {
background: var(--accent);
border-color: var(--accent);
color: #1a0e07;
font-weight: 600;
}
button:disabled { opacity: 0.5; cursor: default; }
button.link {
background: none;
border: none;
color: var(--accent-ink);
padding: 4px 6px;
font-size: 13px;
}
button.small { padding: 5px 10px; font-size: 13px; flex: 0 0 auto; }
.no-findings {
background: var(--panel);
border: 1px dashed var(--line);
border-radius: 10px;
padding: 28px;
text-align: center;
color: var(--ink-dim);
}
.no-findings .big { font-size: 18px; color: var(--ok); margin-bottom: 6px; }
.group { margin: 18px 0; }
.group > h2 {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ink-dim);
margin: 0 0 8px;
display: flex;
align-items: center;
gap: 8px;
}
.finding {
background: var(--panel);
border: 1px solid var(--line);
border-left: 4px solid var(--line);
border-radius: 8px;
margin: 8px 0;
overflow: hidden;
}
.finding.sev-critical { border-left-color: var(--critical); }
.finding.sev-high { border-left-color: var(--high); }
.finding.sev-medium { border-left-color: var(--medium); }
.finding.sev-low { border-left-color: var(--low); }
.finding .row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
}
.finding .row .chk { width: 16px; height: 16px; flex: 0 0 auto; cursor: pointer; }
.finding .row .head { flex: 1 1 auto; cursor: pointer; min-width: 0; }
.finding .row .title { font-weight: 600; }
.finding .row .sub { color: var(--ink-dim); font-size: 12px; margin-top: 2px; }
.finding .tag {
flex: 0 0 auto;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 3px 8px;
border-radius: 6px;
background: var(--panel-2);
border: 1px solid var(--line);
color: var(--ink-dim);
}
.finding .caret { flex: 0 0 auto; color: var(--ink-dim); transition: transform 0.15s; cursor: pointer; }
.finding.open .caret { transform: rotate(90deg); }
.finding .body {
display: none;
padding: 0 14px 14px 42px;
border-top: 1px solid var(--line);
}
.finding.open .body { display: block; }
.finding .body dl { margin: 12px 0 0; display: grid; grid-template-columns: 130px 1fr; gap: 6px 14px; }
.finding .body dt { color: var(--ink-dim); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
.finding .body dd { margin: 0; }
.finding .body code, .finding .body .mono {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 13px;
background: var(--panel-2);
padding: 1px 5px;
border-radius: 4px;
}
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translateX(-50%);
background: var(--ok);
color: #06160c;
padding: 10px 18px;
border-radius: 8px;
font-weight: 600;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.toast.show { opacity: 1; }
.fallback-area { margin-top: 12px; display: none; }
.fallback-area.show { display: block; }
.fallback-area textarea {
width: 100%;
min-height: 160px;
background: var(--panel-2);
color: var(--ink);
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 13px;
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Skill Analysis Report</h1>
<div class="meta">
<span>Subject: <b id="m-subject"></b></span> &nbsp;·&nbsp;
<span>Generated: <b id="m-generated"></b></span> &nbsp;·&nbsp;
<span>Schema: <b id="m-schema"></b></span>
</div>
</header>
<div id="parse-banner" class="banner"></div>
<section id="overview" class="overview" hidden>
<div id="grade" class="grade" hidden></div>
<p id="verdict" class="verdict"></p>
<p id="summary-text" class="summary" hidden></p>
<div id="counts" class="counts"></div>
</section>
<!-- Synthesis layer (themes, strengths, recommendations). Hidden when absent. -->
<section id="themes" class="block" hidden></section>
<section id="strengths" class="block" hidden></section>
<section id="recommendations" class="block" hidden></section>
<!-- Experience: journeys plus headless (experience). Hidden when absent. -->
<section id="experience" class="block" hidden></section>
<div id="toolbar" class="toolbar" hidden>
<button id="btn-copy" class="primary" disabled>Copy selected as paste-back prompt</button>
<span id="sel-count" class="sel-count">0 selected</span>
<button id="btn-select-all" class="link">Select all</button>
<button id="btn-clear" class="link">Clear</button>
<button id="btn-expand-all" class="link">Expand all</button>
<button id="btn-collapse-all" class="link">Collapse all</button>
</div>
<div id="fallback" class="fallback-area">
<p class="sel-count">Clipboard was unavailable. Copy the text below manually:</p>
<textarea id="fallback-text" readonly></textarea>
</div>
<div id="findings-root"></div>
</div>
<div id="toast" class="toast">Copied</div>
<!-- scripts/render_report.py replaces the contents of this island per run.
The placeholder below is intentionally unusable: the shell refuses to
render it, so a failed injection can never look like real findings. -->
<script type="application/json" id="report-data">
{
"schema_version": 2,
"subject": "__PLACEHOLDER__",
"generated": "",
"verdict": "",
"findings": []
}
</script>
<script>
(function () {
"use strict";
var SEVERITIES = ["critical", "high", "medium", "low"];
var SEV_LABEL = { critical: "Critical", high: "High", medium: "Medium", low: "Low" };
var GRADES = ["excellent", "good", "fair", "poor"];
var PLACEHOLDER_SUBJECT = "__PLACEHOLDER__";
var els = {
banner: document.getElementById("parse-banner"),
overview: document.getElementById("overview"),
grade: document.getElementById("grade"),
verdict: document.getElementById("verdict"),
summaryText: document.getElementById("summary-text"),
counts: document.getElementById("counts"),
themes: document.getElementById("themes"),
strengths: document.getElementById("strengths"),
recommendations: document.getElementById("recommendations"),
experience: document.getElementById("experience"),
toolbar: document.getElementById("toolbar"),
root: document.getElementById("findings-root"),
subject: document.getElementById("m-subject"),
generated: document.getElementById("m-generated"),
schema: document.getElementById("m-schema"),
selCount: document.getElementById("sel-count"),
btnCopy: document.getElementById("btn-copy"),
btnSelectAll: document.getElementById("btn-select-all"),
btnClear: document.getElementById("btn-clear"),
btnExpandAll: document.getElementById("btn-expand-all"),
btnCollapseAll: document.getElementById("btn-collapse-all"),
fallback: document.getElementById("fallback"),
fallbackText: document.getElementById("fallback-text"),
toast: document.getElementById("toast")
};
var selected = Object.create(null);
var findings = [];
var findingsById = Object.create(null);
var subjectPath = "";
var standards = null;
function showBanner(message) {
els.banner.textContent = message;
els.banner.classList.add("show");
}
function esc(value) {
var s = value == null ? "" : String(value);
return s.replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}
// Normalize an arbitrary parsed object against schema_version 2, supplying
// defaults so a partial or future island still renders. Unknown fields are
// ignored, not fatal. Severity counts are always derived from the findings
// array, never read from the island, so they cannot disagree with it. The
// synthesis blocks (grade, summary, themes, strengths, recommendations) and
// the experience block are OPTIONAL: each normalizes to an empty value that
// renders nothing rather than an empty panel or an error.
function normalize(raw) {
var obj = raw && typeof raw === "object" ? raw : {};
var rawFindings = Array.isArray(obj.findings) ? obj.findings : [];
var norm = {
schema_version: typeof obj.schema_version === "number" ? obj.schema_version : 2,
subject: obj.subject != null ? String(obj.subject) : "(unspecified)",
generated: obj.generated != null ? String(obj.generated) : "(unspecified)",
verdict: obj.verdict != null ? String(obj.verdict) : "(no verdict supplied)",
grade: GRADES.indexOf(String(obj.grade || "").toLowerCase()) >= 0
? String(obj.grade).toLowerCase() : "",
summary: typeof obj.summary === "string" ? obj.summary : "",
standards: (obj.standards && typeof obj.standards === "object") ? {
canon: obj.standards.canon != null ? String(obj.standards.canon) : "",
principles: obj.standards.principles != null ? String(obj.standards.principles) : "",
scripts: obj.standards.scripts != null ? String(obj.standards.scripts) : ""
} : null,
themes: normalizeThemes(obj.themes),
strengths: normalizeStrengths(obj.strengths),
recommendations: normalizeRecommendations(obj.recommendations),
experience: normalizeExperience(obj.experience),
counts: { critical: 0, high: 0, medium: 0, low: 0 },
findings: []
};
rawFindings.forEach(function (f, i) {
if (!f || typeof f !== "object") { return; }
var sev = SEVERITIES.indexOf(f.severity) >= 0 ? f.severity : "low";
norm.findings.push({
id: f.id != null ? String(f.id) : "finding-" + (i + 1),
lens: f.lens != null ? String(f.lens) : "(unknown)",
severity: sev,
title: f.title != null ? String(f.title) : "(untitled finding)",
location: f.location != null ? String(f.location) : "",
evidence: f.evidence != null ? String(f.evidence) : "",
recommendation: f.recommendation != null ? String(f.recommendation) : "",
proposed_smallest: f.proposed_smallest != null ? String(f.proposed_smallest) : "",
predicted_delta: f.predicted_delta != null ? String(f.predicted_delta) : ""
});
norm.counts[sev] += 1;
});
return norm;
}
function normalizeThemes(raw) {
if (!Array.isArray(raw)) { return []; }
var list = [];
raw.forEach(function (t) {
if (!t || typeof t !== "object") { return; }
var ids = [];
if (Array.isArray(t.finding_ids)) {
t.finding_ids.forEach(function (id) { if (id != null) { ids.push(String(id)); } });
}
var title = t.title != null ? String(t.title) : "";
if (!title && !ids.length) { return; }
list.push({
title: title || "(untitled theme)",
root_cause: t.root_cause != null ? String(t.root_cause) : "",
action: t.action != null ? String(t.action) : "",
finding_ids: ids
});
});
return list;
}
function normalizeStrengths(raw) {
if (!Array.isArray(raw)) { return []; }
var list = [];
raw.forEach(function (s) {
if (typeof s === "string" && s) { list.push(s); }
else if (s && typeof s === "object" && s.title) {
list.push(String(s.title) + (s.detail ? " — " + String(s.detail) : ""));
}
});
return list;
}
function normalizeRecommendations(raw) {
if (!Array.isArray(raw)) { return []; }
var list = [];
raw.forEach(function (r, i) {
if (!r || typeof r !== "object") { return; }
var action = r.action != null ? String(r.action) : "";
if (!action) { return; }
var resolves = "";
if (Array.isArray(r.resolves)) { resolves = r.resolves.map(String).join(", "); }
else if (typeof r.resolves === "number") { resolves = r.resolves + " findings"; }
else if (r.resolves != null) { resolves = String(r.resolves); }
list.push({
rank: typeof r.rank === "number" ? r.rank : i + 1,
action: action,
resolves: resolves
});
});
list.sort(function (a, b) { return a.rank - b.rank; });
return list;
}
// Optional experience block: journeys plus a headless note. Returns null when
// neither is usable.
function normalizeExperience(raw) {
if (!raw || typeof raw !== "object") { return null; }
var journeys = [];
if (Array.isArray(raw.journeys)) {
raw.journeys.forEach(function (j) {
if (!j || typeof j !== "object") { return; }
var name = j.name != null ? String(j.name) : "";
var steps = j.steps != null ? String(j.steps) : "";
if (!name && !steps) { return; }
journeys.push({ name: name, steps: steps });
});
}
var headless = raw.headless != null ? String(raw.headless) : "";
if (!journeys.length && !headless) { return null; }
return { journeys: journeys, headless: headless };
}
function renderOverview(data) {
els.subject.textContent = data.subject;
els.generated.textContent = data.generated;
els.schema.textContent = String(data.schema_version);
els.verdict.textContent = data.verdict;
if (data.grade) {
els.grade.textContent = data.grade;
els.grade.className = "grade g-" + data.grade;
els.grade.hidden = false;
}
if (data.summary) {
els.summaryText.textContent = data.summary;
els.summaryText.hidden = false;
}
els.counts.innerHTML = "";
SEVERITIES.forEach(function (s) {
var pill = document.createElement("span");
pill.className = "pill";
pill.innerHTML =
'<span class="dot ' + s + '"></span>' +
'<span class="lbl">' + SEV_LABEL[s] + '</span>' +
'<span class="n">' + data.counts[s] + "</span>";
els.counts.appendChild(pill);
});
els.overview.hidden = false;
}
// Every copied fix prompt opens by anchoring the fixing session to the same
// standards that produced the findings, so the fix is held to the bar too.
function standardsPreamble() {
if (!standards || !standards.canon) { return []; }
var bar = standards.canon + (standards.principles ? " and " + standards.principles : "");
var lines = [
"Hold " + bar + " as the bar for every line you change — a fix that adds ceremony is a new finding, not a fix."
];
if (standards.scripts) {
lines.push("If the fix adds or changes scripts, follow " + standards.scripts + ".");
}
lines.push("");
return lines;
}
function composeThemePrompt(theme, resolved) {
var lines = standardsPreamble();
lines.push("Fix the following theme in " + subjectPath + ": " + theme.title);
lines.push("");
if (theme.root_cause) { lines.push("Root cause: " + theme.root_cause); }
if (theme.action) { lines.push("Fix: " + theme.action); }
if (resolved.length) {
lines.push("");
lines.push("Findings to address:");
resolved.forEach(function (f, i) {
lines.push((i + 1) + ". " + f.title);
if (f.location) { lines.push(" Location: " + f.location); }
if (f.evidence) { lines.push(" Evidence: " + f.evidence); }
if (f.recommendation) { lines.push(" Recommendation: " + f.recommendation); }
});
}
return lines.join("\n") + "\n";
}
function renderThemes(themes) {
if (!themes.length) { els.themes.hidden = true; return; }
els.themes.innerHTML = "<h2>Themes</h2>";
themes.forEach(function (t) {
var resolved = t.finding_ids
.map(function (id) { return findingsById[id]; })
.filter(function (f) { return !!f; });
var items = resolved.map(function (f) {
return '<div class="t-finding"><span class="mono">' + esc(f.id) + "</span> " +
esc(f.title) +
(f.location ? ' · <span class="mono">' + esc(f.location) + "</span>" : "") +
"</div>";
}).join("");
var node = document.createElement("div");
node.className = "theme";
node.innerHTML =
'<div class="t-head"><span class="t-title">' + esc(t.title) + "</span>" +
'<button class="small t-fix">Fix This Theme</button></div>' +
(t.root_cause ? '<div class="t-cause">Root cause: ' + esc(t.root_cause) + "</div>" : "") +
(t.action ? '<div class="t-action"><b>Fix:</b> ' + esc(t.action) + "</div>" : "") +
(items ? '<div class="t-findings">' + items + "</div>" : "");
node.querySelector(".t-fix").addEventListener("click", function () {
copyText(composeThemePrompt(t, resolved));
});
els.themes.appendChild(node);
});
els.themes.hidden = false;
}
function renderStrengths(list) {
if (!list.length) { els.strengths.hidden = true; return; }
els.strengths.innerHTML =
"<h2>Strengths</h2><ul class=\"strength-list\">" +
list.map(function (s) { return "<li>" + esc(s) + "</li>"; }).join("") +
"</ul>";
els.strengths.hidden = false;
}
function renderRecommendations(recs) {
if (!recs.length) { els.recommendations.hidden = true; return; }
var html = "<h2>Recommendations</h2>";
recs.forEach(function (r) {
html += '<div class="rec"><span class="rank">#' + esc(String(r.rank)) + "</span>" +
esc(r.action) +
(r.resolves ? '<span class="resolves">resolves: ' + esc(r.resolves) + "</span>" : "") +
"</div>";
});
els.recommendations.innerHTML = html;
els.recommendations.hidden = false;
}
function renderExperience(exp) {
if (!exp) { els.experience.hidden = true; return; }
var html = "<h2>Experience</h2>";
if (exp.journeys.length) {
html += exp.journeys.map(function (j) {
return '<div class="journey">' +
'<div class="j-name">' + esc(j.name || "(unnamed journey)") + "</div>" +
(j.steps ? '<div class="j-steps">' + esc(j.steps) + "</div>" : "") +
"</div>";
}).join("");
}
if (exp.headless) {
html += '<dl class="kv" style="margin-top:12px"><dt>Headless</dt><dd>' +
esc(exp.headless) + "</dd></dl>";
}
els.experience.innerHTML = html;
els.experience.hidden = false;
}
function renderNoFindings() {
els.root.innerHTML =
'<div class="no-findings">' +
'<div class="big">No findings</div>' +
"<div>The scanners returned a clean pass for this subject.</div>" +
"</div>";
}
function findingNode(f) {
var node = document.createElement("div");
node.className = "finding sev-" + f.severity;
node.setAttribute("data-id", f.id);
var sub =
esc(f.lens) +
(f.location ? ' · <span class="mono">' + esc(f.location) + "</span>" : "");
var rows =
"<dt>Lens</dt><dd>" + esc(f.lens) + "</dd>" +
(f.location ? "<dt>Location</dt><dd><code>" + esc(f.location) + "</code></dd>" : "") +
(f.evidence ? "<dt>Evidence</dt><dd>" + esc(f.evidence) + "</dd>" : "") +
(f.recommendation ? "<dt>Recommendation</dt><dd>" + esc(f.recommendation) + "</dd>" : "") +
(f.proposed_smallest ? "<dt>Proposed smallest</dt><dd>" + esc(f.proposed_smallest) + "</dd>" : "") +
(f.predicted_delta ? "<dt>Predicted delta</dt><dd>" + esc(f.predicted_delta) + "</dd>" : "");
node.innerHTML =
'<div class="row">' +
'<input type="checkbox" class="chk" aria-label="Select finding">' +
'<div class="head">' +
'<div class="title">' + esc(f.title) + "</div>" +
'<div class="sub">' + sub + "</div>" +
"</div>" +
'<span class="tag">' + SEV_LABEL[f.severity] + "</span>" +
'<span class="caret">▸</span>' +
"</div>" +
'<div class="body"><dl>' + rows + "</dl></div>";
var chk = node.querySelector(".chk");
chk.checked = !!selected[f.id];
chk.addEventListener("change", function () {
if (chk.checked) { selected[f.id] = true; } else { delete selected[f.id]; }
updateSelection();
});
var head = node.querySelector(".head");
var caret = node.querySelector(".caret");
function toggle() { node.classList.toggle("open"); }
head.addEventListener("click", toggle);
caret.addEventListener("click", toggle);
return node;
}
function renderFindings(list) {
els.root.innerHTML = "";
if (list.length === 0) {
renderNoFindings();
els.toolbar.hidden = true;
return;
}
els.toolbar.hidden = false;
SEVERITIES.forEach(function (sev) {
var group = list.filter(function (f) { return f.severity === sev; });
if (group.length === 0) { return; }
var wrap = document.createElement("div");
wrap.className = "group";
var h = document.createElement("h2");
h.innerHTML = '<span class="dot ' + sev + '"></span>' + SEV_LABEL[sev] + " (" + group.length + ")";
wrap.appendChild(h);
group.forEach(function (f) { wrap.appendChild(findingNode(f)); });
els.root.appendChild(wrap);
});
}
function updateSelection() {
var n = Object.keys(selected).length;
els.selCount.textContent = n + " selected";
els.btnCopy.disabled = n === 0;
}
function composePrompt() {
var picked = findings.filter(function (f) { return selected[f.id]; });
if (picked.length === 0) { return ""; }
var lines = standardsPreamble();
lines.push("Fix the following issues in " + subjectPath + ":");
lines.push("");
picked.forEach(function (f, i) {
lines.push((i + 1) + ". " + f.title);
if (f.location) { lines.push(" Location: " + f.location); }
if (f.evidence) { lines.push(" Evidence: " + f.evidence); }
if (f.recommendation) { lines.push(" Recommendation: " + f.recommendation); }
if (f.proposed_smallest) { lines.push(" Proposed smallest: " + f.proposed_smallest); }
lines.push("");
});
return lines.join("\n").replace(/\n+$/, "\n");
}
function showToast(text) {
els.toast.textContent = text;
els.toast.classList.add("show");
setTimeout(function () { els.toast.classList.remove("show"); }, 1600);
}
function fallbackCopy(text) {
els.fallbackText.value = text;
els.fallback.classList.add("show");
els.fallbackText.focus();
els.fallbackText.select();
try {
var ok = document.execCommand && document.execCommand("copy");
if (ok) {
showToast("Copied");
return;
}
} catch (e) { /* fall through to manual */ }
showToast("Copy the text shown below");
}
function copyText(text) {
if (!text) { return; }
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(
function () { showToast("Copied"); },
function () { fallbackCopy(text); }
);
} else {
fallbackCopy(text);
}
}
function doCopy() {
copyText(composePrompt());
}
function wireToolbar() {
els.btnCopy.addEventListener("click", doCopy);
els.btnSelectAll.addEventListener("click", function () {
findings.forEach(function (f) { selected[f.id] = true; });
document.querySelectorAll(".finding .chk").forEach(function (c) { c.checked = true; });
updateSelection();
});
els.btnClear.addEventListener("click", function () {
selected = Object.create(null);
document.querySelectorAll(".finding .chk").forEach(function (c) { c.checked = false; });
els.fallback.classList.remove("show");
updateSelection();
});
els.btnExpandAll.addEventListener("click", function () {
document.querySelectorAll(".finding").forEach(function (n) { n.classList.add("open"); });
});
els.btnCollapseAll.addEventListener("click", function () {
document.querySelectorAll(".finding").forEach(function (n) { n.classList.remove("open"); });
});
}
function init() {
var island = document.getElementById("report-data");
var parsed;
try {
if (!island) { throw new Error("report-data island element not found"); }
parsed = JSON.parse(island.textContent);
} catch (err) {
showBanner(
"Could not parse the report data island.\n\n" +
"Error: " + (err && err.message ? err.message : String(err)) + "\n\n" +
"The findings could not be rendered. The JSON inside the " +
'report-data island (the application/json script tag) is malformed.'
);
return;
}
var data = normalize(parsed);
if (data.subject === PLACEHOLDER_SUBJECT) {
els.subject.textContent = data.subject;
showBanner(
"This is the unfilled report shell.\n\n" +
"The report-data island still carries the placeholder subject, so " +
"there are no findings here. Generate a real report with " +
"scripts/render_report.py."
);
return;
}
findings = data.findings;
subjectPath = data.subject;
standards = data.standards;
findingsById = Object.create(null);
findings.forEach(function (f) { findingsById[f.id] = f; });
renderOverview(data);
renderThemes(data.themes);
renderStrengths(data.strengths);
renderRecommendations(data.recommendations);
renderExperience(data.experience);
renderFindings(findings);
wireToolbar();
updateSelection();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
</script>
</body>
</html>
@@ -0,0 +1,66 @@
# DO NOT EDIT -- overwritten on every update.
#
# Workflow customization surface for bmad-product-brief.
#
# Override files (not edited here):
# {project-root}/_bmad/custom/bmad-product-brief.toml (team)
# {project-root}/_bmad/custom/bmad-product-brief.user.toml (personal)
[workflow]
# --- Configurable below. Overrides merge per BMad structural rules: ---
# scalars: override wins • arrays: append
# Steps to run before the standard activation (config load, greet).
# Use for pre-flight loads, compliance checks, etc.
activation_steps_prepend = []
# Steps to run after greet but before the workflow begins.
# Use for context-heavy setup that should happen once the user has been acknowledged.
activation_steps_append = []
# Persistent facts the workflow keeps in mind for the whole run
# (standards, compliance constraints, stylistic guardrails).
# Each entry is either a literal sentence, a skill prefixed with `skill:`, or a `file:`-prefixed path/glob
# whose contents are loaded as facts.
# Default is empty. Common opt-ins (set in your team/user override TOML):
# "file:{project-root}/_bmad-output/planning-artifacts/project-context.md" # bmad-generate-project-context output
# "skill:acme-co:terms-and-conditions" # a skill that contains some relevant info to the documents that may be generated
# "Elvis has left the building" # generic agent instructions
persistent_facts = []
# Executed when the workflow completes (after the user has been told the
# brief is ready). Accepts either a string scalar (single instruction)
# or an array of instructions executed in order. Empty for none.
on_complete = ""
# Default brief structure. Treated as a starting point — the LLM adapts it
# to the product, purpose, and domain. Override the path in team/user TOML
# to enforce a different structure (e.g. regulated-industry, investor-deck).
brief_template = "assets/brief-template.md"
# Run folder location. The brief, optional addendum, and optional distillate
# all land inside `{output_dir}/{output_folder_name}/`.
output_dir = "{planning_artifacts}/briefs"
output_folder_name = "brief-{project_name}-{date}"
# Document standards applied to human-consumed docs at finalize. Each entry is
# a `skill:`, `file:`, or plain-text directive; the parent LLM applies the
# findings before the user sees the draft. Encodes standards, not options.
#
# Examples:
# "skill:bmad-editorial-review-prose"
# "file:{project-root}/_bmad/style-guides/company-voice.md"
# "Convert all dates to ISO 8601 format."
#
# Suggested order (broader passes first, narrower last):
# 1. Structural (cuts, reorganization, section sizing)
# 2. Content/voice/conventions (org standards, tone, terminology, compliance)
# 3. Prose mechanics (grammar, clarity, typos)
#
# Override the array in team/user TOML to add additional standards. Append-only:
# base entries cannot be removed or replaced (resolver has no removal mechanism).
doc_standards = [
"skill:bmad-editorial-review-structure",
"skill:bmad-editorial-review-prose",
]