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,1246 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Loop Enforcer
5
+ *
6
+ * Ensures self-completing loops actually complete. When enforced:true,
7
+ * the loop cannot be exited until all acceptance criteria pass.
8
+ *
9
+ * v2.0: Now delegates to flow-durable-session.js for unified step tracking.
10
+ * Legacy loop-session.json is still supported for backward compatibility.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { getConfig, getProjectRoot, writeJson } = require('./flow-utils');
16
+
17
+ // v2.0: Import durable session for unified tracking
18
+ const durableSession = require('./flow-durable-session');
19
+
20
+ /**
21
+ * Sanitize a string for safe use in shell commands
22
+ * Only allows alphanumeric, underscore, hyphen, and dot characters
23
+ * @param {string} str - String to sanitize
24
+ * @returns {string} - Sanitized string
25
+ */
26
+ function sanitizeShellArg(str) {
27
+ if (!str || typeof str !== 'string') return '';
28
+ // Only allow safe characters: alphanumeric, underscore, hyphen, dot
29
+ return str.replace(/[^a-zA-Z0-9_.-]/g, '');
30
+ }
31
+
32
+ /**
33
+ * Escape a path for safe use in shell commands
34
+ * @param {string} p - Path to escape
35
+ * @returns {string} - Escaped path
36
+ */
37
+ function escapeShellPath(p) {
38
+ if (!p || typeof p !== 'string') return '';
39
+ // Escape special shell characters in paths
40
+ return p.replace(/(["\s'$`\\!*?#~<>^()[\]{}|;&])/g, '\\$1');
41
+ }
42
+
43
+ /**
44
+ * Check if loop enforcement is enabled
45
+ */
46
+ function isEnforcementEnabled() {
47
+ const config = getConfig();
48
+ return config.loops?.enforced === true;
49
+ }
50
+
51
+ /**
52
+ * Check if exit blocking is enabled
53
+ */
54
+ function isExitBlocked() {
55
+ const config = getConfig();
56
+ return config.loops?.blockExitUntilComplete === true;
57
+ }
58
+
59
+ /**
60
+ * Check if verification is required before marking criteria complete
61
+ */
62
+ function isVerificationRequired() {
63
+ const config = getConfig();
64
+ return config.loops?.requireVerification !== false; // Default true
65
+ }
66
+
67
+ /**
68
+ * Check if skipping is blocked (must complete or explicitly skip with approval)
69
+ */
70
+ function isSkipBlocked() {
71
+ const config = getConfig();
72
+ return config.loops?.blockOnSkip !== false; // Default true
73
+ }
74
+
75
+ /**
76
+ * Check if Simple Mode is enabled
77
+ */
78
+ function isSimpleModeEnabled() {
79
+ const config = getConfig();
80
+ return config.loops?.simpleMode?.enabled === true;
81
+ }
82
+
83
+ /**
84
+ * Check if regression re-check is enabled
85
+ */
86
+ function isRecheckEnabled() {
87
+ const config = getConfig();
88
+ return config.loops?.recheckAllAfterFix !== false; // Default true
89
+ }
90
+
91
+ /**
92
+ * Attempt to skip a criterion (requires approval if blockOnSkip is true)
93
+ * Returns { allowed: boolean, message: string }
94
+ */
95
+ function canSkipCriterion(criterionId, approvalGiven = false) {
96
+ const config = getConfig();
97
+ const session = getActiveLoop();
98
+
99
+ if (!session) {
100
+ return { allowed: false, message: 'No active loop session' };
101
+ }
102
+
103
+ const criterion = session.acceptanceCriteria.find(c => c.id === criterionId);
104
+ if (!criterion) {
105
+ return { allowed: false, message: `Criterion ${criterionId} not found` };
106
+ }
107
+
108
+ // If blockOnSkip is false, always allow
109
+ if (!isSkipBlocked()) {
110
+ return { allowed: true, message: 'Skip allowed (blockOnSkip: false)' };
111
+ }
112
+
113
+ // If blockOnSkip is true, require explicit approval
114
+ if (!approvalGiven) {
115
+ return {
116
+ allowed: false,
117
+ message: `āš ļø Cannot skip "${criterion.description}" without approval.\n` +
118
+ `Options:\n` +
119
+ ` 1. Complete the criterion\n` +
120
+ ` 2. Get explicit approval to skip\n` +
121
+ ` 3. Abort the task`,
122
+ requiresApproval: true
123
+ };
124
+ }
125
+
126
+ return { allowed: true, message: 'Skip approved by user' };
127
+ }
128
+
129
+ /**
130
+ * Get active loop session
131
+ * v2.0: Delegates to durable session with backward-compatible format
132
+ */
133
+ function getActiveLoop() {
134
+ const config = getConfig();
135
+
136
+ // v2.0: Use durable session if enabled
137
+ if (config.durableSteps?.enabled !== false) {
138
+ return durableSession.getActiveLoop();
139
+ }
140
+
141
+ // Legacy fallback: read loop-session.json directly
142
+ const projectRoot = getProjectRoot();
143
+ const sessionPath = path.join(projectRoot, '.workflow', 'state', 'loop-session.json');
144
+
145
+ if (!fs.existsSync(sessionPath)) {
146
+ return null;
147
+ }
148
+
149
+ try {
150
+ return JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
151
+ } catch (parseError) {
152
+ // Always log parse errors (corrupted session files are actionable issues)
153
+ console.warn(`[Warning] Could not parse loop session file: ${parseError.message}`);
154
+ if (process.env.DEBUG) {
155
+ console.warn(`[DEBUG] Session path: ${sessionPath}`);
156
+ }
157
+ return null;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Start a new enforcement loop session
163
+ * v2.0: Delegates to durable session for unified tracking
164
+ */
165
+ function startLoop(taskId, acceptanceCriteria) {
166
+ const config = getConfig();
167
+
168
+ // v2.0: Use durable session if enabled
169
+ if (config.durableSteps?.enabled !== false) {
170
+ const session = durableSession.createDurableSession(taskId, 'task', acceptanceCriteria);
171
+ // Return backward-compatible format
172
+ return durableSession.getActiveLoop();
173
+ }
174
+
175
+ // Legacy fallback: write to loop-session.json directly
176
+ const projectRoot = getProjectRoot();
177
+ const sessionPath = path.join(projectRoot, '.workflow', 'state', 'loop-session.json');
178
+
179
+ const session = {
180
+ taskId,
181
+ startedAt: new Date().toISOString(),
182
+ acceptanceCriteria: acceptanceCriteria.map((c, i) => ({
183
+ id: `AC-${i + 1}`,
184
+ description: c,
185
+ status: 'pending',
186
+ attempts: 0,
187
+ lastAttempt: null,
188
+ verificationResult: null
189
+ })),
190
+ iteration: 0,
191
+ retries: 0,
192
+ status: 'in_progress'
193
+ };
194
+
195
+ writeJson(sessionPath, session);
196
+ return session;
197
+ }
198
+
199
+ // ============================================================
200
+ // Simple Mode - Lightweight loop without formal criteria
201
+ // ============================================================
202
+
203
+ /**
204
+ * Start a Simple Mode loop
205
+ * Uses completion promise detection instead of formal acceptance criteria
206
+ *
207
+ * @param {string} taskId - Task identifier
208
+ * @param {string} completionPromise - String to detect in output for completion
209
+ */
210
+ function startSimpleLoop(taskId, completionPromise = null) {
211
+ const config = getConfig();
212
+ const projectRoot = getProjectRoot();
213
+ const sessionPath = path.join(projectRoot, '.workflow', 'state', 'simple-loop-session.json');
214
+
215
+ // Use configured completion promise or default
216
+ const promise = completionPromise || config.loops?.simpleMode?.completionPromise || 'TASK_COMPLETE';
217
+ const maxIterations = config.loops?.simpleMode?.maxIterations || 10;
218
+
219
+ const session = {
220
+ taskId,
221
+ mode: 'simple',
222
+ startedAt: new Date().toISOString(),
223
+ completionPromise: promise,
224
+ maxIterations,
225
+ iteration: 0,
226
+ status: 'in_progress',
227
+ outputs: [] // Store recent outputs to check for completion
228
+ };
229
+
230
+ writeJson(sessionPath, session);
231
+ return session;
232
+ }
233
+
234
+ /**
235
+ * Get active Simple Mode loop
236
+ */
237
+ function getSimpleLoop() {
238
+ const projectRoot = getProjectRoot();
239
+ const sessionPath = path.join(projectRoot, '.workflow', 'state', 'simple-loop-session.json');
240
+
241
+ if (!fs.existsSync(sessionPath)) {
242
+ return null;
243
+ }
244
+
245
+ try {
246
+ return JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
247
+ } catch (err) {
248
+ return null;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Record output in Simple Mode loop and check for completion
254
+ *
255
+ * @param {string} output - Output to check for completion promise
256
+ * @returns {object} - { completed: boolean, message: string }
257
+ */
258
+ function recordSimpleOutput(output) {
259
+ const session = getSimpleLoop();
260
+ if (!session) {
261
+ return { completed: false, message: 'No active simple loop' };
262
+ }
263
+
264
+ const projectRoot = getProjectRoot();
265
+ const sessionPath = path.join(projectRoot, '.workflow', 'state', 'simple-loop-session.json');
266
+
267
+ // Store output (keep last 5)
268
+ session.outputs = session.outputs || [];
269
+ session.outputs.push({
270
+ timestamp: new Date().toISOString(),
271
+ content: output.substring(0, 500) // Truncate long outputs
272
+ });
273
+ if (session.outputs.length > 5) {
274
+ session.outputs = session.outputs.slice(-5);
275
+ }
276
+
277
+ // Check for completion promise
278
+ const completed = output.includes(session.completionPromise);
279
+
280
+ if (completed) {
281
+ session.status = 'completed';
282
+ session.completedAt = new Date().toISOString();
283
+ writeJson(sessionPath, session);
284
+ return {
285
+ completed: true,
286
+ message: `Completion promise detected: "${session.completionPromise}"`
287
+ };
288
+ }
289
+
290
+ // Check max iterations
291
+ session.iteration++;
292
+ if (session.iteration >= session.maxIterations) {
293
+ session.status = 'max_iterations';
294
+ session.completedAt = new Date().toISOString();
295
+ writeJson(sessionPath, session);
296
+ return {
297
+ completed: true,
298
+ message: `Max iterations (${session.maxIterations}) reached`,
299
+ reason: 'max_iterations'
300
+ };
301
+ }
302
+
303
+ writeJson(sessionPath, session);
304
+ return {
305
+ completed: false,
306
+ message: `Iteration ${session.iteration}/${session.maxIterations}`,
307
+ iteration: session.iteration
308
+ };
309
+ }
310
+
311
+ /**
312
+ * End Simple Mode loop
313
+ */
314
+ function endSimpleLoop(status = 'completed') {
315
+ const session = getSimpleLoop();
316
+ if (!session) return null;
317
+
318
+ const projectRoot = getProjectRoot();
319
+ const sessionPath = path.join(projectRoot, '.workflow', 'state', 'simple-loop-session.json');
320
+
321
+ session.status = status;
322
+ session.endedAt = new Date().toISOString();
323
+
324
+ // Archive to history
325
+ const historyPath = path.join(projectRoot, '.workflow', 'state', 'simple-loop-history.json');
326
+ let history = [];
327
+ if (fs.existsSync(historyPath)) {
328
+ try {
329
+ history = JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
330
+ } catch { history = []; }
331
+ }
332
+ history.push(session);
333
+ if (history.length > 50) {
334
+ history = history.slice(-50);
335
+ }
336
+ fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
337
+
338
+ // Remove active session
339
+ fs.unlinkSync(sessionPath);
340
+ return session;
341
+ }
342
+
343
+ /**
344
+ * Check if Simple Mode loop can exit
345
+ */
346
+ function canExitSimpleLoop() {
347
+ const session = getSimpleLoop();
348
+ if (!session) {
349
+ return { canExit: true, reason: 'no-active-simple-loop' };
350
+ }
351
+
352
+ if (session.status === 'completed' || session.status === 'max_iterations') {
353
+ return {
354
+ canExit: true,
355
+ reason: session.status,
356
+ message: `Simple loop ${session.status}`
357
+ };
358
+ }
359
+
360
+ return {
361
+ canExit: false,
362
+ reason: 'in_progress',
363
+ message: `Simple loop iteration ${session.iteration}/${session.maxIterations}. Output "${session.completionPromise}" to complete.`
364
+ };
365
+ }
366
+
367
+ // ============================================================
368
+ // Criterion Updates with Regression Re-check
369
+ // ============================================================
370
+
371
+ /**
372
+ * Update criterion status in loop session
373
+ * v2.0: Delegates to durable session
374
+ * v2.2: Adds regression re-check after fixing any criterion
375
+ */
376
+ function updateCriterion(criterionId, status, verificationResult = null, context = {}) {
377
+ const config = getConfig();
378
+
379
+ // v2.0: Use durable session if enabled
380
+ if (config.durableSteps?.enabled !== false) {
381
+ // Map old AC-N format to new step-NNN format if needed
382
+ const stepId = criterionId.startsWith('AC-')
383
+ ? `step-${criterionId.replace('AC-', '').padStart(3, '0')}`
384
+ : criterionId;
385
+
386
+ durableSession.updateCriterion(stepId, status, verificationResult);
387
+
388
+ // v2.2: Regression re-check after completion
389
+ if (status === 'completed' && isRecheckEnabled()) {
390
+ performRegressionRecheck(criterionId, context);
391
+ }
392
+
393
+ return durableSession.getActiveLoop();
394
+ }
395
+
396
+ // Legacy fallback: update loop-session.json directly
397
+ const projectRoot = getProjectRoot();
398
+ const sessionPath = path.join(projectRoot, '.workflow', 'state', 'loop-session.json');
399
+
400
+ const session = getActiveLoop();
401
+ if (!session) return null;
402
+
403
+ const criterion = session.acceptanceCriteria.find(c => c.id === criterionId);
404
+ if (criterion) {
405
+ criterion.status = status;
406
+ criterion.attempts++;
407
+ criterion.lastAttempt = new Date().toISOString();
408
+ criterion.verificationResult = verificationResult;
409
+ }
410
+
411
+ // v2.2: Regression re-check after completing a criterion
412
+ if (status === 'completed' && isRecheckEnabled()) {
413
+ const regressions = performRegressionRecheck(criterionId, context);
414
+ if (regressions.length > 0) {
415
+ session.lastRegressionCheck = {
416
+ timestamp: new Date().toISOString(),
417
+ triggeredBy: criterionId,
418
+ regressions: regressions
419
+ };
420
+ }
421
+ }
422
+
423
+ writeJson(sessionPath, session);
424
+ return session;
425
+ }
426
+
427
+ /**
428
+ * Perform regression re-check on all previously completed criteria
429
+ * CRITICAL: After fixing ANY criterion, re-verify ALL criteria
430
+ *
431
+ * @param {string} excludeCriterionId - Criterion that was just completed (exclude from recheck)
432
+ * @param {object} context - Verification context (changedFiles, testResults, etc.)
433
+ * @returns {array} - Array of regressions found
434
+ */
435
+ function performRegressionRecheck(excludeCriterionId, context = {}) {
436
+ const config = getConfig();
437
+ const session = getActiveLoop();
438
+
439
+ if (!session) return [];
440
+
441
+ const regressions = [];
442
+ const completedCriteria = session.acceptanceCriteria
443
+ .filter(c => c.status === 'completed' && c.id !== excludeCriterionId);
444
+
445
+ if (completedCriteria.length === 0) return [];
446
+
447
+ console.log('\n\u{1F504} Re-verifying all completed criteria for regression...');
448
+
449
+ for (const criterion of completedCriteria) {
450
+ const result = verifyCriterion(criterion, context);
451
+
452
+ // If verification returned passed: false, we have a regression
453
+ if (result.passed === false) {
454
+ regressions.push({
455
+ criterionId: criterion.id,
456
+ description: criterion.description,
457
+ message: result.message,
458
+ verification: result.verification
459
+ });
460
+
461
+ // Handle based on config
462
+ const onRegression = config.loops?.regressionOnRecheck || 'warn';
463
+
464
+ if (onRegression === 'block') {
465
+ // Mark criterion as failed - must be fixed
466
+ criterion.status = 'failed';
467
+ criterion.verificationResult = `REGRESSION: ${result.message}`;
468
+ console.log(`\u{26A0}\u{FE0F} REGRESSION DETECTED in ${criterion.id}: ${criterion.description}`);
469
+ console.log(` ${result.message}`);
470
+ } else if (onRegression === 'warn') {
471
+ // Warn but don't change status
472
+ console.log(`\u{26A0}\u{FE0F} Warning: Possible regression in ${criterion.id}: ${criterion.description}`);
473
+ console.log(` ${result.message}`);
474
+ }
475
+ // 'auto-fix' mode would attempt to fix, but that's handled at a higher level
476
+ } else if (result.passed === true) {
477
+ console.log(`\u{2714}\u{FE0F} ${criterion.id} still passes`);
478
+ }
479
+ // null = couldn't verify, skip
480
+ }
481
+
482
+ if (regressions.length > 0) {
483
+ console.log(`\n\u{1F6A8} ${regressions.length} regression(s) detected!`);
484
+ } else if (completedCriteria.length > 0) {
485
+ console.log('\u{2705} All previously completed criteria still pass\n');
486
+ }
487
+
488
+ return regressions;
489
+ }
490
+
491
+ /**
492
+ * Check if loop can exit (all criteria met or max retries reached)
493
+ * v2.0: Uses durable session completion check
494
+ */
495
+ function canExitLoop() {
496
+ const config = getConfig();
497
+
498
+ // v2.0: Use durable session if enabled
499
+ if (config.durableSteps?.enabled !== false) {
500
+ const result = durableSession.canExitLoop();
501
+
502
+ // Add enforcement check
503
+ if (!isEnforcementEnabled() && !result.canExit) {
504
+ return { canExit: true, reason: 'enforcement-disabled' };
505
+ }
506
+
507
+ // Generate enforcement message if needed
508
+ if (!result.canExit) {
509
+ const session = getActiveLoop();
510
+ if (session) {
511
+ const pending = session.acceptanceCriteria.filter(c => c.status === 'pending');
512
+ const failed = session.acceptanceCriteria.filter(c => c.status === 'failed');
513
+ const skipped = session.acceptanceCriteria.filter(c => c.status === 'skipped');
514
+ result.message = generateEnforcementMessage(session, pending, failed, skipped);
515
+ }
516
+ }
517
+
518
+ return result;
519
+ }
520
+
521
+ // Legacy fallback
522
+ const session = getActiveLoop();
523
+
524
+ if (!session) return { canExit: true, reason: 'no-active-loop' };
525
+
526
+ // Not enforced? Can always exit
527
+ if (!isEnforcementEnabled()) {
528
+ return { canExit: true, reason: 'enforcement-disabled' };
529
+ }
530
+
531
+ const pending = session.acceptanceCriteria.filter(c => c.status === 'pending');
532
+ const failed = session.acceptanceCriteria.filter(c => c.status === 'failed');
533
+ const completed = session.acceptanceCriteria.filter(c => c.status === 'completed');
534
+ const skipped = session.acceptanceCriteria.filter(c => c.status === 'skipped');
535
+
536
+ // All criteria completed or skipped (with approval)?
537
+ if (pending.length === 0 && failed.length === 0) {
538
+ const skipNote = skipped.length > 0 ? ` (${skipped.length} skipped with approval)` : '';
539
+ return {
540
+ canExit: true,
541
+ reason: 'all-complete',
542
+ summary: `All ${completed.length} acceptance criteria passed${skipNote}`,
543
+ skippedCriteria: skipped.map(s => s.description)
544
+ };
545
+ }
546
+
547
+ // Max retries exceeded?
548
+ const maxRetries = config.loops?.maxRetries || 5;
549
+ if (session.retries >= maxRetries) {
550
+ return {
551
+ canExit: true,
552
+ reason: 'max-retries',
553
+ summary: `Max retries (${maxRetries}) reached. ${failed.length} criteria still failing.`,
554
+ failedCriteria: failed.map(f => f.description)
555
+ };
556
+ }
557
+
558
+ // Max iterations exceeded?
559
+ const maxIterations = config.loops?.maxIterations || 20;
560
+ if (session.iteration >= maxIterations) {
561
+ return {
562
+ canExit: true,
563
+ reason: 'max-iterations',
564
+ summary: `Max iterations (${maxIterations}) reached.`,
565
+ failedCriteria: failed.map(f => f.description)
566
+ };
567
+ }
568
+
569
+ // Cannot exit - work to do
570
+ return {
571
+ canExit: false,
572
+ reason: 'incomplete',
573
+ pending: pending.length,
574
+ failed: failed.length,
575
+ completed: completed.length,
576
+ skipped: skipped.length,
577
+ message: generateEnforcementMessage(session, pending, failed, skipped)
578
+ };
579
+ }
580
+
581
+ /**
582
+ * Generate the enforcement message
583
+ */
584
+ function generateEnforcementMessage(session, pending, failed, skipped = []) {
585
+ const lines = [
586
+ '🚫 LOOP ENFORCEMENT ACTIVE',
587
+ '─'.repeat(40),
588
+ '',
589
+ `Task: ${session.taskId}`,
590
+ `Iteration: ${session.iteration}`,
591
+ `Retries: ${session.retries}`,
592
+ ''
593
+ ];
594
+
595
+ if (pending.length > 0) {
596
+ lines.push(`ā³ Pending (${pending.length}):`);
597
+ pending.forEach(p => lines.push(` • ${p.description}`));
598
+ lines.push('');
599
+ }
600
+
601
+ if (failed.length > 0) {
602
+ lines.push(`āŒ Failed (${failed.length}):`);
603
+ failed.forEach(f => {
604
+ lines.push(` • ${f.description}`);
605
+ if (f.verificationResult) {
606
+ lines.push(` └─ ${f.verificationResult}`);
607
+ }
608
+ });
609
+ lines.push('');
610
+ }
611
+
612
+ if (skipped.length > 0) {
613
+ lines.push(`ā­ļø Skipped (${skipped.length}):`);
614
+ skipped.forEach(s => lines.push(` • ${s.description}`));
615
+ lines.push('');
616
+ }
617
+
618
+ lines.push('─'.repeat(40));
619
+ lines.push('šŸ”„ You must complete all criteria before exiting.');
620
+
621
+ return lines.join('\n');
622
+ }
623
+
624
+ /**
625
+ * Increment loop iteration
626
+ * v2.0: Delegates to durable session
627
+ */
628
+ function incrementIteration() {
629
+ const config = getConfig();
630
+
631
+ // v2.0: Use durable session if enabled
632
+ if (config.durableSteps?.enabled !== false) {
633
+ durableSession.incrementIteration();
634
+ return getActiveLoop();
635
+ }
636
+
637
+ // Legacy fallback
638
+ const projectRoot = getProjectRoot();
639
+ const sessionPath = path.join(projectRoot, '.workflow', 'state', 'loop-session.json');
640
+
641
+ const session = getActiveLoop();
642
+ if (!session) return null;
643
+
644
+ session.iteration++;
645
+ writeJson(sessionPath, session);
646
+ return session;
647
+ }
648
+
649
+ /**
650
+ * Increment retry count
651
+ * v2.0: Handled via durable session's totalRetries
652
+ */
653
+ function incrementRetry() {
654
+ const config = getConfig();
655
+
656
+ // v2.0: Use durable session - retries are tracked automatically in markStepFailed
657
+ if (config.durableSteps?.enabled !== false) {
658
+ // Durable session tracks retries per-step, but we can load the session to get total
659
+ const session = durableSession.loadDurableSession();
660
+ if (session) {
661
+ session.execution.totalRetries++;
662
+ durableSession.saveDurableSession(session);
663
+ }
664
+ return getActiveLoop();
665
+ }
666
+
667
+ // Legacy fallback
668
+ const projectRoot = getProjectRoot();
669
+ const sessionPath = path.join(projectRoot, '.workflow', 'state', 'loop-session.json');
670
+
671
+ const session = getActiveLoop();
672
+ if (!session) return null;
673
+
674
+ session.retries++;
675
+ writeJson(sessionPath, session);
676
+ return session;
677
+ }
678
+
679
+ /**
680
+ * End the loop session
681
+ * v2.0: Delegates to durable session archival
682
+ */
683
+ function endLoop(status = 'completed') {
684
+ const config = getConfig();
685
+
686
+ // v2.0: Use durable session if enabled
687
+ if (config.durableSteps?.enabled !== false) {
688
+ return durableSession.endLoop(status);
689
+ }
690
+
691
+ // Legacy fallback
692
+ const projectRoot = getProjectRoot();
693
+ const sessionPath = path.join(projectRoot, '.workflow', 'state', 'loop-session.json');
694
+
695
+ const session = getActiveLoop();
696
+ if (!session) return null;
697
+
698
+ session.status = status;
699
+ session.endedAt = new Date().toISOString();
700
+
701
+ // Archive to history
702
+ const historyPath = path.join(projectRoot, '.workflow', 'state', 'loop-history.json');
703
+ let history = [];
704
+ if (fs.existsSync(historyPath)) {
705
+ try {
706
+ history = JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
707
+ } catch {
708
+ history = [];
709
+ }
710
+ }
711
+ history.push(session);
712
+
713
+ // Keep last 50 sessions
714
+ if (history.length > 50) {
715
+ history = history.slice(-50);
716
+ }
717
+
718
+ fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
719
+
720
+ // Remove active session
721
+ fs.unlinkSync(sessionPath);
722
+
723
+ return session;
724
+ }
725
+
726
+ /**
727
+ * Get loop statistics
728
+ * v2.0: Delegates to durable session stats
729
+ */
730
+ function getLoopStats() {
731
+ const config = getConfig();
732
+
733
+ // v2.0: Use durable session stats if enabled
734
+ if (config.durableSteps?.enabled !== false) {
735
+ const stats = durableSession.getSessionStats();
736
+ return {
737
+ totalLoops: stats.totalSessions,
738
+ completed: stats.completed,
739
+ failed: stats.failed,
740
+ avgIterations: stats.avgSteps
741
+ };
742
+ }
743
+
744
+ // Legacy fallback
745
+ const projectRoot = getProjectRoot();
746
+ const historyPath = path.join(projectRoot, '.workflow', 'state', 'loop-history.json');
747
+
748
+ if (!fs.existsSync(historyPath)) {
749
+ return { totalLoops: 0, completed: 0, failed: 0, avgIterations: 0 };
750
+ }
751
+
752
+ try {
753
+ const history = JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
754
+ const completed = history.filter(h => h.status === 'completed').length;
755
+ const failed = history.filter(h => h.status === 'failed').length;
756
+ const avgIterations = history.length > 0
757
+ ? history.reduce((sum, h) => sum + h.iteration, 0) / history.length
758
+ : 0;
759
+
760
+ return {
761
+ totalLoops: history.length,
762
+ completed,
763
+ failed,
764
+ avgIterations: Math.round(avgIterations * 10) / 10
765
+ };
766
+ } catch {
767
+ return { totalLoops: 0, completed: 0, failed: 0, avgIterations: 0 };
768
+ }
769
+ }
770
+
771
+ /**
772
+ * Verify a specific criterion using auto-inference
773
+ * Returns { passed: boolean|null, message: string, verification: string, browserTestSuggested?: boolean }
774
+ */
775
+ function verifyCriterion(criterion, context = {}) {
776
+ const { execSync } = require('child_process');
777
+ const { changedFiles = [], testResults = null, lintResults = null } = context;
778
+ const config = getConfig();
779
+ const desc = criterion.description;
780
+ const descLower = desc.toLowerCase();
781
+
782
+ // Check if auto-inference is enabled
783
+ const autoInfer = config.loops?.autoInferVerification !== false; // Default true
784
+ if (!autoInfer) {
785
+ return { passed: null, message: 'āš ļø Auto-inference disabled', verification: 'disabled' };
786
+ }
787
+
788
+ const projectRoot = getProjectRoot();
789
+
790
+ // ═══════════════════════════════════════════════════════════════
791
+ // FILE EXISTENCE CHECKS
792
+ // ═══════════════════════════════════════════════════════════════
793
+
794
+ const filePatterns = [
795
+ /(?:create|created|add|added|new)\s+(?:a\s+)?(?:file\s+)?["`']?([^\s"`']+\.[a-z]{1,4})["`']?/i,
796
+ /file\s+["`']?([^\s"`']+\.[a-z]{1,4})["`']?\s+(?:created|exists|should exist)/i,
797
+ /["`']([^\s"`']+\.[a-z]{1,4})["`']?\s+(?:file\s+)?(?:created|exists)/i
798
+ ];
799
+
800
+ for (const pattern of filePatterns) {
801
+ const match = desc.match(pattern);
802
+ if (match) {
803
+ const filePath = match[1];
804
+ const fullPath = path.join(projectRoot, filePath);
805
+ const exists = fs.existsSync(fullPath);
806
+ return {
807
+ passed: exists,
808
+ message: exists ? `āœ“ File exists: ${filePath}` : `āœ— File not found: ${filePath}`,
809
+ verification: 'file-exists'
810
+ };
811
+ }
812
+ }
813
+
814
+ // ═══════════════════════════════════════════════════════════════
815
+ // FUNCTION/EXPORT CHECKS
816
+ // ═══════════════════════════════════════════════════════════════
817
+
818
+ const funcPatterns = [
819
+ /(?:function|export|method)\s+["`']?(\w+)["`']?\s+(?:exists?\s+)?(?:in|from)\s+["`']?([^\s"`']+)["`']?/i,
820
+ /["`']?([^\s"`']+)["`']?\s+(?:should\s+)?(?:export|have|contain)\s+["`']?(\w+)["`']?/i
821
+ ];
822
+
823
+ for (const pattern of funcPatterns) {
824
+ const match = desc.match(pattern);
825
+ if (match) {
826
+ let funcName, filePath;
827
+ // Handle both pattern orders
828
+ if (pattern.source.startsWith('(?:function')) {
829
+ [, funcName, filePath] = match;
830
+ } else {
831
+ [, filePath, funcName] = match;
832
+ }
833
+ const fullPath = path.join(projectRoot, filePath);
834
+ if (fs.existsSync(fullPath)) {
835
+ const content = fs.readFileSync(fullPath, 'utf-8');
836
+ const found = content.includes(funcName);
837
+ return {
838
+ passed: found,
839
+ message: found ? `āœ“ Found "${funcName}" in ${filePath}` : `āœ— "${funcName}" not found in ${filePath}`,
840
+ verification: 'function-exists'
841
+ };
842
+ }
843
+ }
844
+ }
845
+
846
+ // ═══════════════════════════════════════════════════════════════
847
+ // COMPONENT CHECKS
848
+ // ═══════════════════════════════════════════════════════════════
849
+
850
+ const componentMatch = descLower.match(/component\s+["`']?(\w+)["`']?\s+(?:renders?|works?|exists?|displays?)/i);
851
+ if (componentMatch) {
852
+ const componentName = componentMatch[1];
853
+ const searchPaths = ['src/components', 'components', 'src/ui', 'app'];
854
+ for (const searchPath of searchPaths) {
855
+ const searchDir = path.join(projectRoot, searchPath);
856
+ if (fs.existsSync(searchDir)) {
857
+ try {
858
+ // Sanitize component name and escape path for shell safety
859
+ const safeName = sanitizeShellArg(componentName);
860
+ const safeLower = sanitizeShellArg(componentName.toLowerCase());
861
+ const safePath = escapeShellPath(searchDir);
862
+ const files = execSync(
863
+ `find "${safePath}" -name "${safeName}.*" -o -name "${safeLower}.*" 2>/dev/null`,
864
+ { encoding: 'utf-8', timeout: 5000 }
865
+ ).trim();
866
+ if (files) {
867
+ return {
868
+ passed: true,
869
+ message: `āœ“ Component found: ${files.split('\n')[0]}`,
870
+ verification: 'component-exists'
871
+ };
872
+ }
873
+ } catch (err) { /* continue searching */ }
874
+ }
875
+ }
876
+ return {
877
+ passed: false,
878
+ message: `āœ— Component "${componentName}" not found`,
879
+ verification: 'component-exists'
880
+ };
881
+ }
882
+
883
+ // ═══════════════════════════════════════════════════════════════
884
+ // CLI COMMAND CHECKS
885
+ // ═══════════════════════════════════════════════════════════════
886
+
887
+ const cliMatch = descLower.match(/(?:command|cli|flow)\s+["`']?(\w+)["`']?\s+(?:works?|runs?|executes?)/i);
888
+ if (cliMatch) {
889
+ const cmd = cliMatch[1];
890
+ // Sanitize command name for shell safety
891
+ const safeCmd = sanitizeShellArg(cmd);
892
+ try {
893
+ execSync(`./scripts/flow ${safeCmd} --help`, {
894
+ cwd: projectRoot,
895
+ encoding: 'utf-8',
896
+ timeout: 10000,
897
+ stdio: ['pipe', 'pipe', 'pipe']
898
+ });
899
+ return {
900
+ passed: true,
901
+ message: `āœ“ Command "flow ${cmd}" works`,
902
+ verification: 'cli-works'
903
+ };
904
+ } catch (err) {
905
+ return {
906
+ passed: false,
907
+ message: `āœ— Command "flow ${cmd}" failed: ${err.message.substring(0, 100)}`,
908
+ verification: 'cli-works'
909
+ };
910
+ }
911
+ }
912
+
913
+ // ═══════════════════════════════════════════════════════════════
914
+ // CONFIG CHECKS
915
+ // ═══════════════════════════════════════════════════════════════
916
+
917
+ // Use original desc (not lowercase) to preserve config key case
918
+ const configMatch = desc.match(/(?:config(?:uration)?|settings?)\s+(?:has|contains|includes)\s+["`']?(\w+(?:\.\w+)*)["`']?/i) ||
919
+ desc.match(/["`']?(\w+(?:\.\w+)*)["`']?\s+(?:in|enabled in)\s+config/i);
920
+ if (configMatch) {
921
+ const configKey = configMatch[1];
922
+ try {
923
+ const currentConfig = getConfig();
924
+ const keys = configKey.split('.');
925
+ let value = currentConfig;
926
+ for (const k of keys) {
927
+ value = value?.[k];
928
+ }
929
+ const exists = value !== undefined;
930
+ return {
931
+ passed: exists,
932
+ message: exists
933
+ ? `āœ“ Config "${configKey}" exists (value: ${JSON.stringify(value).substring(0, 50)})`
934
+ : `āœ— Config "${configKey}" not found`,
935
+ verification: 'config-exists'
936
+ };
937
+ } catch (err) {
938
+ return { passed: false, message: `āœ— Config check failed: ${err.message}`, verification: 'config-exists' };
939
+ }
940
+ }
941
+
942
+ // ═══════════════════════════════════════════════════════════════
943
+ // INTEGRATION CHECKS (Module wired up)
944
+ // ═══════════════════════════════════════════════════════════════
945
+
946
+ const integrationMatch = desc.match(/["`']?(\w+)["`']?\s+(?:integrated|wired|connected)\s+(?:into|to|with)\s+["`']?([^\s"`']+)["`']?/i) ||
947
+ desc.match(/["`']?([^\s"`']+)["`']?\s+(?:requires?|imports?|uses?)\s+["`']?(\w+)["`']?/i);
948
+ if (integrationMatch) {
949
+ const [, moduleA, fileB] = integrationMatch;
950
+ const fullPath = path.join(projectRoot, fileB);
951
+ if (fs.existsSync(fullPath)) {
952
+ const content = fs.readFileSync(fullPath, 'utf-8');
953
+ const found = content.includes(moduleA);
954
+ return {
955
+ passed: found,
956
+ message: found ? `āœ“ "${moduleA}" found in ${fileB}` : `āœ— "${moduleA}" not found in ${fileB}`,
957
+ verification: 'integration'
958
+ };
959
+ }
960
+ }
961
+
962
+ // ═══════════════════════════════════════════════════════════════
963
+ // TEST CHECKS
964
+ // ═══════════════════════════════════════════════════════════════
965
+
966
+ if (descLower.includes('test') && (descLower.includes('pass') || descLower.includes('succeed'))) {
967
+ if (testResults) {
968
+ return {
969
+ passed: testResults.failed === 0,
970
+ message: testResults.failed === 0 ? 'āœ“ All tests pass' : `āœ— ${testResults.failed} tests failing`,
971
+ verification: 'tests'
972
+ };
973
+ }
974
+ // Try running tests
975
+ try {
976
+ execSync('npm test -- --passWithNoTests 2>&1 | tail -5', {
977
+ cwd: projectRoot,
978
+ encoding: 'utf-8',
979
+ timeout: 60000,
980
+ stdio: ['pipe', 'pipe', 'pipe']
981
+ });
982
+ return { passed: true, message: 'āœ“ Tests pass', verification: 'tests' };
983
+ } catch (err) {
984
+ return { passed: false, message: `āœ— Tests failed: ${err.message.substring(0, 100)}`, verification: 'tests' };
985
+ }
986
+ }
987
+
988
+ // ═══════════════════════════════════════════════════════════════
989
+ // LINT CHECKS
990
+ // ═══════════════════════════════════════════════════════════════
991
+
992
+ if (descLower.includes('lint') && (descLower.includes('pass') || descLower.includes('clean') || descLower.includes('no error'))) {
993
+ if (lintResults) {
994
+ return {
995
+ passed: lintResults.errors === 0,
996
+ message: lintResults.errors === 0 ? 'āœ“ No lint errors' : `āœ— ${lintResults.errors} lint errors`,
997
+ verification: 'lint'
998
+ };
999
+ }
1000
+ }
1001
+
1002
+ // ═══════════════════════════════════════════════════════════════
1003
+ // UI/BROWSER TESTING (Claude Browser Extension)
1004
+ // ═══════════════════════════════════════════════════════════════
1005
+
1006
+ const uiPatterns = [
1007
+ /(?:ui|user interface|page|screen|view)\s+(?:renders?|displays?|shows?|works?)/i,
1008
+ /(?:button|form|input|modal|dialog|dropdown)\s+(?:works?|functions?|responds?)/i,
1009
+ /(?:click|submit|select|hover)\s+(?:works?|triggers?)/i,
1010
+ /user\s+(?:can|should be able to)\s+(?:see|click|submit|enter|select)/i,
1011
+ /(?:displays?|shows?|renders?)\s+(?:correctly|properly|as expected)/i
1012
+ ];
1013
+
1014
+ const isUITest = uiPatterns.some(p => p.test(desc));
1015
+ const suggestBrowserTests = config.loops?.suggestBrowserTests !== false; // Default true
1016
+ const browserConfig = config.browserTesting || {};
1017
+
1018
+ if (isUITest && suggestBrowserTests && browserConfig.enabled) {
1019
+ return {
1020
+ passed: null,
1021
+ message: '🌐 UI criterion detected - browser test recommended',
1022
+ verification: 'browser-test',
1023
+ browserTestSuggested: true,
1024
+ suggestedFlow: inferBrowserTestFlow(desc)
1025
+ };
1026
+ }
1027
+
1028
+ // ═══════════════════════════════════════════════════════════════
1029
+ // FALLBACK
1030
+ // ═══════════════════════════════════════════════════════════════
1031
+
1032
+ const fallbackToManual = config.loops?.fallbackToManual !== false; // Default true
1033
+ if (fallbackToManual) {
1034
+ return {
1035
+ passed: null,
1036
+ message: 'āš ļø Could not auto-verify - manual check required',
1037
+ verification: 'manual'
1038
+ };
1039
+ }
1040
+
1041
+ return {
1042
+ passed: false,
1043
+ message: 'āœ— Could not verify and fallbackToManual is disabled',
1044
+ verification: 'failed'
1045
+ };
1046
+ }
1047
+
1048
+ /**
1049
+ * Infer browser test flow from criterion description
1050
+ */
1051
+ function inferBrowserTestFlow(description) {
1052
+ const desc = description.toLowerCase();
1053
+
1054
+ // Try to extract page/screen name (e.g., "the login page renders" -> "login")
1055
+ const pageMatch = desc.match(/(?:the\s+)?(\w+)\s+(?:page|screen|view)\s+(?:renders?|displays?|shows?|works?)/i);
1056
+
1057
+ // Try to extract component name (e.g., "the registration form works" -> "registration")
1058
+ const componentMatch = desc.match(/(?:the\s+)?(\w+)\s+(?:button|form|modal|dialog|dropdown|input)\s+(?:works?|functions?|responds?|renders?)/i);
1059
+
1060
+ // Try to extract action target (e.g., "click the submit button" -> "submit")
1061
+ const actionMatch = desc.match(/(?:click|submit|select|hover|enter)\s+(?:on\s+)?(?:the\s+)?(\w+)/i);
1062
+
1063
+ // Also try to find any named element in quotes
1064
+ const quotedMatch = desc.match(/["`'](\w+)["`']/);
1065
+
1066
+ const target = pageMatch?.[1] || componentMatch?.[1] || actionMatch?.[1] || quotedMatch?.[1] || 'unknown';
1067
+
1068
+ return {
1069
+ type: pageMatch ? 'page' : componentMatch ? 'component' : 'action',
1070
+ target: target,
1071
+ action: actionMatch ? actionMatch[0] : 'verify-renders',
1072
+ description
1073
+ };
1074
+ }
1075
+
1076
+ // ============================================================
1077
+ // Exports
1078
+ // ============================================================
1079
+
1080
+ module.exports = {
1081
+ // Standard loop functions
1082
+ isEnforcementEnabled,
1083
+ isExitBlocked,
1084
+ isVerificationRequired,
1085
+ isSkipBlocked,
1086
+ canSkipCriterion,
1087
+ getActiveLoop,
1088
+ startLoop,
1089
+ updateCriterion,
1090
+ canExitLoop,
1091
+ incrementIteration,
1092
+ incrementRetry,
1093
+ endLoop,
1094
+ getLoopStats,
1095
+ verifyCriterion,
1096
+ inferBrowserTestFlow,
1097
+ generateEnforcementMessage,
1098
+ // Simple Mode functions (v2.2)
1099
+ isSimpleModeEnabled,
1100
+ startSimpleLoop,
1101
+ getSimpleLoop,
1102
+ recordSimpleOutput,
1103
+ endSimpleLoop,
1104
+ canExitSimpleLoop,
1105
+ // Regression re-check (v2.2)
1106
+ isRecheckEnabled,
1107
+ performRegressionRecheck
1108
+ };
1109
+
1110
+ // ============================================================
1111
+ // CLI
1112
+ // ============================================================
1113
+
1114
+ if (require.main === module) {
1115
+ const args = process.argv.slice(2);
1116
+ const command = args[0];
1117
+
1118
+ switch (command) {
1119
+ case 'status': {
1120
+ const session = getActiveLoop();
1121
+ if (!session) {
1122
+ console.log('No active loop session');
1123
+ break;
1124
+ }
1125
+
1126
+ console.log('\nšŸ“Š Active Loop Session\n');
1127
+ console.log(`Task: ${session.taskId}`);
1128
+ console.log(`Started: ${session.startedAt}`);
1129
+ console.log(`Iteration: ${session.iteration}`);
1130
+ console.log(`Retries: ${session.retries}`);
1131
+ console.log('\nAcceptance Criteria:');
1132
+ session.acceptanceCriteria.forEach(c => {
1133
+ const icon = c.status === 'completed' ? 'āœ…' : c.status === 'failed' ? 'āŒ' : c.status === 'skipped' ? 'ā­ļø' : 'ā³';
1134
+ console.log(` ${icon} ${c.id}: ${c.description}`);
1135
+ if (c.verificationResult) {
1136
+ console.log(` └─ ${c.verificationResult}`);
1137
+ }
1138
+ });
1139
+
1140
+ const exit = canExitLoop();
1141
+ console.log(`\nCan exit: ${exit.canExit ? 'Yes' : 'No'} (${exit.reason})`);
1142
+ break;
1143
+ }
1144
+
1145
+ case 'stats': {
1146
+ const stats = getLoopStats();
1147
+ console.log('\nšŸ“ˆ Loop Statistics\n');
1148
+ console.log(`Total loops: ${stats.totalLoops}`);
1149
+ console.log(`Completed: ${stats.completed}`);
1150
+ console.log(`Failed: ${stats.failed}`);
1151
+ console.log(`Avg iterations: ${stats.avgIterations}`);
1152
+ break;
1153
+ }
1154
+
1155
+ case 'can-exit': {
1156
+ const result = canExitLoop();
1157
+ console.log(JSON.stringify(result, null, 2));
1158
+ process.exit(result.canExit ? 0 : 1);
1159
+ break;
1160
+ }
1161
+
1162
+ case 'simple-status': {
1163
+ const session = getSimpleLoop();
1164
+ if (!session) {
1165
+ console.log('No active simple loop session');
1166
+ break;
1167
+ }
1168
+
1169
+ console.log('\n\u{1F504} Simple Mode Loop Session\n');
1170
+ console.log(`Task: ${session.taskId}`);
1171
+ console.log(`Started: ${session.startedAt}`);
1172
+ console.log(`Iteration: ${session.iteration}/${session.maxIterations}`);
1173
+ console.log(`Completion Promise: "${session.completionPromise}"`);
1174
+ console.log(`Status: ${session.status}`);
1175
+
1176
+ const exit = canExitSimpleLoop();
1177
+ console.log(`\nCan exit: ${exit.canExit ? 'Yes' : 'No'} (${exit.reason})`);
1178
+ break;
1179
+ }
1180
+
1181
+ case 'simple-start': {
1182
+ const taskId = args[1] || `SIMPLE-${Date.now()}`;
1183
+ const promise = args[2];
1184
+ const session = startSimpleLoop(taskId, promise);
1185
+ console.log(`\u{2714}\u{FE0F} Simple Mode loop started`);
1186
+ console.log(` Task: ${session.taskId}`);
1187
+ console.log(` Completion Promise: "${session.completionPromise}"`);
1188
+ console.log(` Max Iterations: ${session.maxIterations}`);
1189
+ break;
1190
+ }
1191
+
1192
+ case 'simple-record': {
1193
+ const output = args.slice(1).join(' ');
1194
+ if (!output) {
1195
+ console.log('Error: Output text required');
1196
+ console.log('Usage: node flow-loop-enforcer.js simple-record "output text"');
1197
+ process.exit(1);
1198
+ }
1199
+ const result = recordSimpleOutput(output);
1200
+ console.log(JSON.stringify(result, null, 2));
1201
+ process.exit(result.completed ? 0 : 1);
1202
+ break;
1203
+ }
1204
+
1205
+ case 'simple-end': {
1206
+ const status = args[1] || 'completed';
1207
+ const session = endSimpleLoop(status);
1208
+ if (session) {
1209
+ console.log(`\u{2714}\u{FE0F} Simple loop ended: ${status}`);
1210
+ } else {
1211
+ console.log('No active simple loop to end');
1212
+ }
1213
+ break;
1214
+ }
1215
+
1216
+ default:
1217
+ console.log(`
1218
+ Wogi Flow - Loop Enforcer
1219
+
1220
+ Usage:
1221
+ node flow-loop-enforcer.js <command>
1222
+
1223
+ Standard Loop Commands:
1224
+ status Show active loop session
1225
+ stats Show loop statistics
1226
+ can-exit Check if loop can be exited (exit code 0=yes, 1=no)
1227
+
1228
+ Simple Mode Commands:
1229
+ simple-start [taskId] [promise] Start simple loop with optional completion promise
1230
+ simple-status Show simple loop status
1231
+ simple-record "output" Record output and check for completion
1232
+ simple-end [status] End simple loop
1233
+
1234
+ Configuration (config.json):
1235
+ loops.enforced: true Enable loop enforcement
1236
+ loops.blockExitUntilComplete: true Block session end until complete
1237
+ loops.maxRetries: 5 Max retries before forced exit
1238
+ loops.maxIterations: 20 Max iterations before forced exit
1239
+ loops.recheckAllAfterFix: true Re-verify all criteria after fixing one
1240
+ loops.regressionOnRecheck: "warn" How to handle regressions (warn|block)
1241
+ loops.simpleMode.enabled: true Enable Simple Mode
1242
+ loops.simpleMode.completionPromise: "TASK_COMPLETE"
1243
+ loops.simpleMode.maxIterations: 10
1244
+ `);
1245
+ }
1246
+ }