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.
- package/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- 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
|
+
}
|