moflo 4.9.13 → 4.9.15
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/guidance/shipped/moflo-core-guidance.md +1 -0
- package/.claude/guidance/shipped/moflo-verbose-command-filtering.md +45 -0
- package/.claude/helpers/gate.cjs +21 -5
- package/.claude/helpers/simplify-classify.cjs +211 -0
- package/.claude/skills/eldar/SKILL.md +13 -8
- package/.claude/skills/fl/phases.md +18 -2
- package/.claude/skills/guidance/SKILL.md +1 -1
- package/.claude/skills/simplify/SKILL.md +35 -48
- package/.claude/skills/spell-schedule/SKILL.md +1 -1
- package/bin/gate.cjs +21 -5
- package/bin/session-start-launcher.mjs +1 -1
- package/bin/simplify-classify.cjs +211 -0
- package/dist/src/cli/commands/doctor-checks-config.js +246 -0
- package/dist/src/cli/commands/doctor-checks-deep.js +40 -2
- package/dist/src/cli/commands/doctor-checks-intelligence.js +197 -0
- package/dist/src/cli/commands/doctor-checks-memory.js +207 -0
- package/dist/src/cli/commands/doctor-checks-platform.js +138 -0
- package/dist/src/cli/commands/doctor-checks-runtime.js +170 -0
- package/dist/src/cli/commands/doctor-fixes.js +165 -0
- package/dist/src/cli/commands/doctor-registry.js +109 -0
- package/dist/src/cli/commands/doctor-render.js +203 -0
- package/dist/src/cli/commands/doctor-types.js +9 -0
- package/dist/src/cli/commands/doctor-version.js +134 -0
- package/dist/src/cli/commands/doctor-zombies.js +201 -0
- package/dist/src/cli/commands/doctor.js +35 -1706
- package/dist/src/cli/init/helpers-generator.js +21 -5
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/scripts/post-install-bootstrap.mjs +1 -0
|
@@ -307,3 +307,4 @@ See `moflo-memory-strategy.md` for memory-specific troubleshooting.
|
|
|
307
307
|
- `.claude/guidance/shipped/moflo-session-start.md` — Complete session-start lifecycle (DB heal, sync, migrations, daemon)
|
|
308
308
|
- `.claude/guidance/shipped/moflo-settings-injection.md` — What moflo writes into `.claude/` and how surgical self-heal works
|
|
309
309
|
- `.claude/guidance/shipped/moflo-cross-platform.md` — Windows/macOS/Linux portability rules for any code change
|
|
310
|
+
- `.claude/guidance/shipped/moflo-verbose-command-filtering.md` — Filter long verbose commands at the source; never tee-then-grep
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Verbose Command Filtering — Filter at Source, Never Tee-Then-Read
|
|
2
|
+
|
|
3
|
+
**Purpose:** Pipe long verbose commands (smoke runs, full test suites, builds with `--verbose`) through a filter at execution time so only relevant lines reach the model context. Never tee output to disk and `tail`/`grep` it later — every follow-up read re-loads the full file into context (~5K tokens per round-trip).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The Rule
|
|
8
|
+
|
|
9
|
+
| Pattern | Verdict |
|
|
10
|
+
|---------|---------|
|
|
11
|
+
| `cmd 2>&1 \| grep -E "FAIL\|Summary"` (run_in_background) | ✅ Filter at source |
|
|
12
|
+
| `cmd 2>&1 \| tee .tmp.log` then later `tail`/`grep` `.tmp.log` | ❌ Tee-then-read |
|
|
13
|
+
|
|
14
|
+
Tee-then-read is the silent context killer. The Bash tool surfaces stdout into context, so each follow-up `grep`/`tail` of a tee'd file re-reads the file fresh on every call. Three follow-ups burn 15K+ tokens before any decision lands. Filtering at source emits the matching lines once.
|
|
15
|
+
|
|
16
|
+
## When to Apply
|
|
17
|
+
|
|
18
|
+
Smoke harness runs, full `vitest`/`jest` suites, builds with `--verbose`, anything passing `--trace`, any `node ... --verbose` invocation. If you genuinely need the full log for post-mortem, write it to disk but inspect it OUTSIDE the model loop (have the user open it, attach it to an issue) — do NOT pipe a tee'd file back through Bash.
|
|
19
|
+
|
|
20
|
+
## Concrete Examples
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# ✅ Smoke harness
|
|
24
|
+
node harness/consumer-smoke/run.mjs 2>&1 | grep -E "FAIL|Summary|Zombie"
|
|
25
|
+
# ❌ node harness/consumer-smoke/run.mjs 2>&1 | tee .tmp.log; tail .tmp.log
|
|
26
|
+
|
|
27
|
+
# ✅ Vitest full suite
|
|
28
|
+
npm test -- --reporter=verbose 2>&1 | grep -E "FAIL|✗|Error:"
|
|
29
|
+
# ❌ npm test 2>&1 | tee .test.log; grep FAIL .test.log
|
|
30
|
+
|
|
31
|
+
# ✅ Build with verbose
|
|
32
|
+
npm run build -- --verbose 2>&1 | grep -E "error TS|Failed|Cannot find"
|
|
33
|
+
# ❌ npm run build 2>&1 | tee .build.log; grep "error TS" .build.log
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Why It Matters
|
|
37
|
+
|
|
38
|
+
Case study: issue #903 burned ~25K tokens across 5 tee-then-grep round-trips where a single grep-at-source would have surfaced the same signal once. Filtering at source is not an optimization — it is the default shape for any verbose command whose full output you do not need in your context.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## See Also
|
|
43
|
+
|
|
44
|
+
- `.claude/guidance/shipped/moflo-core-guidance.md` — Hub for moflo's CLI/MCP surface and runtime conventions
|
|
45
|
+
- `.claude/guidance/shipped/moflo-memory-strategy.md` — Companion rules on RAG indexing and context discipline
|
package/.claude/helpers/gate.cjs
CHANGED
|
@@ -88,7 +88,11 @@ var TASK_RE = /\b(fix|bug|error|implement|add|create|build|write|refactor|debug|
|
|
|
88
88
|
var TEST_RUNNER_RE = /(?:^|[^a-z])(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?(?:test|t)(?:[:\s]|$)|\b(?:npx|pnpx)\s+(?:vitest|jest|mocha|ava|tap|jasmine|pytest)\b|(?:^|;|&&|\|\|)\s*(?:vitest|jest|pytest|mocha|jasmine|tap|ava)\s|\b(?:cargo|go|deno|dotnet|mvn)\s+test\b|\bgradle\w*\s+test\b/i;
|
|
89
89
|
// Edits to these don't change runtime behaviour, so they don't invalidate prior test/simplify runs.
|
|
90
90
|
// Lock files and .gitignore are tracked but inert; package.json/*.yaml ARE source — they reset.
|
|
91
|
-
var
|
|
91
|
+
var EDIT_RESET_SKIP_BOTH_RE = /\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^|[\\\/])(CHANGELOG(?:\.md)?|\.env\.example|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/i;
|
|
92
|
+
// Test files: invalidate the testing gate (tests are stale once test code changes)
|
|
93
|
+
// but NOT the simplify gate — /simplify already reviewed the production code; touching
|
|
94
|
+
// a test file or fixture doesn't expose new untested surface for code review (#908).
|
|
95
|
+
var EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE = /(?:^|[\\\/])(__tests__|__mocks__|tests?|spec|specs|cypress|e2e|fixtures?)[\\\/]|\.(test|spec)\.[mc]?[jt]sx?$|\.fixture\.[mc]?[jt]sx?$/i;
|
|
92
96
|
|
|
93
97
|
switch (command) {
|
|
94
98
|
case 'check-before-agent': {
|
|
@@ -180,11 +184,20 @@ switch (command) {
|
|
|
180
184
|
}
|
|
181
185
|
case 'reset-edit-gates': {
|
|
182
186
|
var fp = process.env.TOOL_INPUT_file_path || '';
|
|
183
|
-
|
|
187
|
+
// Inert files (markdown, lockfiles, CHANGELOG, .env.example): no gate reset.
|
|
188
|
+
if (fp && EDIT_RESET_SKIP_BOTH_RE.test(fp)) break;
|
|
184
189
|
var s = readState();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
190
|
+
// Test-only edits invalidate testsRun but preserve simplifyRun (#908).
|
|
191
|
+
var isTestOnly = fp && EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE.test(fp);
|
|
192
|
+
var resetTests = s.testsRun;
|
|
193
|
+
var resetSimplify = s.simplifyRun && !isTestOnly;
|
|
194
|
+
if (!resetTests && !resetSimplify) break;
|
|
195
|
+
var gates = [];
|
|
196
|
+
if (resetTests) { s.testsRun = false; gates.push('tests'); }
|
|
197
|
+
if (resetSimplify) { s.simplifyRun = false; gates.push('simplify'); }
|
|
198
|
+
if (fp) {
|
|
199
|
+
s.lastResetBy = { file: fp, at: new Date().toISOString(), gates: gates };
|
|
200
|
+
}
|
|
188
201
|
writeState(s);
|
|
189
202
|
break;
|
|
190
203
|
}
|
|
@@ -205,6 +218,9 @@ switch (command) {
|
|
|
205
218
|
for (var i = 0; i < missing.length; i++) {
|
|
206
219
|
process.stderr.write(' - ' + missing[i] + '\n');
|
|
207
220
|
}
|
|
221
|
+
if (s.lastResetBy && s.lastResetBy.file) {
|
|
222
|
+
process.stderr.write('Last gate reset: ' + s.lastResetBy.file + ' (' + (s.lastResetBy.gates || []).join(', ') + ')\n');
|
|
223
|
+
}
|
|
208
224
|
process.stderr.write('Disable per-gate via moflo.yaml:\n');
|
|
209
225
|
process.stderr.write(' gates:\n testing_gate: false\n simplify_gate: false\n learnings_gate: false\n');
|
|
210
226
|
process.exit(2);
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* /simplify diff classifier — issue #908.
|
|
4
|
+
*
|
|
5
|
+
* Decides which review tier the current diff warrants and returns a JSON
|
|
6
|
+
* dispatch decision. The /simplify skill MUST call this first so routing is
|
|
7
|
+
* deterministic and unit-testable instead of a prose decision Claude makes
|
|
8
|
+
* over and over per run.
|
|
9
|
+
*
|
|
10
|
+
* Rule (per user direction): default to single-agent Sonnet review. Only
|
|
11
|
+
* escalate to a 3-agent fan-out when diff signals genuinely warrant it.
|
|
12
|
+
* Opus is never selected — the existing skill already documents that.
|
|
13
|
+
*
|
|
14
|
+
* Outputs JSON:
|
|
15
|
+
* {
|
|
16
|
+
* "tier": "TRIVIAL" | "SMALL" | "NORMAL",
|
|
17
|
+
* "model": "sonnet",
|
|
18
|
+
* "agentCount": 0 | 1 | 3,
|
|
19
|
+
* "reasoning": [string, ...],
|
|
20
|
+
* "stats": { added, deleted, fileCount, declAdded, declRemoved, ... }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* node bin/simplify-classify.cjs [--base main]
|
|
25
|
+
* node bin/simplify-classify.cjs --diff <unified-diff-on-stdin>
|
|
26
|
+
*
|
|
27
|
+
* The --diff stdin form exists so unit tests can drive the classifier
|
|
28
|
+
* with synthetic diffs (no git repo required).
|
|
29
|
+
*/
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
const { execSync } = require('child_process');
|
|
33
|
+
|
|
34
|
+
// Paths where new logic warrants the 3-agent fan-out (issue #908).
|
|
35
|
+
// Mechanical edits inside these paths are still SMALL; only adding/removing
|
|
36
|
+
// declarations triggers escalation.
|
|
37
|
+
const SECURITY_PATHS = [
|
|
38
|
+
/(?:^|[\\\/])aidefence[\\\/]/i,
|
|
39
|
+
/(?:^|[\\\/])swarm[\\\/]consensus[\\\/]/i,
|
|
40
|
+
/(?:^|[\\\/])hooks?[\\\/](?:handlers?|gate|wiring)/i,
|
|
41
|
+
/(?:^|[\\\/])services[\\\/]daemon-lock\.ts$/i,
|
|
42
|
+
/(?:^|[\\\/])bin[\\\/]gate\./i,
|
|
43
|
+
/(?:^|[\\\/])bin[\\\/]session-start-launcher\./i,
|
|
44
|
+
/(?:^|[\\\/])\.claude[\\\/]helpers[\\\/]gate/i,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function safeExec(cmd) {
|
|
48
|
+
try { return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); }
|
|
49
|
+
catch { return ''; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readDiffFromGit(base) {
|
|
53
|
+
// Combined diff: committed-since-base + working-tree
|
|
54
|
+
const committed = safeExec(`git diff ${base}...HEAD`);
|
|
55
|
+
const working = safeExec('git diff HEAD');
|
|
56
|
+
return committed + (working ? '\n' + working : '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse a unified-diff string into per-file stats and aggregate signals.
|
|
61
|
+
* No git/I/O — pure function over the diff text. Test-friendly.
|
|
62
|
+
*/
|
|
63
|
+
function parseDiff(diff) {
|
|
64
|
+
const lines = diff.split('\n');
|
|
65
|
+
const files = new Map(); // filename → { added, deleted, declAdded, declRemoved, isNew, isRenamed }
|
|
66
|
+
let current = null;
|
|
67
|
+
|
|
68
|
+
// Match function/class/export-const-arrow/method declarations being
|
|
69
|
+
// added or removed. Conservative — biased toward false negatives so we
|
|
70
|
+
// don't over-escalate.
|
|
71
|
+
const DECL_RE = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|type)\s+\w/;
|
|
72
|
+
const ARROW_DECL_RE = /^(?:export\s+)?(?:const|let|var)\s+\w+\s*[:=].*=>\s*\{?$/;
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
const ln = lines[i];
|
|
76
|
+
|
|
77
|
+
// File header: `diff --git a/path b/path`
|
|
78
|
+
let m = ln.match(/^diff --git (?:a\/)?(.+?) (?:b\/)?(.+)$/);
|
|
79
|
+
if (m) {
|
|
80
|
+
const filename = m[2];
|
|
81
|
+
current = { filename, added: 0, deleted: 0, declAdded: 0, declRemoved: 0, isNew: false, isRenamed: false };
|
|
82
|
+
files.set(filename, current);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!current) continue;
|
|
86
|
+
|
|
87
|
+
if (ln.startsWith('new file mode')) current.isNew = true;
|
|
88
|
+
if (ln.startsWith('rename from') || ln.startsWith('rename to') || ln.startsWith('similarity index')) current.isRenamed = true;
|
|
89
|
+
|
|
90
|
+
// Skip diff headers
|
|
91
|
+
if (ln.startsWith('+++') || ln.startsWith('---') || ln.startsWith('@@') || ln.startsWith('index ')) continue;
|
|
92
|
+
|
|
93
|
+
if (ln.startsWith('+') && !ln.startsWith('+++')) {
|
|
94
|
+
current.added++;
|
|
95
|
+
const body = ln.slice(1).trim();
|
|
96
|
+
if (DECL_RE.test(body) || ARROW_DECL_RE.test(body)) current.declAdded++;
|
|
97
|
+
} else if (ln.startsWith('-') && !ln.startsWith('---')) {
|
|
98
|
+
current.deleted++;
|
|
99
|
+
const body = ln.slice(1).trim();
|
|
100
|
+
if (DECL_RE.test(body) || ARROW_DECL_RE.test(body)) current.declRemoved++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Aggregate
|
|
105
|
+
let added = 0, deleted = 0, declAdded = 0, declRemoved = 0;
|
|
106
|
+
let newFiles = 0, renamedFiles = 0;
|
|
107
|
+
let securityHit = false;
|
|
108
|
+
for (const f of files.values()) {
|
|
109
|
+
added += f.added;
|
|
110
|
+
deleted += f.deleted;
|
|
111
|
+
declAdded += f.declAdded;
|
|
112
|
+
declRemoved += f.declRemoved;
|
|
113
|
+
if (f.isNew) newFiles++;
|
|
114
|
+
if (f.isRenamed) renamedFiles++;
|
|
115
|
+
if (SECURITY_PATHS.some(rx => rx.test(f.filename))) securityHit = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
added, deleted, declAdded, declRemoved,
|
|
120
|
+
netDecls: declAdded - declRemoved,
|
|
121
|
+
fileCount: files.size,
|
|
122
|
+
newFiles, renamedFiles,
|
|
123
|
+
securityHit,
|
|
124
|
+
files: [...files.keys()],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Pure decision function. Takes parsed stats, returns dispatch decision.
|
|
130
|
+
* No I/O. Easy to unit-test with synthetic stats.
|
|
131
|
+
*/
|
|
132
|
+
function decide(stats) {
|
|
133
|
+
const reasoning = [];
|
|
134
|
+
const totalChange = stats.added + stats.deleted;
|
|
135
|
+
|
|
136
|
+
if (totalChange === 0) {
|
|
137
|
+
return { tier: 'TRIVIAL', model: 'sonnet', agentCount: 0, reasoning: ['empty diff — nothing to review'], stats };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// TRIVIAL: tiny diff, no declarations changed
|
|
141
|
+
if (totalChange <= 10 && stats.fileCount <= 1 && stats.netDecls === 0 && stats.declAdded === 0 && stats.declRemoved === 0) {
|
|
142
|
+
reasoning.push(`≤10 LOC in 1 file with no declaration changes`);
|
|
143
|
+
return { tier: 'TRIVIAL', model: 'sonnet', agentCount: 0, reasoning, stats };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Mechanical relocation detection — the #906 case.
|
|
147
|
+
// If declarations were both ADDED and REMOVED at roughly matching rates,
|
|
148
|
+
// it's a structural move, not net-new logic. Judge by declaration balance,
|
|
149
|
+
// not raw LOC balance — formatting/blank-line differences between source
|
|
150
|
+
// and destination files easily push raw LOC out of balance even when the
|
|
151
|
+
// semantic change is purely "moved 5 functions across 5 new files".
|
|
152
|
+
// Mechanical relocations are SMALL even when many files / many lines.
|
|
153
|
+
const declTouched = stats.declAdded + stats.declRemoved;
|
|
154
|
+
const isMostlyRelocation = stats.declAdded >= 2
|
|
155
|
+
&& stats.declRemoved >= 2
|
|
156
|
+
&& Math.abs(stats.netDecls) <= Math.max(2, Math.floor(declTouched * 0.30));
|
|
157
|
+
|
|
158
|
+
if (isMostlyRelocation) {
|
|
159
|
+
reasoning.push(
|
|
160
|
+
`mostly relocation: ${stats.declAdded} decls added, ${stats.declRemoved} removed, net ${stats.netDecls >= 0 ? '+' : ''}${stats.netDecls}`,
|
|
161
|
+
);
|
|
162
|
+
return { tier: 'SMALL', model: 'sonnet', agentCount: 1, reasoning, stats };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Escalation triggers — any one trips NORMAL (3 agents).
|
|
166
|
+
// Always Sonnet — Opus is never the right model for /simplify per skill rule.
|
|
167
|
+
const triggers = [];
|
|
168
|
+
if (totalChange > 500) triggers.push(`>500 LOC changed (${totalChange})`);
|
|
169
|
+
if (stats.fileCount >= 5 && stats.netDecls >= 3) triggers.push(`${stats.fileCount} files with ${stats.netDecls} net new declarations`);
|
|
170
|
+
if (stats.securityHit && stats.netDecls > 0) triggers.push('security-sensitive path with new logic');
|
|
171
|
+
if (stats.newFiles >= 3 && stats.declAdded >= 5) triggers.push(`${stats.newFiles} new files with ${stats.declAdded} new declarations`);
|
|
172
|
+
|
|
173
|
+
if (triggers.length > 0) {
|
|
174
|
+
return { tier: 'NORMAL', model: 'sonnet', agentCount: 3, reasoning: triggers, stats };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Default: SMALL — single sonnet agent
|
|
178
|
+
reasoning.push(`small/medium diff: ${totalChange} LOC across ${stats.fileCount} file(s), +${stats.declAdded}/-${stats.declRemoved} decls`);
|
|
179
|
+
return { tier: 'SMALL', model: 'sonnet', agentCount: 1, reasoning, stats };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function classifyDiff(diffText) {
|
|
183
|
+
return decide(parseDiff(diffText));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function classifyFromGit(base = 'main') {
|
|
187
|
+
return classifyDiff(readDiffFromGit(base));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (require.main === module) {
|
|
191
|
+
const args = process.argv.slice(2);
|
|
192
|
+
const baseIdx = args.indexOf('--base');
|
|
193
|
+
const base = baseIdx >= 0 ? args[baseIdx + 1] : 'main';
|
|
194
|
+
const stdinDiff = args.includes('--diff') || args.includes('--stdin');
|
|
195
|
+
|
|
196
|
+
let result;
|
|
197
|
+
if (stdinDiff) {
|
|
198
|
+
let buf = '';
|
|
199
|
+
process.stdin.setEncoding('utf-8');
|
|
200
|
+
process.stdin.on('data', (d) => { buf += d; });
|
|
201
|
+
process.stdin.on('end', () => {
|
|
202
|
+
result = classifyDiff(buf);
|
|
203
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
result = classifyFromGit(base);
|
|
207
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { parseDiff, decide, classifyDiff, classifyFromGit };
|
|
@@ -92,15 +92,18 @@ Count `.md` files under `.claude/guidance/` (recursive). Severity table:
|
|
|
92
92
|
|
|
93
93
|
### 1g. Guidance Structure (only if 1f found ≥1 file)
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
Invoke `/guidance -a` via the `Skill` tool to run the structural audit. The /guidance skill enforces the universal rules from `.claude/guidance/shipped/moflo-guidance-rules.md` (Purpose lines, See Also, generic H2s, hedged language, 500-line cap, RAG chunking) and is the single source of truth for those checks — never re-implement them here.
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
- Has `## See Also` at end
|
|
99
|
-
- Under 500 lines
|
|
100
|
-
- H2 headings are specific (not "Overview", "Configuration", "Examples")
|
|
101
|
-
- No hedged language in rule contexts (`should`, `might`, `consider`)
|
|
97
|
+
Fold the result into the Eldar report under the "Guidance structure" row:
|
|
102
98
|
|
|
103
|
-
|
|
99
|
+
| Outcome of `/guidance -a` | Eldar row severity |
|
|
100
|
+
|---------------------------|--------------------|
|
|
101
|
+
| 0 files with issues | ok |
|
|
102
|
+
| 1–2 files with issues | info |
|
|
103
|
+
| 3+ files with issues | warn |
|
|
104
|
+
| `/guidance` itself errors | warn — quote the error verbatim so the user can fix the offending file before re-running |
|
|
105
|
+
|
|
106
|
+
When the user is in `--fix` mode and chooses guidance fixes from the triage menu (3b), the same /guidance skill is the handoff target — so the audit and the fix flow share one implementation.
|
|
104
107
|
|
|
105
108
|
### 1h. Memory Health
|
|
106
109
|
|
|
@@ -204,7 +207,9 @@ TOP 3 RECOMMENDATIONS
|
|
|
204
207
|
2. Add Drizzle conventions guidance (info — high leverage)
|
|
205
208
|
You use Drizzle ORM but have no DB-conventions doc. This is the
|
|
206
209
|
single highest-leverage gap for getting Claude to write idiomatic
|
|
207
|
-
queries and migrations in your codebase.
|
|
210
|
+
queries and migrations in your codebase. /guidance -a (run inline
|
|
211
|
+
in step 1g) flagged 3 existing docs with structural issues; pick
|
|
212
|
+
one to fix alongside this new one.
|
|
208
213
|
See: .claude/guidance/shipped/moflo-guidance-rules.md
|
|
209
214
|
|
|
210
215
|
3. Run `flo healer --fix` (warn)
|
|
@@ -4,11 +4,27 @@ Phase-by-phase notes for the full `/flo <issue>` run. Phase 2 (Ticket) lives in
|
|
|
4
4
|
|
|
5
5
|
## Phase 1: Research (also `-r`)
|
|
6
6
|
|
|
7
|
-
### 1.1 Fetch the issue
|
|
7
|
+
### 1.1 Fetch the issue + history (cheap, before any file exploration)
|
|
8
|
+
|
|
9
|
+
Run these BEFORE any `Glob` / `Grep` / `Read` of source files. The goal is to catch "this is already (partially) fixed" in two commands rather than 10K tokens of file scanning.
|
|
10
|
+
|
|
8
11
|
```bash
|
|
9
|
-
|
|
12
|
+
# Issue + closing PRs (one call, one new field vs. before).
|
|
13
|
+
gh issue view <issue-number> --json number,title,body,labels,state,assignees,comments,milestone,closedByPullRequestsReferences
|
|
14
|
+
|
|
15
|
+
# Commits that reference the issue. Silently no-ops outside a git work tree —
|
|
16
|
+
# consumers without git, fresh `npx moflo` shells, non-git VCS all skip cleanly.
|
|
17
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
18
|
+
git log --all --grep="\b<issue-number>\b\|#<issue-number>" --oneline -30 || true
|
|
19
|
+
fi
|
|
10
20
|
```
|
|
11
21
|
|
|
22
|
+
**Surface what you find and proceed — never pause to ask.** `/flo` is fire-and-forget; a prompt that blocks for 30 minutes waiting on a yes/no is a worse failure than re-doing already-shipped work. Specifically:
|
|
23
|
+
|
|
24
|
+
- **Issue is CLOSED with non-empty `closedByPullRequestsReferences`** → read the closing PR body and merge commit as primary context. Treat the run as "look for any remaining work or follow-up" and continue. Do not stop.
|
|
25
|
+
- **Commits reference the issue but it's still open** → those are partial fixes. Summarise them in one line (`partial fix already shipped: <sha> <subject>`), then `git show <sha>` if you need the diff, scope the implementation around what's still missing, and continue. Do not stop.
|
|
26
|
+
- **No history found / scan skipped** → proceed silently to memory + code exploration as before.
|
|
27
|
+
|
|
12
28
|
### 1.2 Check ticket status
|
|
13
29
|
Look for the `## Acceptance Criteria` heading in the body.
|
|
14
30
|
- Present → ticket already enhanced; skip ahead to execute.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: guidance
|
|
3
3
|
description: Add, edit, or audit guidance docs in this project's .claude/guidance/ directory following moflo's universal guidance rules. Default mode walks the user through one doc (creating or improving it); the -a flag audits every doc in the directory and offers per-file improvements.
|
|
4
|
-
arguments: "[-a]
|
|
4
|
+
arguments: "[-a] <topic-or-path>"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# /guidance — Author and audit project guidance
|
|
@@ -15,7 +15,30 @@ Treat the union of staged + unstaged + committed-since-base as the diff to revie
|
|
|
15
15
|
|
|
16
16
|
Also note: was `/simplify` already run on this branch in this session? If yes, you're in a **validation pass** (Phase 2.5 below) — most of the heavy lifting is done.
|
|
17
17
|
|
|
18
|
-
## Phase 2: Classify the diff
|
|
18
|
+
## Phase 2: Classify the diff (deterministic — call the classifier)
|
|
19
|
+
|
|
20
|
+
**Call the classifier first, follow its decision.** Do not eyeball the diff and pick a tier in prose — that's the failure mode that costs ~230K tokens per run on mechanical decompositions (issue #908). The classifier reads the same diff Claude would, applies the rules below, and returns a JSON dispatch decision:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
node .claude/helpers/simplify-classify.cjs --base main
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
(In the moflo source repo, equivalent is `node bin/simplify-classify.cjs --base main`. The launcher syncs `bin/simplify-classify.cjs` → `.claude/helpers/simplify-classify.cjs` in consumer projects.)
|
|
27
|
+
|
|
28
|
+
Output:
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"tier": "TRIVIAL" | "SMALL" | "NORMAL",
|
|
32
|
+
"model": "sonnet",
|
|
33
|
+
"agentCount": 0 | 1 | 3,
|
|
34
|
+
"reasoning": ["..."],
|
|
35
|
+
"stats": { "added": ..., "deleted": ..., "declAdded": ..., "declRemoved": ..., "netDecls": ..., "fileCount": ..., "securityHit": ... }
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If `bin/simplify-classify.cjs` is missing (older moflo install), fall back to the prose rules below — but on a current install the classifier IS the source of truth. Default behavior: **single Sonnet agent** unless the diff signals genuinely warrant escalation.
|
|
40
|
+
|
|
41
|
+
Tier definitions the classifier encodes (for reference, not for re-derivation):
|
|
19
42
|
|
|
20
43
|
Pick the **smallest tier** the diff genuinely fits. When in doubt, escalate one step (not two).
|
|
21
44
|
|
|
@@ -40,13 +63,14 @@ This is the default tier for **most real diffs**, including changes to critical
|
|
|
40
63
|
|
|
41
64
|
Examples that qualify: extracting a constant, inlining a one-liner, swapping a `for` for a `forEach`, adding one early-return, refactoring a single function within a file, adding a cache fast-path inside an existing block.
|
|
42
65
|
|
|
43
|
-
### NORMAL — three parallel agents
|
|
44
|
-
Reserved for **genuinely cross-cutting** changes.
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
|
|
66
|
+
### NORMAL — three parallel agents (high bar)
|
|
67
|
+
Reserved for **genuinely cross-cutting** changes that single-agent review can't cover. The classifier escalates to NORMAL only when ANY of:
|
|
68
|
+
- `>500 LOC changed` (real volume, not just "more than 200")
|
|
69
|
+
- `5+ files AND ≥3 net new declarations` (broad new surface, not relocation)
|
|
70
|
+
- `security-sensitive path AND netDecls > 0` (aidefence/, swarm/consensus/, hooks gate, daemon-lock, launcher — only when adding logic, not on a 1-line touch)
|
|
71
|
+
- `3+ new files AND ≥5 new declarations` (genuinely new subsystem)
|
|
72
|
+
|
|
73
|
+
**Mechanical relocation is NOT NORMAL** even with many files / many lines. If `declAdded` and `declRemoved` are both ≥2 and `netDecls` is small (within 30% of total declarations touched), it's a structural move — SMALL, single agent. This is the #906/#908 case: ~330 LOC across 6 files of pure decomposition was costing 230K tokens via three-agent fan-out when it needed one Sonnet agent.
|
|
50
74
|
|
|
51
75
|
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.
|
|
52
76
|
|
|
@@ -62,48 +86,11 @@ Escalate one tier (self-review → SMALL agent) only if the fix introduced any o
|
|
|
62
86
|
|
|
63
87
|
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.
|
|
64
88
|
|
|
65
|
-
## Phase 2.7:
|
|
66
|
-
|
|
67
|
-
For every tier that spawns an Agent (SMALL / NORMAL — TRIVIAL self-review skips this), call the moflo router to pick the cheapest model that fits the task **before** invoking Agent:
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
mcp__moflo__hooks_model-route — {
|
|
71
|
-
task: "<diff summary — see wording rules below>",
|
|
72
|
-
preferCost: true
|
|
73
|
-
}
|
|
74
|
-
```
|
|
89
|
+
## Phase 2.7: Model selection
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
The router's complexity score is keyword-sensitive. Words like `refactor`, `architect`, `audit`, `system`, `redesign`, `migrate` flip a high-complexity flag and force opus *even when scoring suggests sonnet*. For `/simplify` you are **always doing code review**, never genuine architecture, so frame the task accordingly:
|
|
79
|
-
|
|
80
|
-
- ✅ Good: `"Review 110-line single-file change in bin/session-start-launcher.mjs for reuse, quality, efficiency."`
|
|
81
|
-
- ❌ Bad: `"Review refactor that adds mtime-cache fast-path and architects new caching layer."`
|
|
82
|
-
|
|
83
|
-
Drop the trigger words. State LOC count, file count, and "review for reuse, quality, efficiency". That's enough signal.
|
|
84
|
-
|
|
85
|
-
### Applying the result
|
|
86
|
-
|
|
87
|
-
The router returns `{ model: 'haiku' | 'sonnet' | 'opus', complexity, reasoning, alternatives, ... }`.
|
|
88
|
-
|
|
89
|
-
**Hard rule for `/simplify`: opus is never correct.** Code review does not require Opus-tier reasoning even on critical surface. If the router returns `opus`:
|
|
90
|
-
|
|
91
|
-
1. Look at `alternatives` — if `sonnet` scores higher than the selected model's confidence, downgrade to sonnet.
|
|
92
|
-
2. Otherwise, downgrade to sonnet anyway (treat opus as "router was uncertain — pick the safer middle").
|
|
93
|
-
|
|
94
|
-
Pass the final model verbatim to the Agent's `model` parameter (Agent accepts `'haiku' | 'sonnet' | 'opus'`). On router failure (MCP call errors), default to `'sonnet'`.
|
|
95
|
-
|
|
96
|
-
In practice: comment trims and pure formatting → haiku; everything else for `/simplify` → sonnet.
|
|
97
|
-
|
|
98
|
-
### Feed back the outcome
|
|
99
|
-
|
|
100
|
-
After the agent completes, record the outcome so the router learns:
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
mcp__moflo__hooks_model-outcome — { task: "<same wording as route call>", model: "<chosen>", outcome: "success" | "failure" | "escalated" }
|
|
104
|
-
```
|
|
91
|
+
**Use the model the classifier returned** — always `sonnet` for `/simplify`. Opus is never correct here; the classifier enforces this. No router call needed; the classifier IS the router for this skill.
|
|
105
92
|
|
|
106
|
-
|
|
93
|
+
If you fell back to prose rules in Phase 2 (no classifier available), use `sonnet` unconditionally. Pass the model verbatim to Agent's `model` parameter.
|
|
107
94
|
|
|
108
95
|
## Phase 3: Run the appropriate review
|
|
109
96
|
|
|
@@ -5,7 +5,7 @@ description: |
|
|
|
5
5
|
Use when the user wants to schedule, automate, or recurringly run one of THEIR spells locally —
|
|
6
6
|
e.g. "schedule the oap spell every hour", "run my audit spell every weekday at 9am", "fire X once tomorrow morning".
|
|
7
7
|
This is the LOCAL daemon path. For remote Anthropic-cloud agents, use /schedule instead.
|
|
8
|
-
arguments: "
|
|
8
|
+
arguments: "<spell-name-or-alias>"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# /spell-schedule — Schedule a Local Spell
|
package/bin/gate.cjs
CHANGED
|
@@ -88,7 +88,11 @@ var TASK_RE = /\b(fix|bug|error|implement|add|create|build|write|refactor|debug|
|
|
|
88
88
|
var TEST_RUNNER_RE = /(?:^|[^a-z])(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?(?:test|t)(?:[:\s]|$)|\b(?:npx|pnpx)\s+(?:vitest|jest|mocha|ava|tap|jasmine|pytest)\b|(?:^|;|&&|\|\|)\s*(?:vitest|jest|pytest|mocha|jasmine|tap|ava)\s|\b(?:cargo|go|deno|dotnet|mvn)\s+test\b|\bgradle\w*\s+test\b/i;
|
|
89
89
|
// Edits to these don't change runtime behaviour, so they don't invalidate prior test/simplify runs.
|
|
90
90
|
// Lock files and .gitignore are tracked but inert; package.json/*.yaml ARE source — they reset.
|
|
91
|
-
var
|
|
91
|
+
var EDIT_RESET_SKIP_BOTH_RE = /\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^|[\\\/])(CHANGELOG(?:\.md)?|\.env\.example|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/i;
|
|
92
|
+
// Test files: invalidate the testing gate (tests are stale once test code changes)
|
|
93
|
+
// but NOT the simplify gate — /simplify already reviewed the production code; touching
|
|
94
|
+
// a test file or fixture doesn't expose new untested surface for code review (#908).
|
|
95
|
+
var EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE = /(?:^|[\\\/])(__tests__|__mocks__|tests?|spec|specs|cypress|e2e|fixtures?)[\\\/]|\.(test|spec)\.[mc]?[jt]sx?$|\.fixture\.[mc]?[jt]sx?$/i;
|
|
92
96
|
|
|
93
97
|
switch (command) {
|
|
94
98
|
case 'check-before-agent': {
|
|
@@ -180,11 +184,20 @@ switch (command) {
|
|
|
180
184
|
}
|
|
181
185
|
case 'reset-edit-gates': {
|
|
182
186
|
var fp = process.env.TOOL_INPUT_file_path || '';
|
|
183
|
-
|
|
187
|
+
// Inert files (markdown, lockfiles, CHANGELOG, .env.example): no gate reset.
|
|
188
|
+
if (fp && EDIT_RESET_SKIP_BOTH_RE.test(fp)) break;
|
|
184
189
|
var s = readState();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
190
|
+
// Test-only edits invalidate testsRun but preserve simplifyRun (#908).
|
|
191
|
+
var isTestOnly = fp && EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE.test(fp);
|
|
192
|
+
var resetTests = s.testsRun;
|
|
193
|
+
var resetSimplify = s.simplifyRun && !isTestOnly;
|
|
194
|
+
if (!resetTests && !resetSimplify) break;
|
|
195
|
+
var gates = [];
|
|
196
|
+
if (resetTests) { s.testsRun = false; gates.push('tests'); }
|
|
197
|
+
if (resetSimplify) { s.simplifyRun = false; gates.push('simplify'); }
|
|
198
|
+
if (fp) {
|
|
199
|
+
s.lastResetBy = { file: fp, at: new Date().toISOString(), gates: gates };
|
|
200
|
+
}
|
|
188
201
|
writeState(s);
|
|
189
202
|
break;
|
|
190
203
|
}
|
|
@@ -205,6 +218,9 @@ switch (command) {
|
|
|
205
218
|
for (var i = 0; i < missing.length; i++) {
|
|
206
219
|
process.stderr.write(' - ' + missing[i] + '\n');
|
|
207
220
|
}
|
|
221
|
+
if (s.lastResetBy && s.lastResetBy.file) {
|
|
222
|
+
process.stderr.write('Last gate reset: ' + s.lastResetBy.file + ' (' + (s.lastResetBy.gates || []).join(', ') + ')\n');
|
|
223
|
+
}
|
|
208
224
|
process.stderr.write('Disable per-gate via moflo.yaml:\n');
|
|
209
225
|
process.stderr.write(' gates:\n testing_gate: false\n simplify_gate: false\n learnings_gate: false\n');
|
|
210
226
|
process.exit(2);
|
|
@@ -595,7 +595,7 @@ try {
|
|
|
595
595
|
|
|
596
596
|
// Gate and hook helpers — shipped as static files in bin/
|
|
597
597
|
const binHelperFiles = [
|
|
598
|
-
'gate.cjs', 'gate-hook.mjs', 'prompt-hook.mjs', 'hook-handler.cjs',
|
|
598
|
+
'gate.cjs', 'gate-hook.mjs', 'prompt-hook.mjs', 'hook-handler.cjs', 'simplify-classify.cjs',
|
|
599
599
|
];
|
|
600
600
|
for (const file of binHelperFiles) {
|
|
601
601
|
await syncFile(resolve(binDir, file), resolve(helpersDir, file), `.claude/helpers/${file}`);
|