wogiflow 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,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
+ };