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,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Component Check (Core Module)
|
|
5
|
+
*
|
|
6
|
+
* CLI-agnostic component reuse detection.
|
|
7
|
+
* Checks if a similar component exists before creating a new one.
|
|
8
|
+
*
|
|
9
|
+
* Returns a standardized result that adapters transform for specific CLIs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
|
|
15
|
+
// Import from parent scripts directory
|
|
16
|
+
const { getConfig, PATHS } = require('../../flow-utils');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if component reuse checking is enabled
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
function isComponentCheckEnabled() {
|
|
23
|
+
const config = getConfig();
|
|
24
|
+
return config.hooks?.rules?.componentReuse?.enabled !== false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get component patterns to check
|
|
29
|
+
* @returns {string[]} Glob patterns for component directories
|
|
30
|
+
*/
|
|
31
|
+
function getComponentPatterns() {
|
|
32
|
+
const config = getConfig();
|
|
33
|
+
return config.hooks?.rules?.componentReuse?.patterns ||
|
|
34
|
+
config.componentRules?.directories ||
|
|
35
|
+
['**/components/**', '**/ui/**', '**/src/components/**'];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get similarity threshold
|
|
40
|
+
* @returns {number} Threshold (0-100)
|
|
41
|
+
*/
|
|
42
|
+
function getSimilarityThreshold() {
|
|
43
|
+
const config = getConfig();
|
|
44
|
+
return config.hooks?.rules?.componentReuse?.threshold || 80;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a file path matches component patterns
|
|
49
|
+
* @param {string} filePath - Path to check
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
function isComponentPath(filePath) {
|
|
53
|
+
const patterns = getComponentPatterns();
|
|
54
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
55
|
+
|
|
56
|
+
for (const pattern of patterns) {
|
|
57
|
+
// Simple pattern matching (supports ** and *)
|
|
58
|
+
const regexPattern = pattern
|
|
59
|
+
.replace(/\*\*/g, '.*')
|
|
60
|
+
.replace(/\*/g, '[^/]*');
|
|
61
|
+
|
|
62
|
+
if (new RegExp(regexPattern).test(normalizedPath)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Load the component index
|
|
72
|
+
* @returns {Object|null} Component index or null
|
|
73
|
+
*/
|
|
74
|
+
function loadComponentIndex() {
|
|
75
|
+
try {
|
|
76
|
+
const indexPath = path.join(PATHS.state, 'component-index.json');
|
|
77
|
+
if (!fs.existsSync(indexPath)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse app-map.md for component entries
|
|
88
|
+
* @returns {Array} Component entries from app-map
|
|
89
|
+
*/
|
|
90
|
+
function parseAppMap() {
|
|
91
|
+
try {
|
|
92
|
+
const appMapPath = PATHS.appMap;
|
|
93
|
+
if (!fs.existsSync(appMapPath)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const content = fs.readFileSync(appMapPath, 'utf-8');
|
|
98
|
+
const components = [];
|
|
99
|
+
|
|
100
|
+
// Parse markdown table or list entries
|
|
101
|
+
const lines = content.split('\n');
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
// Match table rows: | ComponentName | description | path |
|
|
104
|
+
const tableMatch = line.match(/^\|\s*([^|]+)\s*\|/);
|
|
105
|
+
if (tableMatch && !tableMatch[1].includes('---')) {
|
|
106
|
+
const name = tableMatch[1].trim();
|
|
107
|
+
if (name && name !== 'Component' && name !== 'Name') {
|
|
108
|
+
components.push({ name, source: 'app-map' });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Match list items: - ComponentName: description
|
|
113
|
+
const listMatch = line.match(/^[-*]\s+\*?\*?([A-Z][a-zA-Z0-9]+)\*?\*?/);
|
|
114
|
+
if (listMatch) {
|
|
115
|
+
components.push({ name: listMatch[1], source: 'app-map' });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return components;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Calculate similarity between two strings (Levenshtein-based)
|
|
127
|
+
* @param {string} a - First string
|
|
128
|
+
* @param {string} b - Second string
|
|
129
|
+
* @returns {number} Similarity score (0-100)
|
|
130
|
+
*/
|
|
131
|
+
function calculateSimilarity(a, b) {
|
|
132
|
+
if (!a || !b) return 0;
|
|
133
|
+
|
|
134
|
+
const aLower = a.toLowerCase();
|
|
135
|
+
const bLower = b.toLowerCase();
|
|
136
|
+
|
|
137
|
+
if (aLower === bLower) return 100;
|
|
138
|
+
|
|
139
|
+
// Check if one contains the other
|
|
140
|
+
if (aLower.includes(bLower) || bLower.includes(aLower)) {
|
|
141
|
+
const longer = Math.max(a.length, b.length);
|
|
142
|
+
const shorter = Math.min(a.length, b.length);
|
|
143
|
+
return Math.round((shorter / longer) * 100);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Levenshtein distance
|
|
147
|
+
const matrix = [];
|
|
148
|
+
for (let i = 0; i <= b.length; i++) {
|
|
149
|
+
matrix[i] = [i];
|
|
150
|
+
}
|
|
151
|
+
for (let j = 0; j <= a.length; j++) {
|
|
152
|
+
matrix[0][j] = j;
|
|
153
|
+
}
|
|
154
|
+
for (let i = 1; i <= b.length; i++) {
|
|
155
|
+
for (let j = 1; j <= a.length; j++) {
|
|
156
|
+
if (bLower[i - 1] === aLower[j - 1]) {
|
|
157
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
158
|
+
} else {
|
|
159
|
+
matrix[i][j] = Math.min(
|
|
160
|
+
matrix[i - 1][j - 1] + 1,
|
|
161
|
+
matrix[i][j - 1] + 1,
|
|
162
|
+
matrix[i - 1][j] + 1
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const distance = matrix[b.length][a.length];
|
|
169
|
+
const maxLen = Math.max(a.length, b.length);
|
|
170
|
+
return Math.round(((maxLen - distance) / maxLen) * 100);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Extract component name from file path
|
|
175
|
+
* @param {string} filePath - File path
|
|
176
|
+
* @returns {string} Extracted component name
|
|
177
|
+
*/
|
|
178
|
+
function extractComponentName(filePath) {
|
|
179
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
180
|
+
// Remove common suffixes
|
|
181
|
+
return fileName
|
|
182
|
+
.replace(/\.(component|view|container|page|screen)$/i, '')
|
|
183
|
+
.replace(/[-_]/g, '');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Find similar components to a given name
|
|
188
|
+
* @param {string} componentName - Name to search for
|
|
189
|
+
* @returns {Array} Similar components sorted by similarity
|
|
190
|
+
*/
|
|
191
|
+
function findSimilarComponents(componentName) {
|
|
192
|
+
const threshold = getSimilarityThreshold();
|
|
193
|
+
const similar = [];
|
|
194
|
+
|
|
195
|
+
// Check component index
|
|
196
|
+
const index = loadComponentIndex();
|
|
197
|
+
if (index && index.components) {
|
|
198
|
+
for (const comp of index.components) {
|
|
199
|
+
const name = comp.name || extractComponentName(comp.path || '');
|
|
200
|
+
const similarity = calculateSimilarity(componentName, name);
|
|
201
|
+
if (similarity >= threshold) {
|
|
202
|
+
similar.push({
|
|
203
|
+
name,
|
|
204
|
+
path: comp.path,
|
|
205
|
+
similarity,
|
|
206
|
+
source: 'component-index'
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check app-map
|
|
213
|
+
const appMapComponents = parseAppMap();
|
|
214
|
+
for (const comp of appMapComponents) {
|
|
215
|
+
const similarity = calculateSimilarity(componentName, comp.name);
|
|
216
|
+
if (similarity >= threshold) {
|
|
217
|
+
// Avoid duplicates
|
|
218
|
+
if (!similar.some(s => s.name === comp.name)) {
|
|
219
|
+
similar.push({
|
|
220
|
+
name: comp.name,
|
|
221
|
+
similarity,
|
|
222
|
+
source: 'app-map'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Sort by similarity descending
|
|
229
|
+
return similar.sort((a, b) => b.similarity - a.similarity);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check component reuse for a new file
|
|
234
|
+
* @param {Object} options
|
|
235
|
+
* @param {string} options.filePath - Path of new file
|
|
236
|
+
* @param {string} options.content - Content of new file (optional)
|
|
237
|
+
* @returns {Object} Result: { allowed, warning, message, similar }
|
|
238
|
+
*/
|
|
239
|
+
function checkComponentReuse(options = {}) {
|
|
240
|
+
const { filePath, content } = options;
|
|
241
|
+
|
|
242
|
+
if (!isComponentCheckEnabled()) {
|
|
243
|
+
return {
|
|
244
|
+
allowed: true,
|
|
245
|
+
warning: false,
|
|
246
|
+
message: null,
|
|
247
|
+
reason: 'component_check_disabled'
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Only check component paths
|
|
252
|
+
if (!isComponentPath(filePath)) {
|
|
253
|
+
return {
|
|
254
|
+
allowed: true,
|
|
255
|
+
warning: false,
|
|
256
|
+
message: null,
|
|
257
|
+
reason: 'not_component_path'
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const componentName = extractComponentName(filePath);
|
|
262
|
+
const similar = findSimilarComponents(componentName);
|
|
263
|
+
|
|
264
|
+
if (similar.length === 0) {
|
|
265
|
+
return {
|
|
266
|
+
allowed: true,
|
|
267
|
+
warning: false,
|
|
268
|
+
message: null,
|
|
269
|
+
reason: 'no_similar_found'
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Found similar components
|
|
274
|
+
const config = getConfig();
|
|
275
|
+
const shouldBlock = config.hooks?.rules?.componentReuse?.blockOnSimilar === true;
|
|
276
|
+
const bestMatch = similar[0];
|
|
277
|
+
|
|
278
|
+
const message = generateSimilarMessage(componentName, similar);
|
|
279
|
+
|
|
280
|
+
if (shouldBlock) {
|
|
281
|
+
return {
|
|
282
|
+
allowed: false,
|
|
283
|
+
warning: false,
|
|
284
|
+
blocked: true,
|
|
285
|
+
message,
|
|
286
|
+
similar,
|
|
287
|
+
bestMatch,
|
|
288
|
+
reason: 'similar_component_exists'
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
allowed: true,
|
|
294
|
+
warning: true,
|
|
295
|
+
message,
|
|
296
|
+
similar,
|
|
297
|
+
bestMatch,
|
|
298
|
+
reason: 'similar_component_warning'
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Generate message about similar components
|
|
304
|
+
*/
|
|
305
|
+
function generateSimilarMessage(componentName, similar) {
|
|
306
|
+
const bestMatch = similar[0];
|
|
307
|
+
let msg = `Similar component found: ${bestMatch.name} (${bestMatch.similarity}% match)`;
|
|
308
|
+
|
|
309
|
+
if (bestMatch.path) {
|
|
310
|
+
msg += ` at ${bestMatch.path}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (similar.length > 1) {
|
|
314
|
+
msg += `\n\nOther similar components:`;
|
|
315
|
+
for (const s of similar.slice(1, 4)) {
|
|
316
|
+
msg += `\n- ${s.name} (${s.similarity}%)`;
|
|
317
|
+
if (s.path) msg += ` at ${s.path}`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
msg += `\n\nConsider:`;
|
|
322
|
+
msg += `\n1. Using the existing component`;
|
|
323
|
+
msg += `\n2. Adding a variant to the existing component`;
|
|
324
|
+
msg += `\n3. Extending the existing component`;
|
|
325
|
+
|
|
326
|
+
return msg;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
module.exports = {
|
|
330
|
+
isComponentCheckEnabled,
|
|
331
|
+
getComponentPatterns,
|
|
332
|
+
getSimilarityThreshold,
|
|
333
|
+
isComponentPath,
|
|
334
|
+
loadComponentIndex,
|
|
335
|
+
parseAppMap,
|
|
336
|
+
calculateSimilarity,
|
|
337
|
+
extractComponentName,
|
|
338
|
+
findSimilarComponents,
|
|
339
|
+
checkComponentReuse,
|
|
340
|
+
generateSimilarMessage
|
|
341
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Core Hooks Index
|
|
5
|
+
*
|
|
6
|
+
* Exports all core hook modules for easy importing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const taskGate = require('./task-gate');
|
|
10
|
+
const validation = require('./validation');
|
|
11
|
+
const loopCheck = require('./loop-check');
|
|
12
|
+
const componentCheck = require('./component-check');
|
|
13
|
+
const sessionContext = require('./session-context');
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
// Task Gating
|
|
17
|
+
...taskGate,
|
|
18
|
+
taskGate,
|
|
19
|
+
|
|
20
|
+
// Validation
|
|
21
|
+
...validation,
|
|
22
|
+
validation,
|
|
23
|
+
|
|
24
|
+
// Loop Check
|
|
25
|
+
...loopCheck,
|
|
26
|
+
loopCheck,
|
|
27
|
+
|
|
28
|
+
// Component Check
|
|
29
|
+
...componentCheck,
|
|
30
|
+
componentCheck,
|
|
31
|
+
|
|
32
|
+
// Session Context
|
|
33
|
+
...sessionContext,
|
|
34
|
+
sessionContext
|
|
35
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Loop Check (Core Module)
|
|
5
|
+
*
|
|
6
|
+
* CLI-agnostic loop enforcement logic.
|
|
7
|
+
* Verifies acceptance criteria are complete before allowing task completion.
|
|
8
|
+
*
|
|
9
|
+
* Returns a standardized result that adapters transform for specific CLIs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
|
|
15
|
+
// Import from parent scripts directory
|
|
16
|
+
const { getConfig, PATHS } = require('../../flow-utils');
|
|
17
|
+
const { checkQueueContinuation, advanceTaskQueue } = require('../../flow-durable-session');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if loop enforcement is enabled
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function isLoopEnforcementEnabled() {
|
|
24
|
+
const config = getConfig();
|
|
25
|
+
|
|
26
|
+
// Check hooks config first
|
|
27
|
+
if (config.hooks?.rules?.loopEnforcement?.enabled === false) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Fall back to loops config
|
|
32
|
+
if (config.loops?.enforced === false) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (config.loops?.enabled === false) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the active loop session (if any)
|
|
45
|
+
* @returns {Object|null} Loop session or null
|
|
46
|
+
*/
|
|
47
|
+
function getActiveLoopSession() {
|
|
48
|
+
try {
|
|
49
|
+
const loopSessionPath = path.join(PATHS.state, 'loop-session.json');
|
|
50
|
+
if (!fs.existsSync(loopSessionPath)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const session = JSON.parse(fs.readFileSync(loopSessionPath, 'utf-8'));
|
|
55
|
+
if (session.status !== 'active') {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return session;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if criteria are complete
|
|
67
|
+
* @param {Object} session - Loop session
|
|
68
|
+
* @returns {Object} Criteria status
|
|
69
|
+
*/
|
|
70
|
+
function checkCriteriaStatus(session) {
|
|
71
|
+
if (!session || !session.acceptanceCriteria) {
|
|
72
|
+
return {
|
|
73
|
+
total: 0,
|
|
74
|
+
completed: 0,
|
|
75
|
+
pending: 0,
|
|
76
|
+
failed: 0,
|
|
77
|
+
skipped: 0,
|
|
78
|
+
allComplete: true
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const criteria = session.acceptanceCriteria;
|
|
83
|
+
const completed = criteria.filter(c => c.status === 'completed' || c.status === 'passed').length;
|
|
84
|
+
const pending = criteria.filter(c => c.status === 'pending' || !c.status).length;
|
|
85
|
+
const failed = criteria.filter(c => c.status === 'failed').length;
|
|
86
|
+
const skipped = criteria.filter(c => c.status === 'skipped').length;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
total: criteria.length,
|
|
90
|
+
completed,
|
|
91
|
+
pending,
|
|
92
|
+
failed,
|
|
93
|
+
skipped,
|
|
94
|
+
allComplete: pending === 0 && failed === 0,
|
|
95
|
+
criteria
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if the current loop can exit (task can complete)
|
|
101
|
+
* @returns {Object} Result: { canExit, blocked, message, reason, criteriaStatus }
|
|
102
|
+
*/
|
|
103
|
+
function checkLoopExit() {
|
|
104
|
+
if (!isLoopEnforcementEnabled()) {
|
|
105
|
+
return {
|
|
106
|
+
canExit: true,
|
|
107
|
+
blocked: false,
|
|
108
|
+
message: null,
|
|
109
|
+
reason: 'loop_enforcement_disabled'
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const session = getActiveLoopSession();
|
|
114
|
+
|
|
115
|
+
if (!session) {
|
|
116
|
+
return {
|
|
117
|
+
canExit: true,
|
|
118
|
+
blocked: false,
|
|
119
|
+
message: null,
|
|
120
|
+
reason: 'no_active_loop'
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const criteriaStatus = checkCriteriaStatus(session);
|
|
125
|
+
|
|
126
|
+
if (criteriaStatus.allComplete) {
|
|
127
|
+
// Task criteria complete - check if there are more tasks in queue
|
|
128
|
+
const queueResult = checkQueueContinuation();
|
|
129
|
+
|
|
130
|
+
if (queueResult.shouldContinue) {
|
|
131
|
+
// Advance queue and signal to continue to next task
|
|
132
|
+
advanceTaskQueue();
|
|
133
|
+
return {
|
|
134
|
+
canExit: false,
|
|
135
|
+
blocked: false,
|
|
136
|
+
continueToNext: true,
|
|
137
|
+
nextTaskId: queueResult.nextTaskId,
|
|
138
|
+
remaining: queueResult.remaining,
|
|
139
|
+
message: queueResult.message,
|
|
140
|
+
reason: 'queue_has_more_tasks',
|
|
141
|
+
criteriaStatus
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (queueResult.shouldPrompt) {
|
|
146
|
+
// Pause between tasks (if configured)
|
|
147
|
+
return {
|
|
148
|
+
canExit: false,
|
|
149
|
+
blocked: false,
|
|
150
|
+
shouldPrompt: true,
|
|
151
|
+
nextTaskId: queueResult.nextTaskId,
|
|
152
|
+
message: queueResult.message,
|
|
153
|
+
reason: 'queue_pause_between_tasks',
|
|
154
|
+
criteriaStatus
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// No queue or queue complete - allow exit
|
|
159
|
+
return {
|
|
160
|
+
canExit: true,
|
|
161
|
+
blocked: false,
|
|
162
|
+
message: queueResult.reason === 'queue_complete'
|
|
163
|
+
? queueResult.message
|
|
164
|
+
: `All ${criteriaStatus.completed} acceptance criteria completed.`,
|
|
165
|
+
reason: queueResult.reason === 'queue_complete' ? 'queue_complete' : 'criteria_complete',
|
|
166
|
+
criteriaStatus
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check if max retries/iterations exceeded
|
|
171
|
+
const config = getConfig();
|
|
172
|
+
const maxRetries = config.loops?.maxRetries || 5;
|
|
173
|
+
const maxIterations = config.loops?.maxIterations || 20;
|
|
174
|
+
|
|
175
|
+
if (session.retries >= maxRetries) {
|
|
176
|
+
return {
|
|
177
|
+
canExit: true,
|
|
178
|
+
blocked: false,
|
|
179
|
+
message: `Max retries (${maxRetries}) reached. Allowing exit.`,
|
|
180
|
+
reason: 'max_retries_exceeded',
|
|
181
|
+
criteriaStatus
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (session.iterations >= maxIterations) {
|
|
186
|
+
return {
|
|
187
|
+
canExit: true,
|
|
188
|
+
blocked: false,
|
|
189
|
+
message: `Max iterations (${maxIterations}) reached. Allowing exit.`,
|
|
190
|
+
reason: 'max_iterations_exceeded',
|
|
191
|
+
criteriaStatus
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Block exit - criteria not complete
|
|
196
|
+
return {
|
|
197
|
+
canExit: false,
|
|
198
|
+
blocked: true,
|
|
199
|
+
message: generateBlockMessage(criteriaStatus, session),
|
|
200
|
+
reason: 'criteria_incomplete',
|
|
201
|
+
criteriaStatus
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Generate block message for incomplete criteria
|
|
207
|
+
*/
|
|
208
|
+
function generateBlockMessage(criteriaStatus, session) {
|
|
209
|
+
let msg = `Cannot complete task. Acceptance criteria not met.\n\n`;
|
|
210
|
+
|
|
211
|
+
if (criteriaStatus.pending > 0) {
|
|
212
|
+
msg += `**Pending (${criteriaStatus.pending}):**\n`;
|
|
213
|
+
const pending = criteriaStatus.criteria.filter(c => c.status === 'pending' || !c.status);
|
|
214
|
+
pending.forEach(c => {
|
|
215
|
+
msg += `- ${c.description || c.text || c}\n`;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (criteriaStatus.failed > 0) {
|
|
220
|
+
msg += `\n**Failed (${criteriaStatus.failed}):**\n`;
|
|
221
|
+
const failed = criteriaStatus.criteria.filter(c => c.status === 'failed');
|
|
222
|
+
failed.forEach(c => {
|
|
223
|
+
msg += `- ${c.description || c.text || c}\n`;
|
|
224
|
+
if (c.error || c.verificationResult) {
|
|
225
|
+
msg += ` Error: ${c.error || c.verificationResult}\n`;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
msg += `\nComplete all criteria or use /wogi-done ${session.taskId} --force to override.`;
|
|
231
|
+
|
|
232
|
+
return msg;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
isLoopEnforcementEnabled,
|
|
237
|
+
getActiveLoopSession,
|
|
238
|
+
checkCriteriaStatus,
|
|
239
|
+
checkLoopExit,
|
|
240
|
+
generateBlockMessage
|
|
241
|
+
};
|