convoke-agents 3.1.0 → 3.2.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 +31 -0
- package/README.md +1 -1
- 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 +12 -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 +1133 -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,176 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const yaml = require('js-yaml');
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('./types/factory-types').TeamSpec} TeamSpec */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Step completion states.
|
|
10
|
+
* @type {Object<string, string>}
|
|
11
|
+
*/
|
|
12
|
+
const STEP_ORDER = ['orient', 'scope', 'connect', 'review', 'generate', 'validate'];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Determine the resume point for a spec file by reading its progress section.
|
|
16
|
+
* Returns the first step that is not 'complete'.
|
|
17
|
+
*
|
|
18
|
+
* For the 'generate' step, also checks per-agent completion status
|
|
19
|
+
* and returns which agents still need generation.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} specPath - Absolute path to the spec file
|
|
22
|
+
* @returns {Promise<ResumeResult>}
|
|
23
|
+
*/
|
|
24
|
+
async function findResumePoint(specPath) {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = await fs.readFile(specPath, 'utf8');
|
|
28
|
+
} catch (err) {
|
|
29
|
+
return { resumable: false, resumeStep: null, pendingAgents: [], errors: [`Cannot read spec file: ${err.message}`] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let doc;
|
|
33
|
+
try {
|
|
34
|
+
doc = yaml.load(raw);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return { resumable: false, resumeStep: null, pendingAgents: [], errors: [`Invalid YAML: ${err.message}`] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!doc || !doc.progress) {
|
|
40
|
+
return { resumable: false, resumeStep: null, pendingAgents: [], errors: ['Spec file has no progress section'] };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const progress = doc.progress;
|
|
44
|
+
|
|
45
|
+
// Find first non-complete step
|
|
46
|
+
for (const step of STEP_ORDER) {
|
|
47
|
+
const status = typeof progress[step] === 'string' ? progress[step] : null;
|
|
48
|
+
|
|
49
|
+
if (step === 'generate' && typeof progress[step] === 'object') {
|
|
50
|
+
// Generate step has per-agent tracking
|
|
51
|
+
const pending = [];
|
|
52
|
+
for (const [agentId, agentStatus] of Object.entries(progress[step])) {
|
|
53
|
+
if (agentStatus !== 'complete') {
|
|
54
|
+
pending.push(agentId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (pending.length > 0) {
|
|
58
|
+
return { resumable: true, resumeStep: 'generate', pendingAgents: pending, errors: [] };
|
|
59
|
+
}
|
|
60
|
+
// All agents complete — continue to next step
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (status !== 'complete') {
|
|
65
|
+
return { resumable: true, resumeStep: step, pendingAgents: [], errors: [] };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// All steps complete
|
|
70
|
+
return { resumable: false, resumeStep: null, pendingAgents: [], errors: [], allComplete: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compare two spec objects and return which fields changed.
|
|
75
|
+
* Used for displaying diffs when resuming or in express mode.
|
|
76
|
+
*
|
|
77
|
+
* @param {TeamSpec} oldSpec - Previous spec state
|
|
78
|
+
* @param {TeamSpec} newSpec - Current spec state
|
|
79
|
+
* @returns {SpecDiff}
|
|
80
|
+
*/
|
|
81
|
+
function diffSpecs(oldSpec, newSpec) {
|
|
82
|
+
const changes = [];
|
|
83
|
+
|
|
84
|
+
// Compare top-level scalar fields
|
|
85
|
+
const scalarFields = ['team_name', 'team_name_kebab', 'description', 'composition_pattern', 'factory_version'];
|
|
86
|
+
for (const field of scalarFields) {
|
|
87
|
+
if (oldSpec[field] !== newSpec[field]) {
|
|
88
|
+
changes.push({ field, oldValue: oldSpec[field], newValue: newSpec[field] });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Compare agent count
|
|
93
|
+
const oldAgentCount = (oldSpec.agents || []).length;
|
|
94
|
+
const newAgentCount = (newSpec.agents || []).length;
|
|
95
|
+
if (oldAgentCount !== newAgentCount) {
|
|
96
|
+
changes.push({ field: 'agents.length', oldValue: String(oldAgentCount), newValue: String(newAgentCount) });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Compare agent IDs
|
|
100
|
+
const oldIds = (oldSpec.agents || []).map(a => a.id).sort();
|
|
101
|
+
const newIds = (newSpec.agents || []).map(a => a.id).sort();
|
|
102
|
+
const addedAgents = newIds.filter(id => !oldIds.includes(id));
|
|
103
|
+
const removedAgents = oldIds.filter(id => !newIds.includes(id));
|
|
104
|
+
if (addedAgents.length > 0) {
|
|
105
|
+
changes.push({ field: 'agents.added', oldValue: '', newValue: addedAgents.join(', ') });
|
|
106
|
+
}
|
|
107
|
+
if (removedAgents.length > 0) {
|
|
108
|
+
changes.push({ field: 'agents.removed', oldValue: removedAgents.join(', '), newValue: '' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Compare contract count (Sequential only)
|
|
112
|
+
const oldContracts = (oldSpec.contracts || []).length;
|
|
113
|
+
const newContracts = (newSpec.contracts || []).length;
|
|
114
|
+
if (oldContracts !== newContracts) {
|
|
115
|
+
changes.push({ field: 'contracts.length', oldValue: String(oldContracts), newValue: String(newContracts) });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Compare progress
|
|
119
|
+
for (const step of STEP_ORDER) {
|
|
120
|
+
const oldStatus = typeof oldSpec.progress?.[step] === 'string' ? oldSpec.progress[step] : stableStringify(oldSpec.progress?.[step]);
|
|
121
|
+
const newStatus = typeof newSpec.progress?.[step] === 'string' ? newSpec.progress[step] : stableStringify(newSpec.progress?.[step]);
|
|
122
|
+
if (oldStatus !== newStatus) {
|
|
123
|
+
changes.push({ field: `progress.${step}`, oldValue: oldStatus || 'undefined', newValue: newStatus || 'undefined' });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
hasChanges: changes.length > 0,
|
|
129
|
+
changeCount: changes.length,
|
|
130
|
+
changes,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* JSON.stringify with sorted keys for order-independent comparison.
|
|
136
|
+
* @param {*} obj
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
function stableStringify(obj) {
|
|
140
|
+
if (obj === null || obj === undefined) return String(obj);
|
|
141
|
+
if (typeof obj !== 'object') return JSON.stringify(obj);
|
|
142
|
+
const sorted = Object.keys(obj).sort().reduce((acc, key) => {
|
|
143
|
+
acc[key] = obj[key];
|
|
144
|
+
return acc;
|
|
145
|
+
}, {});
|
|
146
|
+
return JSON.stringify(sorted);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @typedef {Object} ResumeResult
|
|
151
|
+
* @property {boolean} resumable - True if there's a step to resume from
|
|
152
|
+
* @property {string|null} resumeStep - The step to resume from
|
|
153
|
+
* @property {string[]} pendingAgents - For generate step: agents still pending
|
|
154
|
+
* @property {string[]} errors
|
|
155
|
+
* @property {boolean} [allComplete] - True if all steps are complete
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @typedef {Object} SpecDiff
|
|
160
|
+
* @property {boolean} hasChanges
|
|
161
|
+
* @property {number} changeCount
|
|
162
|
+
* @property {SpecChange[]} changes
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @typedef {Object} SpecChange
|
|
167
|
+
* @property {string} field - Dot-path to the changed field
|
|
168
|
+
* @property {string} oldValue
|
|
169
|
+
* @property {string} newValue
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
findResumePoint,
|
|
174
|
+
diffSpecs,
|
|
175
|
+
STEP_ORDER,
|
|
176
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('./types/factory-types').TeamSpec} TeamSpec */
|
|
8
|
+
|
|
9
|
+
const SCHEMAS_DIR = path.join(__dirname, '..', 'schemas');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load and validate a team spec YAML file.
|
|
13
|
+
* Selects schema by composition_pattern, validates required fields and naming.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} specPath - Absolute path to team spec YAML file
|
|
16
|
+
* @returns {Promise<{ valid: boolean, spec: TeamSpec|null, errors: string[] }>}
|
|
17
|
+
*/
|
|
18
|
+
async function parseSpec(specPath) {
|
|
19
|
+
let raw;
|
|
20
|
+
try {
|
|
21
|
+
raw = await fs.readFile(specPath, 'utf8');
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return { valid: false, spec: null, errors: [`Cannot read spec file: ${err.message}`] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let doc;
|
|
27
|
+
try {
|
|
28
|
+
doc = yaml.load(raw);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return { valid: false, spec: null, errors: [`Invalid YAML: ${err.message}`] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!doc || typeof doc !== 'object') {
|
|
34
|
+
return { valid: false, spec: null, errors: ['Spec file is empty or not an object'] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Determine pattern and load schema
|
|
38
|
+
const pattern = doc.composition_pattern;
|
|
39
|
+
if (!pattern) {
|
|
40
|
+
return { valid: false, spec: null, errors: ['Missing required field: composition_pattern'] };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const schemaFile = pattern === 'Sequential'
|
|
44
|
+
? 'schema-sequential.json'
|
|
45
|
+
: pattern === 'Independent'
|
|
46
|
+
? 'schema-independent.json'
|
|
47
|
+
: null;
|
|
48
|
+
|
|
49
|
+
if (!schemaFile) {
|
|
50
|
+
return { valid: false, spec: null, errors: [`Unknown composition_pattern: "${pattern}". Expected "Independent" or "Sequential"`] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Load schema for reference (structural validation below)
|
|
54
|
+
const schemaPath = path.join(SCHEMAS_DIR, schemaFile);
|
|
55
|
+
let schema;
|
|
56
|
+
try {
|
|
57
|
+
schema = JSON.parse(await fs.readFile(schemaPath, 'utf8'));
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return { valid: false, spec: null, errors: [`Cannot load schema ${schemaFile}: ${err.message}`] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Validate against schema required fields and patterns
|
|
63
|
+
const errors = validateAgainstSchema(doc, schema, pattern);
|
|
64
|
+
|
|
65
|
+
if (errors.length > 0) {
|
|
66
|
+
return { valid: false, spec: null, errors };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { valid: true, spec: doc, errors: [] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate a parsed document against schema required fields, types, and patterns.
|
|
74
|
+
* Lightweight validation without ajv — checks required fields, naming patterns, and structural rules.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} doc - Parsed YAML document
|
|
77
|
+
* @param {Object} schema - JSON Schema object
|
|
78
|
+
* @param {string} pattern - "Independent" or "Sequential"
|
|
79
|
+
* @returns {string[]} errors
|
|
80
|
+
*/
|
|
81
|
+
function validateAgainstSchema(doc, schema, pattern) {
|
|
82
|
+
const errors = [];
|
|
83
|
+
|
|
84
|
+
// Check required root fields
|
|
85
|
+
const required = schema.required || [];
|
|
86
|
+
for (const field of required) {
|
|
87
|
+
if (doc[field] === undefined || doc[field] === null) {
|
|
88
|
+
errors.push(`Missing required field: ${field}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Type checks on key fields
|
|
93
|
+
if (doc.schema_version !== undefined && typeof doc.schema_version !== 'string') {
|
|
94
|
+
errors.push(`schema_version must be a string, got ${typeof doc.schema_version}`);
|
|
95
|
+
} else if (doc.schema_version && !/^\d+\.\d+$/.test(doc.schema_version)) {
|
|
96
|
+
errors.push(`schema_version "${doc.schema_version}" does not match pattern "N.N" (e.g., "1.0")`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (doc.composition_pattern !== undefined && doc.composition_pattern !== pattern) {
|
|
100
|
+
errors.push(`composition_pattern "${doc.composition_pattern}" does not match expected "${pattern}"`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (doc.team_name !== undefined && (typeof doc.team_name !== 'string' || doc.team_name.length === 0)) {
|
|
104
|
+
errors.push('team_name must be a non-empty string');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate naming patterns
|
|
108
|
+
const KEBAB_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
109
|
+
const AGENT_ID_RE = /^[a-z]+(-[a-z]+)*$/;
|
|
110
|
+
|
|
111
|
+
if (doc.team_name_kebab && !KEBAB_RE.test(doc.team_name_kebab)) {
|
|
112
|
+
errors.push(`team_name_kebab "${doc.team_name_kebab}" does not match kebab-case pattern: ${KEBAB_RE}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Validate agents array
|
|
116
|
+
if (Array.isArray(doc.agents)) {
|
|
117
|
+
if (doc.agents.length === 0) {
|
|
118
|
+
errors.push('agents array must contain at least one agent');
|
|
119
|
+
}
|
|
120
|
+
for (let i = 0; i < doc.agents.length; i++) {
|
|
121
|
+
const agent = doc.agents[i];
|
|
122
|
+
if (!agent.id) {
|
|
123
|
+
errors.push(`agents[${i}]: missing required field "id"`);
|
|
124
|
+
} else if (!AGENT_ID_RE.test(agent.id)) {
|
|
125
|
+
errors.push(`agents[${i}].id "${agent.id}" does not match agent ID pattern: ${AGENT_ID_RE}`);
|
|
126
|
+
}
|
|
127
|
+
if (!agent.role) {
|
|
128
|
+
errors.push(`agents[${i}]: missing required field "role"`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Sequential-specific: pipeline_position required
|
|
132
|
+
if (pattern === 'Sequential' && agent.pipeline_position === undefined) {
|
|
133
|
+
errors.push(`agents[${i}]: Sequential pattern requires pipeline_position`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check for duplicate agent IDs
|
|
138
|
+
const ids = doc.agents.map(a => a.id).filter(Boolean);
|
|
139
|
+
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
140
|
+
if (dupes.length > 0) {
|
|
141
|
+
errors.push(`Duplicate agent IDs: ${[...new Set(dupes)].join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
} else if (required.includes('agents')) {
|
|
144
|
+
errors.push('agents must be an array');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Sequential-specific validations
|
|
148
|
+
if (pattern === 'Sequential') {
|
|
149
|
+
if (!Array.isArray(doc.contracts) || doc.contracts.length === 0) {
|
|
150
|
+
errors.push('Sequential pattern requires at least one contract');
|
|
151
|
+
} else {
|
|
152
|
+
for (let i = 0; i < doc.contracts.length; i++) {
|
|
153
|
+
const c = doc.contracts[i];
|
|
154
|
+
if (!c.id) errors.push(`contracts[${i}]: missing required field "id"`);
|
|
155
|
+
if (!c.source_agent) errors.push(`contracts[${i}]: missing required field "source_agent"`);
|
|
156
|
+
if (!Array.isArray(c.target_agents) || c.target_agents.length === 0) {
|
|
157
|
+
errors.push(`contracts[${i}]: target_agents must be a non-empty array`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!doc.integration?.contract_prefix) {
|
|
163
|
+
errors.push('Sequential pattern requires integration.contract_prefix');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Validate integration block
|
|
168
|
+
if (doc.integration) {
|
|
169
|
+
if (typeof doc.integration !== 'object' || Array.isArray(doc.integration)) {
|
|
170
|
+
errors.push('integration must be an object');
|
|
171
|
+
} else if (!doc.integration.output_directory) {
|
|
172
|
+
errors.push('integration.output_directory is required');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return errors;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parse a spec from a YAML string (for testing or in-memory use).
|
|
181
|
+
* Pattern is determined from the document's composition_pattern field.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} yamlString - Raw YAML content
|
|
184
|
+
* @returns {Promise<{ valid: boolean, spec: TeamSpec|null, errors: string[] }>}
|
|
185
|
+
*/
|
|
186
|
+
async function parseSpecFromString(yamlString) {
|
|
187
|
+
const tmpDir = await fs.mkdtemp(path.join(require('os').tmpdir(), 'bmad-tf-parse-'));
|
|
188
|
+
const tmpFile = path.join(tmpDir, 'spec.yaml');
|
|
189
|
+
try {
|
|
190
|
+
await fs.writeFile(tmpFile, yamlString, 'utf8');
|
|
191
|
+
return await parseSpec(tmpFile);
|
|
192
|
+
} finally {
|
|
193
|
+
await fs.remove(tmpDir);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
parseSpec,
|
|
199
|
+
parseSpecFromString,
|
|
200
|
+
validateAgainstSchema,
|
|
201
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('./types/factory-types').TeamSpec} TeamSpec */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Write a team spec to a YAML file with atomic write safety.
|
|
11
|
+
* Protocol: write to .tmp → validate parse → rename to target.
|
|
12
|
+
*
|
|
13
|
+
* @param {TeamSpec} spec - The team spec object to write
|
|
14
|
+
* @param {string} targetPath - Absolute path to the output YAML file
|
|
15
|
+
* @returns {Promise<{ success: boolean, errors: string[] }>}
|
|
16
|
+
*/
|
|
17
|
+
async function writeSpec(spec, targetPath) {
|
|
18
|
+
if (!spec || typeof spec !== 'object') {
|
|
19
|
+
return { success: false, errors: ['spec must be a non-null object'] };
|
|
20
|
+
}
|
|
21
|
+
if (!targetPath || !path.isAbsolute(targetPath)) {
|
|
22
|
+
return { success: false, errors: ['targetPath must be an absolute path'] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Serialize to YAML
|
|
26
|
+
let yamlContent;
|
|
27
|
+
try {
|
|
28
|
+
yamlContent = yaml.dump(spec, {
|
|
29
|
+
indent: 2,
|
|
30
|
+
lineWidth: 120,
|
|
31
|
+
noRefs: true,
|
|
32
|
+
sortKeys: false,
|
|
33
|
+
quotingType: '"',
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return { success: false, errors: [`YAML serialization failed: ${err.message}`] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Atomic write: .tmp → validate → rename
|
|
40
|
+
const tmpPath = targetPath + '.tmp';
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
44
|
+
await fs.writeFile(tmpPath, yamlContent, 'utf8');
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { success: false, errors: [`Failed to write temp file: ${err.message}`] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate: re-read and parse to confirm round-trip integrity
|
|
50
|
+
try {
|
|
51
|
+
const reRead = await fs.readFile(tmpPath, 'utf8');
|
|
52
|
+
const parsed = yaml.load(reRead);
|
|
53
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
54
|
+
await fs.remove(tmpPath);
|
|
55
|
+
return { success: false, errors: ['Round-trip validation failed: parsed result is empty or not an object'] };
|
|
56
|
+
}
|
|
57
|
+
// Verify key fields survived serialization
|
|
58
|
+
const checks = [
|
|
59
|
+
['team_name_kebab', parsed.team_name_kebab, spec.team_name_kebab],
|
|
60
|
+
['composition_pattern', parsed.composition_pattern, spec.composition_pattern],
|
|
61
|
+
['agents.length', (parsed.agents || []).length, (spec.agents || []).length],
|
|
62
|
+
];
|
|
63
|
+
for (const [field, actual, expected] of checks) {
|
|
64
|
+
if (actual !== expected) {
|
|
65
|
+
await fs.remove(tmpPath);
|
|
66
|
+
return { success: false, errors: [`Round-trip validation failed: ${field} mismatch ("${actual}" vs "${expected}")`] };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
await fs.remove(tmpPath).catch(() => {});
|
|
71
|
+
return { success: false, errors: [`Round-trip validation failed: ${err.message}`] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Rename .tmp → target
|
|
75
|
+
try {
|
|
76
|
+
await fs.rename(tmpPath, targetPath);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
await fs.remove(tmpPath).catch(() => {});
|
|
79
|
+
return { success: false, errors: [`Failed to rename temp file to target: ${err.message}`] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { success: true, errors: [] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Update specific fields in an existing spec file.
|
|
87
|
+
* Loads current spec, merges updates, writes back atomically.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} specPath - Absolute path to existing spec file
|
|
90
|
+
* @param {Object} updates - Fields to update (shallow merge at top level, deep merge for nested objects)
|
|
91
|
+
* @returns {Promise<{ success: boolean, errors: string[] }>}
|
|
92
|
+
*/
|
|
93
|
+
async function updateSpec(specPath, updates) {
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = await fs.readFile(specPath, 'utf8');
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return { success: false, errors: [`Cannot read spec file: ${err.message}`] };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let current;
|
|
102
|
+
try {
|
|
103
|
+
current = yaml.load(raw);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return { success: false, errors: [`Invalid YAML in existing spec: ${err.message}`] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Shallow merge with deep merge for known nested objects
|
|
109
|
+
const merged = { ...current };
|
|
110
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
111
|
+
if (key === 'progress' && typeof value === 'object' && typeof merged.progress === 'object') {
|
|
112
|
+
merged.progress = { ...merged.progress, ...value };
|
|
113
|
+
} else if (key === 'integration' && typeof value === 'object' && typeof merged.integration === 'object') {
|
|
114
|
+
merged.integration = { ...merged.integration, ...value };
|
|
115
|
+
} else if (key === 'metrics' && typeof value === 'object' && typeof merged.metrics === 'object') {
|
|
116
|
+
merged.metrics = { ...merged.metrics, ...value };
|
|
117
|
+
} else {
|
|
118
|
+
merged[key] = value;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return writeSpec(merged, specPath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
writeSpec,
|
|
127
|
+
updateSpec,
|
|
128
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JSDoc type definitions for Team Factory integration wiring modules.
|
|
5
|
+
* Canonical source for all factory shapes — no runtime code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parsed team spec file shape.
|
|
10
|
+
* @typedef {Object} TeamSpec
|
|
11
|
+
* @property {string} schema_version
|
|
12
|
+
* @property {string} team_name
|
|
13
|
+
* @property {string} team_name_kebab
|
|
14
|
+
* @property {string} [description] - Optional team description for config.yaml
|
|
15
|
+
* @property {string} composition_pattern - "Sequential" or "Independent"
|
|
16
|
+
* @property {string} created - ISO date string
|
|
17
|
+
* @property {string} factory_version
|
|
18
|
+
* @property {AgentSpec[]} agents
|
|
19
|
+
* @property {ContractSpec[]} contracts
|
|
20
|
+
* @property {ContractSpec[]} feedback_contracts
|
|
21
|
+
* @property {IntegrationSpec} integration
|
|
22
|
+
* @property {Object} progress
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Agent specification from spec file.
|
|
27
|
+
* @typedef {Object} AgentSpec
|
|
28
|
+
* @property {string} id
|
|
29
|
+
* @property {string} role
|
|
30
|
+
* @property {string[]} capabilities
|
|
31
|
+
* @property {number} [pipeline_position] - Sequential only
|
|
32
|
+
* @property {string[]} overlap_acknowledgments
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Contract specification from spec file.
|
|
37
|
+
* @typedef {Object} ContractSpec
|
|
38
|
+
* @property {string} id
|
|
39
|
+
* @property {string} source_agent
|
|
40
|
+
* @property {string[]} target_agents
|
|
41
|
+
* @property {string} artifact_title
|
|
42
|
+
* @property {string} artifact_description
|
|
43
|
+
* @property {string[]} key_sections
|
|
44
|
+
* @property {string} file_name
|
|
45
|
+
* @property {string[]} input_artifacts
|
|
46
|
+
* @property {boolean} optional
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Integration decisions from spec file.
|
|
51
|
+
* @typedef {Object} IntegrationSpec
|
|
52
|
+
* @property {string} output_directory
|
|
53
|
+
* @property {string} compass_routing - "optional", "per-agent", "required", or "shared-reference"
|
|
54
|
+
* @property {string} [contract_prefix]
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Config.yaml data shape (matches Gyre/Vortex schema).
|
|
59
|
+
* @typedef {Object} ConfigData
|
|
60
|
+
* @property {string} submodule_name
|
|
61
|
+
* @property {string} description
|
|
62
|
+
* @property {string} module
|
|
63
|
+
* @property {string} output_folder
|
|
64
|
+
* @property {string[]} agents
|
|
65
|
+
* @property {string[]} workflows
|
|
66
|
+
* @property {string} version
|
|
67
|
+
* @property {string} user_name
|
|
68
|
+
* @property {string} communication_language
|
|
69
|
+
* @property {boolean} party_mode_enabled
|
|
70
|
+
* @property {string} core_module
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Module-help.csv row shape.
|
|
75
|
+
* @typedef {Object} CsvRow
|
|
76
|
+
* @property {string} module
|
|
77
|
+
* @property {string} phase
|
|
78
|
+
* @property {string} name
|
|
79
|
+
* @property {string} code
|
|
80
|
+
* @property {number} sequence
|
|
81
|
+
* @property {string} workflow_file
|
|
82
|
+
* @property {string} command
|
|
83
|
+
* @property {string} required
|
|
84
|
+
* @property {string} agent
|
|
85
|
+
* @property {string} options
|
|
86
|
+
* @property {string} description
|
|
87
|
+
* @property {string} output_location
|
|
88
|
+
* @property {string} outputs
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Activation validation result per agent.
|
|
93
|
+
* @typedef {Object} ActivationResult
|
|
94
|
+
* @property {string} agentFile
|
|
95
|
+
* @property {ActivationCheck[]} checks
|
|
96
|
+
* @property {string[]} errors
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Individual activation check.
|
|
101
|
+
* @typedef {Object} ActivationCheck
|
|
102
|
+
* @property {string} check - Description of what was checked
|
|
103
|
+
* @property {boolean} passed
|
|
104
|
+
* @property {string} [detail] - Additional info if failed
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Collision detection result.
|
|
109
|
+
* @typedef {Object} Collision
|
|
110
|
+
* @property {string} field - "submodule_name", "agent", or "workflow"
|
|
111
|
+
* @property {string} value - The colliding value
|
|
112
|
+
* @property {string} existingModule - Module that already has this value
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creator result shape (shared by config-creator and csv-creator).
|
|
117
|
+
* @typedef {Object} CreatorResult
|
|
118
|
+
* @property {boolean} success
|
|
119
|
+
* @property {string} [filePath]
|
|
120
|
+
* @property {string[]} errors
|
|
121
|
+
* @property {number} [rowCount] - csv-creator only
|
|
122
|
+
* @property {Collision[]} [collisions] - config-creator only
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Activation validator result shape.
|
|
127
|
+
* @typedef {Object} ValidationResult
|
|
128
|
+
* @property {boolean} valid
|
|
129
|
+
* @property {ActivationResult[]} results
|
|
130
|
+
*/
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Registry writer result shape (Full Write Safety Protocol).
|
|
134
|
+
* Differs from CreatorResult intentionally — writers return written[]/skipped[] per architecture rule 2.
|
|
135
|
+
* @typedef {Object} RegistryResult
|
|
136
|
+
* @property {boolean} success
|
|
137
|
+
* @property {string[]} written - Const names added to module.exports
|
|
138
|
+
* @property {string[]} skipped - Reasons for skipping (e.g., 'block already exists')
|
|
139
|
+
* @property {string[]} errors
|
|
140
|
+
* @property {boolean} rollbackApplied - True if .bak was restored after verify failure
|
|
141
|
+
* @property {boolean} [dirty] - True if dirty-tree detection found uncommitted changes
|
|
142
|
+
* @property {string} [diff] - Git diff output when dirty
|
|
143
|
+
*/
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Agent entry in agent-registry.js.
|
|
147
|
+
* @typedef {Object} RegistryAgentEntry
|
|
148
|
+
* @property {string} id
|
|
149
|
+
* @property {string} name - Display name (first name or derived)
|
|
150
|
+
* @property {string} icon - Unicode emoji character
|
|
151
|
+
* @property {string} title - Role-based title
|
|
152
|
+
* @property {string} stream - Team name kebab
|
|
153
|
+
* @property {RegistryPersona} persona
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Persona sub-object within a registry agent entry.
|
|
158
|
+
* @typedef {Object} RegistryPersona
|
|
159
|
+
* @property {string} role
|
|
160
|
+
* @property {string} identity
|
|
161
|
+
* @property {string} communication_style
|
|
162
|
+
* @property {string} expertise
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* File manifest entry tracking a created or modified file.
|
|
167
|
+
* @typedef {Object} ManifestEntry
|
|
168
|
+
* @property {string} path - File path (relative to project root)
|
|
169
|
+
* @property {'created' | 'modified'} operation - Whether the file was created or modified
|
|
170
|
+
* @property {string} module - Team name kebab identifying the owning module
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* End-to-end validation result.
|
|
175
|
+
* @typedef {Object} E2EValidationResult
|
|
176
|
+
* @property {boolean} valid - True if all checks passed
|
|
177
|
+
* @property {E2ECheck[]} checks - Individual check results
|
|
178
|
+
* @property {string[]} errors - Human-readable error messages for failed checks
|
|
179
|
+
*/
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Individual end-to-end validation check.
|
|
183
|
+
* Name uses {PROP}-{SEMANTIC-NAME} format per architecture (line 590).
|
|
184
|
+
* @typedef {Object} E2ECheck
|
|
185
|
+
* @property {string} name - Check ID in {PROP}-{SEMANTIC-NAME} format (e.g., CONFIG-EXISTS)
|
|
186
|
+
* @property {string} stepName - Step that produced the check (e.g., 'structural', 'regression', 'wiring')
|
|
187
|
+
* @property {boolean} passed - Whether the check passed
|
|
188
|
+
* @property {string} [expected] - Expected value (included on failure per TF-NFR11)
|
|
189
|
+
* @property {string} [actual] - Actual value found (included on failure per TF-NFR11)
|
|
190
|
+
* @property {string} [detail] - Additional context (e.g., file path)
|
|
191
|
+
*/
|
|
192
|
+
|
|
193
|
+
module.exports = {};
|