moflo 4.9.17 → 4.9.19
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 +140 -5
- package/.claude/helpers/simplify-classify.cjs +4 -1
- package/.claude/skills/simplify/SKILL.md +13 -11
- package/bin/gate.cjs +140 -5
- package/bin/simplify-classify.cjs +4 -1
- package/dist/src/cli/commands/doctor-fixes.js +62 -16
- 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
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
var fs = require('fs');
|
|
4
4
|
var path = require('path');
|
|
5
|
+
var cp = require('child_process');
|
|
5
6
|
|
|
6
7
|
var PROJECT_DIR = (process.env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
|
|
7
8
|
var STATE_FILE = path.join(PROJECT_DIR, '.claude', 'workflow-state.json');
|
|
8
9
|
|
|
9
|
-
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 };
|
|
10
11
|
|
|
11
12
|
// Per-actor memory-search tracking (#838). The legacy `memorySearched` boolean
|
|
12
13
|
// is session-wide, so once the parent searches memory, every spawned subagent
|
|
@@ -93,6 +94,109 @@ var EDIT_RESET_SKIP_BOTH_RE = /\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^
|
|
|
93
94
|
// but NOT the simplify gate — /simplify already reviewed the production code; touching
|
|
94
95
|
// a test file or fixture doesn't expose new untested surface for code review (#908).
|
|
95
96
|
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;
|
|
97
|
+
// Docs-only PR exemption: text/markup/image extensions that cannot change runtime behaviour.
|
|
98
|
+
// If EVERY file in the PR diff matches this, skip testing/simplify/learnings gates.
|
|
99
|
+
// Anchored to end-of-path so e.g. `foo.md.js` does not match. Excludes lock files / configs
|
|
100
|
+
// on purpose — those are inert for edit-reset (above) but not "documentation".
|
|
101
|
+
var DOCS_ONLY_RE = /\.(md|markdown|txt|rst|adoc|html?|pdf|png|jpe?g|gif|svg|webp|ico|bmp)$/i;
|
|
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
|
+
|
|
176
|
+
// Get the file list changed on the current branch vs the merge-base with origin/main
|
|
177
|
+
// (falling back to local main). Returns an array of repo-relative paths, or null on
|
|
178
|
+
// failure — in which case callers MUST fall through to the standard gate (fail-safe).
|
|
179
|
+
function getChangedFilesVsBase() {
|
|
180
|
+
var bases = ['origin/main', 'main', 'origin/master', 'master'];
|
|
181
|
+
var base = null;
|
|
182
|
+
for (var i = 0; i < bases.length; i++) {
|
|
183
|
+
try {
|
|
184
|
+
base = cp.execFileSync('git', ['merge-base', 'HEAD', bases[i]], {
|
|
185
|
+
cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
|
|
186
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
187
|
+
}).trim();
|
|
188
|
+
if (base) break;
|
|
189
|
+
} catch (e) { /* try next */ }
|
|
190
|
+
}
|
|
191
|
+
if (!base) return null;
|
|
192
|
+
try {
|
|
193
|
+
var out = cp.execFileSync('git', ['diff', '--name-only', base + '...HEAD'], {
|
|
194
|
+
cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
|
|
195
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
196
|
+
});
|
|
197
|
+
return out.split('\n').map(function(l) { return l.trim(); }).filter(Boolean);
|
|
198
|
+
} catch (e) { return null; }
|
|
199
|
+
}
|
|
96
200
|
|
|
97
201
|
switch (command) {
|
|
98
202
|
case 'check-before-agent': {
|
|
@@ -175,10 +279,19 @@ switch (command) {
|
|
|
175
279
|
case 'record-skill-run': {
|
|
176
280
|
if ((process.env.TOOL_INPUT_skill || '') === 'simplify') {
|
|
177
281
|
var s = readState();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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);
|
|
182
295
|
}
|
|
183
296
|
break;
|
|
184
297
|
}
|
|
@@ -208,7 +321,29 @@ switch (command) {
|
|
|
208
321
|
// optional ENV=val prefix segment catches `GH_TOKEN=x gh pr create`.
|
|
209
322
|
var cmd = process.env.TOOL_INPUT_command || '';
|
|
210
323
|
if (!/(?:^|&&\s*|\|\|\s*|;\s*)\s*(?:[A-Z_][A-Z0-9_]*=\S+\s+)*gh\s+pr\s+create\b/.test(cmd)) break;
|
|
324
|
+
// Docs-only exemption: if every file changed vs the merge-base is a docs/image
|
|
325
|
+
// file (no runtime-behaviour surface), skip the testing/simplify/learnings gates
|
|
326
|
+
// and surface a one-line transparency note. Falls through to the standard gate
|
|
327
|
+
// on any failure (no base, no diff, exec error) — fail-safe by design.
|
|
328
|
+
var changed = getChangedFilesVsBase();
|
|
329
|
+
if (changed && changed.length > 0 && changed.every(function(f) { return DOCS_ONLY_RE.test(f); })) {
|
|
330
|
+
process.stdout.write('Docs-only PR (' + changed.length + ' file' + (changed.length === 1 ? '' : 's') + ') — skipping testing/simplify/learnings gates.\n');
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
211
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
|
+
}
|
|
212
347
|
var missing = [];
|
|
213
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)');
|
|
214
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
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
var fs = require('fs');
|
|
4
4
|
var path = require('path');
|
|
5
|
+
var cp = require('child_process');
|
|
5
6
|
|
|
6
7
|
var PROJECT_DIR = (process.env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
|
|
7
8
|
var STATE_FILE = path.join(PROJECT_DIR, '.claude', 'workflow-state.json');
|
|
8
9
|
|
|
9
|
-
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 };
|
|
10
11
|
|
|
11
12
|
// Per-actor memory-search tracking (#838). The legacy `memorySearched` boolean
|
|
12
13
|
// is session-wide, so once the parent searches memory, every spawned subagent
|
|
@@ -93,6 +94,109 @@ var EDIT_RESET_SKIP_BOTH_RE = /\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^
|
|
|
93
94
|
// but NOT the simplify gate — /simplify already reviewed the production code; touching
|
|
94
95
|
// a test file or fixture doesn't expose new untested surface for code review (#908).
|
|
95
96
|
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;
|
|
97
|
+
// Docs-only PR exemption: text/markup/image extensions that cannot change runtime behaviour.
|
|
98
|
+
// If EVERY file in the PR diff matches this, skip testing/simplify/learnings gates.
|
|
99
|
+
// Anchored to end-of-path so e.g. `foo.md.js` does not match. Excludes lock files / configs
|
|
100
|
+
// on purpose — those are inert for edit-reset (above) but not "documentation".
|
|
101
|
+
var DOCS_ONLY_RE = /\.(md|markdown|txt|rst|adoc|html?|pdf|png|jpe?g|gif|svg|webp|ico|bmp)$/i;
|
|
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
|
+
|
|
176
|
+
// Get the file list changed on the current branch vs the merge-base with origin/main
|
|
177
|
+
// (falling back to local main). Returns an array of repo-relative paths, or null on
|
|
178
|
+
// failure — in which case callers MUST fall through to the standard gate (fail-safe).
|
|
179
|
+
function getChangedFilesVsBase() {
|
|
180
|
+
var bases = ['origin/main', 'main', 'origin/master', 'master'];
|
|
181
|
+
var base = null;
|
|
182
|
+
for (var i = 0; i < bases.length; i++) {
|
|
183
|
+
try {
|
|
184
|
+
base = cp.execFileSync('git', ['merge-base', 'HEAD', bases[i]], {
|
|
185
|
+
cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
|
|
186
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
187
|
+
}).trim();
|
|
188
|
+
if (base) break;
|
|
189
|
+
} catch (e) { /* try next */ }
|
|
190
|
+
}
|
|
191
|
+
if (!base) return null;
|
|
192
|
+
try {
|
|
193
|
+
var out = cp.execFileSync('git', ['diff', '--name-only', base + '...HEAD'], {
|
|
194
|
+
cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
|
|
195
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
196
|
+
});
|
|
197
|
+
return out.split('\n').map(function(l) { return l.trim(); }).filter(Boolean);
|
|
198
|
+
} catch (e) { return null; }
|
|
199
|
+
}
|
|
96
200
|
|
|
97
201
|
switch (command) {
|
|
98
202
|
case 'check-before-agent': {
|
|
@@ -175,10 +279,19 @@ switch (command) {
|
|
|
175
279
|
case 'record-skill-run': {
|
|
176
280
|
if ((process.env.TOOL_INPUT_skill || '') === 'simplify') {
|
|
177
281
|
var s = readState();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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);
|
|
182
295
|
}
|
|
183
296
|
break;
|
|
184
297
|
}
|
|
@@ -208,7 +321,29 @@ switch (command) {
|
|
|
208
321
|
// optional ENV=val prefix segment catches `GH_TOKEN=x gh pr create`.
|
|
209
322
|
var cmd = process.env.TOOL_INPUT_command || '';
|
|
210
323
|
if (!/(?:^|&&\s*|\|\|\s*|;\s*)\s*(?:[A-Z_][A-Z0-9_]*=\S+\s+)*gh\s+pr\s+create\b/.test(cmd)) break;
|
|
324
|
+
// Docs-only exemption: if every file changed vs the merge-base is a docs/image
|
|
325
|
+
// file (no runtime-behaviour surface), skip the testing/simplify/learnings gates
|
|
326
|
+
// and surface a one-line transparency note. Falls through to the standard gate
|
|
327
|
+
// on any failure (no base, no diff, exec error) — fail-safe by design.
|
|
328
|
+
var changed = getChangedFilesVsBase();
|
|
329
|
+
if (changed && changed.length > 0 && changed.every(function(f) { return DOCS_ONLY_RE.test(f); })) {
|
|
330
|
+
process.stdout.write('Docs-only PR (' + changed.length + ' file' + (changed.length === 1 ? '' : 's') + ') — skipping testing/simplify/learnings gates.\n');
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
211
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
|
+
}
|
|
212
347
|
var missing = [];
|
|
213
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)');
|
|
214
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).
|
|
@@ -23,26 +23,72 @@ async function runFixCommand(cmd) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Fix
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Fix Gate Health failures: bin/.claude-helpers gate.cjs drift AND missing
|
|
27
|
+
* settings.json hook wiring. The check has three independent failure modes
|
|
28
|
+
* and the prior fix only handled hook wiring — leaving bin/helper drift
|
|
29
|
+
* unresolved while still claiming success (the "Auto-fixed 1 issue" lie that
|
|
30
|
+
* surfaced when #920 mirrored the docs-only PR exemption into only one of
|
|
31
|
+
* the two gate.cjs files).
|
|
32
|
+
*
|
|
33
|
+
* Sync direction is decided by which source file is "ahead" of its installed
|
|
34
|
+
* counterpart in `node_modules/moflo/`:
|
|
35
|
+
* - If only source `bin/gate.cjs` differs from installed bin → mirror bin → helper.
|
|
36
|
+
* - If only source `.claude/helpers/gate.cjs` differs from installed helper → mirror helper → bin.
|
|
37
|
+
* - If both are ahead with different content (genuine ambiguity) → bail
|
|
38
|
+
* and let the caller report failure; refuse to silently pick a side.
|
|
39
|
+
* - If `node_modules/moflo/` is missing entirely (consumer never installed,
|
|
40
|
+
* unusual layout) → bail.
|
|
29
41
|
*/
|
|
30
42
|
async function fixGateHealthHooks() {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
let driftFixed = true; // true means "no drift to fix or drift resolved"
|
|
45
|
+
const binGate = join(cwd, 'bin', 'gate.cjs');
|
|
46
|
+
const helperGate = join(cwd, '.claude', 'helpers', 'gate.cjs');
|
|
47
|
+
const installedBin = join(cwd, 'node_modules', 'moflo', 'bin', 'gate.cjs');
|
|
48
|
+
const installedHelper = join(cwd, 'node_modules', 'moflo', '.claude', 'helpers', 'gate.cjs');
|
|
49
|
+
if (existsSync(binGate) && existsSync(helperGate)) {
|
|
50
|
+
try {
|
|
51
|
+
const binContent = readFileSync(binGate, 'utf8');
|
|
52
|
+
const helperContent = readFileSync(helperGate, 'utf8');
|
|
53
|
+
if (binContent !== helperContent) {
|
|
54
|
+
const installedBinContent = existsSync(installedBin) ? readFileSync(installedBin, 'utf8') : null;
|
|
55
|
+
const installedHelperContent = existsSync(installedHelper) ? readFileSync(installedHelper, 'utf8') : null;
|
|
56
|
+
const binAhead = installedBinContent !== null && binContent !== installedBinContent;
|
|
57
|
+
const helperAhead = installedHelperContent !== null && helperContent !== installedHelperContent;
|
|
58
|
+
if (binAhead && !helperAhead) {
|
|
59
|
+
writeFileSync(helperGate, binContent, 'utf-8');
|
|
60
|
+
}
|
|
61
|
+
else if (helperAhead && !binAhead) {
|
|
62
|
+
writeFileSync(binGate, helperContent, 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Both ahead with different content, OR neither ahead (no install
|
|
66
|
+
// to anchor on). Refuse to pick a side — surface the failure.
|
|
67
|
+
driftFixed = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
driftFixed = false;
|
|
73
|
+
}
|
|
42
74
|
}
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
// Hook-wiring repair (separate failure mode that this fixer also owns).
|
|
76
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
77
|
+
let wiringFixed = true;
|
|
78
|
+
if (existsSync(settingsPath)) {
|
|
79
|
+
try {
|
|
80
|
+
const raw = readFileSync(settingsPath, 'utf8');
|
|
81
|
+
const settings = JSON.parse(raw);
|
|
82
|
+
const { repaired } = repairHookWiring(settings);
|
|
83
|
+
if (repaired.length > 0) {
|
|
84
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
wiringFixed = false;
|
|
89
|
+
}
|
|
45
90
|
}
|
|
91
|
+
return driftFixed && wiringFixed;
|
|
46
92
|
}
|
|
47
93
|
/**
|
|
48
94
|
* Execute the fix for a failed/warned health check.
|