speccrew 0.6.69 → 0.7.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/.speccrew/agents/speccrew-task-worker.md +1 -1
- package/.speccrew/agents/speccrew-team-leader.md +336 -189
- package/.speccrew/skills/speccrew-agentflow-manager/SKILL.md +161 -0
- package/.speccrew/skills/speccrew-agentflow-manager/workflow.agentflow.xml +347 -0
- package/.speccrew/skills/speccrew-deploy-build/SKILL.md +3 -56
- package/.speccrew/skills/speccrew-deploy-build/workflow.agentflow.xml +125 -0
- package/.speccrew/skills/speccrew-deploy-migrate/SKILL.md +3 -64
- package/.speccrew/skills/speccrew-deploy-migrate/workflow.agentflow.xml +135 -0
- package/.speccrew/skills/speccrew-deploy-smoke-test/SKILL.md +4 -156
- package/.speccrew/skills/speccrew-deploy-smoke-test/workflow.agentflow.xml +178 -0
- package/.speccrew/skills/speccrew-deploy-startup/SKILL.md +3 -135
- package/.speccrew/skills/speccrew-deploy-startup/workflow.agentflow.xml +223 -0
- package/.speccrew/skills/speccrew-dev-backend/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-backend/workflow.agentflow.xml +254 -0
- package/.speccrew/skills/speccrew-dev-desktop-electron/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-desktop-electron/workflow.agentflow.xml +259 -0
- package/.speccrew/skills/speccrew-dev-desktop-tauri/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-desktop-tauri/workflow.agentflow.xml +245 -0
- package/.speccrew/skills/speccrew-dev-frontend/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-frontend/workflow.agentflow.xml +262 -0
- package/.speccrew/skills/speccrew-dev-mobile/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-mobile/workflow.agentflow.xml +244 -0
- package/.speccrew/skills/speccrew-dev-review-backend/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-review-backend/workflow.agentflow.xml +251 -0
- package/.speccrew/skills/speccrew-dev-review-desktop/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-review-desktop/workflow.agentflow.xml +214 -0
- package/.speccrew/skills/speccrew-dev-review-frontend/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-review-frontend/workflow.agentflow.xml +213 -0
- package/.speccrew/skills/speccrew-dev-review-mobile/SKILL.md +10 -2
- package/.speccrew/skills/speccrew-dev-review-mobile/workflow.agentflow.xml +214 -0
- package/.speccrew/skills/speccrew-fd-api-contract/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-fd-api-contract/workflow.agentflow.xml +222 -0
- package/.speccrew/skills/speccrew-fd-feature-analyze/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-fd-feature-analyze/workflow.agentflow.xml +223 -0
- package/.speccrew/skills/speccrew-fd-feature-design/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-fd-feature-design/workflow.agentflow.xml +322 -0
- package/.speccrew/skills/speccrew-get-timestamp/SKILL.md +3 -39
- package/.speccrew/skills/speccrew-get-timestamp/workflow.agentflow.xml +43 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/SKILL.md +57 -508
- package/.speccrew/skills/{speccrew-knowledge-bizs-api-analyze-xml/SKILL.md → speccrew-knowledge-bizs-api-analyze/workflow.agentflow.xml} +1 -154
- package/.speccrew/skills/speccrew-knowledge-bizs-api-graph/SKILL.md +73 -283
- package/.speccrew/skills/{speccrew-knowledge-bizs-api-graph-xml/SKILL.md → speccrew-knowledge-bizs-api-graph/workflow.agentflow.xml} +0 -298
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/SKILL.md +931 -801
- package/.speccrew/skills/{speccrew-knowledge-bizs-dispatch-xml/SKILL.md → speccrew-knowledge-bizs-dispatch/workflow.agentflow.xml} +42 -272
- package/.speccrew/skills/speccrew-knowledge-bizs-identify-entries/SKILL.md +263 -71
- package/.speccrew/skills/{speccrew-knowledge-bizs-identify-entries-xml/SKILL.md → speccrew-knowledge-bizs-identify-entries/workflow.agentflow.xml} +8 -184
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/SKILL.md +200 -181
- package/.speccrew/skills/{speccrew-knowledge-bizs-init-features-xml/SKILL.md → speccrew-knowledge-bizs-init-features/workflow.agentflow.xml} +7 -134
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/SKILL.md +5 -89
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/workflow.agentflow.xml +129 -0
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/SKILL.md +454 -326
- package/.speccrew/skills/{speccrew-knowledge-bizs-ui-analyze-xml/SKILL.md → speccrew-knowledge-bizs-ui-analyze/workflow.agentflow.xml} +8 -128
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-graph/SKILL.md +302 -247
- package/.speccrew/skills/{speccrew-knowledge-bizs-ui-graph-xml/SKILL.md → speccrew-knowledge-bizs-ui-graph/workflow.agentflow.xml} +7 -199
- package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/SKILL.md +267 -156
- package/.speccrew/skills/{speccrew-knowledge-bizs-ui-style-extract-xml/SKILL.md → speccrew-knowledge-bizs-ui-style-extract/workflow.agentflow.xml} +7 -151
- package/.speccrew/skills/speccrew-knowledge-graph-query/SKILL.md +3 -122
- package/.speccrew/skills/speccrew-knowledge-graph-query/workflow.agentflow.xml +106 -0
- package/.speccrew/skills/speccrew-knowledge-graph-write/SKILL.md +3 -80
- package/.speccrew/skills/speccrew-knowledge-graph-write/workflow.agentflow.xml +152 -0
- package/.speccrew/skills/speccrew-knowledge-module-summarize/SKILL.md +371 -265
- package/.speccrew/skills/{speccrew-knowledge-module-summarize-xml/SKILL.md → speccrew-knowledge-module-summarize/workflow.agentflow.xml} +7 -197
- package/.speccrew/skills/speccrew-knowledge-system-summarize/SKILL.md +45 -333
- package/.speccrew/skills/{speccrew-knowledge-system-summarize-xml/SKILL.md → speccrew-knowledge-system-summarize/workflow.agentflow.xml} +0 -177
- package/.speccrew/skills/speccrew-knowledge-techs-dispatch/SKILL.md +174 -727
- package/.speccrew/skills/{speccrew-knowledge-techs-dispatch-xml/SKILL.md → speccrew-knowledge-techs-dispatch/workflow.agentflow.xml} +10 -351
- package/.speccrew/skills/speccrew-knowledge-techs-generate/SKILL.md +20 -150
- package/.speccrew/skills/{speccrew-knowledge-techs-generate-xml/SKILL.md → speccrew-knowledge-techs-generate/workflow.agentflow.xml} +0 -169
- package/.speccrew/skills/speccrew-knowledge-techs-generate-conventions/SKILL.md +75 -587
- package/.speccrew/skills/{speccrew-knowledge-techs-generate-conventions-xml/SKILL.md → speccrew-knowledge-techs-generate-conventions/workflow.agentflow.xml} +0 -153
- package/.speccrew/skills/speccrew-knowledge-techs-generate-quality/SKILL.md +463 -297
- package/.speccrew/skills/{speccrew-knowledge-techs-generate-quality-xml/SKILL.md → speccrew-knowledge-techs-generate-quality/workflow.agentflow.xml} +0 -164
- package/.speccrew/skills/speccrew-knowledge-techs-generate-ui-style/SKILL.md +57 -292
- package/.speccrew/skills/{speccrew-knowledge-techs-generate-ui-style-xml/SKILL.md → speccrew-knowledge-techs-generate-ui-style/workflow.agentflow.xml} +2 -193
- package/.speccrew/skills/speccrew-knowledge-techs-index/SKILL.md +49 -335
- package/.speccrew/skills/{speccrew-knowledge-techs-index-xml/SKILL.md → speccrew-knowledge-techs-index/workflow.agentflow.xml} +0 -167
- package/.speccrew/skills/speccrew-knowledge-techs-init/SKILL.md +28 -109
- package/.speccrew/skills/{speccrew-knowledge-techs-init-xml/SKILL.md → speccrew-knowledge-techs-init/workflow.agentflow.xml} +0 -189
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/SKILL.md +3 -487
- package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/workflow.agentflow.xml +278 -0
- package/.speccrew/skills/speccrew-pm-knowledge-detector/SKILL.md +3 -71
- package/.speccrew/skills/speccrew-pm-knowledge-detector/workflow.agentflow.xml +108 -0
- package/.speccrew/skills/speccrew-pm-module-initializer/SKILL.md +3 -107
- package/.speccrew/skills/speccrew-pm-module-initializer/workflow.agentflow.xml +139 -0
- package/.speccrew/skills/speccrew-pm-module-matcher/SKILL.md +3 -115
- package/.speccrew/skills/speccrew-pm-module-matcher/workflow.agentflow.xml +146 -0
- package/.speccrew/skills/speccrew-pm-requirement-analysis/SKILL.md +3 -343
- package/.speccrew/skills/speccrew-pm-requirement-analysis/workflow.agentflow.xml +174 -0
- package/.speccrew/skills/speccrew-pm-requirement-assess/SKILL.md +3 -91
- package/.speccrew/skills/speccrew-pm-requirement-assess/workflow.agentflow.xml +173 -0
- package/.speccrew/skills/speccrew-pm-requirement-clarify/SKILL.md +3 -224
- package/.speccrew/skills/speccrew-pm-requirement-clarify/workflow.agentflow.xml +159 -0
- package/.speccrew/skills/speccrew-pm-requirement-model/SKILL.md +3 -275
- package/.speccrew/skills/speccrew-pm-requirement-model/workflow.agentflow.xml +210 -0
- package/.speccrew/skills/speccrew-pm-requirement-simple/SKILL.md +3 -76
- package/.speccrew/skills/speccrew-pm-requirement-simple/workflow.agentflow.xml +120 -0
- package/.speccrew/skills/speccrew-pm-sub-prd-generate/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-pm-sub-prd-generate/workflow.agentflow.xml +218 -0
- package/.speccrew/skills/speccrew-sd-backend/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-sd-backend/workflow.agentflow.xml +264 -0
- package/.speccrew/skills/speccrew-sd-desktop/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-sd-desktop/workflow.agentflow.xml +288 -0
- package/.speccrew/skills/speccrew-sd-framework-evaluate/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-sd-framework-evaluate/workflow.agentflow.xml +235 -0
- package/.speccrew/skills/speccrew-sd-frontend/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-sd-frontend/workflow.agentflow.xml +299 -0
- package/.speccrew/skills/speccrew-sd-mobile/SKILL.md +7 -1
- package/.speccrew/skills/speccrew-sd-mobile/workflow.agentflow.xml +301 -0
- package/.speccrew/skills/speccrew-test-case-design/SKILL.md +165 -284
- package/.speccrew/skills/speccrew-test-case-design/workflow.agentflow.xml +210 -0
- package/.speccrew/skills/speccrew-test-code-gen/SKILL.md +204 -324
- package/.speccrew/skills/speccrew-test-code-gen/workflow.agentflow.xml +265 -0
- package/.speccrew/skills/speccrew-test-reporter/SKILL.md +205 -184
- package/.speccrew/skills/speccrew-test-reporter/workflow.agentflow.xml +284 -0
- package/.speccrew/skills/speccrew-test-runner/SKILL.md +242 -241
- package/.speccrew/skills/speccrew-test-runner/workflow.agentflow.xml +314 -0
- package/bin/cli.js +8 -1
- package/lib/commands/init.js +11 -3
- package/lib/commands/update.js +11 -3
- package/lib/commands/validate.js +565 -0
- package/lib/utils.js +43 -0
- package/package.json +1 -1
- package/workspace-template/docs/rules/{xml-workflow-spec.md → agentflow-spec.md} +5 -5
- package/workspace-template/scripts/validate-agentflow.js +637 -0
- package/.speccrew/agents/speccrew-team-leader-xml.md +0 -480
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/STATUS-FORMATS.md +0 -99
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/batch-orchestrator.js +0 -176
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-next-batch.js +0 -150
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-pending-features.js +0 -106
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/mark-stale.js +0 -249
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/merge-features.js +0 -300
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/process-batch-results.js +0 -915
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/update-feature-status.js +0 -226
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/examples/features.json +0 -34
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js +0 -1087
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/test-inventory.js +0 -26
- package/.speccrew/skills/speccrew-knowledge-techs-dispatch/STATUS-FORMATS.md +0 -550
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentFlow XML Validator
|
|
5
|
+
* Validates .agentflow.xml files for syntax and semantic correctness
|
|
6
|
+
* Zero dependencies - uses regex and string parsing only
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// Valid block types per specification
|
|
13
|
+
const VALID_BLOCK_TYPES = [
|
|
14
|
+
'input',
|
|
15
|
+
'output',
|
|
16
|
+
'task',
|
|
17
|
+
'gateway',
|
|
18
|
+
'loop',
|
|
19
|
+
'event',
|
|
20
|
+
'error-handler',
|
|
21
|
+
'checkpoint',
|
|
22
|
+
'rule'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Built-in variables that don't need prior definition
|
|
26
|
+
const BUILTIN_VARIABLES = [
|
|
27
|
+
'workspace',
|
|
28
|
+
'platform',
|
|
29
|
+
'timestamp',
|
|
30
|
+
'workflow.id',
|
|
31
|
+
'workflow.status'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse command line arguments
|
|
36
|
+
* @returns {Object} Parsed arguments
|
|
37
|
+
*/
|
|
38
|
+
function parseArgs() {
|
|
39
|
+
const args = process.argv.slice(2);
|
|
40
|
+
const result = {
|
|
41
|
+
target: null,
|
|
42
|
+
format: 'text',
|
|
43
|
+
strict: false
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < args.length; i++) {
|
|
47
|
+
const arg = args[i];
|
|
48
|
+
if (arg === '--format' && i + 1 < args.length) {
|
|
49
|
+
result.format = args[i + 1];
|
|
50
|
+
i++;
|
|
51
|
+
} else if (arg === '--strict') {
|
|
52
|
+
result.strict = true;
|
|
53
|
+
} else if (!arg.startsWith('--') && !result.target) {
|
|
54
|
+
result.target = arg;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get line number for a position in content
|
|
63
|
+
* @param {string} content - File content
|
|
64
|
+
* @param {number} position - Character position
|
|
65
|
+
* @returns {number} Line number (1-based)
|
|
66
|
+
*/
|
|
67
|
+
function getLineNumber(content, position) {
|
|
68
|
+
const lines = content.substring(0, position).split('\n');
|
|
69
|
+
return lines.length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract all block elements from XML content
|
|
74
|
+
* @param {string} content - XML content
|
|
75
|
+
* @returns {Array} Array of block objects with metadata
|
|
76
|
+
*/
|
|
77
|
+
function extractBlocks(content) {
|
|
78
|
+
const blocks = [];
|
|
79
|
+
// Match block elements with their attributes
|
|
80
|
+
const blockRegex = /<block\s+([^>]+)>/g;
|
|
81
|
+
let match;
|
|
82
|
+
|
|
83
|
+
while ((match = blockRegex.exec(content)) !== null) {
|
|
84
|
+
const attrsString = match[1];
|
|
85
|
+
const startPos = match.index;
|
|
86
|
+
const line = getLineNumber(content, startPos);
|
|
87
|
+
|
|
88
|
+
// Parse attributes
|
|
89
|
+
const attrs = {};
|
|
90
|
+
const attrRegex = /(\w+)=["']([^"']*)["']/g;
|
|
91
|
+
let attrMatch;
|
|
92
|
+
while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
|
|
93
|
+
attrs[attrMatch[1]] = attrMatch[2];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
blocks.push({
|
|
97
|
+
line,
|
|
98
|
+
position: startPos,
|
|
99
|
+
attributes: attrs,
|
|
100
|
+
raw: match[0]
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return blocks;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract all field elements from XML content
|
|
109
|
+
* @param {string} content - XML content
|
|
110
|
+
* @returns {Array} Array of field objects
|
|
111
|
+
*/
|
|
112
|
+
function extractFields(content) {
|
|
113
|
+
const fields = [];
|
|
114
|
+
const fieldRegex = /<field\s+([^>]+)\/?>/g;
|
|
115
|
+
let match;
|
|
116
|
+
|
|
117
|
+
while ((match = fieldRegex.exec(content)) !== null) {
|
|
118
|
+
const attrsString = match[1];
|
|
119
|
+
const line = getLineNumber(content, match.index);
|
|
120
|
+
|
|
121
|
+
const attrs = {};
|
|
122
|
+
const attrRegex = /(\w+)=["']([^"']*)["']/g;
|
|
123
|
+
let attrMatch;
|
|
124
|
+
while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
|
|
125
|
+
attrs[attrMatch[1]] = attrMatch[2];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fields.push({
|
|
129
|
+
line,
|
|
130
|
+
attributes: attrs,
|
|
131
|
+
raw: match[0]
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return fields;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extract branch elements from XML content
|
|
140
|
+
* @param {string} content - XML content
|
|
141
|
+
* @returns {Array} Array of branch objects
|
|
142
|
+
*/
|
|
143
|
+
function extractBranches(content) {
|
|
144
|
+
const branches = [];
|
|
145
|
+
const branchRegex = /<branch\s+([^>]*)>/g;
|
|
146
|
+
let match;
|
|
147
|
+
|
|
148
|
+
while ((match = branchRegex.exec(content)) !== null) {
|
|
149
|
+
const attrsString = match[1];
|
|
150
|
+
const line = getLineNumber(content, match.index);
|
|
151
|
+
|
|
152
|
+
const attrs = {};
|
|
153
|
+
const attrRegex = /(\w+)=["']([^"']*)["']/g;
|
|
154
|
+
let attrMatch;
|
|
155
|
+
while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
|
|
156
|
+
attrs[attrMatch[1]] = attrMatch[2];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
branches.push({
|
|
160
|
+
line,
|
|
161
|
+
attributes: attrs,
|
|
162
|
+
raw: match[0]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return branches;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract variable references from a string
|
|
171
|
+
* @param {string} str - String to search
|
|
172
|
+
* @returns {Array} Array of variable names
|
|
173
|
+
*/
|
|
174
|
+
function extractVariableRefs(str) {
|
|
175
|
+
const vars = [];
|
|
176
|
+
const varRegex = /\$\{([^}]+)\}/g;
|
|
177
|
+
let match;
|
|
178
|
+
|
|
179
|
+
while ((match = varRegex.exec(str)) !== null) {
|
|
180
|
+
// Extract base variable name (remove property access)
|
|
181
|
+
const varName = match[1].split('.')[0].split('[')[0];
|
|
182
|
+
if (!vars.includes(varName)) {
|
|
183
|
+
vars.push(varName);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return vars;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if XML has proper structure
|
|
192
|
+
* @param {string} content - XML content
|
|
193
|
+
* @param {string} filePath - File path for error reporting
|
|
194
|
+
* @returns {Array} Array of error objects
|
|
195
|
+
*/
|
|
196
|
+
function validateXmlStructure(content, filePath) {
|
|
197
|
+
const errors = [];
|
|
198
|
+
|
|
199
|
+
// Check for workflow root element
|
|
200
|
+
const workflowMatch = content.match(/<workflow\s+[^>]*>/);
|
|
201
|
+
if (!workflowMatch) {
|
|
202
|
+
errors.push({
|
|
203
|
+
line: 1,
|
|
204
|
+
rule: 'root-element',
|
|
205
|
+
message: 'Missing root element <workflow>'
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check for unclosed tags (basic check)
|
|
210
|
+
const openTags = content.match(/<block\s+[^>]*>/g) || [];
|
|
211
|
+
const closeTags = content.match(/<\/block>/g) || [];
|
|
212
|
+
|
|
213
|
+
// Check for self-closing blocks vs properly closed blocks
|
|
214
|
+
const selfClosingBlocks = content.match(/<block\s+[^>]*\/>/g) || [];
|
|
215
|
+
const nonSelfClosingBlocks = openTags.length - selfClosingBlocks.length;
|
|
216
|
+
|
|
217
|
+
if (nonSelfClosingBlocks !== closeTags.length) {
|
|
218
|
+
errors.push({
|
|
219
|
+
line: 1,
|
|
220
|
+
rule: 'unclosed-tags',
|
|
221
|
+
message: `Block tag mismatch: ${openTags.length} opening, ${closeTags.length} closing tags`
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check for unquoted attributes
|
|
226
|
+
const unquotedAttrRegex = /<block\s+[^>]*\w+=[^"'][^>]*>/g;
|
|
227
|
+
let match;
|
|
228
|
+
while ((match = unquotedAttrRegex.exec(content)) !== null) {
|
|
229
|
+
const line = getLineNumber(content, match.index);
|
|
230
|
+
errors.push({
|
|
231
|
+
line,
|
|
232
|
+
rule: 'unquoted-attribute',
|
|
233
|
+
message: 'Attribute values must be quoted'
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return errors;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Validate block types
|
|
242
|
+
* @param {Array} blocks - Extracted blocks
|
|
243
|
+
* @returns {Array} Array of error objects
|
|
244
|
+
*/
|
|
245
|
+
function validateBlockTypes(blocks) {
|
|
246
|
+
const errors = [];
|
|
247
|
+
|
|
248
|
+
for (const block of blocks) {
|
|
249
|
+
const type = block.attributes.type;
|
|
250
|
+
if (!type) {
|
|
251
|
+
errors.push({
|
|
252
|
+
line: block.line,
|
|
253
|
+
rule: 'missing-type',
|
|
254
|
+
message: 'Block is missing required "type" attribute'
|
|
255
|
+
});
|
|
256
|
+
} else if (!VALID_BLOCK_TYPES.includes(type)) {
|
|
257
|
+
errors.push({
|
|
258
|
+
line: block.line,
|
|
259
|
+
rule: 'invalid-type',
|
|
260
|
+
message: `Invalid block type "${type}". Valid types: ${VALID_BLOCK_TYPES.join(', ')}`
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return errors;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Validate block ID uniqueness
|
|
270
|
+
* @param {Array} blocks - Extracted blocks
|
|
271
|
+
* @returns {Array} Array of error objects
|
|
272
|
+
*/
|
|
273
|
+
function validateUniqueIds(blocks) {
|
|
274
|
+
const errors = [];
|
|
275
|
+
const idMap = new Map();
|
|
276
|
+
|
|
277
|
+
for (const block of blocks) {
|
|
278
|
+
const id = block.attributes.id;
|
|
279
|
+
if (id) {
|
|
280
|
+
if (idMap.has(id)) {
|
|
281
|
+
errors.push({
|
|
282
|
+
line: block.line,
|
|
283
|
+
rule: 'unique-id',
|
|
284
|
+
message: `Duplicate block id "${id}" (first defined at line ${idMap.get(id)})`
|
|
285
|
+
});
|
|
286
|
+
} else {
|
|
287
|
+
idMap.set(id, block.line);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return errors;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Validate required attributes for each block type
|
|
297
|
+
* @param {Array} blocks - Extracted blocks
|
|
298
|
+
* @returns {Array} Array of error objects
|
|
299
|
+
*/
|
|
300
|
+
function validateRequiredAttributes(blocks) {
|
|
301
|
+
const errors = [];
|
|
302
|
+
|
|
303
|
+
for (const block of blocks) {
|
|
304
|
+
const type = block.attributes.type;
|
|
305
|
+
const id = block.attributes.id;
|
|
306
|
+
|
|
307
|
+
// All blocks must have id
|
|
308
|
+
if (!id) {
|
|
309
|
+
errors.push({
|
|
310
|
+
line: block.line,
|
|
311
|
+
rule: 'missing-id',
|
|
312
|
+
message: 'Block is missing required "id" attribute'
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Type-specific required attributes
|
|
317
|
+
if (type === 'task') {
|
|
318
|
+
if (!block.attributes.action) {
|
|
319
|
+
errors.push({
|
|
320
|
+
line: block.line,
|
|
321
|
+
rule: 'missing-action',
|
|
322
|
+
message: 'Task block is missing required "action" attribute'
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (!block.attributes.desc) {
|
|
326
|
+
errors.push({
|
|
327
|
+
line: block.line,
|
|
328
|
+
rule: 'missing-desc',
|
|
329
|
+
message: 'Task block is missing required "desc" attribute'
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (type === 'input' || type === 'output') {
|
|
335
|
+
if (!block.attributes.desc) {
|
|
336
|
+
errors.push({
|
|
337
|
+
line: block.line,
|
|
338
|
+
rule: 'missing-desc',
|
|
339
|
+
message: `${type} block is missing required "desc" attribute`
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return errors;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Validate next references
|
|
350
|
+
* @param {string} content - XML content
|
|
351
|
+
* @param {Array} blocks - Extracted blocks
|
|
352
|
+
* @returns {Array} Array of error objects
|
|
353
|
+
*/
|
|
354
|
+
function validateNextReferences(content, blocks) {
|
|
355
|
+
const errors = [];
|
|
356
|
+
const validIds = new Set(blocks.map(b => b.attributes.id).filter(Boolean));
|
|
357
|
+
|
|
358
|
+
// Check field name="next" references
|
|
359
|
+
const fieldRegex = /<field\s+name=["']next["'][^>]*>([^<]*)<\/field>/g;
|
|
360
|
+
let match;
|
|
361
|
+
|
|
362
|
+
while ((match = fieldRegex.exec(content)) !== null) {
|
|
363
|
+
const nextId = match[1].trim();
|
|
364
|
+
const line = getLineNumber(content, match.index);
|
|
365
|
+
|
|
366
|
+
if (nextId && !validIds.has(nextId)) {
|
|
367
|
+
errors.push({
|
|
368
|
+
line,
|
|
369
|
+
rule: 'invalid-next-ref',
|
|
370
|
+
message: `Reference to undefined block id "${nextId}"`
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check branch default="true" and test attributes for next references
|
|
376
|
+
const branches = extractBranches(content);
|
|
377
|
+
for (const branch of branches) {
|
|
378
|
+
// Check test attribute for variable references (these are warnings, not errors)
|
|
379
|
+
// But we don't validate variable content here, just structural issues
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return errors;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Validate variable references
|
|
387
|
+
* @param {string} content - XML content
|
|
388
|
+
* @param {Array} blocks - Extracted blocks
|
|
389
|
+
* @returns {Array} Array of warning objects
|
|
390
|
+
*/
|
|
391
|
+
function validateVariableRefs(content, blocks) {
|
|
392
|
+
const warnings = [];
|
|
393
|
+
const definedVars = new Set(BUILTIN_VARIABLES);
|
|
394
|
+
|
|
395
|
+
// Collect output variables from blocks
|
|
396
|
+
for (const block of blocks) {
|
|
397
|
+
// Check for output var attribute
|
|
398
|
+
const blockContent = content.substring(block.position, content.indexOf('</block>', block.position) || block.position + 500);
|
|
399
|
+
const outputMatch = blockContent.match(/<field[^>]*\s+var=["']([^"']+)["']/);
|
|
400
|
+
if (outputMatch) {
|
|
401
|
+
definedVars.add(outputMatch[1]);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Check for output field in task blocks
|
|
405
|
+
const outputFieldMatch = blockContent.match(/<field\s+name=["']output["'][^>]*\s+var=["']([^"']+)["']/);
|
|
406
|
+
if (outputFieldMatch) {
|
|
407
|
+
definedVars.add(outputFieldMatch[1]);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Loop variables
|
|
411
|
+
if (block.attributes.type === 'loop') {
|
|
412
|
+
const asAttr = block.attributes.as;
|
|
413
|
+
if (asAttr) {
|
|
414
|
+
definedVars.add(asAttr);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check all variable references
|
|
420
|
+
const varRegex = /\$\{([^}]+)\}/g;
|
|
421
|
+
let match;
|
|
422
|
+
|
|
423
|
+
while ((match = varRegex.exec(content)) !== null) {
|
|
424
|
+
const fullVar = match[1];
|
|
425
|
+
const baseVar = fullVar.split('.')[0].split('[')[0];
|
|
426
|
+
const line = getLineNumber(content, match.index);
|
|
427
|
+
|
|
428
|
+
if (!definedVars.has(baseVar) && !BUILTIN_VARIABLES.includes(baseVar)) {
|
|
429
|
+
// Check if it's a property of a defined variable
|
|
430
|
+
const parentVar = fullVar.split('.')[0];
|
|
431
|
+
if (!definedVars.has(parentVar)) {
|
|
432
|
+
warnings.push({
|
|
433
|
+
line,
|
|
434
|
+
rule: 'var-ref',
|
|
435
|
+
message: `Variable "${baseVar}" may not be defined before use`
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return warnings;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Validate a single AgentFlow XML file
|
|
446
|
+
* @param {string} filePath - Path to the XML file
|
|
447
|
+
* @returns {Object} Validation result
|
|
448
|
+
*/
|
|
449
|
+
function validateFile(filePath) {
|
|
450
|
+
const result = {
|
|
451
|
+
file: filePath,
|
|
452
|
+
errors: [],
|
|
453
|
+
warnings: [],
|
|
454
|
+
summary: {
|
|
455
|
+
blocks: 0,
|
|
456
|
+
errors: 0,
|
|
457
|
+
warnings: 0
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
463
|
+
|
|
464
|
+
// Extract all blocks
|
|
465
|
+
const blocks = extractBlocks(content);
|
|
466
|
+
result.summary.blocks = blocks.length;
|
|
467
|
+
|
|
468
|
+
// Run all validations
|
|
469
|
+
result.errors.push(...validateXmlStructure(content, filePath));
|
|
470
|
+
result.errors.push(...validateBlockTypes(blocks));
|
|
471
|
+
result.errors.push(...validateUniqueIds(blocks));
|
|
472
|
+
result.errors.push(...validateRequiredAttributes(blocks));
|
|
473
|
+
result.errors.push(...validateNextReferences(content, blocks));
|
|
474
|
+
result.warnings.push(...validateVariableRefs(content, blocks));
|
|
475
|
+
|
|
476
|
+
// Update summary
|
|
477
|
+
result.summary.errors = result.errors.length;
|
|
478
|
+
result.summary.warnings = result.warnings.length;
|
|
479
|
+
|
|
480
|
+
} catch (error) {
|
|
481
|
+
result.errors.push({
|
|
482
|
+
line: 0,
|
|
483
|
+
rule: 'file-error',
|
|
484
|
+
message: `Failed to read file: ${error.message}`
|
|
485
|
+
});
|
|
486
|
+
result.summary.errors = 1;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Find all .agentflow.xml files in a directory recursively
|
|
494
|
+
* @param {string} dir - Directory to search
|
|
495
|
+
* @returns {Array} Array of file paths
|
|
496
|
+
*/
|
|
497
|
+
function findAgentFlowFiles(dir) {
|
|
498
|
+
const files = [];
|
|
499
|
+
|
|
500
|
+
function scan(directory) {
|
|
501
|
+
if (!fs.existsSync(directory)) return;
|
|
502
|
+
|
|
503
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
504
|
+
for (const entry of entries) {
|
|
505
|
+
const fullPath = path.join(directory, entry.name);
|
|
506
|
+
if (entry.isDirectory()) {
|
|
507
|
+
scan(fullPath);
|
|
508
|
+
} else if (entry.name.endsWith('.agentflow.xml')) {
|
|
509
|
+
files.push(fullPath);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
scan(dir);
|
|
515
|
+
return files;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Format output as text
|
|
520
|
+
* @param {Array} results - Validation results
|
|
521
|
+
* @param {boolean} strict - Treat warnings as errors
|
|
522
|
+
* @returns {string} Formatted text output
|
|
523
|
+
*/
|
|
524
|
+
function formatTextOutput(results, strict) {
|
|
525
|
+
const lines = [];
|
|
526
|
+
let totalErrors = 0;
|
|
527
|
+
let totalWarnings = 0;
|
|
528
|
+
let totalBlocks = 0;
|
|
529
|
+
|
|
530
|
+
for (const result of results) {
|
|
531
|
+
lines.push('');
|
|
532
|
+
lines.push(result.file);
|
|
533
|
+
|
|
534
|
+
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
535
|
+
lines.push(' PASS No issues found');
|
|
536
|
+
} else {
|
|
537
|
+
for (const error of result.errors) {
|
|
538
|
+
lines.push(` ERROR Line ${error.line}: [${error.rule}] ${error.message}`);
|
|
539
|
+
}
|
|
540
|
+
for (const warning of result.warnings) {
|
|
541
|
+
const label = strict ? 'ERROR' : 'WARN';
|
|
542
|
+
lines.push(` ${label} Line ${warning.line}: [${warning.rule}] ${warning.message}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
totalErrors += result.summary.errors;
|
|
547
|
+
totalWarnings += result.summary.warnings;
|
|
548
|
+
totalBlocks += result.summary.blocks;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
lines.push('');
|
|
552
|
+
lines.push('─'.repeat(50));
|
|
553
|
+
lines.push(`Summary: ${results.length} file(s), ${totalBlocks} block(s), ${totalErrors} error(s), ${totalWarnings} warning(s)`);
|
|
554
|
+
|
|
555
|
+
if (strict) {
|
|
556
|
+
const totalIssues = totalErrors + totalWarnings;
|
|
557
|
+
lines.push(`Strict mode: ${totalIssues} total issue(s)`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return lines.join('\n');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Format output as JSON
|
|
565
|
+
* @param {Array} results - Validation results
|
|
566
|
+
* @returns {string} JSON string
|
|
567
|
+
*/
|
|
568
|
+
function formatJsonOutput(results) {
|
|
569
|
+
return JSON.stringify(results, null, 2);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Main entry point
|
|
574
|
+
*/
|
|
575
|
+
function main() {
|
|
576
|
+
const args = parseArgs();
|
|
577
|
+
|
|
578
|
+
// Determine target
|
|
579
|
+
let target = args.target;
|
|
580
|
+
if (!target) {
|
|
581
|
+
// Default to speccrew-workspace directory
|
|
582
|
+
target = path.join(process.cwd(), 'speccrew-workspace');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Resolve to absolute path
|
|
586
|
+
target = path.resolve(target);
|
|
587
|
+
|
|
588
|
+
// Collect files to validate
|
|
589
|
+
let files = [];
|
|
590
|
+
if (fs.existsSync(target)) {
|
|
591
|
+
const stats = fs.statSync(target);
|
|
592
|
+
if (stats.isDirectory()) {
|
|
593
|
+
files = findAgentFlowFiles(target);
|
|
594
|
+
} else if (target.endsWith('.agentflow.xml')) {
|
|
595
|
+
files = [target];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (files.length === 0) {
|
|
600
|
+
console.error(`No .agentflow.xml files found in ${target}`);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Validate all files
|
|
605
|
+
const results = files.map(file => validateFile(file));
|
|
606
|
+
|
|
607
|
+
// Calculate exit code
|
|
608
|
+
let totalErrors = 0;
|
|
609
|
+
for (const result of results) {
|
|
610
|
+
totalErrors += result.summary.errors;
|
|
611
|
+
if (args.strict) {
|
|
612
|
+
totalErrors += result.summary.warnings;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Output results
|
|
617
|
+
if (args.format === 'json') {
|
|
618
|
+
console.log(formatJsonOutput(results));
|
|
619
|
+
} else {
|
|
620
|
+
console.log(formatTextOutput(results, args.strict));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
process.exit(totalErrors > 0 ? 1 : 0);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Run if called directly
|
|
627
|
+
if (require.main === module) {
|
|
628
|
+
main();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Export for use as module
|
|
632
|
+
module.exports = {
|
|
633
|
+
validateFile,
|
|
634
|
+
findAgentFlowFiles,
|
|
635
|
+
VALID_BLOCK_TYPES,
|
|
636
|
+
BUILTIN_VARIABLES
|
|
637
|
+
};
|