bmad-plus 0.13.0 → 0.16.0
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 +74 -0
- package/README.md +113 -487
- package/SECURITY.md +71 -0
- package/THIRD-PARTY-LICENSES.md +349 -0
- package/osint-agent-package/README.md +1 -1
- package/package.json +14 -3
- package/readme-international/README.de.md +18 -8
- package/readme-international/README.es.md +19 -9
- package/readme-international/README.fr.md +18 -8
- package/src/bmad-plus/agents/agent-architect-dev/SKILL.md +11 -13
- package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +148 -9
- package/src/bmad-plus/agents/agent-quality/SKILL.md +41 -11
- package/src/bmad-plus/data/role-triggers.yaml +19 -0
- package/src/bmad-plus/module-help.csv +1 -0
- package/src/bmad-plus/module.yaml +1 -0
- package/src/bmad-plus/packs/pack-dev-studio/README.md +133 -141
- package/src/bmad-plus/packs/pack-dev-studio/SKILL.md +49 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +35 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +59 -59
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +55 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +58 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +55 -132
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +63 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +54 -69
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +54 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +32 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +67 -71
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +61 -90
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +56 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +55 -88
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +20 -153
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +20 -173
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +14 -220
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +20 -329
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +15 -314
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +15 -355
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +15 -375
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +14 -357
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +13 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +52 -65
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +14 -81
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +14 -31
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +14 -45
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +13 -128
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +61 -87
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +55 -298
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +54 -426
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +48 -69
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +24 -80
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +64 -482
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +50 -184
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +57 -173
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +56 -108
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +54 -1509
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +54 -296
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +41 -283
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +58 -18
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +103 -72
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +55 -27
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +34 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +46 -87
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/steps/step-01-init.md +10 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +30 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +57 -27
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +47 -138
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +48 -34
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +51 -68
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +46 -3
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +68 -109
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +53 -174
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +39 -53
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +45 -83
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +45 -176
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +45 -63
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +53 -124
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +44 -100
- package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +56 -116
- package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +49 -12
- package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +51 -328
- package/src/bmad-plus/packs/pack-dev-studio/shared/catalog.json +489 -0
- package/src/bmad-plus/packs/pack-dev-studio/shared/execution.md +69 -0
- package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +39 -32
- package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +85 -14
- package/src/bmad-plus/packs/pack-memory/README.md +35 -4
- package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +33 -6
- package/src/bmad-plus/packs/pack-memory/shared/karpathy-guardrails.md +3 -3
- package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +27 -3
- package/src/bmad-plus/packs/pack-memory/zecher-agent.md +18 -2
- package/src/bmad-plus/packs/pack-seo/SKILL.md +27 -1
- package/src/bmad-plus/packs/pack-seo/seo-chief.md +16 -1
- package/src/bmad-plus/packs/pack-seo/seo-judge.md +12 -0
- package/src/bmad-plus/packs/pack-seo/seo-scout.md +12 -0
- package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +49 -12
- package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +17 -3
- package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +76 -65
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +144 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +60 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-spec.schema.json +121 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-triage.schema.json +60 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +552 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +362 -0
- package/src/bmad-plus/skills/dev-studio/SKILL.md +19 -0
- package/tools/build/check-install-contract.js +367 -17
- package/tools/build/generate.js +229 -40
- package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
- package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
- package/tools/build/generated-adapters/AGENTS.md +1 -1
- package/tools/build/generated-adapters/CLAUDE.md +1 -1
- package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
- package/tools/build/generated-adapters/GEMINI.md +1 -1
- package/tools/build/pack-delivery.js +78 -0
- package/tools/cli/bmad-plus-cli.js +15 -12
- package/tools/cli/commands/doctor.js +50 -189
- package/tools/cli/commands/install.js +22 -3
- package/tools/cli/commands/memory-journal-cmd.js +119 -19
- package/tools/cli/commands/nexus.js +111 -0
- package/tools/cli/commands/studio.js +68 -0
- package/tools/cli/commands/uat.js +389 -0
- package/tools/cli/lib/README-memory-journal.md +19 -8
- package/tools/cli/lib/installation-health.js +366 -0
- package/tools/cli/lib/memory-journal.js +0 -0
- package/tools/cli/lib/memory-outcomes.js +293 -0
- package/tools/cli/lib/memory-store.js +139 -0
- package/tools/cli/lib/nexus-process.js +377 -0
- package/tools/cli/lib/nexus.js +1532 -0
- package/tools/cli/lib/pack-copy.js +39 -11
- package/tools/cli/lib/packs.js +134 -11
- package/tools/cli/lib/python-health.js +233 -0
- package/tools/cli/lib/python-provision.js +2 -2
- package/tools/cli/lib/studio.js +310 -0
- package/tools/cli/lib/uat.js +869 -0
- package/tools/maintain/upstream-candidate.js +456 -0
- package/tools/release/publication-content.js +903 -0
- package/tools/release/supply-chain.js +282 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/** Read-only by default; optional Python import probes never provision or repair. */
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const semver = require('semver');
|
|
5
|
+
const { DERIVED } = require('./packs');
|
|
6
|
+
const { readInstallManifest } = require('./install-manifest');
|
|
7
|
+
const { inventoryOf, collectManagedFiles, MANIFEST } = require('./update-transaction');
|
|
8
|
+
const { fileHash } = require('./pack-copy');
|
|
9
|
+
const { contentHash } = require('./installed-adapters');
|
|
10
|
+
const {
|
|
11
|
+
safeTarget,
|
|
12
|
+
GENERATED_MARKER,
|
|
13
|
+
USER_CONFIG_MARKER,
|
|
14
|
+
} = require('../../build/generate-adapters');
|
|
15
|
+
|
|
16
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '../../..');
|
|
17
|
+
|
|
18
|
+
function inspectFile(root, file) {
|
|
19
|
+
try {
|
|
20
|
+
return { state: 'present', bytes: fs.readFileSync(safeTarget(root, file)) };
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return { state: error.code === 'ENOENT' ? 'missing' : 'unreadable' };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function collectInstallationHealth({
|
|
27
|
+
projectDir = process.cwd(),
|
|
28
|
+
packageRoot = PACKAGE_ROOT,
|
|
29
|
+
cliVersion = require('../../../package.json').version,
|
|
30
|
+
nodeVersion = process.versions.node,
|
|
31
|
+
verifyPython = false,
|
|
32
|
+
pythonRunner,
|
|
33
|
+
} = {}) {
|
|
34
|
+
projectDir = path.resolve(projectDir);
|
|
35
|
+
const report = {
|
|
36
|
+
schemaVersion: 1,
|
|
37
|
+
projectDir,
|
|
38
|
+
status: 'ok',
|
|
39
|
+
versions: { installed: null, cli: cliVersion, published: null },
|
|
40
|
+
inventory: { status: 'unknown', trackedFiles: 0 },
|
|
41
|
+
packs: [],
|
|
42
|
+
integrations: [],
|
|
43
|
+
processExecution: DERIVED.diagnostics.processExecution
|
|
44
|
+
? {
|
|
45
|
+
...DERIVED.diagnostics.processExecution,
|
|
46
|
+
availability: 'not-probed',
|
|
47
|
+
}
|
|
48
|
+
: null,
|
|
49
|
+
runtimes: [],
|
|
50
|
+
checks: [],
|
|
51
|
+
summary: { passed: 0, warnings: 0, errors: 0 },
|
|
52
|
+
};
|
|
53
|
+
function check(code, status, message, extra = {}) {
|
|
54
|
+
report.checks.push({ code, status, message, ...extra });
|
|
55
|
+
}
|
|
56
|
+
function finish() {
|
|
57
|
+
for (const entry of report.checks) {
|
|
58
|
+
report.summary[
|
|
59
|
+
entry.status === 'pass' ? 'passed' : entry.status === 'warning' ? 'warnings' : 'errors'
|
|
60
|
+
]++;
|
|
61
|
+
}
|
|
62
|
+
report.status = report.summary.errors ? 'error' : report.summary.warnings ? 'warning' : 'ok';
|
|
63
|
+
return report;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const manifestFile = inspectFile(projectDir, MANIFEST);
|
|
67
|
+
if (manifestFile.state !== 'present') {
|
|
68
|
+
check(
|
|
69
|
+
manifestFile.state === 'missing' ? 'installation.missing' : 'manifest.unreadable',
|
|
70
|
+
'error',
|
|
71
|
+
manifestFile.state === 'missing'
|
|
72
|
+
? 'BMAD+ is not installed in this directory.'
|
|
73
|
+
: 'Installation manifest cannot be read safely.',
|
|
74
|
+
{ path: MANIFEST }
|
|
75
|
+
);
|
|
76
|
+
return finish();
|
|
77
|
+
}
|
|
78
|
+
let manifest;
|
|
79
|
+
try {
|
|
80
|
+
manifest = readInstallManifest(safeTarget(projectDir, MANIFEST));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
check('manifest.invalid', 'error', 'Invalid installation manifest: ' + error.message, {
|
|
83
|
+
path: MANIFEST,
|
|
84
|
+
});
|
|
85
|
+
return finish();
|
|
86
|
+
}
|
|
87
|
+
report.versions.installed = manifest.version;
|
|
88
|
+
check('manifest.valid', 'pass', 'Manifest found: v' + manifest.version);
|
|
89
|
+
check(
|
|
90
|
+
'version.cli',
|
|
91
|
+
manifest.version === cliVersion ? 'pass' : 'warning',
|
|
92
|
+
manifest.version === cliVersion
|
|
93
|
+
? 'Installed and executing CLI versions match.'
|
|
94
|
+
: 'Version mismatch: installed v' + manifest.version + ', executing CLI v' + cliVersion + '.'
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const selectedPacks = [...new Set(manifest.packs)];
|
|
98
|
+
const validPacks = selectedPacks.filter((id) => Object.hasOwn(DERIVED.packs, id));
|
|
99
|
+
for (const id of selectedPacks) {
|
|
100
|
+
if (!validPacks.includes(id))
|
|
101
|
+
check('pack.unknown', 'error', 'Unknown installed pack: ' + id, { pack: id });
|
|
102
|
+
}
|
|
103
|
+
if (!selectedPacks.includes('core'))
|
|
104
|
+
check('pack.core-missing', 'error', 'The required Core pack is not selected.');
|
|
105
|
+
|
|
106
|
+
if (selectedPacks.includes('dev-studio')) {
|
|
107
|
+
try {
|
|
108
|
+
const studio = require('./studio');
|
|
109
|
+
const resolved = studio.validatePack(studio.installedPack(projectDir));
|
|
110
|
+
studio.resolveConfig(projectDir);
|
|
111
|
+
check(
|
|
112
|
+
'studio.resources',
|
|
113
|
+
'pass',
|
|
114
|
+
`Dev Studio: ${resolved.catalog.workflows.length} routes and ${resolved.resources.length} resources resolved; host execution is not verified.`
|
|
115
|
+
);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
check(
|
|
118
|
+
'studio.resources',
|
|
119
|
+
'error',
|
|
120
|
+
'Dev Studio cannot prepare its workflows: ' + error.message,
|
|
121
|
+
{ path: '.agents/skills/pack-dev-studio' }
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const inventory = inventoryOf(manifest);
|
|
127
|
+
const validInventory =
|
|
128
|
+
inventory &&
|
|
129
|
+
Object.values(inventory).every((hash) => typeof hash === 'string') &&
|
|
130
|
+
typeof manifest.fileInventory.complete === 'boolean';
|
|
131
|
+
report.inventory.status =
|
|
132
|
+
manifest.fileInventory === undefined
|
|
133
|
+
? 'legacy'
|
|
134
|
+
: !validInventory
|
|
135
|
+
? 'invalid'
|
|
136
|
+
: manifest.fileInventory.complete
|
|
137
|
+
? 'complete'
|
|
138
|
+
: 'incomplete';
|
|
139
|
+
const owned = validInventory ? inventory : {};
|
|
140
|
+
report.inventory.trackedFiles = Object.keys(owned).length;
|
|
141
|
+
|
|
142
|
+
const expectedFiles = new Set(['_bmad/module.yaml', '_bmad/module-help.csv']);
|
|
143
|
+
for (const id of validPacks) {
|
|
144
|
+
const pack = DERIVED.packs[id];
|
|
145
|
+
report.packs.push({ id, runtimes: pack.runtimes, requiredResources: pack.requiredResources });
|
|
146
|
+
for (const file of pack.requiredResources) expectedFiles.add(file);
|
|
147
|
+
}
|
|
148
|
+
// Check delivery against the executing package, even if a manifest omits a file.
|
|
149
|
+
try {
|
|
150
|
+
for (const { file } of collectManagedFiles({ projectDir, packageRoot, packs: validPacks }))
|
|
151
|
+
expectedFiles.add(file);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
check('package.unreadable', 'error', 'Cannot inspect the executing package: ' + error.message);
|
|
154
|
+
}
|
|
155
|
+
const resources = new Set([...expectedFiles, ...Object.keys(owned)]);
|
|
156
|
+
for (const file of [...resources].sort()) {
|
|
157
|
+
if (!Object.hasOwn(owned, file) && report.inventory.status === 'complete')
|
|
158
|
+
report.inventory.status = 'incomplete';
|
|
159
|
+
const observed = inspectFile(projectDir, file);
|
|
160
|
+
if (observed.state !== 'present') {
|
|
161
|
+
check(
|
|
162
|
+
'file.' + observed.state,
|
|
163
|
+
'error',
|
|
164
|
+
observed.state === 'missing'
|
|
165
|
+
? 'Missing required file: ' + file
|
|
166
|
+
: 'File cannot be read safely: ' + file,
|
|
167
|
+
{ path: file }
|
|
168
|
+
);
|
|
169
|
+
} else if (Object.hasOwn(owned, file)) {
|
|
170
|
+
const unchanged = fileHash(observed.bytes) === owned[file];
|
|
171
|
+
check(
|
|
172
|
+
unchanged ? 'file.unchanged' : 'file.modified',
|
|
173
|
+
unchanged ? 'pass' : 'warning',
|
|
174
|
+
unchanged
|
|
175
|
+
? 'Managed file unchanged.'
|
|
176
|
+
: 'Managed file modified locally; review before updating.',
|
|
177
|
+
{ path: file }
|
|
178
|
+
);
|
|
179
|
+
} else {
|
|
180
|
+
check('file.unowned', 'warning', 'File present without a recorded ownership hash.', {
|
|
181
|
+
path: file,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (manifest.fileInventory?.complete && report.inventory.status === 'incomplete') {
|
|
186
|
+
check(
|
|
187
|
+
'inventory.omissions',
|
|
188
|
+
'warning',
|
|
189
|
+
'Inventory claims completeness but omits expected files.'
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
check(
|
|
193
|
+
'inventory.' + report.inventory.status,
|
|
194
|
+
report.inventory.status === 'complete'
|
|
195
|
+
? 'pass'
|
|
196
|
+
: report.inventory.status === 'invalid'
|
|
197
|
+
? 'error'
|
|
198
|
+
: 'warning',
|
|
199
|
+
'File ownership inventory: ' + report.inventory.status + '.'
|
|
200
|
+
);
|
|
201
|
+
const config = inspectFile(projectDir, '_bmad/config.yaml');
|
|
202
|
+
check(
|
|
203
|
+
'config.' + config.state,
|
|
204
|
+
config.state === 'present' ? 'pass' : 'error',
|
|
205
|
+
config.state === 'present'
|
|
206
|
+
? 'Project configuration present.'
|
|
207
|
+
: 'Project configuration missing or unreadable.',
|
|
208
|
+
{ path: '_bmad/config.yaml' }
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const contract = DERIVED.diagnostics;
|
|
212
|
+
const declaredTools = Object.keys(contract.integrations);
|
|
213
|
+
let selectedTools = [];
|
|
214
|
+
if (manifest.ides === undefined) {
|
|
215
|
+
selectedTools = declaredTools.filter(
|
|
216
|
+
(tool) =>
|
|
217
|
+
inspectFile(projectDir, contract.integrations[tool].instructionFile).state !== 'missing'
|
|
218
|
+
);
|
|
219
|
+
check(
|
|
220
|
+
'integrations.legacy',
|
|
221
|
+
'warning',
|
|
222
|
+
'IDE selection was not recorded; existing adapter paths were inspected.'
|
|
223
|
+
);
|
|
224
|
+
} else if (
|
|
225
|
+
!Array.isArray(manifest.ides) ||
|
|
226
|
+
manifest.ides.some((id) => typeof id !== 'string' || !declaredTools.includes(id))
|
|
227
|
+
) {
|
|
228
|
+
check(
|
|
229
|
+
'integrations.invalid',
|
|
230
|
+
'error',
|
|
231
|
+
'Manifest IDE selection contains invalid or unknown targets.'
|
|
232
|
+
);
|
|
233
|
+
} else selectedTools = [...new Set(manifest.ides)];
|
|
234
|
+
|
|
235
|
+
const adapterPaths =
|
|
236
|
+
selectedTools.length || manifest.ides === undefined
|
|
237
|
+
? [
|
|
238
|
+
...new Set([
|
|
239
|
+
DERIVED.targets.spine,
|
|
240
|
+
...selectedTools.map((tool) => contract.integrations[tool].instructionFile),
|
|
241
|
+
]),
|
|
242
|
+
]
|
|
243
|
+
: [];
|
|
244
|
+
const ownership = new Map();
|
|
245
|
+
const hashes = manifest.adapterHashes;
|
|
246
|
+
const validHashes =
|
|
247
|
+
hashes === undefined ||
|
|
248
|
+
(hashes &&
|
|
249
|
+
typeof hashes === 'object' &&
|
|
250
|
+
!Array.isArray(hashes) &&
|
|
251
|
+
Object.entries(hashes).every(
|
|
252
|
+
([file, hash]) =>
|
|
253
|
+
[DERIVED.targets.spine, ...DERIVED.targets.adapters.map((entry) => entry.file)].includes(
|
|
254
|
+
file
|
|
255
|
+
) &&
|
|
256
|
+
typeof hash === 'string' &&
|
|
257
|
+
/^[a-f0-9]{64}$/.test(hash)
|
|
258
|
+
));
|
|
259
|
+
if (!validHashes) check('adapters.invalid', 'error', 'Adapter ownership hashes are invalid.');
|
|
260
|
+
for (const file of adapterPaths) {
|
|
261
|
+
const observed = inspectFile(projectDir, file);
|
|
262
|
+
let state = observed.state;
|
|
263
|
+
if (state === 'present') {
|
|
264
|
+
const text = observed.bytes.toString('utf8');
|
|
265
|
+
const previous = validHashes && hashes && Object.hasOwn(hashes, file) ? hashes[file] : null;
|
|
266
|
+
state =
|
|
267
|
+
text.includes(GENERATED_MARKER) && !text.includes(USER_CONFIG_MARKER)
|
|
268
|
+
? 'adopted'
|
|
269
|
+
: !previous || !text.includes(USER_CONFIG_MARKER)
|
|
270
|
+
? 'unowned'
|
|
271
|
+
: contentHash(text) === previous
|
|
272
|
+
? 'unchanged'
|
|
273
|
+
: 'modified';
|
|
274
|
+
}
|
|
275
|
+
ownership.set(file, state);
|
|
276
|
+
check(
|
|
277
|
+
'adapter.' + state,
|
|
278
|
+
['missing', 'unreadable'].includes(state)
|
|
279
|
+
? 'error'
|
|
280
|
+
: state === 'unchanged'
|
|
281
|
+
? 'pass'
|
|
282
|
+
: 'warning',
|
|
283
|
+
'Instruction file: ' + state + '.',
|
|
284
|
+
{ path: file }
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
report.integrations = selectedTools.map((tool) => ({
|
|
288
|
+
tool,
|
|
289
|
+
...contract.integrations[tool],
|
|
290
|
+
ownership: ownership.get(contract.integrations[tool].instructionFile),
|
|
291
|
+
hostVerified: false,
|
|
292
|
+
}));
|
|
293
|
+
|
|
294
|
+
const nodeMinimum = contract.runtimeMinimums.node;
|
|
295
|
+
const nodeReady = Boolean(semver.valid(nodeVersion) && semver.gte(nodeVersion, nodeMinimum));
|
|
296
|
+
report.runtimes.push({
|
|
297
|
+
runtime: 'node',
|
|
298
|
+
minimum: nodeMinimum,
|
|
299
|
+
version: nodeVersion,
|
|
300
|
+
status: nodeReady ? 'verified' : 'unsupported',
|
|
301
|
+
});
|
|
302
|
+
check(
|
|
303
|
+
'runtime.node',
|
|
304
|
+
nodeReady ? 'pass' : 'error',
|
|
305
|
+
'Executing Node.js ' + nodeVersion + '; minimum ' + nodeMinimum + '.'
|
|
306
|
+
);
|
|
307
|
+
for (const id of validPacks.filter((pack) => DERIVED.packs[pack].runtimes.includes('python'))) {
|
|
308
|
+
const requirements = DERIVED.pythonPacks[id].requirements.join('/');
|
|
309
|
+
const delivery = inspectFile(packageRoot, requirements).state;
|
|
310
|
+
const environmentFile = '.bmad/venv/' + id + '/pyvenv.cfg';
|
|
311
|
+
const environment = inspectFile(projectDir, environmentFile).state;
|
|
312
|
+
const runtime = {
|
|
313
|
+
runtime: 'python',
|
|
314
|
+
pack: id,
|
|
315
|
+
minimum: contract.runtimeMinimums.python,
|
|
316
|
+
requirements,
|
|
317
|
+
packageRequirements: delivery,
|
|
318
|
+
environment,
|
|
319
|
+
imports: 'not-verified',
|
|
320
|
+
};
|
|
321
|
+
report.runtimes.push(runtime);
|
|
322
|
+
check(
|
|
323
|
+
'runtime.requirements',
|
|
324
|
+
delivery === 'present' ? 'pass' : 'error',
|
|
325
|
+
delivery === 'present'
|
|
326
|
+
? 'Python requirements are available in the executing package.'
|
|
327
|
+
: 'Python requirements are missing or unreadable in the executing package.',
|
|
328
|
+
{ pack: id, path: requirements, scope: 'package' }
|
|
329
|
+
);
|
|
330
|
+
if (verifyPython) {
|
|
331
|
+
const { probePythonRuntime } = require('./python-health');
|
|
332
|
+
const probe = probePythonRuntime({
|
|
333
|
+
projectDir,
|
|
334
|
+
packId: id,
|
|
335
|
+
minimum: contract.runtimeMinimums.python,
|
|
336
|
+
verifyModules: DERIVED.pythonPacks[id].verifyModules,
|
|
337
|
+
runner: pythonRunner,
|
|
338
|
+
});
|
|
339
|
+
Object.assign(runtime, probe);
|
|
340
|
+
check(
|
|
341
|
+
'runtime.python-probe',
|
|
342
|
+
probe.status === 'verified' ? 'pass' : 'warning',
|
|
343
|
+
probe.message +
|
|
344
|
+
(probe.status === 'verified'
|
|
345
|
+
? ''
|
|
346
|
+
: ' Use bmad-plus install --provision-python to provision selected Python packs.'),
|
|
347
|
+
{
|
|
348
|
+
pack: id,
|
|
349
|
+
path: environmentFile,
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
} else {
|
|
353
|
+
check(
|
|
354
|
+
'runtime.python-unverified',
|
|
355
|
+
'warning',
|
|
356
|
+
'Python environment ' +
|
|
357
|
+
environment +
|
|
358
|
+
'; interpreter version and imports have not been executed. Use doctor --verify-python to probe them.',
|
|
359
|
+
{ pack: id, path: environmentFile }
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return finish();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = { collectInstallationHealth };
|
|
Binary file
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const nexus = require('./nexus');
|
|
5
|
+
const store = require('./memory-store');
|
|
6
|
+
|
|
7
|
+
const OUTCOMES = '.bmad/memory/outcomes.ndjson';
|
|
8
|
+
const BOOST = 0.25;
|
|
9
|
+
const digest = (value) =>
|
|
10
|
+
crypto
|
|
11
|
+
.createHash('sha256')
|
|
12
|
+
.update(typeof value === 'string' ? value : JSON.stringify(value))
|
|
13
|
+
.digest('hex');
|
|
14
|
+
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
15
|
+
|
|
16
|
+
function requiredText(value, label, maximum = 1000) {
|
|
17
|
+
if (typeof value !== 'string' || !value.trim() || value.length > maximum)
|
|
18
|
+
throw new Error(label + ' must be non-empty bounded text.');
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function scopePath(value) {
|
|
22
|
+
if (
|
|
23
|
+
typeof value !== 'string' ||
|
|
24
|
+
!value ||
|
|
25
|
+
value.includes('\\') ||
|
|
26
|
+
value.startsWith('/') ||
|
|
27
|
+
value.includes(':') ||
|
|
28
|
+
value.split('/').some((p) => !p || p === '.' || p === '..')
|
|
29
|
+
) {
|
|
30
|
+
throw new Error('Outcome scope must be a normalized relative path.');
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function within(child, parent) {
|
|
35
|
+
return child === parent || child.startsWith(parent + '/');
|
|
36
|
+
}
|
|
37
|
+
function overlaps(a, b) {
|
|
38
|
+
return a.some((left) => b.some((right) => within(left, right) || within(right, left)));
|
|
39
|
+
}
|
|
40
|
+
function scopes(values) {
|
|
41
|
+
if (!Array.isArray(values) || !values.length || values.length > 30)
|
|
42
|
+
throw new Error('An explicit non-empty project scope is required.');
|
|
43
|
+
return [...new Set(values.map(scopePath))].sort();
|
|
44
|
+
}
|
|
45
|
+
function sourceSection(baseDir, input) {
|
|
46
|
+
if (
|
|
47
|
+
!input ||
|
|
48
|
+
!/^(?:\.agents|\.bmad)\/memory\/(?:decisions|lessons|patterns)\.md$/.test(input.file)
|
|
49
|
+
)
|
|
50
|
+
throw new Error(
|
|
51
|
+
'Outcome memory source must be a project decisions, lessons or patterns Markdown file.'
|
|
52
|
+
);
|
|
53
|
+
const heading = requiredText(input.heading, 'Memory heading', 300);
|
|
54
|
+
const sections = require('./memory-journal')._internal.splitSections(
|
|
55
|
+
store.readText(baseDir, input.file),
|
|
56
|
+
input.file
|
|
57
|
+
);
|
|
58
|
+
const matches = sections.filter((section) => section.heading === heading);
|
|
59
|
+
if (matches.length !== 1) throw new Error('Memory heading must identify exactly one section.');
|
|
60
|
+
const text = (matches[0].heading + '\n' + matches[0].text).trim();
|
|
61
|
+
return { file: input.file, heading, textSha256: digest(text) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Read-only adapter: Nexus owns the actual verification and freshness policy. */
|
|
65
|
+
function acceptedSource(baseDir, runId, taskId) {
|
|
66
|
+
const run = nexus.inspectRun(baseDir, runId);
|
|
67
|
+
if (run.lock || run.recoveryLock)
|
|
68
|
+
throw new Error(
|
|
69
|
+
'Nexus state is being written or recovered; retry observation after its owner finishes.'
|
|
70
|
+
);
|
|
71
|
+
const task = run.tasks.find((item) => item.id === taskId);
|
|
72
|
+
const observation = run.observations.find((item) => item.taskId === taskId);
|
|
73
|
+
const attempt = task?.attempts.at(-1);
|
|
74
|
+
if (
|
|
75
|
+
!task ||
|
|
76
|
+
task.integration !== 'accepted' ||
|
|
77
|
+
!observation?.evidenceEligible ||
|
|
78
|
+
observation.stale ||
|
|
79
|
+
attempt?.execution !== 'completed' ||
|
|
80
|
+
attempt.verification.status !== 'passed'
|
|
81
|
+
) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'Outcome source is not an accepted task with current independently verified evidence.'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
task.integrationReceipt?.attemptId !== attempt.id ||
|
|
88
|
+
!equal(task.integrationReceipt.artifactHashes, attempt.verification.artifactHashes)
|
|
89
|
+
) {
|
|
90
|
+
throw new Error('Nexus acceptance does not bind the current attempt and artifacts.');
|
|
91
|
+
}
|
|
92
|
+
const definition = run.plan.tasks.find((item) => item.id === taskId);
|
|
93
|
+
return {
|
|
94
|
+
kind: 'nexus',
|
|
95
|
+
runId,
|
|
96
|
+
taskId,
|
|
97
|
+
attemptId: attempt.id,
|
|
98
|
+
verificationId: attempt.verification.id,
|
|
99
|
+
verificationSha256: digest(attempt.verification),
|
|
100
|
+
acceptedAt: task.acceptedAt,
|
|
101
|
+
artifactHashes: task.integrationReceipt.artifactHashes,
|
|
102
|
+
resourceHashes: definition.resourceHashes,
|
|
103
|
+
scope: definition.scope,
|
|
104
|
+
objective: definition.objective,
|
|
105
|
+
baseline: attempt.baseline || null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readOutcomes(baseDir) {
|
|
110
|
+
const text = store.readText(baseDir, OUTCOMES, { missing: true });
|
|
111
|
+
if (text === null) return [];
|
|
112
|
+
if (text && !text.endsWith('\n'))
|
|
113
|
+
throw new Error('Outcome store has an incomplete final record.');
|
|
114
|
+
const records = text
|
|
115
|
+
.split('\n')
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.map((line) => JSON.parse(line));
|
|
118
|
+
if (records.length > 1000) throw new Error('Outcome store exceeds 1000 records.');
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const attempts = new Set();
|
|
121
|
+
for (const record of records) {
|
|
122
|
+
const { id, ...body } = record;
|
|
123
|
+
if (
|
|
124
|
+
body.schemaVersion !== 1 ||
|
|
125
|
+
body.kind !== 'accepted-outcome' ||
|
|
126
|
+
id !== digest(body) ||
|
|
127
|
+
seen.has(id)
|
|
128
|
+
)
|
|
129
|
+
throw new Error('Outcome receipt integrity or uniqueness check failed.');
|
|
130
|
+
if (
|
|
131
|
+
!Array.isArray(record.supersedes) ||
|
|
132
|
+
!Array.isArray(record.contradicts) ||
|
|
133
|
+
[...record.supersedes, ...record.contradicts].some((ref) => !seen.has(ref))
|
|
134
|
+
)
|
|
135
|
+
throw new Error('Outcome relationships must identify earlier receipts.');
|
|
136
|
+
const attemptKey = [record.source?.runId, record.source?.taskId, record.source?.attemptId].join(
|
|
137
|
+
'\0'
|
|
138
|
+
);
|
|
139
|
+
if (!record.source?.attemptId || attempts.has(attemptKey))
|
|
140
|
+
throw new Error('Duplicate or missing outcome attempt identity.');
|
|
141
|
+
attempts.add(attemptKey);
|
|
142
|
+
seen.add(id);
|
|
143
|
+
}
|
|
144
|
+
return records;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function observeOutcome(baseDir, input, { now } = {}) {
|
|
148
|
+
if (!input || typeof input !== 'object') throw new Error('Outcome input is required.');
|
|
149
|
+
if (typeof now !== 'string' || !Number.isFinite(Date.parse(now)))
|
|
150
|
+
throw new Error('Outcome timestamp must be injected as an ISO string.');
|
|
151
|
+
return store.withMemoryLock(baseDir, () => {
|
|
152
|
+
const records = readOutcomes(baseDir);
|
|
153
|
+
const source = acceptedSource(
|
|
154
|
+
baseDir,
|
|
155
|
+
requiredText(input.runId, 'Run ID'),
|
|
156
|
+
requiredText(input.taskId, 'Task ID')
|
|
157
|
+
);
|
|
158
|
+
if (
|
|
159
|
+
records.some(
|
|
160
|
+
(record) =>
|
|
161
|
+
record.source.runId === source.runId &&
|
|
162
|
+
record.source.taskId === source.taskId &&
|
|
163
|
+
record.source.attemptId === source.attemptId
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
throw new Error('Duplicate outcome: this task attempt already supplied a receipt.');
|
|
167
|
+
const memory = sourceSection(baseDir, input.memory);
|
|
168
|
+
const scope = scopes(input.scope);
|
|
169
|
+
if (scope.some((item) => !source.scope.some((parent) => within(item, parent))))
|
|
170
|
+
throw new Error('Outcome scope exceeds the accepted task scope.');
|
|
171
|
+
const relationships = {};
|
|
172
|
+
for (const key of ['supersedes', 'contradicts']) {
|
|
173
|
+
const values = input[key] || [];
|
|
174
|
+
if (!Array.isArray(values) || values.length > 30 || new Set(values).size !== values.length)
|
|
175
|
+
throw new Error('Outcome relationships must be a unique bounded receipt-ID list.');
|
|
176
|
+
for (const id of values) {
|
|
177
|
+
const prior = records.find((record) => record.id === id);
|
|
178
|
+
if (!prior || !overlaps(scope, prior.scope))
|
|
179
|
+
throw new Error('Related outcome must exist in the same applicable project scope.');
|
|
180
|
+
}
|
|
181
|
+
relationships[key] = [...values].sort();
|
|
182
|
+
}
|
|
183
|
+
if (relationships.supersedes.some((id) => relationships.contradicts.includes(id)))
|
|
184
|
+
throw new Error('A receipt cannot be both superseded and contradicted.');
|
|
185
|
+
const body = {
|
|
186
|
+
schemaVersion: 1,
|
|
187
|
+
kind: 'accepted-outcome',
|
|
188
|
+
ts: now,
|
|
189
|
+
memory,
|
|
190
|
+
scope,
|
|
191
|
+
source,
|
|
192
|
+
interpretation: requiredText(input.interpretation, 'Lesson interpretation', 2000),
|
|
193
|
+
verification: { status: 'passed', acceptance: 'accepted', lessonCausality: 'unmeasured' },
|
|
194
|
+
...relationships,
|
|
195
|
+
};
|
|
196
|
+
// Re-read after loading relationships/Markdown; do not persist a torn source observation.
|
|
197
|
+
if (
|
|
198
|
+
!equal(source, acceptedSource(baseDir, source.runId, source.taskId)) ||
|
|
199
|
+
!equal(memory, sourceSection(baseDir, input.memory))
|
|
200
|
+
)
|
|
201
|
+
throw new Error('Outcome sources changed while being observed.');
|
|
202
|
+
if (records.length >= 1000) throw new Error('Outcome store is full; archive it explicitly.');
|
|
203
|
+
return store.appendRecord(baseDir, OUTCOMES, { id: digest(body), ...body });
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function inspectOutcomes(baseDir, { contextScope } = {}) {
|
|
208
|
+
const selectedScope = contextScope === undefined ? null : scopes(contextScope);
|
|
209
|
+
const records = readOutcomes(baseDir);
|
|
210
|
+
const superseded = new Set(records.flatMap((record) => record.supersedes));
|
|
211
|
+
const contradictory = new Set();
|
|
212
|
+
for (const record of records.filter((record) => !superseded.has(record.id))) {
|
|
213
|
+
for (const other of record.contradicts) {
|
|
214
|
+
if (!superseded.has(other)) {
|
|
215
|
+
contradictory.add(record.id);
|
|
216
|
+
contradictory.add(other);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const conflictingAnchors = new Set(
|
|
221
|
+
records
|
|
222
|
+
.filter((record) => contradictory.has(record.id))
|
|
223
|
+
.map((record) => record.memory.file + '\0' + record.memory.heading)
|
|
224
|
+
);
|
|
225
|
+
return records.map((record) => {
|
|
226
|
+
let reason = null;
|
|
227
|
+
if (superseded.has(record.id)) reason = 'superseded';
|
|
228
|
+
else if (conflictingAnchors.has(record.memory.file + '\0' + record.memory.heading))
|
|
229
|
+
reason = 'contradictory';
|
|
230
|
+
else if (selectedScope && !overlaps(record.scope, selectedScope))
|
|
231
|
+
reason = 'outside-query-scope';
|
|
232
|
+
else {
|
|
233
|
+
try {
|
|
234
|
+
if (!equal(sourceSection(baseDir, record.memory), record.memory))
|
|
235
|
+
reason = 'stale-memory-section';
|
|
236
|
+
else if (
|
|
237
|
+
!equal(acceptedSource(baseDir, record.source.runId, record.source.taskId), record.source)
|
|
238
|
+
)
|
|
239
|
+
reason = 'stale-outcome-source';
|
|
240
|
+
} catch (error) {
|
|
241
|
+
reason = 'unavailable-or-ineligible-source: ' + error.message;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { ...record, eligible: reason === null, reason };
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function rankWithEvidence(baseDir, candidates, { contextScope } = {}) {
|
|
249
|
+
const status = inspectOutcomes(baseDir, { contextScope: scopes(contextScope) });
|
|
250
|
+
const key = (value) => value.file + '\0' + value.heading;
|
|
251
|
+
const results = [];
|
|
252
|
+
for (const entry of candidates) {
|
|
253
|
+
const matching =
|
|
254
|
+
entry.kind === 'note'
|
|
255
|
+
? status.filter(
|
|
256
|
+
(record) => key(record.memory) === key({ file: entry.sourceFile, heading: entry.ref })
|
|
257
|
+
)
|
|
258
|
+
: [];
|
|
259
|
+
if (!matching.length) {
|
|
260
|
+
results.push({ ...entry, evidence: { status: 'unverified', receiptIds: [], boost: 0 } });
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
// A stale, contradicted or superseded anchor must not silently fall back to an authoritative note.
|
|
264
|
+
const eligible = matching.filter(
|
|
265
|
+
(record) => record.eligible && record.memory.textSha256 === digest(entry.text)
|
|
266
|
+
);
|
|
267
|
+
if (!eligible.length) continue;
|
|
268
|
+
const reward = require('./memory-journal').computeReward({ acceptance: true });
|
|
269
|
+
results.push({
|
|
270
|
+
...entry,
|
|
271
|
+
lexicalScore: entry.score,
|
|
272
|
+
score: entry.score * (1 + BOOST * reward),
|
|
273
|
+
evidence: {
|
|
274
|
+
status: 'accepted-current',
|
|
275
|
+
receiptIds: eligible.map((record) => record.id),
|
|
276
|
+
reward,
|
|
277
|
+
boost: BOOST,
|
|
278
|
+
lessonCausality: 'unmeasured',
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return results;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
observeOutcome,
|
|
287
|
+
inspectOutcomes,
|
|
288
|
+
readOutcomes,
|
|
289
|
+
rankWithEvidence,
|
|
290
|
+
acceptedSource,
|
|
291
|
+
OUTCOMES,
|
|
292
|
+
BOOST,
|
|
293
|
+
};
|