speccrew 0.6.68 → 0.7.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 (134) hide show
  1. package/.speccrew/agents/speccrew-task-worker.md +1 -1
  2. package/.speccrew/agents/speccrew-team-leader.md +336 -189
  3. package/.speccrew/skills/speccrew-agentflow-manager/SKILL.md +161 -0
  4. package/.speccrew/skills/speccrew-agentflow-manager/workflow.agentflow.xml +347 -0
  5. package/.speccrew/skills/speccrew-deploy-build/SKILL.md +3 -56
  6. package/.speccrew/skills/speccrew-deploy-build/workflow.agentflow.xml +125 -0
  7. package/.speccrew/skills/speccrew-deploy-migrate/SKILL.md +3 -64
  8. package/.speccrew/skills/speccrew-deploy-migrate/workflow.agentflow.xml +135 -0
  9. package/.speccrew/skills/speccrew-deploy-smoke-test/SKILL.md +4 -156
  10. package/.speccrew/skills/speccrew-deploy-smoke-test/workflow.agentflow.xml +178 -0
  11. package/.speccrew/skills/speccrew-deploy-startup/SKILL.md +3 -135
  12. package/.speccrew/skills/speccrew-deploy-startup/workflow.agentflow.xml +223 -0
  13. package/.speccrew/skills/speccrew-dev-backend/SKILL.md +10 -2
  14. package/.speccrew/skills/speccrew-dev-backend/workflow.agentflow.xml +254 -0
  15. package/.speccrew/skills/speccrew-dev-desktop-electron/SKILL.md +10 -2
  16. package/.speccrew/skills/speccrew-dev-desktop-electron/workflow.agentflow.xml +259 -0
  17. package/.speccrew/skills/speccrew-dev-desktop-tauri/SKILL.md +10 -2
  18. package/.speccrew/skills/speccrew-dev-desktop-tauri/workflow.agentflow.xml +245 -0
  19. package/.speccrew/skills/speccrew-dev-frontend/SKILL.md +10 -2
  20. package/.speccrew/skills/speccrew-dev-frontend/workflow.agentflow.xml +262 -0
  21. package/.speccrew/skills/speccrew-dev-mobile/SKILL.md +10 -2
  22. package/.speccrew/skills/speccrew-dev-mobile/workflow.agentflow.xml +244 -0
  23. package/.speccrew/skills/speccrew-dev-review-backend/SKILL.md +10 -2
  24. package/.speccrew/skills/speccrew-dev-review-backend/workflow.agentflow.xml +251 -0
  25. package/.speccrew/skills/speccrew-dev-review-desktop/SKILL.md +10 -2
  26. package/.speccrew/skills/speccrew-dev-review-desktop/workflow.agentflow.xml +214 -0
  27. package/.speccrew/skills/speccrew-dev-review-frontend/SKILL.md +10 -2
  28. package/.speccrew/skills/speccrew-dev-review-frontend/workflow.agentflow.xml +213 -0
  29. package/.speccrew/skills/speccrew-dev-review-mobile/SKILL.md +10 -2
  30. package/.speccrew/skills/speccrew-dev-review-mobile/workflow.agentflow.xml +214 -0
  31. package/.speccrew/skills/speccrew-fd-api-contract/SKILL.md +7 -1
  32. package/.speccrew/skills/speccrew-fd-api-contract/workflow.agentflow.xml +222 -0
  33. package/.speccrew/skills/speccrew-fd-feature-analyze/SKILL.md +7 -1
  34. package/.speccrew/skills/speccrew-fd-feature-analyze/workflow.agentflow.xml +223 -0
  35. package/.speccrew/skills/speccrew-fd-feature-design/SKILL.md +7 -1
  36. package/.speccrew/skills/speccrew-fd-feature-design/workflow.agentflow.xml +322 -0
  37. package/.speccrew/skills/speccrew-get-timestamp/SKILL.md +3 -39
  38. package/.speccrew/skills/speccrew-get-timestamp/workflow.agentflow.xml +43 -0
  39. package/.speccrew/skills/speccrew-knowledge-bizs-api-analyze/SKILL.md +57 -508
  40. package/.speccrew/skills/{speccrew-knowledge-bizs-api-analyze-xml/SKILL.md → speccrew-knowledge-bizs-api-analyze/workflow.agentflow.xml} +1 -154
  41. package/.speccrew/skills/speccrew-knowledge-bizs-api-graph/SKILL.md +73 -283
  42. package/.speccrew/skills/{speccrew-knowledge-bizs-api-graph-xml/SKILL.md → speccrew-knowledge-bizs-api-graph/workflow.agentflow.xml} +0 -298
  43. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/SKILL.md +931 -801
  44. package/.speccrew/skills/{speccrew-knowledge-bizs-dispatch-xml/SKILL.md → speccrew-knowledge-bizs-dispatch/workflow.agentflow.xml} +42 -272
  45. package/.speccrew/skills/speccrew-knowledge-bizs-identify-entries/SKILL.md +263 -71
  46. package/.speccrew/skills/{speccrew-knowledge-bizs-identify-entries-xml/SKILL.md → speccrew-knowledge-bizs-identify-entries/workflow.agentflow.xml} +8 -184
  47. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/SKILL.md +200 -181
  48. package/.speccrew/skills/{speccrew-knowledge-bizs-init-features-xml/SKILL.md → speccrew-knowledge-bizs-init-features/workflow.agentflow.xml} +7 -134
  49. package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/SKILL.md +5 -89
  50. package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/workflow.agentflow.xml +129 -0
  51. package/.speccrew/skills/speccrew-knowledge-bizs-ui-analyze/SKILL.md +454 -326
  52. package/.speccrew/skills/{speccrew-knowledge-bizs-ui-analyze-xml/SKILL.md → speccrew-knowledge-bizs-ui-analyze/workflow.agentflow.xml} +8 -128
  53. package/.speccrew/skills/speccrew-knowledge-bizs-ui-graph/SKILL.md +302 -247
  54. package/.speccrew/skills/{speccrew-knowledge-bizs-ui-graph-xml/SKILL.md → speccrew-knowledge-bizs-ui-graph/workflow.agentflow.xml} +7 -199
  55. package/.speccrew/skills/speccrew-knowledge-bizs-ui-style-extract/SKILL.md +267 -156
  56. package/.speccrew/skills/{speccrew-knowledge-bizs-ui-style-extract-xml/SKILL.md → speccrew-knowledge-bizs-ui-style-extract/workflow.agentflow.xml} +7 -151
  57. package/.speccrew/skills/speccrew-knowledge-graph-query/SKILL.md +3 -122
  58. package/.speccrew/skills/speccrew-knowledge-graph-query/workflow.agentflow.xml +106 -0
  59. package/.speccrew/skills/speccrew-knowledge-graph-write/SKILL.md +3 -80
  60. package/.speccrew/skills/speccrew-knowledge-graph-write/workflow.agentflow.xml +152 -0
  61. package/.speccrew/skills/speccrew-knowledge-module-summarize/SKILL.md +371 -265
  62. package/.speccrew/skills/{speccrew-knowledge-module-summarize-xml/SKILL.md → speccrew-knowledge-module-summarize/workflow.agentflow.xml} +7 -197
  63. package/.speccrew/skills/speccrew-knowledge-system-summarize/SKILL.md +45 -333
  64. package/.speccrew/skills/{speccrew-knowledge-system-summarize-xml/SKILL.md → speccrew-knowledge-system-summarize/workflow.agentflow.xml} +0 -177
  65. package/.speccrew/skills/speccrew-knowledge-techs-dispatch/SKILL.md +174 -727
  66. package/.speccrew/skills/{speccrew-knowledge-techs-dispatch-xml/SKILL.md → speccrew-knowledge-techs-dispatch/workflow.agentflow.xml} +10 -351
  67. package/.speccrew/skills/speccrew-knowledge-techs-generate/SKILL.md +20 -150
  68. package/.speccrew/skills/{speccrew-knowledge-techs-generate-xml/SKILL.md → speccrew-knowledge-techs-generate/workflow.agentflow.xml} +0 -169
  69. package/.speccrew/skills/speccrew-knowledge-techs-generate-conventions/SKILL.md +75 -587
  70. package/.speccrew/skills/{speccrew-knowledge-techs-generate-conventions-xml/SKILL.md → speccrew-knowledge-techs-generate-conventions/workflow.agentflow.xml} +0 -153
  71. package/.speccrew/skills/speccrew-knowledge-techs-generate-quality/SKILL.md +463 -297
  72. package/.speccrew/skills/{speccrew-knowledge-techs-generate-quality-xml/SKILL.md → speccrew-knowledge-techs-generate-quality/workflow.agentflow.xml} +0 -164
  73. package/.speccrew/skills/speccrew-knowledge-techs-generate-ui-style/SKILL.md +57 -292
  74. package/.speccrew/skills/{speccrew-knowledge-techs-generate-ui-style-xml/SKILL.md → speccrew-knowledge-techs-generate-ui-style/workflow.agentflow.xml} +2 -193
  75. package/.speccrew/skills/speccrew-knowledge-techs-index/SKILL.md +49 -335
  76. package/.speccrew/skills/{speccrew-knowledge-techs-index-xml/SKILL.md → speccrew-knowledge-techs-index/workflow.agentflow.xml} +0 -167
  77. package/.speccrew/skills/speccrew-knowledge-techs-init/SKILL.md +28 -109
  78. package/.speccrew/skills/{speccrew-knowledge-techs-init-xml/SKILL.md → speccrew-knowledge-techs-init/workflow.agentflow.xml} +0 -189
  79. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/SKILL.md +3 -487
  80. package/.speccrew/skills/speccrew-knowledge-techs-ui-analyze/workflow.agentflow.xml +278 -0
  81. package/.speccrew/skills/speccrew-pm-knowledge-detector/SKILL.md +3 -71
  82. package/.speccrew/skills/speccrew-pm-knowledge-detector/workflow.agentflow.xml +108 -0
  83. package/.speccrew/skills/speccrew-pm-module-initializer/SKILL.md +3 -107
  84. package/.speccrew/skills/speccrew-pm-module-initializer/workflow.agentflow.xml +139 -0
  85. package/.speccrew/skills/speccrew-pm-module-matcher/SKILL.md +3 -115
  86. package/.speccrew/skills/speccrew-pm-module-matcher/workflow.agentflow.xml +146 -0
  87. package/.speccrew/skills/speccrew-pm-requirement-analysis/SKILL.md +3 -343
  88. package/.speccrew/skills/speccrew-pm-requirement-analysis/workflow.agentflow.xml +174 -0
  89. package/.speccrew/skills/speccrew-pm-requirement-assess/SKILL.md +3 -91
  90. package/.speccrew/skills/speccrew-pm-requirement-assess/workflow.agentflow.xml +173 -0
  91. package/.speccrew/skills/speccrew-pm-requirement-clarify/SKILL.md +3 -224
  92. package/.speccrew/skills/speccrew-pm-requirement-clarify/workflow.agentflow.xml +159 -0
  93. package/.speccrew/skills/speccrew-pm-requirement-model/SKILL.md +3 -275
  94. package/.speccrew/skills/speccrew-pm-requirement-model/workflow.agentflow.xml +210 -0
  95. package/.speccrew/skills/speccrew-pm-requirement-simple/SKILL.md +3 -76
  96. package/.speccrew/skills/speccrew-pm-requirement-simple/workflow.agentflow.xml +120 -0
  97. package/.speccrew/skills/speccrew-pm-sub-prd-generate/SKILL.md +7 -1
  98. package/.speccrew/skills/speccrew-pm-sub-prd-generate/workflow.agentflow.xml +218 -0
  99. package/.speccrew/skills/speccrew-sd-backend/SKILL.md +7 -1
  100. package/.speccrew/skills/speccrew-sd-backend/workflow.agentflow.xml +264 -0
  101. package/.speccrew/skills/speccrew-sd-desktop/SKILL.md +7 -1
  102. package/.speccrew/skills/speccrew-sd-desktop/workflow.agentflow.xml +288 -0
  103. package/.speccrew/skills/speccrew-sd-framework-evaluate/SKILL.md +7 -1
  104. package/.speccrew/skills/speccrew-sd-framework-evaluate/workflow.agentflow.xml +235 -0
  105. package/.speccrew/skills/speccrew-sd-frontend/SKILL.md +7 -1
  106. package/.speccrew/skills/speccrew-sd-frontend/workflow.agentflow.xml +299 -0
  107. package/.speccrew/skills/speccrew-sd-mobile/SKILL.md +7 -1
  108. package/.speccrew/skills/speccrew-sd-mobile/workflow.agentflow.xml +301 -0
  109. package/.speccrew/skills/speccrew-test-case-design/SKILL.md +165 -284
  110. package/.speccrew/skills/speccrew-test-case-design/workflow.agentflow.xml +210 -0
  111. package/.speccrew/skills/speccrew-test-code-gen/SKILL.md +204 -324
  112. package/.speccrew/skills/speccrew-test-code-gen/workflow.agentflow.xml +265 -0
  113. package/.speccrew/skills/speccrew-test-reporter/SKILL.md +205 -184
  114. package/.speccrew/skills/speccrew-test-reporter/workflow.agentflow.xml +284 -0
  115. package/.speccrew/skills/speccrew-test-runner/SKILL.md +242 -241
  116. package/.speccrew/skills/speccrew-test-runner/workflow.agentflow.xml +314 -0
  117. package/bin/cli.js +8 -1
  118. package/lib/commands/validate.js +565 -0
  119. package/package.json +1 -1
  120. package/workspace-template/docs/rules/{xml-workflow-spec.md → agentflow-spec.md} +5 -5
  121. package/workspace-template/scripts/validate-agentflow.js +637 -0
  122. package/.speccrew/agents/speccrew-team-leader-xml.md +0 -480
  123. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/STATUS-FORMATS.md +0 -99
  124. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/batch-orchestrator.js +0 -176
  125. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-next-batch.js +0 -150
  126. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/get-pending-features.js +0 -106
  127. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/mark-stale.js +0 -249
  128. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/merge-features.js +0 -300
  129. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/process-batch-results.js +0 -915
  130. package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/scripts/update-feature-status.js +0 -226
  131. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/examples/features.json +0 -34
  132. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js +0 -1071
  133. package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/test-inventory.js +0 -26
  134. 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
+ };