moflo 4.9.18 → 4.9.20
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/simplify.md +11 -13
- package/.claude/helpers/gate.cjs +100 -5
- package/.claude/helpers/simplify-classify.cjs +4 -1
- package/.claude/skills/simplify/SKILL.md +13 -11
- package/bin/gate.cjs +100 -5
- package/bin/session-start-launcher.mjs +47 -0
- package/bin/simplify-classify.cjs +4 -1
- package/dist/src/cli/commands/spell-credentials.js +248 -0
- package/dist/src/cli/commands/spell.js +5 -3
- package/dist/src/cli/services/moflo-paths.js +5 -0
- package/dist/src/cli/spells/commands/prompt-command.js +45 -0
- package/dist/src/cli/spells/core/prerequisite-checker.js +89 -26
- package/dist/src/cli/spells/core/runner.js +4 -3
- package/dist/src/cli/spells/credentials/credential-store.js +20 -4
- package/dist/src/cli/spells/credentials/default-store.js +126 -0
- package/dist/src/cli/spells/credentials/index.js +6 -0
- package/dist/src/cli/spells/factory/runner-bridge.js +5 -2
- package/dist/src/cli/spells/factory/runner-factory.js +4 -2
- package/dist/src/cli/spells/index.js +1 -1
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -34,20 +34,15 @@ Pick the smallest tier the diff genuinely fits.
|
|
|
34
34
|
|
|
35
35
|
Critical-surface files (launcher, hooks, MCP wiring) raise the *care* of the agent prompt — sharper checklist, blast-radius framing — they do **not** automatically escalate to NORMAL. Risk-weighted ≠ headcount-weighted.
|
|
36
36
|
|
|
37
|
-
## Phase 3:
|
|
37
|
+
## Phase 3: Use the classifier's model (skip for TRIVIAL)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
The classifier returns the right model for the tier — no separate router call needed:
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
preferCost: true
|
|
45
|
-
}
|
|
46
|
-
```
|
|
41
|
+
- `sonnet` (default) — real logic edits, single agent or 3-agent fan-out.
|
|
42
|
+
- `haiku` — mostly-relocation diffs (mechanical moves; pattern-matching beats deep reasoning).
|
|
43
|
+
- `opus` — never returned. Code review is breadth-bound, not depth-bound; sonnet 3-way IS the high-effort tier.
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
**Hard rule for `/simplify`: opus is never correct.** Code review never needs Opus reasoning, even on critical surface. If the router returns `opus`, downgrade to `sonnet`. On router failure, default to `sonnet`. Comment trims and pure formatting → `haiku`.
|
|
45
|
+
Pass the classifier's `model` field verbatim to Agent's `model` parameter. If you fell back to prose rules in Phase 2 (no classifier), default to `sonnet`.
|
|
51
46
|
|
|
52
47
|
## Phase 4: Validation pass (re-run after fixes from a prior simplify)
|
|
53
48
|
|
|
@@ -57,8 +52,11 @@ Escalate one tier (self-review → SMALL agent) only if the fix introduced a new
|
|
|
57
52
|
|
|
58
53
|
## Phase 5: Run the appropriate review
|
|
59
54
|
|
|
60
|
-
### TRIVIAL
|
|
61
|
-
|
|
55
|
+
### TRIVIAL
|
|
56
|
+
Print one confirmation line (`simplify: TRIVIAL — N LOC, 1 file — stamped`) and exit. **Do not** walk the three-category check; the classifier already concluded the diff is below the review-value threshold. Budget: <5 seconds, no Agent.
|
|
57
|
+
|
|
58
|
+
### Validation pass
|
|
59
|
+
Run the three category checks against the post-fix diff in one pass. Most are clean — confirm and exit. Budget: ~30 seconds, no Agent.
|
|
62
60
|
|
|
63
61
|
### SMALL — one agent
|
|
64
62
|
```
|
package/.claude/helpers/gate.cjs
CHANGED
|
@@ -7,7 +7,7 @@ var cp = require('child_process');
|
|
|
7
7
|
var PROJECT_DIR = (process.env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
|
|
8
8
|
var STATE_FILE = path.join(PROJECT_DIR, '.claude', 'workflow-state.json');
|
|
9
9
|
|
|
10
|
-
var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, interactionCount: 0, sessionStart: null, lastBlockedAt: null };
|
|
10
|
+
var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, simplifySnapshotSha: null, interactionCount: 0, sessionStart: null, lastBlockedAt: null };
|
|
11
11
|
|
|
12
12
|
// Per-actor memory-search tracking (#838). The legacy `memorySearched` boolean
|
|
13
13
|
// is session-wide, so once the parent searches memory, every spawned subagent
|
|
@@ -100,6 +100,79 @@ var EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE = /(?:^|[\\\/])(__tests__|__mocks__|tests?|
|
|
|
100
100
|
// on purpose — those are inert for edit-reset (above) but not "documentation".
|
|
101
101
|
var DOCS_ONLY_RE = /\.(md|markdown|txt|rst|adoc|html?|pdf|png|jpe?g|gif|svg|webp|ico|bmp)$/i;
|
|
102
102
|
|
|
103
|
+
// Classifier-aware simplify gate skip. Returns a string reason if the gate
|
|
104
|
+
// can be auto-passed, or null if /simplify must run. Uses simplify-classify.cjs
|
|
105
|
+
// so the gate's "trivial" definition matches the skill's exactly.
|
|
106
|
+
//
|
|
107
|
+
// Two paths:
|
|
108
|
+
// 1. snapshot path — /simplify ran earlier on this branch. Classify the diff
|
|
109
|
+
// between simplifySnapshotSha and current HEAD/working-tree. If TRIVIAL,
|
|
110
|
+
// the prior review still covers the branch — no re-run needed.
|
|
111
|
+
// 2. baseline path — no snapshot (first time). Classify the entire branch
|
|
112
|
+
// diff vs merge-base. If TRIVIAL, the whole PR is below the threshold
|
|
113
|
+
// where /simplify provides value — auto-pass without ever invoking it.
|
|
114
|
+
//
|
|
115
|
+
// Fail-safe: any error (no classifier, no git, no merge-base) returns null,
|
|
116
|
+
// which forces /simplify to run as today.
|
|
117
|
+
function classifyForGateSkip(state) {
|
|
118
|
+
var classify;
|
|
119
|
+
try {
|
|
120
|
+
classify = require('./simplify-classify.cjs').classifyDiff;
|
|
121
|
+
} catch (e) { return null; }
|
|
122
|
+
if (typeof classify !== 'function') return null;
|
|
123
|
+
|
|
124
|
+
function tryClassify(diffText, label) {
|
|
125
|
+
try {
|
|
126
|
+
var dec = classify(diffText);
|
|
127
|
+
if (dec.tier === 'TRIVIAL') {
|
|
128
|
+
var loc = (dec.stats.added || 0) + (dec.stats.deleted || 0);
|
|
129
|
+
return label + ' is TRIVIAL (' + loc + ' LOC, ' + (dec.stats.fileCount || 0) + ' file(s))';
|
|
130
|
+
}
|
|
131
|
+
} catch (e) { /* fall through */ }
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function gitDiff(args) {
|
|
136
|
+
try {
|
|
137
|
+
return cp.execFileSync('git', args, {
|
|
138
|
+
cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 5000, windowsHide: true,
|
|
139
|
+
stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 8 * 1024 * 1024
|
|
140
|
+
});
|
|
141
|
+
} catch (e) { return null; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Snapshot path: classify everything since /simplify last ran.
|
|
145
|
+
if (state.simplifySnapshotSha) {
|
|
146
|
+
var snapDiff = gitDiff(['diff', state.simplifySnapshotSha + '...HEAD']);
|
|
147
|
+
var workTreeA = gitDiff(['diff', 'HEAD']) || '';
|
|
148
|
+
if (snapDiff !== null) {
|
|
149
|
+
var combined = snapDiff + (workTreeA ? '\n' + workTreeA : '');
|
|
150
|
+
var hit = tryClassify(combined, 'delta since last /simplify');
|
|
151
|
+
if (hit) return hit;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Baseline path: classify the whole branch vs merge-base.
|
|
156
|
+
var bases = ['origin/main', 'main', 'origin/master', 'master'];
|
|
157
|
+
for (var i = 0; i < bases.length; i++) {
|
|
158
|
+
var base;
|
|
159
|
+
try {
|
|
160
|
+
base = cp.execFileSync('git', ['merge-base', 'HEAD', bases[i]], {
|
|
161
|
+
cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
|
|
162
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
163
|
+
}).trim();
|
|
164
|
+
} catch (e) { continue; }
|
|
165
|
+
if (!base) continue;
|
|
166
|
+
var branchDiff = gitDiff(['diff', base + '...HEAD']);
|
|
167
|
+
var workTreeB = gitDiff(['diff', 'HEAD']) || '';
|
|
168
|
+
if (branchDiff !== null) {
|
|
169
|
+
return tryClassify(branchDiff + (workTreeB ? '\n' + workTreeB : ''), 'branch diff');
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
103
176
|
// Get the file list changed on the current branch vs the merge-base with origin/main
|
|
104
177
|
// (falling back to local main). Returns an array of repo-relative paths, or null on
|
|
105
178
|
// failure — in which case callers MUST fall through to the standard gate (fail-safe).
|
|
@@ -206,10 +279,19 @@ switch (command) {
|
|
|
206
279
|
case 'record-skill-run': {
|
|
207
280
|
if ((process.env.TOOL_INPUT_skill || '') === 'simplify') {
|
|
208
281
|
var s = readState();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
282
|
+
var changed = false;
|
|
283
|
+
if (!s.simplifyRun) { s.simplifyRun = true; changed = true; }
|
|
284
|
+
// Snapshot HEAD so check-before-pr can classify delta-since-simplify and
|
|
285
|
+
// skip a redundant /simplify re-run when only trivial fixes followed.
|
|
286
|
+
// Non-fatal — gate falls through to current behaviour without the snapshot.
|
|
287
|
+
try {
|
|
288
|
+
var sha = cp.execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
289
|
+
cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
|
|
290
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
291
|
+
}).trim();
|
|
292
|
+
if (sha && s.simplifySnapshotSha !== sha) { s.simplifySnapshotSha = sha; changed = true; }
|
|
293
|
+
} catch (e) { /* no git or detached state — skip snapshot, gate still works */ }
|
|
294
|
+
if (changed) writeState(s);
|
|
213
295
|
}
|
|
214
296
|
break;
|
|
215
297
|
}
|
|
@@ -249,6 +331,19 @@ switch (command) {
|
|
|
249
331
|
break;
|
|
250
332
|
}
|
|
251
333
|
var s = readState();
|
|
334
|
+
// Classifier-aware skip: if delta-since-snapshot or whole-branch diff is
|
|
335
|
+
// TRIVIAL, satisfy the simplify gate silently. Reuses the same classifier
|
|
336
|
+
// the skill uses — same "trivial" definition, no drift. Same threshold that
|
|
337
|
+
// already maps to TRIVIAL=0 agents inside /simplify, so trusting it at the
|
|
338
|
+
// gate level is the same trust profile, just one decision earlier.
|
|
339
|
+
if (config.simplify_gate && !s.simplifyRun) {
|
|
340
|
+
var skipReason = classifyForGateSkip(s);
|
|
341
|
+
if (skipReason) {
|
|
342
|
+
s.simplifyRun = true;
|
|
343
|
+
writeState(s);
|
|
344
|
+
process.stdout.write('Simplify gate auto-passed: ' + skipReason + '\n');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
252
347
|
var missing = [];
|
|
253
348
|
if (config.testing_gate && !s.testsRun) missing.push('tests have not run since the last code edit (run npm test, vitest, jest, pytest, or similar)');
|
|
254
349
|
if (config.simplify_gate && !s.simplifyRun) missing.push('/simplify has not run since the last code edit');
|
|
@@ -159,7 +159,10 @@ function decide(stats) {
|
|
|
159
159
|
reasoning.push(
|
|
160
160
|
`mostly relocation: ${stats.declAdded} decls added, ${stats.declRemoved} removed, net ${stats.netDecls >= 0 ? '+' : ''}${stats.netDecls}`,
|
|
161
161
|
);
|
|
162
|
-
|
|
162
|
+
// Haiku is sufficient for mechanical moves: code already existed and worked,
|
|
163
|
+
// so review reduces to copy-paste-divergence / dead-after-move pattern checks
|
|
164
|
+
// — exactly haiku's strength. ~5x cheaper than sonnet on relocation-shape diffs.
|
|
165
|
+
return { tier: 'SMALL', model: 'haiku', agentCount: 1, reasoning, stats };
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
// Escalation triggers — any one trips NORMAL (3 agents).
|
|
@@ -42,13 +42,8 @@ Tier definitions the classifier encodes (for reference, not for re-derivation):
|
|
|
42
42
|
|
|
43
43
|
Pick the **smallest tier** the diff genuinely fits. When in doubt, escalate one step (not two).
|
|
44
44
|
|
|
45
|
-
### TRIVIAL —
|
|
46
|
-
|
|
47
|
-
- ≤10 net LOC changed (insertions + deletions, excluding pure whitespace)
|
|
48
|
-
- Single file
|
|
49
|
-
- No logic changes — only comments, formatting, renames of local vars, JSDoc, or string-literal edits
|
|
50
|
-
- No new imports, no new exports, no new function/class declarations
|
|
51
|
-
- No removed safety checks, error handlers, or guards
|
|
45
|
+
### TRIVIAL — gate stamp, no review
|
|
46
|
+
The classifier already proved the diff is below the threshold where review provides value (≤10 net LOC, single file, no declaration/import/export changes, no removed guards). **Stamp the gate with one line of confirmation and exit immediately.** Do not walk the three-category check yourself — the classifier IS the review at this tier, and at the gate level the classifier-aware skip (`bin/gate.cjs:classifyForGateSkip`) often satisfies the gate without invoking this skill at all.
|
|
52
47
|
|
|
53
48
|
Examples that qualify: trimming a comment, fixing a typo in a log message, renaming a private helper, reformatting a single block.
|
|
54
49
|
Examples that DON'T qualify: changing an `if` condition, reordering function args, deleting a try/catch.
|
|
@@ -88,14 +83,21 @@ Do **not** escalate to NORMAL on a validation pass. If the fix is so structural
|
|
|
88
83
|
|
|
89
84
|
## Phase 2.7: Model selection
|
|
90
85
|
|
|
91
|
-
**Use the model the classifier returned
|
|
86
|
+
**Use the model the classifier returned.** The classifier IS the router for this skill — no separate router call needed. Possible outputs:
|
|
87
|
+
|
|
88
|
+
- `sonnet` (default) — real logic changes, single agent or three-agent fan-out.
|
|
89
|
+
- `haiku` — mostly-relocation diffs (mechanical moves where pattern-matching beats deep reasoning, ~5x cheaper).
|
|
90
|
+
- `opus` — never. Code review is breadth-bound, not depth-bound; the three-agent fan-out at sonnet is the high-effort tier.
|
|
92
91
|
|
|
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.
|
|
92
|
+
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.
|
|
94
93
|
|
|
95
94
|
## Phase 3: Run the appropriate review
|
|
96
95
|
|
|
97
|
-
### TRIVIAL
|
|
98
|
-
|
|
96
|
+
### TRIVIAL: stamp and exit
|
|
97
|
+
The classifier proved the diff is below the review-value threshold. Print one confirmation line (e.g. `simplify: TRIVIAL — N LOC, 1 file — stamped`) and exit. **Do not** walk the three-category check; the classifier already concluded the answer is "clean." Budget: <5 seconds, no Agent calls.
|
|
98
|
+
|
|
99
|
+
### Validation pass: confirm clean
|
|
100
|
+
Run the three category checks (reuse / quality / efficiency) yourself, in one pass, against the post-fix diff. Most validation passes are clean — the goal is to confirm fixes didn't introduce new concerns, not to fan out. Budget: ~30 seconds, no Agent calls.
|
|
99
101
|
|
|
100
102
|
### SMALL: one agent (model from router)
|
|
101
103
|
Launch a SINGLE Agent with subagent_type `reviewer`, passing the model returned by Phase 2.7's router call. Cap the agent's tool budget by being explicit:
|
package/bin/gate.cjs
CHANGED
|
@@ -7,7 +7,7 @@ var cp = require('child_process');
|
|
|
7
7
|
var PROJECT_DIR = (process.env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
|
|
8
8
|
var STATE_FILE = path.join(PROJECT_DIR, '.claude', 'workflow-state.json');
|
|
9
9
|
|
|
10
|
-
var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, interactionCount: 0, sessionStart: null, lastBlockedAt: null };
|
|
10
|
+
var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, simplifySnapshotSha: null, interactionCount: 0, sessionStart: null, lastBlockedAt: null };
|
|
11
11
|
|
|
12
12
|
// Per-actor memory-search tracking (#838). The legacy `memorySearched` boolean
|
|
13
13
|
// is session-wide, so once the parent searches memory, every spawned subagent
|
|
@@ -100,6 +100,79 @@ var EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE = /(?:^|[\\\/])(__tests__|__mocks__|tests?|
|
|
|
100
100
|
// on purpose — those are inert for edit-reset (above) but not "documentation".
|
|
101
101
|
var DOCS_ONLY_RE = /\.(md|markdown|txt|rst|adoc|html?|pdf|png|jpe?g|gif|svg|webp|ico|bmp)$/i;
|
|
102
102
|
|
|
103
|
+
// Classifier-aware simplify gate skip. Returns a string reason if the gate
|
|
104
|
+
// can be auto-passed, or null if /simplify must run. Uses simplify-classify.cjs
|
|
105
|
+
// so the gate's "trivial" definition matches the skill's exactly.
|
|
106
|
+
//
|
|
107
|
+
// Two paths:
|
|
108
|
+
// 1. snapshot path — /simplify ran earlier on this branch. Classify the diff
|
|
109
|
+
// between simplifySnapshotSha and current HEAD/working-tree. If TRIVIAL,
|
|
110
|
+
// the prior review still covers the branch — no re-run needed.
|
|
111
|
+
// 2. baseline path — no snapshot (first time). Classify the entire branch
|
|
112
|
+
// diff vs merge-base. If TRIVIAL, the whole PR is below the threshold
|
|
113
|
+
// where /simplify provides value — auto-pass without ever invoking it.
|
|
114
|
+
//
|
|
115
|
+
// Fail-safe: any error (no classifier, no git, no merge-base) returns null,
|
|
116
|
+
// which forces /simplify to run as today.
|
|
117
|
+
function classifyForGateSkip(state) {
|
|
118
|
+
var classify;
|
|
119
|
+
try {
|
|
120
|
+
classify = require('./simplify-classify.cjs').classifyDiff;
|
|
121
|
+
} catch (e) { return null; }
|
|
122
|
+
if (typeof classify !== 'function') return null;
|
|
123
|
+
|
|
124
|
+
function tryClassify(diffText, label) {
|
|
125
|
+
try {
|
|
126
|
+
var dec = classify(diffText);
|
|
127
|
+
if (dec.tier === 'TRIVIAL') {
|
|
128
|
+
var loc = (dec.stats.added || 0) + (dec.stats.deleted || 0);
|
|
129
|
+
return label + ' is TRIVIAL (' + loc + ' LOC, ' + (dec.stats.fileCount || 0) + ' file(s))';
|
|
130
|
+
}
|
|
131
|
+
} catch (e) { /* fall through */ }
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function gitDiff(args) {
|
|
136
|
+
try {
|
|
137
|
+
return cp.execFileSync('git', args, {
|
|
138
|
+
cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 5000, windowsHide: true,
|
|
139
|
+
stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 8 * 1024 * 1024
|
|
140
|
+
});
|
|
141
|
+
} catch (e) { return null; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Snapshot path: classify everything since /simplify last ran.
|
|
145
|
+
if (state.simplifySnapshotSha) {
|
|
146
|
+
var snapDiff = gitDiff(['diff', state.simplifySnapshotSha + '...HEAD']);
|
|
147
|
+
var workTreeA = gitDiff(['diff', 'HEAD']) || '';
|
|
148
|
+
if (snapDiff !== null) {
|
|
149
|
+
var combined = snapDiff + (workTreeA ? '\n' + workTreeA : '');
|
|
150
|
+
var hit = tryClassify(combined, 'delta since last /simplify');
|
|
151
|
+
if (hit) return hit;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Baseline path: classify the whole branch vs merge-base.
|
|
156
|
+
var bases = ['origin/main', 'main', 'origin/master', 'master'];
|
|
157
|
+
for (var i = 0; i < bases.length; i++) {
|
|
158
|
+
var base;
|
|
159
|
+
try {
|
|
160
|
+
base = cp.execFileSync('git', ['merge-base', 'HEAD', bases[i]], {
|
|
161
|
+
cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
|
|
162
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
163
|
+
}).trim();
|
|
164
|
+
} catch (e) { continue; }
|
|
165
|
+
if (!base) continue;
|
|
166
|
+
var branchDiff = gitDiff(['diff', base + '...HEAD']);
|
|
167
|
+
var workTreeB = gitDiff(['diff', 'HEAD']) || '';
|
|
168
|
+
if (branchDiff !== null) {
|
|
169
|
+
return tryClassify(branchDiff + (workTreeB ? '\n' + workTreeB : ''), 'branch diff');
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
103
176
|
// Get the file list changed on the current branch vs the merge-base with origin/main
|
|
104
177
|
// (falling back to local main). Returns an array of repo-relative paths, or null on
|
|
105
178
|
// failure — in which case callers MUST fall through to the standard gate (fail-safe).
|
|
@@ -206,10 +279,19 @@ switch (command) {
|
|
|
206
279
|
case 'record-skill-run': {
|
|
207
280
|
if ((process.env.TOOL_INPUT_skill || '') === 'simplify') {
|
|
208
281
|
var s = readState();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
282
|
+
var changed = false;
|
|
283
|
+
if (!s.simplifyRun) { s.simplifyRun = true; changed = true; }
|
|
284
|
+
// Snapshot HEAD so check-before-pr can classify delta-since-simplify and
|
|
285
|
+
// skip a redundant /simplify re-run when only trivial fixes followed.
|
|
286
|
+
// Non-fatal — gate falls through to current behaviour without the snapshot.
|
|
287
|
+
try {
|
|
288
|
+
var sha = cp.execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
289
|
+
cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
|
|
290
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
291
|
+
}).trim();
|
|
292
|
+
if (sha && s.simplifySnapshotSha !== sha) { s.simplifySnapshotSha = sha; changed = true; }
|
|
293
|
+
} catch (e) { /* no git or detached state — skip snapshot, gate still works */ }
|
|
294
|
+
if (changed) writeState(s);
|
|
213
295
|
}
|
|
214
296
|
break;
|
|
215
297
|
}
|
|
@@ -249,6 +331,19 @@ switch (command) {
|
|
|
249
331
|
break;
|
|
250
332
|
}
|
|
251
333
|
var s = readState();
|
|
334
|
+
// Classifier-aware skip: if delta-since-snapshot or whole-branch diff is
|
|
335
|
+
// TRIVIAL, satisfy the simplify gate silently. Reuses the same classifier
|
|
336
|
+
// the skill uses — same "trivial" definition, no drift. Same threshold that
|
|
337
|
+
// already maps to TRIVIAL=0 agents inside /simplify, so trusting it at the
|
|
338
|
+
// gate level is the same trust profile, just one decision earlier.
|
|
339
|
+
if (config.simplify_gate && !s.simplifyRun) {
|
|
340
|
+
var skipReason = classifyForGateSkip(s);
|
|
341
|
+
if (skipReason) {
|
|
342
|
+
s.simplifyRun = true;
|
|
343
|
+
writeState(s);
|
|
344
|
+
process.stdout.write('Simplify gate auto-passed: ' + skipReason + '\n');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
252
347
|
var missing = [];
|
|
253
348
|
if (config.testing_gate && !s.testsRun) missing.push('tests have not run since the last code edit (run npm test, vitest, jest, pytest, or similar)');
|
|
254
349
|
if (config.simplify_gate && !s.simplifyRun) missing.push('/simplify has not run since the last code edit');
|
|
@@ -53,6 +53,36 @@ function findProjectRoot() {
|
|
|
53
53
|
|
|
54
54
|
const projectRoot = findProjectRoot();
|
|
55
55
|
|
|
56
|
+
// Dogfood guard (#928). When this launcher runs inside the moflo repo itself,
|
|
57
|
+
// .claude/scripts/, .claude/helpers/, and .claude/guidance/ are committed git
|
|
58
|
+
// files — they ARE moflo's source of truth, not destinations to be re-synced
|
|
59
|
+
// from node_modules. Drift heal would silently revert any post-publish edit
|
|
60
|
+
// to one of those files (e.g. story #927's gate.cjs got reverted overnight
|
|
61
|
+
// because manifest.size still pointed at the previously-published version).
|
|
62
|
+
// Detection is `package.json#name === "moflo"` — the project's own package.json,
|
|
63
|
+
// NOT node_modules/moflo/package.json. Defaults to false on any read/parse
|
|
64
|
+
// error so a corrupt package.json never silently disables drift heal in a
|
|
65
|
+
// real consumer.
|
|
66
|
+
//
|
|
67
|
+
// Workspace caveat: findProjectRoot() walks up to the nearest package.json,
|
|
68
|
+
// so in a workspace child (packages/foo/) the read sees the child package
|
|
69
|
+
// (name !== "moflo") and the guard stays false. moflo isn't a workspace
|
|
70
|
+
// today; if it ever becomes one, run sessions from the repo root or extend
|
|
71
|
+
// this check to walk further up.
|
|
72
|
+
let isMofloDogfood = false;
|
|
73
|
+
try {
|
|
74
|
+
const projectPkgPath = resolve(projectRoot, 'package.json');
|
|
75
|
+
if (existsSync(projectPkgPath)) {
|
|
76
|
+
const projectPkg = JSON.parse(readFileSync(projectPkgPath, 'utf-8'));
|
|
77
|
+
isMofloDogfood = projectPkg?.name === 'moflo';
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// Defaults to false — safer than accidentally disabling drift heal in a
|
|
81
|
+
// real consumer. Surface the failure so a corrupt project package.json
|
|
82
|
+
// doesn't silently change launcher behavior (per feedback_no_silent_failures).
|
|
83
|
+
process.stderr.write(`[moflo] dogfood-guard package.json read failed: ${err && err.message ? err.message : String(err)}\n`);
|
|
84
|
+
}
|
|
85
|
+
|
|
56
86
|
// Visible mutation reporter (#716). Claude Code's SessionStart hook captures
|
|
57
87
|
// stdout as `additionalContext`, so each line here surfaces to Claude — and
|
|
58
88
|
// through it to the user — explaining what the launcher just changed. Keep
|
|
@@ -360,6 +390,8 @@ try {
|
|
|
360
390
|
}
|
|
361
391
|
}
|
|
362
392
|
}
|
|
393
|
+
// Dogfood (#928): never drift-heal moflo's own committed copies.
|
|
394
|
+
if (isMofloDogfood) manifestDrifted = false;
|
|
363
395
|
|
|
364
396
|
if (installedVersion !== cachedVersion || manifestDrifted) {
|
|
365
397
|
if (installedVersion !== cachedVersion) {
|
|
@@ -444,6 +476,20 @@ try {
|
|
|
444
476
|
|
|
445
477
|
const binDir = resolve(projectRoot, 'node_modules/moflo/bin');
|
|
446
478
|
|
|
479
|
+
// Dogfood (#928): in moflo's own repo, the destinations under
|
|
480
|
+
// .claude/scripts/, .claude/helpers/, .claude/guidance/ are committed
|
|
481
|
+
// git files — copying node_modules/moflo content over them clobbers
|
|
482
|
+
// in-flight work (the same bug that silently reverted #927 between
|
|
483
|
+
// commit and publish). Skip the sync, cleanup, and manifest write
|
|
484
|
+
// entirely; queue the version-stamp write so we don't re-enter this
|
|
485
|
+
// branch on every subsequent session. Daemon recycle still happens
|
|
486
|
+
// (the stopDaemon call earlier handled this) and 3a-pre will spawn a
|
|
487
|
+
// fresh daemon under the new code.
|
|
488
|
+
if (isMofloDogfood) {
|
|
489
|
+
pendingVersionStampWrite = { path: versionStampPath, version: installedVersion };
|
|
490
|
+
emitMutation('skipped file-sync', 'moflo dogfood — committed dogfood copies preserved');
|
|
491
|
+
} else {
|
|
492
|
+
|
|
447
493
|
// ── Manifest-based auto-update ──────────────────────────────────────
|
|
448
494
|
//
|
|
449
495
|
// IMPORTANT: Every file moflo installs into the destination project
|
|
@@ -709,6 +755,7 @@ try {
|
|
|
709
755
|
// queued for 3g.
|
|
710
756
|
emitWarning(`manifest write failed (${errMessage(err)})`);
|
|
711
757
|
}
|
|
758
|
+
} // end !isMofloDogfood file-sync branch (#928)
|
|
712
759
|
}
|
|
713
760
|
}
|
|
714
761
|
} catch (err) {
|
|
@@ -159,7 +159,10 @@ function decide(stats) {
|
|
|
159
159
|
reasoning.push(
|
|
160
160
|
`mostly relocation: ${stats.declAdded} decls added, ${stats.declRemoved} removed, net ${stats.netDecls >= 0 ? '+' : ''}${stats.netDecls}`,
|
|
161
161
|
);
|
|
162
|
-
|
|
162
|
+
// Haiku is sufficient for mechanical moves: code already existed and worked,
|
|
163
|
+
// so review reduces to copy-paste-divergence / dead-after-move pattern checks
|
|
164
|
+
// — exactly haiku's strength. ~5x cheaper than sonnet on relocation-shape diffs.
|
|
165
|
+
return { tier: 'SMALL', model: 'haiku', agentCount: 1, reasoning, stats };
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
// Escalation triggers — any one trips NORMAL (3 agents).
|