wogiflow 1.0.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 (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Component Check (Core Module)
5
+ *
6
+ * CLI-agnostic component reuse detection.
7
+ * Checks if a similar component exists before creating a new one.
8
+ *
9
+ * Returns a standardized result that adapters transform for specific CLIs.
10
+ */
11
+
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+
15
+ // Import from parent scripts directory
16
+ const { getConfig, PATHS } = require('../../flow-utils');
17
+
18
+ /**
19
+ * Check if component reuse checking is enabled
20
+ * @returns {boolean}
21
+ */
22
+ function isComponentCheckEnabled() {
23
+ const config = getConfig();
24
+ return config.hooks?.rules?.componentReuse?.enabled !== false;
25
+ }
26
+
27
+ /**
28
+ * Get component patterns to check
29
+ * @returns {string[]} Glob patterns for component directories
30
+ */
31
+ function getComponentPatterns() {
32
+ const config = getConfig();
33
+ return config.hooks?.rules?.componentReuse?.patterns ||
34
+ config.componentRules?.directories ||
35
+ ['**/components/**', '**/ui/**', '**/src/components/**'];
36
+ }
37
+
38
+ /**
39
+ * Get similarity threshold
40
+ * @returns {number} Threshold (0-100)
41
+ */
42
+ function getSimilarityThreshold() {
43
+ const config = getConfig();
44
+ return config.hooks?.rules?.componentReuse?.threshold || 80;
45
+ }
46
+
47
+ /**
48
+ * Check if a file path matches component patterns
49
+ * @param {string} filePath - Path to check
50
+ * @returns {boolean}
51
+ */
52
+ function isComponentPath(filePath) {
53
+ const patterns = getComponentPatterns();
54
+ const normalizedPath = filePath.replace(/\\/g, '/');
55
+
56
+ for (const pattern of patterns) {
57
+ // Simple pattern matching (supports ** and *)
58
+ const regexPattern = pattern
59
+ .replace(/\*\*/g, '.*')
60
+ .replace(/\*/g, '[^/]*');
61
+
62
+ if (new RegExp(regexPattern).test(normalizedPath)) {
63
+ return true;
64
+ }
65
+ }
66
+
67
+ return false;
68
+ }
69
+
70
+ /**
71
+ * Load the component index
72
+ * @returns {Object|null} Component index or null
73
+ */
74
+ function loadComponentIndex() {
75
+ try {
76
+ const indexPath = path.join(PATHS.state, 'component-index.json');
77
+ if (!fs.existsSync(indexPath)) {
78
+ return null;
79
+ }
80
+ return JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
81
+ } catch (err) {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Parse app-map.md for component entries
88
+ * @returns {Array} Component entries from app-map
89
+ */
90
+ function parseAppMap() {
91
+ try {
92
+ const appMapPath = PATHS.appMap;
93
+ if (!fs.existsSync(appMapPath)) {
94
+ return [];
95
+ }
96
+
97
+ const content = fs.readFileSync(appMapPath, 'utf-8');
98
+ const components = [];
99
+
100
+ // Parse markdown table or list entries
101
+ const lines = content.split('\n');
102
+ for (const line of lines) {
103
+ // Match table rows: | ComponentName | description | path |
104
+ const tableMatch = line.match(/^\|\s*([^|]+)\s*\|/);
105
+ if (tableMatch && !tableMatch[1].includes('---')) {
106
+ const name = tableMatch[1].trim();
107
+ if (name && name !== 'Component' && name !== 'Name') {
108
+ components.push({ name, source: 'app-map' });
109
+ }
110
+ }
111
+
112
+ // Match list items: - ComponentName: description
113
+ const listMatch = line.match(/^[-*]\s+\*?\*?([A-Z][a-zA-Z0-9]+)\*?\*?/);
114
+ if (listMatch) {
115
+ components.push({ name: listMatch[1], source: 'app-map' });
116
+ }
117
+ }
118
+
119
+ return components;
120
+ } catch (err) {
121
+ return [];
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Calculate similarity between two strings (Levenshtein-based)
127
+ * @param {string} a - First string
128
+ * @param {string} b - Second string
129
+ * @returns {number} Similarity score (0-100)
130
+ */
131
+ function calculateSimilarity(a, b) {
132
+ if (!a || !b) return 0;
133
+
134
+ const aLower = a.toLowerCase();
135
+ const bLower = b.toLowerCase();
136
+
137
+ if (aLower === bLower) return 100;
138
+
139
+ // Check if one contains the other
140
+ if (aLower.includes(bLower) || bLower.includes(aLower)) {
141
+ const longer = Math.max(a.length, b.length);
142
+ const shorter = Math.min(a.length, b.length);
143
+ return Math.round((shorter / longer) * 100);
144
+ }
145
+
146
+ // Levenshtein distance
147
+ const matrix = [];
148
+ for (let i = 0; i <= b.length; i++) {
149
+ matrix[i] = [i];
150
+ }
151
+ for (let j = 0; j <= a.length; j++) {
152
+ matrix[0][j] = j;
153
+ }
154
+ for (let i = 1; i <= b.length; i++) {
155
+ for (let j = 1; j <= a.length; j++) {
156
+ if (bLower[i - 1] === aLower[j - 1]) {
157
+ matrix[i][j] = matrix[i - 1][j - 1];
158
+ } else {
159
+ matrix[i][j] = Math.min(
160
+ matrix[i - 1][j - 1] + 1,
161
+ matrix[i][j - 1] + 1,
162
+ matrix[i - 1][j] + 1
163
+ );
164
+ }
165
+ }
166
+ }
167
+
168
+ const distance = matrix[b.length][a.length];
169
+ const maxLen = Math.max(a.length, b.length);
170
+ return Math.round(((maxLen - distance) / maxLen) * 100);
171
+ }
172
+
173
+ /**
174
+ * Extract component name from file path
175
+ * @param {string} filePath - File path
176
+ * @returns {string} Extracted component name
177
+ */
178
+ function extractComponentName(filePath) {
179
+ const fileName = path.basename(filePath, path.extname(filePath));
180
+ // Remove common suffixes
181
+ return fileName
182
+ .replace(/\.(component|view|container|page|screen)$/i, '')
183
+ .replace(/[-_]/g, '');
184
+ }
185
+
186
+ /**
187
+ * Find similar components to a given name
188
+ * @param {string} componentName - Name to search for
189
+ * @returns {Array} Similar components sorted by similarity
190
+ */
191
+ function findSimilarComponents(componentName) {
192
+ const threshold = getSimilarityThreshold();
193
+ const similar = [];
194
+
195
+ // Check component index
196
+ const index = loadComponentIndex();
197
+ if (index && index.components) {
198
+ for (const comp of index.components) {
199
+ const name = comp.name || extractComponentName(comp.path || '');
200
+ const similarity = calculateSimilarity(componentName, name);
201
+ if (similarity >= threshold) {
202
+ similar.push({
203
+ name,
204
+ path: comp.path,
205
+ similarity,
206
+ source: 'component-index'
207
+ });
208
+ }
209
+ }
210
+ }
211
+
212
+ // Check app-map
213
+ const appMapComponents = parseAppMap();
214
+ for (const comp of appMapComponents) {
215
+ const similarity = calculateSimilarity(componentName, comp.name);
216
+ if (similarity >= threshold) {
217
+ // Avoid duplicates
218
+ if (!similar.some(s => s.name === comp.name)) {
219
+ similar.push({
220
+ name: comp.name,
221
+ similarity,
222
+ source: 'app-map'
223
+ });
224
+ }
225
+ }
226
+ }
227
+
228
+ // Sort by similarity descending
229
+ return similar.sort((a, b) => b.similarity - a.similarity);
230
+ }
231
+
232
+ /**
233
+ * Check component reuse for a new file
234
+ * @param {Object} options
235
+ * @param {string} options.filePath - Path of new file
236
+ * @param {string} options.content - Content of new file (optional)
237
+ * @returns {Object} Result: { allowed, warning, message, similar }
238
+ */
239
+ function checkComponentReuse(options = {}) {
240
+ const { filePath, content } = options;
241
+
242
+ if (!isComponentCheckEnabled()) {
243
+ return {
244
+ allowed: true,
245
+ warning: false,
246
+ message: null,
247
+ reason: 'component_check_disabled'
248
+ };
249
+ }
250
+
251
+ // Only check component paths
252
+ if (!isComponentPath(filePath)) {
253
+ return {
254
+ allowed: true,
255
+ warning: false,
256
+ message: null,
257
+ reason: 'not_component_path'
258
+ };
259
+ }
260
+
261
+ const componentName = extractComponentName(filePath);
262
+ const similar = findSimilarComponents(componentName);
263
+
264
+ if (similar.length === 0) {
265
+ return {
266
+ allowed: true,
267
+ warning: false,
268
+ message: null,
269
+ reason: 'no_similar_found'
270
+ };
271
+ }
272
+
273
+ // Found similar components
274
+ const config = getConfig();
275
+ const shouldBlock = config.hooks?.rules?.componentReuse?.blockOnSimilar === true;
276
+ const bestMatch = similar[0];
277
+
278
+ const message = generateSimilarMessage(componentName, similar);
279
+
280
+ if (shouldBlock) {
281
+ return {
282
+ allowed: false,
283
+ warning: false,
284
+ blocked: true,
285
+ message,
286
+ similar,
287
+ bestMatch,
288
+ reason: 'similar_component_exists'
289
+ };
290
+ }
291
+
292
+ return {
293
+ allowed: true,
294
+ warning: true,
295
+ message,
296
+ similar,
297
+ bestMatch,
298
+ reason: 'similar_component_warning'
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Generate message about similar components
304
+ */
305
+ function generateSimilarMessage(componentName, similar) {
306
+ const bestMatch = similar[0];
307
+ let msg = `Similar component found: ${bestMatch.name} (${bestMatch.similarity}% match)`;
308
+
309
+ if (bestMatch.path) {
310
+ msg += ` at ${bestMatch.path}`;
311
+ }
312
+
313
+ if (similar.length > 1) {
314
+ msg += `\n\nOther similar components:`;
315
+ for (const s of similar.slice(1, 4)) {
316
+ msg += `\n- ${s.name} (${s.similarity}%)`;
317
+ if (s.path) msg += ` at ${s.path}`;
318
+ }
319
+ }
320
+
321
+ msg += `\n\nConsider:`;
322
+ msg += `\n1. Using the existing component`;
323
+ msg += `\n2. Adding a variant to the existing component`;
324
+ msg += `\n3. Extending the existing component`;
325
+
326
+ return msg;
327
+ }
328
+
329
+ module.exports = {
330
+ isComponentCheckEnabled,
331
+ getComponentPatterns,
332
+ getSimilarityThreshold,
333
+ isComponentPath,
334
+ loadComponentIndex,
335
+ parseAppMap,
336
+ calculateSimilarity,
337
+ extractComponentName,
338
+ findSimilarComponents,
339
+ checkComponentReuse,
340
+ generateSimilarMessage
341
+ };
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Core Hooks Index
5
+ *
6
+ * Exports all core hook modules for easy importing.
7
+ */
8
+
9
+ const taskGate = require('./task-gate');
10
+ const validation = require('./validation');
11
+ const loopCheck = require('./loop-check');
12
+ const componentCheck = require('./component-check');
13
+ const sessionContext = require('./session-context');
14
+
15
+ module.exports = {
16
+ // Task Gating
17
+ ...taskGate,
18
+ taskGate,
19
+
20
+ // Validation
21
+ ...validation,
22
+ validation,
23
+
24
+ // Loop Check
25
+ ...loopCheck,
26
+ loopCheck,
27
+
28
+ // Component Check
29
+ ...componentCheck,
30
+ componentCheck,
31
+
32
+ // Session Context
33
+ ...sessionContext,
34
+ sessionContext
35
+ };
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Loop Check (Core Module)
5
+ *
6
+ * CLI-agnostic loop enforcement logic.
7
+ * Verifies acceptance criteria are complete before allowing task completion.
8
+ *
9
+ * Returns a standardized result that adapters transform for specific CLIs.
10
+ */
11
+
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+
15
+ // Import from parent scripts directory
16
+ const { getConfig, PATHS } = require('../../flow-utils');
17
+ const { checkQueueContinuation, advanceTaskQueue } = require('../../flow-durable-session');
18
+
19
+ /**
20
+ * Check if loop enforcement is enabled
21
+ * @returns {boolean}
22
+ */
23
+ function isLoopEnforcementEnabled() {
24
+ const config = getConfig();
25
+
26
+ // Check hooks config first
27
+ if (config.hooks?.rules?.loopEnforcement?.enabled === false) {
28
+ return false;
29
+ }
30
+
31
+ // Fall back to loops config
32
+ if (config.loops?.enforced === false) {
33
+ return false;
34
+ }
35
+
36
+ if (config.loops?.enabled === false) {
37
+ return false;
38
+ }
39
+
40
+ return true;
41
+ }
42
+
43
+ /**
44
+ * Get the active loop session (if any)
45
+ * @returns {Object|null} Loop session or null
46
+ */
47
+ function getActiveLoopSession() {
48
+ try {
49
+ const loopSessionPath = path.join(PATHS.state, 'loop-session.json');
50
+ if (!fs.existsSync(loopSessionPath)) {
51
+ return null;
52
+ }
53
+
54
+ const session = JSON.parse(fs.readFileSync(loopSessionPath, 'utf-8'));
55
+ if (session.status !== 'active') {
56
+ return null;
57
+ }
58
+
59
+ return session;
60
+ } catch (err) {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Check if criteria are complete
67
+ * @param {Object} session - Loop session
68
+ * @returns {Object} Criteria status
69
+ */
70
+ function checkCriteriaStatus(session) {
71
+ if (!session || !session.acceptanceCriteria) {
72
+ return {
73
+ total: 0,
74
+ completed: 0,
75
+ pending: 0,
76
+ failed: 0,
77
+ skipped: 0,
78
+ allComplete: true
79
+ };
80
+ }
81
+
82
+ const criteria = session.acceptanceCriteria;
83
+ const completed = criteria.filter(c => c.status === 'completed' || c.status === 'passed').length;
84
+ const pending = criteria.filter(c => c.status === 'pending' || !c.status).length;
85
+ const failed = criteria.filter(c => c.status === 'failed').length;
86
+ const skipped = criteria.filter(c => c.status === 'skipped').length;
87
+
88
+ return {
89
+ total: criteria.length,
90
+ completed,
91
+ pending,
92
+ failed,
93
+ skipped,
94
+ allComplete: pending === 0 && failed === 0,
95
+ criteria
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Check if the current loop can exit (task can complete)
101
+ * @returns {Object} Result: { canExit, blocked, message, reason, criteriaStatus }
102
+ */
103
+ function checkLoopExit() {
104
+ if (!isLoopEnforcementEnabled()) {
105
+ return {
106
+ canExit: true,
107
+ blocked: false,
108
+ message: null,
109
+ reason: 'loop_enforcement_disabled'
110
+ };
111
+ }
112
+
113
+ const session = getActiveLoopSession();
114
+
115
+ if (!session) {
116
+ return {
117
+ canExit: true,
118
+ blocked: false,
119
+ message: null,
120
+ reason: 'no_active_loop'
121
+ };
122
+ }
123
+
124
+ const criteriaStatus = checkCriteriaStatus(session);
125
+
126
+ if (criteriaStatus.allComplete) {
127
+ // Task criteria complete - check if there are more tasks in queue
128
+ const queueResult = checkQueueContinuation();
129
+
130
+ if (queueResult.shouldContinue) {
131
+ // Advance queue and signal to continue to next task
132
+ advanceTaskQueue();
133
+ return {
134
+ canExit: false,
135
+ blocked: false,
136
+ continueToNext: true,
137
+ nextTaskId: queueResult.nextTaskId,
138
+ remaining: queueResult.remaining,
139
+ message: queueResult.message,
140
+ reason: 'queue_has_more_tasks',
141
+ criteriaStatus
142
+ };
143
+ }
144
+
145
+ if (queueResult.shouldPrompt) {
146
+ // Pause between tasks (if configured)
147
+ return {
148
+ canExit: false,
149
+ blocked: false,
150
+ shouldPrompt: true,
151
+ nextTaskId: queueResult.nextTaskId,
152
+ message: queueResult.message,
153
+ reason: 'queue_pause_between_tasks',
154
+ criteriaStatus
155
+ };
156
+ }
157
+
158
+ // No queue or queue complete - allow exit
159
+ return {
160
+ canExit: true,
161
+ blocked: false,
162
+ message: queueResult.reason === 'queue_complete'
163
+ ? queueResult.message
164
+ : `All ${criteriaStatus.completed} acceptance criteria completed.`,
165
+ reason: queueResult.reason === 'queue_complete' ? 'queue_complete' : 'criteria_complete',
166
+ criteriaStatus
167
+ };
168
+ }
169
+
170
+ // Check if max retries/iterations exceeded
171
+ const config = getConfig();
172
+ const maxRetries = config.loops?.maxRetries || 5;
173
+ const maxIterations = config.loops?.maxIterations || 20;
174
+
175
+ if (session.retries >= maxRetries) {
176
+ return {
177
+ canExit: true,
178
+ blocked: false,
179
+ message: `Max retries (${maxRetries}) reached. Allowing exit.`,
180
+ reason: 'max_retries_exceeded',
181
+ criteriaStatus
182
+ };
183
+ }
184
+
185
+ if (session.iterations >= maxIterations) {
186
+ return {
187
+ canExit: true,
188
+ blocked: false,
189
+ message: `Max iterations (${maxIterations}) reached. Allowing exit.`,
190
+ reason: 'max_iterations_exceeded',
191
+ criteriaStatus
192
+ };
193
+ }
194
+
195
+ // Block exit - criteria not complete
196
+ return {
197
+ canExit: false,
198
+ blocked: true,
199
+ message: generateBlockMessage(criteriaStatus, session),
200
+ reason: 'criteria_incomplete',
201
+ criteriaStatus
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Generate block message for incomplete criteria
207
+ */
208
+ function generateBlockMessage(criteriaStatus, session) {
209
+ let msg = `Cannot complete task. Acceptance criteria not met.\n\n`;
210
+
211
+ if (criteriaStatus.pending > 0) {
212
+ msg += `**Pending (${criteriaStatus.pending}):**\n`;
213
+ const pending = criteriaStatus.criteria.filter(c => c.status === 'pending' || !c.status);
214
+ pending.forEach(c => {
215
+ msg += `- ${c.description || c.text || c}\n`;
216
+ });
217
+ }
218
+
219
+ if (criteriaStatus.failed > 0) {
220
+ msg += `\n**Failed (${criteriaStatus.failed}):**\n`;
221
+ const failed = criteriaStatus.criteria.filter(c => c.status === 'failed');
222
+ failed.forEach(c => {
223
+ msg += `- ${c.description || c.text || c}\n`;
224
+ if (c.error || c.verificationResult) {
225
+ msg += ` Error: ${c.error || c.verificationResult}\n`;
226
+ }
227
+ });
228
+ }
229
+
230
+ msg += `\nComplete all criteria or use /wogi-done ${session.taskId} --force to override.`;
231
+
232
+ return msg;
233
+ }
234
+
235
+ module.exports = {
236
+ isLoopEnforcementEnabled,
237
+ getActiveLoopSession,
238
+ checkCriteriaStatus,
239
+ checkLoopExit,
240
+ generateBlockMessage
241
+ };