convoke-agents 3.1.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +37 -10
- package/_bmad/bme/_artifacts/config.yaml +15 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/SKILL.md +6 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-01-scope.md +138 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-02-dryrun.md +199 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-03-resolve.md +174 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-04-execute.md +213 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/workflow.md +85 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/SKILL.md +6 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-01-scan.md +131 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-02-explore.md +131 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-03-recommend.md +149 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/workflow.md +78 -0
- package/_bmad/bme/_portability/skills/bmad-export-skill/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-export-skill/workflow.md +74 -0
- package/_bmad/bme/_portability/skills/bmad-generate-catalog/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-generate-catalog/workflow.md +42 -0
- package/_bmad/bme/_portability/skills/bmad-seed-catalog/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-seed-catalog/workflow.md +61 -0
- package/_bmad/bme/_portability/skills/bmad-validate-exports/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-validate-exports/workflow.md +43 -0
- package/_bmad/bme/_team-factory/agents/team-factory.md +128 -0
- package/_bmad/bme/_team-factory/config.yaml +13 -0
- package/_bmad/bme/_team-factory/lib/cascade-logic.js +184 -0
- package/_bmad/bme/_team-factory/lib/collision-detector.js +228 -0
- package/_bmad/bme/_team-factory/lib/manifest-tracker.js +214 -0
- package/_bmad/bme/_team-factory/lib/spec-differ.js +176 -0
- package/_bmad/bme/_team-factory/lib/spec-parser.js +201 -0
- package/_bmad/bme/_team-factory/lib/spec-writer.js +128 -0
- package/_bmad/bme/_team-factory/lib/types/factory-types.js +193 -0
- package/_bmad/bme/_team-factory/lib/utils/csv-utils.js +62 -0
- package/_bmad/bme/_team-factory/lib/utils/naming-utils.js +45 -0
- package/_bmad/bme/_team-factory/lib/validators/end-to-end-validator.js +898 -0
- package/_bmad/bme/_team-factory/lib/writers/activation-validator.js +175 -0
- package/_bmad/bme/_team-factory/lib/writers/config-appender.js +192 -0
- package/_bmad/bme/_team-factory/lib/writers/config-creator.js +215 -0
- package/_bmad/bme/_team-factory/lib/writers/csv-appender.js +118 -0
- package/_bmad/bme/_team-factory/lib/writers/csv-creator.js +190 -0
- package/_bmad/bme/_team-factory/lib/writers/registry-appender.js +372 -0
- package/_bmad/bme/_team-factory/lib/writers/registry-writer.js +409 -0
- package/_bmad/bme/_team-factory/module-help.csv +3 -0
- package/_bmad/bme/_team-factory/schemas/schema-independent.json +147 -0
- package/_bmad/bme/_team-factory/schemas/schema-sequential.json +242 -0
- package/_bmad/bme/_team-factory/templates/team-spec-template.yaml +86 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-01-scope.md +105 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-02-connect.md +110 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-03-review.md +116 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-04-generate.md +160 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-05-validate.md +146 -0
- package/_bmad/bme/_team-factory/workflows/step-00-route.md +76 -0
- package/_bmad/bme/_vortex/config.yaml +4 -4
- package/package.json +13 -7
- package/scripts/convoke-doctor.js +172 -1
- package/scripts/install-gyre-agents.js +0 -0
- package/scripts/lib/artifact-utils.js +521 -13
- package/scripts/lib/portfolio/portfolio-engine.js +301 -34
- package/scripts/lib/portfolio/rules/artifact-chain-rule.js +33 -3
- package/scripts/lib/portfolio/rules/conflict-resolver.js +22 -0
- package/scripts/migrate-artifacts.js +69 -10
- package/scripts/portability/catalog-generator.js +353 -0
- package/scripts/portability/classify-skills.js +646 -0
- package/scripts/portability/convoke-export.js +522 -0
- package/scripts/portability/export-engine.js +1156 -0
- package/scripts/portability/generate-adapters.js +79 -0
- package/scripts/portability/manifest-csv.js +147 -0
- package/scripts/portability/seed-catalog-repo.js +427 -0
- package/scripts/portability/templates/canonical-example.md +102 -0
- package/scripts/portability/templates/canonical-format.md +218 -0
- package/scripts/portability/templates/readme-template.md +72 -0
- package/scripts/portability/test-constants.js +42 -0
- package/scripts/portability/validate-classification.js +529 -0
- package/scripts/portability/validate-exports.js +348 -0
- package/scripts/update/lib/agent-registry.js +35 -0
- package/scripts/update/lib/config-merger.js +140 -10
- package/scripts/update/lib/refresh-installation.js +293 -8
- package/scripts/update/lib/utils.js +27 -1
- package/scripts/update/lib/validator.js +114 -4
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const { toKebab, deriveWorkflowName } = require('../utils/naming-utils');
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('../types/factory-types')} Types */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Write a new module block to agent-registry.js with Full Write Safety Protocol.
|
|
12
|
+
*
|
|
13
|
+
* Protocol: stage → validate → check → apply → verify → rollback
|
|
14
|
+
*
|
|
15
|
+
* This is the ONLY writer that uses the full protocol because agent-registry.js
|
|
16
|
+
* is a shared file consumed by refresh-installation, validator, convoke-doctor,
|
|
17
|
+
* installer, index.js, and migration-runner.
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} specData - Parsed team spec with agents enriched with persona fields
|
|
20
|
+
* @param {string} registryPath - Absolute path to agent-registry.js
|
|
21
|
+
* @param {Object} [options]
|
|
22
|
+
* @param {boolean} [options.skipDirtyCheck] - Skip git dirty-tree detection (for tests)
|
|
23
|
+
* @returns {Promise<import('../types/factory-types').RegistryResult>}
|
|
24
|
+
*/
|
|
25
|
+
async function writeRegistryBlock(specData, registryPath, options = {}) {
|
|
26
|
+
if (!specData.team_name_kebab || !specData.team_name_kebab.trim()) {
|
|
27
|
+
return { success: false, written: [], skipped: [], errors: ['team_name_kebab is required and must not be empty'], rollbackApplied: false };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const prefix = derivePrefix(specData.team_name_kebab);
|
|
31
|
+
const teamName = specData.team_name || specData.team_name_kebab;
|
|
32
|
+
|
|
33
|
+
// --- Idempotency check ---
|
|
34
|
+
let currentContent;
|
|
35
|
+
try {
|
|
36
|
+
currentContent = await fs.readFile(registryPath, 'utf8');
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return { success: false, written: [], skipped: [], errors: [`Cannot read registry file: ${err.message}`], rollbackApplied: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (currentContent.includes(`const ${prefix}_AGENTS`)) {
|
|
42
|
+
return { success: true, written: [], skipped: ['block already exists'], errors: [], rollbackApplied: false };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- 1. STAGE: Build module block + export additions ---
|
|
46
|
+
const workflowNames = buildWorkflowNames(specData);
|
|
47
|
+
const moduleBlock = buildModuleBlock(specData, prefix, teamName, workflowNames);
|
|
48
|
+
const exportNames = buildExportNames(prefix);
|
|
49
|
+
|
|
50
|
+
// --- 2. VALIDATE: Syntax, prefix uniqueness, additive-only ---
|
|
51
|
+
const validateErrors = validateStaged(moduleBlock, prefix, currentContent);
|
|
52
|
+
if (validateErrors.length > 0) {
|
|
53
|
+
return { success: false, written: [], skipped: [], errors: validateErrors, rollbackApplied: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Validate staged block syntax via temp file
|
|
57
|
+
const syntaxError = await validateSyntax(moduleBlock, prefix);
|
|
58
|
+
if (syntaxError) {
|
|
59
|
+
return { success: false, written: [], skipped: [], errors: [syntaxError], rollbackApplied: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- 3. CHECK: Dirty-tree detection ---
|
|
63
|
+
if (!options.skipDirtyCheck) {
|
|
64
|
+
const dirtyResult = checkDirtyTree(registryPath);
|
|
65
|
+
if (dirtyResult.dirty) {
|
|
66
|
+
return { success: false, written: [], skipped: [], errors: [], rollbackApplied: false, dirty: true, diff: dirtyResult.diff };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- 4. APPLY: Read → save .bak → insert → write ---
|
|
71
|
+
const bakPath = `${registryPath}.bak`;
|
|
72
|
+
if (await fs.pathExists(bakPath)) {
|
|
73
|
+
return { success: false, written: [], skipped: [], errors: ['Stale .bak file exists — a previous run may have crashed. Remove it manually before retrying.'], rollbackApplied: false };
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await fs.writeFile(bakPath, currentContent, 'utf8');
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return { success: false, written: [], skipped: [], errors: [`Failed to create backup: ${err.message}`], rollbackApplied: false };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let modified;
|
|
82
|
+
try {
|
|
83
|
+
modified = applyInsertions(currentContent, moduleBlock, exportNames);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
await fs.remove(bakPath);
|
|
86
|
+
return { success: false, written: [], skipped: [], errors: [`Insertion failed: ${err.message}`], rollbackApplied: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await fs.writeFile(registryPath, modified, 'utf8');
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Restore from backup
|
|
93
|
+
await fs.writeFile(registryPath, currentContent, 'utf8');
|
|
94
|
+
await fs.remove(bakPath);
|
|
95
|
+
return { success: false, written: [], skipped: [], errors: [`Write failed: ${err.message}`], rollbackApplied: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- 5. VERIFY: Re-read + node require() ---
|
|
99
|
+
const verifyError = verifyRequire(registryPath);
|
|
100
|
+
if (verifyError) {
|
|
101
|
+
// Rollback
|
|
102
|
+
await fs.writeFile(registryPath, currentContent, 'utf8');
|
|
103
|
+
await fs.remove(bakPath);
|
|
104
|
+
return { success: false, written: [], skipped: [], errors: [verifyError], rollbackApplied: true };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Cleanup: Remove .bak ---
|
|
108
|
+
await fs.remove(bakPath);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
success: true,
|
|
112
|
+
written: [`${prefix}_AGENTS`, `${prefix}_WORKFLOWS`, `${prefix}_AGENT_FILES`, `${prefix}_AGENT_IDS`, `${prefix}_WORKFLOW_NAMES`],
|
|
113
|
+
skipped: [],
|
|
114
|
+
errors: [],
|
|
115
|
+
rollbackApplied: false
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Derive SCREAMING_SNAKE_CASE prefix from team name kebab.
|
|
123
|
+
* @param {string} teamNameKebab - e.g. "test-team"
|
|
124
|
+
* @returns {string} - e.g. "TEST_TEAM"
|
|
125
|
+
*/
|
|
126
|
+
function derivePrefix(teamNameKebab) {
|
|
127
|
+
return (teamNameKebab || '').replace(/^_/, '').replace(/-/g, '_').toUpperCase();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build workflow names from spec data using shared deriveWorkflowName().
|
|
132
|
+
* @param {Object} specData
|
|
133
|
+
* @returns {Object} Map of agent_id → workflow_name
|
|
134
|
+
*/
|
|
135
|
+
function buildWorkflowNames(specData) {
|
|
136
|
+
const map = {};
|
|
137
|
+
for (const agent of (specData.agents || [])) {
|
|
138
|
+
map[agent.id] = deriveWorkflowName(agent, specData);
|
|
139
|
+
}
|
|
140
|
+
return map;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Escape single quotes in a string for JS string literal output.
|
|
145
|
+
* @param {string} str
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
function escapeSingleQuotes(str) {
|
|
149
|
+
if (!str) return '';
|
|
150
|
+
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build a registry agent entry object from enriched agent spec data.
|
|
155
|
+
* @param {Object} agentSpec - Agent spec with persona fields
|
|
156
|
+
* @param {string} teamNameKebab - Team name for stream field
|
|
157
|
+
* @returns {Object}
|
|
158
|
+
*/
|
|
159
|
+
function buildAgentEntry(agentSpec, teamNameKebab) {
|
|
160
|
+
return {
|
|
161
|
+
id: agentSpec.id,
|
|
162
|
+
name: agentSpec.name || agentSpec.id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
163
|
+
icon: agentSpec.icon || '\u{2699}',
|
|
164
|
+
title: agentSpec.role || agentSpec.title || agentSpec.id,
|
|
165
|
+
stream: teamNameKebab,
|
|
166
|
+
persona: {
|
|
167
|
+
role: agentSpec.persona?.role || agentSpec.role || '',
|
|
168
|
+
identity: agentSpec.persona?.identity || '',
|
|
169
|
+
communication_style: agentSpec.persona?.communication_style || '',
|
|
170
|
+
expertise: agentSpec.persona?.expertise || '',
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build the module block as a JS string.
|
|
177
|
+
* @param {Object} specData
|
|
178
|
+
* @param {string} prefix - SCREAMING_SNAKE_CASE
|
|
179
|
+
* @param {string} teamName - Display name
|
|
180
|
+
* @param {Object} workflowNames - Map of agent_id → workflow_name
|
|
181
|
+
* @returns {string}
|
|
182
|
+
*/
|
|
183
|
+
function buildModuleBlock(specData, prefix, teamName, workflowNames) {
|
|
184
|
+
const agents = (specData.agents || []).map(a => buildAgentEntry(a, specData.team_name_kebab));
|
|
185
|
+
const workflows = [];
|
|
186
|
+
for (const agent of (specData.agents || [])) {
|
|
187
|
+
const wfName = workflowNames[agent.id];
|
|
188
|
+
if (wfName) {
|
|
189
|
+
workflows.push({ name: wfName, agent: agent.id });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const lines = [];
|
|
194
|
+
|
|
195
|
+
// Section comment (padded to ~72 chars like Gyre)
|
|
196
|
+
const label = `── ${teamName} Module `;
|
|
197
|
+
const pad = Math.max(0, 72 - 3 - label.length);
|
|
198
|
+
lines.push(`// ${label}${'─'.repeat(pad)}`);
|
|
199
|
+
|
|
200
|
+
// AGENTS array
|
|
201
|
+
lines.push(`const ${prefix}_AGENTS = [`);
|
|
202
|
+
for (const agent of agents) {
|
|
203
|
+
lines.push(' {');
|
|
204
|
+
lines.push(` id: '${escapeSingleQuotes(agent.id)}', name: '${escapeSingleQuotes(agent.name)}', icon: '${agent.icon}',`);
|
|
205
|
+
lines.push(` title: '${escapeSingleQuotes(agent.title)}', stream: '${escapeSingleQuotes(agent.stream)}',`);
|
|
206
|
+
lines.push(' persona: {');
|
|
207
|
+
lines.push(` role: '${escapeSingleQuotes(agent.persona.role)}',`);
|
|
208
|
+
lines.push(` identity: '${escapeSingleQuotes(agent.persona.identity)}',`);
|
|
209
|
+
lines.push(` communication_style: '${escapeSingleQuotes(agent.persona.communication_style)}',`);
|
|
210
|
+
lines.push(` expertise: '${escapeSingleQuotes(agent.persona.expertise)}',`);
|
|
211
|
+
lines.push(' },');
|
|
212
|
+
lines.push(' },');
|
|
213
|
+
}
|
|
214
|
+
lines.push('];');
|
|
215
|
+
lines.push('');
|
|
216
|
+
|
|
217
|
+
// WORKFLOWS array
|
|
218
|
+
lines.push(`const ${prefix}_WORKFLOWS = [`);
|
|
219
|
+
for (const wf of workflows) {
|
|
220
|
+
lines.push(` { name: '${escapeSingleQuotes(wf.name)}', agent: '${escapeSingleQuotes(wf.agent)}' },`);
|
|
221
|
+
}
|
|
222
|
+
lines.push('];');
|
|
223
|
+
lines.push('');
|
|
224
|
+
|
|
225
|
+
// Derived lists
|
|
226
|
+
lines.push(`// Derived lists for ${teamName}`);
|
|
227
|
+
lines.push(`const ${prefix}_AGENT_FILES = ${prefix}_AGENTS.map(a => \`\${a.id}.md\`);`);
|
|
228
|
+
lines.push(`const ${prefix}_AGENT_IDS = ${prefix}_AGENTS.map(a => a.id);`);
|
|
229
|
+
lines.push(`const ${prefix}_WORKFLOW_NAMES = ${prefix}_WORKFLOWS.map(w => w.name);`);
|
|
230
|
+
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Build the list of export names to add to module.exports.
|
|
236
|
+
* @param {string} prefix
|
|
237
|
+
* @returns {string[]}
|
|
238
|
+
*/
|
|
239
|
+
function buildExportNames(prefix) {
|
|
240
|
+
return [
|
|
241
|
+
`${prefix}_AGENTS`,
|
|
242
|
+
`${prefix}_WORKFLOWS`,
|
|
243
|
+
`${prefix}_AGENT_FILES`,
|
|
244
|
+
`${prefix}_AGENT_IDS`,
|
|
245
|
+
`${prefix}_WORKFLOW_NAMES`,
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Validate staged content: prefix uniqueness, additive-only.
|
|
251
|
+
* @param {string} moduleBlock
|
|
252
|
+
* @param {string} prefix
|
|
253
|
+
* @param {string} currentContent
|
|
254
|
+
* @returns {string[]} errors
|
|
255
|
+
*/
|
|
256
|
+
function validateStaged(moduleBlock, prefix, currentContent) {
|
|
257
|
+
const errors = [];
|
|
258
|
+
|
|
259
|
+
// Check prefix collision
|
|
260
|
+
if (currentContent.includes(`const ${prefix}_AGENTS`)) {
|
|
261
|
+
errors.push(`Prefix collision: ${prefix}_AGENTS already exists in registry`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check additive-only — no reassignment to existing variables
|
|
265
|
+
const existingConsts = [...currentContent.matchAll(/const\s+(\w+)\s*=/g)].map(m => m[1]);
|
|
266
|
+
const newConsts = [...moduleBlock.matchAll(/const\s+(\w+)\s*=/g)].map(m => m[1]);
|
|
267
|
+
for (const nc of newConsts) {
|
|
268
|
+
if (existingConsts.includes(nc)) {
|
|
269
|
+
errors.push(`Additive-only violation: ${nc} already exists`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return errors;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Validate staged block syntax by writing to temp file and require()ing it.
|
|
278
|
+
* @param {string} moduleBlock
|
|
279
|
+
* @param {string} prefix
|
|
280
|
+
* @returns {Promise<string|null>} error message or null
|
|
281
|
+
*/
|
|
282
|
+
async function validateSyntax(moduleBlock, prefix) {
|
|
283
|
+
const tmpDir = await fs.mkdtemp(path.join(require('os').tmpdir(), 'bmad-tf-validate-'));
|
|
284
|
+
const tmpFile = path.join(tmpDir, 'validate-block.js');
|
|
285
|
+
try {
|
|
286
|
+
// Wrap the block in a module so require() can parse it
|
|
287
|
+
const wrapped = `'use strict';\n${moduleBlock}\nmodule.exports = { ${buildExportNames(prefix).join(', ')} };\n`;
|
|
288
|
+
await fs.writeFile(tmpFile, wrapped, 'utf8');
|
|
289
|
+
execSync(`node -e "require('${tmpFile.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')"`, { timeout: 5000, stdio: 'pipe' });
|
|
290
|
+
return null;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
return `Staged block syntax validation failed: ${err.stderr ? err.stderr.toString().trim() : err.message}`;
|
|
293
|
+
} finally {
|
|
294
|
+
await fs.remove(tmpDir);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check if the registry file has uncommitted changes.
|
|
300
|
+
* @param {string} registryPath
|
|
301
|
+
* @returns {{ dirty: boolean, diff: string }}
|
|
302
|
+
*/
|
|
303
|
+
function checkDirtyTree(registryPath) {
|
|
304
|
+
try {
|
|
305
|
+
const cwd = path.dirname(registryPath);
|
|
306
|
+
const unstaged = execSync(`git diff --name-only -- "${registryPath}"`, { cwd, timeout: 5000, stdio: 'pipe' }).toString().trim();
|
|
307
|
+
const staged = execSync(`git diff --cached --name-only -- "${registryPath}"`, { cwd, timeout: 5000, stdio: 'pipe' }).toString().trim();
|
|
308
|
+
const allDiffs = [unstaged, staged].filter(Boolean).join('\n');
|
|
309
|
+
return { dirty: allDiffs.length > 0, diff: allDiffs };
|
|
310
|
+
} catch {
|
|
311
|
+
// If git is not available or file is not in a repo, proceed
|
|
312
|
+
return { dirty: false, diff: '' };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Insert module block and export names into registry content.
|
|
318
|
+
* @param {string} content - Current file content
|
|
319
|
+
* @param {string} moduleBlock - New module block to insert
|
|
320
|
+
* @param {string[]} exportNames - Export names to add
|
|
321
|
+
* @returns {string} Modified content
|
|
322
|
+
*/
|
|
323
|
+
function applyInsertions(content, moduleBlock, exportNames) {
|
|
324
|
+
// Insert module block before `module.exports = {`
|
|
325
|
+
const exportsMarker = 'module.exports = {';
|
|
326
|
+
const markerIdx = content.indexOf(exportsMarker);
|
|
327
|
+
if (markerIdx === -1) {
|
|
328
|
+
throw new Error('Cannot find module.exports = { marker in registry file');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const before = content.slice(0, markerIdx);
|
|
332
|
+
const after = content.slice(markerIdx);
|
|
333
|
+
|
|
334
|
+
const withBlock = before + moduleBlock + '\n\n' + after;
|
|
335
|
+
|
|
336
|
+
// Insert export names before the closing `};` of module.exports
|
|
337
|
+
const closingIdx = withBlock.lastIndexOf('};');
|
|
338
|
+
if (closingIdx === -1) {
|
|
339
|
+
throw new Error('Cannot find closing }; of module.exports');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const beforeClosing = withBlock.slice(0, closingIdx);
|
|
343
|
+
const afterClosing = withBlock.slice(closingIdx);
|
|
344
|
+
|
|
345
|
+
const exportLines = exportNames.map(name => ` ${name},`).join('\n');
|
|
346
|
+
const withExports = beforeClosing + exportLines + '\n' + afterClosing;
|
|
347
|
+
|
|
348
|
+
return withExports;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Verify the written file can be require()d by Node.
|
|
353
|
+
* @param {string} registryPath
|
|
354
|
+
* @returns {string|null} error message or null
|
|
355
|
+
*/
|
|
356
|
+
function verifyRequire(registryPath) {
|
|
357
|
+
try {
|
|
358
|
+
const absPath = path.resolve(registryPath);
|
|
359
|
+
execSync(`node -e "require('${absPath.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')"`, { timeout: 5000, stdio: 'pipe' });
|
|
360
|
+
return null;
|
|
361
|
+
} catch (err) {
|
|
362
|
+
return `Post-write require() verification failed: ${err.stderr ? err.stderr.toString().trim() : err.message}`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// --- CLI entry point ---
|
|
367
|
+
if (require.main === module) {
|
|
368
|
+
const args = process.argv.slice(2);
|
|
369
|
+
const specFileIdx = args.indexOf('--spec-file');
|
|
370
|
+
const registryPathIdx = args.indexOf('--registry-path');
|
|
371
|
+
|
|
372
|
+
if (specFileIdx === -1 || !args[specFileIdx + 1]) {
|
|
373
|
+
console.error('Usage: node registry-writer.js --spec-file <path> [--registry-path <path>]');
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const specFilePath = args[specFileIdx + 1];
|
|
378
|
+
const registryPath = registryPathIdx !== -1 && args[registryPathIdx + 1]
|
|
379
|
+
? path.resolve(args[registryPathIdx + 1])
|
|
380
|
+
: path.resolve(__dirname, '../../../../../scripts/update/lib/agent-registry.js');
|
|
381
|
+
|
|
382
|
+
(async () => {
|
|
383
|
+
try {
|
|
384
|
+
const yaml = require('js-yaml');
|
|
385
|
+
const specContent = await fs.readFile(specFilePath, 'utf8');
|
|
386
|
+
const specData = yaml.load(specContent);
|
|
387
|
+
|
|
388
|
+
const result = await writeRegistryBlock(specData, registryPath);
|
|
389
|
+
console.log(JSON.stringify(result, null, 2));
|
|
390
|
+
process.exit(result.success ? 0 : 1);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.log(JSON.stringify({ success: false, errors: [err.message] }, null, 2));
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
})();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
module.exports = {
|
|
399
|
+
writeRegistryBlock,
|
|
400
|
+
derivePrefix,
|
|
401
|
+
buildAgentEntry,
|
|
402
|
+
buildModuleBlock,
|
|
403
|
+
buildExportNames,
|
|
404
|
+
buildWorkflowNames,
|
|
405
|
+
applyInsertions,
|
|
406
|
+
checkDirtyTree,
|
|
407
|
+
verifyRequire,
|
|
408
|
+
escapeSingleQuotes,
|
|
409
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs,
|
|
2
|
+
_team-factory,solutioning,Create Team,CT,1,_bmad/bme/_team-factory/workflows/step-00-route.md,,optional,team-factory,,Create a new BMAD-compliant team through guided architectural discovery,_bmad-output/planning-artifacts,team-spec-{name}.yaml,
|
|
3
|
+
_team-factory,solutioning,Validate Team,VT,2,_bmad/bme/_team-factory/workflows/add-team/step-05-validate.md,,optional,team-factory,,Run end-to-end validation on an existing team module,_bmad-output/planning-artifacts,validation-report.md,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Team Spec — Independent Pattern",
|
|
4
|
+
"description": "Schema for Independent composition pattern team specs. Agents operate standalone with no handoff contracts.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": [
|
|
7
|
+
"schema_version",
|
|
8
|
+
"team_name",
|
|
9
|
+
"team_name_kebab",
|
|
10
|
+
"composition_pattern",
|
|
11
|
+
"agents",
|
|
12
|
+
"integration"
|
|
13
|
+
],
|
|
14
|
+
"additionalProperties": true,
|
|
15
|
+
"properties": {
|
|
16
|
+
"schema_version": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"pattern": "^\\d+\\.\\d+$",
|
|
19
|
+
"description": "Schema version (e.g., '1.0')"
|
|
20
|
+
},
|
|
21
|
+
"team_name": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"minLength": 1,
|
|
24
|
+
"description": "Human-readable team name"
|
|
25
|
+
},
|
|
26
|
+
"team_name_kebab": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
|
|
29
|
+
"description": "Kebab-case team name for directory naming"
|
|
30
|
+
},
|
|
31
|
+
"description": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "What the team does"
|
|
34
|
+
},
|
|
35
|
+
"composition_pattern": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"enum": ["Independent"],
|
|
38
|
+
"description": "Must be 'Independent' for this schema"
|
|
39
|
+
},
|
|
40
|
+
"created": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "ISO date string"
|
|
43
|
+
},
|
|
44
|
+
"factory_version": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Factory version that created this spec"
|
|
47
|
+
},
|
|
48
|
+
"agents": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"minItems": 1,
|
|
51
|
+
"items": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"required": ["id", "role"],
|
|
54
|
+
"properties": {
|
|
55
|
+
"id": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"pattern": "^[a-z]+(-[a-z]+)*$",
|
|
58
|
+
"description": "Agent ID in kebab-case"
|
|
59
|
+
},
|
|
60
|
+
"name": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Display name (first name persona)"
|
|
63
|
+
},
|
|
64
|
+
"icon": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Single emoji"
|
|
67
|
+
},
|
|
68
|
+
"role": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"minLength": 1,
|
|
71
|
+
"description": "What this agent does"
|
|
72
|
+
},
|
|
73
|
+
"title": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"description": "Formal title"
|
|
76
|
+
},
|
|
77
|
+
"capabilities": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"items": { "type": "string" }
|
|
80
|
+
},
|
|
81
|
+
"overlap_acknowledgments": {
|
|
82
|
+
"type": "array",
|
|
83
|
+
"items": { "type": "string" }
|
|
84
|
+
},
|
|
85
|
+
"persona": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
"role": { "type": "string" },
|
|
89
|
+
"identity": { "type": "string" },
|
|
90
|
+
"communication_style": { "type": "string" },
|
|
91
|
+
"expertise": { "type": "string" }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"additionalProperties": false
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"integration": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"required": ["output_directory"],
|
|
101
|
+
"properties": {
|
|
102
|
+
"output_directory": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"minLength": 1,
|
|
105
|
+
"description": "Where output artifacts are saved"
|
|
106
|
+
},
|
|
107
|
+
"compass_routing": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"enum": ["optional", "per-agent"],
|
|
110
|
+
"description": "Compass routing mode (optional for Independent)"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"additionalProperties": false
|
|
114
|
+
},
|
|
115
|
+
"progress": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {
|
|
118
|
+
"orient": { "type": "string" },
|
|
119
|
+
"scope": { "type": "string" },
|
|
120
|
+
"connect": { "type": "string" },
|
|
121
|
+
"review": { "type": "string" },
|
|
122
|
+
"generate": {},
|
|
123
|
+
"validate": { "type": "string" }
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"decisions": {
|
|
127
|
+
"type": "array",
|
|
128
|
+
"items": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"step": { "type": "string" },
|
|
132
|
+
"decision": { "type": "string" },
|
|
133
|
+
"default_accepted": { "type": "boolean" },
|
|
134
|
+
"rationale": { "type": "string" }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"metrics": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {
|
|
141
|
+
"discovery_path": {},
|
|
142
|
+
"hardest_step": {},
|
|
143
|
+
"would_use_again": {}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|