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.
Files changed (111) hide show
  1. package/.claude/commands/wogi-audit.md +212 -17
  2. package/.claude/commands/wogi-research.md +37 -0
  3. package/.claude/commands/wogi-review.md +200 -22
  4. package/.claude/commands/wogi-start.md +45 -0
  5. package/.claude/docs/claude-code-compatibility.md +46 -1
  6. package/.claude/docs/intent-grounded-review.md +209 -0
  7. package/.claude/settings.json +34 -1
  8. package/.workflow/agents/logic-adversary.md +8 -0
  9. package/.workflow/templates/claude-md.hbs +18 -0
  10. package/lib/installer.js +22 -0
  11. package/lib/utils.js +29 -3
  12. package/lib/workspace-changelog.js +2 -1
  13. package/lib/workspace-channel-server.js +4 -6
  14. package/lib/workspace-contracts.js +5 -4
  15. package/lib/workspace-events.js +8 -7
  16. package/lib/workspace-gates.js +4 -3
  17. package/lib/workspace-integration-tests.js +2 -1
  18. package/lib/workspace-intelligence.js +3 -2
  19. package/lib/workspace-locks.js +2 -1
  20. package/lib/workspace-messages.js +7 -6
  21. package/lib/workspace-routing.js +14 -26
  22. package/lib/workspace-session.js +7 -6
  23. package/lib/workspace-sync.js +9 -8
  24. package/package.json +4 -2
  25. package/scripts/base-workflow-step.js +1 -1
  26. package/scripts/flow +19 -0
  27. package/scripts/flow-adaptive-learning.js +1 -1
  28. package/scripts/flow-aggregate.js +2 -1
  29. package/scripts/flow-architect-pass.js +3 -3
  30. package/scripts/flow-archive-runs.js +372 -0
  31. package/scripts/flow-ask.js +1 -1
  32. package/scripts/flow-ast-grep.js +216 -0
  33. package/scripts/flow-audit-gates.js +1 -1
  34. package/scripts/flow-auto-learn.js +8 -11
  35. package/scripts/flow-bug.js +2 -2
  36. package/scripts/flow-capture-gate.js +644 -0
  37. package/scripts/flow-capture.js +4 -3
  38. package/scripts/flow-cli-flags.js +95 -0
  39. package/scripts/flow-community-sync.js +2 -1
  40. package/scripts/flow-community.js +6 -6
  41. package/scripts/flow-conclusion-classifier.js +310 -0
  42. package/scripts/flow-config-defaults.js +3 -3
  43. package/scripts/flow-constants.js +8 -11
  44. package/scripts/flow-context-scoring.js +1 -0
  45. package/scripts/flow-correction-detector.js +344 -3
  46. package/scripts/flow-damage-control.js +1 -1
  47. package/scripts/flow-decisions-merge.js +1 -0
  48. package/scripts/flow-done-gates.js +20 -0
  49. package/scripts/flow-done-report.js +2 -2
  50. package/scripts/flow-done.js +4 -4
  51. package/scripts/flow-epics.js +5 -11
  52. package/scripts/flow-health.js +145 -1
  53. package/scripts/flow-id.js +92 -0
  54. package/scripts/flow-io.js +15 -5
  55. package/scripts/flow-knowledge-router.js +2 -1
  56. package/scripts/flow-links.js +1 -1
  57. package/scripts/flow-log-manager.js +2 -1
  58. package/scripts/flow-logic-adversary.js +4 -4
  59. package/scripts/flow-long-input-cli.js +6 -0
  60. package/scripts/flow-long-input-stories.js +1 -1
  61. package/scripts/flow-loop-retry-learning.js +1 -1
  62. package/scripts/flow-mcp-capabilities.js +2 -3
  63. package/scripts/flow-mcp-docs.js +2 -1
  64. package/scripts/flow-memory-blocks.js +2 -1
  65. package/scripts/flow-memory-sync.js +1 -1
  66. package/scripts/flow-memory.js +767 -0
  67. package/scripts/flow-migrate-igr.js +1 -1
  68. package/scripts/flow-migrate.js +2 -1
  69. package/scripts/flow-model-adapter.js +1 -1
  70. package/scripts/flow-model-config.js +5 -1
  71. package/scripts/flow-model-profile.js +2 -1
  72. package/scripts/flow-orchestrate.js +3 -3
  73. package/scripts/flow-output.js +29 -0
  74. package/scripts/flow-parallel.js +10 -9
  75. package/scripts/flow-pattern-enforcer.js +2 -1
  76. package/scripts/flow-permissions-audit.js +124 -0
  77. package/scripts/flow-plugin-registry.js +2 -2
  78. package/scripts/flow-progress.js +5 -1
  79. package/scripts/flow-project-analyzer.js +1 -1
  80. package/scripts/flow-promote.js +510 -0
  81. package/scripts/flow-registries.js +86 -0
  82. package/scripts/flow-request-log.js +133 -0
  83. package/scripts/flow-research-protocol.js +0 -1
  84. package/scripts/flow-revision-tracker.js +2 -1
  85. package/scripts/flow-roadmap.js +2 -1
  86. package/scripts/flow-rules-sync.js +3 -7
  87. package/scripts/flow-session-end.js +3 -1
  88. package/scripts/flow-session-learning.js +6 -13
  89. package/scripts/flow-session-state.js +2 -2
  90. package/scripts/flow-setup-hooks.js +2 -1
  91. package/scripts/flow-skill-create.js +1 -1
  92. package/scripts/flow-skill-freshness.js +6 -7
  93. package/scripts/flow-skill-learn.js +1 -1
  94. package/scripts/flow-step-coverage.js +1 -1
  95. package/scripts/flow-step-security.js +1 -1
  96. package/scripts/flow-story.js +58 -10
  97. package/scripts/flow-sys.js +204 -0
  98. package/scripts/flow-task-hierarchy.js +88 -0
  99. package/scripts/flow-tech-debt.js +2 -1
  100. package/scripts/flow-test-api.js +1 -1
  101. package/scripts/flow-utils.js +60 -890
  102. package/scripts/hooks/core/bugfix-scope-gate.js +5 -4
  103. package/scripts/hooks/core/deploy-gate.js +1 -1
  104. package/scripts/hooks/core/pre-tool-helpers.js +72 -0
  105. package/scripts/hooks/core/pre-tool-orchestrator.js +442 -0
  106. package/scripts/hooks/core/routing-gate.js +8 -0
  107. package/scripts/hooks/core/session-end.js +28 -0
  108. package/scripts/hooks/entry/claude-code/pre-tool-use.js +48 -492
  109. package/scripts/hooks/entry/shared/hook-runner.js +1 -1
  110. package/scripts/registries/schema-registry.js +1 -1
  111. package/scripts/registries/service-registry.js +1 -1
@@ -0,0 +1,767 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Memory CLI (query / fetch / stats / tag)
5
+ *
6
+ * Unified query layer over WogiFlow's state-file landscape. Instead of
7
+ * grepping across ready.json / decisions.md / feedback-patterns.md /
8
+ * corrections/ / adversary-runs/ / request-log.md, users run
9
+ * `flow memory query <filters>` or `flow memory fetch <ref>`.
10
+ *
11
+ * Story: wf-e64cacd0 (epic-episodic-memory, re-scoped post-pivot).
12
+ *
13
+ * Boundaries:
14
+ * - Does NOT modify memory file contents (read-only across all sources).
15
+ * - Tags are stored in a sidecar `.workflow/state/memory-tags.json` — the
16
+ * source memory files stay immutable.
17
+ *
18
+ * Usage (CLI):
19
+ * flow memory query [--since=<dur>] [--task=<id>] [--kind=<k>] [--tag=<#t>]
20
+ * flow memory fetch <ref> # ref = wf-ID, R-N, CORR-N, adversary-run filename
21
+ * flow memory stats
22
+ * flow memory tag <ref> <#tag>
23
+ * flow memory untag <ref> <#tag>
24
+ *
25
+ * Usage (programmatic):
26
+ * const { queryMemory, fetchByRef, memoryStats, addTag } = require('./flow-memory');
27
+ */
28
+
29
+ const fs = require('node:fs');
30
+ const path = require('node:path');
31
+
32
+ const {
33
+ PATHS,
34
+ safeJsonParse,
35
+ safeJsonParseString,
36
+ writeJson,
37
+ withLock,
38
+ } = require('./flow-utils');
39
+
40
+ // ============================================================================
41
+ // Constants
42
+ // ============================================================================
43
+
44
+ const MEMORY_TAGS_FILE = path.join(PATHS.state, 'memory-tags.json');
45
+
46
+ const ADVERSARY_RUNS_DIR = path.join(PATHS.state, 'adversary-runs');
47
+ const CORRECTIONS_DIR = path.join(PATHS.workflow, 'corrections');
48
+ const READY_FILE = path.join(PATHS.state, 'ready.json');
49
+ const REQUEST_LOG_FILE = path.join(PATHS.state, 'request-log.md');
50
+ const DECISIONS_FILE = path.join(PATHS.state, 'decisions.md');
51
+ const FEEDBACK_PATTERNS_FILE = path.join(PATHS.state, 'feedback-patterns.md');
52
+ const CORRECTION_PATTERNS_FILE = path.join(PATHS.state, 'correction-patterns.json');
53
+
54
+ const KINDS = Object.freeze({
55
+ task: 'task', // ready.json tasks
56
+ requestlog: 'requestlog', // R-NNN entries
57
+ correction: 'correction', // CORR-NNN records
58
+ adversary: 'adversary', // adversary-runs/*.json
59
+ rule: 'rule', // decisions.md sections
60
+ pattern: 'pattern', // feedback-patterns.md entries
61
+ phrase: 'phrase', // correction-patterns.json phrases
62
+ });
63
+
64
+ // ============================================================================
65
+ // Duration parser — accepts 30m / 2h / 7d / 2w
66
+ // ============================================================================
67
+
68
+ function parseDuration(spec) {
69
+ if (!spec || typeof spec !== 'string') return null;
70
+ const m = spec.trim().match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/i);
71
+ if (!m) return null;
72
+ const n = Number(m[1]);
73
+ const unit = m[2].toLowerCase();
74
+ const mult = unit === 'm' ? 60_000 :
75
+ unit === 'h' ? 3_600_000 :
76
+ unit === 'd' ? 86_400_000 :
77
+ 604_800_000;
78
+ return n * mult;
79
+ }
80
+
81
+ function isAfter(iso, cutoffMs) {
82
+ if (!iso) return false;
83
+ const t = new Date(iso).getTime();
84
+ if (!Number.isFinite(t)) return false;
85
+ return t >= cutoffMs;
86
+ }
87
+
88
+ // ============================================================================
89
+ // Tag sidecar
90
+ // ============================================================================
91
+
92
+ function loadMemoryTags() {
93
+ if (!fs.existsSync(MEMORY_TAGS_FILE)) return {};
94
+ const data = safeJsonParse(MEMORY_TAGS_FILE, {});
95
+ return data && typeof data === 'object' ? data : {};
96
+ }
97
+
98
+ function normalizeTag(tag) {
99
+ let t = String(tag || '').trim();
100
+ if (!t) return '';
101
+ if (!t.startsWith('#')) t = '#' + t;
102
+ return t.toLowerCase();
103
+ }
104
+
105
+ async function addTag(ref, tag) {
106
+ const nref = String(ref || '').trim();
107
+ const ntag = normalizeTag(tag);
108
+ if (!nref || !ntag) return { ok: false, reason: 'ref and tag required' };
109
+ await withLock(MEMORY_TAGS_FILE, async () => {
110
+ const tags = loadMemoryTags();
111
+ if (!Array.isArray(tags[nref])) tags[nref] = [];
112
+ if (!tags[nref].includes(ntag)) tags[nref].push(ntag);
113
+ writeJson(MEMORY_TAGS_FILE, tags);
114
+ });
115
+ return { ok: true, ref: nref, tag: ntag };
116
+ }
117
+
118
+ async function removeTag(ref, tag) {
119
+ const nref = String(ref || '').trim();
120
+ const ntag = normalizeTag(tag);
121
+ if (!nref || !ntag) return { ok: false, reason: 'ref and tag required' };
122
+ let removed = false;
123
+ await withLock(MEMORY_TAGS_FILE, async () => {
124
+ const tags = loadMemoryTags();
125
+ if (!Array.isArray(tags[nref])) return;
126
+ const before = tags[nref].length;
127
+ tags[nref] = tags[nref].filter(t => t !== ntag);
128
+ if (tags[nref].length !== before) removed = true;
129
+ if (tags[nref].length === 0) delete tags[nref];
130
+ writeJson(MEMORY_TAGS_FILE, tags);
131
+ });
132
+ return { ok: removed, ref: nref, tag: ntag };
133
+ }
134
+
135
+ function getTagsForRef(ref, tagsMap) {
136
+ const tags = tagsMap || loadMemoryTags();
137
+ return Array.isArray(tags[ref]) ? tags[ref].slice() : [];
138
+ }
139
+
140
+ // ============================================================================
141
+ // Source readers — each returns a normalized entry shape:
142
+ // { kind, ref, title, summary, timestamp, taskIds: [], tags: [], source: path }
143
+ // ============================================================================
144
+
145
+ function loadTasks() {
146
+ const data = safeJsonParse(READY_FILE, null);
147
+ if (!data || typeof data !== 'object') return [];
148
+ const out = [];
149
+ const buckets = ['inProgress', 'ready', 'blocked', 'recentlyCompleted', 'backlog'];
150
+ for (const b of buckets) {
151
+ const arr = Array.isArray(data[b]) ? data[b] : [];
152
+ for (const t of arr) {
153
+ if (!t || !t.id) continue;
154
+ const ts = t.completedAt || t.startedAt || t.created || null;
155
+ out.push({
156
+ kind: KINDS.task,
157
+ ref: t.id,
158
+ title: t.title || '',
159
+ summary: `[${b}] ${t.type || 'task'} level=${t.level || '?'} priority=${t.priority || '?'}`,
160
+ timestamp: ts,
161
+ taskIds: [t.id],
162
+ tags: [],
163
+ source: READY_FILE,
164
+ extra: { bucket: b, raw: t },
165
+ });
166
+ }
167
+ }
168
+ return out;
169
+ }
170
+
171
+ // Parses request-log.md into structured R-NNN entries.
172
+ // Format per entry (observed):
173
+ // ### R-NNN | YYYY-MM-DD
174
+ // **Type**: ...
175
+ // **Tags**: #a #b ...
176
+ // **Task**: wf-... (sometimes)
177
+ // **Request**: "..."
178
+ // **Result**: ...
179
+ // **Files**: ...
180
+ function loadRequestLog() {
181
+ if (!fs.existsSync(REQUEST_LOG_FILE)) return [];
182
+ let raw;
183
+ try {
184
+ raw = fs.readFileSync(REQUEST_LOG_FILE, 'utf-8');
185
+ } catch (_err) {
186
+ return [];
187
+ }
188
+ const out = [];
189
+ const entryRe = /^### (R-\d+)\s*\|\s*([0-9-]+)(?:\s+[0-9:]+)?$/gm;
190
+ const matches = [...raw.matchAll(entryRe)];
191
+ for (let i = 0; i < matches.length; i++) {
192
+ const m = matches[i];
193
+ const start = m.index;
194
+ const end = i + 1 < matches.length ? matches[i + 1].index : raw.length;
195
+ const block = raw.slice(start, end);
196
+ const ref = m[1];
197
+ const dateStr = m[2];
198
+ // Parse fields
199
+ const tagsMatch = block.match(/^\*\*Tags\*\*:\s*(.+)$/m);
200
+ const typeMatch = block.match(/^\*\*Type\*\*:\s*(.+)$/m);
201
+ const requestMatch = block.match(/^\*\*Request\*\*:\s*(.+)$/m);
202
+ const resultMatch = block.match(/^\*\*Result\*\*:\s*(.+)$/m);
203
+ const filesMatch = block.match(/^\*\*Files\*\*:\s*(.+)$/m);
204
+ const tagsStr = tagsMatch ? tagsMatch[1] : '';
205
+ const entryTags = (tagsStr.match(/#[\w:-]+/g) || []).map(t => t.toLowerCase());
206
+ // Extract task IDs from tags and from block content
207
+ const taskIds = new Set();
208
+ for (const t of entryTags) {
209
+ const mm = t.match(/#task:(wf-[a-f0-9]{8})/i);
210
+ if (mm) taskIds.add(mm[1].toLowerCase());
211
+ }
212
+ for (const mm of block.matchAll(/\bwf-[a-f0-9]{8}\b/gi)) {
213
+ taskIds.add(mm[0].toLowerCase());
214
+ }
215
+ out.push({
216
+ kind: KINDS.requestlog,
217
+ ref,
218
+ title: (requestMatch ? requestMatch[1] : '').trim().replace(/^["']|["']$/g, '').slice(0, 120),
219
+ summary: (typeMatch ? `[${typeMatch[1].trim()}] ` : '') +
220
+ (resultMatch ? resultMatch[1].trim().slice(0, 200) : ''),
221
+ timestamp: dateStr ? `${dateStr}T00:00:00.000Z` : null,
222
+ taskIds: [...taskIds],
223
+ tags: entryTags,
224
+ source: REQUEST_LOG_FILE,
225
+ extra: { block, files: filesMatch ? filesMatch[1] : '' },
226
+ });
227
+ }
228
+ return out;
229
+ }
230
+
231
+ function loadCorrections() {
232
+ if (!fs.existsSync(CORRECTIONS_DIR)) return [];
233
+ let names;
234
+ try {
235
+ names = fs.readdirSync(CORRECTIONS_DIR);
236
+ } catch (_err) {
237
+ return [];
238
+ }
239
+ const out = [];
240
+ for (const name of names) {
241
+ if (!/^CORR-\d+\.md$/i.test(name)) continue;
242
+ const full = path.join(CORRECTIONS_DIR, name);
243
+ let body;
244
+ try {
245
+ body = fs.readFileSync(full, 'utf-8');
246
+ } catch (_err) {
247
+ continue;
248
+ }
249
+ const ref = name.replace(/\.md$/i, '');
250
+ const stat = fs.statSync(full);
251
+ // Parse optional frontmatter fields (first-section)
252
+ const firstLine = body.split('\n').slice(0, 15).join('\n');
253
+ const summary = firstLine
254
+ .replace(/^#.*$/gm, '')
255
+ .split('\n')
256
+ .filter(l => l.trim())
257
+ .slice(0, 2)
258
+ .join(' ')
259
+ .slice(0, 180);
260
+ const taskMatch = body.match(/\bwf-[a-f0-9]{8}\b/i);
261
+ out.push({
262
+ kind: KINDS.correction,
263
+ ref,
264
+ title: body.split('\n').find(l => l.startsWith('# ')) || ref,
265
+ summary,
266
+ timestamp: stat.mtime.toISOString(),
267
+ taskIds: taskMatch ? [taskMatch[0].toLowerCase()] : [],
268
+ tags: [],
269
+ source: full,
270
+ extra: { body },
271
+ });
272
+ }
273
+ return out;
274
+ }
275
+
276
+ function loadAdversaryRuns() {
277
+ if (!fs.existsSync(ADVERSARY_RUNS_DIR)) return [];
278
+ let names;
279
+ try {
280
+ names = fs.readdirSync(ADVERSARY_RUNS_DIR);
281
+ } catch (_err) {
282
+ return [];
283
+ }
284
+ const out = [];
285
+ for (const name of names) {
286
+ if (!name.endsWith('.json')) continue;
287
+ const full = path.join(ADVERSARY_RUNS_DIR, name);
288
+ let stat;
289
+ try {
290
+ stat = fs.statSync(full);
291
+ if (!stat.isFile()) continue;
292
+ } catch (_err) {
293
+ continue;
294
+ }
295
+ let raw;
296
+ try {
297
+ raw = fs.readFileSync(full, 'utf-8');
298
+ } catch (_err) {
299
+ continue;
300
+ }
301
+ const parsed = safeJsonParseString(raw, null);
302
+ if (!parsed || typeof parsed !== 'object') continue;
303
+ const principles = Array.isArray(parsed.principles) ? parsed.principles : [];
304
+ const failCount = principles.filter(p => /^(FAIL|CONCERN)$/i.test(String(p?.verdict || ''))).length;
305
+ const verdict = parsed.overallVerdict || '?';
306
+ out.push({
307
+ kind: KINDS.adversary,
308
+ ref: name.replace(/\.json$/, ''),
309
+ title: `${parsed.taskId || '?'} r${parsed.round || '?'}`,
310
+ summary: `overall=${verdict} fail/concern=${failCount}/${principles.length}`,
311
+ timestamp: parsed.runAt || stat.mtime.toISOString(),
312
+ taskIds: parsed.taskId ? [String(parsed.taskId).toLowerCase()] : [],
313
+ tags: [],
314
+ source: full,
315
+ extra: parsed,
316
+ });
317
+ }
318
+ return out;
319
+ }
320
+
321
+ // Parse decisions.md into rule entries (one per `### <Title>` heading under a `## <Section>`)
322
+ function loadRules() {
323
+ if (!fs.existsSync(DECISIONS_FILE)) return [];
324
+ let raw;
325
+ try {
326
+ raw = fs.readFileSync(DECISIONS_FILE, 'utf-8');
327
+ } catch (_err) {
328
+ return [];
329
+ }
330
+ const out = [];
331
+ const sectionRe = /^##\s+(.+)$/gm;
332
+ const sections = [];
333
+ let m;
334
+ while ((m = sectionRe.exec(raw)) !== null) {
335
+ sections.push({ title: m[1].trim(), start: m.index });
336
+ }
337
+ for (let i = 0; i < sections.length; i++) {
338
+ const s = sections[i];
339
+ const end = i + 1 < sections.length ? sections[i + 1].start : raw.length;
340
+ const body = raw.slice(s.start, end);
341
+ // Each ### under this section is a rule.
342
+ const ruleRe = /^###\s+(.+)$/gm;
343
+ let rm;
344
+ while ((rm = ruleRe.exec(body)) !== null) {
345
+ const title = rm[1].trim();
346
+ // Slug for ref: section/title
347
+ const slug = (s.title + '/' + title)
348
+ .toLowerCase().replace(/[^a-z0-9/]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80);
349
+ out.push({
350
+ kind: KINDS.rule,
351
+ ref: `rule/${slug}`,
352
+ title,
353
+ summary: `[${s.title}] ${title}`,
354
+ timestamp: null,
355
+ taskIds: [],
356
+ tags: [],
357
+ source: DECISIONS_FILE,
358
+ extra: { section: s.title },
359
+ });
360
+ }
361
+ }
362
+ return out;
363
+ }
364
+
365
+ // Parse feedback-patterns.md — looks for table rows in `## Auto-Captured Patterns`.
366
+ function loadFeedbackPatterns() {
367
+ if (!fs.existsSync(FEEDBACK_PATTERNS_FILE)) return [];
368
+ let raw;
369
+ try {
370
+ raw = fs.readFileSync(FEEDBACK_PATTERNS_FILE, 'utf-8');
371
+ } catch (_err) {
372
+ return [];
373
+ }
374
+ const out = [];
375
+ const lines = raw.split('\n');
376
+ for (const line of lines) {
377
+ if (!line.startsWith('|')) continue;
378
+ // Skip header + separator rows
379
+ if (/^\|\s*-+\s*\|/.test(line)) continue;
380
+ if (/\bDate\s*\|\s*Pattern\b/i.test(line)) continue;
381
+ const cells = line.split('|').map(c => c.trim()).filter((_c, i, arr) => i > 0 && i < arr.length - 1);
382
+ if (cells.length < 3) continue;
383
+ const [date, pattern, source] = cells;
384
+ if (!date || !pattern) continue;
385
+ out.push({
386
+ kind: KINDS.pattern,
387
+ ref: `pattern/${pattern.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60)}`,
388
+ title: pattern,
389
+ summary: `[${source || '?'}] ${pattern}`,
390
+ timestamp: /^\d{4}-\d{2}-\d{2}/.test(date) ? `${date.slice(0, 10)}T00:00:00.000Z` : null,
391
+ taskIds: [],
392
+ tags: [],
393
+ source: FEEDBACK_PATTERNS_FILE,
394
+ extra: { row: line, cells },
395
+ });
396
+ }
397
+ return out;
398
+ }
399
+
400
+ function loadPhrases() {
401
+ if (!fs.existsSync(CORRECTION_PATTERNS_FILE)) return [];
402
+ let raw;
403
+ try {
404
+ raw = fs.readFileSync(CORRECTION_PATTERNS_FILE, 'utf-8');
405
+ } catch (_err) {
406
+ return [];
407
+ }
408
+ const parsed = safeJsonParseString(raw, null);
409
+ if (!Array.isArray(parsed)) return [];
410
+ const out = [];
411
+ for (const p of parsed) {
412
+ if (!p || typeof p !== 'object' || !p.phrase) continue;
413
+ out.push({
414
+ kind: KINDS.phrase,
415
+ ref: `phrase/${String(p.phrase).toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60)}`,
416
+ title: p.phrase,
417
+ summary: `hits=${p.hits || 0} confirmed=${p.confirmedHits || 0} fp=${p.falsePositives || 0}`,
418
+ timestamp: p.lastHitAt || p.lastConfirmedAt || p.addedAt || null,
419
+ taskIds: [],
420
+ tags: [],
421
+ source: CORRECTION_PATTERNS_FILE,
422
+ extra: p,
423
+ });
424
+ }
425
+ return out;
426
+ }
427
+
428
+ function loadAllMemory() {
429
+ return [
430
+ ...loadTasks(),
431
+ ...loadRequestLog(),
432
+ ...loadCorrections(),
433
+ ...loadAdversaryRuns(),
434
+ ...loadRules(),
435
+ ...loadFeedbackPatterns(),
436
+ ...loadPhrases(),
437
+ ];
438
+ }
439
+
440
+ // ============================================================================
441
+ // Query
442
+ // ============================================================================
443
+
444
+ /**
445
+ * @param {Object} filters
446
+ * @param {string} [filters.since] duration spec (e.g. "2h", "7d")
447
+ * @param {string} [filters.task] wf-XXXXXXXX
448
+ * @param {string} [filters.kind] one of KINDS values
449
+ * @param {string} [filters.tag] e.g. "#important"
450
+ * @param {number} [filters.limit] max results (default 100)
451
+ * @returns {Array} normalized entries
452
+ */
453
+ function queryMemory(filters = {}) {
454
+ const { since, task, kind, tag, limit = 100 } = filters;
455
+ let entries = loadAllMemory();
456
+
457
+ // Attach tags from sidecar
458
+ const tagsMap = loadMemoryTags();
459
+ for (const e of entries) {
460
+ const t = tagsMap[e.ref];
461
+ if (Array.isArray(t) && t.length) e.tags = [...new Set([...e.tags, ...t])];
462
+ }
463
+
464
+ if (kind) {
465
+ const k = String(kind).toLowerCase();
466
+ if (!Object.values(KINDS).includes(k)) {
467
+ return { error: `unknown kind: ${kind}`, valid: Object.values(KINDS) };
468
+ }
469
+ entries = entries.filter(e => e.kind === k);
470
+ }
471
+
472
+ if (since) {
473
+ const ms = parseDuration(since);
474
+ if (ms === null) return { error: `unparsable duration: ${since}` };
475
+ const cutoff = Date.now() - ms;
476
+ entries = entries.filter(e => isAfter(e.timestamp, cutoff));
477
+ }
478
+
479
+ if (task) {
480
+ const tnorm = String(task).toLowerCase();
481
+ entries = entries.filter(e =>
482
+ e.ref.toLowerCase() === tnorm ||
483
+ (e.taskIds || []).some(id => id.toLowerCase() === tnorm)
484
+ );
485
+ }
486
+
487
+ if (tag) {
488
+ const tnorm = normalizeTag(tag);
489
+ entries = entries.filter(e => (e.tags || []).includes(tnorm));
490
+ }
491
+
492
+ // Sort newest first (undefined timestamps last)
493
+ entries.sort((a, b) => {
494
+ const ta = a.timestamp ? Date.parse(a.timestamp) : 0;
495
+ const tb = b.timestamp ? Date.parse(b.timestamp) : 0;
496
+ return tb - ta;
497
+ });
498
+
499
+ return entries.slice(0, limit);
500
+ }
501
+
502
+ // ============================================================================
503
+ // Fetch
504
+ // ============================================================================
505
+
506
+ /**
507
+ * Resolve a ref to a full record with related entries.
508
+ * Accepts: wf-XXXXXXXX, R-NNN, CORR-NNN, adversary-run filename (without .json), rule/*, pattern/*, phrase/*.
509
+ */
510
+ function fetchByRef(ref) {
511
+ const r = String(ref || '').trim();
512
+ if (!r) return { found: false, reason: 'empty ref' };
513
+ const all = loadAllMemory();
514
+ const tagsMap = loadMemoryTags();
515
+ for (const e of all) {
516
+ const t = tagsMap[e.ref];
517
+ if (Array.isArray(t) && t.length) e.tags = [...new Set([...e.tags, ...t])];
518
+ }
519
+
520
+ // Normalize comparison
521
+ const rl = r.toLowerCase();
522
+
523
+ // 1. Direct ref match
524
+ const direct = all.find(e => e.ref.toLowerCase() === rl);
525
+ if (direct) {
526
+ // If it's a task, gather all related entries.
527
+ const related = [];
528
+ if (direct.kind === KINDS.task) {
529
+ for (const e of all) {
530
+ if (e === direct) continue;
531
+ if ((e.taskIds || []).some(id => id.toLowerCase() === rl)) related.push(e);
532
+ }
533
+ }
534
+ return { found: true, entry: direct, related };
535
+ }
536
+
537
+ // 2. Task-ID lookup if ref is wf-xxx but not in ready.json any more (e.g., archived)
538
+ if (/^wf-[a-f0-9]{8}$/i.test(r)) {
539
+ const related = all.filter(e => (e.taskIds || []).some(id => id.toLowerCase() === rl));
540
+ if (related.length > 0) {
541
+ return {
542
+ found: true,
543
+ entry: {
544
+ kind: KINDS.task,
545
+ ref: r,
546
+ title: `(task not in ready.json; reconstructed from ${related.length} related entries)`,
547
+ summary: related.map(e => `${e.kind}:${e.ref}`).join(', '),
548
+ taskIds: [r],
549
+ tags: [],
550
+ timestamp: null,
551
+ source: null,
552
+ extra: {},
553
+ },
554
+ related,
555
+ };
556
+ }
557
+ }
558
+
559
+ return {
560
+ found: false,
561
+ reason: `no memory entry matches "${r}"`,
562
+ suggestion: 'try: flow memory query (to list recent entries)',
563
+ };
564
+ }
565
+
566
+ // ============================================================================
567
+ // Stats
568
+ // ============================================================================
569
+
570
+ function memoryStats() {
571
+ const tasks = loadTasks();
572
+ const byBucket = { inProgress: 0, ready: 0, blocked: 0, recentlyCompleted: 0, backlog: 0 };
573
+ for (const t of tasks) {
574
+ const b = t.extra?.bucket;
575
+ if (b && byBucket[b] !== undefined) byBucket[b] += 1;
576
+ }
577
+ return {
578
+ tasks: { total: tasks.length, byBucket },
579
+ requestLog: loadRequestLog().length,
580
+ corrections: loadCorrections().length,
581
+ adversaryRuns: loadAdversaryRuns().length,
582
+ rules: loadRules().length,
583
+ feedbackPatterns: loadFeedbackPatterns().length,
584
+ correctionPhrases: loadPhrases().length,
585
+ tags: Object.keys(loadMemoryTags()).length,
586
+ };
587
+ }
588
+
589
+ // ============================================================================
590
+ // Exports
591
+ // ============================================================================
592
+
593
+ module.exports = {
594
+ queryMemory,
595
+ fetchByRef,
596
+ memoryStats,
597
+ addTag,
598
+ removeTag,
599
+ getTagsForRef,
600
+ loadMemoryTags,
601
+ // Source loaders (exposed for tests)
602
+ loadTasks,
603
+ loadRequestLog,
604
+ loadCorrections,
605
+ loadAdversaryRuns,
606
+ loadRules,
607
+ loadFeedbackPatterns,
608
+ loadPhrases,
609
+ loadAllMemory,
610
+ // Helpers
611
+ parseDuration,
612
+ normalizeTag,
613
+ KINDS,
614
+ MEMORY_TAGS_FILE,
615
+ };
616
+
617
+ // ============================================================================
618
+ // CLI
619
+ // ============================================================================
620
+
621
+ function parseArgs(argv) {
622
+ const args = { _: [] };
623
+ for (let i = 0; i < argv.length; i++) {
624
+ const a = argv[i];
625
+ if (a.startsWith('--')) {
626
+ const eq = a.indexOf('=');
627
+ if (eq >= 0) {
628
+ args[a.slice(2, eq)] = a.slice(eq + 1);
629
+ } else {
630
+ const key = a.slice(2);
631
+ const next = argv[i + 1];
632
+ if (next && !next.startsWith('--')) { args[key] = next; i++; }
633
+ else args[key] = true;
634
+ }
635
+ } else {
636
+ args._.push(a);
637
+ }
638
+ }
639
+ return args;
640
+ }
641
+
642
+ function printEntries(entries) {
643
+ if (!Array.isArray(entries)) {
644
+ console.log(JSON.stringify(entries, null, 2));
645
+ return;
646
+ }
647
+ if (entries.length === 0) {
648
+ console.log('(no results)');
649
+ return;
650
+ }
651
+ for (const e of entries) {
652
+ const tagStr = e.tags && e.tags.length ? ` ${e.tags.join(' ')}` : '';
653
+ const ts = e.timestamp ? ` @ ${e.timestamp.slice(0, 19).replace('T', ' ')}` : '';
654
+ const title = e.title ? ` — ${String(e.title).slice(0, 80)}` : '';
655
+ console.log(`${e.kind.padEnd(11)} ${e.ref.padEnd(40)}${ts}${title}${tagStr}`);
656
+ if (e.summary) console.log(` ${String(e.summary).slice(0, 160)}`);
657
+ }
658
+ console.log(`\n(${entries.length} result${entries.length === 1 ? '' : 's'})`);
659
+ }
660
+
661
+ if (require.main === module) {
662
+ const args = parseArgs(process.argv.slice(2));
663
+ const cmd = args._[0];
664
+
665
+ (async () => {
666
+ if (cmd === 'query' || cmd === undefined) {
667
+ const result = queryMemory({
668
+ since: args.since,
669
+ task: args.task,
670
+ kind: args.kind,
671
+ tag: args.tag,
672
+ limit: args.limit ? Number(args.limit) : 100,
673
+ });
674
+ if (result && result.error) {
675
+ console.error(`error: ${result.error}`);
676
+ if (result.valid) console.error(`valid kinds: ${result.valid.join(', ')}`);
677
+ process.exit(1);
678
+ }
679
+ if (args.json) console.log(JSON.stringify(result, null, 2));
680
+ else printEntries(result);
681
+ return;
682
+ }
683
+
684
+ if (cmd === 'fetch') {
685
+ const ref = args._[1];
686
+ const r = fetchByRef(ref);
687
+ if (!r.found) {
688
+ console.error(`error: ${r.reason}`);
689
+ if (r.suggestion) console.error(`hint: ${r.suggestion}`);
690
+ process.exit(1);
691
+ }
692
+ if (args.raw && r.entry.extra?.body) {
693
+ console.log(r.entry.extra.body);
694
+ return;
695
+ }
696
+ if (args.json) {
697
+ console.log(JSON.stringify(r, null, 2));
698
+ return;
699
+ }
700
+ console.log(`=== ${r.entry.kind} ${r.entry.ref} ===`);
701
+ if (r.entry.title) console.log(`title: ${r.entry.title}`);
702
+ if (r.entry.timestamp) console.log(`timestamp: ${r.entry.timestamp}`);
703
+ if (r.entry.summary) console.log(`summary: ${r.entry.summary}`);
704
+ if (r.entry.taskIds?.length) console.log(`tasks: ${r.entry.taskIds.join(', ')}`);
705
+ if (r.entry.tags?.length) console.log(`tags: ${r.entry.tags.join(' ')}`);
706
+ if (r.entry.source) console.log(`source: ${path.relative(PATHS.root || process.cwd(), r.entry.source)}`);
707
+ if (r.related && r.related.length) {
708
+ console.log(`\nRelated (${r.related.length}):`);
709
+ for (const e of r.related) {
710
+ console.log(` ${e.kind.padEnd(11)} ${e.ref} ${String(e.title || '').slice(0, 60)}`);
711
+ }
712
+ }
713
+ return;
714
+ }
715
+
716
+ if (cmd === 'stats') {
717
+ const s = memoryStats();
718
+ if (args.json) { console.log(JSON.stringify(s, null, 2)); return; }
719
+ console.log('=== flow memory stats ===');
720
+ console.log(`Tasks: ${s.tasks.total} total`);
721
+ for (const [b, c] of Object.entries(s.tasks.byBucket)) {
722
+ console.log(` ${b.padEnd(20)} ${c}`);
723
+ }
724
+ console.log(`Request log: ${s.requestLog}`);
725
+ console.log(`Corrections: ${s.corrections}`);
726
+ console.log(`Adversary runs: ${s.adversaryRuns}`);
727
+ console.log(`Rules: ${s.rules}`);
728
+ console.log(`Feedback patterns: ${s.feedbackPatterns}`);
729
+ console.log(`Correction phrases:${s.correctionPhrases}`);
730
+ console.log(`Tags (refs): ${s.tags}`);
731
+ return;
732
+ }
733
+
734
+ if (cmd === 'tag') {
735
+ const ref = args._[1];
736
+ const tag = args._[2];
737
+ if (!ref || !tag) { console.error('Usage: flow memory tag <ref> <#tag>'); process.exit(1); }
738
+ const r = await addTag(ref, tag);
739
+ console.log(JSON.stringify(r, null, 2));
740
+ return;
741
+ }
742
+
743
+ if (cmd === 'untag') {
744
+ const ref = args._[1];
745
+ const tag = args._[2];
746
+ if (!ref || !tag) { console.error('Usage: flow memory untag <ref> <#tag>'); process.exit(1); }
747
+ const r = await removeTag(ref, tag);
748
+ console.log(JSON.stringify(r, null, 2));
749
+ return;
750
+ }
751
+
752
+ console.log(`Usage:
753
+ flow memory query [--since=<dur>] [--task=<id>] [--kind=<k>] [--tag=<#t>] [--limit=N] [--json]
754
+ flow memory fetch <ref> [--raw] [--json]
755
+ flow memory stats [--json]
756
+ flow memory tag <ref> <#tag>
757
+ flow memory untag <ref> <#tag>
758
+
759
+ Durations: 30m, 2h, 7d, 2w
760
+ Kinds: ${Object.values(KINDS).join(', ')}
761
+ Refs: wf-XXXXXXXX | R-NNN | CORR-NNN | <adversary-run-filename>`);
762
+ process.exit(cmd ? 1 : 0);
763
+ })().catch((err) => {
764
+ console.error(`[memory] error: ${err.message}`);
765
+ process.exit(1);
766
+ });
767
+ }