moflo 4.9.14 → 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.
@@ -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
@@ -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
- Apply the universal rules from `.claude/guidance/shipped/moflo-guidance-rules.md`. For each `.md` file, check:
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
- - Has `**Purpose:**` line right after H1
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
- Do **not** duplicate `/guidance -a`'s logic verbatim — just produce a one-line summary per file (`<file>: <N issues>`). The Eldar surface gaps; `/guidance -a` does the deep audit.
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)
@@ -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] [<topic-or-path>]"
4
+ arguments: "[-a] <topic-or-path>"
5
5
  ---
6
6
 
7
7
  # /guidance — Author and audit project guidance
@@ -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: "[spell-name-or-alias]"
8
+ arguments: "<spell-name-or-alias>"
9
9
  ---
10
10
 
11
11
  # /spell-schedule — Schedule a Local Spell
@@ -432,6 +432,31 @@ const REQUIRED_GATE_CASES = [
432
432
  // session-start-launcher.mjs in consumer projects without transitive failures.
433
433
  import { REQUIRED_HOOK_WIRING } from '../services/hook-wiring.js';
434
434
  export { REQUIRED_HOOK_WIRING };
435
+ /**
436
+ * Detect "expected pre-publish drift" — source `bin/gate.cjs` is ahead of the
437
+ * installed `node_modules/moflo/bin/gate.cjs`, but the deployed
438
+ * `.claude/helpers/gate.cjs` still matches the installed version. This is the
439
+ * steady state in the moflo dogfood repo while a PR has landed but no
440
+ * `npm install moflo@<new>` has rotated the package.
441
+ *
442
+ * Returns true only when both are true:
443
+ * - helper content equals installed bin content (helper is correctly synced
444
+ * to what's installed)
445
+ * - installed bin content differs from source bin content (source is ahead)
446
+ *
447
+ * If `node_modules/moflo/bin/gate.cjs` is missing (consumer never installed
448
+ * moflo, or path is unusual) we conservatively return false so other drift
449
+ * detection still applies.
450
+ */
451
+ export function isExpectedPrePublishDrift(installedBinGate, helperContent, sourceBinContent) {
452
+ try {
453
+ const installedContent = readFileSync(installedBinGate, 'utf8');
454
+ return installedContent === helperContent && installedContent !== sourceBinContent;
455
+ }
456
+ catch {
457
+ return false;
458
+ }
459
+ }
435
460
  /**
436
461
  * Verify gate infrastructure health:
437
462
  * 1. gate.cjs exists and contains all required cases
@@ -474,14 +499,27 @@ export async function checkGateHealth() {
474
499
  issues.push(`gate.cjs missing cases: ${missingCases.join(', ')}`);
475
500
  }
476
501
  // 2. Check bin/gate.cjs sync
502
+ //
503
+ // The launcher syncs `node_modules/moflo/bin/gate.cjs` → `.claude/helpers/gate.cjs`
504
+ // on version change. Source `bin/gate.cjs` is only present in the moflo dogfood
505
+ // repo. During the dogfood publish window — between a PR landing and the next
506
+ // `npm install moflo@<new>` — source bin/ legitimately moves ahead of the
507
+ // installed bin/, while the helper continues to mirror the installed version.
508
+ // That's the expected steady state, not a bug; downgrade it to `warn` and skip
509
+ // the `fix` field so `--fix` doesn't paint a false success (#913).
477
510
  const binGate = join(projectDir, 'bin', 'gate.cjs');
511
+ const installedBinGate = join(projectDir, 'node_modules', 'moflo', 'bin', 'gate.cjs');
478
512
  if (existsSync(binGate)) {
479
513
  try {
480
514
  const binContent = readFileSync(binGate, 'utf8');
481
515
  if (binContent !== gateContent) {
482
- // Check if it's a size difference (likely out of sync) vs whitespace
483
516
  const sizeDiff = Math.abs(binContent.length - gateContent.length);
484
- if (sizeDiff > 10) {
517
+ const prePublishDrift = isExpectedPrePublishDrift(installedBinGate, gateContent, binContent);
518
+ if (prePublishDrift) {
519
+ warnings.push(`source bin/gate.cjs is ${sizeDiff} chars ahead of node_modules/moflo/bin/gate.cjs ` +
520
+ '(expected pre-publish drift; resolves on next `npm install moflo@<new>`)');
521
+ }
522
+ else if (sizeDiff > 10) {
485
523
  issues.push(`bin/gate.cjs out of sync with .claude/helpers/gate.cjs (${sizeDiff} chars differ)`);
486
524
  }
487
525
  else {
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.9.14';
5
+ export const VERSION = '4.9.15';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.9.14",
3
+ "version": "4.9.15",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -81,7 +81,7 @@
81
81
  "@typescript-eslint/eslint-plugin": "^7.18.0",
82
82
  "@typescript-eslint/parser": "^7.18.0",
83
83
  "eslint": "^8.0.0",
84
- "moflo": "^4.9.13",
84
+ "moflo": "^4.9.14",
85
85
  "tsx": "^4.21.0",
86
86
  "typescript": "^5.9.3",
87
87
  "vitest": "^4.0.0"