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
|
@@ -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
|
-
|
|
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 (
|
|
103
|
-
if (process.env.DEBUG) console.error(`[DEBUG] Error analysis: ${
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (process.env.DEBUG) console.error(`[DEBUG] Error analysis: ${err.message}`);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
}
|
package/scripts/flow-done.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
706
|
-
if (process.env.DEBUG) console.error(`[DEBUG] High-refinement flagging: ${
|
|
705
|
+
} catch (err) {
|
|
706
|
+
if (process.env.DEBUG) console.error(`[DEBUG] High-refinement flagging: ${err.message}`);
|
|
707
707
|
}
|
|
708
708
|
}
|
|
709
709
|
|
package/scripts/flow-epics.js
CHANGED
|
@@ -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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|