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,886 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * flow-context-scoring.js
5
+ *
6
+ * Phase 4.2: Context Priority Scoring System
7
+ *
8
+ * Smarter context selection than "include everything".
9
+ * Scores context items by relevance and fits them within token budgets.
10
+ *
11
+ * Usage:
12
+ * node flow-context-scoring.js score --task "<description>" --files <files>
13
+ * node flow-context-scoring.js budget --tokens 50000 --task "<description>"
14
+ * node flow-context-scoring.js analyze --file <file>
15
+ *
16
+ * @module flow-context-scoring
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ // ============================================================
23
+ // Imports
24
+ // ============================================================
25
+
26
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
27
+
28
+ const {
29
+ getConfig,
30
+ parseFlags,
31
+ info,
32
+ success,
33
+ warn,
34
+ error,
35
+ color,
36
+ outputJson,
37
+ printHeader,
38
+ printSection,
39
+ estimateTokens: utilsEstimateTokens
40
+ } = require('./flow-utils');
41
+
42
+ // ============================================================
43
+ // Constants
44
+ // ============================================================
45
+
46
+ /**
47
+ * Context priority weights.
48
+ * Higher = more important to include.
49
+ */
50
+ const CONTEXT_PRIORITIES = {
51
+ // Must-have context
52
+ required_types: 1.0, // Type definitions referenced in task
53
+ target_file: 0.95, // The file being modified
54
+ error_context: 0.93, // Error messages, stack traces
55
+
56
+ // High-value context
57
+ direct_imports: 0.90, // Files directly imported by target
58
+ interface_definitions: 0.88, // Interface/type definitions
59
+ api_contracts: 0.85, // API schemas, endpoints
60
+
61
+ // Medium-value context
62
+ related_imports: 0.80, // Secondary imports
63
+ test_files: 0.75, // Related test files
64
+ patterns: 0.70, // Pattern examples from decisions.md
65
+ similar_implementations: 0.65, // Similar code elsewhere
66
+
67
+ // Lower-value context
68
+ documentation: 0.50, // README, docs
69
+ examples: 0.45, // Example code
70
+ config_files: 0.40, // Config files
71
+ full_files: 0.30, // Full file contents (vs snippets)
72
+
73
+ // Minimal value
74
+ package_info: 0.20, // package.json
75
+ changelog: 0.10, // CHANGELOG
76
+ generated_files: 0.05 // Auto-generated code
77
+ };
78
+
79
+ /**
80
+ * Context categories for grouping.
81
+ */
82
+ const CONTEXT_CATEGORIES = {
83
+ REQUIRED: 'required',
84
+ HIGH: 'high',
85
+ MEDIUM: 'medium',
86
+ LOW: 'low',
87
+ MINIMAL: 'minimal'
88
+ };
89
+
90
+ /**
91
+ * Default context scoring configuration.
92
+ */
93
+ const DEFAULT_CONTEXT_CONFIG = {
94
+ enabled: true,
95
+ maxContextTokens: 100000,
96
+ reserveOutputTokens: 8000,
97
+ priorities: CONTEXT_PRIORITIES,
98
+ includeMinScore: 0.3,
99
+ snippetMaxLines: 50,
100
+ fullFileMaxLines: 200
101
+ };
102
+
103
+ // ============================================================
104
+ // Configuration
105
+ // ============================================================
106
+
107
+ /**
108
+ * Get context scoring configuration from config.json with defaults.
109
+ * @returns {Object} Context scoring configuration
110
+ */
111
+ function getContextConfig() {
112
+ const config = getConfig();
113
+ return {
114
+ ...DEFAULT_CONTEXT_CONFIG,
115
+ ...(config.contextScoring || {})
116
+ };
117
+ }
118
+
119
+ // ============================================================
120
+ // Context Item Types
121
+ // ============================================================
122
+
123
+ /**
124
+ * Create a context item.
125
+ * @param {Object} params - Item parameters
126
+ * @returns {Object} Context item
127
+ */
128
+ function createContextItem({
129
+ id,
130
+ type,
131
+ content,
132
+ source,
133
+ relevance = 0,
134
+ tokens = 0,
135
+ metadata = {}
136
+ }) {
137
+ const baseScore = CONTEXT_PRIORITIES[type] || 0.5;
138
+
139
+ return {
140
+ id: id || `ctx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
141
+ type,
142
+ content,
143
+ source,
144
+ baseScore,
145
+ relevance,
146
+ finalScore: baseScore * (0.5 + relevance * 0.5), // Combine base and relevance
147
+ tokens: tokens || estimateTokens(content),
148
+ category: categorizeScore(baseScore),
149
+ metadata,
150
+ included: false
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Categorize a score into a category.
156
+ * @param {number} score - Score value
157
+ * @returns {string} Category
158
+ */
159
+ function categorizeScore(score) {
160
+ if (score >= 0.9) return CONTEXT_CATEGORIES.REQUIRED;
161
+ if (score >= 0.7) return CONTEXT_CATEGORIES.HIGH;
162
+ if (score >= 0.5) return CONTEXT_CATEGORIES.MEDIUM;
163
+ if (score >= 0.2) return CONTEXT_CATEGORIES.LOW;
164
+ return CONTEXT_CATEGORIES.MINIMAL;
165
+ }
166
+
167
+ // ============================================================
168
+ // Token Estimation
169
+ // ============================================================
170
+
171
+ /**
172
+ * Estimate tokens for a piece of content.
173
+ * Uses centralized estimateTokens with hybrid char+line estimation.
174
+ * @param {string} content - Content to estimate
175
+ * @returns {number} Estimated tokens
176
+ */
177
+ function estimateTokens(content) {
178
+ return utilsEstimateTokens(content, { useLineEstimate: true });
179
+ }
180
+
181
+ /**
182
+ * Estimate tokens for a file.
183
+ * @param {string} filePath - Path to file
184
+ * @returns {number} Estimated tokens
185
+ */
186
+ function estimateFileTokens(filePath) {
187
+ try {
188
+ const content = fs.readFileSync(filePath, 'utf8');
189
+ return estimateTokens(content);
190
+ } catch {
191
+ return 0;
192
+ }
193
+ }
194
+
195
+ // ============================================================
196
+ // Relevance Scoring
197
+ // ============================================================
198
+
199
+ /**
200
+ * Score relevance of content to a task.
201
+ * @param {string} content - Content to score
202
+ * @param {Object} taskContext - Task context
203
+ * @returns {number} Relevance score (0-1)
204
+ */
205
+ function scoreRelevance(content, taskContext) {
206
+ if (!content || !taskContext) return 0;
207
+
208
+ const contentLower = content.toLowerCase();
209
+ let score = 0;
210
+ let matches = 0;
211
+
212
+ // Check for keyword matches
213
+ const keywords = taskContext.keywords || [];
214
+ for (const keyword of keywords) {
215
+ if (contentLower.includes(keyword.toLowerCase())) {
216
+ score += 0.1;
217
+ matches++;
218
+ }
219
+ }
220
+
221
+ // Check for file references
222
+ const files = taskContext.files || [];
223
+ for (const file of files) {
224
+ const fileName = path.basename(file).toLowerCase();
225
+ if (contentLower.includes(fileName)) {
226
+ score += 0.2;
227
+ matches++;
228
+ }
229
+ }
230
+
231
+ // Check for type/interface references
232
+ const types = taskContext.types || [];
233
+ for (const type of types) {
234
+ if (content.includes(type)) {
235
+ score += 0.15;
236
+ matches++;
237
+ }
238
+ }
239
+
240
+ // Check for function/method references
241
+ const functions = taskContext.functions || [];
242
+ for (const func of functions) {
243
+ if (content.includes(func)) {
244
+ score += 0.15;
245
+ matches++;
246
+ }
247
+ }
248
+
249
+ // Boost if multiple matches
250
+ if (matches > 3) score *= 1.2;
251
+ if (matches > 5) score *= 1.3;
252
+
253
+ return Math.min(1, score);
254
+ }
255
+
256
+ /**
257
+ * Extract task context from a description.
258
+ * @param {string} description - Task description
259
+ * @returns {Object} Extracted context
260
+ */
261
+ function extractTaskContext(description) {
262
+ const context = {
263
+ keywords: [],
264
+ files: [],
265
+ types: [],
266
+ functions: []
267
+ };
268
+
269
+ // Extract file references
270
+ const filePattern = /[a-zA-Z0-9_-]+\.(ts|tsx|js|jsx|json|md|css|scss|html|vue|svelte)/g;
271
+ let match;
272
+ while ((match = filePattern.exec(description)) !== null) {
273
+ context.files.push(match[0]);
274
+ }
275
+
276
+ // Extract type/interface names (PascalCase)
277
+ const typePattern = /\b([A-Z][a-zA-Z]+(?:Type|Interface|Props|State|Config|Options|Params))\b/g;
278
+ while ((match = typePattern.exec(description)) !== null) {
279
+ context.types.push(match[1]);
280
+ }
281
+
282
+ // Extract function names (camelCase verbs)
283
+ const funcPattern = /\b(get|set|create|update|delete|fetch|load|save|handle|process|validate|render|format|parse|build|init)[A-Z][a-zA-Z]+/g;
284
+ while ((match = funcPattern.exec(description)) !== null) {
285
+ context.functions.push(match[0]);
286
+ }
287
+
288
+ // Extract significant keywords
289
+ const keywords = description
290
+ .toLowerCase()
291
+ .replace(/[^a-z0-9\s]/g, ' ')
292
+ .split(/\s+/)
293
+ .filter(word => word.length > 3)
294
+ .filter(word => !['with', 'that', 'this', 'from', 'have', 'will', 'should', 'would', 'could'].includes(word));
295
+
296
+ context.keywords = [...new Set(keywords)].slice(0, 20);
297
+
298
+ return context;
299
+ }
300
+
301
+ // ============================================================
302
+ // Context Collection
303
+ // ============================================================
304
+
305
+ /**
306
+ * Collect context items for a task.
307
+ * @param {Object} params - Collection parameters
308
+ * @returns {Array} Context items
309
+ */
310
+ function collectContext({ description, targetFiles = [], additionalContext = [] }) {
311
+ const items = [];
312
+ const taskContext = extractTaskContext(description);
313
+
314
+ // Add target files (highest priority)
315
+ for (const file of targetFiles) {
316
+ try {
317
+ if (!fs.existsSync(file)) continue;
318
+
319
+ const content = fs.readFileSync(file, 'utf8');
320
+ items.push(createContextItem({
321
+ type: 'target_file',
322
+ content,
323
+ source: file,
324
+ relevance: 1.0,
325
+ metadata: { isTarget: true }
326
+ }));
327
+
328
+ // Extract imports from target file
329
+ const imports = extractImports(content, file);
330
+ for (const imp of imports) {
331
+ // Validate resolvedPath exists before using
332
+ if (!imp.resolvedPath) continue;
333
+
334
+ try {
335
+ if (!fs.existsSync(imp.resolvedPath)) continue;
336
+
337
+ const importContent = fs.readFileSync(imp.resolvedPath, 'utf8');
338
+ items.push(createContextItem({
339
+ type: 'direct_imports',
340
+ content: importContent,
341
+ source: imp.resolvedPath,
342
+ relevance: scoreRelevance(importContent, taskContext),
343
+ metadata: { importedFrom: file, importPath: imp.path }
344
+ }));
345
+ } catch {
346
+ // Skip unreadable import files
347
+ }
348
+ }
349
+ } catch {
350
+ // Skip unreadable target files
351
+ }
352
+ }
353
+
354
+ // Add additional context with scoring
355
+ for (const ctx of additionalContext) {
356
+ const relevance = scoreRelevance(ctx.content, taskContext);
357
+ items.push(createContextItem({
358
+ type: ctx.type || 'full_files',
359
+ content: ctx.content,
360
+ source: ctx.source,
361
+ relevance,
362
+ metadata: ctx.metadata || {}
363
+ }));
364
+ }
365
+
366
+ // Add patterns from decisions.md
367
+ const decisionsPath = path.join(PROJECT_ROOT, '.workflow', 'state', 'decisions.md');
368
+ try {
369
+ if (fs.existsSync(decisionsPath)) {
370
+ const decisions = fs.readFileSync(decisionsPath, 'utf8');
371
+ const relevance = scoreRelevance(decisions, taskContext);
372
+ if (relevance > 0.3) {
373
+ items.push(createContextItem({
374
+ type: 'patterns',
375
+ content: decisions,
376
+ source: decisionsPath,
377
+ relevance
378
+ }));
379
+ }
380
+ }
381
+ } catch {
382
+ // Skip if decisions.md is unreadable
383
+ }
384
+
385
+ return items;
386
+ }
387
+
388
+ /**
389
+ * Extract imports from a file.
390
+ * @param {string} content - File content
391
+ * @param {string} filePath - File path
392
+ * @returns {Array} Import information
393
+ */
394
+ function extractImports(content, filePath) {
395
+ const imports = [];
396
+ const dir = path.dirname(filePath);
397
+
398
+ // ES6 imports
399
+ const es6Pattern = /import\s+(?:[\w{},\s*]+\s+from\s+)?['"]([^'"]+)['"]/g;
400
+ let match;
401
+ while ((match = es6Pattern.exec(content)) !== null) {
402
+ const importPath = match[1];
403
+ if (importPath.startsWith('.')) {
404
+ const resolvedPath = resolveImportPath(importPath, dir);
405
+ if (resolvedPath) {
406
+ imports.push({ path: importPath, resolvedPath });
407
+ }
408
+ }
409
+ }
410
+
411
+ // CommonJS requires
412
+ const cjsPattern = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
413
+ while ((match = cjsPattern.exec(content)) !== null) {
414
+ const importPath = match[1];
415
+ if (importPath.startsWith('.')) {
416
+ const resolvedPath = resolveImportPath(importPath, dir);
417
+ if (resolvedPath) {
418
+ imports.push({ path: importPath, resolvedPath });
419
+ }
420
+ }
421
+ }
422
+
423
+ return imports;
424
+ }
425
+
426
+ /**
427
+ * Resolve an import path to an actual file.
428
+ * @param {string} importPath - Import path
429
+ * @param {string} baseDir - Base directory
430
+ * @returns {string|null} Resolved path or null
431
+ */
432
+ function resolveImportPath(importPath, baseDir) {
433
+ const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];
434
+
435
+ for (const ext of extensions) {
436
+ try {
437
+ const fullPath = path.resolve(baseDir, importPath + ext);
438
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
439
+ return fullPath;
440
+ }
441
+ } catch {
442
+ // Skip if path check fails (permissions, symlink issues)
443
+ }
444
+ }
445
+
446
+ return null;
447
+ }
448
+
449
+ // ============================================================
450
+ // Budget Fitting
451
+ // ============================================================
452
+
453
+ /**
454
+ * Fit context items within a token budget.
455
+ * @param {Array} items - Context items
456
+ * @param {number} budget - Token budget
457
+ * @param {Object} options - Fitting options
458
+ * @returns {Object} Fitting result
459
+ */
460
+ function fitToBudget(items, budget, options = {}) {
461
+ const config = getContextConfig();
462
+ const minScore = options.minScore || config.includeMinScore;
463
+
464
+ // Sort by final score (descending)
465
+ const sorted = [...items].sort((a, b) => b.finalScore - a.finalScore);
466
+
467
+ let usedTokens = 0;
468
+ const included = [];
469
+ const excluded = [];
470
+
471
+ for (const item of sorted) {
472
+ // Skip if below minimum score
473
+ if (item.finalScore < minScore) {
474
+ excluded.push({ ...item, reason: 'Below minimum score' });
475
+ continue;
476
+ }
477
+
478
+ // Check if it fits
479
+ if (usedTokens + item.tokens <= budget) {
480
+ item.included = true;
481
+ included.push(item);
482
+ usedTokens += item.tokens;
483
+ } else {
484
+ // Try to include a snippet instead
485
+ if (options.allowSnippets && item.tokens > config.snippetMaxLines * TOKENS_PER_LINE) {
486
+ const snippet = createSnippet(item, config.snippetMaxLines);
487
+ if (usedTokens + snippet.tokens <= budget) {
488
+ snippet.included = true;
489
+ included.push(snippet);
490
+ usedTokens += snippet.tokens;
491
+ excluded.push({ ...item, reason: 'Included as snippet' });
492
+ } else {
493
+ excluded.push({ ...item, reason: 'Exceeds budget (even as snippet)' });
494
+ }
495
+ } else {
496
+ excluded.push({ ...item, reason: 'Exceeds budget' });
497
+ }
498
+ }
499
+ }
500
+
501
+ return {
502
+ included,
503
+ excluded,
504
+ usedTokens,
505
+ budget,
506
+ utilizationPercent: (usedTokens / budget) * 100,
507
+ summary: {
508
+ totalItems: items.length,
509
+ includedCount: included.length,
510
+ excludedCount: excluded.length,
511
+ byCategory: summarizeByCategory(included)
512
+ }
513
+ };
514
+ }
515
+
516
+ /**
517
+ * Create a snippet from a context item.
518
+ * @param {Object} item - Context item
519
+ * @param {number} maxLines - Maximum lines
520
+ * @returns {Object} Snippet item
521
+ */
522
+ function createSnippet(item, maxLines) {
523
+ const lines = item.content.split('\n');
524
+ const snippetLines = lines.slice(0, maxLines);
525
+
526
+ if (lines.length > maxLines) {
527
+ snippetLines.push(`... (${lines.length - maxLines} more lines)`);
528
+ }
529
+
530
+ return {
531
+ ...item,
532
+ id: `${item.id}-snippet`,
533
+ content: snippetLines.join('\n'),
534
+ tokens: estimateTokens(snippetLines.join('\n')),
535
+ metadata: {
536
+ ...item.metadata,
537
+ isSnippet: true,
538
+ originalLines: lines.length,
539
+ snippetLines: maxLines
540
+ }
541
+ };
542
+ }
543
+
544
+ /**
545
+ * Summarize items by category.
546
+ * @param {Array} items - Items to summarize
547
+ * @returns {Object} Summary by category
548
+ */
549
+ function summarizeByCategory(items) {
550
+ const summary = {};
551
+
552
+ for (const category of Object.values(CONTEXT_CATEGORIES)) {
553
+ summary[category] = {
554
+ count: 0,
555
+ tokens: 0
556
+ };
557
+ }
558
+
559
+ for (const item of items) {
560
+ if (summary[item.category]) {
561
+ summary[item.category].count++;
562
+ summary[item.category].tokens += item.tokens;
563
+ }
564
+ }
565
+
566
+ return summary;
567
+ }
568
+
569
+ // ============================================================
570
+ // Analysis
571
+ // ============================================================
572
+
573
+ /**
574
+ * Analyze a file for context scoring.
575
+ * @param {string} filePath - Path to file
576
+ * @returns {Object} Analysis result
577
+ */
578
+ function analyzeFile(filePath) {
579
+ // Validate path is within project directory (prevent path traversal)
580
+ const resolvedPath = path.resolve(filePath);
581
+ if (!resolvedPath.startsWith(PROJECT_ROOT)) {
582
+ return { error: 'File must be within project directory' };
583
+ }
584
+
585
+ if (!fs.existsSync(resolvedPath)) {
586
+ return { error: 'File not found' };
587
+ }
588
+
589
+ let content;
590
+ try {
591
+ content = fs.readFileSync(resolvedPath, 'utf8');
592
+ } catch {
593
+ return { error: 'Failed to read file' };
594
+ }
595
+ const lines = content.split('\n');
596
+ const tokens = estimateTokens(content);
597
+
598
+ // Determine file type
599
+ const ext = path.extname(filePath);
600
+ let fileType = 'full_files';
601
+
602
+ if (/\.(d\.ts|types\.ts)$/.test(filePath)) {
603
+ fileType = 'required_types';
604
+ } else if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filePath)) {
605
+ fileType = 'test_files';
606
+ } else if (/\.(md|txt)$/.test(filePath)) {
607
+ fileType = 'documentation';
608
+ } else if (/\.(json|yaml|yml|toml)$/.test(filePath)) {
609
+ fileType = 'config_files';
610
+ } else if (/\.generated\.|\.min\./.test(filePath)) {
611
+ fileType = 'generated_files';
612
+ }
613
+
614
+ const baseScore = CONTEXT_PRIORITIES[fileType] || 0.5;
615
+
616
+ // Extract structure info
617
+ const exports = extractExports(content);
618
+ const imports = extractImports(content, filePath);
619
+
620
+ return {
621
+ path: filePath,
622
+ lines: lines.length,
623
+ tokens,
624
+ fileType,
625
+ baseScore,
626
+ category: categorizeScore(baseScore),
627
+ exports,
628
+ importCount: imports.length,
629
+ wouldFitIn: {
630
+ small: tokens <= 2000 ? 'full' : tokens <= 5000 ? 'snippet' : 'summary',
631
+ medium: tokens <= 10000 ? 'full' : tokens <= 20000 ? 'snippet' : 'summary',
632
+ large: tokens <= 50000 ? 'full' : 'snippet'
633
+ }
634
+ };
635
+ }
636
+
637
+ /**
638
+ * Extract exports from a file.
639
+ * @param {string} content - File content
640
+ * @returns {Array} Export information
641
+ */
642
+ function extractExports(content) {
643
+ const exports = [];
644
+
645
+ // Named exports
646
+ const namedPattern = /export\s+(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/g;
647
+ let match;
648
+ while ((match = namedPattern.exec(content)) !== null) {
649
+ exports.push({ name: match[1], type: 'named' });
650
+ }
651
+
652
+ // Default export
653
+ if (/export\s+default/.test(content)) {
654
+ exports.push({ name: 'default', type: 'default' });
655
+ }
656
+
657
+ // module.exports
658
+ const cjsPattern = /module\.exports\s*=\s*\{([^}]+)\}/;
659
+ const cjsMatch = content.match(cjsPattern);
660
+ if (cjsMatch) {
661
+ const names = cjsMatch[1].match(/\w+/g) || [];
662
+ for (const name of names) {
663
+ exports.push({ name, type: 'cjs' });
664
+ }
665
+ }
666
+
667
+ return exports;
668
+ }
669
+
670
+ // ============================================================
671
+ // CLI Output
672
+ // ============================================================
673
+
674
+ /**
675
+ * Print context scoring results.
676
+ * @param {Object} result - Fitting result
677
+ */
678
+ function printScoringResult(result) {
679
+ printHeader('CONTEXT SCORING RESULT');
680
+
681
+ printSection('Budget');
682
+ console.log(` ${color('dim', 'Total budget:')} ${result.budget} tokens`);
683
+ console.log(` ${color('dim', 'Used:')} ${result.usedTokens} tokens (${result.utilizationPercent.toFixed(1)}%)`);
684
+ console.log(` ${color('dim', 'Remaining:')} ${result.budget - result.usedTokens} tokens`);
685
+
686
+ printSection('Included Items');
687
+ const byCategory = result.summary.byCategory;
688
+ for (const [category, data] of Object.entries(byCategory)) {
689
+ if (data.count > 0) {
690
+ const icon = category === 'required' ? '🔴' :
691
+ category === 'high' ? '🟠' :
692
+ category === 'medium' ? '🟡' :
693
+ category === 'low' ? '🟢' : '⚪';
694
+ console.log(` ${icon} ${category}: ${data.count} items (${data.tokens} tokens)`);
695
+ }
696
+ }
697
+
698
+ console.log('');
699
+ for (const item of result.included.slice(0, 10)) {
700
+ const scoreBar = '█'.repeat(Math.round(item.finalScore * 10));
701
+ console.log(` ${color('dim', item.type.padEnd(20))} ${scoreBar} ${item.finalScore.toFixed(2)}`);
702
+ console.log(` ${color('dim', item.source)} (${item.tokens} tokens)`);
703
+ }
704
+
705
+ if (result.included.length > 10) {
706
+ console.log(color('dim', ` ... and ${result.included.length - 10} more items`));
707
+ }
708
+
709
+ if (result.excluded.length > 0) {
710
+ printSection('Excluded Items');
711
+ console.log(color('dim', ` ${result.excluded.length} items excluded`));
712
+ const reasons = {};
713
+ for (const item of result.excluded) {
714
+ reasons[item.reason] = (reasons[item.reason] || 0) + 1;
715
+ }
716
+ for (const [reason, count] of Object.entries(reasons)) {
717
+ console.log(` ${color('dim', reason)}: ${count}`);
718
+ }
719
+ }
720
+ }
721
+
722
+ /**
723
+ * Print file analysis.
724
+ * @param {Object} analysis - File analysis
725
+ */
726
+ function printFileAnalysis(analysis) {
727
+ if (analysis.error) {
728
+ error(analysis.error);
729
+ return;
730
+ }
731
+
732
+ printHeader('FILE ANALYSIS');
733
+
734
+ console.log(` ${color('dim', 'Path:')} ${analysis.path}`);
735
+ console.log(` ${color('dim', 'Lines:')} ${analysis.lines}`);
736
+ console.log(` ${color('dim', 'Tokens:')} ${analysis.tokens}`);
737
+ console.log(` ${color('dim', 'Type:')} ${analysis.fileType}`);
738
+ console.log(` ${color('dim', 'Base score:')} ${analysis.baseScore.toFixed(2)}`);
739
+ console.log(` ${color('dim', 'Category:')} ${analysis.category}`);
740
+
741
+ printSection('Exports');
742
+ if (analysis.exports.length === 0) {
743
+ console.log(color('dim', ' No exports found'));
744
+ } else {
745
+ for (const exp of analysis.exports.slice(0, 10)) {
746
+ console.log(` ${exp.type === 'default' ? '★' : '○'} ${exp.name}`);
747
+ }
748
+ if (analysis.exports.length > 10) {
749
+ console.log(color('dim', ` ... and ${analysis.exports.length - 10} more`));
750
+ }
751
+ }
752
+
753
+ printSection('Context Budget Fit');
754
+ console.log(` ${color('dim', 'Small context (10k):')} ${analysis.wouldFitIn.small}`);
755
+ console.log(` ${color('dim', 'Medium context (50k):')} ${analysis.wouldFitIn.medium}`);
756
+ console.log(` ${color('dim', 'Large context (200k):')} ${analysis.wouldFitIn.large}`);
757
+ }
758
+
759
+ // ============================================================
760
+ // Exports
761
+ // ============================================================
762
+
763
+ module.exports = {
764
+ // Core functions
765
+ createContextItem,
766
+ collectContext,
767
+ fitToBudget,
768
+ analyzeFile,
769
+
770
+ // Scoring
771
+ scoreRelevance,
772
+ extractTaskContext,
773
+ estimateTokens,
774
+
775
+ // Configuration
776
+ getContextConfig,
777
+ CONTEXT_PRIORITIES,
778
+ CONTEXT_CATEGORIES,
779
+ DEFAULT_CONTEXT_CONFIG
780
+ };
781
+
782
+ // ============================================================
783
+ // CLI Entry Point
784
+ // ============================================================
785
+
786
+ function main() {
787
+ const { positional, flags } = parseFlags(process.argv.slice(2));
788
+ const command = positional[0];
789
+
790
+ if (flags.help || !command) {
791
+ console.log(`
792
+ Usage: flow context <command> [options]
793
+
794
+ Commands:
795
+ score Score context items for a task
796
+ budget Fit context within a token budget
797
+ analyze Analyze a file for context inclusion
798
+
799
+ Options:
800
+ --task "<desc>" Task description for relevance scoring
801
+ --files <f1,f2> Files to include (comma-separated)
802
+ --tokens <n> Token budget (default: 100000)
803
+ --json Output as JSON
804
+ --help Show this help
805
+
806
+ Examples:
807
+ flow context score --task "Add user authentication" --files src/auth.ts
808
+ flow context budget --tokens 50000 --task "Fix login bug"
809
+ flow context analyze --file src/components/Button.tsx
810
+ `);
811
+ return;
812
+ }
813
+
814
+ switch (command) {
815
+ case 'score': {
816
+ const task = flags.task;
817
+ const files = flags.files ? flags.files.split(',') : [];
818
+
819
+ if (!task) {
820
+ error('Please provide a task with --task');
821
+ process.exit(1);
822
+ }
823
+
824
+ const items = collectContext({
825
+ description: task,
826
+ targetFiles: files
827
+ });
828
+
829
+ if (flags.json) {
830
+ outputJson(items);
831
+ } else {
832
+ printHeader('CONTEXT ITEMS SCORED');
833
+ for (const item of items.slice(0, 20)) {
834
+ console.log(` ${item.finalScore.toFixed(2)} ${item.type} - ${item.source}`);
835
+ }
836
+ }
837
+ break;
838
+ }
839
+
840
+ case 'budget': {
841
+ const task = flags.task || 'General task';
842
+ const files = flags.files ? flags.files.split(',') : [];
843
+ const budget = parseInt(flags.tokens) || 100000;
844
+
845
+ const items = collectContext({
846
+ description: task,
847
+ targetFiles: files
848
+ });
849
+
850
+ const result = fitToBudget(items, budget, { allowSnippets: true });
851
+
852
+ if (flags.json) {
853
+ outputJson(result);
854
+ } else {
855
+ printScoringResult(result);
856
+ }
857
+ break;
858
+ }
859
+
860
+ case 'analyze': {
861
+ const file = flags.file || positional[1];
862
+
863
+ if (!file) {
864
+ error('Please provide a file with --file or as argument');
865
+ process.exit(1);
866
+ }
867
+
868
+ const analysis = analyzeFile(file);
869
+
870
+ if (flags.json) {
871
+ outputJson(analysis);
872
+ } else {
873
+ printFileAnalysis(analysis);
874
+ }
875
+ break;
876
+ }
877
+
878
+ default:
879
+ error(`Unknown command: ${command}`);
880
+ process.exit(1);
881
+ }
882
+ }
883
+
884
+ if (require.main === module) {
885
+ main();
886
+ }