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,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Session Context (Core Module)
|
|
5
|
+
*
|
|
6
|
+
* CLI-agnostic session context gathering.
|
|
7
|
+
* Gathers context to inject at session start.
|
|
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, getReadyData } = require('../../flow-utils');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if session context is enabled
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
function isSessionContextEnabled() {
|
|
23
|
+
const config = getConfig();
|
|
24
|
+
return config.hooks?.rules?.sessionContext?.enabled !== false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get suspended task info
|
|
29
|
+
* @returns {Object|null} Suspended task info or null
|
|
30
|
+
*/
|
|
31
|
+
function getSuspendedTask() {
|
|
32
|
+
try {
|
|
33
|
+
const suspensionPath = path.join(PATHS.state, 'suspension.json');
|
|
34
|
+
if (!fs.existsSync(suspensionPath)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const suspension = JSON.parse(fs.readFileSync(suspensionPath, 'utf-8'));
|
|
39
|
+
if (!suspension.taskId || suspension.status === 'resumed') {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return suspension;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get current task in progress
|
|
51
|
+
* @returns {Object|null} Current task or null
|
|
52
|
+
*/
|
|
53
|
+
function getCurrentTask() {
|
|
54
|
+
try {
|
|
55
|
+
const readyData = getReadyData();
|
|
56
|
+
if (readyData.inProgress && readyData.inProgress.length > 0) {
|
|
57
|
+
const task = readyData.inProgress[0];
|
|
58
|
+
return typeof task === 'string' ? { id: task } : task;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get key decisions from decisions.md
|
|
68
|
+
* @param {number} maxEntries - Max number of decisions to return
|
|
69
|
+
* @returns {Array} Key decisions
|
|
70
|
+
*/
|
|
71
|
+
function getKeyDecisions(maxEntries = 5) {
|
|
72
|
+
try {
|
|
73
|
+
if (!fs.existsSync(PATHS.decisions)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const content = fs.readFileSync(PATHS.decisions, 'utf-8');
|
|
78
|
+
const decisions = [];
|
|
79
|
+
|
|
80
|
+
// Parse markdown sections
|
|
81
|
+
const sections = content.split(/^##\s+/m).slice(1);
|
|
82
|
+
|
|
83
|
+
for (const section of sections.slice(0, maxEntries)) {
|
|
84
|
+
const lines = section.split('\n');
|
|
85
|
+
const title = lines[0].trim();
|
|
86
|
+
const body = lines.slice(1).join('\n').trim();
|
|
87
|
+
|
|
88
|
+
if (title && body) {
|
|
89
|
+
decisions.push({
|
|
90
|
+
title,
|
|
91
|
+
summary: body.split('\n')[0].substring(0, 150)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return decisions;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get recent activity from request log
|
|
104
|
+
* @param {number} maxEntries - Max entries to return
|
|
105
|
+
* @returns {Array} Recent activity
|
|
106
|
+
*/
|
|
107
|
+
function getRecentActivity(maxEntries = 3) {
|
|
108
|
+
try {
|
|
109
|
+
if (!fs.existsSync(PATHS.requestLog)) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const content = fs.readFileSync(PATHS.requestLog, 'utf-8');
|
|
114
|
+
const entries = [];
|
|
115
|
+
|
|
116
|
+
// Parse request log entries (### R-XXX format)
|
|
117
|
+
const entryRegex = /^###\s+R-(\d+)\s*\|\s*(\d{4}-\d{2}-\d{2}[^]*?)(?=^###\s+R-|\Z)/gm;
|
|
118
|
+
let match;
|
|
119
|
+
|
|
120
|
+
while ((match = entryRegex.exec(content)) !== null && entries.length < maxEntries) {
|
|
121
|
+
const id = `R-${match[1]}`;
|
|
122
|
+
const body = match[2];
|
|
123
|
+
|
|
124
|
+
// Extract request line
|
|
125
|
+
const requestMatch = body.match(/\*\*Request\*\*:\s*"?([^"\n]+)"?/);
|
|
126
|
+
const request = requestMatch ? requestMatch[1] : 'Unknown';
|
|
127
|
+
|
|
128
|
+
entries.push({ id, request });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return entries.reverse(); // Most recent first
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get session state summary
|
|
139
|
+
* @returns {Object|null} Session state or null
|
|
140
|
+
*/
|
|
141
|
+
function getSessionState() {
|
|
142
|
+
try {
|
|
143
|
+
const sessionPath = path.join(PATHS.state, 'session-state.json');
|
|
144
|
+
if (!fs.existsSync(sessionPath)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const state = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
|
|
149
|
+
return {
|
|
150
|
+
lastActive: state.lastActive,
|
|
151
|
+
recentFiles: (state.recentFiles || []).slice(0, 5),
|
|
152
|
+
recentDecisions: (state.recentDecisions || []).slice(0, 3)
|
|
153
|
+
};
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Gather all session context
|
|
161
|
+
* @param {Object} options
|
|
162
|
+
* @param {boolean} options.includeSuspended - Include suspended task info
|
|
163
|
+
* @param {boolean} options.includeDecisions - Include key decisions
|
|
164
|
+
* @param {boolean} options.includeActivity - Include recent activity
|
|
165
|
+
* @returns {Object} Session context
|
|
166
|
+
*/
|
|
167
|
+
function gatherSessionContext(options = {}) {
|
|
168
|
+
const config = getConfig();
|
|
169
|
+
const hookConfig = config.hooks?.rules?.sessionContext || {};
|
|
170
|
+
|
|
171
|
+
const {
|
|
172
|
+
includeSuspended = hookConfig.loadSuspendedTasks !== false,
|
|
173
|
+
includeDecisions = hookConfig.loadDecisions !== false,
|
|
174
|
+
includeActivity = hookConfig.loadRecentActivity !== false
|
|
175
|
+
} = options;
|
|
176
|
+
|
|
177
|
+
if (!isSessionContextEnabled()) {
|
|
178
|
+
return {
|
|
179
|
+
enabled: false,
|
|
180
|
+
context: null
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const context = {
|
|
185
|
+
timestamp: new Date().toISOString(),
|
|
186
|
+
projectName: config.projectName || path.basename(PATHS.root)
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Suspended task
|
|
190
|
+
if (includeSuspended) {
|
|
191
|
+
const suspended = getSuspendedTask();
|
|
192
|
+
if (suspended) {
|
|
193
|
+
context.suspendedTask = {
|
|
194
|
+
taskId: suspended.taskId,
|
|
195
|
+
reason: suspended.reason,
|
|
196
|
+
resumeCondition: suspended.resumeCondition,
|
|
197
|
+
suspendedAt: suspended.suspendedAt
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Current task
|
|
203
|
+
const currentTask = getCurrentTask();
|
|
204
|
+
if (currentTask) {
|
|
205
|
+
context.currentTask = currentTask;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Key decisions
|
|
209
|
+
if (includeDecisions) {
|
|
210
|
+
context.keyDecisions = getKeyDecisions(5);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Recent activity
|
|
214
|
+
if (includeActivity) {
|
|
215
|
+
context.recentActivity = getRecentActivity(3);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Session state
|
|
219
|
+
const sessionState = getSessionState();
|
|
220
|
+
if (sessionState) {
|
|
221
|
+
context.sessionState = sessionState;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
enabled: true,
|
|
226
|
+
context
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Format context for injection into a session
|
|
232
|
+
* @param {Object} context - Context from gatherSessionContext
|
|
233
|
+
* @returns {string} Formatted context string
|
|
234
|
+
*/
|
|
235
|
+
function formatContextForInjection(context) {
|
|
236
|
+
if (!context || !context.context) {
|
|
237
|
+
return '';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const ctx = context.context;
|
|
241
|
+
let output = '## Wogi Flow Session Context\n\n';
|
|
242
|
+
|
|
243
|
+
// Suspended task alert
|
|
244
|
+
if (ctx.suspendedTask) {
|
|
245
|
+
output += `### Suspended Task\n`;
|
|
246
|
+
output += `Task **${ctx.suspendedTask.taskId}** is suspended.\n`;
|
|
247
|
+
output += `- Reason: ${ctx.suspendedTask.reason || 'Not specified'}\n`;
|
|
248
|
+
if (ctx.suspendedTask.resumeCondition) {
|
|
249
|
+
output += `- Resume condition: ${ctx.suspendedTask.resumeCondition}\n`;
|
|
250
|
+
}
|
|
251
|
+
output += `\nRun \`/wogi-resume\` to continue.\n\n`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Current task
|
|
255
|
+
if (ctx.currentTask) {
|
|
256
|
+
output += `### Current Task\n`;
|
|
257
|
+
output += `Working on: **${ctx.currentTask.id}**\n`;
|
|
258
|
+
if (ctx.currentTask.title) {
|
|
259
|
+
output += `Title: ${ctx.currentTask.title}\n`;
|
|
260
|
+
}
|
|
261
|
+
output += '\n';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Key decisions
|
|
265
|
+
if (ctx.keyDecisions && ctx.keyDecisions.length > 0) {
|
|
266
|
+
output += `### Key Decisions\n`;
|
|
267
|
+
for (const decision of ctx.keyDecisions) {
|
|
268
|
+
output += `- **${decision.title}**: ${decision.summary}\n`;
|
|
269
|
+
}
|
|
270
|
+
output += '\n';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Recent activity
|
|
274
|
+
if (ctx.recentActivity && ctx.recentActivity.length > 0) {
|
|
275
|
+
output += `### Recent Activity\n`;
|
|
276
|
+
for (const activity of ctx.recentActivity) {
|
|
277
|
+
output += `- ${activity.id}: ${activity.request}\n`;
|
|
278
|
+
}
|
|
279
|
+
output += '\n';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return output;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
isSessionContextEnabled,
|
|
287
|
+
getSuspendedTask,
|
|
288
|
+
getCurrentTask,
|
|
289
|
+
getKeyDecisions,
|
|
290
|
+
getRecentActivity,
|
|
291
|
+
getSessionState,
|
|
292
|
+
gatherSessionContext,
|
|
293
|
+
formatContextForInjection
|
|
294
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Task Gate (Core Module)
|
|
5
|
+
*
|
|
6
|
+
* CLI-agnostic task gating logic.
|
|
7
|
+
* Checks if there's an active task before allowing implementation actions.
|
|
8
|
+
*
|
|
9
|
+
* Returns a standardized result that adapters transform for specific CLIs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Import from parent scripts directory
|
|
15
|
+
const { getConfig, getReadyData, findTask, PATHS } = require('../../flow-utils');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if task gating should be enforced
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function isTaskGatingEnabled() {
|
|
22
|
+
const config = getConfig();
|
|
23
|
+
|
|
24
|
+
// Check hooks config first
|
|
25
|
+
if (config.hooks?.rules?.taskGating?.enabled === false) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Fall back to enforcement config
|
|
30
|
+
if (config.enforcement?.strictMode === false) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (config.enforcement?.requireTaskForImplementation === false) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the currently active task (if any)
|
|
43
|
+
* @returns {Object|null} Task object or null
|
|
44
|
+
*/
|
|
45
|
+
function getActiveTask() {
|
|
46
|
+
try {
|
|
47
|
+
const readyData = getReadyData();
|
|
48
|
+
|
|
49
|
+
// Check inProgress queue
|
|
50
|
+
if (readyData.inProgress && readyData.inProgress.length > 0) {
|
|
51
|
+
const task = readyData.inProgress[0];
|
|
52
|
+
return typeof task === 'string' ? { id: task } : task;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check durable session
|
|
56
|
+
const fs = require('fs');
|
|
57
|
+
const durableSessionPath = path.join(PATHS.state, 'durable-session.json');
|
|
58
|
+
if (fs.existsSync(durableSessionPath)) {
|
|
59
|
+
const session = JSON.parse(fs.readFileSync(durableSessionPath, 'utf-8'));
|
|
60
|
+
if (session.taskId && session.status === 'active') {
|
|
61
|
+
return { id: session.taskId, fromDurableSession: true };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// If we can't read state, assume no active task
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check task gating for an edit/write operation
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} options
|
|
76
|
+
* @param {string} options.filePath - Path being edited/written
|
|
77
|
+
* @param {string} options.operation - 'edit' or 'write'
|
|
78
|
+
* @returns {Object} Result: { allowed, blocked, message, task }
|
|
79
|
+
*/
|
|
80
|
+
function checkTaskGate(options = {}) {
|
|
81
|
+
const { filePath, operation = 'edit' } = options;
|
|
82
|
+
// Exempt workflow state files from task gating
|
|
83
|
+
if (filePath && filePath.includes('.workflow/state/')) {
|
|
84
|
+
return {
|
|
85
|
+
allowed: true,
|
|
86
|
+
blocked: false,
|
|
87
|
+
message: null,
|
|
88
|
+
reason: 'workflow_state_exempt'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Also exempt plan files
|
|
93
|
+
if (filePath && filePath.includes('.claude/plans/')) {
|
|
94
|
+
return {
|
|
95
|
+
allowed: true,
|
|
96
|
+
blocked: false,
|
|
97
|
+
message: null,
|
|
98
|
+
reason: 'plan_file_exempt'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
// Check if gating is enabled
|
|
104
|
+
if (!isTaskGatingEnabled()) {
|
|
105
|
+
return {
|
|
106
|
+
allowed: true,
|
|
107
|
+
blocked: false,
|
|
108
|
+
message: null,
|
|
109
|
+
reason: 'task_gating_disabled'
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check for active task
|
|
114
|
+
const activeTask = getActiveTask();
|
|
115
|
+
|
|
116
|
+
if (activeTask) {
|
|
117
|
+
return {
|
|
118
|
+
allowed: true,
|
|
119
|
+
blocked: false,
|
|
120
|
+
message: null,
|
|
121
|
+
task: activeTask,
|
|
122
|
+
reason: 'task_active'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// No active task - should we block?
|
|
127
|
+
const config = getConfig();
|
|
128
|
+
const shouldBlock = config.hooks?.rules?.taskGating?.blockWithoutTask !== false;
|
|
129
|
+
|
|
130
|
+
if (!shouldBlock) {
|
|
131
|
+
return {
|
|
132
|
+
allowed: true,
|
|
133
|
+
blocked: false,
|
|
134
|
+
message: generateWarningMessage(operation, filePath),
|
|
135
|
+
reason: 'warn_only'
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Block the operation
|
|
140
|
+
return {
|
|
141
|
+
allowed: false,
|
|
142
|
+
blocked: true,
|
|
143
|
+
message: generateBlockMessage(operation, filePath),
|
|
144
|
+
reason: 'no_active_task'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Generate warning message (when not blocking)
|
|
150
|
+
*/
|
|
151
|
+
function generateWarningMessage(operation, filePath) {
|
|
152
|
+
const fileName = filePath ? path.basename(filePath) : 'file';
|
|
153
|
+
return `Warning: ${operation === 'write' ? 'Creating' : 'Editing'} ${fileName} without an active task. Consider starting a task first.`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generate block message
|
|
158
|
+
*/
|
|
159
|
+
function generateBlockMessage(operation, filePath) {
|
|
160
|
+
const fileName = filePath ? path.basename(filePath) : 'file';
|
|
161
|
+
return `Cannot ${operation} ${fileName} without an active task.
|
|
162
|
+
|
|
163
|
+
To proceed:
|
|
164
|
+
1. Check available tasks: /wogi-ready
|
|
165
|
+
2. Start an existing task: /wogi-start wf-XXXXXXXX
|
|
166
|
+
3. Or create a new task: /wogi-story "description"
|
|
167
|
+
|
|
168
|
+
Task gating is enforced when strictMode is enabled.`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
isTaskGatingEnabled,
|
|
173
|
+
getActiveTask,
|
|
174
|
+
checkTaskGate,
|
|
175
|
+
generateBlockMessage,
|
|
176
|
+
generateWarningMessage
|
|
177
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Validation (Core Module)
|
|
5
|
+
*
|
|
6
|
+
* CLI-agnostic validation logic.
|
|
7
|
+
* Runs lint/typecheck after file edits.
|
|
8
|
+
*
|
|
9
|
+
* Returns a standardized result that adapters transform for specific CLIs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
// Import from parent scripts directory
|
|
16
|
+
const { getConfig, PATHS } = require('../../flow-utils');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if validation is enabled
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
function isValidationEnabled() {
|
|
23
|
+
const config = getConfig();
|
|
24
|
+
return config.hooks?.rules?.validation?.enabled !== false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get validation commands for a file extension
|
|
29
|
+
* @param {string} ext - File extension (e.g., '.ts', '.tsx')
|
|
30
|
+
* @returns {string[]} Array of commands to run
|
|
31
|
+
*/
|
|
32
|
+
function getValidationCommands(ext) {
|
|
33
|
+
const config = getConfig();
|
|
34
|
+
|
|
35
|
+
// Check hooks config first
|
|
36
|
+
const hooksCommands = config.hooks?.rules?.validation?.commands;
|
|
37
|
+
if (hooksCommands && hooksCommands[`*${ext}`]) {
|
|
38
|
+
return hooksCommands[`*${ext}`];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Fall back to validation.afterFileEdit config
|
|
42
|
+
const legacyCommands = config.validation?.afterFileEdit?.commands;
|
|
43
|
+
if (legacyCommands && legacyCommands[`*${ext}`]) {
|
|
44
|
+
return legacyCommands[`*${ext}`];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Default commands by extension
|
|
48
|
+
const defaults = {
|
|
49
|
+
'.ts': ['npx tsc --noEmit'],
|
|
50
|
+
'.tsx': ['npx tsc --noEmit', 'npx eslint {file}'],
|
|
51
|
+
'.js': ['npx eslint {file}'],
|
|
52
|
+
'.jsx': ['npx eslint {file}']
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return defaults[ext] || [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run a single validation command
|
|
60
|
+
* @param {string} command - Command to run (may contain {file} placeholder)
|
|
61
|
+
* @param {string} filePath - Path to the file being validated
|
|
62
|
+
* @param {number} timeout - Timeout in ms
|
|
63
|
+
* @returns {Promise<Object>} Result: { passed, output, error, duration }
|
|
64
|
+
*/
|
|
65
|
+
async function runValidationCommand(command, filePath, timeout = 30000) {
|
|
66
|
+
const startTime = Date.now();
|
|
67
|
+
const actualCommand = command.replace('{file}', `"${filePath}"`);
|
|
68
|
+
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
try {
|
|
71
|
+
const result = execSync(actualCommand, {
|
|
72
|
+
cwd: PATHS.root,
|
|
73
|
+
encoding: 'utf-8',
|
|
74
|
+
timeout,
|
|
75
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
resolve({
|
|
79
|
+
passed: true,
|
|
80
|
+
output: result,
|
|
81
|
+
error: null,
|
|
82
|
+
duration: Date.now() - startTime,
|
|
83
|
+
command: actualCommand
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
resolve({
|
|
87
|
+
passed: false,
|
|
88
|
+
output: err.stdout || '',
|
|
89
|
+
error: err.stderr || err.message,
|
|
90
|
+
duration: Date.now() - startTime,
|
|
91
|
+
command: actualCommand
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run all validation for a file
|
|
99
|
+
* @param {Object} options
|
|
100
|
+
* @param {string} options.filePath - Path to the file
|
|
101
|
+
* @param {number} options.timeout - Timeout per command in ms
|
|
102
|
+
* @returns {Promise<Object>} Result: { passed, results, summary }
|
|
103
|
+
*/
|
|
104
|
+
async function runValidation(options = {}) {
|
|
105
|
+
const { filePath, timeout = 30000 } = options;
|
|
106
|
+
|
|
107
|
+
if (!isValidationEnabled()) {
|
|
108
|
+
return {
|
|
109
|
+
passed: true,
|
|
110
|
+
skipped: true,
|
|
111
|
+
reason: 'validation_disabled',
|
|
112
|
+
results: []
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ext = path.extname(filePath);
|
|
117
|
+
const commands = getValidationCommands(ext);
|
|
118
|
+
|
|
119
|
+
if (commands.length === 0) {
|
|
120
|
+
return {
|
|
121
|
+
passed: true,
|
|
122
|
+
skipped: true,
|
|
123
|
+
reason: 'no_commands_for_extension',
|
|
124
|
+
extension: ext,
|
|
125
|
+
results: []
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const results = [];
|
|
130
|
+
let allPassed = true;
|
|
131
|
+
|
|
132
|
+
for (const cmd of commands) {
|
|
133
|
+
const result = await runValidationCommand(cmd, filePath, timeout);
|
|
134
|
+
results.push(result);
|
|
135
|
+
if (!result.passed) {
|
|
136
|
+
allPassed = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
passed: allPassed,
|
|
142
|
+
skipped: false,
|
|
143
|
+
results,
|
|
144
|
+
summary: generateValidationSummary(results, filePath)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Generate human-readable validation summary
|
|
150
|
+
*/
|
|
151
|
+
function generateValidationSummary(results, filePath) {
|
|
152
|
+
const fileName = path.basename(filePath);
|
|
153
|
+
const passed = results.filter(r => r.passed).length;
|
|
154
|
+
const failed = results.filter(r => !r.passed).length;
|
|
155
|
+
|
|
156
|
+
if (failed === 0) {
|
|
157
|
+
return `Validation passed for ${fileName} (${passed} check${passed !== 1 ? 's' : ''})`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let summary = `Validation failed for ${fileName}:\n`;
|
|
161
|
+
for (const result of results.filter(r => !r.passed)) {
|
|
162
|
+
summary += `\n- ${result.command}:\n`;
|
|
163
|
+
if (result.error) {
|
|
164
|
+
// Truncate long error output
|
|
165
|
+
const errorLines = result.error.split('\n').slice(0, 10);
|
|
166
|
+
summary += errorLines.map(line => ` ${line}`).join('\n');
|
|
167
|
+
if (result.error.split('\n').length > 10) {
|
|
168
|
+
summary += '\n ... (truncated)';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return summary;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse TypeScript errors from output
|
|
178
|
+
* @param {string} output - TypeScript compiler output
|
|
179
|
+
* @returns {Array} Parsed errors
|
|
180
|
+
*/
|
|
181
|
+
function parseTypeScriptErrors(output) {
|
|
182
|
+
const errors = [];
|
|
183
|
+
const errorRegex = /(.+)\((\d+),(\d+)\):\s*error\s+(TS\d+):\s*(.+)/g;
|
|
184
|
+
|
|
185
|
+
let match;
|
|
186
|
+
while ((match = errorRegex.exec(output)) !== null) {
|
|
187
|
+
errors.push({
|
|
188
|
+
file: match[1],
|
|
189
|
+
line: parseInt(match[2], 10),
|
|
190
|
+
column: parseInt(match[3], 10),
|
|
191
|
+
code: match[4],
|
|
192
|
+
message: match[5]
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return errors;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Parse ESLint errors from output
|
|
201
|
+
* @param {string} output - ESLint output
|
|
202
|
+
* @returns {Array} Parsed errors
|
|
203
|
+
*/
|
|
204
|
+
function parseEslintErrors(output) {
|
|
205
|
+
const errors = [];
|
|
206
|
+
const errorRegex = /(\d+):(\d+)\s+(error|warning)\s+(.+?)\s+(\S+)$/gm;
|
|
207
|
+
|
|
208
|
+
let match;
|
|
209
|
+
while ((match = errorRegex.exec(output)) !== null) {
|
|
210
|
+
errors.push({
|
|
211
|
+
line: parseInt(match[1], 10),
|
|
212
|
+
column: parseInt(match[2], 10),
|
|
213
|
+
severity: match[3],
|
|
214
|
+
message: match[4],
|
|
215
|
+
rule: match[5]
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return errors;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
isValidationEnabled,
|
|
224
|
+
getValidationCommands,
|
|
225
|
+
runValidationCommand,
|
|
226
|
+
runValidation,
|
|
227
|
+
generateValidationSummary,
|
|
228
|
+
parseTypeScriptErrors,
|
|
229
|
+
parseEslintErrors
|
|
230
|
+
};
|