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,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
|
+
}
|