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
@@ -19,9 +19,13 @@ const path = require('node:path');
19
19
  const {
20
20
  PATHS,
21
21
  safeJsonParse,
22
+ safeJsonParseString,
22
23
  writeJson,
23
- ensureDir
24
+ ensureDir,
25
+ withLock,
26
+ getTodayDate,
24
27
  } = require('./flow-utils');
28
+ const { getConfig } = require('./flow-config-loader');
25
29
 
26
30
  // ============================================================================
27
31
  // Constants
@@ -29,6 +33,7 @@ const {
29
33
 
30
34
  const PENDING_CORRECTIONS_FILE = 'pending-corrections.json';
31
35
  const SESSION_VIEW_FILE = 'session-corrections.json'; // IGR Stage 5 — session-scoped view
36
+ const PATTERNS_FILE = 'correction-patterns.json'; // wf-e6d65edf — hybrid keyword layer
32
37
  const MAX_PENDING_CORRECTIONS = 20;
33
38
  const MIN_CONFIDENCE_THRESHOLD = 70;
34
39
 
@@ -36,6 +41,21 @@ const MIN_CONFIDENCE_THRESHOLD = 70;
36
41
  const MIN_PROMPT_LENGTH = 8;
37
42
  const MAX_PROMPT_LENGTH = 1000;
38
43
 
44
+ // Hybrid layer (wf-e6d65edf) — defaults align with config.schema.json
45
+ const HYBRID_DEFAULTS = Object.freeze({
46
+ hybridEnabled: true,
47
+ learningEnabled: true,
48
+ learningThreshold: 85,
49
+ demotionThreshold: 0.5,
50
+ demotionMinHits: 10,
51
+ patternConfidenceFloor: 65,
52
+ patternConfidenceCap: 90,
53
+ ngramMinWords: 2,
54
+ ngramMaxWords: 4,
55
+ ngramMinChars: 8,
56
+ ngramMaxChars: 60,
57
+ });
58
+
39
59
  // IGR gate IDs that participate in missRate cross-reference (Story 0 telemetry)
40
60
  const CORRELATABLE_GATE_IDS = [
41
61
  'logic-adversary',
@@ -56,6 +76,258 @@ function getPendingCorrectionsPath() {
56
76
  return path.join(PATHS.state, PENDING_CORRECTIONS_FILE);
57
77
  }
58
78
 
79
+ function getPatternsPath() {
80
+ return path.join(PATHS.state, PATTERNS_FILE);
81
+ }
82
+
83
+ // ============================================================================
84
+ // Hybrid Layer (wf-e6d65edf) — keyword pre-classifier + self-learning
85
+ // ============================================================================
86
+
87
+ /**
88
+ * Read merged hybrid config, applying HYBRID_DEFAULTS for any missing key.
89
+ */
90
+ function getHybridConfig() {
91
+ let cfg = {};
92
+ try {
93
+ cfg = getConfig() || {};
94
+ } catch (_err) {
95
+ cfg = {};
96
+ }
97
+ const cd = cfg.correctionDetector || {};
98
+ const hybrid = cd.hybrid || {};
99
+ const learning = cd.learning || {};
100
+ return {
101
+ hybridEnabled: hybrid.enabled !== false ? (hybrid.enabled === true || HYBRID_DEFAULTS.hybridEnabled) : false,
102
+ learningEnabled: learning.enabled !== false ? (learning.enabled === true || HYBRID_DEFAULTS.learningEnabled) : false,
103
+ learningThreshold: Number.isFinite(learning.learningThreshold) ? learning.learningThreshold : HYBRID_DEFAULTS.learningThreshold,
104
+ demotionThreshold: Number.isFinite(learning.demotionThreshold) ? learning.demotionThreshold : HYBRID_DEFAULTS.demotionThreshold,
105
+ demotionMinHits: Number.isFinite(learning.demotionMinHits) ? learning.demotionMinHits : HYBRID_DEFAULTS.demotionMinHits,
106
+ };
107
+ }
108
+
109
+ // Module-level lazy cache. Cleared by _invalidatePatternCache() (tests) and on
110
+ // every successful upsert (so the same process sees its own writes).
111
+ let _patternCache = null;
112
+
113
+ /**
114
+ * Read the raw patterns array from disk. Returns [] when absent / malformed.
115
+ * Bypasses safeJsonParse (array-rooted JSON), with explicit prototype-pollution
116
+ * guard per security-patterns.md §2 (mirrors loadPendingCorrections pattern).
117
+ */
118
+ function readRawPatterns() {
119
+ const fs = require('node:fs');
120
+ const patternsPath = getPatternsPath();
121
+ try {
122
+ if (!fs.existsSync(patternsPath)) return [];
123
+ const raw = fs.readFileSync(patternsPath, 'utf-8');
124
+ // Use safeJsonParseString — it accepts array-rooted JSON and applies the
125
+ // prototype-pollution guard internally. (safeJsonParse rejects arrays.)
126
+ const parsed = safeJsonParseString(raw, null);
127
+ if (!Array.isArray(parsed)) {
128
+ if (process.env.DEBUG) {
129
+ console.error(`[correction-patterns] file is not an array — ignoring`);
130
+ }
131
+ return [];
132
+ }
133
+ return parsed;
134
+ } catch (_err) {
135
+ return [];
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Load the patterns array from disk, applying the demotion filter. Returns []
141
+ * when the file is absent / empty / malformed (graceful bootstrap).
142
+ */
143
+ function loadPatterns() {
144
+ if (_patternCache) return _patternCache;
145
+ const raw = readRawPatterns();
146
+ const cfg = getHybridConfig();
147
+ const valid = raw.filter(p => p && typeof p === 'object' && typeof p.phrase === 'string' && p.phrase.length > 0);
148
+ // Demotion: drop patterns that have proven unreliable.
149
+ const kept = valid.filter(p => {
150
+ const hits = Number(p.hits) || 0;
151
+ const fps = Number(p.falsePositives) || 0;
152
+ if (hits < cfg.demotionMinHits) return true;
153
+ return (fps / hits) <= cfg.demotionThreshold;
154
+ });
155
+ _patternCache = kept;
156
+ return _patternCache;
157
+ }
158
+
159
+ /**
160
+ * Test/maintenance helper.
161
+ */
162
+ function _invalidatePatternCache() {
163
+ _patternCache = null;
164
+ }
165
+
166
+ /**
167
+ * Find the first pattern whose phrase appears (case-insensitive) in the message.
168
+ * Returns the matched pattern object or null.
169
+ */
170
+ function findKeywordMatch(message) {
171
+ if (!message || typeof message !== 'string') return null;
172
+ const patterns = loadPatterns();
173
+ if (patterns.length === 0) return null;
174
+ const haystack = message.toLowerCase();
175
+ for (const p of patterns) {
176
+ const needle = String(p.phrase || '').toLowerCase();
177
+ if (needle && haystack.includes(needle)) return p;
178
+ }
179
+ return null;
180
+ }
181
+
182
+ /**
183
+ * Compute a confidence value for a pattern given its hit history. Linear ramp
184
+ * from `patternConfidenceFloor` (at 1 confirmedHit) to `patternConfidenceCap`
185
+ * (at 20+ confirmedHits). Falls back to floor when fields missing.
186
+ */
187
+ function patternConfidence(pattern) {
188
+ const ch = Math.max(1, Number(pattern?.confirmedHits) || 1);
189
+ const span = HYBRID_DEFAULTS.patternConfidenceCap - HYBRID_DEFAULTS.patternConfidenceFloor;
190
+ const ramp = HYBRID_DEFAULTS.patternConfidenceFloor + Math.round(span * Math.min(ch, 20) / 20);
191
+ return Math.min(HYBRID_DEFAULTS.patternConfidenceCap, ramp);
192
+ }
193
+
194
+ // Token classes we should drop when extracting candidate phrases.
195
+ // - pure numerics ("42", "1.5,2.0")
196
+ // - hex literals ("0xdeadbeef")
197
+ // - long hex blobs (likely IDs/hashes)
198
+ // - WogiFlow task IDs ("wf-...")
199
+ // - file paths (anything containing "/")
200
+ // - filenames with code extensions ("foo.js", "src/foo.ts")
201
+ // - URLs
202
+ const NGRAM_DROP_RE = /^([0-9.,]+|0x[0-9a-fA-F]+|[a-fA-F0-9]{8,}|wf-[a-fA-F0-9]+|[\w./_-]*\/[\w./_-]*|[\w._-]+\.(js|ts|jsx|tsx|json|md|sh|py|go|rs|java|rb|cs|cpp|h)|https?:\/\/\S+)$/;
203
+
204
+ /**
205
+ * Extract candidate phrases (n-grams) from a message that AI confirmed as a
206
+ * correction. Conservative filters keep generic / specific tokens out.
207
+ *
208
+ * @param {string} message
209
+ * @returns {string[]} unique normalized n-grams
210
+ */
211
+ function extractCandidatePhrases(message) {
212
+ if (!message || typeof message !== 'string') return [];
213
+ const cleaned = message
214
+ .toLowerCase()
215
+ .replace(/[\r\n]+/g, ' ')
216
+ .replace(/[^\p{L}\p{N}\s/.\-_]/gu, ' ');
217
+ const tokens = cleaned.split(/\s+/).filter(Boolean).filter(t => !NGRAM_DROP_RE.test(t));
218
+ if (tokens.length === 0) return [];
219
+ const out = new Set();
220
+ const { ngramMinWords, ngramMaxWords, ngramMinChars, ngramMaxChars } = HYBRID_DEFAULTS;
221
+ for (let n = ngramMinWords; n <= ngramMaxWords; n++) {
222
+ for (let i = 0; i + n <= tokens.length; i++) {
223
+ const phrase = tokens.slice(i, i + n).join(' ').trim();
224
+ if (phrase.length < ngramMinChars || phrase.length > ngramMaxChars) continue;
225
+ out.add(phrase);
226
+ }
227
+ }
228
+ return Array.from(out);
229
+ }
230
+
231
+ /**
232
+ * Upsert a list of candidate phrases into the patterns file. Increments
233
+ * confirmedHits when a phrase already exists. Writes are race-safe via withLock.
234
+ *
235
+ * @param {string[]} phrases
236
+ * @param {string} [source='ai-confirmation']
237
+ */
238
+ async function upsertPatterns(phrases, source = 'ai-confirmation') {
239
+ if (!Array.isArray(phrases) || phrases.length === 0) return { added: 0, updated: 0 };
240
+ const patternsPath = getPatternsPath();
241
+ ensureDir(path.dirname(patternsPath));
242
+ let added = 0;
243
+ let updated = 0;
244
+ await withLock(patternsPath, async () => {
245
+ const arr = readRawPatterns().slice();
246
+ const byPhrase = new Map(arr.map((p, i) => [String(p?.phrase || '').toLowerCase(), i]));
247
+ const now = new Date().toISOString();
248
+ for (const phrase of phrases) {
249
+ const key = phrase.toLowerCase();
250
+ if (byPhrase.has(key)) {
251
+ const idx = byPhrase.get(key);
252
+ arr[idx] = {
253
+ ...arr[idx],
254
+ confirmedHits: (Number(arr[idx].confirmedHits) || 0) + 1,
255
+ lastConfirmedAt: now,
256
+ };
257
+ updated += 1;
258
+ } else {
259
+ arr.push({
260
+ phrase,
261
+ language: 'unknown',
262
+ hits: 0,
263
+ confirmedHits: 1,
264
+ falsePositives: 0,
265
+ addedAt: now,
266
+ source,
267
+ });
268
+ added += 1;
269
+ }
270
+ }
271
+ writeJson(patternsPath, arr);
272
+ _patternCache = null; // bust cache so next loadPatterns sees the update
273
+ });
274
+ return { added, updated };
275
+ }
276
+
277
+ /**
278
+ * Increment counters for a matched pattern. Race-safe via withLock.
279
+ *
280
+ * @param {string} matchedPhrase
281
+ * @param {{ confirmed?: boolean, falsePositive?: boolean }} [opts]
282
+ */
283
+ async function recordPatternHit(matchedPhrase, opts = {}) {
284
+ if (!matchedPhrase || typeof matchedPhrase !== 'string') return;
285
+ const patternsPath = getPatternsPath();
286
+ // File absent → nothing to update (no-op, not an error).
287
+ const fs = require('node:fs');
288
+ if (!fs.existsSync(patternsPath)) return;
289
+ await withLock(patternsPath, async () => {
290
+ const arr = readRawPatterns().slice();
291
+ const key = matchedPhrase.toLowerCase();
292
+ const idx = arr.findIndex(p => String(p?.phrase || '').toLowerCase() === key);
293
+ if (idx < 0) return;
294
+ const next = { ...arr[idx] };
295
+ next.hits = (Number(next.hits) || 0) + 1;
296
+ if (opts.confirmed) next.confirmedHits = (Number(next.confirmedHits) || 0) + 1;
297
+ if (opts.falsePositive) next.falsePositives = (Number(next.falsePositives) || 0) + 1;
298
+ next.lastHitAt = new Date().toISOString();
299
+ arr[idx] = next;
300
+ writeJson(patternsPath, arr);
301
+ _patternCache = null;
302
+ });
303
+ }
304
+
305
+ /**
306
+ * Fire-and-forget — emit a telemetry event for the hybrid layer.
307
+ */
308
+ function recordHybridTelemetry(verdict, runCtx = {}) {
309
+ try {
310
+ const tel = require('./flow-gate-telemetry');
311
+ tel.recordGateEvent({
312
+ gateId: 'correction-keyword',
313
+ gateVersion: '1.0',
314
+ taskId: runCtx.taskId || null,
315
+ verdict,
316
+ findingCount: 0,
317
+ findingSummary: [],
318
+ durationMs: runCtx.durationMs,
319
+ metadata: {
320
+ method: runCtx.method || null,
321
+ matchedPattern: runCtx.matchedPattern || null,
322
+ learningTriggered: runCtx.learningTriggered || false,
323
+ confidence: runCtx.confidence ?? null,
324
+ },
325
+ });
326
+ } catch (_err) {
327
+ // Telemetry failure must never break the detector.
328
+ }
329
+ }
330
+
59
331
  // ============================================================================
60
332
  // AI-Based Detection (Haiku — language-agnostic)
61
333
  // ============================================================================
@@ -70,6 +342,7 @@ function getPendingCorrectionsPath() {
70
342
  * @returns {Promise<Object>} Detection result
71
343
  */
72
344
  async function detectCorrection(userMessage, previousContext = '') {
345
+ const start = Date.now();
73
346
  // Pre-filter: skip messages unlikely to be corrections (length-based only, language-agnostic)
74
347
  if (!userMessage || typeof userMessage !== 'string') {
75
348
  return { isCorrection: false, confidence: 0, method: 'skipped', reason: 'invalid-input' };
@@ -80,6 +353,32 @@ async function detectCorrection(userMessage, previousContext = '') {
80
353
  return { isCorrection: false, confidence: 0, method: 'skipped', reason: 'length-filter' };
81
354
  }
82
355
 
356
+ // Layer 1 (wf-e6d65edf) — keyword pre-classifier. Skips Haiku entirely on a hit.
357
+ const hybridCfg = getHybridConfig();
358
+ if (hybridCfg.hybridEnabled) {
359
+ const matched = findKeywordMatch(trimmed);
360
+ if (matched) {
361
+ const conf = patternConfidence(matched);
362
+ // Increment hits asynchronously — don't block the return.
363
+ recordPatternHit(matched.phrase, { confirmed: false }).catch(() => {});
364
+ recordHybridTelemetry('PASS', {
365
+ method: 'keyword',
366
+ matchedPattern: matched.phrase,
367
+ confidence: conf,
368
+ durationMs: Date.now() - start,
369
+ });
370
+ return {
371
+ isCorrection: true,
372
+ confidence: conf,
373
+ correctionType: 'behavior',
374
+ whatWasWrong: null,
375
+ whatUserWants: null,
376
+ method: 'keyword',
377
+ matchedPattern: matched.phrase,
378
+ };
379
+ }
380
+ }
381
+
83
382
  // Check if API key is available
84
383
  const apiKey = process.env.ANTHROPIC_API_KEY;
85
384
  if (!apiKey) {
@@ -157,7 +456,7 @@ Respond with JSON only (no markdown, no explanation):
157
456
  return { isCorrection: false, confidence: 0, method: 'ai', reason: 'invalid-confidence' };
158
457
  }
159
458
 
160
- return {
459
+ const aiResult = {
161
460
  isCorrection: result.isCorrection,
162
461
  confidence: result.confidence,
163
462
  correctionType: result.correctionType || null,
@@ -165,6 +464,34 @@ Respond with JSON only (no markdown, no explanation):
165
464
  whatUserWants: result.whatUserWants || null,
166
465
  method: 'ai'
167
466
  };
467
+
468
+ // Layer 3 (wf-e6d65edf) — back-propagate confirmed high-confidence corrections
469
+ // into the keyword-pattern store. Fire-and-forget (background-safe).
470
+ let learningTriggered = false;
471
+ if (
472
+ hybridCfg.learningEnabled &&
473
+ aiResult.isCorrection &&
474
+ aiResult.confidence >= hybridCfg.learningThreshold
475
+ ) {
476
+ const candidates = extractCandidatePhrases(trimmed);
477
+ if (candidates.length > 0) {
478
+ learningTriggered = true;
479
+ upsertPatterns(candidates, 'ai-confirmation').catch((err) => {
480
+ if (process.env.DEBUG) {
481
+ console.error(`[correction-patterns] upsert failed: ${err.message}`);
482
+ }
483
+ });
484
+ }
485
+ }
486
+
487
+ recordHybridTelemetry(aiResult.isCorrection ? 'PASS' : 'SKIP', {
488
+ method: 'ai',
489
+ confidence: aiResult.confidence,
490
+ learningTriggered,
491
+ durationMs: Date.now() - start,
492
+ });
493
+
494
+ return aiResult;
168
495
  } catch (err) {
169
496
  if (process.env.DEBUG) {
170
497
  console.error(`[DEBUG] AI correction detection failed: ${err.message}`);
@@ -999,7 +1326,21 @@ module.exports = {
999
1326
 
1000
1327
  // Paths
1001
1328
  getPendingCorrectionsPath,
1329
+ getPatternsPath,
1330
+
1331
+ // Hybrid layer (wf-e6d65edf)
1332
+ loadPatterns,
1333
+ findKeywordMatch,
1334
+ patternConfidence,
1335
+ extractCandidatePhrases,
1336
+ upsertPatterns,
1337
+ recordPatternHit,
1338
+ getHybridConfig,
1002
1339
 
1003
1340
  // Constants
1004
- MIN_CONFIDENCE_THRESHOLD
1341
+ MIN_CONFIDENCE_THRESHOLD,
1342
+ HYBRID_DEFAULTS,
1343
+
1344
+ // Test helpers
1345
+ _invalidatePatternCache,
1005
1346
  };
@@ -23,7 +23,7 @@
23
23
 
24
24
  const fs = require('node:fs');
25
25
  const path = require('node:path');
26
- const { getProjectRoot, colors, getConfig, PATHS } = require('./flow-utils');
26
+ const { getProjectRoot, colors, getConfig, PATHS, getTodayDate } = require('./flow-utils');
27
27
 
28
28
  const PATTERNS_FILE = path.join(PATHS.workflow, 'damage-control.yaml');
29
29
 
@@ -21,6 +21,7 @@
21
21
  const fs = require('node:fs');
22
22
  const path = require('node:path');
23
23
  const { safeJsonParse } = require('./flow-io');
24
+ const { getTodayDate } = require('./flow-output');
24
25
 
25
26
  // ============================================================
26
27
  // Section Parser
@@ -33,6 +33,22 @@ try {
33
33
  };
34
34
  }
35
35
 
36
+ // Capture-at-task-boundary gate (Story wf-a3cc5f2a, epic-episodic-memory).
37
+ // Detects durable conclusions in a completed task and blocks close until they
38
+ // are captured to the correct state file. Ships OFF (config.externalMemory.capture.enabled).
39
+ // Synchronous interface — subprocess spawnSync inside for the AI classifier call.
40
+ let captureGate;
41
+ try {
42
+ captureGate = require('./flow-capture-gate').captureGate;
43
+ } catch (_err) {
44
+ captureGate = (ctx) => {
45
+ if (ctx?.color) {
46
+ console.log(` ${ctx.color('yellow', '\u25CB')} captureGate (module unavailable — skipping)`);
47
+ }
48
+ return { passed: true };
49
+ };
50
+ }
51
+
36
52
  // v2.1 task enforcement
37
53
  const { canExitLoop, getActiveLoop } = require('./flow-task-enforcer');
38
54
 
@@ -906,6 +922,8 @@ const GATE_REGISTRY = {
906
922
  verificationProof: verificationProofGate,
907
923
  // IGR Stage 6 — richer tier-aware successor to verificationProof; coexists.
908
924
  completionTruth: completionTruthGate,
925
+ // Capture-at-task-boundary enforcement (epic-episodic-memory wf-a3cc5f2a).
926
+ captureGate: captureGate,
909
927
  // Workspace gates (conditional — auto-skip when not in workspace)
910
928
  workspaceCompliance: workspaceGate,
911
929
  };
@@ -937,6 +955,7 @@ function runGate(gateName, ctx) {
937
955
  const SELF_INSTRUMENTED_GATES = new Set([
938
956
  'standardsCompliance', // flow-standards-gate.js emits gateId: standards-gate
939
957
  'completionTruth', // flow-completion-truth-gate.js emits gateId: completion-truth-gate
958
+ 'captureGate', // flow-capture-gate.js emits gateId: capture-gate
940
959
  ]);
941
960
  if (!SELF_INSTRUMENTED_GATES.has(gateName)) {
942
961
  try {
@@ -995,6 +1014,7 @@ module.exports = {
995
1014
  verificationGate,
996
1015
  testDiscoveryGate,
997
1016
  verificationProofGate,
1017
+ captureGate,
998
1018
  workspaceGate,
999
1019
  unknownGate,
1000
1020
  };
@@ -99,8 +99,8 @@ function printErrorRecoveryAnalysis(gateResult, config) {
99
99
  }
100
100
  }
101
101
  console.log('');
102
- } catch (analysisErr) {
103
- if (process.env.DEBUG) console.error(`[DEBUG] Error analysis: ${analysisErr.message}`);
102
+ } catch (err) {
103
+ if (process.env.DEBUG) console.error(`[DEBUG] Error analysis: ${err.message}`);
104
104
  }
105
105
  }
106
106
  }
@@ -15,7 +15,7 @@ const execSync = (...args) => _cp.execSync(...args);
15
15
  const fs = require('node:fs');
16
16
  const path = require('node:path');
17
17
  const _flowUtils = require('./flow-utils');
18
- const { PATHS, moveTaskAsync, findTask, writeJson, color, success, warn, error } = _flowUtils;
18
+ const { PATHS, moveTaskAsync, findTask, writeJson, color, success, warn, error, getTodayDate } = _flowUtils;
19
19
  // Indirect access for testable functions — tests can swap _io members
20
20
  const _io = {
21
21
  getConfig: _flowUtils.getConfig,
@@ -695,15 +695,15 @@ async function main() {
695
695
  try {
696
696
  const { writeToFeedbackPatterns: writeFP } = require('./flow-learning-orchestrator');
697
697
  writeFP({ content: newContent, entryText: 'high-refinement-request', caller: 'flow-done/highRefinementFlag' }).catch(() => {});
698
- } catch (_orcErr) { /* fallback: already computed newContent but orchestrator unavailable */ }
698
+ } catch (_err) { /* fallback: already computed newContent but orchestrator unavailable */ }
699
699
  }
700
700
  }
701
701
  }
702
702
 
703
703
  warn(`High-refinement pattern flagged (${learningResult.refinementCount} clarifications needed)`);
704
704
  console.log(color('dim', 'Consider adding clearer guidance to decisions.md'));
705
- } catch (flagErr) {
706
- if (process.env.DEBUG) console.error(`[DEBUG] High-refinement flagging: ${flagErr.message}`);
705
+ } catch (err) {
706
+ if (process.env.DEBUG) console.error(`[DEBUG] High-refinement flagging: ${err.message}`);
707
707
  }
708
708
  }
709
709
 
@@ -28,7 +28,8 @@ const {
28
28
  info,
29
29
  findAllWithParent,
30
30
  normalizeTask,
31
- generateEpicId
31
+ generateEpicId,
32
+ getReadyData,
32
33
  } = require('./flow-utils');
33
34
 
34
35
  // ============================================================
@@ -78,16 +79,9 @@ function saveEpicsState(state) {
78
79
  * Load ready.json for task data
79
80
  * @returns {Object} Ready data
80
81
  */
81
- function loadReadyData() {
82
- if (!fs.existsSync(PATHS.ready)) {
83
- return { ready: [], inProgress: [], recentlyCompleted: [] };
84
- }
85
- try {
86
- return readJson(PATHS.ready) || { ready: [], inProgress: [], recentlyCompleted: [] };
87
- } catch (_err) {
88
- return { ready: [], inProgress: [], recentlyCompleted: [] };
89
- }
90
- }
82
+ // loadReadyData replaced by canonical getReadyData from flow-utils (wf-7072d3ac / dup-007).
83
+ // Kept as a thin shim for backward-compat of the module export at L812.
84
+ function loadReadyData() { return getReadyData(); }
91
85
 
92
86
  // ============================================================
93
87
  // Epic File Operations