wogiflow 2.26.2 → 2.29.1

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 (169) hide show
  1. package/.claude/commands/wogi-bug.md +30 -0
  2. package/.claude/commands/wogi-debug-hypothesis.md +33 -0
  3. package/.claude/commands/wogi-morning.md +1 -2
  4. package/.claude/commands/wogi-review.md +31 -2
  5. package/.claude/commands/wogi-start.md +32 -0
  6. package/.claude/commands/wogi-statusline-setup.md +12 -0
  7. package/.claude/commands/wogi-story.md +3 -2
  8. package/.claude/docs/claude-code-compatibility.md +40 -0
  9. package/.claude/docs/phases/01-explore.md +2 -1
  10. package/.claude/docs/phases/03-implement.md +4 -0
  11. package/.claude/docs/phases/04-verify.md +45 -0
  12. package/.claude/rules/README.md +36 -0
  13. package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
  14. package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
  15. package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
  16. package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
  17. package/.claude/rules/alternative-short-name.md +12 -0
  18. package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
  19. package/.claude/rules/architecture/hook-three-layer.md +68 -0
  20. package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
  21. package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
  22. package/.claude/settings.json +1 -1
  23. package/.workflow/agents/logic-adversary.md +2 -1
  24. package/.workflow/agents/personas/README.md +48 -0
  25. package/.workflow/agents/personas/platform-rigor.md +38 -0
  26. package/.workflow/agents/personas/scale-skeptic.md +28 -0
  27. package/.workflow/agents/personas/security-hawk.md +34 -0
  28. package/.workflow/agents/personas/simplicity-champion.md +37 -0
  29. package/.workflow/agents/personas/user-advocate.md +36 -0
  30. package/.workflow/bridges/base-bridge.js +46 -23
  31. package/.workflow/templates/claude-md.hbs +44 -122
  32. package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
  33. package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
  34. package/.workflow/templates/partials/methodology-rules.hbs +85 -79
  35. package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
  36. package/lib/fuzzy-patch.js +251 -0
  37. package/lib/installer.js +8 -0
  38. package/lib/memory-proposal-store.js +458 -0
  39. package/lib/mode-schema.js +255 -0
  40. package/lib/skill-proposal-store.js +432 -0
  41. package/lib/skill-registry.js +1 -1
  42. package/lib/wogi-claude +149 -9
  43. package/lib/wogi-claude-expect.exp +113 -76
  44. package/lib/workspace-channel-server.js +19 -0
  45. package/lib/workspace-contracts.js +1 -1
  46. package/lib/workspace-dispatch-tracking.js +144 -0
  47. package/lib/workspace-gates.js +1 -1
  48. package/lib/workspace-ipc-sqlite.js +550 -0
  49. package/lib/workspace-messages.js +92 -0
  50. package/lib/workspace-routing.js +1 -1
  51. package/lib/workspace-task-injector.js +223 -0
  52. package/lib/workspace.js +23 -0
  53. package/lib/worktree-review.js +315 -0
  54. package/package.json +2 -2
  55. package/scripts/base-workflow-step.js +1 -1
  56. package/scripts/flow +28 -4
  57. package/scripts/flow-ac-scope-preservation.js +238 -0
  58. package/scripts/flow-auto-review-worker.js +75 -0
  59. package/scripts/flow-auto-review.js +102 -0
  60. package/scripts/flow-autonomous-detector.js +118 -0
  61. package/scripts/flow-autonomous-mode.js +153 -0
  62. package/scripts/flow-best-of-n.js +1 -1
  63. package/scripts/flow-bulk-loop.js +1 -1
  64. package/scripts/flow-checkpoint.js +2 -6
  65. package/scripts/flow-community-sync.js +1 -1
  66. package/scripts/flow-completion-summary.js +176 -0
  67. package/scripts/flow-completion-truth-gate.js +343 -4
  68. package/scripts/flow-config-defaults.js +52 -5
  69. package/scripts/flow-config-loader.js +3 -2
  70. package/scripts/flow-context-compact/expander.js +1 -1
  71. package/scripts/flow-context-compact/section-extractor.js +2 -2
  72. package/scripts/flow-context-gatherer.js +1 -1
  73. package/scripts/flow-context-generator.js +1 -1
  74. package/scripts/flow-context-scoring.js +1 -1
  75. package/scripts/flow-correct.js +1 -1
  76. package/scripts/flow-correction-detector.js +3 -2
  77. package/scripts/flow-decision-authority.js +66 -15
  78. package/scripts/flow-done.js +33 -1
  79. package/scripts/flow-epic-cascade.js +171 -0
  80. package/scripts/flow-epics.js +2 -7
  81. package/scripts/flow-eval-judge.js +1 -1
  82. package/scripts/flow-eval.js +1 -1
  83. package/scripts/flow-export-scanner.js +2 -6
  84. package/scripts/flow-failure-learning.js +1 -1
  85. package/scripts/flow-feature-dossier.js +787 -0
  86. package/scripts/flow-figma-extract.js +2 -2
  87. package/scripts/flow-figma-generate.js +1 -1
  88. package/scripts/flow-gate-confidence.js +1 -1
  89. package/scripts/flow-health.js +52 -1
  90. package/scripts/flow-hooks.js +1 -1
  91. package/scripts/flow-id.js +19 -3
  92. package/scripts/flow-instruction-richness.js +1 -1
  93. package/scripts/flow-knowledge-router.js +1 -1
  94. package/scripts/flow-knowledge-sync.js +1 -1
  95. package/scripts/flow-logic-adversary.js +76 -1
  96. package/scripts/flow-logic-rules.js +380 -0
  97. package/scripts/flow-long-input.js +5 -5
  98. package/scripts/flow-memory-sync.js +1 -1
  99. package/scripts/flow-memory.js +78 -7
  100. package/scripts/flow-migrate.js +1 -1
  101. package/scripts/flow-model-caller.js +1 -1
  102. package/scripts/flow-models.js +2 -2
  103. package/scripts/flow-morning.js +0 -17
  104. package/scripts/flow-multi-approach.js +1 -1
  105. package/scripts/flow-orchestrate-context.js +4 -4
  106. package/scripts/flow-orchestrate-templates.js +1 -1
  107. package/scripts/flow-orchestrate.js +8 -8
  108. package/scripts/flow-peer-review.js +1 -1
  109. package/scripts/flow-phase.js +9 -0
  110. package/scripts/flow-proactive-compact.js +1 -1
  111. package/scripts/flow-prompt-composer.js +3 -2
  112. package/scripts/flow-prompt-template.js +3 -2
  113. package/scripts/flow-providers.js +1 -1
  114. package/scripts/flow-question-queue.js +255 -0
  115. package/scripts/flow-repo-map.js +312 -0
  116. package/scripts/flow-review-passes/index.js +1 -1
  117. package/scripts/flow-review-passes/integration.js +1 -1
  118. package/scripts/flow-review-passes/structure.js +1 -1
  119. package/scripts/flow-revision-tracker.js +1 -1
  120. package/scripts/flow-section-resolver.js +1 -1
  121. package/scripts/flow-session-end.js +74 -5
  122. package/scripts/flow-session-state.js +103 -1
  123. package/scripts/flow-setup-hooks.js +1 -1
  124. package/scripts/flow-skeptical-evaluator.js +274 -0
  125. package/scripts/flow-skill-generator.js +3 -3
  126. package/scripts/flow-skill-learn.js +3 -6
  127. package/scripts/flow-skill-manage.js +248 -0
  128. package/scripts/flow-spec-verifier.js +1 -1
  129. package/scripts/flow-standards-checker.js +75 -0
  130. package/scripts/flow-standards-gate.js +1 -1
  131. package/scripts/flow-statusline-setup.js +8 -2
  132. package/scripts/flow-step-changelog.js +2 -2
  133. package/scripts/flow-step-coverage.js +1 -1
  134. package/scripts/flow-step-knowledge.js +1 -1
  135. package/scripts/flow-step-regression.js +1 -1
  136. package/scripts/flow-step-simplifier.js +1 -1
  137. package/scripts/flow-task-analyzer.js +1 -1
  138. package/scripts/flow-task-classifier.js +1 -1
  139. package/scripts/flow-task-enforcer.js +1 -1
  140. package/scripts/flow-template-extractor.js +1 -1
  141. package/scripts/flow-trap-zone.js +1 -1
  142. package/scripts/flow-utils.js +4 -0
  143. package/scripts/flow-worker-mcp-strip.js +122 -0
  144. package/scripts/flow-worker-question-classifier.js +51 -5
  145. package/scripts/flow-workspace-migrate-ipc.js +216 -0
  146. package/scripts/flow-workspace-summary.js +256 -0
  147. package/scripts/hooks/adapters/base-adapter.js +2 -2
  148. package/scripts/hooks/core/feature-dossier-gate.js +194 -0
  149. package/scripts/hooks/core/observation-capture.js +24 -0
  150. package/scripts/hooks/core/overdue-dispatches.js +20 -1
  151. package/scripts/hooks/core/phase-gate.js +15 -1
  152. package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
  153. package/scripts/hooks/core/post-compact.js +5 -2
  154. package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
  155. package/scripts/hooks/core/routing-gate.js +58 -0
  156. package/scripts/hooks/core/session-context.js +108 -0
  157. package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
  158. package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
  159. package/scripts/hooks/core/session-end.js +25 -0
  160. package/scripts/hooks/core/setup-handler.js +1 -1
  161. package/scripts/hooks/core/task-boundary-reset.js +110 -4
  162. package/scripts/hooks/core/worker-boundary-gate.js +71 -0
  163. package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
  164. package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
  165. package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
  166. package/scripts/hooks/entry/claude-code/session-start.js +74 -30
  167. package/scripts/hooks/entry/claude-code/stop.js +47 -1
  168. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
  169. package/.workflow/templates/partials/user-commands.hbs +0 -20
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow — Question Queue (Story C / wf-d712002e)
5
+ *
6
+ * Persistent queue for product/UX questions surfaced during autonomous mode.
7
+ * Questions blocking dependent tasks are recorded together with the skipped
8
+ * tasks so the completion summary can render them and the user can resolve
9
+ * them in one batch.
10
+ *
11
+ * Dependency classification is deliberately conservative — it is safer to
12
+ * over-flag (false positive: extra re-run) than to under-flag (false
13
+ * negative: dependent task ran on a stale assumption).
14
+ *
15
+ * File: .workflow/state/question-queue.json
16
+ *
17
+ * Programmatic:
18
+ * const q = require('./flow-question-queue');
19
+ * q.addQuestion({ text, classifiedBucket, taskContext, dependencies, runId });
20
+ * q.skipTask({ taskId, reason });
21
+ * q.loadQueue();
22
+ * q.clearQueue();
23
+ * q.classifyDependencies(questionText, pendingTaskIds);
24
+ */
25
+
26
+ const path = require('node:path');
27
+ const fs = require('node:fs');
28
+ const { PATHS } = require('./flow-paths');
29
+ const { readJson, writeJson } = require('./flow-io');
30
+
31
+ const QUEUE_PATH = path.join(PATHS.state, 'question-queue.json');
32
+
33
+ function emptyQueue() {
34
+ return { questions: [], skippedTasks: [] };
35
+ }
36
+
37
+ function loadQueue() {
38
+ try {
39
+ const data = readJson(QUEUE_PATH, null);
40
+ if (!data || typeof data !== 'object') return emptyQueue();
41
+ return {
42
+ questions: Array.isArray(data.questions) ? data.questions : [],
43
+ skippedTasks: Array.isArray(data.skippedTasks) ? data.skippedTasks : []
44
+ };
45
+ } catch (_err) {
46
+ return emptyQueue();
47
+ }
48
+ }
49
+
50
+ function saveQueue(data) {
51
+ writeJson(QUEUE_PATH, data);
52
+ return data;
53
+ }
54
+
55
+ function clearQueue() {
56
+ try {
57
+ if (fs.existsSync(QUEUE_PATH)) fs.unlinkSync(QUEUE_PATH);
58
+ } catch (_err) { /* ignore */ }
59
+ return emptyQueue();
60
+ }
61
+
62
+ function shortId() {
63
+ return Math.random().toString(36).slice(2, 10);
64
+ }
65
+
66
+ /**
67
+ * Append a question to the queue.
68
+ * @param {object} q
69
+ * @param {string} q.text - Question text
70
+ * @param {string} [q.classifiedBucket] - Original classifier bucket
71
+ * @param {string} [q.taskContext] - Task ID where the question arose
72
+ * @param {string[]} [q.dependencies] - Task IDs likely depending on the answer
73
+ * @param {string} [q.runId] - Autonomous run ID
74
+ */
75
+ function addQuestion(q) {
76
+ if (!q || !q.text) throw new Error('addQuestion: text is required');
77
+ const queue = loadQueue();
78
+ const entry = {
79
+ id: `q-${shortId()}`,
80
+ text: q.text,
81
+ classifiedBucket: q.classifiedBucket || null,
82
+ taskContext: q.taskContext || null,
83
+ dependencies: Array.isArray(q.dependencies) ? q.dependencies : [],
84
+ createdAt: new Date().toISOString(),
85
+ runId: q.runId || null,
86
+ answered: false
87
+ };
88
+ queue.questions.push(entry);
89
+ saveQueue(queue);
90
+ return entry;
91
+ }
92
+
93
+ /**
94
+ * Mark a task as skipped, recording the reason and (optionally) the question
95
+ * blocking it.
96
+ */
97
+ function skipTask({ taskId, reason, blockingQuestionId } = {}) {
98
+ if (!taskId) throw new Error('skipTask: taskId is required');
99
+ const queue = loadQueue();
100
+ const existing = queue.skippedTasks.find(s => s.taskId === taskId);
101
+ const record = {
102
+ taskId,
103
+ reason: reason || 'awaiting answer',
104
+ blockingQuestionId: blockingQuestionId || null,
105
+ skippedAt: new Date().toISOString()
106
+ };
107
+ if (existing) {
108
+ Object.assign(existing, record);
109
+ } else {
110
+ queue.skippedTasks.push(record);
111
+ }
112
+ saveQueue(queue);
113
+ return record;
114
+ }
115
+
116
+ /**
117
+ * Conservative dependency classifier — text-match only.
118
+ * AI classifier (Haiku) fallback is intentionally NOT inlined here; callers
119
+ * that have access to Haiku invoke `classifyDependenciesWithAi()` and merge
120
+ * results with `unionDependencies()`. This keeps the hot path classifier-free
121
+ * for tests and for environments without Anthropic credentials.
122
+ *
123
+ * Rules:
124
+ * 1. Exact task ID match (wf-XXXXXXXX) → flag dependency.
125
+ * 2. Title substring match (case-insensitive, ≥6 chars to avoid noise) → flag.
126
+ * 3. File-path match (anywhere in question text) → flag the task whose changed
127
+ * files include that path.
128
+ *
129
+ * @param {string} questionText
130
+ * @param {Array<{id:string,title?:string,files?:string[]}>} pendingTasks
131
+ * @returns {string[]} task IDs flagged as dependent
132
+ */
133
+ function classifyDependencies(questionText, pendingTasks = []) {
134
+ if (!questionText || !pendingTasks.length) return [];
135
+ const text = String(questionText);
136
+ const lower = text.toLowerCase();
137
+ const flagged = new Set();
138
+
139
+ const idRegex = /\bwf-[a-f0-9]{8}\b/gi;
140
+ const ids = text.match(idRegex) || [];
141
+ for (const id of ids) flagged.add(id.toLowerCase());
142
+
143
+ for (const task of pendingTasks) {
144
+ if (!task || !task.id) continue;
145
+ if (flagged.has(task.id.toLowerCase())) continue;
146
+ if (titleMatchesText(task.title, lower)) {
147
+ flagged.add(task.id);
148
+ continue;
149
+ }
150
+ if (Array.isArray(task.files)) {
151
+ for (const f of task.files) {
152
+ if (typeof f === 'string' && f.length >= 4 && lower.includes(f.toLowerCase())) {
153
+ flagged.add(task.id);
154
+ break;
155
+ }
156
+ }
157
+ }
158
+ }
159
+ return [...flagged];
160
+ }
161
+
162
+ function titleMatchesText(title, lowerText) {
163
+ if (!title || title.length < 6) return false;
164
+ const t = title.toLowerCase();
165
+ if (lowerText.includes(t)) return true;
166
+ const words = t.split(/\s+/).filter(w => w.length >= 4);
167
+ for (let i = 0; i < words.length - 1; i++) {
168
+ const bigram = `${words[i]} ${words[i + 1]}`;
169
+ if (bigram.length >= 6 && lowerText.includes(bigram)) return true;
170
+ }
171
+ return false;
172
+ }
173
+
174
+ /**
175
+ * Conservative union — combines text-match + AI classifier results, deduped.
176
+ */
177
+ function unionDependencies(...lists) {
178
+ const out = new Set();
179
+ for (const list of lists) {
180
+ if (!Array.isArray(list)) continue;
181
+ for (const id of list) out.add(id);
182
+ }
183
+ return [...out];
184
+ }
185
+
186
+ /**
187
+ * Fail-safe wrapper: if the classifier is unavailable or throws, mark ALL
188
+ * pending tasks as dependent (the safest over-flag per the spec).
189
+ */
190
+ function classifyDependenciesSafe(questionText, pendingTasks, aiClassifier = null) {
191
+ const textMatched = classifyDependencies(questionText, pendingTasks);
192
+ if (typeof aiClassifier !== 'function') {
193
+ if (textMatched.length === 0) {
194
+ return pendingTasks.map(t => t && t.id).filter(Boolean);
195
+ }
196
+ return textMatched;
197
+ }
198
+ try {
199
+ const aiResult = aiClassifier(questionText, pendingTasks);
200
+ if (!Array.isArray(aiResult)) {
201
+ return pendingTasks.map(t => t && t.id).filter(Boolean);
202
+ }
203
+ return unionDependencies(textMatched, aiResult);
204
+ } catch (_err) {
205
+ return pendingTasks.map(t => t && t.id).filter(Boolean);
206
+ }
207
+ }
208
+
209
+ function listOpenQuestions() {
210
+ return loadQueue().questions.filter(q => !q.answered);
211
+ }
212
+
213
+ function listSkippedTasks() {
214
+ return loadQueue().skippedTasks;
215
+ }
216
+
217
+ module.exports = {
218
+ QUEUE_PATH,
219
+ emptyQueue,
220
+ loadQueue,
221
+ saveQueue,
222
+ clearQueue,
223
+ addQuestion,
224
+ skipTask,
225
+ classifyDependencies,
226
+ classifyDependenciesSafe,
227
+ unionDependencies,
228
+ listOpenQuestions,
229
+ listSkippedTasks
230
+ };
231
+
232
+ if (require.main === module) {
233
+ const [,, cmd, ...args] = process.argv;
234
+ switch (cmd) {
235
+ case 'list': {
236
+ const q = loadQueue();
237
+ console.log(JSON.stringify(q, null, 2));
238
+ break;
239
+ }
240
+ case 'clear': {
241
+ clearQueue();
242
+ console.log('cleared');
243
+ break;
244
+ }
245
+ case 'add': {
246
+ const text = args.join(' ');
247
+ if (!text) { console.error('Usage: flow-question-queue add <text>'); process.exit(1); }
248
+ const entry = addQuestion({ text });
249
+ console.log(JSON.stringify(entry, null, 2));
250
+ break;
251
+ }
252
+ default:
253
+ console.log('Usage: flow-question-queue <list|add|clear>');
254
+ }
255
+ }
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow — Aider-style Repo Map (wf-f3707d2f / C1).
5
+ *
6
+ * Generates a compact, task-aware repo map that fits in a bounded token
7
+ * budget. Intended for injection at Step 1 Load Context (and refresh at each
8
+ * turn during exploring + coding phases) so the AI sees:
9
+ * - TOUCHED — files the current task modifies (summary + top-level symbols)
10
+ * - ADJACENT — files that import or are imported by the touched set
11
+ * - SHAPE — compressed tree of the rest of the project (names only)
12
+ *
13
+ * This complements the existing registry maps (app-map, function-map, api-map)
14
+ * which are manually curated; the repo map is cheap, disposable, and always-fresh.
15
+ *
16
+ * Story: wf-f3707d2f (C1)
17
+ * Epic: wf-34290000
18
+ */
19
+
20
+ const fs = require('node:fs');
21
+ const path = require('node:path');
22
+ const { execFileSync } = require('node:child_process');
23
+
24
+ const { PATHS } = require('./flow-paths');
25
+ const { getConfig } = require('./flow-config-loader');
26
+
27
+ const DEFAULT_BUDGET_BYTES = 16 * 1024; // ~4k tokens
28
+ const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.workflow', '.worktrees', 'out']);
29
+ const CODE_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs']);
30
+ const DOC_EXTS = new Set(['.md']);
31
+ const STATE_EXTS = new Set(['.json', '.yaml', '.yml', '.toml']);
32
+
33
+ function _getRepoMapConfig() {
34
+ const cfg = getConfig();
35
+ return cfg.repoMap || {};
36
+ }
37
+
38
+ /**
39
+ * Resolve changed-files list for a task.
40
+ * Priority: explicit opts.changedFiles → durable-session checkpoint → `git diff --name-only`.
41
+ *
42
+ * @param {object} opts
43
+ * @returns {string[]}
44
+ */
45
+ function resolveChangedFiles(opts = {}) {
46
+ if (Array.isArray(opts.changedFiles)) return opts.changedFiles;
47
+
48
+ // Try task-checkpoint.json
49
+ const checkpointPath = path.join(PATHS.state, 'task-checkpoint.json');
50
+ if (fs.existsSync(checkpointPath)) {
51
+ try {
52
+ const cp = JSON.parse(fs.readFileSync(checkpointPath, 'utf8'));
53
+ if (Array.isArray(cp.changedFiles) && cp.changedFiles.length > 0) return cp.changedFiles;
54
+ } catch { /* fall through */ }
55
+ }
56
+
57
+ // Git diff
58
+ try {
59
+ const out = execFileSync('git', ['diff', '--name-only', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
60
+ const files = out.split('\n').filter(Boolean);
61
+ if (files.length > 0) return files;
62
+ } catch { /* no git */ }
63
+
64
+ try {
65
+ const out = execFileSync('git', ['status', '--porcelain'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
66
+ return out.split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
67
+ } catch { return []; }
68
+ }
69
+
70
+ /**
71
+ * Extract top-level symbol signatures from a file. Lightweight — regex only,
72
+ * no AST parser. Captures: function decls, class decls, const declarations,
73
+ * module.exports / export default, named exports.
74
+ *
75
+ * @param {string} filePath - absolute path
76
+ * @returns {{ symbols: string[], firstLine: string, loc: number }}
77
+ */
78
+ function extractSymbols(filePath) {
79
+ const out = { symbols: [], firstLine: '', loc: 0 };
80
+ let content;
81
+ try {
82
+ content = fs.readFileSync(filePath, 'utf8');
83
+ } catch { return out; }
84
+
85
+ const lines = content.split('\n');
86
+ out.loc = lines.length;
87
+ // First docblock / comment line
88
+ for (const line of lines.slice(0, 5)) {
89
+ const t = line.trim().replace(/^[*/\s]+|[*/\s]+$/g, '');
90
+ if (t.length > 4 && !/^@|^#!\//.test(t)) { out.firstLine = t.slice(0, 100); break; }
91
+ }
92
+
93
+ const patterns = [
94
+ /^(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm,
95
+ /^(?:export\s+(?:default\s+)?)?class\s+(\w+)/gm,
96
+ /^(?:export\s+)?const\s+([A-Z][A-Z0-9_]+)\s*=/gm, // SCREAMING_SNAKE constants
97
+ /^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>)/gm,
98
+ ];
99
+ for (const re of patterns) {
100
+ for (const m of content.matchAll(re)) {
101
+ const sig = m[2] !== undefined ? `${m[1]}(${m[2].length > 40 ? '...' : m[2]})` : m[1];
102
+ if (!out.symbols.includes(sig)) out.symbols.push(sig);
103
+ if (out.symbols.length >= 12) break;
104
+ }
105
+ }
106
+
107
+ // module.exports = {...} named-key extraction
108
+ const mexRe = /module\.exports\s*=\s*\{([^}]+)\}/;
109
+ const mex = content.match(mexRe);
110
+ if (mex) {
111
+ const keys = mex[1].split(',').map((s) => s.trim().split(/[:=\s]/)[0]).filter(Boolean);
112
+ for (const k of keys.slice(0, 8)) if (/^\w+$/.test(k) && !out.symbols.some((s) => s.startsWith(k))) out.symbols.push(k);
113
+ }
114
+
115
+ return out;
116
+ }
117
+
118
+ /**
119
+ * Find files that import or are imported by the given seed files.
120
+ * Strict depth=1 (direct neighbors only).
121
+ *
122
+ * @param {string[]} seedFiles - paths relative to repo root
123
+ * @param {string[]} allCodeFiles - paths to consider
124
+ * @returns {string[]} paths of adjacent files
125
+ */
126
+ function findAdjacent(seedFiles, allCodeFiles) {
127
+ const seedSet = new Set(seedFiles);
128
+ const adjacent = new Set();
129
+ const seedBasenames = seedFiles.map((f) => path.basename(f, path.extname(f)));
130
+
131
+ for (const file of allCodeFiles) {
132
+ if (seedSet.has(file)) continue;
133
+ let content;
134
+ try { content = fs.readFileSync(file, 'utf8'); } catch { continue; }
135
+
136
+ // File imports something FROM the seed set
137
+ for (const base of seedBasenames) {
138
+ const re = new RegExp(`(?:require|import|from)\\s*\\(?[\`'"][^\`'"]*?${base}[^\`'"]*?[\`'"]\\)?`);
139
+ if (re.test(content)) { adjacent.add(file); break; }
140
+ }
141
+ }
142
+
143
+ // Reverse: what do seed files import?
144
+ for (const seed of seedFiles) {
145
+ let content;
146
+ try { content = fs.readFileSync(seed, 'utf8'); } catch { continue; }
147
+ const importRe = /(?:require|from)\s*\(?[`'"]([^`'"]+)[`'"]\)?/g;
148
+ for (const m of content.matchAll(importRe)) {
149
+ const resolved = _resolveImport(seed, m[1], allCodeFiles);
150
+ if (resolved && !seedSet.has(resolved)) adjacent.add(resolved);
151
+ }
152
+ }
153
+
154
+ return [...adjacent];
155
+ }
156
+
157
+ function _resolveImport(fromFile, spec, allFiles) {
158
+ if (!spec.startsWith('.')) return null; // external package
159
+ const dir = path.dirname(fromFile);
160
+ const base = path.resolve(dir, spec);
161
+ for (const ext of ['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '/index.js', '/index.ts']) {
162
+ const candidate = base + ext;
163
+ const rel = path.relative(process.cwd(), candidate);
164
+ if (allFiles.includes(rel)) return rel;
165
+ }
166
+ return null;
167
+ }
168
+
169
+ /**
170
+ * Walk the repo collecting code file paths (relative to cwd).
171
+ * @returns {string[]}
172
+ */
173
+ function collectCodeFiles() {
174
+ const root = process.cwd();
175
+ const out = [];
176
+ function walk(dir) {
177
+ let entries;
178
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
179
+ for (const e of entries) {
180
+ if (e.name.startsWith('.') && e.name !== '.claude' && e.name !== '.workflow') continue;
181
+ if (IGNORED_DIRS.has(e.name)) continue;
182
+ const p = path.join(dir, e.name);
183
+ if (e.isDirectory()) walk(p);
184
+ else if (e.isFile() && CODE_EXTS.has(path.extname(e.name))) out.push(path.relative(root, p));
185
+ }
186
+ }
187
+ walk(root);
188
+ return out;
189
+ }
190
+
191
+ /**
192
+ * Generate a compact shape-of-repo summary (file count per top-level dir).
193
+ */
194
+ function generateShape(allCodeFiles) {
195
+ const buckets = {};
196
+ for (const f of allCodeFiles) {
197
+ const top = f.split('/')[0];
198
+ buckets[top] = (buckets[top] || 0) + 1;
199
+ }
200
+ return Object.entries(buckets)
201
+ .sort((a, b) => b[1] - a[1])
202
+ .slice(0, 12)
203
+ .map(([k, v]) => `${k}/ (${v})`)
204
+ .join(', ');
205
+ }
206
+
207
+ /**
208
+ * Generate the repo map markdown.
209
+ *
210
+ * @param {object} [opts]
211
+ * @param {string} [opts.taskId]
212
+ * @param {string[]} [opts.changedFiles]
213
+ * @param {number} [opts.budgetBytes] - max output size
214
+ * @param {boolean} [opts.includeShape=true]
215
+ * @returns {{ markdown: string, stats: object }}
216
+ */
217
+ function generateRepoMap(opts = {}) {
218
+ const cfg = _getRepoMapConfig();
219
+ if (cfg.enabled === false) {
220
+ return { markdown: '', stats: { skipped: true, reason: 'config-disabled' } };
221
+ }
222
+
223
+ const budget = opts.budgetBytes ?? cfg.budgetBytes ?? DEFAULT_BUDGET_BYTES;
224
+ const includeShape = opts.includeShape !== false;
225
+ const changed = resolveChangedFiles(opts).filter((f) => CODE_EXTS.has(path.extname(f)) || DOC_EXTS.has(path.extname(f)) || STATE_EXTS.has(path.extname(f)));
226
+
227
+ const allCode = collectCodeFiles();
228
+ const touched = changed.filter((f) => fs.existsSync(f));
229
+ const adjacent = touched.length > 0 ? findAdjacent(touched, allCode).slice(0, 20) : [];
230
+
231
+ const lines = [];
232
+ lines.push(`# Repo Map${opts.taskId ? ' — ' + opts.taskId : ''}`);
233
+ lines.push('');
234
+ lines.push(`Generated: ${new Date().toISOString()} | files-scanned: ${allCode.length} | touched: ${touched.length} | adjacent: ${adjacent.length}`);
235
+ lines.push('');
236
+
237
+ if (touched.length > 0) {
238
+ lines.push('## TOUCHED');
239
+ for (const f of touched.slice(0, 20)) {
240
+ const abs = path.resolve(f);
241
+ const info = extractSymbols(abs);
242
+ lines.push(`### ${f}`);
243
+ if (info.firstLine) lines.push(`_${info.firstLine}_`);
244
+ lines.push(`- LOC: ${info.loc}`);
245
+ if (info.symbols.length > 0) lines.push(`- Symbols: \`${info.symbols.slice(0, 10).join('`, `')}\``);
246
+ lines.push('');
247
+ }
248
+ } else {
249
+ lines.push('## TOUCHED\n_(no changed code files detected)_\n');
250
+ }
251
+
252
+ if (adjacent.length > 0) {
253
+ lines.push('## ADJACENT (depth=1 imports)');
254
+ for (const f of adjacent) {
255
+ const info = extractSymbols(path.resolve(f));
256
+ const sigs = info.symbols.slice(0, 5).join(', ');
257
+ lines.push(`- ${f}${sigs ? ` — \`${sigs}\`` : ''}`);
258
+ }
259
+ lines.push('');
260
+ }
261
+
262
+ if (includeShape) {
263
+ lines.push('## SHAPE');
264
+ lines.push(generateShape(allCode));
265
+ lines.push('');
266
+ }
267
+
268
+ let markdown = lines.join('\n');
269
+ const wasTruncated = markdown.length > budget;
270
+ if (wasTruncated) {
271
+ markdown = markdown.slice(0, budget - 40) + '\n\n_(repo map truncated at budget)_\n';
272
+ }
273
+
274
+ return {
275
+ markdown,
276
+ stats: {
277
+ touched: touched.length,
278
+ adjacent: adjacent.length,
279
+ filesScanned: allCode.length,
280
+ bytes: markdown.length,
281
+ budget,
282
+ truncated: wasTruncated,
283
+ },
284
+ };
285
+ }
286
+
287
+ module.exports = {
288
+ generateRepoMap,
289
+ resolveChangedFiles,
290
+ extractSymbols,
291
+ findAdjacent,
292
+ collectCodeFiles,
293
+ DEFAULT_BUDGET_BYTES,
294
+ };
295
+
296
+ // CLI
297
+ if (require.main === module) {
298
+ const args = process.argv.slice(2);
299
+ const cmd = args[0];
300
+ if (cmd === 'generate' || !cmd) {
301
+ const taskId = args.find((a) => a.startsWith('--task='))?.split('=')[1];
302
+ const budgetArg = args.find((a) => a.startsWith('--budget='))?.split('=')[1];
303
+ const result = generateRepoMap({ taskId, budgetBytes: budgetArg ? parseInt(budgetArg, 10) : undefined });
304
+ process.stdout.write(result.markdown);
305
+ if (args.includes('--stats')) {
306
+ process.stderr.write('\n\nStats: ' + JSON.stringify(result.stats) + '\n');
307
+ }
308
+ } else {
309
+ console.error('usage: flow-repo-map generate [--task=<id>] [--budget=<bytes>] [--stats]');
310
+ process.exit(2);
311
+ }
312
+ }
@@ -183,7 +183,7 @@ async function runMultiPassReview(context, options = {}) {
183
183
  passes = ['structure', 'logic', 'security', 'integration'],
184
184
  earlyExitOnCritical = reviewConfig.earlyExitOnCritical !== false,
185
185
  passForward = reviewConfig.passForward !== false,
186
- parallel = false
186
+ _parallel = false
187
187
  } = options;
188
188
 
189
189
  const results = {
@@ -502,7 +502,7 @@ function analyzeDependencyGraph(files) {
502
502
  * @returns {Promise<Object>} Pass results
503
503
  */
504
504
  async function run(context) {
505
- const { files = [], previousResults = {} } = context;
505
+ const { files = [], _previousResults = {} } = context;
506
506
 
507
507
  const issues = [];
508
508
  const suggestions = [];
@@ -287,7 +287,7 @@ function checkDirectoryStructure(files) {
287
287
  * @returns {Promise<Object>} Pass results
288
288
  */
289
289
  async function run(context) {
290
- const { files = [], previousResults = {} } = context;
290
+ const { files = [], _previousResults = {} } = context;
291
291
  const config = getConfig();
292
292
  const namingConvention = config.componentReuse?.namingConvention || 'kebab-case';
293
293
 
@@ -19,7 +19,7 @@ const {
19
19
  PATHS,
20
20
  readJson: _readJson,
21
21
  fileExists: _fileExists,
22
- getTodayDate
22
+ _getTodayDate
23
23
  } = require('./flow-utils');
24
24
  const { recordRevision, loadStats } = require('./flow-stats-collector');
25
25
 
@@ -29,7 +29,7 @@ const {
29
29
  fileExists: _fileExists,
30
30
  readFile: _readFile,
31
31
  info,
32
- warn
32
+ _warn
33
33
  } = require('./flow-utils');
34
34
 
35
35
  const {