bmad-method 6.10.1-next.2 → 6.10.1-next.21
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/.claude-plugin/marketplace.json +52 -27
- package/package.json +4 -3
- package/removals.txt +5 -0
- package/src/bmm-skills/1-analysis/bmad-agent-analyst/customize.toml +19 -4
- package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +1 -1
- package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +5 -3
- package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +2 -0
- package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +5 -3
- package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/SKILL.md +18 -3
- package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/assets/spec-template.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-spec/assets/stories-schema.md +44 -0
- package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/customize.toml +3 -4
- package/src/bmm-skills/2-plan-workflows/bmad-ux/customize.toml +4 -2
- package/src/bmm-skills/3-solutioning/bmad-architecture/SKILL.md +2 -2
- package/src/bmm-skills/3-solutioning/bmad-architecture/customize.toml +4 -2
- package/src/bmm-skills/4-implementation/bmad-agent-dev/customize.toml +3 -0
- package/src/bmm-skills/4-implementation/bmad-code-review/SKILL.md +2 -4
- package/src/bmm-skills/4-implementation/bmad-code-review/customize.toml +52 -0
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-01-gather-context.md +0 -1
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-02-review.md +10 -24
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-03-triage.md +3 -4
- package/src/bmm-skills/4-implementation/bmad-dev-auto/SKILL.md +33 -12
- package/src/bmm-skills/4-implementation/bmad-dev-auto/customize.toml +75 -0
- package/src/bmm-skills/4-implementation/bmad-dev-auto/spec-template.md +4 -4
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-01-clarify-and-route.md +22 -4
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-02-plan.md +5 -6
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-03-implement.md +8 -4
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-04-review.md +13 -19
- package/src/bmm-skills/4-implementation/bmad-dev-story/customize.toml +3 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/SKILL.md +6 -108
- package/src/bmm-skills/4-implementation/bmad-quick-dev/customize.toml +73 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/render.py +381 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/spec-template.md +2 -2
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md +11 -13
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-02-plan.md +4 -9
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-03-implement.md +12 -4
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-04-review.md +8 -20
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-05-present.md +4 -4
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +12 -13
- package/src/bmm-skills/4-implementation/bmad-quick-dev/sync-sprint-status.md +6 -6
- package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +85 -0
- package/src/bmm-skills/module-help.csv +2 -4
- package/src/bmm-skills/module.yaml +2 -5
- package/src/bmm-skills/v6-shims/README.md +24 -0
- package/src/bmm-skills/{3-solutioning → v6-shims}/bmad-create-architecture/SKILL.md +1 -1
- package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-create-prd/SKILL.md +3 -3
- package/src/bmm-skills/v6-shims/bmad-domain-research/SKILL.md +14 -0
- package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-edit-prd/SKILL.md +3 -3
- package/src/bmm-skills/v6-shims/bmad-market-research/SKILL.md +14 -0
- package/src/bmm-skills/v6-shims/bmad-technical-research/SKILL.md +14 -0
- package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-validate-prd/SKILL.md +3 -3
- package/src/core-skills/bmad-advanced-elicitation/SKILL.md +26 -103
- package/src/core-skills/bmad-advanced-elicitation/customize.toml +54 -0
- package/src/core-skills/bmad-advanced-elicitation/scripts/pick_methods.py +233 -0
- package/src/core-skills/bmad-advanced-elicitation/scripts/tests/test_pick_methods.py +228 -0
- package/src/core-skills/bmad-brainstorming/SKILL.md +3 -3
- package/src/core-skills/bmad-brainstorming/assets/brain-selector.html +2 -0
- package/src/core-skills/bmad-brainstorming/references/mode-autonomous.md +1 -1
- package/src/core-skills/bmad-brainstorming/scripts/brain.py +36 -6
- package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +22 -0
- package/src/core-skills/bmad-customize/SKILL.md +2 -2
- package/src/core-skills/bmad-deep-recon/SKILL.md +82 -0
- package/src/core-skills/bmad-deep-recon/assets/research.template.md +18 -0
- package/src/core-skills/bmad-deep-recon/customize.toml +212 -0
- package/src/core-skills/bmad-deep-recon/references/draft.md +8 -0
- package/src/core-skills/bmad-deep-recon/references/finalize.md +11 -0
- package/src/core-skills/bmad-deep-recon/references/html-briefing.md +16 -0
- package/src/core-skills/bmad-deep-recon/references/lifecycle.md +11 -0
- package/src/core-skills/bmad-deep-recon/references/process.md +10 -0
- package/src/core-skills/bmad-deep-recon/references/run.md +73 -0
- package/src/core-skills/bmad-deep-recon/references/selection.md +13 -0
- package/src/core-skills/bmad-deep-recon/references/synthesis.md +16 -0
- package/src/core-skills/bmad-deep-recon/references/verification.md +29 -0
- package/src/core-skills/bmad-deep-recon/scripts/recon_kit.py +322 -0
- package/src/core-skills/bmad-deep-recon/scripts/tests/test_recon_kit.py +144 -0
- package/src/core-skills/bmad-deep-recon/types/academic-lit.md +19 -0
- package/src/core-skills/bmad-deep-recon/types/competitive.md +19 -0
- package/src/core-skills/bmad-deep-recon/types/domain.md +19 -0
- package/src/core-skills/bmad-deep-recon/types/market.md +19 -0
- package/src/core-skills/bmad-deep-recon/types/technical.md +19 -0
- package/src/core-skills/bmad-deep-recon/types/user-voice.md +19 -0
- package/src/core-skills/bmad-forge-idea/SKILL.md +2 -2
- package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +4 -1
- package/src/core-skills/bmad-help/SKILL.md +1 -1
- package/src/core-skills/bmad-party-mode/SKILL.md +2 -2
- package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +11 -3
- package/src/core-skills/bmad-review/SKILL.md +49 -0
- package/src/core-skills/bmad-review/customize.toml +141 -0
- package/src/core-skills/bmad-review/references/editorial-common.md +56 -0
- package/src/core-skills/bmad-review/references/lens-adversarial.md +18 -0
- package/src/core-skills/bmad-review/references/lens-edge-case-hunter.md +54 -0
- package/src/core-skills/bmad-review/references/lens-prose.md +7 -0
- package/src/core-skills/bmad-review/references/lens-structure.md +9 -0
- package/src/core-skills/{bmad-review-verification-gap/SKILL.md → bmad-review/references/lens-verification-gap.md} +15 -39
- package/src/core-skills/bmad-review/references/structure-models.md +44 -0
- package/src/core-skills/bmad-review/scripts/tests/test_word_metrics.py +62 -0
- package/src/core-skills/bmad-review/scripts/word_metrics.py +102 -0
- package/src/core-skills/module-help.csv +3 -7
- package/src/core-skills/module.yaml +5 -0
- package/src/core-skills/v6-shims/README.md +25 -0
- package/src/core-skills/v6-shims/bmad-editorial-review/SKILL.md +6 -0
- package/src/core-skills/v6-shims/bmad-editorial-review/customize.toml +31 -0
- package/src/core-skills/v6-shims/bmad-editorial-review-prose/SKILL.md +6 -0
- package/src/core-skills/v6-shims/bmad-editorial-review-structure/SKILL.md +6 -0
- package/src/core-skills/v6-shims/bmad-review-adversarial-general/SKILL.md +6 -0
- package/src/core-skills/v6-shims/bmad-review-edge-case-hunter/SKILL.md +6 -0
- package/src/core-skills/v6-shims/bmad-review-verification-gap/SKILL.md +6 -0
- package/tools/installer/ide/platform-codes.yaml +7 -0
- package/tools/installer/modules/custom-module-manager.js +12 -6
- package/tools/installer/modules/external-manager.js +12 -8
- package/tools/installer/modules/git-env.js +47 -0
- package/tools/skill-validator.md +11 -1
- package/tools/validate-skills.js +33 -0
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/SKILL.md +0 -96
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/customize.toml +0 -41
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-01-init.md +0 -137
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-02-domain-analysis.md +0 -229
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-03-competitive-landscape.md +0 -238
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-04-regulatory-focus.md +0 -206
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-05-technical-trends.md +0 -234
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-06-research-synthesis.md +0 -450
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/research.template.md +0 -29
- package/src/bmm-skills/1-analysis/research/bmad-market-research/SKILL.md +0 -96
- package/src/bmm-skills/1-analysis/research/bmad-market-research/customize.toml +0 -41
- package/src/bmm-skills/1-analysis/research/bmad-market-research/research.template.md +0 -29
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-01-init.md +0 -184
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-02-customer-behavior.md +0 -239
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-03-customer-pain-points.md +0 -251
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-04-customer-decisions.md +0 -261
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-05-competitive-analysis.md +0 -173
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-06-research-completion.md +0 -484
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/SKILL.md +0 -96
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/customize.toml +0 -41
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/research.template.md +0 -29
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-01-init.md +0 -137
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-02-technical-overview.md +0 -239
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-03-integration-patterns.md +0 -248
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-04-architectural-patterns.md +0 -202
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-05-implementation-research.md +0 -233
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-06-research-synthesis.md +0 -493
- package/src/core-skills/bmad-brainstorming/analysis/catalog-analysis.md +0 -239
- package/src/core-skills/bmad-brainstorming/analysis/method-matrix.csv +0 -109
- package/src/core-skills/bmad-editorial-review-prose/SKILL.md +0 -86
- package/src/core-skills/bmad-editorial-review-structure/SKILL.md +0 -179
- package/src/core-skills/bmad-index-docs/SKILL.md +0 -66
- package/src/core-skills/bmad-review-adversarial-general/SKILL.md +0 -37
- package/src/core-skills/bmad-review-edge-case-hunter/SKILL.md +0 -73
- package/src/core-skills/bmad-review-edge-case-hunter/references/deletion-check.md +0 -14
- package/src/core-skills/bmad-shard-doc/SKILL.md +0 -105
- /package/src/{core-skills → bmm-skills/2-plan-workflows}/bmad-spec/assets/headless-schemas.md +0 -0
- /package/src/bmm-skills/{3-solutioning → v6-shims}/bmad-create-architecture/customize.toml +0 -0
- /package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-create-prd/customize.toml +0 -0
- /package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-edit-prd/customize.toml +0 -0
- /package/src/bmm-skills/{2-plan-workflows → v6-shims}/bmad-validate-prd/customize.toml +0 -0
- /package/src/core-skills/bmad-advanced-elicitation/{methods.csv → assets/methods.csv} +0 -0
- /package/src/core-skills/bmad-party-mode/scripts/tests/{test-resolve_party.py → test_resolve_party.py} +0 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# ///
|
|
5
|
+
"""recon_kit — deterministic helpers for bmad-deep-recon.
|
|
6
|
+
|
|
7
|
+
The mechanical half of the research workflow: everything here is exact,
|
|
8
|
+
repeatable work the LLM should never re-derive by hand. All subcommands
|
|
9
|
+
print one JSON object to stdout; diagnostics go to stderr. Exit codes:
|
|
10
|
+
0 = pass, 1 = findings that need attention, 2 = usage/parse error.
|
|
11
|
+
|
|
12
|
+
Subcommands:
|
|
13
|
+
citations RESEARCH_MD
|
|
14
|
+
Cross-check inline [n] markers against the source-appendix table:
|
|
15
|
+
dangling markers (no appendix row) and orphaned rows (never cited).
|
|
16
|
+
tally MEMLOG_MD
|
|
17
|
+
Count memlog entries by type, and claim entries by status.
|
|
18
|
+
Claim lines carry `status=<word>` and optionally `ref=[n]`; for a
|
|
19
|
+
given ref the LAST status wins, so status changes are appends.
|
|
20
|
+
staleness CLAIMS_JSON --windows JSON [--today YYYY-MM-DD]
|
|
21
|
+
Given claims [{claim, class, pub_date}] and a months-per-class map
|
|
22
|
+
(e.g. '{"size/growth": 18, "pricing": 3}'), compute each claim's
|
|
23
|
+
re-check date, flag stale ones, and report the earliest re-check.
|
|
24
|
+
slug TOPIC --type TYPE [--pattern P] [--date YYYY-MM-DD]
|
|
25
|
+
Expand the run-folder pattern deterministically so the same topic
|
|
26
|
+
always lands in the same folder across draft -> process -> refresh.
|
|
27
|
+
escape-sources RESEARCH_MD
|
|
28
|
+
Emit the source-appendix table as HTML with every cell escaped and
|
|
29
|
+
only validated http(s) URLs turned into links, for the briefing.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import calendar
|
|
35
|
+
import html
|
|
36
|
+
import json
|
|
37
|
+
import re
|
|
38
|
+
import sys
|
|
39
|
+
import unicodedata
|
|
40
|
+
from datetime import date, datetime
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from urllib.parse import urlparse
|
|
43
|
+
|
|
44
|
+
MARKER_RE = re.compile(r"\[(\d+)\](?!\()") # [3] but not a [3](url) link
|
|
45
|
+
MD_LINK_RE = re.compile(r"\[([^\]]*)\]\((\S+?)\)")
|
|
46
|
+
BARE_URL_RE = re.compile(r"https?://[^\s|)\]]+")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def out(payload: dict, exit_code: int) -> int:
|
|
50
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False, default=str))
|
|
51
|
+
return exit_code
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_text(path_arg: str) -> str:
|
|
55
|
+
if path_arg == "-":
|
|
56
|
+
return sys.stdin.read()
|
|
57
|
+
return Path(path_arg).read_text(encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def strip_fences(text: str) -> str:
|
|
61
|
+
"""Blank out fenced code blocks so their contents never count as markers or rows."""
|
|
62
|
+
lines, fenced = [], False
|
|
63
|
+
for ln in text.splitlines():
|
|
64
|
+
if ln.lstrip().startswith("```"):
|
|
65
|
+
fenced = not fenced
|
|
66
|
+
lines.append("")
|
|
67
|
+
continue
|
|
68
|
+
lines.append("" if fenced else ln)
|
|
69
|
+
return "\n".join(lines)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def table_cells(line: str) -> list[str]:
|
|
73
|
+
return [c.strip() for c in line.strip().strip("|").split("|")]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def appendix_rows(text: str) -> dict[int, list[str]]:
|
|
77
|
+
"""Source-appendix rows: markdown table rows whose first cell is a bare [n] / n."""
|
|
78
|
+
rows: dict[int, list[str]] = {}
|
|
79
|
+
for ln in text.splitlines():
|
|
80
|
+
stripped = ln.strip()
|
|
81
|
+
if not stripped.startswith("|"):
|
|
82
|
+
continue
|
|
83
|
+
cells = table_cells(stripped)
|
|
84
|
+
if not cells or len(cells) < 2:
|
|
85
|
+
continue
|
|
86
|
+
m = re.fullmatch(r"\[?(\d+)\]?", cells[0])
|
|
87
|
+
if m:
|
|
88
|
+
rows[int(m.group(1))] = cells
|
|
89
|
+
return rows
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# --- citations ---------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def cmd_citations(args) -> int:
|
|
95
|
+
text = strip_fences(read_text(args.file))
|
|
96
|
+
rows = appendix_rows(text)
|
|
97
|
+
markers: set[int] = set()
|
|
98
|
+
for ln in text.splitlines():
|
|
99
|
+
stripped = ln.strip()
|
|
100
|
+
if stripped.startswith("|"):
|
|
101
|
+
cells = table_cells(stripped)
|
|
102
|
+
if cells and re.fullmatch(r"\[?(\d+)\]?", cells[0]):
|
|
103
|
+
continue # an appendix row is not a citation of itself
|
|
104
|
+
markers.update(int(n) for n in MARKER_RE.findall(ln))
|
|
105
|
+
dangling = sorted(markers - set(rows))
|
|
106
|
+
orphaned = sorted(set(rows) - markers)
|
|
107
|
+
ok = not dangling and not orphaned
|
|
108
|
+
return out({
|
|
109
|
+
"markers": sorted(markers),
|
|
110
|
+
"appendix_rows": sorted(rows),
|
|
111
|
+
"dangling_markers": dangling,
|
|
112
|
+
"orphaned_rows": orphaned,
|
|
113
|
+
"ok": ok,
|
|
114
|
+
}, 0 if ok else 1)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# --- tally -------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
ENTRY_RE = re.compile(r"^- (?:\(([\w-]+)(?: by [^)]*)?\)\s*)?(.*)$")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def cmd_tally(args) -> int:
|
|
123
|
+
text = read_text(args.file)
|
|
124
|
+
body = text.split("---", 2)[-1] if text.startswith("---") else text
|
|
125
|
+
by_type: dict[str, int] = {}
|
|
126
|
+
by_ref: dict[int, str] = {}
|
|
127
|
+
unref_status: dict[str, int] = {}
|
|
128
|
+
entries = 0
|
|
129
|
+
for ln in body.splitlines():
|
|
130
|
+
m = ENTRY_RE.match(ln)
|
|
131
|
+
if not m or not ln.startswith("- "):
|
|
132
|
+
continue
|
|
133
|
+
entries += 1
|
|
134
|
+
etype = m.group(1) or "note"
|
|
135
|
+
by_type[etype] = by_type.get(etype, 0) + 1
|
|
136
|
+
if etype == "claim":
|
|
137
|
+
status_m = re.search(r"status=([\w-]+)", m.group(2))
|
|
138
|
+
status = status_m.group(1) if status_m else "unknown"
|
|
139
|
+
ref_m = re.search(r"ref=\[?(\d+)\]?", m.group(2))
|
|
140
|
+
if ref_m:
|
|
141
|
+
by_ref[int(ref_m.group(1))] = status # last status wins per ref
|
|
142
|
+
else:
|
|
143
|
+
unref_status[status] = unref_status.get(status, 0) + 1
|
|
144
|
+
claims: dict[str, int] = dict(unref_status)
|
|
145
|
+
for status in by_ref.values():
|
|
146
|
+
claims[status] = claims.get(status, 0) + 1
|
|
147
|
+
return out({
|
|
148
|
+
"entries": entries,
|
|
149
|
+
"by_type": dict(sorted(by_type.items())),
|
|
150
|
+
"claims": dict(sorted(claims.items())),
|
|
151
|
+
"claims_total": sum(claims.values()),
|
|
152
|
+
}, 0)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# --- staleness ---------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def parse_date(raw: str) -> date:
|
|
158
|
+
raw = raw.strip()
|
|
159
|
+
for fmt in ("%Y-%m-%d", "%Y-%m", "%Y"):
|
|
160
|
+
try:
|
|
161
|
+
return datetime.strptime(raw, fmt).date()
|
|
162
|
+
except ValueError:
|
|
163
|
+
continue
|
|
164
|
+
raise ValueError(f"unparseable date: {raw!r} (want YYYY[-MM[-DD]])")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def add_months(d: date, months: int) -> date:
|
|
168
|
+
total = d.month - 1 + months
|
|
169
|
+
year, month = d.year + total // 12, total % 12 + 1
|
|
170
|
+
return date(year, month, min(d.day, calendar.monthrange(year, month)[1]))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def cmd_staleness(args) -> int:
|
|
174
|
+
try:
|
|
175
|
+
payload = json.loads(read_text(args.file))
|
|
176
|
+
windows = {k.lower(): int(v) for k, v in json.loads(args.windows).items()}
|
|
177
|
+
today = parse_date(args.today) if args.today else date.today()
|
|
178
|
+
except (ValueError, json.JSONDecodeError) as e:
|
|
179
|
+
print(f"error: {e}", file=sys.stderr)
|
|
180
|
+
return 2
|
|
181
|
+
claims = payload["claims"] if isinstance(payload, dict) else payload
|
|
182
|
+
results, no_window, stale_count = [], set(), 0
|
|
183
|
+
earliest: date | None = None
|
|
184
|
+
for c in claims:
|
|
185
|
+
cls = str(c.get("class", "")).lower()
|
|
186
|
+
try:
|
|
187
|
+
pub = parse_date(str(c["pub_date"]))
|
|
188
|
+
except (KeyError, ValueError) as e:
|
|
189
|
+
print(f"error in claim {c!r}: {e}", file=sys.stderr)
|
|
190
|
+
return 2
|
|
191
|
+
months = windows.get(cls)
|
|
192
|
+
if months is None:
|
|
193
|
+
no_window.add(cls)
|
|
194
|
+
results.append({**c, "recheck": None, "stale": None})
|
|
195
|
+
continue
|
|
196
|
+
recheck = add_months(pub, months)
|
|
197
|
+
stale = recheck <= today
|
|
198
|
+
stale_count += stale
|
|
199
|
+
earliest = recheck if earliest is None or recheck < earliest else earliest
|
|
200
|
+
results.append({**c, "recheck": recheck.isoformat(), "stale": stale})
|
|
201
|
+
return out({
|
|
202
|
+
"today": today.isoformat(),
|
|
203
|
+
"claims": results,
|
|
204
|
+
"stale_count": stale_count,
|
|
205
|
+
"earliest_recheck": earliest.isoformat() if earliest else None,
|
|
206
|
+
"no_window_classes": sorted(no_window),
|
|
207
|
+
}, 1 if stale_count else 0)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# --- slug --------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
def slugify(text: str, max_len: int = 40) -> str:
|
|
213
|
+
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode()
|
|
214
|
+
text = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
|
|
215
|
+
return re.sub(r"-{2,}", "-", text)[:max_len].rstrip("-")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def cmd_slug(args) -> int:
|
|
219
|
+
slug = slugify(args.topic)
|
|
220
|
+
if not slug:
|
|
221
|
+
print("error: topic slugified to an empty string", file=sys.stderr)
|
|
222
|
+
return 2
|
|
223
|
+
folder = (args.pattern
|
|
224
|
+
.replace("{research_type}", args.type)
|
|
225
|
+
.replace("{topic_slug}", slug)
|
|
226
|
+
.replace("{date}", args.date or date.today().isoformat()))
|
|
227
|
+
return out({"topic_slug": slug, "folder": folder}, 0)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# --- escape-sources ----------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
def safe_url(raw: str) -> str | None:
|
|
233
|
+
parsed = urlparse(raw)
|
|
234
|
+
return raw if parsed.scheme in ("http", "https") and parsed.netloc else None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def cell_html(cell: str, invalid: list[str]) -> str:
|
|
238
|
+
"""Escape a cell; a markdown link or bare URL becomes an <a> only when http(s)."""
|
|
239
|
+
link = MD_LINK_RE.search(cell)
|
|
240
|
+
if link:
|
|
241
|
+
url = safe_url(link.group(2))
|
|
242
|
+
label = html.escape(link.group(1) or link.group(2))
|
|
243
|
+
if url:
|
|
244
|
+
return html.escape(cell[:link.start()]) + \
|
|
245
|
+
f'<a href="{html.escape(url, quote=True)}" target="_blank" rel="noopener">{label}</a>' + \
|
|
246
|
+
html.escape(cell[link.end():])
|
|
247
|
+
invalid.append(link.group(2))
|
|
248
|
+
return html.escape(cell.replace(link.group(0), link.group(1) or link.group(2)))
|
|
249
|
+
bare = BARE_URL_RE.search(cell)
|
|
250
|
+
if bare:
|
|
251
|
+
url = safe_url(bare.group(0))
|
|
252
|
+
if url:
|
|
253
|
+
escaped = html.escape(url, quote=True)
|
|
254
|
+
return html.escape(cell[:bare.start()]) + \
|
|
255
|
+
f'<a href="{escaped}" target="_blank" rel="noopener">{escaped}</a>' + \
|
|
256
|
+
html.escape(cell[bare.end():])
|
|
257
|
+
invalid.append(bare.group(0))
|
|
258
|
+
return html.escape(cell)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def cmd_escape_sources(args) -> int:
|
|
262
|
+
text = strip_fences(read_text(args.file))
|
|
263
|
+
rows = appendix_rows(text)
|
|
264
|
+
if not rows:
|
|
265
|
+
print("error: no source-appendix table rows found", file=sys.stderr)
|
|
266
|
+
return 2
|
|
267
|
+
invalid: list[str] = []
|
|
268
|
+
body_rows = []
|
|
269
|
+
for n in sorted(rows):
|
|
270
|
+
cells = rows[n]
|
|
271
|
+
tds = "".join(f"<td>{cell_html(c, invalid)}</td>" for c in cells[1:])
|
|
272
|
+
body_rows.append(f'<tr id="src-{n}"><td>[{n}]</td>{tds}</tr>')
|
|
273
|
+
table = ('<table class="sources"><tbody>' + "".join(body_rows) + "</tbody></table>")
|
|
274
|
+
return out({"rows": len(rows), "invalid_urls": invalid, "html": table},
|
|
275
|
+
1 if invalid else 0)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# --- entry point -------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
def main(argv: list[str] | None = None) -> int:
|
|
281
|
+
p = argparse.ArgumentParser(description=__doc__,
|
|
282
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
283
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
284
|
+
|
|
285
|
+
pc = sub.add_parser("citations", help="cross-check [n] markers vs the source appendix")
|
|
286
|
+
pc.add_argument("file", help="path to research.md (or - for stdin)")
|
|
287
|
+
pc.set_defaults(func=cmd_citations)
|
|
288
|
+
|
|
289
|
+
pt = sub.add_parser("tally", help="count memlog entries by type and claims by status")
|
|
290
|
+
pt.add_argument("file", help="path to .memlog.md (or - for stdin)")
|
|
291
|
+
pt.set_defaults(func=cmd_tally)
|
|
292
|
+
|
|
293
|
+
ps = sub.add_parser("staleness", help="compute re-check dates from freshness windows")
|
|
294
|
+
ps.add_argument("file", help="claims JSON: [{claim, class, pub_date}] (or - for stdin)")
|
|
295
|
+
ps.add_argument("--windows", required=True,
|
|
296
|
+
help='JSON months-per-class map, e.g. \'{"pricing": 3}\'')
|
|
297
|
+
ps.add_argument("--today", help="override today's date (YYYY-MM-DD)")
|
|
298
|
+
ps.set_defaults(func=cmd_staleness)
|
|
299
|
+
|
|
300
|
+
pg = sub.add_parser("slug", help="expand the run-folder pattern deterministically")
|
|
301
|
+
pg.add_argument("topic", help="research topic text")
|
|
302
|
+
pg.add_argument("--type", required=True, help="research type code (e.g. market)")
|
|
303
|
+
pg.add_argument("--pattern", default="{research_type}-{topic_slug}-{date}",
|
|
304
|
+
help="folder pattern (default: {research_type}-{topic_slug}-{date})")
|
|
305
|
+
pg.add_argument("--date", help="override date (YYYY-MM-DD; default today)")
|
|
306
|
+
pg.set_defaults(func=cmd_slug)
|
|
307
|
+
|
|
308
|
+
pe = sub.add_parser("escape-sources",
|
|
309
|
+
help="source appendix as escaped HTML with validated links")
|
|
310
|
+
pe.add_argument("file", help="path to research.md (or - for stdin)")
|
|
311
|
+
pe.set_defaults(func=cmd_escape_sources)
|
|
312
|
+
|
|
313
|
+
args = p.parse_args(argv)
|
|
314
|
+
try:
|
|
315
|
+
return args.func(args)
|
|
316
|
+
except FileNotFoundError as e:
|
|
317
|
+
print(f"error: {e}", file=sys.stderr)
|
|
318
|
+
return 2
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
sys.exit(main())
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# ///
|
|
5
|
+
"""Tests for recon_kit.py."""
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import unittest
|
|
11
|
+
from contextlib import redirect_stdout
|
|
12
|
+
from datetime import date
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
16
|
+
|
|
17
|
+
from recon_kit import add_months, main, parse_date, slugify
|
|
18
|
+
|
|
19
|
+
REPORT = """---
|
|
20
|
+
title: 'market research: widgets'
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Report
|
|
24
|
+
|
|
25
|
+
The market is growing fast [1] and pricing clusters at $20 [2].
|
|
26
|
+
An uncited assertion sits here [4].
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
[9] inside a fence never counts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Source appendix
|
|
33
|
+
|
|
34
|
+
| [n] | Supports | Publisher | Pub date | Accessed | Confidence |
|
|
35
|
+
| --- | --- | --- | --- | --- | --- |
|
|
36
|
+
| [1] | market growth | [Gartner](https://example.com/g) | 2026-01 | 2026-07-01 | high |
|
|
37
|
+
| [2] | pricing | [Acme](javascript:alert(1)) | 2026-05 | 2026-07-01 | medium |
|
|
38
|
+
| [3] | never cited | https://example.com/x | 2025-11 | 2026-07-01 | low |
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
MEMLOG = """---
|
|
42
|
+
topic: widgets
|
|
43
|
+
updated: 2026-07-22T10:00
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
- (decision) plan approved
|
|
47
|
+
- (source) round 1 batch: 4 sources
|
|
48
|
+
- (claim) ref=[1] status=verified class=size/growth pub=2026-01 — market growing 12% CAGR
|
|
49
|
+
- (claim) ref=[2] status=unverified class=pricing pub=2026-05 — pricing clusters at $20
|
|
50
|
+
- (claim) ref=[2] status=verified class=pricing pub=2026-05 — confirmed by second source
|
|
51
|
+
- (claim) status=unverified class=behavior pub=2025-03 — users churn at day 8
|
|
52
|
+
- (event) dimension 1 complete
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run(argv):
|
|
57
|
+
buf = io.StringIO()
|
|
58
|
+
with redirect_stdout(buf):
|
|
59
|
+
code = main(argv)
|
|
60
|
+
return code, json.loads(buf.getvalue())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CitationsTest(unittest.TestCase):
|
|
64
|
+
def test_cross_check(self):
|
|
65
|
+
report = Path(__file__).parent / "_report.md"
|
|
66
|
+
report.write_text(REPORT, encoding="utf-8")
|
|
67
|
+
try:
|
|
68
|
+
code, result = run(["citations", str(report)])
|
|
69
|
+
finally:
|
|
70
|
+
report.unlink()
|
|
71
|
+
self.assertEqual(result["dangling_markers"], [4])
|
|
72
|
+
self.assertEqual(result["orphaned_rows"], [3])
|
|
73
|
+
self.assertNotIn(9, result["markers"]) # fenced content ignored
|
|
74
|
+
self.assertEqual(code, 1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TallyTest(unittest.TestCase):
|
|
78
|
+
def test_last_status_wins_per_ref(self):
|
|
79
|
+
log = Path(__file__).parent / "_memlog.md"
|
|
80
|
+
log.write_text(MEMLOG, encoding="utf-8")
|
|
81
|
+
try:
|
|
82
|
+
code, result = run(["tally", str(log)])
|
|
83
|
+
finally:
|
|
84
|
+
log.unlink()
|
|
85
|
+
self.assertEqual(result["by_type"]["claim"], 4)
|
|
86
|
+
self.assertEqual(result["claims"], {"unverified": 1, "verified": 2})
|
|
87
|
+
self.assertEqual(result["claims_total"], 3) # ref=[2] counted once
|
|
88
|
+
self.assertEqual(code, 0)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class StalenessTest(unittest.TestCase):
|
|
92
|
+
def test_dates(self):
|
|
93
|
+
self.assertEqual(parse_date("2026-01"), date(2026, 1, 1))
|
|
94
|
+
self.assertEqual(add_months(date(2026, 1, 31), 1), date(2026, 2, 28))
|
|
95
|
+
|
|
96
|
+
def test_windows(self):
|
|
97
|
+
claims = json.dumps([
|
|
98
|
+
{"claim": "sizing", "class": "size/growth", "pub_date": "2024-06"},
|
|
99
|
+
{"claim": "pricing", "class": "pricing", "pub_date": "2026-06"},
|
|
100
|
+
{"claim": "odd", "class": "unmapped", "pub_date": "2026-06"},
|
|
101
|
+
])
|
|
102
|
+
f = Path(__file__).parent / "_claims.json"
|
|
103
|
+
f.write_text(claims, encoding="utf-8")
|
|
104
|
+
try:
|
|
105
|
+
code, result = run([
|
|
106
|
+
"staleness", str(f),
|
|
107
|
+
"--windows", '{"size/growth": 18, "pricing": 3}',
|
|
108
|
+
"--today", "2026-07-22",
|
|
109
|
+
])
|
|
110
|
+
finally:
|
|
111
|
+
f.unlink()
|
|
112
|
+
self.assertEqual(result["stale_count"], 1) # sizing recheck 2025-12 < today
|
|
113
|
+
self.assertEqual(result["earliest_recheck"], "2025-12-01")
|
|
114
|
+
self.assertEqual(result["no_window_classes"], ["unmapped"])
|
|
115
|
+
self.assertEqual(code, 1)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class SlugTest(unittest.TestCase):
|
|
119
|
+
def test_deterministic_folder(self):
|
|
120
|
+
self.assertEqual(slugify("Créme Brûlée: AI Tools!"), "creme-brulee-ai-tools")
|
|
121
|
+
code, result = run(["slug", "SMB Accounting SaaS", "--type", "market",
|
|
122
|
+
"--date", "2026-07-22"])
|
|
123
|
+
self.assertEqual(result["folder"], "market-smb-accounting-saas-2026-07-22")
|
|
124
|
+
self.assertEqual(code, 0)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class EscapeSourcesTest(unittest.TestCase):
|
|
128
|
+
def test_escaping_and_url_validation(self):
|
|
129
|
+
report = Path(__file__).parent / "_report.md"
|
|
130
|
+
report.write_text(REPORT, encoding="utf-8")
|
|
131
|
+
try:
|
|
132
|
+
code, result = run(["escape-sources", str(report)])
|
|
133
|
+
finally:
|
|
134
|
+
report.unlink()
|
|
135
|
+
self.assertEqual(result["rows"], 3)
|
|
136
|
+
self.assertTrue(any(u.startswith("javascript:") for u in result["invalid_urls"]))
|
|
137
|
+
self.assertNotIn("javascript:", result["html"]) # never linked
|
|
138
|
+
self.assertIn('href="https://example.com/g"', result["html"])
|
|
139
|
+
self.assertIn('id="src-1"', result["html"])
|
|
140
|
+
self.assertEqual(code, 1)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
unittest.main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Academic Literature Pack
|
|
2
|
+
|
|
3
|
+
Serves: ground an approach in published research, scan the state of the art, run a defensible literature review, cite properly in technical writing.
|
|
4
|
+
|
|
5
|
+
**Dimensions (priority order — prune to the decision):**
|
|
6
|
+
|
|
7
|
+
1. The canon — seminal papers and the best recent surveys of the area
|
|
8
|
+
2. State of the art — current best results, benchmarks, and how they're measured
|
|
9
|
+
3. Methods & limitations — what the leading approaches assume and where they break
|
|
10
|
+
4. Open problems & live debates — what the field disagrees about right now
|
|
11
|
+
5. Who works on this — the labs and groups whose output to watch
|
|
12
|
+
|
|
13
|
+
**Craft (the non-obvious):** find one good survey before reading twenty abstracts; chase citations both directions — who they cite and who cites them (Semantic Scholar/Google Scholar); label preprint vs peer-reviewed on every citation — arXiv is not acceptance; take benchmark numbers from the original paper, never from a competitor's comparison table; check retraction and replication status on any load-bearing empirical claim; a result only ever shown by one lab is a lead, not a fact.
|
|
14
|
+
|
|
15
|
+
**Freshness:** state-of-the-art claims ≤ 12 mo (ML ≤ 6 mo) · seminal work has no freshness bar — but check whether it was since superseded.
|
|
16
|
+
|
|
17
|
+
**Two-source classes:** any empirical claim a conclusion rests on — independent replication or corroboration, not the same lab twice.
|
|
18
|
+
|
|
19
|
+
**Feeds (bmm):** technical and architecture bets · content and writing that cites · build-vs-adopt judgments on research-grade techniques.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Competitive Research Pack
|
|
2
|
+
|
|
3
|
+
Serves: position against *named* competitors, build battlecards, sharpen differentiation, anticipate their next move. (Surveying an unnamed field is the market type; this pack is for teardowns of specific players.)
|
|
4
|
+
|
|
5
|
+
**Dimensions (priority order — prune to the decision):**
|
|
6
|
+
|
|
7
|
+
1. Offer & feature teardown — what they actually ship, tried directly where possible
|
|
8
|
+
2. Pricing & packaging — models, tiers, what changed recently and which direction
|
|
9
|
+
3. Positioning & messaging — who they claim to serve, the story they tell, the gap between claim and product
|
|
10
|
+
4. Trajectory — funding, hiring, release cadence: where they're headed
|
|
11
|
+
5. Their customers' voice — what users of *their* product praise and complain about
|
|
12
|
+
|
|
13
|
+
**Craft (the non-obvious):** their changelog and release notes are roadmap truth; job postings reveal strategy six months early; their customers' 1–3★ reviews are your wedge; archived pricing pages (Wayback) show pricing direction, not just position; sales-facing comparison pages overclaim — verify capability claims against their docs; try the product yourself when a trial exists — an hour in-product beats ten reviews.
|
|
14
|
+
|
|
15
|
+
**Freshness:** pricing & features ≤ 3 mo · trajectory signals ≤ 6 mo · customer sentiment ≤ 12 mo.
|
|
16
|
+
|
|
17
|
+
**Two-source classes:** traction and market-share claims; any capability claim of theirs that your differentiation rests on.
|
|
18
|
+
|
|
19
|
+
**Feeds (bmm):** brief (alternatives) · PRD (differentiation) · GTM battlecards and positioning.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Domain Research Pack
|
|
2
|
+
|
|
3
|
+
Serves: commit to building in an industry, talk credibly with domain experts, scope a product for a regulated field, brief a team entering unfamiliar territory.
|
|
4
|
+
|
|
5
|
+
**Dimensions (priority order — prune to the decision):**
|
|
6
|
+
|
|
7
|
+
1. Industry structure & value chain — how value flows, who captures margin where
|
|
8
|
+
2. Key players & gatekeepers — incumbents, platforms, whose APIs/standards/marketplaces you build with or against
|
|
9
|
+
3. Rules of the game — laws, licenses, de-facto standards, what compliance costs a new entrant
|
|
10
|
+
4. Language & mental models — the vocabulary and implicit workflows practitioners think in; **build the glossary — it's why domain research exists**
|
|
11
|
+
5. Technology adoption — the current technical baseline and where the industry sits on the adoption curve
|
|
12
|
+
|
|
13
|
+
**Craft (the non-obvious):** annual-report industry sections are free structured teardowns; conference keynotes reveal who actually matters; go to regulator sites directly for any load-bearing claim — and read enforcement actions to learn what's actually punished versus merely written; job postings name the real tools and skills; pending regulatory changes matter as much as current text.
|
|
14
|
+
|
|
15
|
+
**Freshness:** structure ≤ 3 yr · player landscape ≤ 18 mo · regulatory status: verify current on every load-bearing claim, whatever its date · tech adoption ≤ 18 mo, AI-adoption claims ≤ 6 mo.
|
|
16
|
+
|
|
17
|
+
**Two-source classes:** regulatory and compliance assertions; quantitative industry figures a recommendation rests on; claims about a gatekeeper's policy.
|
|
18
|
+
|
|
19
|
+
**Feeds (bmm):** brief (context, feasibility) · PRD (constraints, domain vocabulary) · architecture (integration landscape, compliance requirements).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Market Research Pack
|
|
2
|
+
|
|
3
|
+
Serves: enter or skip a market, position a product, pick a segment, price an offer, pitch investors.
|
|
4
|
+
|
|
5
|
+
**Dimensions (priority order — prune to the decision):**
|
|
6
|
+
|
|
7
|
+
1. Market size & growth — the *reachable* market, not the headline TAM
|
|
8
|
+
2. Customer segments & behavior — who buys, deciding how, valuing what
|
|
9
|
+
3. Pain points & unmet needs — what they complain about, work around, pay to avoid
|
|
10
|
+
4. Competitive landscape — who competes for this budget, including substitutes and "do nothing"
|
|
11
|
+
5. GTM & pricing dynamics — channels, sales motion, accepted pricing models, realistic CAC
|
|
12
|
+
|
|
13
|
+
**Craft (the non-obvious):** 1–3★ reviews are the gold for pains; public-company 10-K/S-1 industry sections are free analyst-grade sizing; read competitor pricing pages directly, never roundups; funding history + job postings reveal competitor trajectory; a complaint pattern persisting across years is a stronger finding, not a stale one.
|
|
14
|
+
|
|
15
|
+
**Freshness:** size/growth ≤ 18 mo · pricing & feature claims ≤ 3 mo · behavior data ≤ 2 yr · GTM benchmarks ≤ 12 mo.
|
|
16
|
+
|
|
17
|
+
**Two-source classes:** market size and growth figures; any quantitative claim a recommendation rests on; competitor traction claims.
|
|
18
|
+
|
|
19
|
+
**Feeds (bmm):** brief (opportunity, problem, users) · PRD (personas, differentiation) · pricing and GTM decisions.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Technical Research Pack
|
|
2
|
+
|
|
3
|
+
Serves: adopt a technology area, design an integration approach, ground an architecture in current practice, assess feasibility before committing a roadmap.
|
|
4
|
+
|
|
5
|
+
**Dimensions (priority order — prune to the decision):**
|
|
6
|
+
|
|
7
|
+
1. Landscape & maturity — dominant approaches, what's consolidating vs churning, what the current generation newly makes possible
|
|
8
|
+
2. Integration & interoperability — protocols, formats, auth patterns, where integrations actually hurt
|
|
9
|
+
3. Architecture patterns in practice — which named patterns dominate at what scale, and what the failures teach
|
|
10
|
+
4. Implementation reality — learning curve, tooling, operational burden, what teams say 6–12 months in
|
|
11
|
+
5. Ecosystem health — contributor/release vitality, backing durability, the five-year regret risk
|
|
12
|
+
|
|
13
|
+
**Craft (the non-obvious):** read the retrospective threads, not the launch threads; favor accounts with production numbers over advocacy; before citing a pain point, check whether it was since fixed — an old complaint against a current version is a false claim; read repository metrics over time, never snapshots; issue trackers reveal the gap between docs and reality.
|
|
14
|
+
|
|
15
|
+
**Freshness:** versions & compatibility ≤ 1 mo · ecosystem signals ≤ 6 mo · landscape ≤ 12 mo (AI-adjacent ≤ 3 mo) · patterns ≤ 2 yr.
|
|
16
|
+
|
|
17
|
+
**Two-source classes:** version/compatibility claims; performance or scale numbers a recommendation rests on; claims that a technology or pattern failed — one post-mortem is an anecdote.
|
|
18
|
+
|
|
19
|
+
**Feeds (bmm):** architecture spine (candidate paradigms, operational constraints) · brief (feasibility) · roadmap risk and estimates.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# User-Voice Research Pack
|
|
2
|
+
|
|
3
|
+
Serves: understand what users of a product or category actually experience and want — personas, jobs-to-be-done, requirements grounded in evidence rather than assumption.
|
|
4
|
+
|
|
5
|
+
**Dimensions (priority order — prune to the decision):**
|
|
6
|
+
|
|
7
|
+
1. Who they are & their jobs-to-be-done — the progress they're hiring the product to make
|
|
8
|
+
2. Complaint & workaround patterns — where current options fail them
|
|
9
|
+
3. Delight & switching triggers — why they stay, what made them move
|
|
10
|
+
4. Unmet needs & requests — what they ask for, and the deeper need under the ask
|
|
11
|
+
5. Their language — the words users say, versus the words vendors use
|
|
12
|
+
|
|
13
|
+
**Craft (the non-obvious):** mine 1–3★ reviews for pain *and* 5★ for why they stay; a workaround is unpriced demand — someone laboring around a gap has already voted; forums, Discord, and Reddit surface what surveys miss — people lie less when nobody's asking — but they over-sample the loud, so triangulate against reviews and any survey data; keep verbatim quotes, redacted — user words carry evidence paraphrase destroys, but usernames, handles, emails, and identifying links never enter the report or memlog; feature-request boards measure willingness to wait, not willingness to pay; distinguish loud power-users from the silent majority — count distinct voices, not thread length.
|
|
14
|
+
|
|
15
|
+
**Freshness:** sentiment ≤ 18 mo · complaints re-checked against the current version before citing.
|
|
16
|
+
|
|
17
|
+
**Two-source classes:** any prevalence claim ("most users…", "the top complaint is…") — two independent communities, not two threads in the same one.
|
|
18
|
+
|
|
19
|
+
**Feeds (bmm):** PRD (personas, requirements rationale) · UX research inputs · brief (problem) · product copy in the users' own language.
|
|
@@ -24,7 +24,7 @@ Lead by questioning, not lecturing. Ask one question at a time, press on weak po
|
|
|
24
24
|
|
|
25
25
|
1. Resolve customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly with defaults. Apply the resolved `{workflow.*}` values throughout.
|
|
26
26
|
2. Run each `{workflow.activation_steps_prepend}` entry; treat each `{workflow.persistent_facts}` entry as foundational context (`file:` entries load their contents, `skill:` names a skill to consult, others are facts verbatim).
|
|
27
|
-
3.
|
|
27
|
+
3. Resolve central config: `uv run {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key core`; from the merged JSON read `{user_name}`, `{communication_language}`, `{output_folder}`. On failure use neutral defaults; never block. Greet `{user_name}` in `{communication_language}` and stay in it.
|
|
28
28
|
4. Note whether a BMad persona is already active in this conversation — the user loaded one (e.g. the analyst, the storyteller) and invoked the forge from within it. If so, that persona leads the session, in voice, throughout.
|
|
29
29
|
5. Resume: glob `{workflow.forge_output_path}/**/.memlog.md` (recursive, so it still finds sessions when `run_folder_pattern` is overridden to nest paths) and read only each match's frontmatter to find any whose `status` is not `complete`. Offer to resume one — then read its full memlog once to rebuild state and continue append-only — or to start fresh.
|
|
30
30
|
6. Run each `{workflow.activation_steps_append}` entry.
|
|
@@ -97,7 +97,7 @@ Voice the personas yourself by default. Spawn separate agents only when a branch
|
|
|
97
97
|
|
|
98
98
|
The session can end in three valid states:
|
|
99
99
|
|
|
100
|
-
- **Hardened** — the idea is stronger and specific enough to use. Distill the memlog into `{workspace}/forged-idea.md`. Keep it extremely short: only the decisions, rejected options, and reasons that matter downstream, in the user's meaning. Do not write a prose summary, template, or conversation recap. If it reads like a document, it is too long.
|
|
100
|
+
- **Hardened** — the idea is stronger and specific enough to use. Distill the memlog into `{workspace}/forged-idea.md`. Keep it extremely short: only the decisions, rejected options, and reasons that matter downstream, in the user's meaning. Do not write a prose summary, template, or conversation recap. If it reads like a document, it is too long. If planning or dev skills are installed (`bmad-spec`, `bmad-prd`, `bmad-prfaq`, `bmad-quick-dev`), offer the file as their input; if none are, the file stands on its own — never treat a missing skill as an error.
|
|
101
101
|
- **Killed** — the idea does not hold up. Say so plainly and record why. Finding that out early is a valid outcome.
|
|
102
102
|
- **Clearer** — the user understands the idea better, but there is no hardened idea to hand off. Leave the memlog as the record; no `forged-idea.md` is needed.
|
|
103
103
|
|
|
@@ -186,7 +186,10 @@ def build_pool(agents: dict, party_members: list):
|
|
|
186
186
|
continue
|
|
187
187
|
canonical = index.get(code) or index.get(code.lower()) or code
|
|
188
188
|
was_installed = canonical in pool
|
|
189
|
-
entry
|
|
189
|
+
# Start from the installed entry so fields the override omits
|
|
190
|
+
# (icon, title, description) survive.
|
|
191
|
+
entry = dict(pool.get(canonical, {}))
|
|
192
|
+
entry.update({"code": canonical, "source": "custom"})
|
|
190
193
|
for field in ("name", "icon", "title", "persona", "capabilities", "model"):
|
|
191
194
|
if m.get(field) is not None:
|
|
192
195
|
entry[field] = m[field]
|
|
@@ -23,7 +23,7 @@ When this skill completes, the user should:
|
|
|
23
23
|
## Data Sources
|
|
24
24
|
|
|
25
25
|
- **Catalog**: `{project-root}/_bmad/_config/bmad-help.csv` — assembled manifest of all installed module skills
|
|
26
|
-
- **Config**: Run `uv run
|
|
26
|
+
- **Config**: Run `uv run {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` and use the merged JSON to resolve `output-location` variables and read `core.communication_language` and `modules.bmm.project_knowledge`. The resolver merges `_bmad/config.toml`, `_bmad/config.user.toml`, `_bmad/custom/config.toml`, and `_bmad/custom/config.user.toml` in that order.
|
|
27
27
|
- **Artifacts**: Files matching `outputs` patterns at resolved `output-location` paths reveal which steps are possibly completed; their content may also provide grounding context for recommendations
|
|
28
28
|
- **Project knowledge**: If `project_knowledge` resolves to an existing path, read it for grounding context. Never fabricate project-specific details.
|
|
29
29
|
- **Module docs**: Rows with `_meta` in the `skill` column carry a URL or path in `output-location` pointing to the module's documentation (e.g., llms.txt). Fetch and use these to answer general questions about that module.
|
|
@@ -10,14 +10,14 @@ Run a round-table where these agents talk to each other and to the user like rea
|
|
|
10
10
|
## Conventions
|
|
11
11
|
|
|
12
12
|
- **Paths:** bare paths (e.g. `references/create-party.md`) resolve from `{skill-root}` (where `customize.toml` lives); `{project-root}`-prefixed paths from the project working dir. `{workflow.<name>}` resolves to `customize.toml`'s `[workflow]` table (overrides win).
|
|
13
|
-
- **Scripts** (run via `uv run`): `{project-root}/_bmad/scripts/resolve_customization.py` resolves `{workflow.*}`; `{skill-root}/scripts/resolve_party.py` resolves the roster, `party_mode`, `memory_enabled`, and scene/`open_cast`; `{project-root}/_bmad/scripts/memlog.py` reads/writes per-party memory.
|
|
13
|
+
- **Scripts** (run via `uv run`): `{project-root}/_bmad/scripts/resolve_config.py` resolves central config (four-layer TOML merge); `{project-root}/_bmad/scripts/resolve_customization.py` resolves `{workflow.*}`; `{skill-root}/scripts/resolve_party.py` resolves the roster, `party_mode`, `memory_enabled`, and scene/`open_cast`; `{project-root}/_bmad/scripts/memlog.py` reads/writes per-party memory.
|
|
14
14
|
- **File roles:** a party's memory is the per-party memlog at `{workflow.memory_dir}/<party>/.memlog.md`; custom members and groups live in the user's `customize.toml` overrides. Mechanics in `references/party-memory.md` (memory) and `references/create-party.md` (authoring).
|
|
15
15
|
- **Search:** Web-search, don't guess — anything past your cutoff or unfamiliar; subagents too.
|
|
16
16
|
|
|
17
17
|
## On Activation
|
|
18
18
|
|
|
19
19
|
1. **Resolve customization:** `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults. Then run each `{workflow.activation_steps_prepend}` entry, and hold each `{workflow.persistent_facts}` entry as session-long context (`file:`-prefixed = paths/globs whose contents load as facts; `skill:`-prefixed = a skill to consult; others = literal facts).
|
|
20
|
-
2.
|
|
20
|
+
2. **Resolve core config:** `uv run {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}`. From the merged JSON's `core` table: greet with `{user_name}`, speak in `{communication_language}`, and resolve `{output_folder}`; `{date}` is today's date.
|
|
21
21
|
3. **Detect intent and route.** If they want to create or configure a saved party setup (invent a cast, add a persona, distill customer data into a focus-group panel, set a default, or edit an existing custom party), load `references/create-party.md` and follow it. Otherwise run a party — continue below.
|
|
22
22
|
4. **Resolve the roster:** `uv run {skill-root}/scripts/resolve_party.py --project-root {project-root} --skill {skill-root}`. It returns the active roster (`{workflow.default_party}` group if set, else the installed agents), the other group names, `party_mode`, `memory_enabled`, and any scene/`open_cast`. Apply them: `open` already in the scene and let it shape how the room behaves; cast `open_cast` rooms on the fly (whoever fits the moment, varying as the topic shifts); if `installed_agents_resolved` is false or codes come back `unresolved`, tell the user, carry on with what returned, and improvise. Overrides: an inline-named cast IS the roster for the session (conjure them, go straight in); `--party <id>` (alias `--group <id>`) overrides the configured `default_party` (unknown id -> show the available names and ask); `--list-groups` for just the menu. Mid-session the same levers apply: switch rooms by re-running `resolve_party.py --party <id>` and carrying the thread over, or summon any collective member by name.
|
|
23
23
|
5. **Memory.** If `memory_enabled` (from `resolve_party.py`), follow `references/party-memory.md` for the whole run.
|