bmad-plus 0.22.0 → 0.23.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 +19 -0
- package/README.md +23 -13
- package/SECURITY.md +1 -1
- package/package.json +4 -1
- package/readme-international/README.de.md +23 -13
- package/readme-international/README.es.md +23 -13
- package/readme-international/README.fr.md +23 -13
- package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +5 -1
- package/src/bmad-plus/data/role-triggers.yaml +34 -0
- package/src/bmad-plus/module-help.csv +2 -0
- package/src/bmad-plus/module.yaml +21 -0
- package/src/bmad-plus/packs/pack-design/SKILL.md +72 -0
- package/src/bmad-plus/packs/pack-design/bmad-skill-manifest.yaml +13 -0
- package/src/bmad-plus/packs/pack-design/design-engine.md +126 -0
- package/src/bmad-plus/packs/pack-design/intake.yaml +105 -0
- package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +7 -0
- package/src/bmad-plus/skills/bmad-plus-intake/SKILL.md +87 -0
- package/src/bmad-plus/skills/bmad-plus-intake/ref/intake-format.md +112 -0
- package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +18 -0
- package/src/bmad-plus/skills/bmad-plus-swarm/SKILL.md +115 -0
- package/src/bmad-plus/skills/bmad-plus-swarm/ref/definition.md +188 -0
- package/tools/build/adapters.config.js +2 -0
- package/tools/build/generate-adapters.js +25 -1
- package/tools/build/generate.js +269 -7
- package/tools/build/intake-contract.js +522 -0
- package/tools/cli/bmad-plus-cli.js +2 -0
- package/tools/cli/commands/install.js +28 -1
- package/tools/cli/commands/intake.js +124 -0
- package/tools/cli/commands/swarm.js +119 -0
- package/tools/cli/commands/uninstall.js +17 -0
- package/tools/cli/i18n.js +20 -0
- package/tools/cli/lib/intake.js +202 -0
- package/tools/cli/lib/pack-copy.js +95 -5
- package/tools/cli/lib/packs.js +97 -7
- package/tools/cli/lib/swarm-deal.js +116 -0
- package/tools/cli/lib/swarm-defaults.js +6 -0
- package/tools/cli/lib/swarm-tally.js +195 -0
- package/tools/cli/lib/swarm-validate.js +589 -0
- package/tools/cli/lib/swarm.js +712 -0
- package/tools/cli/lib/update-transaction.js +8 -4
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** Validate, plan, record and render a pack intake; the host asks, the user answers. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const intake = require('../lib/intake');
|
|
6
|
+
|
|
7
|
+
const collect = (value, list) => [...list, value];
|
|
8
|
+
|
|
9
|
+
function describePlan(result) {
|
|
10
|
+
const lines = [
|
|
11
|
+
`${result.intake} (${result.pack}): ${result.ready ? 'ready' : 'not ready'} — ` +
|
|
12
|
+
`${result.counts.answered} answered, ${result.counts.defaulted} defaulted, ` +
|
|
13
|
+
`${result.counts.unknown} unknown, ${result.counts.pending} not asked yet.`,
|
|
14
|
+
];
|
|
15
|
+
if (result.missing.length) lines.push(`Must-haves still open: ${result.missing.join(', ')}`);
|
|
16
|
+
if (result.next.length) {
|
|
17
|
+
lines.push(`Ask next (at most ${result.maxPerMessage} per message):`);
|
|
18
|
+
for (const question of result.next) {
|
|
19
|
+
const hints = [];
|
|
20
|
+
if (question.mustHave) hints.push('must-have');
|
|
21
|
+
if (question.choices) hints.push(`choices: ${question.choices.join(', ')}`);
|
|
22
|
+
if (question.default !== undefined)
|
|
23
|
+
hints.push(`default: ${JSON.stringify(question.default)}`);
|
|
24
|
+
lines.push(
|
|
25
|
+
` - ${question.id} [${question.kind}] ${question.ask}${hints.length ? ` (${hints.join('; ')})` : ''}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const unknown of result.unknowns) {
|
|
30
|
+
lines.push(`Unknown: ${unknown.id}${unknown.risk ? ` — risk: ${unknown.risk}` : ''}`);
|
|
31
|
+
}
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
command: 'intake <action> [file]',
|
|
37
|
+
description:
|
|
38
|
+
'Pack intake: check a file, plan the next questions, record answers, render the brief',
|
|
39
|
+
options: [
|
|
40
|
+
['-d, --directory <path>', 'Project directory (default: current directory)'],
|
|
41
|
+
['--intake <file>', 'Intake file, relative to the project'],
|
|
42
|
+
['--answers <file>', 'Answers file, relative to the project (created by record)'],
|
|
43
|
+
[
|
|
44
|
+
'--set <id=VALUE>',
|
|
45
|
+
'Record an answer, or id=UNKNOWN to mark it unknown (repeatable)',
|
|
46
|
+
collect,
|
|
47
|
+
[],
|
|
48
|
+
],
|
|
49
|
+
['--default <id>', 'Record the proposed default of a question (repeatable)', collect, []],
|
|
50
|
+
['--reset', 'Start the answers file again, empty, for this intake (record)'],
|
|
51
|
+
['--out <file>', 'Brief to write (render)'],
|
|
52
|
+
['--json', 'Machine-readable output'],
|
|
53
|
+
],
|
|
54
|
+
action: (action, file, options = {}) => {
|
|
55
|
+
const projectDir = path.resolve(options.directory || process.cwd());
|
|
56
|
+
const needs = (name) => {
|
|
57
|
+
if (!options[name]) throw new Error(`--${name} is required for intake ${action}`);
|
|
58
|
+
return options[name];
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
let result;
|
|
62
|
+
let lines;
|
|
63
|
+
if (action === 'check') {
|
|
64
|
+
if (!file) throw new Error('Use intake check FILE.');
|
|
65
|
+
result = intake.check({ projectDir, file });
|
|
66
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
67
|
+
lines = result.ok
|
|
68
|
+
? [
|
|
69
|
+
`${result.file}: valid intake ${result.id} (${result.pack}), ${result.questions} questions, ${result.mustHave} must-have.`,
|
|
70
|
+
]
|
|
71
|
+
: [
|
|
72
|
+
`${result.file}: not a valid intake`,
|
|
73
|
+
...result.problems.map((problem) => ` - ${problem}`),
|
|
74
|
+
];
|
|
75
|
+
result = { status: result.ok ? 'valid' : 'invalid', ...result };
|
|
76
|
+
} else if (action === 'plan') {
|
|
77
|
+
result = intake.plan({
|
|
78
|
+
projectDir,
|
|
79
|
+
intakeFile: needs('intake'),
|
|
80
|
+
answersFile: needs('answers'),
|
|
81
|
+
});
|
|
82
|
+
process.exitCode = result.ready ? 0 : 2;
|
|
83
|
+
lines = describePlan(result);
|
|
84
|
+
result = { status: result.ready ? 'ready' : 'needs-answers', ...result };
|
|
85
|
+
} else if (action === 'record') {
|
|
86
|
+
result = intake.record({
|
|
87
|
+
projectDir,
|
|
88
|
+
intakeFile: needs('intake'),
|
|
89
|
+
answersFile: needs('answers'),
|
|
90
|
+
sets: options.set,
|
|
91
|
+
defaults: options.default,
|
|
92
|
+
reset: options.reset === true,
|
|
93
|
+
});
|
|
94
|
+
process.exitCode = 0;
|
|
95
|
+
lines = [
|
|
96
|
+
result.reset
|
|
97
|
+
? `Reset ${result.answersFile}${result.recorded.length ? `, recorded ${result.recorded.join(', ')}` : ''}.`
|
|
98
|
+
: `Recorded ${result.recorded.join(', ')} in ${result.answersFile}.`,
|
|
99
|
+
...describePlan(result),
|
|
100
|
+
];
|
|
101
|
+
result = { status: 'recorded', ...result };
|
|
102
|
+
} else if (action === 'render') {
|
|
103
|
+
result = intake.render({
|
|
104
|
+
projectDir,
|
|
105
|
+
intakeFile: needs('intake'),
|
|
106
|
+
answersFile: needs('answers'),
|
|
107
|
+
out: needs('out'),
|
|
108
|
+
});
|
|
109
|
+
process.exitCode = 0;
|
|
110
|
+
lines = [
|
|
111
|
+
`Brief written to ${result.out} (${result.bytes} bytes); ${result.ready ? 'the intake is ready' : 'a must-have is still open'}.`,
|
|
112
|
+
];
|
|
113
|
+
result = { status: 'rendered', ...result };
|
|
114
|
+
} else throw new Error('Use intake check FILE, plan, record or render.');
|
|
115
|
+
if (options.json) console.log(JSON.stringify({ schemaVersion: 1, ...result }, null, 2));
|
|
116
|
+
else for (const line of lines) console.log(line);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
if (options.json)
|
|
120
|
+
console.log(JSON.stringify({ schemaVersion: 1, status: 'error', error: error.message }));
|
|
121
|
+
else console.error('Intake: ' + error.message);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Run a swarm of sub-agents under a budget: deal, brief, check, file, tally; the host launches. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const swarm = require('../lib/swarm');
|
|
6
|
+
|
|
7
|
+
const USAGE =
|
|
8
|
+
'Use swarm init --definition FILE [--profile FILE | --units FILE] [--context NAME=FILE] [--n N] [--seed N] [--approved], ' +
|
|
9
|
+
'briefs RUN [--wave N], check RUN, save RUN UNIT --input FILE, tally RUN [--out FILE], inspect RUN, or plan RUN [--out FILE].';
|
|
10
|
+
|
|
11
|
+
function lines(action, result) {
|
|
12
|
+
if (result.status === 'needs-approval') return [`${result.run}: needs approval. ${result.next}`];
|
|
13
|
+
switch (action) {
|
|
14
|
+
case 'init':
|
|
15
|
+
return [
|
|
16
|
+
`${result.run}: ${result.kind} of ${result.n} units, budget ${result.budget}, approval ${result.approval}.`,
|
|
17
|
+
result.seed === null
|
|
18
|
+
? 'Units listed, no seed.'
|
|
19
|
+
: `Seed ${result.seed} (same seed, same cards).`,
|
|
20
|
+
`Next: ${result.next}`,
|
|
21
|
+
];
|
|
22
|
+
case 'briefs':
|
|
23
|
+
return [
|
|
24
|
+
`${result.run}: ${result.pending} of ${result.units} units need a sub-agent, in waves of ${result.wave}.`,
|
|
25
|
+
...result.waves.flatMap((wave) => [
|
|
26
|
+
`Wave ${wave.index}:`,
|
|
27
|
+
...wave.units.map((unit) => ` ${unit.id} ${unit.brief}`),
|
|
28
|
+
]),
|
|
29
|
+
`Each sub-agent: ${result.instruction}`,
|
|
30
|
+
`Disclosure: ${result.disclosure}`,
|
|
31
|
+
];
|
|
32
|
+
case 'check':
|
|
33
|
+
return [
|
|
34
|
+
`${result.run}: ${result.valid} of ${result.units} valid answers.`,
|
|
35
|
+
...result.pending.map(
|
|
36
|
+
(unit) => ` ${unit.id} ${unit.state}${unit.reason ? `: ${unit.reason}` : ''}`
|
|
37
|
+
),
|
|
38
|
+
];
|
|
39
|
+
case 'save':
|
|
40
|
+
return [`${result.run}: answer of ${result.unit} filed at ${result.answer}`];
|
|
41
|
+
case 'tally': {
|
|
42
|
+
const { valid, total, skipped } = result.tally.units;
|
|
43
|
+
return [
|
|
44
|
+
result.files.markdown,
|
|
45
|
+
`${valid} of ${total} units counted; ${skipped.length} skipped.`,
|
|
46
|
+
`Disclosure: ${result.tally.disclosure}`,
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
case 'inspect': {
|
|
50
|
+
const { counts } = result;
|
|
51
|
+
return [
|
|
52
|
+
`${result.run.id}: ${result.status} (${result.run.kind}, ${result.budget.decision})`,
|
|
53
|
+
`units ${counts.units}, briefed ${counts.briefed}, valid ${counts.valid}, invalid ${counts.invalid}, missing ${counts.missing}`,
|
|
54
|
+
`Disclosure: ${result.disclosure}`,
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
case 'plan':
|
|
58
|
+
return [
|
|
59
|
+
`${result.plan}: ${result.tasks} tasks, ${result.maxParallel} in parallel.`,
|
|
60
|
+
`Next: ${result.next}`,
|
|
61
|
+
];
|
|
62
|
+
default:
|
|
63
|
+
return [JSON.stringify(result)];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
command: 'swarm <action> [run] [unit]',
|
|
69
|
+
description:
|
|
70
|
+
'Deal, brief, check, file and tally a swarm of sub-agents within the configured budget',
|
|
71
|
+
options: [
|
|
72
|
+
['-d, --directory <path>', 'Project directory'],
|
|
73
|
+
['--definition <file>', 'Swarm definition (project-relative JSON) for init'],
|
|
74
|
+
['--profile <file>', 'Panel profile (project-relative JSON) for init'],
|
|
75
|
+
['--units <file>', 'Fanout units (project-relative JSON) for init'],
|
|
76
|
+
[
|
|
77
|
+
'--context <name=file>',
|
|
78
|
+
'Context file included verbatim in the briefs (repeatable)',
|
|
79
|
+
(value, all) => [...all, value],
|
|
80
|
+
[],
|
|
81
|
+
],
|
|
82
|
+
['--n <count>', 'Units to deal for a panel'],
|
|
83
|
+
['--seed <number>', 'Dealing seed; drawn once and recorded when absent'],
|
|
84
|
+
['--approved', 'Record that the user agreed to exceed the budget'],
|
|
85
|
+
['--wave <count>', 'Sub-agents launched together (default: the configured wave)'],
|
|
86
|
+
['--input <file>', 'Printed reply to file with save (project-relative)'],
|
|
87
|
+
['--out <file>', 'Output file for tally or plan, under .bmad-plus/swarm/'],
|
|
88
|
+
['--json', 'Machine-readable output'],
|
|
89
|
+
],
|
|
90
|
+
action: (action, runId, unitId, options = {}) => {
|
|
91
|
+
const directory = path.resolve(options.directory || process.cwd());
|
|
92
|
+
try {
|
|
93
|
+
let result;
|
|
94
|
+
if (action === 'init' && !runId) result = swarm.init(directory, options);
|
|
95
|
+
else if (action === 'briefs' && runId && !unitId)
|
|
96
|
+
result = swarm.briefs(directory, runId, options);
|
|
97
|
+
else if (action === 'check' && runId && !unitId) result = swarm.check(directory, runId);
|
|
98
|
+
else if (action === 'save' && runId && unitId)
|
|
99
|
+
result = swarm.save(directory, runId, unitId, options);
|
|
100
|
+
else if (action === 'tally' && runId && !unitId)
|
|
101
|
+
result = swarm.tally(directory, runId, options);
|
|
102
|
+
else if (action === 'inspect' && runId && !unitId) result = swarm.inspect(directory, runId);
|
|
103
|
+
else if (action === 'plan' && runId && !unitId)
|
|
104
|
+
result = swarm.plan(directory, runId, options);
|
|
105
|
+
else throw new Error(USAGE);
|
|
106
|
+
process.exitCode =
|
|
107
|
+
result.status === 'needs-approval' ? 2 : result.status === 'incomplete' ? 1 : 0;
|
|
108
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
109
|
+
else for (const line of lines(action, result)) console.log(line);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
if (options.json)
|
|
113
|
+
console.log(
|
|
114
|
+
JSON.stringify({ schemaVersion: 1, status: 'error', error: error.message }, null, 2)
|
|
115
|
+
);
|
|
116
|
+
else console.error('Swarm: ' + error.message.replace(/^Swarm: /, ''));
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -11,6 +11,7 @@ const pc = require('picocolors');
|
|
|
11
11
|
const { t } = require('../i18n');
|
|
12
12
|
const { PACKS, DERIVED } = require('../lib/packs');
|
|
13
13
|
const { readInstallManifest } = require('../lib/install-manifest');
|
|
14
|
+
const { externalModuleRoot } = require('../lib/pack-copy');
|
|
14
15
|
const { generateUserFiles, USER_CONFIG_MARKER } = require('../../build/generate-adapters');
|
|
15
16
|
const { contentHash } = require('../lib/installed-adapters');
|
|
16
17
|
|
|
@@ -44,6 +45,16 @@ function safeStat(projectDir, target) {
|
|
|
44
45
|
return stat;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
// The skills copied from an npm package are matched against that package. When it is gone
|
|
49
|
+
// or no longer the pinned version, nothing can be compared and they stay.
|
|
50
|
+
function resolvedExternalModule(externalModule, packageRoot) {
|
|
51
|
+
try {
|
|
52
|
+
return externalModuleRoot({ externalModule, projectRoot: packageRoot });
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
function removalPlan(projectDir, manifest, manifestPath) {
|
|
48
59
|
const packageRoot = path.join(__dirname, '..', '..', '..');
|
|
49
60
|
const sourceRoot = path.join(packageRoot, 'src', 'bmad-plus');
|
|
@@ -91,6 +102,12 @@ function removalPlan(projectDir, manifest, manifestPath) {
|
|
|
91
102
|
path.join(packageRoot, pack.externalPackage, 'skills'),
|
|
92
103
|
targetPath('.agents', 'skills')
|
|
93
104
|
);
|
|
105
|
+
if (pack.externalModule) {
|
|
106
|
+
const moduleRoot = resolvedExternalModule(pack.externalModule, packageRoot);
|
|
107
|
+
if (moduleRoot)
|
|
108
|
+
for (const name of pack.externalModule.skills)
|
|
109
|
+
matchSource(path.join(moduleRoot, 'skills', name), targetPath('.agents', 'skills', name));
|
|
110
|
+
}
|
|
94
111
|
}
|
|
95
112
|
for (const name of ['module.yaml', 'module-help.csv'])
|
|
96
113
|
matchSource(path.join(sourceRoot, name), targetPath('_bmad', name));
|
package/tools/cli/i18n.js
CHANGED
|
@@ -48,6 +48,7 @@ const LANGUAGES = {
|
|
|
48
48
|
guide_seo: 'SEO / GEO audit (360°)',
|
|
49
49
|
guide_backup: 'Smart project backup',
|
|
50
50
|
guide_animated: 'Animated luxury website',
|
|
51
|
+
guide_design: 'Design with Atelier',
|
|
51
52
|
guide_shield: '\ud83d\udee1\ufe0f GRC Compliance',
|
|
52
53
|
guide_example_shield_1: '\ud83d\udee1\ufe0f GRC: "Shield, audit my app for GDPR compliance"',
|
|
53
54
|
guide_example_shield_2: '\ud83d\udee1\ufe0f GRC: "Shield, gap analysis ISO 27001 vs NIST CSF"',
|
|
@@ -88,6 +89,7 @@ const LANGUAGES = {
|
|
|
88
89
|
guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
|
|
89
90
|
guide_example_backup: '🗂️ Backup: "/backup create" → ZIP timestamped',
|
|
90
91
|
guide_example_animated: '🎬 Animated: "/animated build hero.mp4"',
|
|
92
|
+
guide_example_design: '🎨 Design: "/design read" → Atelier interviews you, then builds',
|
|
91
93
|
guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
|
|
92
94
|
guide_memory: '🧠 Persistent Brain',
|
|
93
95
|
guide_dev_studio: '🏗️ Dev Studio',
|
|
@@ -141,6 +143,7 @@ const LANGUAGES = {
|
|
|
141
143
|
guide_seo: 'Audit SEO / GEO (360°)',
|
|
142
144
|
guide_backup: 'Sauvegarde intelligente',
|
|
143
145
|
guide_animated: 'Site web animé de luxe',
|
|
146
|
+
guide_design: 'Design avec Atelier',
|
|
144
147
|
guide_shield: '🛡️ Conformité GRC',
|
|
145
148
|
guide_example_shield_1: '🛡️ GRC : "Shield, audite mon app pour la conformité RGPD"',
|
|
146
149
|
guide_example_shield_2: '🛡️ GRC : "Shield, gap analysis ISO 27001 vs NIST CSF"',
|
|
@@ -180,6 +183,7 @@ const LANGUAGES = {
|
|
|
180
183
|
guide_example_seo: '🔍 SEO : "/seo audit https://example.com"',
|
|
181
184
|
guide_example_backup: '🗂️ Backup : "/backup create" → ZIP horodaté',
|
|
182
185
|
guide_example_animated: '🎬 Animé : "/animated build hero.mp4"',
|
|
186
|
+
guide_example_design: '🎨 Design : "/design read" → Atelier vous interroge, puis construit',
|
|
183
187
|
guide_example_osint: '🔍 OSINT : "Shadow, investigate John Doe"',
|
|
184
188
|
guide_memory: '🧠 Cerveau persistant',
|
|
185
189
|
guide_dev_studio: '🏗️ Dev Studio',
|
|
@@ -233,6 +237,7 @@ const LANGUAGES = {
|
|
|
233
237
|
guide_seo: 'Auditoría SEO / GEO (360°)',
|
|
234
238
|
guide_backup: 'Copia de seguridad inteligente',
|
|
235
239
|
guide_animated: 'Sitio web animado de lujo',
|
|
240
|
+
guide_design: 'Diseño con Atelier',
|
|
236
241
|
guide_shield: '\ud83d\udee1\ufe0f Cumplimiento GRC',
|
|
237
242
|
guide_example_shield_1: '\ud83d\udee1\ufe0f GRC: "Shield, audita mi app para GDPR"',
|
|
238
243
|
guide_example_shield_2:
|
|
@@ -274,6 +279,7 @@ const LANGUAGES = {
|
|
|
274
279
|
guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
|
|
275
280
|
guide_example_backup: '🗂️ Backup: "/backup create" → ZIP con marca de tiempo',
|
|
276
281
|
guide_example_animated: '🎬 Animado: "/animated build hero.mp4"',
|
|
282
|
+
guide_example_design: '🎨 Diseño: "/design read" → Atelier te entrevista y luego construye',
|
|
277
283
|
guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
|
|
278
284
|
guide_memory: '🧠 Cerebro persistente',
|
|
279
285
|
guide_dev_studio: '🏗️ Dev Studio',
|
|
@@ -328,6 +334,7 @@ const LANGUAGES = {
|
|
|
328
334
|
guide_seo: 'SEO / GEO Audit (360°)',
|
|
329
335
|
guide_backup: 'Intelligente Projektsicherung',
|
|
330
336
|
guide_animated: 'Animierte Luxus-Website',
|
|
337
|
+
guide_design: 'Design mit Atelier',
|
|
331
338
|
guide_shield: '\ud83d\udee1\ufe0f GRC-Compliance',
|
|
332
339
|
guide_example_shield_1:
|
|
333
340
|
'\ud83d\udee1\ufe0f GRC: "Shield, pr\u00fcfe meine App auf DSGVO-Konformit\u00e4t"',
|
|
@@ -370,6 +377,7 @@ const LANGUAGES = {
|
|
|
370
377
|
guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
|
|
371
378
|
guide_example_backup: '🗂️ Backup: "/backup create" → ZIP mit Zeitstempel',
|
|
372
379
|
guide_example_animated: '🎬 Animiert: "/animated build hero.mp4"',
|
|
380
|
+
guide_example_design: '🎨 Design: "/design read" → Atelier befragt dich, dann baut es',
|
|
373
381
|
guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
|
|
374
382
|
guide_memory: '🧠 Beständiges Gehirn',
|
|
375
383
|
guide_dev_studio: '🏗️ Dev Studio',
|
|
@@ -424,6 +432,7 @@ const LANGUAGES = {
|
|
|
424
432
|
guide_seo: 'Auditoria SEO / GEO (360°)',
|
|
425
433
|
guide_backup: 'Backup inteligente do projeto',
|
|
426
434
|
guide_animated: 'Site animado de luxo',
|
|
435
|
+
guide_design: 'Design com Atelier',
|
|
427
436
|
guide_shield: '\ud83d\udee1\ufe0f Conformidade GRC',
|
|
428
437
|
guide_example_shield_1: '\ud83d\udee1\ufe0f GRC: "Shield, audite meu app para LGPD"',
|
|
429
438
|
guide_example_shield_2:
|
|
@@ -465,6 +474,7 @@ const LANGUAGES = {
|
|
|
465
474
|
guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
|
|
466
475
|
guide_example_backup: '🗂️ Backup: "/backup create" → ZIP com timestamp',
|
|
467
476
|
guide_example_animated: '🎬 Animado: "/animated build hero.mp4"',
|
|
477
|
+
guide_example_design: '🎨 Design: "/design read" → Atelier entrevista você, depois constrói',
|
|
468
478
|
guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
|
|
469
479
|
guide_memory: '🧠 Cérebro persistente',
|
|
470
480
|
guide_dev_studio: '🏗️ Dev Studio',
|
|
@@ -519,6 +529,7 @@ const LANGUAGES = {
|
|
|
519
529
|
guide_seo: 'SEO / GEO аудит (360°)',
|
|
520
530
|
guide_backup: 'Умное резервное копирование',
|
|
521
531
|
guide_animated: 'Анимированный люксовый сайт',
|
|
532
|
+
guide_design: 'Дизайн с Atelier',
|
|
522
533
|
guide_shield: '🛡️ Соответствие GRC',
|
|
523
534
|
guide_example_shield_1: '🛡️ GRC: "Shield, проверь моё приложение на GDPR"',
|
|
524
535
|
guide_example_shield_2: '🛡️ GRC: "Shield, gap-анализ ISO 27001 vs NIST CSF"',
|
|
@@ -557,6 +568,7 @@ const LANGUAGES = {
|
|
|
557
568
|
guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
|
|
558
569
|
guide_example_backup: '🗂️ Бэкап: "/backup create" → ZIP',
|
|
559
570
|
guide_example_animated: '🎬 Анимация: "/animated build hero.mp4"',
|
|
571
|
+
guide_example_design: '🎨 Дизайн: "/design read" → Atelier опрашивает вас, затем строит',
|
|
560
572
|
guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
|
|
561
573
|
guide_memory: '🧠 Постоянный мозг',
|
|
562
574
|
guide_dev_studio: '🏗️ Dev Studio',
|
|
@@ -610,6 +622,7 @@ const LANGUAGES = {
|
|
|
610
622
|
guide_seo: 'SEO / GEO 审计 (360°)',
|
|
611
623
|
guide_backup: '智能项目备份',
|
|
612
624
|
guide_animated: '动画奢华网站',
|
|
625
|
+
guide_design: '与 Atelier 一起设计',
|
|
613
626
|
guide_shield: '🛡️ GRC 合规',
|
|
614
627
|
guide_example_shield_1: '🛡️ GRC: "Shield, 审查我的应用 GDPR 合规性"',
|
|
615
628
|
guide_example_shield_2: '🛡️ GRC: "Shield, ISO 27001 vs NIST CSF 差距分析"',
|
|
@@ -645,6 +658,7 @@ const LANGUAGES = {
|
|
|
645
658
|
guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
|
|
646
659
|
guide_example_backup: '🗂️ 备份: "/backup create" → ZIP',
|
|
647
660
|
guide_example_animated: '🎬 动画: "/animated build hero.mp4"',
|
|
661
|
+
guide_example_design: '🎨 设计: "/design read" → Atelier 先向你提问,再构建',
|
|
648
662
|
guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
|
|
649
663
|
guide_memory: '🧠 持久记忆',
|
|
650
664
|
guide_dev_studio: '🏗️ Dev Studio',
|
|
@@ -698,6 +712,7 @@ const LANGUAGES = {
|
|
|
698
712
|
guide_seo: 'ביקורת SEO / GEO (360°)',
|
|
699
713
|
guide_backup: 'גיבוי חכם של הפרויקט',
|
|
700
714
|
guide_animated: 'אתר אנימציה יוקרתי',
|
|
715
|
+
guide_design: 'עיצוב עם Atelier',
|
|
701
716
|
guide_shield: '🛡️ תאימות GRC',
|
|
702
717
|
guide_example_shield_1: '🛡️ GRC: "Shield, בדוק את האפליקציה שלי ל-GDPR"',
|
|
703
718
|
guide_example_shield_2: '🛡️ GRC: "Shield, ניתוח פערים ISO 27001 מול NIST CSF"',
|
|
@@ -733,6 +748,7 @@ const LANGUAGES = {
|
|
|
733
748
|
guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
|
|
734
749
|
guide_example_backup: '🗂️ גיבוי: "/backup create" → ZIP',
|
|
735
750
|
guide_example_animated: '🎬 אנימציה: "/animated build hero.mp4"',
|
|
751
|
+
guide_example_design: '🎨 עיצוב: "/design read" → Atelier מראיין אותך ואז בונה',
|
|
736
752
|
guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
|
|
737
753
|
guide_memory: '🧠 זיכרון מתמשך',
|
|
738
754
|
guide_dev_studio: '🏗️ Dev Studio',
|
|
@@ -786,6 +802,7 @@ const LANGUAGES = {
|
|
|
786
802
|
guide_seo: 'SEO / GEO 監査 (360°)',
|
|
787
803
|
guide_backup: 'スマートプロジェクトバックアップ',
|
|
788
804
|
guide_animated: 'アニメーション高級ウェブサイト',
|
|
805
|
+
guide_design: 'Atelier でデザイン',
|
|
789
806
|
guide_shield: '🛡️ GRC コンプライアンス',
|
|
790
807
|
guide_example_shield_1: '🛡️ GRC: "Shield, アプリのGDPR準拠を監査"',
|
|
791
808
|
guide_example_shield_2: '🛡️ GRC: "Shield, ISO 27001 vs NIST CSF ギャップ分析"',
|
|
@@ -825,6 +842,7 @@ const LANGUAGES = {
|
|
|
825
842
|
guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
|
|
826
843
|
guide_example_backup: '🗂️ バックアップ: "/backup create" → ZIP',
|
|
827
844
|
guide_example_animated: '🎬 アニメ: "/animated build hero.mp4"',
|
|
845
|
+
guide_example_design: '🎨 デザイン: "/design read" → Atelier がヒアリングしてから構築します',
|
|
828
846
|
guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
|
|
829
847
|
guide_memory: '🧠 永続的な脳',
|
|
830
848
|
guide_dev_studio: '🏗️ Dev Studio',
|
|
@@ -879,6 +897,7 @@ const LANGUAGES = {
|
|
|
879
897
|
guide_seo: 'Audit SEO / GEO (360°)',
|
|
880
898
|
guide_backup: 'Backup intelligente del progetto',
|
|
881
899
|
guide_animated: 'Sito web animato di lusso',
|
|
900
|
+
guide_design: 'Design con Atelier',
|
|
882
901
|
guide_shield: '\ud83d\udee1\ufe0f Conformit\u00e0 GRC',
|
|
883
902
|
guide_example_shield_1: '\ud83d\udee1\ufe0f GRC: "Shield, verifica la mia app per il GDPR"',
|
|
884
903
|
guide_example_shield_2: '\ud83d\udee1\ufe0f GRC: "Shield, gap analysis ISO 27001 vs NIST CSF"',
|
|
@@ -918,6 +937,7 @@ const LANGUAGES = {
|
|
|
918
937
|
guide_example_seo: '🔍 SEO: "/seo audit https://example.com"',
|
|
919
938
|
guide_example_backup: '🗂️ Backup: "/backup create" → ZIP con timestamp',
|
|
920
939
|
guide_example_animated: '🎬 Animato: "/animated build hero.mp4"',
|
|
940
|
+
guide_example_design: '🎨 Design: "/design read" → Atelier ti intervista, poi costruisce',
|
|
921
941
|
guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
|
|
922
942
|
guide_memory: '🧠 Memoria persistente',
|
|
923
943
|
guide_dev_studio: '🏗️ Dev Studio',
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/** Read, plan, record and render a pack intake inside one project; the contract decides. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
const yaml = require('js-yaml');
|
|
8
|
+
const { safeTarget } = require('../../build/generate-adapters');
|
|
9
|
+
const contract = require('../../build/intake-contract');
|
|
10
|
+
|
|
11
|
+
const MAX_FILE = 1024 * 1024;
|
|
12
|
+
// Trees the intake never writes into, wherever they sit in the project.
|
|
13
|
+
const RESERVED_DIRS = new Set(['.git', '_bmad', '.bmad-plus', 'node_modules']);
|
|
14
|
+
|
|
15
|
+
/** A user-supplied path, kept inside the project and free of symlinks. */
|
|
16
|
+
function projectFile(projectDir, file) {
|
|
17
|
+
if (typeof file !== 'string' || file.trim() === '') throw new Error('a file path is required');
|
|
18
|
+
const root = path.resolve(projectDir);
|
|
19
|
+
const relative = path.relative(root, path.resolve(root, file)).split(path.sep).join('/');
|
|
20
|
+
if (!relative || relative === '..' || relative.startsWith('../'))
|
|
21
|
+
throw new Error(`${file}: outside the project directory ${root}`);
|
|
22
|
+
return { relative, absolute: safeTarget(root, relative) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A path the intake may write: outside the reserved trees, and never through a link. */
|
|
26
|
+
function writableFile(projectDir, file) {
|
|
27
|
+
const target = projectFile(projectDir, file);
|
|
28
|
+
const reserved = target.relative.split('/').find((part) => RESERVED_DIRS.has(part));
|
|
29
|
+
if (reserved) throw new Error(`${target.relative}: the intake never writes under ${reserved}/`);
|
|
30
|
+
return target;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readBounded(absolute, label) {
|
|
34
|
+
const stat = fs.lstatSync(absolute);
|
|
35
|
+
if (!stat.isFile()) throw new Error(`${label}: not a regular file`);
|
|
36
|
+
if (stat.size > MAX_FILE) throw new Error(`${label}: larger than ${MAX_FILE} bytes`);
|
|
37
|
+
return fs.readFileSync(absolute, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function describe(problems) {
|
|
41
|
+
return problems.map((problem) => ` - ${problem}`).join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Parse and validate an intake file; throws listing every problem. */
|
|
45
|
+
function loadIntake(projectDir, file) {
|
|
46
|
+
const target = projectFile(projectDir, file);
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = yaml.load(readBounded(target.absolute, target.relative), { schema: yaml.JSON_SCHEMA });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(`${target.relative}: ${error.message}`, { cause: error });
|
|
52
|
+
}
|
|
53
|
+
const result = contract.validateIntake(parsed);
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
throw new Error(`${target.relative} is not a valid intake:\n${describe(result.problems)}`);
|
|
56
|
+
return { file: target.relative, intake: parsed };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The parsed JSON of an existing answers file when it belongs to this intake; else throws. */
|
|
60
|
+
function ownedAnswers(target, intake) {
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(readBounded(target.absolute, target.relative));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`${target.relative}: exists and is not an answers file of intake "${intake.id}" (${error.message}); move it away or pick another path`,
|
|
67
|
+
{ cause: error }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (!parsed || typeof parsed !== 'object' || parsed.intake !== intake.id) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`${target.relative}: exists and belongs to ${JSON.stringify(parsed && parsed.intake)}, not to intake "${intake.id}"; move it away or pick another path`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Parse and validate an answers file; an absent file means no answer yet. */
|
|
79
|
+
function loadAnswers(projectDir, file, intake) {
|
|
80
|
+
const target = writableFile(projectDir, file);
|
|
81
|
+
if (!fs.existsSync(target.absolute)) {
|
|
82
|
+
return { file: target.relative, answers: contract.emptyAnswers(intake), exists: false };
|
|
83
|
+
}
|
|
84
|
+
const parsed = ownedAnswers(target, intake);
|
|
85
|
+
const result = contract.validateAnswers(parsed, intake);
|
|
86
|
+
if (!result.ok)
|
|
87
|
+
throw new Error(
|
|
88
|
+
`${target.relative} does not answer this intake:\n${describe(result.problems)}\nRecord again with --reset to start over.`
|
|
89
|
+
);
|
|
90
|
+
return { file: target.relative, answers: parsed, exists: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function atomicWrite(projectDir, relative, content) {
|
|
94
|
+
const target = safeTarget(projectDir, relative);
|
|
95
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
96
|
+
const temporaryFile = `${relative}.${crypto.randomUUID()}.tmp`;
|
|
97
|
+
const temporary = safeTarget(projectDir, temporaryFile);
|
|
98
|
+
let fd;
|
|
99
|
+
try {
|
|
100
|
+
fd = fs.openSync(temporary, 'wx');
|
|
101
|
+
fs.writeFileSync(fd, content);
|
|
102
|
+
fs.fsyncSync(fd);
|
|
103
|
+
fs.closeSync(fd);
|
|
104
|
+
fd = undefined;
|
|
105
|
+
safeTarget(projectDir, relative);
|
|
106
|
+
fs.renameSync(temporary, target);
|
|
107
|
+
} finally {
|
|
108
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
109
|
+
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function check({ projectDir, file }) {
|
|
114
|
+
const target = projectFile(projectDir, file);
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = yaml.load(readBounded(target.absolute, target.relative), { schema: yaml.JSON_SCHEMA });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return { file: target.relative, ok: false, problems: [error.message] };
|
|
120
|
+
}
|
|
121
|
+
const result = contract.validateIntake(parsed);
|
|
122
|
+
const summary = result.ok
|
|
123
|
+
? {
|
|
124
|
+
id: parsed.id,
|
|
125
|
+
pack: parsed.pack,
|
|
126
|
+
questions: contract.listQuestions(parsed).length,
|
|
127
|
+
mustHave: contract.listQuestions(parsed).filter((q) => q.must_have === true).length,
|
|
128
|
+
}
|
|
129
|
+
: {};
|
|
130
|
+
return { file: target.relative, ...result, ...summary };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function plan({ projectDir, intakeFile, answersFile }) {
|
|
134
|
+
const { intake } = loadIntake(projectDir, intakeFile);
|
|
135
|
+
const { answers, file } = loadAnswers(projectDir, answersFile, intake);
|
|
136
|
+
return { answersFile: file, ...contract.planIntake(intake, answers) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* `sets` is a list of "id=VALUE" texts, `defaults` a list of question ids. With
|
|
141
|
+
* `reset`, the file starts again empty for this intake (an existing file must
|
|
142
|
+
* already belong to it), then takes the sets and defaults of the same call.
|
|
143
|
+
*/
|
|
144
|
+
function record({ projectDir, intakeFile, answersFile, sets = [], defaults = [], reset = false }) {
|
|
145
|
+
const { intake } = loadIntake(projectDir, intakeFile);
|
|
146
|
+
let loaded;
|
|
147
|
+
if (reset) {
|
|
148
|
+
const target = writableFile(projectDir, answersFile);
|
|
149
|
+
if (fs.existsSync(target.absolute)) ownedAnswers(target, intake);
|
|
150
|
+
loaded = { file: target.relative, answers: contract.emptyAnswers(intake) };
|
|
151
|
+
} else loaded = loadAnswers(projectDir, answersFile, intake);
|
|
152
|
+
const pairs = {};
|
|
153
|
+
for (const assignment of sets) {
|
|
154
|
+
const at = typeof assignment === 'string' ? assignment.indexOf('=') : -1;
|
|
155
|
+
if (at < 1) throw new Error(`--set expects id=VALUE, got "${assignment}"`);
|
|
156
|
+
const id = assignment.slice(0, at);
|
|
157
|
+
if (Object.hasOwn(pairs, id)) throw new Error(`question "${id}" is set twice in one call`);
|
|
158
|
+
pairs[id] = assignment.slice(at + 1);
|
|
159
|
+
}
|
|
160
|
+
if (!reset && Object.keys(pairs).length === 0 && defaults.length === 0)
|
|
161
|
+
throw new Error('nothing to record: pass --set id=VALUE, --default id or --reset');
|
|
162
|
+
const answers = contract.recordAnswers(intake, loaded.answers, { sets: pairs, defaults });
|
|
163
|
+
const result = contract.validateAnswers(answers, intake);
|
|
164
|
+
if (!result.ok) throw new Error(`recorded answers are invalid:\n${describe(result.problems)}`);
|
|
165
|
+
atomicWrite(projectDir, loaded.file, JSON.stringify(answers, null, 2) + '\n');
|
|
166
|
+
return {
|
|
167
|
+
answersFile: loaded.file,
|
|
168
|
+
reset,
|
|
169
|
+
recorded: [...Object.keys(pairs), ...defaults],
|
|
170
|
+
...contract.planIntake(intake, answers),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The brief overwrites only a file it wrote before for the same intake. */
|
|
175
|
+
function render({ projectDir, intakeFile, answersFile, out }) {
|
|
176
|
+
const { intake } = loadIntake(projectDir, intakeFile);
|
|
177
|
+
const { answers } = loadAnswers(projectDir, answersFile, intake);
|
|
178
|
+
const target = writableFile(projectDir, out);
|
|
179
|
+
if (fs.existsSync(target.absolute)) {
|
|
180
|
+
const marker = contract.briefMarker(intake);
|
|
181
|
+
const head = readBounded(target.absolute, target.relative).split(/\r?\n/, 2);
|
|
182
|
+
if (head[1] !== marker)
|
|
183
|
+
throw new Error(
|
|
184
|
+
`${target.relative}: exists and is not a brief of intake "${intake.id}" (its second line is not the ownership marker); move it away or pick another path`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const brief = contract.renderBrief(intake, answers);
|
|
188
|
+
atomicWrite(projectDir, target.relative, brief);
|
|
189
|
+
const planned = contract.planIntake(intake, answers);
|
|
190
|
+
return { out: target.relative, bytes: Buffer.byteLength(brief), ready: planned.ready };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
projectFile,
|
|
195
|
+
writableFile,
|
|
196
|
+
loadIntake,
|
|
197
|
+
loadAnswers,
|
|
198
|
+
check,
|
|
199
|
+
plan,
|
|
200
|
+
record,
|
|
201
|
+
render,
|
|
202
|
+
};
|