moflo 4.10.25-rc.1 → 4.10.26
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/helpers/simplify-classify.cjs +172 -29
- package/.claude/skills/flo-simplify/SKILL.md +26 -5
- package/.claude/skills/meditate/SKILL.md +2 -2
- package/bin/lib/meditate.mjs +55 -6
- package/bin/meditate-distill.mjs +4 -0
- package/bin/simplify-classify.cjs +134 -12
- package/dist/src/cli/memory/bridge-loader.js +42 -0
- package/dist/src/cli/memory/embedding-model.js +157 -0
- package/dist/src/cli/memory/entries-read.js +380 -0
- package/dist/src/cli/memory/entries-shared.js +73 -0
- package/dist/src/cli/memory/entries-write.js +394 -0
- package/dist/src/cli/memory/hnsw-singleton.js +242 -0
- package/dist/src/cli/memory/init.js +367 -0
- package/dist/src/cli/memory/learnings-overview.js +156 -0
- package/dist/src/cli/memory/memory-initializer.js +37 -2257
- package/dist/src/cli/memory/quantization.js +221 -0
- package/dist/src/cli/memory/schema.js +382 -0
- package/dist/src/cli/memory/verify.js +178 -0
- package/dist/src/cli/services/daemon-dashboard.js +108 -7
- package/dist/src/cli/services/daemon-lock.js +25 -8
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -7,17 +7,28 @@
|
|
|
7
7
|
* deterministic and unit-testable instead of a prose decision Claude makes
|
|
8
8
|
* over and over per run.
|
|
9
9
|
*
|
|
10
|
-
* Rule: default to single-agent Sonnet review.
|
|
11
|
-
* fan-out when diff signals
|
|
12
|
-
*
|
|
10
|
+
* Rule: default to single-agent Sonnet review. Escalate to a 3-agent Sonnet
|
|
11
|
+
* fan-out (NORMAL) when diff signals warrant it, and to a 3-agent Opus fan-out
|
|
12
|
+
* (DEEP) only for genuinely architectural diffs — ordinary review is
|
|
13
|
+
* breadth-bound (Sonnet wins), but architectural review is depth-bound (Opus
|
|
14
|
+
* earns its cost). The most extreme diffs additionally suggest handing off to
|
|
15
|
+
* Claude Code's built-in /simplify via escalate.suggested. (#1222 follow-up)
|
|
16
|
+
*
|
|
17
|
+
* Opus escalation is gated on genuine new-logic evidence, NEVER raw volume:
|
|
18
|
+
* TS/JS uses net-new declarations; other languages use net-new lines
|
|
19
|
+
* (added − deleted, aggregate → relocation/churn cancels out). Noise
|
|
20
|
+
* (lockfiles, snapshots, generated/vendored) and docs/data never count toward
|
|
21
|
+
* the opus bar. So a lockfile bump, a reformatting sweep, or a big rename can
|
|
22
|
+
* never reach Opus.
|
|
13
23
|
*
|
|
14
24
|
* Outputs JSON:
|
|
15
25
|
* {
|
|
16
|
-
* "tier": "TRIVIAL" | "SMALL" | "NORMAL",
|
|
17
|
-
* "model": "sonnet",
|
|
26
|
+
* "tier": "TRIVIAL" | "SMALL" | "NORMAL" | "DEEP",
|
|
27
|
+
* "model": "sonnet" | "haiku" | "opus",
|
|
18
28
|
* "agentCount": 0 | 1 | 3,
|
|
29
|
+
* "escalate": { suggested: bool, target: "builtin-simplify"|null, reason: string|null },
|
|
19
30
|
* "reasoning": [string, ...],
|
|
20
|
-
* "stats": { added, deleted, fileCount, declAdded, declRemoved, ... }
|
|
31
|
+
* "stats": { added, deleted, fileCount, declAdded, declRemoved, tsjsLOC, tsjsNetDecls, otherNetAdded, ... }
|
|
21
32
|
* }
|
|
22
33
|
*
|
|
23
34
|
* Usage:
|
|
@@ -45,34 +56,97 @@ const SECURITY_PATHS = [
|
|
|
45
56
|
/(?:^|[\\\/])\.claude[\\\/]helpers[\\\/]gate/i,
|
|
46
57
|
];
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
// ── File-family classification for the opus-escalation gate (#1222) ───────────
|
|
60
|
+
// Opus is gated on genuine new-logic evidence, never raw volume — so generated
|
|
61
|
+
// noise and docs/data are stripped before measuring, and TS/JS (where the decl
|
|
62
|
+
// parser is accurate) is measured by net-new declarations while other languages
|
|
63
|
+
// fall back to net-new lines.
|
|
64
|
+
|
|
65
|
+
// Generated / vendored noise — inflates LOC without adding reviewable logic.
|
|
66
|
+
const NOISE_FILE = [
|
|
67
|
+
/(?:^|[\\\/])(?:package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|composer\.lock|Cargo\.lock|poetry\.lock|Gemfile\.lock|go\.sum)$/i,
|
|
68
|
+
/\.snap$/i,
|
|
69
|
+
/(?:^|[\\\/])__snapshots__[\\\/]/i,
|
|
70
|
+
/(?:^|[\\\/])(?:dist|build|out|coverage|node_modules)[\\\/]/i,
|
|
71
|
+
/\.min\.(?:js|css)$/i,
|
|
72
|
+
/\.(?:map|bundle\.js)$/i,
|
|
73
|
+
/(?:^|[\\\/])vendor[\\\/]/i,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// Docs / data — reviewed at normal tiers, but never counted toward the opus bar.
|
|
77
|
+
const DOCDATA_FILE = /\.(?:md|mdx|markdown|txt|rst|json|json5|ya?ml|toml|ini|cfg|conf|xml|csv|tsv|svg|properties|env)$/i;
|
|
78
|
+
|
|
79
|
+
// TS/JS source — the declaration parser is accurate here, so the opus gate uses
|
|
80
|
+
// net-new declarations for these files.
|
|
81
|
+
const TSJS_FILE = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/i;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Bucket a file path into the family the opus-escalation gate cares about:
|
|
85
|
+
* 'noise' / 'docdata' (both excluded from the opus bar), 'tsjs' (decl-gated),
|
|
86
|
+
* or 'othercode' (net-line-gated). Pure function over the path string.
|
|
87
|
+
*/
|
|
88
|
+
function fileFamily(filename) {
|
|
89
|
+
if (NOISE_FILE.some((rx) => rx.test(filename))) return 'noise';
|
|
90
|
+
if (TSJS_FILE.test(filename)) return 'tsjs';
|
|
91
|
+
if (DOCDATA_FILE.test(filename)) return 'docdata';
|
|
92
|
+
return 'othercode';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Default "no escalation" marker attached to every non-DEEP decision so the
|
|
96
|
+
// output shape (decision.escalate) is stable for every consumer.
|
|
97
|
+
function noEscalate() {
|
|
98
|
+
return { suggested: false, target: null, reason: null };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function safeExec(cmd, opts) {
|
|
102
|
+
try {
|
|
103
|
+
return execSync(cmd, {
|
|
104
|
+
encoding: 'utf-8',
|
|
105
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
106
|
+
...(opts && opts.cwd ? { cwd: opts.cwd } : {}),
|
|
107
|
+
});
|
|
108
|
+
} catch { return ''; }
|
|
51
109
|
}
|
|
52
110
|
|
|
53
111
|
// Detect the consumer's default branch. Hardcoding 'main' silently miscalibrates
|
|
54
112
|
// classification on repos that use 'master', 'develop', etc. — empty diff →
|
|
55
113
|
// TRIVIAL → gate stamps clean without any real review.
|
|
56
114
|
let _cachedDefaultBranch = null;
|
|
57
|
-
function detectDefaultBranch() {
|
|
58
|
-
|
|
115
|
+
function detectDefaultBranch(cwd) {
|
|
116
|
+
// Cache by cwd so tests probing multiple repos in-process don't return a
|
|
117
|
+
// single stale value; CLI use passes no cwd and benefits from the cache.
|
|
118
|
+
if (cwd === undefined && _cachedDefaultBranch !== null) return _cachedDefaultBranch;
|
|
119
|
+
const opts = cwd ? { cwd } : undefined;
|
|
59
120
|
|
|
60
121
|
// Preferred: origin/HEAD points to whatever the remote considers default.
|
|
61
|
-
const symbolic = safeExec('git symbolic-ref --short refs/remotes/origin/HEAD').trim();
|
|
62
|
-
if (symbolic.startsWith('origin/'))
|
|
122
|
+
const symbolic = safeExec('git symbolic-ref --short refs/remotes/origin/HEAD', opts).trim();
|
|
123
|
+
if (symbolic.startsWith('origin/')) {
|
|
124
|
+
const v = symbolic.slice('origin/'.length);
|
|
125
|
+
if (cwd === undefined) _cachedDefaultBranch = v;
|
|
126
|
+
return v;
|
|
127
|
+
}
|
|
63
128
|
|
|
64
129
|
// Fallback: local init.defaultBranch (set by `git init -b <name>` or config).
|
|
65
|
-
const configured = safeExec('git config --get init.defaultBranch').trim();
|
|
66
|
-
if (configured)
|
|
130
|
+
const configured = safeExec('git config --get init.defaultBranch', opts).trim();
|
|
131
|
+
if (configured) {
|
|
132
|
+
if (cwd === undefined) _cachedDefaultBranch = configured;
|
|
133
|
+
return configured;
|
|
134
|
+
}
|
|
67
135
|
|
|
68
136
|
// Last resort: 'main' (most common modern default).
|
|
69
|
-
|
|
137
|
+
if (cwd === undefined) _cachedDefaultBranch = 'main';
|
|
138
|
+
return 'main';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _resetCacheForTest() {
|
|
142
|
+
_cachedDefaultBranch = null;
|
|
70
143
|
}
|
|
71
144
|
|
|
72
|
-
function readDiffFromGit(base) {
|
|
145
|
+
function readDiffFromGit(base, cwd) {
|
|
146
|
+
const opts = cwd ? { cwd } : undefined;
|
|
73
147
|
// Combined diff: committed-since-base + working-tree
|
|
74
|
-
const committed = safeExec(`git diff ${base}...HEAD
|
|
75
|
-
const working = safeExec('git diff HEAD');
|
|
148
|
+
const committed = safeExec(`git diff ${base}...HEAD`, opts);
|
|
149
|
+
const working = safeExec('git diff HEAD', opts);
|
|
76
150
|
return committed + (working ? '\n' + working : '');
|
|
77
151
|
}
|
|
78
152
|
|
|
@@ -125,6 +199,11 @@ function parseDiff(diff) {
|
|
|
125
199
|
let added = 0, deleted = 0, declAdded = 0, declRemoved = 0;
|
|
126
200
|
let newFiles = 0, renamedFiles = 0;
|
|
127
201
|
let securityHit = false;
|
|
202
|
+
// Family-segregated signals for the opus-escalation gate (#1222). Noise +
|
|
203
|
+
// docs/data still contribute to the global totals (so existing
|
|
204
|
+
// TRIVIAL/SMALL/NORMAL routing is unchanged) but never to the opus bar.
|
|
205
|
+
let tsjsAdded = 0, tsjsDeleted = 0, tsjsDeclAdded = 0, tsjsDeclRemoved = 0;
|
|
206
|
+
let otherAdded = 0, otherDeleted = 0;
|
|
128
207
|
for (const f of files.values()) {
|
|
129
208
|
added += f.added;
|
|
130
209
|
deleted += f.deleted;
|
|
@@ -133,6 +212,14 @@ function parseDiff(diff) {
|
|
|
133
212
|
if (f.isNew) newFiles++;
|
|
134
213
|
if (f.isRenamed) renamedFiles++;
|
|
135
214
|
if (SECURITY_PATHS.some(rx => rx.test(f.filename))) securityHit = true;
|
|
215
|
+
|
|
216
|
+
const fam = fileFamily(f.filename);
|
|
217
|
+
if (fam === 'tsjs') {
|
|
218
|
+
tsjsAdded += f.added; tsjsDeleted += f.deleted;
|
|
219
|
+
tsjsDeclAdded += f.declAdded; tsjsDeclRemoved += f.declRemoved;
|
|
220
|
+
} else if (fam === 'othercode') {
|
|
221
|
+
otherAdded += f.added; otherDeleted += f.deleted;
|
|
222
|
+
}
|
|
136
223
|
}
|
|
137
224
|
|
|
138
225
|
return {
|
|
@@ -141,6 +228,13 @@ function parseDiff(diff) {
|
|
|
141
228
|
fileCount: files.size,
|
|
142
229
|
newFiles, renamedFiles,
|
|
143
230
|
securityHit,
|
|
231
|
+
// Opus-gate signals (#1222): net-new declarations for TS/JS, net-new lines
|
|
232
|
+
// for other code. Aggregate net → relocation/churn cancels to ~0.
|
|
233
|
+
tsjsLOC: tsjsAdded + tsjsDeleted,
|
|
234
|
+
tsjsNetDecls: tsjsDeclAdded - tsjsDeclRemoved,
|
|
235
|
+
tsjsDeclAdded,
|
|
236
|
+
otherNetAdded: otherAdded - otherDeleted,
|
|
237
|
+
otherLOC: otherAdded + otherDeleted,
|
|
144
238
|
files: [...files.keys()],
|
|
145
239
|
};
|
|
146
240
|
}
|
|
@@ -154,13 +248,13 @@ function decide(stats) {
|
|
|
154
248
|
const totalChange = stats.added + stats.deleted;
|
|
155
249
|
|
|
156
250
|
if (totalChange === 0) {
|
|
157
|
-
return { tier: 'TRIVIAL', model: 'sonnet', agentCount: 0, reasoning: ['empty diff — nothing to review'], stats };
|
|
251
|
+
return { tier: 'TRIVIAL', model: 'sonnet', agentCount: 0, escalate: noEscalate(), reasoning: ['empty diff — nothing to review'], stats };
|
|
158
252
|
}
|
|
159
253
|
|
|
160
254
|
// TRIVIAL: tiny diff, no declarations changed
|
|
161
255
|
if (totalChange <= 10 && stats.fileCount <= 1 && stats.netDecls === 0 && stats.declAdded === 0 && stats.declRemoved === 0) {
|
|
162
256
|
reasoning.push(`≤10 LOC in 1 file with no declaration changes`);
|
|
163
|
-
return { tier: 'TRIVIAL', model: 'sonnet', agentCount: 0, reasoning, stats };
|
|
257
|
+
return { tier: 'TRIVIAL', model: 'sonnet', agentCount: 0, escalate: noEscalate(), reasoning, stats };
|
|
164
258
|
}
|
|
165
259
|
|
|
166
260
|
// Mechanical relocation detection — the #906 case.
|
|
@@ -182,11 +276,60 @@ function decide(stats) {
|
|
|
182
276
|
// Haiku is sufficient for mechanical moves: code already existed and worked,
|
|
183
277
|
// so review reduces to copy-paste-divergence / dead-after-move pattern checks
|
|
184
278
|
// — exactly haiku's strength. ~5x cheaper than sonnet on relocation-shape diffs.
|
|
185
|
-
return { tier: 'SMALL', model: 'haiku', agentCount: 1, reasoning, stats };
|
|
279
|
+
return { tier: 'SMALL', model: 'haiku', agentCount: 1, escalate: noEscalate(), reasoning, stats };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Architectural escalation to Opus (#1222) ────────────────────────────────
|
|
283
|
+
// Two rungs above NORMAL, BOTH gated on genuine new-logic evidence so volume
|
|
284
|
+
// alone never escalates: TS/JS by net-new declarations, other languages by
|
|
285
|
+
// net-new lines. The relocation guard above already returned SMALL, so
|
|
286
|
+
// mechanical moves never reach here, and noise/docs/data were stripped from
|
|
287
|
+
// these signals in parseDiff.
|
|
288
|
+
//
|
|
289
|
+
// • DEEP → runs a 3-agent Opus pass automatically (depth-bound
|
|
290
|
+
// architectural review; ordinary review stays Sonnet).
|
|
291
|
+
// • DEEP + handoff → also suggests Claude Code's built-in /simplify for the
|
|
292
|
+
// most extreme diffs (escalate.suggested = true). The
|
|
293
|
+
// Opus pass still runs as the floor; the handoff is a
|
|
294
|
+
// prompt, not an auto-switch.
|
|
295
|
+
const tsjsLOC = stats.tsjsLOC || 0;
|
|
296
|
+
const tsjsNetDecls = stats.tsjsNetDecls || 0;
|
|
297
|
+
const tsjsDeclAdded = stats.tsjsDeclAdded || 0;
|
|
298
|
+
const otherNetAdded = stats.otherNetAdded || 0;
|
|
299
|
+
|
|
300
|
+
// The new-subsystem triggers count TS/JS declarations only (tsjs-scoped, not
|
|
301
|
+
// global) so a docs/data file with a fenced `export function` code sample
|
|
302
|
+
// can't leak into the opus gate — consistent with the net-new-logic contract.
|
|
303
|
+
const handoffTriggers = [];
|
|
304
|
+
if (tsjsLOC > 4000 && tsjsNetDecls >= 25) handoffTriggers.push(`${tsjsLOC} LOC of TS/JS with ${tsjsNetDecls} net-new declarations`);
|
|
305
|
+
if (otherNetAdded > 3000) handoffTriggers.push(`${otherNetAdded} net-new lines of non-TS/JS source`);
|
|
306
|
+
if (stats.newFiles >= 10 && tsjsDeclAdded >= 30 && tsjsNetDecls >= 20) handoffTriggers.push(`${stats.newFiles} new files with ${tsjsDeclAdded} new TS/JS declarations`);
|
|
307
|
+
|
|
308
|
+
if (handoffTriggers.length > 0) {
|
|
309
|
+
return {
|
|
310
|
+
tier: 'DEEP', model: 'opus', agentCount: 3,
|
|
311
|
+
escalate: { suggested: true, target: 'builtin-simplify', reason: handoffTriggers.join('; ') },
|
|
312
|
+
reasoning: handoffTriggers, stats,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const deepTriggers = [];
|
|
317
|
+
if (tsjsLOC > 1500 && tsjsNetDecls >= 10) deepTriggers.push(`${tsjsLOC} LOC of TS/JS with ${tsjsNetDecls} net-new declarations`);
|
|
318
|
+
if (otherNetAdded > 1200) deepTriggers.push(`${otherNetAdded} net-new lines of non-TS/JS source`);
|
|
319
|
+
if (stats.newFiles >= 5 && tsjsDeclAdded >= 15 && tsjsNetDecls >= 10) deepTriggers.push(`${stats.newFiles} new files with ${tsjsDeclAdded} new TS/JS declarations`);
|
|
320
|
+
if (stats.securityHit && stats.netDecls >= 8) deepTriggers.push(`security-sensitive path with ${stats.netDecls} net-new declarations`);
|
|
321
|
+
|
|
322
|
+
if (deepTriggers.length > 0) {
|
|
323
|
+
return {
|
|
324
|
+
tier: 'DEEP', model: 'opus', agentCount: 3,
|
|
325
|
+
escalate: noEscalate(),
|
|
326
|
+
reasoning: deepTriggers, stats,
|
|
327
|
+
};
|
|
186
328
|
}
|
|
187
329
|
|
|
188
330
|
// Escalation triggers — any one trips NORMAL (3 agents).
|
|
189
|
-
//
|
|
331
|
+
// Sonnet — ordinary cross-cutting review is breadth-bound, so 3 Sonnet agents
|
|
332
|
+
// are the right tool; Opus is reserved for the DEEP (architectural) tier above.
|
|
190
333
|
const triggers = [];
|
|
191
334
|
if (totalChange > 500) triggers.push(`>500 LOC changed (${totalChange})`);
|
|
192
335
|
if (stats.fileCount >= 5 && stats.netDecls >= 3) triggers.push(`${stats.fileCount} files with ${stats.netDecls} net new declarations`);
|
|
@@ -194,21 +337,21 @@ function decide(stats) {
|
|
|
194
337
|
if (stats.newFiles >= 3 && stats.declAdded >= 5) triggers.push(`${stats.newFiles} new files with ${stats.declAdded} new declarations`);
|
|
195
338
|
|
|
196
339
|
if (triggers.length > 0) {
|
|
197
|
-
return { tier: 'NORMAL', model: 'sonnet', agentCount: 3, reasoning: triggers, stats };
|
|
340
|
+
return { tier: 'NORMAL', model: 'sonnet', agentCount: 3, escalate: noEscalate(), reasoning: triggers, stats };
|
|
198
341
|
}
|
|
199
342
|
|
|
200
343
|
// Default: SMALL — single sonnet agent
|
|
201
344
|
reasoning.push(`small/medium diff: ${totalChange} LOC across ${stats.fileCount} file(s), +${stats.declAdded}/-${stats.declRemoved} decls`);
|
|
202
|
-
return { tier: 'SMALL', model: 'sonnet', agentCount: 1, reasoning, stats };
|
|
345
|
+
return { tier: 'SMALL', model: 'sonnet', agentCount: 1, escalate: noEscalate(), reasoning, stats };
|
|
203
346
|
}
|
|
204
347
|
|
|
205
348
|
function classifyDiff(diffText) {
|
|
206
349
|
return decide(parseDiff(diffText));
|
|
207
350
|
}
|
|
208
351
|
|
|
209
|
-
function classifyFromGit(base) {
|
|
210
|
-
const resolved = base || detectDefaultBranch();
|
|
211
|
-
return classifyDiff(readDiffFromGit(resolved));
|
|
352
|
+
function classifyFromGit(base, cwd) {
|
|
353
|
+
const resolved = base || detectDefaultBranch(cwd);
|
|
354
|
+
return classifyDiff(readDiffFromGit(resolved, cwd));
|
|
212
355
|
}
|
|
213
356
|
|
|
214
357
|
if (require.main === module) {
|
|
@@ -232,4 +375,4 @@ if (require.main === module) {
|
|
|
232
375
|
}
|
|
233
376
|
}
|
|
234
377
|
|
|
235
|
-
module.exports = { parseDiff, decide, classifyDiff, classifyFromGit, detectDefaultBranch };
|
|
378
|
+
module.exports = { parseDiff, decide, classifyDiff, classifyFromGit, detectDefaultBranch, _resetCacheForTest };
|
|
@@ -28,11 +28,12 @@ The classifier auto-detects the repo's default branch (origin/HEAD, then `init.d
|
|
|
28
28
|
Output:
|
|
29
29
|
```json
|
|
30
30
|
{
|
|
31
|
-
"tier": "TRIVIAL" | "SMALL" | "NORMAL",
|
|
32
|
-
"model": "sonnet",
|
|
31
|
+
"tier": "TRIVIAL" | "SMALL" | "NORMAL" | "DEEP",
|
|
32
|
+
"model": "sonnet" | "haiku" | "opus",
|
|
33
33
|
"agentCount": 0 | 1 | 3,
|
|
34
|
+
"escalate": { "suggested": false, "target": null, "reason": null },
|
|
34
35
|
"reasoning": ["..."],
|
|
35
|
-
"stats": { "added": ..., "deleted": ..., "declAdded": ..., "declRemoved": ..., "netDecls": ..., "fileCount": ..., "securityHit": ... }
|
|
36
|
+
"stats": { "added": ..., "deleted": ..., "declAdded": ..., "declRemoved": ..., "netDecls": ..., "fileCount": ..., "securityHit": ..., "tsjsLOC": ..., "tsjsNetDecls": ..., "otherNetAdded": ... }
|
|
36
37
|
}
|
|
37
38
|
```
|
|
38
39
|
|
|
@@ -69,6 +70,17 @@ Reserved for **genuinely cross-cutting** changes that single-agent review can't
|
|
|
69
70
|
|
|
70
71
|
Three agents exist to cover orthogonal axes (Reuse / Quality / Efficiency) when the change is broad enough that one agent's tool-call budget can't survey it all. For single-file edits, one focused agent always covers all three axes — three is duplication, not coverage.
|
|
71
72
|
|
|
73
|
+
### DEEP — three parallel Opus agents (architectural, rare)
|
|
74
|
+
The top rung, for genuinely **architectural** change where *depth* of reasoning — not just breadth — earns Opus's cost (judging whether a large refactor picked the right abstractions is reasoning, not surveying). The classifier escalates to DEEP only on real new-logic evidence, **never raw volume** — any of:
|
|
75
|
+
- `>1500 LOC of TS/JS AND ≥10 net-new declarations`
|
|
76
|
+
- `>1200 net-new lines of non-TS/JS source` (other languages are gated on net lines because the declaration parser is TS/JS-shaped)
|
|
77
|
+
- `5+ new files AND ≥15 new declarations AND ≥10 net` (a real new subsystem)
|
|
78
|
+
- `security-sensitive path AND ≥8 net-new declarations`
|
|
79
|
+
|
|
80
|
+
DEEP runs **automatically — no prompt.** Noise (lockfiles, snapshots, generated/vendored) and docs/data are stripped before measuring, and the signal is *net of churn* (TS/JS by net-new declarations, other code by net-new lines), so a lockfile bump, a reformatting sweep, or a large rename/relocation can never reach Opus — they cancel to ~0 and stay SMALL/NORMAL.
|
|
81
|
+
|
|
82
|
+
For the most **extreme** diffs (`>4000 LOC TS/JS + ≥25 net decls`, `>3000 net-new non-TS/JS lines`, or `10+ new files + ≥30 decls + ≥20 net`), the classifier also sets `escalate.suggested = true` with `target: "builtin-simplify"`. The Opus pass still runs as the floor; you then **offer** the user a handoff to Claude Code's built-in `/simplify` (Phase 3) — a suggestion, not an auto-switch.
|
|
83
|
+
|
|
72
84
|
## Phase 2.5: Validation pass (re-run after fixes)
|
|
73
85
|
|
|
74
86
|
If `/flo-simplify` already ran on this branch in this session AND the only edits since are fixes driven by the prior pass's findings, default to **self-review tier** regardless of LOC count. The fan-out already happened; the fix is small relative to the diff that was already reviewed.
|
|
@@ -79,7 +91,7 @@ Escalate one tier (self-review → SMALL agent) only if the fix introduced any o
|
|
|
79
91
|
- A new dependency or import from a previously-untouched module
|
|
80
92
|
- A change to control flow not covered in the original findings
|
|
81
93
|
|
|
82
|
-
Do **not** escalate to NORMAL on a validation pass. If the fix is so structural that NORMAL is warranted, treat it as a fresh diff and start over from Phase 1.
|
|
94
|
+
Do **not** escalate to NORMAL or DEEP on a validation pass. If the fix is so structural that NORMAL/DEEP is warranted, treat it as a fresh diff and start over from Phase 1.
|
|
83
95
|
|
|
84
96
|
## Phase 2.7: Model selection
|
|
85
97
|
|
|
@@ -87,7 +99,7 @@ Do **not** escalate to NORMAL on a validation pass. If the fix is so structural
|
|
|
87
99
|
|
|
88
100
|
- `sonnet` (default) — real logic changes, single agent or three-agent fan-out.
|
|
89
101
|
- `haiku` — mostly-relocation diffs (mechanical moves where pattern-matching beats deep reasoning, ~5x cheaper).
|
|
90
|
-
- `opus` —
|
|
102
|
+
- `opus` — the **DEEP** tier only. Architectural review is depth-bound (judging whether a large refactor picked the right abstractions is reasoning, not surveying), so the classifier returns opus when — and only when — the diff clears the DEEP bar. Never pick opus yourself for SMALL/NORMAL; ordinary review is breadth-bound and three sonnet agents are the right tool.
|
|
91
103
|
|
|
92
104
|
If you fell back to prose rules in Phase 2 (no classifier available), use `sonnet` unconditionally. Pass the classifier's `model` field verbatim to Agent's `model` parameter.
|
|
93
105
|
|
|
@@ -123,6 +135,15 @@ Launch three agents in a single message — Reuse, Quality, Efficiency — passi
|
|
|
123
135
|
|
|
124
136
|
**Efficiency**: unnecessary work, missed concurrency, hot-path bloat, recurring no-op updates, TOCTOU existence checks, unbounded structures, over-broad reads.
|
|
125
137
|
|
|
138
|
+
### DEEP: three parallel Opus agents (architectural) — and maybe a handoff
|
|
139
|
+
Same three-agent fan-out as NORMAL (Reuse / Quality / Efficiency in one message), but pass `model: "opus"` to each — the classifier already decided depth is warranted. **Print one line first** so the heavier cost is never silent, e.g. `distill: DEEP — 1,800 LOC of new logic, ran opus depth pass`.
|
|
140
|
+
|
|
141
|
+
If the classifier set `escalate.suggested = true`, then **after** the Opus pass finishes, surface a handoff suggestion to the user — never switch automatically:
|
|
142
|
+
|
|
143
|
+
> This change is large enough (`<escalate.reason>`) that Claude Code's built-in `/simplify` — a deeper, heavier multi-agent pass — may add value beyond this review. Want to run it? It costs more tokens, so it's your call.
|
|
144
|
+
|
|
145
|
+
Then stop and let the user decide. Do **not** invoke the built-in `/simplify` yourself — the handoff is the user's choice, and the Opus pass you just ran already covers the diff.
|
|
146
|
+
|
|
126
147
|
## Phase 4: Fix or skip
|
|
127
148
|
|
|
128
149
|
Aggregate findings. Fix each one directly. False positives or not-worth-fixing — note and skip without arguing. If self-review found nothing, just confirm clean and exit.
|
|
@@ -87,11 +87,11 @@ mcp__moflo__memory_store {
|
|
|
87
87
|
namespace: "learnings",
|
|
88
88
|
key: "<stable descriptive slug, e.g. pattern:daemon-port-resolver or gotcha:windows-spell-path>",
|
|
89
89
|
value: "<the lesson> — Why: <why it matters>. How to apply: <what to do next time>.",
|
|
90
|
-
tags: ["<topic>", "<area>"]
|
|
90
|
+
tags: ["<topic>", "<area>", "source:meditate-manual"]
|
|
91
91
|
}
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
Keep keys stable and descriptive so the next `/meditate` updates rather than re-adds. In `--preview` mode, **stop here** — print the candidates and their would-be keys/dedup verdicts, write nothing.
|
|
94
|
+
Always include the `source:meditate-manual` tag — it lets the Luminarium "Learnings" panel attribute each lesson to `/meditate` vs the automatic auto-meditate distill. Keep keys stable and descriptive so the next `/meditate` updates rather than re-adds. In `--preview` mode, **stop here** — print the candidates and their would-be keys/dedup verdicts, write nothing.
|
|
95
95
|
|
|
96
96
|
## Step 4 — Report
|
|
97
97
|
|
package/bin/lib/meditate.mjs
CHANGED
|
@@ -39,11 +39,12 @@ import { randomBytes } from 'crypto';
|
|
|
39
39
|
// ── Tunables (exported for tests) ───────────────────────────────────────────
|
|
40
40
|
/** Model used for the bounded distillation pass (cheap; Haiku is a formatter). */
|
|
41
41
|
export const HAIKU_MODEL_ID = 'claude-haiku-4-5-20251001';
|
|
42
|
-
/** Min gap between in-session capture directives
|
|
43
|
-
*
|
|
44
|
-
|
|
42
|
+
/** Min gap between in-session capture directives — a debounce so a burst of
|
|
43
|
+
* signals in quick succession injects the directive at most once per window.
|
|
44
|
+
* Shorter window = more capture opportunities (and more injected context). */
|
|
45
|
+
export const INJECT_WINDOW_MS = 5 * 60_000;
|
|
45
46
|
/** Hard cap on directives per session (belt-and-braces over the time window). */
|
|
46
|
-
export const INJECT_MAX_PER_SESSION =
|
|
47
|
+
export const INJECT_MAX_PER_SESSION = 10;
|
|
47
48
|
/** Ledger size cap — distilled entries are pruned first, then oldest. */
|
|
48
49
|
export const MAX_LEDGER_ENTRIES = 200;
|
|
49
50
|
/** Keep this many already-distilled entries for idempotency/debug before pruning. */
|
|
@@ -148,8 +149,23 @@ const DECISION_PATTERNS = [
|
|
|
148
149
|
/\b(decided|decision)\b/i,
|
|
149
150
|
/\bgoing with\b/i,
|
|
150
151
|
/\bthe plan is\b/i,
|
|
152
|
+
/\bthe approach is\b/i,
|
|
151
153
|
/\bwe should (use|go with|do)\b/i,
|
|
152
154
|
/\bi'?ll go with\b/i,
|
|
155
|
+
// Selection / preference / finalization phrasing.
|
|
156
|
+
/\bgo with (option|approach|the|that|a)\b/i,
|
|
157
|
+
/\bi'?d rather\b/i,
|
|
158
|
+
/\b(we'?ll|let'?s) keep\b/i,
|
|
159
|
+
/\bfinal (decision|answer|call)\b/i,
|
|
160
|
+
// Approval ATTACHED to a concrete action/decision — "go ahead and ship the
|
|
161
|
+
// fix", "ok, use the daemon", "proceed with option B". Bare approval alone
|
|
162
|
+
// ("ok"/"yes"/"go ahead"/"sounds good") deliberately does NOT fire: it must
|
|
163
|
+
// carry an action to count as a decision moment, else it would trip on nearly
|
|
164
|
+
// every agreeing turn and burn the per-session injection budget. The live
|
|
165
|
+
// model's "append nothing" remains the final precision filter.
|
|
166
|
+
/\bgo ahead (and|with)\b/i,
|
|
167
|
+
/\bproceed with\b/i,
|
|
168
|
+
/\b(yes|yeah|yep|sure|ok|okay|sounds good|lgtm)[\s,!.]+(go with|use|ship|build|implement|add|proceed)\b/i,
|
|
153
169
|
];
|
|
154
170
|
|
|
155
171
|
// Tight, low-false-positive markers only — tool output mentions "error" benignly
|
|
@@ -162,6 +178,32 @@ const ERROR_PATTERNS = [
|
|
|
162
178
|
/\b[1-9]\d* (failed|failing)\b/i,
|
|
163
179
|
];
|
|
164
180
|
|
|
181
|
+
// Bare approval that, on its own, looks like mere assent ("yes", "ok", "go
|
|
182
|
+
// ahead") — but confirms a real decision when the PRIOR assistant turn proposed
|
|
183
|
+
// one. Anchored at the start of the user's message so it matches a reply that
|
|
184
|
+
// LEADS with approval. Paired with PROPOSAL_PATTERNS below as the precision gate.
|
|
185
|
+
const APPROVAL_PATTERNS = [
|
|
186
|
+
/^\s*(y(es|ep|eah|up)?|sure|ok(ay)?|kk?|roger|agreed?|perfect|great|nice|sounds good|lgtm|do it|go ahead|proceed|approved?|let'?s do it|make it so|ship it)\b/i,
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// Proposal / recommendation / options language the assistant uses when it asks
|
|
190
|
+
// the user to choose or approve. Scanned against the recent transcript tail; a
|
|
191
|
+
// match here means a bare approval in the prompt is confirming a real decision,
|
|
192
|
+
// so "yes" after "I recommend we use the daemon" fires, but "ok thanks" after a
|
|
193
|
+
// plain status report does not. Recall-biased — the live model still filters.
|
|
194
|
+
const PROPOSAL_PATTERNS = [
|
|
195
|
+
/\bi (propose|recommend|suggest|advise)\b/i,
|
|
196
|
+
/\bi'?(d|ll) (recommend|suggest|propose|go with|use|implement|add|build|refactor)\b/i,
|
|
197
|
+
/\bwe (could|should|can|might) (use|go with|implement|refactor|add|switch|split)\b/i,
|
|
198
|
+
/\b(option|approach|plan|choice) (a|b|c|1|2|3|one|two)\b/i,
|
|
199
|
+
/\b(two|three|several|a few) (options|approaches|choices|ways)\b/i,
|
|
200
|
+
/\bshould i (proceed|go ahead|use|implement|start|continue|create|add)\b/i,
|
|
201
|
+
/\b(would|do) you (like|want) me to\b/i,
|
|
202
|
+
/\bhere'?s (the|my|a) (plan|approach|proposal|recommendation)\b/i,
|
|
203
|
+
/\brecommendation\b/i,
|
|
204
|
+
/\bpropos(e|ed|al)\b/i,
|
|
205
|
+
];
|
|
206
|
+
|
|
165
207
|
function anyMatch(patterns, text) {
|
|
166
208
|
if (!text) return false;
|
|
167
209
|
for (const re of patterns) if (re.test(text)) return true;
|
|
@@ -182,6 +224,12 @@ export function detectSignal(prompt, transcriptTail = '') {
|
|
|
182
224
|
const t = typeof transcriptTail === 'string' ? transcriptTail : '';
|
|
183
225
|
if (anyMatch(CORRECTION_PATTERNS, p)) return { hit: true, kind: 'correction' };
|
|
184
226
|
if (anyMatch(DECISION_PATTERNS, p) || anyMatch(DECISION_PATTERNS, t)) return { hit: true, kind: 'decision' };
|
|
227
|
+
// Bare approval in the prompt that confirms a proposal/recommendation the
|
|
228
|
+
// assistant made in the immediately prior turn — "yes"/"go ahead" answering
|
|
229
|
+
// "I recommend we use the daemon". The prior-turn proposal (in transcriptTail)
|
|
230
|
+
// is the precision gate: a contextless "ok thanks" with nothing proposed above
|
|
231
|
+
// it does NOT fire.
|
|
232
|
+
if (anyMatch(APPROVAL_PATTERNS, p) && anyMatch(PROPOSAL_PATTERNS, t)) return { hit: true, kind: 'decision' };
|
|
185
233
|
if (anyMatch(ERROR_PATTERNS, t)) return { hit: true, kind: 'error_fix' };
|
|
186
234
|
return { hit: false, kind: null };
|
|
187
235
|
}
|
|
@@ -241,11 +289,11 @@ export function buildCaptureDirective(kind) {
|
|
|
241
289
|
return [
|
|
242
290
|
`[auto-meditate] A ${label} just occurred this session.`,
|
|
243
291
|
`FIRST: fully address the user's request as you normally would — do NOT let this note change your answer.`,
|
|
244
|
-
`THEN,
|
|
292
|
+
`THEN, if a reusable lesson emerged, append exactly ONE final line of the form:`,
|
|
245
293
|
`<meditate-capture>LESSON</meditate-capture>`,
|
|
246
294
|
`where LESSON is a single concise sentence (a pattern, gotcha, or decision+rationale).`,
|
|
247
295
|
DURABILITY_BAR,
|
|
248
|
-
`
|
|
296
|
+
`When in doubt, capture it — a borderline lesson is cheap and the distill dedups; append nothing only for pure session state or git history. Never emit more than one such line.`,
|
|
249
297
|
].join('\n');
|
|
250
298
|
}
|
|
251
299
|
|
|
@@ -488,6 +536,7 @@ export function buildDistillPrompt(entries) {
|
|
|
488
536
|
`1. For EACH candidate, call mcp__moflo__memory_search { namespace: "learnings", query: <bare keywords>, threshold: 0.6, limit: 5 }.`,
|
|
489
537
|
`2. If the top hit is the SAME fact at similarity >= 0.80, call mcp__moflo__memory_store with that SAME key (upsert), merging any new nuance — do NOT create a near-duplicate.`,
|
|
490
538
|
`3. Otherwise call mcp__moflo__memory_store { namespace: "learnings", key: <stable descriptive slug>, value: "<lesson> — Why: <why it matters>. How to apply: <what to do next time>.", tags: [<topic>, <area>] }.`,
|
|
539
|
+
` PROVENANCE (#1203): every memory_store you make — both the step-2 upsert and the step-3 new write — MUST include the tag "source:auto-meditate" in its tags array, so the Luminarium "Learnings" panel can attribute these to the automatic distill pass.`,
|
|
491
540
|
`4. Skip any candidate that does not clear the durability bar below, or that merely restates an existing entry.`,
|
|
492
541
|
``,
|
|
493
542
|
DURABILITY_BAR,
|
package/bin/meditate-distill.mjs
CHANGED
|
@@ -69,6 +69,10 @@ function runHeadless(projectRoot, prompt) {
|
|
|
69
69
|
...process.env,
|
|
70
70
|
CLAUDE_CODE_HEADLESS: 'true', // mark the child so its own hooks no-op (#860)
|
|
71
71
|
ANTHROPIC_MODEL: HAIKU_MODEL_ID, // cheap formatter model
|
|
72
|
+
// Deterministic provenance: the write path stamps source:auto-meditate on
|
|
73
|
+
// these learnings writes even if Haiku omits the tag it's asked to add
|
|
74
|
+
// (#1203 follow-up). Honoured by storeEntry in memory/entries-write.ts.
|
|
75
|
+
MOFLO_LEARNINGS_SOURCE: 'auto-meditate',
|
|
72
76
|
};
|
|
73
77
|
|
|
74
78
|
let child;
|