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.
@@ -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: Route the model (skip for TRIVIAL)
37
+ ## Phase 3: Use the classifier's model (skip for TRIVIAL)
38
38
 
39
- Before spawning any Agent, ask the moflo router which model to use:
39
+ The classifier returns the right model for the tier no separate router call needed:
40
40
 
41
- ```
42
- mcp__moflo__hooks_model-route{
43
- task: "Review N-line change in <files> for reuse, quality, efficiency",
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
- **Wording rules:** the router's complexity score is keyword-sensitive. Avoid `refactor`, `architect`, `audit`, `system`, `redesign`, `migrate` those force opus even when scoring suggests sonnet. State LOC count, file count, and "review for reuse, quality, efficiency". Nothing more.
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 / Validation
61
- Run the three category checks (reuse / quality / efficiency) yourself in one pass against the diff. Most TRIVIAL diffs are clean confirm and exit. Budget: ~30 seconds, no Agent.
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
  ```
@@ -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
- if (!s.simplifyRun) {
210
- s.simplifyRun = true;
211
- writeState(s);
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
- return { tier: 'SMALL', model: 'sonnet', agentCount: 1, reasoning, stats };
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 — self-review, no agent spawn
46
- ALL of these must hold:
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** 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.
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 / Validation: self-review
98
- Run the same three category checks (reuse / quality / efficiency) yourself, in one pass, against the diff. Most TRIVIAL and validation diffs will be clean the goal is to confirm, not to fan out. If you find an issue, fix it; otherwise stamp clean. Total budget: ~30 seconds, no Agent calls. No router call needed.
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 filestamped`) 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
- if (!s.simplifyRun) {
210
- s.simplifyRun = true;
211
- writeState(s);
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
- return { tier: 'SMALL', model: 'sonnet', agentCount: 1, reasoning, stats };
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).