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.
- package/.claude/commands/wogi-bug.md +30 -0
- package/.claude/commands/wogi-debug-hypothesis.md +33 -0
- package/.claude/commands/wogi-morning.md +1 -2
- package/.claude/commands/wogi-review.md +31 -2
- package/.claude/commands/wogi-start.md +32 -0
- package/.claude/commands/wogi-statusline-setup.md +12 -0
- package/.claude/commands/wogi-story.md +3 -2
- package/.claude/docs/claude-code-compatibility.md +40 -0
- package/.claude/docs/phases/01-explore.md +2 -1
- package/.claude/docs/phases/03-implement.md +4 -0
- package/.claude/docs/phases/04-verify.md +45 -0
- package/.claude/rules/README.md +36 -0
- package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
- package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
- package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
- package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
- package/.claude/rules/alternative-short-name.md +12 -0
- package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
- package/.claude/rules/architecture/hook-three-layer.md +68 -0
- package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
- package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
- package/.claude/settings.json +1 -1
- package/.workflow/agents/logic-adversary.md +2 -1
- package/.workflow/agents/personas/README.md +48 -0
- package/.workflow/agents/personas/platform-rigor.md +38 -0
- package/.workflow/agents/personas/scale-skeptic.md +28 -0
- package/.workflow/agents/personas/security-hawk.md +34 -0
- package/.workflow/agents/personas/simplicity-champion.md +37 -0
- package/.workflow/agents/personas/user-advocate.md +36 -0
- package/.workflow/bridges/base-bridge.js +46 -23
- package/.workflow/templates/claude-md.hbs +44 -122
- package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
- package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
- package/.workflow/templates/partials/methodology-rules.hbs +85 -79
- package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
- package/lib/fuzzy-patch.js +251 -0
- package/lib/installer.js +8 -0
- package/lib/memory-proposal-store.js +458 -0
- package/lib/mode-schema.js +255 -0
- package/lib/skill-proposal-store.js +432 -0
- package/lib/skill-registry.js +1 -1
- package/lib/wogi-claude +149 -9
- package/lib/wogi-claude-expect.exp +113 -76
- package/lib/workspace-channel-server.js +19 -0
- package/lib/workspace-contracts.js +1 -1
- package/lib/workspace-dispatch-tracking.js +144 -0
- package/lib/workspace-gates.js +1 -1
- package/lib/workspace-ipc-sqlite.js +550 -0
- package/lib/workspace-messages.js +92 -0
- package/lib/workspace-routing.js +1 -1
- package/lib/workspace-task-injector.js +223 -0
- package/lib/workspace.js +23 -0
- package/lib/worktree-review.js +315 -0
- package/package.json +2 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +28 -4
- package/scripts/flow-ac-scope-preservation.js +238 -0
- package/scripts/flow-auto-review-worker.js +75 -0
- package/scripts/flow-auto-review.js +102 -0
- package/scripts/flow-autonomous-detector.js +118 -0
- package/scripts/flow-autonomous-mode.js +153 -0
- package/scripts/flow-best-of-n.js +1 -1
- package/scripts/flow-bulk-loop.js +1 -1
- package/scripts/flow-checkpoint.js +2 -6
- package/scripts/flow-community-sync.js +1 -1
- package/scripts/flow-completion-summary.js +176 -0
- package/scripts/flow-completion-truth-gate.js +343 -4
- package/scripts/flow-config-defaults.js +52 -5
- package/scripts/flow-config-loader.js +3 -2
- package/scripts/flow-context-compact/expander.js +1 -1
- package/scripts/flow-context-compact/section-extractor.js +2 -2
- package/scripts/flow-context-gatherer.js +1 -1
- package/scripts/flow-context-generator.js +1 -1
- package/scripts/flow-context-scoring.js +1 -1
- package/scripts/flow-correct.js +1 -1
- package/scripts/flow-correction-detector.js +3 -2
- package/scripts/flow-decision-authority.js +66 -15
- package/scripts/flow-done.js +33 -1
- package/scripts/flow-epic-cascade.js +171 -0
- package/scripts/flow-epics.js +2 -7
- package/scripts/flow-eval-judge.js +1 -1
- package/scripts/flow-eval.js +1 -1
- package/scripts/flow-export-scanner.js +2 -6
- package/scripts/flow-failure-learning.js +1 -1
- package/scripts/flow-feature-dossier.js +787 -0
- package/scripts/flow-figma-extract.js +2 -2
- package/scripts/flow-figma-generate.js +1 -1
- package/scripts/flow-gate-confidence.js +1 -1
- package/scripts/flow-health.js +52 -1
- package/scripts/flow-hooks.js +1 -1
- package/scripts/flow-id.js +19 -3
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-knowledge-router.js +1 -1
- package/scripts/flow-knowledge-sync.js +1 -1
- package/scripts/flow-logic-adversary.js +76 -1
- package/scripts/flow-logic-rules.js +380 -0
- package/scripts/flow-long-input.js +5 -5
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +78 -7
- package/scripts/flow-migrate.js +1 -1
- package/scripts/flow-model-caller.js +1 -1
- package/scripts/flow-models.js +2 -2
- package/scripts/flow-morning.js +0 -17
- package/scripts/flow-multi-approach.js +1 -1
- package/scripts/flow-orchestrate-context.js +4 -4
- package/scripts/flow-orchestrate-templates.js +1 -1
- package/scripts/flow-orchestrate.js +8 -8
- package/scripts/flow-peer-review.js +1 -1
- package/scripts/flow-phase.js +9 -0
- package/scripts/flow-proactive-compact.js +1 -1
- package/scripts/flow-prompt-composer.js +3 -2
- package/scripts/flow-prompt-template.js +3 -2
- package/scripts/flow-providers.js +1 -1
- package/scripts/flow-question-queue.js +255 -0
- package/scripts/flow-repo-map.js +312 -0
- package/scripts/flow-review-passes/index.js +1 -1
- package/scripts/flow-review-passes/integration.js +1 -1
- package/scripts/flow-review-passes/structure.js +1 -1
- package/scripts/flow-revision-tracker.js +1 -1
- package/scripts/flow-section-resolver.js +1 -1
- package/scripts/flow-session-end.js +74 -5
- package/scripts/flow-session-state.js +103 -1
- package/scripts/flow-setup-hooks.js +1 -1
- package/scripts/flow-skeptical-evaluator.js +274 -0
- package/scripts/flow-skill-generator.js +3 -3
- package/scripts/flow-skill-learn.js +3 -6
- package/scripts/flow-skill-manage.js +248 -0
- package/scripts/flow-spec-verifier.js +1 -1
- package/scripts/flow-standards-checker.js +75 -0
- package/scripts/flow-standards-gate.js +1 -1
- package/scripts/flow-statusline-setup.js +8 -2
- package/scripts/flow-step-changelog.js +2 -2
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-knowledge.js +1 -1
- package/scripts/flow-step-regression.js +1 -1
- package/scripts/flow-step-simplifier.js +1 -1
- package/scripts/flow-task-analyzer.js +1 -1
- package/scripts/flow-task-classifier.js +1 -1
- package/scripts/flow-task-enforcer.js +1 -1
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-trap-zone.js +1 -1
- package/scripts/flow-utils.js +4 -0
- package/scripts/flow-worker-mcp-strip.js +122 -0
- package/scripts/flow-worker-question-classifier.js +51 -5
- package/scripts/flow-workspace-migrate-ipc.js +216 -0
- package/scripts/flow-workspace-summary.js +256 -0
- package/scripts/hooks/adapters/base-adapter.js +2 -2
- package/scripts/hooks/core/feature-dossier-gate.js +194 -0
- package/scripts/hooks/core/observation-capture.js +24 -0
- package/scripts/hooks/core/overdue-dispatches.js +20 -1
- package/scripts/hooks/core/phase-gate.js +15 -1
- package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
- package/scripts/hooks/core/post-compact.js +5 -2
- package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
- package/scripts/hooks/core/routing-gate.js +58 -0
- package/scripts/hooks/core/session-context.js +108 -0
- package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
- package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
- package/scripts/hooks/core/session-end.js +25 -0
- package/scripts/hooks/core/setup-handler.js +1 -1
- package/scripts/hooks/core/task-boundary-reset.js +110 -4
- package/scripts/hooks/core/worker-boundary-gate.js +71 -0
- package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
- package/scripts/hooks/entry/claude-code/session-start.js +74 -30
- package/scripts/hooks/entry/claude-code/stop.js +47 -1
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
- 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
|
-
|
|
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 = [],
|
|
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 = [],
|
|
290
|
+
const { files = [], _previousResults = {} } = context;
|
|
291
291
|
const config = getConfig();
|
|
292
292
|
const namingConvention = config.componentReuse?.namingConvention || 'kebab-case';
|
|
293
293
|
|