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.
Files changed (78) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -1
  3. package/_bmad/bme/_artifacts/config.yaml +15 -0
  4. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/SKILL.md +6 -0
  5. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-01-scope.md +138 -0
  6. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-02-dryrun.md +199 -0
  7. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-03-resolve.md +174 -0
  8. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-04-execute.md +213 -0
  9. package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/workflow.md +85 -0
  10. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/SKILL.md +6 -0
  11. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-01-scan.md +131 -0
  12. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-02-explore.md +131 -0
  13. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-03-recommend.md +149 -0
  14. package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/workflow.md +78 -0
  15. package/_bmad/bme/_portability/skills/bmad-export-skill/SKILL.md +6 -0
  16. package/_bmad/bme/_portability/skills/bmad-export-skill/workflow.md +74 -0
  17. package/_bmad/bme/_portability/skills/bmad-generate-catalog/SKILL.md +6 -0
  18. package/_bmad/bme/_portability/skills/bmad-generate-catalog/workflow.md +42 -0
  19. package/_bmad/bme/_portability/skills/bmad-seed-catalog/SKILL.md +6 -0
  20. package/_bmad/bme/_portability/skills/bmad-seed-catalog/workflow.md +61 -0
  21. package/_bmad/bme/_portability/skills/bmad-validate-exports/SKILL.md +6 -0
  22. package/_bmad/bme/_portability/skills/bmad-validate-exports/workflow.md +43 -0
  23. package/_bmad/bme/_team-factory/agents/team-factory.md +128 -0
  24. package/_bmad/bme/_team-factory/config.yaml +13 -0
  25. package/_bmad/bme/_team-factory/lib/cascade-logic.js +184 -0
  26. package/_bmad/bme/_team-factory/lib/collision-detector.js +228 -0
  27. package/_bmad/bme/_team-factory/lib/manifest-tracker.js +214 -0
  28. package/_bmad/bme/_team-factory/lib/spec-differ.js +176 -0
  29. package/_bmad/bme/_team-factory/lib/spec-parser.js +201 -0
  30. package/_bmad/bme/_team-factory/lib/spec-writer.js +128 -0
  31. package/_bmad/bme/_team-factory/lib/types/factory-types.js +193 -0
  32. package/_bmad/bme/_team-factory/lib/utils/csv-utils.js +62 -0
  33. package/_bmad/bme/_team-factory/lib/utils/naming-utils.js +45 -0
  34. package/_bmad/bme/_team-factory/lib/validators/end-to-end-validator.js +898 -0
  35. package/_bmad/bme/_team-factory/lib/writers/activation-validator.js +175 -0
  36. package/_bmad/bme/_team-factory/lib/writers/config-appender.js +192 -0
  37. package/_bmad/bme/_team-factory/lib/writers/config-creator.js +215 -0
  38. package/_bmad/bme/_team-factory/lib/writers/csv-appender.js +118 -0
  39. package/_bmad/bme/_team-factory/lib/writers/csv-creator.js +190 -0
  40. package/_bmad/bme/_team-factory/lib/writers/registry-appender.js +372 -0
  41. package/_bmad/bme/_team-factory/lib/writers/registry-writer.js +409 -0
  42. package/_bmad/bme/_team-factory/module-help.csv +3 -0
  43. package/_bmad/bme/_team-factory/schemas/schema-independent.json +147 -0
  44. package/_bmad/bme/_team-factory/schemas/schema-sequential.json +242 -0
  45. package/_bmad/bme/_team-factory/templates/team-spec-template.yaml +86 -0
  46. package/_bmad/bme/_team-factory/workflows/add-team/step-01-scope.md +105 -0
  47. package/_bmad/bme/_team-factory/workflows/add-team/step-02-connect.md +110 -0
  48. package/_bmad/bme/_team-factory/workflows/add-team/step-03-review.md +116 -0
  49. package/_bmad/bme/_team-factory/workflows/add-team/step-04-generate.md +160 -0
  50. package/_bmad/bme/_team-factory/workflows/add-team/step-05-validate.md +146 -0
  51. package/_bmad/bme/_team-factory/workflows/step-00-route.md +76 -0
  52. package/_bmad/bme/_vortex/config.yaml +4 -4
  53. package/package.json +12 -7
  54. package/scripts/convoke-doctor.js +172 -1
  55. package/scripts/install-gyre-agents.js +0 -0
  56. package/scripts/lib/artifact-utils.js +521 -13
  57. package/scripts/lib/portfolio/portfolio-engine.js +301 -34
  58. package/scripts/lib/portfolio/rules/artifact-chain-rule.js +33 -3
  59. package/scripts/lib/portfolio/rules/conflict-resolver.js +22 -0
  60. package/scripts/migrate-artifacts.js +69 -10
  61. package/scripts/portability/catalog-generator.js +353 -0
  62. package/scripts/portability/classify-skills.js +646 -0
  63. package/scripts/portability/convoke-export.js +522 -0
  64. package/scripts/portability/export-engine.js +1133 -0
  65. package/scripts/portability/generate-adapters.js +79 -0
  66. package/scripts/portability/manifest-csv.js +147 -0
  67. package/scripts/portability/seed-catalog-repo.js +427 -0
  68. package/scripts/portability/templates/canonical-example.md +102 -0
  69. package/scripts/portability/templates/canonical-format.md +218 -0
  70. package/scripts/portability/templates/readme-template.md +72 -0
  71. package/scripts/portability/test-constants.js +42 -0
  72. package/scripts/portability/validate-classification.js +529 -0
  73. package/scripts/portability/validate-exports.js +348 -0
  74. package/scripts/update/lib/agent-registry.js +35 -0
  75. package/scripts/update/lib/config-merger.js +140 -10
  76. package/scripts/update/lib/refresh-installation.js +293 -8
  77. package/scripts/update/lib/utils.js +27 -1
  78. 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 = {};