wogiflow 2.17.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/wogi-audit.md +212 -17
- package/.claude/commands/wogi-research.md +37 -0
- package/.claude/commands/wogi-review.md +200 -22
- package/.claude/commands/wogi-start.md +45 -0
- package/.claude/docs/claude-code-compatibility.md +46 -1
- package/.claude/docs/intent-grounded-review.md +209 -0
- package/.claude/settings.json +34 -1
- package/.workflow/agents/logic-adversary.md +8 -0
- package/.workflow/templates/claude-md.hbs +18 -0
- package/lib/installer.js +22 -0
- package/lib/utils.js +29 -3
- package/lib/workspace-changelog.js +2 -1
- package/lib/workspace-channel-server.js +4 -6
- package/lib/workspace-contracts.js +5 -4
- package/lib/workspace-events.js +8 -7
- package/lib/workspace-gates.js +4 -3
- package/lib/workspace-integration-tests.js +2 -1
- package/lib/workspace-intelligence.js +3 -2
- package/lib/workspace-locks.js +2 -1
- package/lib/workspace-messages.js +7 -6
- package/lib/workspace-routing.js +14 -26
- package/lib/workspace-session.js +7 -6
- package/lib/workspace-sync.js +9 -8
- package/package.json +4 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +19 -0
- package/scripts/flow-adaptive-learning.js +1 -1
- package/scripts/flow-aggregate.js +2 -1
- package/scripts/flow-architect-pass.js +3 -3
- package/scripts/flow-archive-runs.js +372 -0
- package/scripts/flow-ask.js +1 -1
- package/scripts/flow-ast-grep.js +216 -0
- package/scripts/flow-audit-gates.js +1 -1
- package/scripts/flow-auto-learn.js +8 -11
- package/scripts/flow-bug.js +2 -2
- package/scripts/flow-capture-gate.js +644 -0
- package/scripts/flow-capture.js +4 -3
- package/scripts/flow-cli-flags.js +95 -0
- package/scripts/flow-community-sync.js +2 -1
- package/scripts/flow-community.js +6 -6
- package/scripts/flow-conclusion-classifier.js +310 -0
- package/scripts/flow-config-defaults.js +3 -3
- package/scripts/flow-constants.js +8 -11
- package/scripts/flow-context-scoring.js +1 -0
- package/scripts/flow-correction-detector.js +344 -3
- package/scripts/flow-damage-control.js +1 -1
- package/scripts/flow-decisions-merge.js +1 -0
- package/scripts/flow-done-gates.js +20 -0
- package/scripts/flow-done-report.js +2 -2
- package/scripts/flow-done.js +4 -4
- package/scripts/flow-epics.js +5 -11
- package/scripts/flow-health.js +145 -1
- package/scripts/flow-id.js +92 -0
- package/scripts/flow-io.js +15 -5
- package/scripts/flow-knowledge-router.js +2 -1
- package/scripts/flow-links.js +1 -1
- package/scripts/flow-log-manager.js +2 -1
- package/scripts/flow-logic-adversary.js +4 -4
- package/scripts/flow-long-input-cli.js +6 -0
- package/scripts/flow-long-input-stories.js +1 -1
- package/scripts/flow-loop-retry-learning.js +1 -1
- package/scripts/flow-mcp-capabilities.js +2 -3
- package/scripts/flow-mcp-docs.js +2 -1
- package/scripts/flow-memory-blocks.js +2 -1
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +767 -0
- package/scripts/flow-migrate-igr.js +1 -1
- package/scripts/flow-migrate.js +2 -1
- package/scripts/flow-model-adapter.js +1 -1
- package/scripts/flow-model-config.js +5 -1
- package/scripts/flow-model-profile.js +2 -1
- package/scripts/flow-orchestrate.js +3 -3
- package/scripts/flow-output.js +29 -0
- package/scripts/flow-parallel.js +10 -9
- package/scripts/flow-pattern-enforcer.js +2 -1
- package/scripts/flow-permissions-audit.js +124 -0
- package/scripts/flow-plugin-registry.js +2 -2
- package/scripts/flow-progress.js +5 -1
- package/scripts/flow-project-analyzer.js +1 -1
- package/scripts/flow-promote.js +510 -0
- package/scripts/flow-registries.js +86 -0
- package/scripts/flow-request-log.js +133 -0
- package/scripts/flow-research-protocol.js +0 -1
- package/scripts/flow-revision-tracker.js +2 -1
- package/scripts/flow-roadmap.js +2 -1
- package/scripts/flow-rules-sync.js +3 -7
- package/scripts/flow-session-end.js +3 -1
- package/scripts/flow-session-learning.js +6 -13
- package/scripts/flow-session-state.js +2 -2
- package/scripts/flow-setup-hooks.js +2 -1
- package/scripts/flow-skill-create.js +1 -1
- package/scripts/flow-skill-freshness.js +6 -7
- package/scripts/flow-skill-learn.js +1 -1
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-security.js +1 -1
- package/scripts/flow-story.js +58 -10
- package/scripts/flow-sys.js +204 -0
- package/scripts/flow-task-hierarchy.js +88 -0
- package/scripts/flow-tech-debt.js +2 -1
- package/scripts/flow-test-api.js +1 -1
- package/scripts/flow-utils.js +60 -890
- package/scripts/hooks/core/bugfix-scope-gate.js +5 -4
- package/scripts/hooks/core/deploy-gate.js +1 -1
- package/scripts/hooks/core/pre-tool-helpers.js +72 -0
- package/scripts/hooks/core/pre-tool-orchestrator.js +442 -0
- package/scripts/hooks/core/routing-gate.js +8 -0
- package/scripts/hooks/core/session-end.js +28 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +48 -492
- package/scripts/hooks/entry/shared/hook-runner.js +1 -1
- package/scripts/registries/schema-registry.js +1 -1
- package/scripts/registries/service-registry.js +1 -1
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Archive Runs (stale-cleanup)
|
|
5
|
+
*
|
|
6
|
+
* Manual-only archival for adversary-runs/*.json + gate-telemetry.jsonl.
|
|
7
|
+
*
|
|
8
|
+
* Story: wf-6a352aae (epic-episodic-memory). User-approved scope:
|
|
9
|
+
* - archive.adversaryRunsDays: 30
|
|
10
|
+
* - archive.telemetryMaxLines: 5000
|
|
11
|
+
* - archive.autoAtSessionEnd: false (manual-only — destructive operations)
|
|
12
|
+
*
|
|
13
|
+
* Boundaries:
|
|
14
|
+
* - Files referenced by the active task-checkpoint are NEVER archived.
|
|
15
|
+
* - Original content is preserved in gzip form; nothing deleted.
|
|
16
|
+
* - Idempotent: re-running on the same input writes no duplicate archives.
|
|
17
|
+
*
|
|
18
|
+
* Usage (programmatic):
|
|
19
|
+
* const { archiveAdversaryRuns, archiveTelemetryLog, archiveAll } = require('./flow-archive-runs');
|
|
20
|
+
* const result = await archiveAll({ dryRun: false });
|
|
21
|
+
*
|
|
22
|
+
* CLI:
|
|
23
|
+
* node scripts/flow-archive-runs.js # archive (writes + moves)
|
|
24
|
+
* node scripts/flow-archive-runs.js --dry-run # preview only
|
|
25
|
+
* node scripts/flow-archive-runs.js status # show what would archive
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const zlib = require('node:zlib');
|
|
31
|
+
const { pipeline } = require('node:stream/promises');
|
|
32
|
+
const { createReadStream, createWriteStream } = require('node:fs');
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
PATHS,
|
|
36
|
+
safeJsonParse,
|
|
37
|
+
writeJson,
|
|
38
|
+
ensureDir,
|
|
39
|
+
withLock,
|
|
40
|
+
} = require('./flow-utils');
|
|
41
|
+
const { getConfig } = require('./flow-config-loader');
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Constants & paths
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
const ADVERSARY_RUNS_DIR = path.join(PATHS.state, 'adversary-runs');
|
|
48
|
+
const ADVERSARY_ARCHIVE_DIR = path.join(ADVERSARY_RUNS_DIR, '_archive');
|
|
49
|
+
const ADVERSARY_INDEX_FILE = path.join(ADVERSARY_ARCHIVE_DIR, 'index.json');
|
|
50
|
+
const TELEMETRY_LOG = path.join(PATHS.state, 'gate-telemetry.jsonl');
|
|
51
|
+
const TELEMETRY_ARCHIVE_DIR = path.join(PATHS.state, '_archive');
|
|
52
|
+
const TASK_CHECKPOINT_FILE = path.join(PATHS.state, 'task-checkpoint.json');
|
|
53
|
+
|
|
54
|
+
const ARCHIVE_DEFAULTS = Object.freeze({
|
|
55
|
+
autoAtSessionEnd: false,
|
|
56
|
+
adversaryRunsDays: 30,
|
|
57
|
+
telemetryMaxLines: 5000,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Config
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
function getArchiveConfig() {
|
|
65
|
+
let cfg = {};
|
|
66
|
+
try {
|
|
67
|
+
cfg = getConfig() || {};
|
|
68
|
+
} catch (_err) {
|
|
69
|
+
cfg = {};
|
|
70
|
+
}
|
|
71
|
+
const a = cfg.archive || {};
|
|
72
|
+
return {
|
|
73
|
+
autoAtSessionEnd: a.autoAtSessionEnd === true,
|
|
74
|
+
adversaryRunsDays: Number.isFinite(a.adversaryRunsDays)
|
|
75
|
+
? a.adversaryRunsDays
|
|
76
|
+
: ARCHIVE_DEFAULTS.adversaryRunsDays,
|
|
77
|
+
telemetryMaxLines: Number.isFinite(a.telemetryMaxLines)
|
|
78
|
+
? a.telemetryMaxLines
|
|
79
|
+
: ARCHIVE_DEFAULTS.telemetryMaxLines,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Helpers
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read active taskId from task-checkpoint.json (used as do-not-archive guard).
|
|
89
|
+
*/
|
|
90
|
+
function getActiveTaskId() {
|
|
91
|
+
if (!fs.existsSync(TASK_CHECKPOINT_FILE)) return null;
|
|
92
|
+
const checkpoint = safeJsonParse(TASK_CHECKPOINT_FILE, null);
|
|
93
|
+
return checkpoint?.taskId || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ageDays(filePath) {
|
|
97
|
+
try {
|
|
98
|
+
const stat = fs.statSync(filePath);
|
|
99
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
100
|
+
return ageMs / (1000 * 60 * 60 * 24);
|
|
101
|
+
} catch (_err) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function yyyyMm(date) {
|
|
107
|
+
const d = new Date(date);
|
|
108
|
+
const y = d.getUTCFullYear();
|
|
109
|
+
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
110
|
+
return `${y}-${m}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function yyyyMmDd(date) {
|
|
114
|
+
const d = new Date(date);
|
|
115
|
+
const y = d.getUTCFullYear();
|
|
116
|
+
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
117
|
+
const day = String(d.getUTCDate()).padStart(2, '0');
|
|
118
|
+
return `${y}-${m}-${day}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Stream-gzip a source file to a destination .gz. Cross-platform via Node zlib.
|
|
123
|
+
*/
|
|
124
|
+
async function gzipFile(srcPath, destPath) {
|
|
125
|
+
ensureDir(path.dirname(destPath));
|
|
126
|
+
await pipeline(createReadStream(srcPath), zlib.createGzip(), createWriteStream(destPath));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Cross-platform "move": copy then unlink. fs.rename can fail across volumes
|
|
131
|
+
* on Windows; copy+unlink is portable.
|
|
132
|
+
*/
|
|
133
|
+
function safeUnlink(filePath) {
|
|
134
|
+
try {
|
|
135
|
+
fs.unlinkSync(filePath);
|
|
136
|
+
return true;
|
|
137
|
+
} catch (_err) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// Adversary-runs archival
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
function loadAdversaryArchiveIndex() {
|
|
147
|
+
if (!fs.existsSync(ADVERSARY_INDEX_FILE)) return {};
|
|
148
|
+
const idx = safeJsonParse(ADVERSARY_INDEX_FILE, {});
|
|
149
|
+
return idx && typeof idx === 'object' ? idx : {};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function saveAdversaryArchiveIndex(index) {
|
|
153
|
+
ensureDir(ADVERSARY_ARCHIVE_DIR);
|
|
154
|
+
await withLock(ADVERSARY_INDEX_FILE, async () => {
|
|
155
|
+
writeJson(ADVERSARY_INDEX_FILE, index);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* List adversary-run files eligible for archival.
|
|
161
|
+
*/
|
|
162
|
+
function listEligibleAdversaryRuns(config, activeTaskId) {
|
|
163
|
+
if (!fs.existsSync(ADVERSARY_RUNS_DIR)) return [];
|
|
164
|
+
let entries;
|
|
165
|
+
try {
|
|
166
|
+
entries = fs.readdirSync(ADVERSARY_RUNS_DIR);
|
|
167
|
+
} catch (_err) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const out = [];
|
|
171
|
+
for (const name of entries) {
|
|
172
|
+
if (!name.endsWith('.json')) continue;
|
|
173
|
+
// _archive is the archive subdirectory (no .json extension, won't reach here);
|
|
174
|
+
// index.json (under _archive) has a parent dir of _archive so won't appear here.
|
|
175
|
+
// We only filter individual run files now.
|
|
176
|
+
const full = path.join(ADVERSARY_RUNS_DIR, name);
|
|
177
|
+
try {
|
|
178
|
+
const stat = fs.statSync(full);
|
|
179
|
+
if (!stat.isFile()) continue;
|
|
180
|
+
} catch (_err) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
// Active-task guard: skip any file whose name contains the active task ID.
|
|
184
|
+
if (activeTaskId && name.includes(activeTaskId)) continue;
|
|
185
|
+
const age = ageDays(full);
|
|
186
|
+
if (age < config.adversaryRunsDays) continue;
|
|
187
|
+
out.push({ name, full, ageDays: Math.round(age * 10) / 10 });
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Archive eligible adversary-runs.
|
|
194
|
+
* @returns {{ archived: number, skipped: number, failed: number, archives: Array }}
|
|
195
|
+
*/
|
|
196
|
+
async function archiveAdversaryRuns(opts = {}) {
|
|
197
|
+
const config = opts.config || getArchiveConfig();
|
|
198
|
+
const dryRun = !!opts.dryRun;
|
|
199
|
+
const activeTaskId = getActiveTaskId();
|
|
200
|
+
const eligible = listEligibleAdversaryRuns(config, activeTaskId);
|
|
201
|
+
const result = { archived: 0, skipped: 0, failed: 0, archives: [], dryRun, activeTaskId };
|
|
202
|
+
if (eligible.length === 0) return result;
|
|
203
|
+
if (dryRun) {
|
|
204
|
+
result.archives = eligible.map((e) => ({
|
|
205
|
+
name: e.name,
|
|
206
|
+
ageDays: e.ageDays,
|
|
207
|
+
destinationDir: path.join(ADVERSARY_ARCHIVE_DIR, yyyyMm(new Date())),
|
|
208
|
+
}));
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
const index = loadAdversaryArchiveIndex();
|
|
212
|
+
for (const f of eligible) {
|
|
213
|
+
const ymDir = yyyyMm(new Date());
|
|
214
|
+
const destPath = path.join(ADVERSARY_ARCHIVE_DIR, ymDir, `${f.name}.gz`);
|
|
215
|
+
// Idempotency: if archive already exists for this filename, skip.
|
|
216
|
+
if (index[f.name]) {
|
|
217
|
+
result.skipped += 1;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await gzipFile(f.full, destPath);
|
|
222
|
+
// Read the run to extract metadata for the index. safeJsonParse handles
|
|
223
|
+
// file errors and prototype-pollution per security-patterns.md §2.
|
|
224
|
+
const meta = { taskId: null, round: null };
|
|
225
|
+
const parsed = safeJsonParse(f.full, null);
|
|
226
|
+
if (parsed && typeof parsed === 'object') {
|
|
227
|
+
meta.taskId = parsed.taskId || null;
|
|
228
|
+
meta.round = parsed.round || null;
|
|
229
|
+
}
|
|
230
|
+
const removed = safeUnlink(f.full);
|
|
231
|
+
if (!removed) {
|
|
232
|
+
// Source unlink failed → leave .gz in place; don't update index so
|
|
233
|
+
// a retry will skip (file already gzipped) and complete the unlink later.
|
|
234
|
+
result.failed += 1;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
index[f.name] = {
|
|
238
|
+
archivedAt: new Date().toISOString(),
|
|
239
|
+
archivePath: path.relative(PATHS.state, destPath),
|
|
240
|
+
taskId: meta.taskId,
|
|
241
|
+
round: meta.round,
|
|
242
|
+
ageDaysAtArchive: f.ageDays,
|
|
243
|
+
};
|
|
244
|
+
result.archived += 1;
|
|
245
|
+
result.archives.push({ name: f.name, archivePath: index[f.name].archivePath });
|
|
246
|
+
} catch (err) {
|
|
247
|
+
result.failed += 1;
|
|
248
|
+
if (process.env.DEBUG) {
|
|
249
|
+
console.error(`[archive] gzip failed for ${f.name}: ${err.message}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (result.archived > 0) {
|
|
254
|
+
await saveAdversaryArchiveIndex(index);
|
|
255
|
+
}
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ============================================================================
|
|
260
|
+
// Telemetry-log rotation
|
|
261
|
+
// ============================================================================
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Count lines in a file (streaming, no full-read).
|
|
265
|
+
*/
|
|
266
|
+
function countLines(filePath) {
|
|
267
|
+
return new Promise((resolve) => {
|
|
268
|
+
if (!fs.existsSync(filePath)) return resolve(0);
|
|
269
|
+
let count = 0;
|
|
270
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
271
|
+
let trailingNewline = false;
|
|
272
|
+
stream.on('data', (chunk) => {
|
|
273
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
274
|
+
if (chunk[i] === '\n') count += 1;
|
|
275
|
+
}
|
|
276
|
+
trailingNewline = chunk[chunk.length - 1] === '\n';
|
|
277
|
+
});
|
|
278
|
+
stream.on('end', () => {
|
|
279
|
+
// If file doesn't end with \n, the last partial line still counts as a line.
|
|
280
|
+
// We're a JSONL log so most lines end with \n; keep the count as-is.
|
|
281
|
+
resolve(count + (trailingNewline ? 0 : (count > 0 || fs.existsSync(filePath) ? 1 : 0)));
|
|
282
|
+
});
|
|
283
|
+
stream.on('error', () => resolve(0));
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Rotate gate-telemetry.jsonl when over the line cap.
|
|
289
|
+
* @returns {{ rotated: boolean, lineCount: number, archivePath?: string, dryRun: boolean }}
|
|
290
|
+
*/
|
|
291
|
+
async function archiveTelemetryLog(opts = {}) {
|
|
292
|
+
const config = opts.config || getArchiveConfig();
|
|
293
|
+
const dryRun = !!opts.dryRun;
|
|
294
|
+
const lineCount = await countLines(TELEMETRY_LOG);
|
|
295
|
+
if (lineCount <= config.telemetryMaxLines) {
|
|
296
|
+
return { rotated: false, lineCount, dryRun };
|
|
297
|
+
}
|
|
298
|
+
const archivePath = path.join(
|
|
299
|
+
TELEMETRY_ARCHIVE_DIR,
|
|
300
|
+
`gate-telemetry-${yyyyMmDd(new Date())}.jsonl.gz`
|
|
301
|
+
);
|
|
302
|
+
if (dryRun) {
|
|
303
|
+
return { rotated: false, lineCount, archivePath, dryRun: true };
|
|
304
|
+
}
|
|
305
|
+
await withLock(TELEMETRY_LOG, async () => {
|
|
306
|
+
await gzipFile(TELEMETRY_LOG, archivePath);
|
|
307
|
+
// Truncate live file (keeps existing FD-readers from breaking — file replaced).
|
|
308
|
+
fs.writeFileSync(TELEMETRY_LOG, '');
|
|
309
|
+
});
|
|
310
|
+
return { rotated: true, lineCount, archivePath: path.relative(PATHS.state, archivePath), dryRun };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// Top-level archiveAll
|
|
315
|
+
// ============================================================================
|
|
316
|
+
|
|
317
|
+
async function archiveAll(opts = {}) {
|
|
318
|
+
const config = opts.config || getArchiveConfig();
|
|
319
|
+
const dryRun = !!opts.dryRun;
|
|
320
|
+
const adversary = await archiveAdversaryRuns({ config, dryRun });
|
|
321
|
+
const telemetry = await archiveTelemetryLog({ config, dryRun });
|
|
322
|
+
return { adversary, telemetry, dryRun };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// Exports
|
|
327
|
+
// ============================================================================
|
|
328
|
+
|
|
329
|
+
module.exports = {
|
|
330
|
+
archiveAll,
|
|
331
|
+
archiveAdversaryRuns,
|
|
332
|
+
archiveTelemetryLog,
|
|
333
|
+
loadAdversaryArchiveIndex,
|
|
334
|
+
listEligibleAdversaryRuns,
|
|
335
|
+
getActiveTaskId,
|
|
336
|
+
getArchiveConfig,
|
|
337
|
+
countLines,
|
|
338
|
+
yyyyMm,
|
|
339
|
+
yyyyMmDd,
|
|
340
|
+
ARCHIVE_DEFAULTS,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// ============================================================================
|
|
344
|
+
// CLI
|
|
345
|
+
// ============================================================================
|
|
346
|
+
|
|
347
|
+
if (require.main === module) {
|
|
348
|
+
const args = process.argv.slice(2);
|
|
349
|
+
const dryRun = args.includes('--dry-run');
|
|
350
|
+
const cmd = args.find((a) => !a.startsWith('--')) || 'run';
|
|
351
|
+
(async () => {
|
|
352
|
+
if (cmd === 'status') {
|
|
353
|
+
const cfg = getArchiveConfig();
|
|
354
|
+
const eligible = listEligibleAdversaryRuns(cfg, getActiveTaskId());
|
|
355
|
+
const lineCount = await countLines(TELEMETRY_LOG);
|
|
356
|
+
console.log(JSON.stringify({
|
|
357
|
+
adversaryEligible: eligible.length,
|
|
358
|
+
adversaryFiles: eligible.map((e) => `${e.name} (${e.ageDays}d)`),
|
|
359
|
+
telemetryLineCount: lineCount,
|
|
360
|
+
telemetryMaxLines: cfg.telemetryMaxLines,
|
|
361
|
+
wouldRotateTelemetry: lineCount > cfg.telemetryMaxLines,
|
|
362
|
+
activeTaskId: getActiveTaskId(),
|
|
363
|
+
}, null, 2));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const r = await archiveAll({ dryRun });
|
|
367
|
+
console.log(JSON.stringify(r, null, 2));
|
|
368
|
+
})().catch((err) => {
|
|
369
|
+
console.error(`[archive] error: ${err.message}`);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
});
|
|
372
|
+
}
|
package/scripts/flow-ask.js
CHANGED
|
@@ -80,7 +80,7 @@ function clearPendingQuestion() {
|
|
|
80
80
|
* @returns {boolean}
|
|
81
81
|
*/
|
|
82
82
|
function hasPendingQuestion() {
|
|
83
|
-
try { return fs.existsSync(getPendingQuestionPath()); } catch (
|
|
83
|
+
try { return fs.existsSync(getPendingQuestionPath()); } catch (_err) { return false; }
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
module.exports = {
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - AST-Grep Integration
|
|
5
|
+
*
|
|
6
|
+
* Structural pattern search via the `sg` CLI (ast-grep). Extracted from
|
|
7
|
+
* flow-utils.js (wf-94cc3b72 epic — flow-utils decomposition) to keep the
|
|
8
|
+
* barrel thin. Re-exported from flow-utils.js for backwards compatibility.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const { execSync, execFileSync } = require('node:child_process');
|
|
15
|
+
const { PROJECT_ROOT } = require('./flow-paths');
|
|
16
|
+
const { dirExists } = require('./flow-io');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Common AST patterns for code discovery
|
|
20
|
+
*/
|
|
21
|
+
const AST_PATTERNS = {
|
|
22
|
+
// React patterns
|
|
23
|
+
reactComponent: 'function $NAME($PROPS) { return <$_>$$$</$_> }',
|
|
24
|
+
reactArrowComponent: 'const $NAME = ($PROPS) => { return <$_>$$$</$_> }',
|
|
25
|
+
useStateHook: 'const [$STATE, $SETTER] = useState($INIT)',
|
|
26
|
+
useEffectHook: 'useEffect($FN, [$$$DEPS])',
|
|
27
|
+
useCustomHook: 'const $RESULT = use$NAME($$$ARGS)',
|
|
28
|
+
|
|
29
|
+
// TypeScript patterns
|
|
30
|
+
interfaceDefinition: 'interface $NAME { $$$ }',
|
|
31
|
+
typeDefinition: 'type $NAME = $$$',
|
|
32
|
+
exportedFunction: 'export function $NAME($$$PARAMS) { $$$ }',
|
|
33
|
+
exportedConst: 'export const $NAME = $$$',
|
|
34
|
+
|
|
35
|
+
// Import patterns
|
|
36
|
+
namedImport: 'import { $$$IMPORTS } from "$PATH"',
|
|
37
|
+
defaultImport: 'import $NAME from "$PATH"',
|
|
38
|
+
|
|
39
|
+
// Class patterns
|
|
40
|
+
classDefinition: 'class $NAME { $$$ }',
|
|
41
|
+
classExtends: 'class $NAME extends $BASE { $$$ }'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if ast-grep CLI (sg) is available
|
|
46
|
+
*/
|
|
47
|
+
function isAstGrepAvailable() {
|
|
48
|
+
try {
|
|
49
|
+
execSync('which sg', { stdio: 'ignore' });
|
|
50
|
+
return true;
|
|
51
|
+
} catch (_err) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Allowed languages for ast-grep to prevent command injection (Security Rule 8)
|
|
57
|
+
const ALLOWED_AST_GREP_LANGUAGES = new Set([
|
|
58
|
+
'typescript', 'javascript', 'tsx', 'jsx', 'python', 'go', 'rust',
|
|
59
|
+
'java', 'c', 'cpp', 'csharp', 'ruby', 'swift', 'kotlin', 'html', 'css'
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Search codebase using ast-grep for structural patterns
|
|
64
|
+
* @param {string} pattern - AST pattern (e.g., "useState($INIT)")
|
|
65
|
+
* @param {object} options - { lang, cwd, maxResults }
|
|
66
|
+
* @returns {Array|null} Array of matches or null if ast-grep unavailable
|
|
67
|
+
*/
|
|
68
|
+
function astGrepSearch(pattern, options = {}) {
|
|
69
|
+
const {
|
|
70
|
+
lang = 'typescript',
|
|
71
|
+
cwd = PROJECT_ROOT,
|
|
72
|
+
maxResults = 20,
|
|
73
|
+
searchDir = 'src'
|
|
74
|
+
} = options;
|
|
75
|
+
|
|
76
|
+
// Validate lang parameter to prevent command injection (Security Rule 8)
|
|
77
|
+
if (!ALLOWED_AST_GREP_LANGUAGES.has(lang)) {
|
|
78
|
+
if (process.env.DEBUG) {
|
|
79
|
+
console.error(`[ast-grep] Invalid language: ${lang}. Allowed: ${[...ALLOWED_AST_GREP_LANGUAGES].join(', ')}`);
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check if ast-grep is available
|
|
85
|
+
if (!isAstGrepAvailable()) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const searchPath = path.join(cwd, searchDir);
|
|
90
|
+
if (!dirExists(searchPath)) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Use execFileSync with array args to prevent shell injection (Security Rule 8)
|
|
96
|
+
const result = execFileSync('sg', [
|
|
97
|
+
'--pattern', pattern,
|
|
98
|
+
'--lang', lang,
|
|
99
|
+
'--json', searchPath
|
|
100
|
+
], {
|
|
101
|
+
encoding: 'utf-8',
|
|
102
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
103
|
+
timeout: 30000
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const matches = JSON.parse(result || '[]');
|
|
107
|
+
return matches.slice(0, maxResults).map(m => ({
|
|
108
|
+
file: path.relative(cwd, m.file || m.path),
|
|
109
|
+
line: m.range?.start?.line ?? m.startLine ?? 0,
|
|
110
|
+
endLine: m.range?.end?.line ?? m.endLine ?? 0,
|
|
111
|
+
content: m.text || m.match,
|
|
112
|
+
meta: m.metaVariables || {} // Captured $VARS
|
|
113
|
+
}));
|
|
114
|
+
} catch (err) {
|
|
115
|
+
// Parse error, timeout, or no matches
|
|
116
|
+
if (err.stdout) {
|
|
117
|
+
try {
|
|
118
|
+
const matches = JSON.parse(err.stdout);
|
|
119
|
+
return matches.slice(0, maxResults).map(m => ({
|
|
120
|
+
file: path.relative(cwd, m.file || m.path),
|
|
121
|
+
line: m.range?.start?.line ?? 0,
|
|
122
|
+
content: m.text || m.match,
|
|
123
|
+
meta: m.metaVariables || {}
|
|
124
|
+
}));
|
|
125
|
+
} catch (_err) {
|
|
126
|
+
// Ignore parse errors
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Search for React components in the codebase
|
|
135
|
+
* @param {object} options - Search options
|
|
136
|
+
*/
|
|
137
|
+
function findReactComponents(options = {}) {
|
|
138
|
+
const { maxResults = 10 } = options;
|
|
139
|
+
|
|
140
|
+
// Try function components first
|
|
141
|
+
let results = astGrepSearch(AST_PATTERNS.reactComponent, { ...options, maxResults });
|
|
142
|
+
|
|
143
|
+
// If ast-grep not available, return null
|
|
144
|
+
if (results === null) return null;
|
|
145
|
+
|
|
146
|
+
// Also search arrow function components
|
|
147
|
+
const arrowResults = astGrepSearch(AST_PATTERNS.reactArrowComponent, { ...options, maxResults });
|
|
148
|
+
if (arrowResults) {
|
|
149
|
+
results = [...results, ...arrowResults];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Dedupe by file
|
|
153
|
+
const seen = new Set();
|
|
154
|
+
return results.filter(r => {
|
|
155
|
+
if (seen.has(r.file)) return false;
|
|
156
|
+
seen.add(r.file);
|
|
157
|
+
return true;
|
|
158
|
+
}).slice(0, maxResults);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Search for custom hooks in the codebase
|
|
163
|
+
* @param {object} options - Search options
|
|
164
|
+
*/
|
|
165
|
+
function findCustomHooks(options = {}) {
|
|
166
|
+
const { maxResults = 10 } = options;
|
|
167
|
+
|
|
168
|
+
// Search for function use* pattern
|
|
169
|
+
const results = astGrepSearch('function use$NAME($$$) { $$$ }', { ...options, maxResults });
|
|
170
|
+
|
|
171
|
+
if (results === null) return null;
|
|
172
|
+
|
|
173
|
+
return results.filter(r => {
|
|
174
|
+
// Filter to only actual hook files
|
|
175
|
+
const fileName = path.basename(r.file).toLowerCase();
|
|
176
|
+
return fileName.startsWith('use') || fileName.includes('hook');
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Search for TypeScript interfaces/types
|
|
182
|
+
* @param {string} namePattern - Optional name pattern to filter by
|
|
183
|
+
* @param {object} options - Search options
|
|
184
|
+
*/
|
|
185
|
+
function findTypeDefinitions(namePattern = null, options = {}) {
|
|
186
|
+
const { maxResults = 10 } = options;
|
|
187
|
+
|
|
188
|
+
// Search interfaces
|
|
189
|
+
let results = astGrepSearch(AST_PATTERNS.interfaceDefinition, { ...options, maxResults });
|
|
190
|
+
|
|
191
|
+
if (results === null) return null;
|
|
192
|
+
|
|
193
|
+
// Also search type aliases
|
|
194
|
+
const typeResults = astGrepSearch(AST_PATTERNS.typeDefinition, { ...options, maxResults });
|
|
195
|
+
if (typeResults) {
|
|
196
|
+
results = [...results, ...typeResults];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Filter by name pattern if provided
|
|
200
|
+
if (namePattern) {
|
|
201
|
+
const regex = new RegExp(namePattern, 'i');
|
|
202
|
+
results = results.filter(r => regex.test(r.content));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return results.slice(0, maxResults);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
AST_PATTERNS,
|
|
210
|
+
ALLOWED_AST_GREP_LANGUAGES,
|
|
211
|
+
isAstGrepAvailable,
|
|
212
|
+
astGrepSearch,
|
|
213
|
+
findReactComponents,
|
|
214
|
+
findCustomHooks,
|
|
215
|
+
findTypeDefinitions,
|
|
216
|
+
};
|
|
@@ -15,13 +15,13 @@ const fs = require('node:fs');
|
|
|
15
15
|
const path = require('node:path');
|
|
16
16
|
const {
|
|
17
17
|
PATHS,
|
|
18
|
-
PROJECT_ROOT,
|
|
19
18
|
fileExists,
|
|
20
19
|
getConfig,
|
|
21
20
|
color,
|
|
22
21
|
success,
|
|
23
22
|
info,
|
|
24
|
-
warn
|
|
23
|
+
warn,
|
|
24
|
+
getTodayDate
|
|
25
25
|
} = require('./flow-utils');
|
|
26
26
|
|
|
27
27
|
// ============================================================
|
|
@@ -142,15 +142,9 @@ function normalizeIssueToPattern(issue) {
|
|
|
142
142
|
* @param {string} str - Input string
|
|
143
143
|
* @returns {string} Kebab-case string
|
|
144
144
|
*/
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
.replace(/[^a-z0-9\s-]/g, '')
|
|
149
|
-
.replace(/\s+/g, '-')
|
|
150
|
-
.replace(/-+/g, '-')
|
|
151
|
-
.replace(/^-|-$/g, '')
|
|
152
|
-
.slice(0, 50);
|
|
153
|
-
}
|
|
145
|
+
// Local slugify removed in favor of canonical flow-output slugify (wf-7072d3ac).
|
|
146
|
+
const { slugify: _slugify } = require('./flow-output');
|
|
147
|
+
const slugify = (str) => _slugify(str, { maxLength: 50 });
|
|
154
148
|
|
|
155
149
|
// ============================================================
|
|
156
150
|
// Feedback Patterns File Management
|
|
@@ -608,6 +602,9 @@ module.exports = {
|
|
|
608
602
|
getAutoLearnConfig,
|
|
609
603
|
loadAutoPatterns,
|
|
610
604
|
saveAutoPatterns,
|
|
605
|
+
// Promotion API exposed for wf-6a352aae callers (adversary-promotion + pattern-phrase-promotion)
|
|
606
|
+
handlePromotion,
|
|
607
|
+
promoteToDecisions,
|
|
611
608
|
showStatus
|
|
612
609
|
};
|
|
613
610
|
|
package/scripts/flow-bug.js
CHANGED
|
@@ -42,10 +42,10 @@ let loadSessionState;
|
|
|
42
42
|
try {
|
|
43
43
|
const sessionModule = require('./flow-session-state');
|
|
44
44
|
loadSessionState = sessionModule.loadSessionState;
|
|
45
|
-
} catch (
|
|
45
|
+
} catch (err) {
|
|
46
46
|
// Log in debug mode - don't silently hide potential syntax errors
|
|
47
47
|
if (process.env.DEBUG) {
|
|
48
|
-
console.warn(`[DEBUG] Could not load flow-session-state: ${
|
|
48
|
+
console.warn(`[DEBUG] Could not load flow-session-state: ${err.message}`);
|
|
49
49
|
}
|
|
50
50
|
loadSessionState = () => ({});
|
|
51
51
|
}
|