moflo 4.10.11 → 4.10.13

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.
@@ -176,6 +176,21 @@ Checks: Node version (20+), Git, config validity, daemon status, memory database
176
176
 
177
177
  ---
178
178
 
179
+ ## Monorepo Layout
180
+
181
+ **Purpose:** prevent the most common moflo misconfig — daemon islands in monorepos (#1174).
182
+
183
+ | Rule | Why |
184
+ |------|-----|
185
+ | One `.moflo/` per monorepo, at the repo root | The daemon, MCP server, and CLI all walk up from `cwd` to locate state. Two `.moflo/` directories under one tree means two daemons with separate sockets, ports, and registries — the MCP server bound to one will not see tools/state from the other. |
186
+ | Never run `flo init` inside a sub-workspace of an existing moflo project | `flo init` refuses by default when an ancestor `.moflo/moflo.db` is detected. `--force` overrides if you genuinely want isolated state. |
187
+ | `flo doctor` flags every nested `.moflo/` it finds | Component name: `nested-moflo`. Status warns when any island exists. |
188
+ | `flo doctor --fix -c nested-moflo` archives each nested directory | Renames `<sub>/.moflo` → `<sub>/.moflo-archived-<ISO>` (never deletes). Manual review path: archived directories stay on disk. |
189
+
190
+ The resolver (`findProjectRoot`) prefers the topmost ancestor with `.moflo/moflo.db` so every cwd in the tree agrees on the canonical anchor. If `CLAUDE_PROJECT_DIR` is set explicitly, it overrides this — only use that override when you intend isolated state.
191
+
192
+ ---
193
+
179
194
  ## Troubleshooting
180
195
 
181
196
  | Symptom | Cause | Fix |
@@ -188,6 +203,7 @@ Checks: Node version (20+), Git, config validity, daemon status, memory database
188
203
  | Embeddings fail offline / air-gapped | `fastembed` model cache missing | Pre-populate `~/.cache/fastembed` or set `FASTEMBED_CACHE` (see `docs/modules/embeddings.md`) |
189
204
  | `flo` command not found | Not in PATH | Use `npx flo` or `node node_modules/moflo/bin/index-guidance.mjs` |
190
205
  | Bundled guidance not indexed | Running inside the moflo repo | Bundled guidance only indexes when installed as a dependency in a different project |
206
+ | `mcp__moflo__*` tools missing in monorepo session | Nested `.moflo/` directories spawned separate daemons (#1174) | Run `flo doctor -c nested-moflo`; if any are found, `flo doctor --fix -c nested-moflo` archives them. Restart Claude Code to reconnect. |
191
207
 
192
208
  See `.claude/guidance/moflo-memory-strategy.md` for memory-specific troubleshooting and `.claude/guidance/moflo-spell-troubleshooting.md` for spell sandbox/network failures.
193
209
 
@@ -1,34 +1,194 @@
1
- # MoFlo Memory Protocol — Search, Traverse, Retrieve
1
+ # MoFlo Memory Protocol — Pick Namespace, Query, Traverse
2
2
 
3
- **Purpose:** How to use moflo's chunked memory effectively. Search returns navigable chunkstraverse the chunk graph, do not bulk-retrieve every hit.
3
+ **Purpose:** How to use moflo's chunked memory effectively. Memory indexes far more than user-feedback narratives: per-directory symbol tables, test-to-source reverse indexes, RAG-chunked guidance docs, code patterns, and incident learnings. Querying the wrong namespace or asking the wrong shape of question — wastes the gate-enforced first search. Pick the namespace, pivot on the symbol, read the similarity score, traverse via neighbors.
4
+
5
+ ## Rule (MUST)
6
+
7
+ When a search hit carries a non-null `navigation` field, you MUST traverse via `mcp__moflo__memory_get_neighbors` — NOT bulk-retrieve every hit with `memory_retrieve`. The `navigation` crumb is the chunking architecture's contract; bulk-retrieving every search hit defeats it and is a protocol violation. Use `memory_retrieve` only for a single specific key you already hold, or for non-chunk entries where `navigation` is null.
4
8
 
5
9
  ---
6
10
 
7
- ## Rule (MUST)
11
+ ## What's Actually in Each Namespace
12
+
13
+ The moflo index is large and dense. Sizing as of 2026-05-16 on the moflo repo (similar shape in any consumer project after first indexing pass):
14
+
15
+ | Namespace | Entries | What's indexed | Use for |
16
+ |-----------|---------|----------------|---------|
17
+ | `code-map` | ~1,400 | Per-directory symbol tables (`dir:src/cli/services` lists ~430 symbols → defining files), per-file type/function summaries | "where is X defined", "what's in directory Y", "what types live in file Z" |
18
+ | `guidance` | ~1,000 | RAG-chunked `.claude/guidance/**/*.md` — every chunk has nav crumbs (parent doc, prev/next/siblings) | Project rules, decision context, "how should I…" docs |
19
+ | `patterns` | ~970 | Per-file pattern entries, aggregate stats ("middleware: 16 occurrences"), narrative fix-patterns (`pattern:swarm-dir-recreation-fix-1168`) | "what's our pattern for X", recurring fix shapes, conventions |
20
+ | `tests` | ~850 | `test-file:` entries per test, plus `test-map:<production path>` reverse-index ("2 test files cover this") | "what tests cover module X", test inventory, coverage gaps |
21
+ | `learnings` | grows over time | Incident narratives keyed by issue/PR (`1145-daemon-port-collision-fix`) | Error → fix recall, "did we hit this before", post-mortems |
22
+
23
+ `code-map` and `tests` are auto-indexed from the source tree. `guidance` is auto-indexed from `.claude/guidance/**`. `patterns` mixes auto-mined heuristics with narratives stored via `memory_store`. `learnings` is curated — store with `mcp__moflo__memory_store` when you finish a non-trivial debug.
24
+
25
+ ---
26
+
27
+ ## Worked Examples — Real Queries Against the Live Store
28
+
29
+ Each example shows the query, namespace, top hit, similarity score, and why memory wins over the alternative.
30
+
31
+ ### Example 1 — "Where is symbol X defined" → `code-map`
32
+
33
+ ```
34
+ mcp__moflo__memory_search {
35
+ query: "where is BashSafetyHook defined",
36
+ namespace: "code-map"
37
+ }
38
+ ```
39
+
40
+ Top hits:
41
+
42
+ ```
43
+ file:src/cli/shared/hooks/safety/bash-safety.ts similarity 0.86
44
+ dir:src/cli/shared/hooks/safety similarity 0.82
45
+ ```
46
+
47
+ **Wins over Grep.** Grep returns every mention of the identifier across the tree; `code-map` returns the defining file plus the sibling-symbol context (the dir entry lists all 18 types in that directory) in one call.
48
+
49
+ ### Example 2 — "Tests for module Y" → `tests`
50
+
51
+ ```
52
+ mcp__moflo__memory_search {
53
+ query: "tests for daemon port collision",
54
+ namespace: "tests"
55
+ }
56
+ ```
57
+
58
+ Top hits:
59
+
60
+ ```
61
+ test-file:src/cli/__tests__/commands/daemon-port-resolution.test.ts similarity 0.79
62
+ test-map:src/cli/commands/daemon similarity 0.75
63
+ ```
64
+
65
+ **Wins over Glob + Read.** The `test-map:` reverse-index answers "what tests cover this production path" directly — no need to Glob for filename matches and Read each candidate to confirm coverage.
66
+
67
+ ### Example 3 — "Patterns for X" → `patterns`
68
+
69
+ ```
70
+ mcp__moflo__memory_search {
71
+ query: "vitest mocking patterns for middleware",
72
+ namespace: "patterns"
73
+ }
74
+ ```
75
+
76
+ Top hits:
77
+
78
+ ```
79
+ pattern-import-module-e9oqfm similarity 0.87
80
+ pattern-test-mocking-76mtme similarity 0.81
81
+ pattern-api-middleware-q9gk50 similarity 0.78
82
+ ```
83
+
84
+ **Competitive with Grep + multiple Reads.** Returns conceptual hits without needing to know file paths or naming conventions.
8
85
 
9
- When a search hit carries a non-null `navigation`, you MUST call `mcp__moflo__memory_get_neighbors` to traverse not `mcp__moflo__memory_retrieve`. `memory_retrieve` is for fetching one specific chunk in full, or for non-chunk entries where `navigation` is null. Bulk-retrieving search hits defeats the chunking architecture.
86
+ ### Example 4"How does feature work" `guidance`
10
87
 
11
- ## Decision Table
88
+ ```
89
+ mcp__moflo__memory_search {
90
+ query: "how should subagents handle the memory search gate",
91
+ namespace: "guidance"
92
+ }
93
+ ```
94
+
95
+ Top hits — four chunks across four different docs, all 0.86+:
96
+
97
+ ```
98
+ chunk-skill-eldar-2 (parent: doc-skill-eldar)
99
+ chunk-guidance-upgrade-contract-6 (parent: doc-guidance-upgrade-contract)
100
+ chunk-guidance-memory-traversal-architecture-5 (parent: doc-guidance-memory-traversal-architecture)
101
+ chunk-guidance-moflo-subagents-0 (parent: doc-guidance-moflo-subagents)
102
+ ```
103
+
104
+ **Wins over Read.** Semantic recall across docs without knowing filenames. Each hit carries a `navigation` object — traverse to siblings/prev/next for adjacent context.
105
+
106
+ ### Example 5 — "Did we hit this error before" → `learnings`
107
+
108
+ ```
109
+ mcp__moflo__memory_search {
110
+ query: "daemon port collision fix",
111
+ namespace: "learnings"
112
+ }
113
+ ```
114
+
115
+ Hits a curated incident narrative (e.g. `1145-daemon-port-collision-fix`) with the prior diagnosis + fix shape. **Wins over searching closed GitHub issues** — the narrative is the distilled fix, not the raw discussion.
116
+
117
+ ---
118
+
119
+ ## Querying Technique — Five Rules for High-Signal Searches
120
+
121
+ Five rules that turn ceremonial searches into high-signal ones. Each one fixes a real failure mode observed in long sessions.
122
+
123
+ | Rule | Why |
124
+ |------|-----|
125
+ | **Pivot on the symbol.** Include the bare identifier (`BashSafetyHook`, `findProjectRoot`) verbatim in the query | Embedding similarity is much higher on the literal token than on a natural-language paraphrase |
126
+ | **Always specify `namespace`** when you know the answer shape | Cross-namespace results dilute relevance — a `tests` query without namespace pulls noisy `code-map` and `patterns` hits |
127
+ | **Read the `similarity` score** | `≥ 0.80` is a confident hit; `0.60–0.79` is tangential; `< 0.60` is noise. Re-query with a sharper symbol/keyword rather than acting on weak hits |
128
+ | **Traverse, don't re-search** | When a chunk hit has `navigation`, use `mcp__moflo__memory_get_neighbors` for adjacent context — one round-trip, no re-embedding cost |
129
+ | **Don't bulk-retrieve search hits** | `memory_retrieve` is for one specific key you already hold. Calling it for every search hit defeats the chunking architecture |
130
+
131
+ ### Query shape — do / don't
132
+
133
+ | Don't | Do |
134
+ |-------|-----|
135
+ | `memory_search { query: "where is the bash safety hook implemented in the project" }` | `memory_search { query: "BashSafetyHook", namespace: "code-map" }` |
136
+ | `memory_search { query: "find all tests for daemon stuff" }` | `memory_search { query: "daemon port collision", namespace: "tests" }` |
137
+ | `memory_search { query: "how do I do mocks" }` (no namespace, vague) | `memory_search { query: "vitest mocking middleware", namespace: "patterns" }` |
138
+ | `memory_retrieve` on each of 5 search hits | `memory_get_neighbors { key: <best hit>, include: ['prev','next','siblings'] }` |
139
+
140
+ ---
141
+
142
+ ## Tool Selection — Memory API
143
+
144
+ Once you have a search hit, pick the right follow-up call by what you need next. Bulk-retrieving every search hit is a protocol violation; the table below maps each follow-up shape to its single correct tool.
12
145
 
13
146
  | You want | Use | Why |
14
147
  |----------|-----|-----|
15
- | Find an entry-point | `mcp__moflo__memory_search` | Returns chunk hits with compact `navigation` (parentDoc, prev/next, chunkTitle) |
16
- | Adjacent context (1 chunk over) | `mcp__moflo__memory_get_neighbors` `{ key, include: ['prev','next'] }` | One round-trip, returns shaped entries with full nav |
148
+ | Find an entry-point | `mcp__moflo__memory_search` | Returns chunk hits with `navigation` (parentDoc, prev/next, chunkTitle) |
149
+ | Adjacent context (1 chunk over) | `mcp__moflo__memory_get_neighbors` `{ key, include: ['prev','next'] }` | One round-trip, shaped entries with full nav |
17
150
  | Same-section peers (h2/h3 family) | `memory_get_neighbors` `{ include: ['siblings'] }` or `['parent','children']` | Hierarchical traversal — cheaper than re-searching |
18
- | Full content of one chunk | `mcp__moflo__memory_retrieve` `{ key }` | Returns full nav object for further traversal |
151
+ | Full content of one specific chunk | `mcp__moflo__memory_retrieve` `{ key }` | For a key you already hold — never for bulk-retrieving search hits |
19
152
  | Whole source doc when truly needed | `Read` `parentPath` from any chunk's nav | Disk read is cheaper than re-indexed `doc-*` |
20
153
 
21
- ## Anti-Patterns
154
+ ---
155
+
156
+ ## Tool Selection — Memory vs. Filesystem Tools
157
+
158
+ Pick memory when the question is about indexed knowledge (symbols, tests, patterns, docs, incidents); pick filesystem/git when the question is about *current authoritative state* the index can't guarantee fresher than disk.
159
+
160
+ | Question shape | First call |
161
+ |----------------|-----------|
162
+ | "Where is symbol X defined" | `memory_search` namespace `code-map` (pivot on bare symbol) |
163
+ | "What tests cover module Y" | `memory_search` namespace `tests` |
164
+ | "What's our pattern for Z" | `memory_search` namespace `patterns` |
165
+ | "How / why / project rule for W" | `memory_search` namespace `guidance` |
166
+ | "Did we hit error V before" | `memory_search` namespace `learnings` |
167
+ | "Find files matching glob `**/*.test.ts`" | `Glob` — file-system metadata, not indexed content |
168
+ | "What's in the working tree right now / what did I just edit" | `Read` — memory is a snapshot, working-tree is authoritative |
169
+ | "What's in this commit / git history" | `git log` / `git diff` |
170
+
171
+ The first five rows pivot through memory because the index already knows the answer. The last three pivot through filesystem/git because they ask about *current authoritative state*, which memory cannot guarantee fresher than disk.
172
+
173
+ ---
174
+
175
+ ## Anti-Patterns — Common Memory Query Mistakes
176
+
177
+ These six failure modes account for almost every "memory returned noise" complaint. Each row pairs the antipattern with the corrective shape from the cheat sheet above.
22
178
 
23
179
  | Don't | Do instead |
24
180
  |-------|-----------|
181
+ | Search memory without a namespace, get noise, fall back to Grep | Specify the namespace that matches the question shape (table above) |
25
182
  | Retrieve every search hit blindly | Traverse via `memory_get_neighbors` when `navigation` is present — bulk `memory_retrieve` per hit is a protocol violation |
26
183
  | Open the source file when a chunk would do | Stay in the chunk graph; `Read` `parentPath` only for the rare full-doc case |
27
184
  | Search again for a key you already have | `memory_retrieve` or `memory_get_neighbors` directly |
185
+ | Phrase queries as natural-language questions | Pivot on the bare symbol or keyword — embedding similarity is higher on the literal token |
186
+ | Act on a hit with similarity < 0.6 | Re-query with a sharper symbol/keyword; weak hits are noise, not signal |
28
187
 
29
188
  ---
30
189
 
31
190
  ## See Also
32
191
 
33
- - `.claude/guidance/moflo-agent-rules.md` § Memory-First Protocol — namespaces, query examples, MCP-first tool selection
34
- - `.claude/guidance/moflo-memory-strategy.md` — How chunking, embeddings, and the RAG index work
192
+ - `.claude/guidance/moflo-agent-rules.md` § Memory-First Protocol — gate enforcement, namespace selection rules
193
+ - `.claude/guidance/moflo-memory-strategy.md` — How chunking, embeddings, and the RAG index work under the hood
194
+ - `.claude/guidance/moflo-memorydb-maintenance.md` — How the underlying memory DB is kept healthy (indexing, vacuum, integrity)
@@ -81,7 +81,21 @@ var config = loadGateConfig();
81
81
  var command = process.argv[2];
82
82
 
83
83
  var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules', 'moflo.yaml'];
84
- var DANGEROUS = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:', 'mkfs.', '> /dev/sda'];
84
+ // #1171 DANGEROUS gained PowerShell additions to match the matcher widening
85
+ // that now routes the dedicated `PowerShell` tool through check-dangerous-command.
86
+ // POSIX entries still apply because PS will execute them when invoked. Substring
87
+ // match (case-insensitive) inside the gate.
88
+ var DANGEROUS = [
89
+ 'rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:', 'mkfs.', '> /dev/sda',
90
+ // PowerShell destructive patterns. Won't catch every adversarial spelling
91
+ // (PS aliases let `ri -r -force C:\` mean the same thing) but covers the
92
+ // common-typo destruction class — symmetric to the POSIX list's intent.
93
+ 'remove-item -recurse -force c:\\',
94
+ 'remove-item -recurse -force /',
95
+ 'remove-item -recurse -force ~',
96
+ 'format-volume',
97
+ 'clear-disk',
98
+ ];
85
99
 
86
100
  // #1132 — Bash memory-first gate.
87
101
  //
@@ -94,6 +108,9 @@ var CREDIT_MEMORY_SEARCH_RE = /semantic-search|memory search|memory retrieve|mem
94
108
  // check-before-scan gates by going through the shell. Anchored to the start of
95
109
  // the line so subcommands inside pipelines or `npm install grep` don't trip.
96
110
  // Covers POSIX read/search tools, Windows cmd `type`, and PowerShell readers.
111
+ // #1171 — extended with PowerShell-native exploration forms now that the matcher
112
+ // widens to the `PowerShell` tool. Plain `Get-ChildItem` without -Recurse stays
113
+ // uncovered (it's `ls`-equivalent and plain `ls` is allowed).
97
114
  var READ_LIKE_BASH_RE = new RegExp([
98
115
  '^\\s*(?:cat|head|tail|less|more|bat|xxd|od|hexdump)\\b',
99
116
  '^\\s*(?:grep|rg|ag|fgrep|egrep|find|fd)\\b',
@@ -108,6 +125,17 @@ var READ_LIKE_BASH_RE = new RegExp([
108
125
  // primary risk pattern is leaking past the gate via `type src\foo.ts`.
109
126
  '^\\s*type\\s+\\S*[\\\\/.]',
110
127
  '^\\s*(?:Get-Content|gc|Select-String|sls)\\b',
128
+ // #1171 — PowerShell recursive exploration (parallel to POSIX `find`/`fd`).
129
+ // The `-Recurse` flag is what makes it expensive enough to gate; plain
130
+ // `Get-ChildItem` is `ls`-shaped and intentionally not blocked.
131
+ '^\\s*(?:Get-ChildItem|gci)\\b[^|]*-Recurse\\b',
132
+ // #1171 — cmd-style recursive listing (`dir /s` or `dir /S`). Only the
133
+ // Windows `/s` form, NOT POSIX `dir -s` (sort-by-size, where `dir` is aliased
134
+ // to `ls -l` on many distros) — false-positive blocking that would break
135
+ // legitimate POSIX listings.
136
+ '^\\s*dir\\b[^|]*\\s\\/[sS]\\b',
137
+ // #1171 — PowerShell hex dump, parallel to POSIX `xxd`/`hexdump`.
138
+ '^\\s*Format-Hex\\b',
111
139
  ].join('|'), 'i');
112
140
  // CARVE-OUT: commands that LOOK read-like but are operational. Anchored to the
113
141
  // LEADING command — the pipe-filter case (`npm test | grep FAIL`) is already
@@ -129,6 +157,32 @@ var BASH_CARVE_OUT_RE = new RegExp([
129
157
  // `find` commands that lack a `-delete` / `-exec rm` suffix.
130
158
  '^\\s*find\\s+.+?-(delete|exec\\s+rm)\\b',
131
159
  ].join('|'));
160
+ // #1171 follow-up — strip quoted string bodies and heredoc bodies from a shell
161
+ // command for purposes of dangerous-pattern substring matching. Used by
162
+ // check-dangerous-command. Does NOT strip $(...) or `...` because those bodies
163
+ // execute. Double-quoted strings handle escaped quotes (`\"`) correctly so
164
+ // `git commit -m "fix \"X\""` strips the whole quoted body, not just the first
165
+ // `\"` pair. Single quotes don't have escapes in bash/sh — `'[^']*'` is exact.
166
+ function stripQuotedAndHeredocs(cmd) {
167
+ var out = cmd;
168
+ // Heredoc tail: `<<TOKEN`, `<<-TOKEN`, `<<'TOKEN'`, `<<"TOKEN"` through end-of-input.
169
+ // Bash heredocs are multi-line; in single-line tool inputs they show up as the
170
+ // tail after `<<TOKEN`. Conservative tail-strip — benign content after a heredoc
171
+ // body on the same logical line is also stripped, harmless for this gate.
172
+ // Token class includes `-` so hyphenated heredoc tags (`<<END-OF-DOC`) match
173
+ // the full token, not just the leading word — without this the strip would
174
+ // halt at `<<END` and leave `-OF-DOC` plus the body as literal text.
175
+ out = out.replace(/<<-?\s*['"]?[\w-]+['"]?[\s\S]*$/, '');
176
+ // Here-string `<<<word` — strip the word.
177
+ out = out.replace(/<<<\s*\S+/g, '');
178
+ // Single-quoted strings — no escapes inside single quotes in sh/bash.
179
+ out = out.replace(/'[^']*'/g, "''");
180
+ // Double-quoted strings — `(?:[^"\\]|\\.)*` matches anything except an
181
+ // unescaped `"`, so escaped `\"` mid-string doesn't terminate the strip early.
182
+ out = out.replace(/"(?:[^"\\]|\\.)*"/g, '""');
183
+ return out;
184
+ }
185
+
132
186
  var DIRECTIVE_RE = /^(yes|no|yeah|yep|nope|sure|ok|okay|correct|right|exactly|perfect)\b/i;
133
187
  var TASK_RE = /\b(fix|bug|error|implement|add|create|build|write|refactor|debug|test|feature|issue|security|optimi)\b/i;
134
188
 
@@ -254,14 +308,27 @@ var TEST_RUNNER_RE = /(?:^|[^a-z])(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?(?:test|t)(
254
308
  // Edits to these don't change runtime behaviour, so they don't invalidate prior test/simplify runs.
255
309
  // Lock files and .gitignore are tracked but inert; package.json/*.yaml ARE source — they reset.
256
310
  var EDIT_RESET_SKIP_BOTH_RE = /\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^|[\\\/])(CHANGELOG(?:\.md)?|\.env\.example|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/i;
311
+ // #1176 — path-based inert markers. The extension-based RE above can't cover
312
+ // `.github/workflows/*.yml` without also exempting `moflo.yaml` / `tsconfig.yaml`
313
+ // (which ARE source). Anchor on the GitHub-meta directories that hold CI config
314
+ // and template scaffolds — editing those doesn't expose new runtime surface, so
315
+ // they shouldn't reset testsRun/simplifyRun the way a real source edit does.
316
+ // Trailing terminator includes `.` so the single-file template form
317
+ // `.github/PULL_REQUEST_TEMPLATE.md` matches alongside the directory form.
318
+ var EDIT_RESET_SKIP_PATH_RE = /(?:^|[\\\/])\.github[\\\/](?:workflows|ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE)(?:[\\\/.]|$)/i;
257
319
  // Test files: invalidate the testing gate (tests are stale once test code changes)
258
320
  // but NOT the simplify gate — /simplify already reviewed the production code; touching
259
321
  // a test file or fixture doesn't expose new untested surface for code review (#908).
260
322
  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;
323
+ // #1176 — source-file extensions used by the no-source-files PR exemption.
324
+ // When the cumulative branch diff has zero files matching this RE (i.e. only
325
+ // YAML/MD/JSON/lockfiles/images/templates), the testing/simplify/learnings
326
+ // gates auto-pass at `check-before-pr`. Lists every language moflo ships
327
+ // against — additions here should match TEST_RUNNER_RE's language coverage.
328
+ var SOURCE_FILE_RE = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|rb|java|kt|swift|c|cc|cpp|h|hpp|sh|bash|ps1)$/i;
261
329
  // Docs-only PR exemption: text/markup/image extensions that cannot change runtime behaviour.
262
- // If EVERY file in the PR diff matches this, skip testing/simplify/learnings gates.
263
- // Anchored to end-of-path so e.g. `foo.md.js` does not match. Excludes lock files / configs
264
- // on purpose — those are inert for edit-reset (above) but not "documentation".
330
+ // Retained for the transparency message when the diff is *purely* docs (no YAML/JSON either)
331
+ // gives a more specific reason than "no source files" in that subset.
265
332
  var DOCS_ONLY_RE = /\.(md|markdown|txt|rst|adoc|html?|pdf|png|jpe?g|gif|svg|webp|ico|bmp)$/i;
266
333
 
267
334
  // Classifier-aware simplify gate skip. Returns a string reason if the gate
@@ -285,13 +352,24 @@ function classifyForGateSkip(state) {
285
352
  } catch (e) { return null; }
286
353
  if (typeof classify !== 'function') return null;
287
354
 
288
- function tryClassify(diffText, label) {
355
+ function tryClassify(diffText, label, allowSmallReviewFix) {
289
356
  try {
290
357
  var dec = classify(diffText);
291
358
  if (dec.tier === 'TRIVIAL') {
292
359
  var loc = (dec.stats.added || 0) + (dec.stats.deleted || 0);
293
360
  return label + ' is TRIVIAL (' + loc + ' LOC, ' + (dec.stats.fileCount || 0) + ' file(s))';
294
361
  }
362
+ // #1176 — SMALL review-fix shape (snapshot path only). A ≤30-LOC delta with
363
+ // zero new declarations on top of an already-reviewed branch is the typical
364
+ // "apply 3 review fixes" cycle — re-running /flo-simplify against the same
365
+ // surface plus a few-line tweak adds no new signal. Baseline path stays
366
+ // TRIVIAL-only so brand-new SMALL features still get reviewed.
367
+ if (allowSmallReviewFix && dec.tier === 'SMALL') {
368
+ var totalLoc = (dec.stats.added || 0) + (dec.stats.deleted || 0);
369
+ if (totalLoc <= 30 && (dec.stats.declAdded || 0) === 0) {
370
+ return label + ' is SMALL review-fix shape (' + totalLoc + ' LOC, no new declarations)';
371
+ }
372
+ }
295
373
  } catch (e) { /* fall through */ }
296
374
  return null;
297
375
  }
@@ -311,7 +389,9 @@ function classifyForGateSkip(state) {
311
389
  var workTreeA = gitDiff(['diff', 'HEAD']) || '';
312
390
  if (snapDiff !== null) {
313
391
  var combined = snapDiff + (workTreeA ? '\n' + workTreeA : '');
314
- var hit = tryClassify(combined, 'delta since last /simplify');
392
+ // Snapshot path: allow SMALL review-fix shape because the original /simplify
393
+ // already covered the surface and only tiny no-decl-touching tweaks followed.
394
+ var hit = tryClassify(combined, 'delta since last /simplify', true);
315
395
  if (hit) return hit;
316
396
  }
317
397
  }
@@ -475,6 +555,11 @@ switch (command) {
475
555
  // #1132 — preserve CREDIT side-effect AND add a BLOCK arm for read-like
476
556
  // Bash commands. Wired as PreToolUse[Bash] (was PostToolUse before #1132)
477
557
  // so process.exit(2) actually prevents the read from reaching the shell.
558
+ //
559
+ // #1171 — the case name is historical. The matcher now also covers the
560
+ // dedicated `PowerShell` tool, and READ_LIKE_BASH_RE already matched PS
561
+ // readers (Get-Content/Select-String/Get-ChildItem -Recurse/Format-Hex).
562
+ // Treat this case as shell-agnostic read-gate logic.
478
563
  var cmd = process.env.TOOL_INPUT_command || '';
479
564
 
480
565
  // 1) CREDIT — preserved behavior. A real memory-search invocation flips
@@ -531,6 +616,15 @@ switch (command) {
531
616
  s.testsRun = true;
532
617
  writeState(s);
533
618
  }
619
+ } else if (cmd) {
620
+ // #1176 — emit a stderr crumb when invoked with a non-empty command that
621
+ // doesn't match the test-runner pattern. Common pitfall: users run the
622
+ // stamp manually from a terminal to "satisfy the gate"; the silent no-op
623
+ // looks indistinguishable from success. gate-hook.mjs drops stderr from
624
+ // exit-0 invocations, so this only surfaces to direct CLI use — exactly
625
+ // the case where the friction lives.
626
+ var preview = cmd.length > 80 ? cmd.slice(0, 77) + '...' : cmd;
627
+ process.stderr.write('gate: record-test-run no-op — TOOL_INPUT_command="' + preview + '" did not match TEST_RUNNER_RE\n');
534
628
  }
535
629
  break;
536
630
  }
@@ -551,13 +645,21 @@ switch (command) {
551
645
  if (sha && s.simplifySnapshotSha !== sha) { s.simplifySnapshotSha = sha; changed = true; }
552
646
  } catch (e) { /* no git or detached state — skip snapshot, gate still works */ }
553
647
  if (changed) writeState(s);
648
+ } else if (skName) {
649
+ // #1176 — same rationale as record-test-run. A no-op stamp on a non-simplify
650
+ // skill name is silent to hooks (gate-hook.mjs drops exit-0 stderr) but
651
+ // visible when a user runs the stamp directly to "satisfy the gate" and
652
+ // wonders why simplifyRun stays false.
653
+ process.stderr.write('gate: record-skill-run no-op — TOOL_INPUT_skill="' + skName + '" is not simplify/flo-simplify\n');
554
654
  }
555
655
  break;
556
656
  }
557
657
  case 'reset-edit-gates': {
558
658
  var fp = process.env.TOOL_INPUT_file_path || '';
559
- // Inert files (markdown, lockfiles, CHANGELOG, .env.example): no gate reset.
560
- if (fp && EDIT_RESET_SKIP_BOTH_RE.test(fp)) break;
659
+ // Inert files (markdown, lockfiles, CHANGELOG, .env.example) AND inert paths
660
+ // (.github/workflows/, .github/ISSUE_TEMPLATE/, .github/PULL_REQUEST_TEMPLATE/, #1176):
661
+ // no gate reset — editing these doesn't expose new runtime surface.
662
+ if (fp && (EDIT_RESET_SKIP_BOTH_RE.test(fp) || EDIT_RESET_SKIP_PATH_RE.test(fp))) break;
561
663
  var s = readState();
562
664
  // Test-only edits invalidate testsRun but preserve simplifyRun (#908).
563
665
  var isTestOnly = fp && EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE.test(fp);
@@ -580,14 +682,27 @@ switch (command) {
580
682
  // optional ENV=val prefix segment catches `GH_TOKEN=x gh pr create`.
581
683
  var cmd = process.env.TOOL_INPUT_command || '';
582
684
  if (!/(?:^|&&\s*|\|\|\s*|;\s*)\s*(?:[A-Z_][A-Z0-9_]*=\S+\s+)*gh\s+pr\s+create\b/.test(cmd)) break;
583
- // Docs-only exemption: if every file changed vs the merge-base is a docs/image
584
- // file (no runtime-behaviour surface), skip the testing/simplify/learnings gates
685
+ // No-source-files exemption (#1176, supersedes the original docs-only path).
686
+ // If every file changed vs the merge-base is either a docs/image file or a
687
+ // path-inert file (.github/workflows/, ISSUE_TEMPLATE/, PULL_REQUEST_TEMPLATE/)
688
+ // — i.e. NO source files in the diff — skip testing/simplify/learnings gates
585
689
  // and surface a one-line transparency note. Falls through to the standard gate
586
690
  // on any failure (no base, no diff, exec error) — fail-safe by design.
691
+ //
692
+ // Source-file detection is the inverse of the inert checks: a file is "source"
693
+ // when it matches SOURCE_FILE_RE AND is not inside an inert path. This catches
694
+ // `.github/workflows/foo.sh` (sh extension but path-inert → no source).
587
695
  var changed = getChangedFilesVsBase();
588
- if (changed && changed.length > 0 && changed.every(function(f) { return DOCS_ONLY_RE.test(f); })) {
589
- process.stdout.write('Docs-only PR (' + changed.length + ' file' + (changed.length === 1 ? '' : 's') + ') — skipping testing/simplify/learnings gates.\n');
590
- break;
696
+ if (changed && changed.length > 0) {
697
+ var hasSource = changed.some(function(f) {
698
+ return SOURCE_FILE_RE.test(f) && !EDIT_RESET_SKIP_PATH_RE.test(f);
699
+ });
700
+ if (!hasSource) {
701
+ var allDocs = changed.every(function(f) { return DOCS_ONLY_RE.test(f); });
702
+ var reason = allDocs ? 'Docs-only' : 'No source files in branch diff';
703
+ process.stdout.write(reason + ' (' + changed.length + ' file' + (changed.length === 1 ? '' : 's') + ') — skipping testing/simplify/learnings gates.\n');
704
+ break;
705
+ }
591
706
  }
592
707
  var s = readState();
593
708
  // Classifier-aware skip: if delta-since-snapshot or whole-branch diff is
@@ -620,7 +735,17 @@ switch (command) {
620
735
  process.exit(2);
621
736
  }
622
737
  case 'check-dangerous-command': {
623
- var cmd = (process.env.TOOL_INPUT_command || '').toLowerCase();
738
+ // #1171 follow-up strip quoted string bodies and heredoc bodies before
739
+ // substring-matching DANGEROUS. Without this, `git commit -m "...remove-item
740
+ // -recurse -force c:\..."` blocks because the literal pattern appears in
741
+ // the quoted message body. Quoted text isn't executing — the gate's job is
742
+ // to catch typo-class destruction in the actual command, not text mentions
743
+ // inside arguments. Trade-off: `bash -c "rm -rf /"` also bypasses now; the
744
+ // gate is a typo safety net, not a security boundary, so this is acceptable.
745
+ // Command substitutions `$(...)` and backticks are NOT stripped — those
746
+ // bodies execute and dangerous content there is real.
747
+ var raw = process.env.TOOL_INPUT_command || '';
748
+ var cmd = stripQuotedAndHeredocs(raw).toLowerCase();
624
749
  for (var i = 0; i < DANGEROUS.length; i++) {
625
750
  if (cmd.indexOf(DANGEROUS[i]) >= 0) {
626
751
  console.log('[BLOCKED] Dangerous command: ' + DANGEROUS[i]);
@@ -25,9 +25,9 @@ Automated release pipeline for moflo. Bumps version, commits, builds, tests, run
25
25
 
26
26
  ## --check / -ch flag (presence-only)
27
27
 
28
- **Default (flag absent):** runs build + doctor + trigger-based manual checks only. Skips lint/test/smoke because those gates are covered by CI on every PR + push to main (`ci.yml`, `consumer-install-smoke.yml`).
28
+ **Default (flag absent):** runs build + doctor + trigger-based manual checks only. Skips local lint/test/smoke because lint+test are covered by `ci.yml` on every PR + push to main, and **cross-platform smoke is dispatched as part of this skill** (Step 8.5 below) — the `release-smoke.yml` workflow runs the full 3-OS matrix on the exact commit about to be published.
29
29
 
30
- **With `--check` or `-ch` (any presence):** runs the full pre-flight from `pre-publish-rules.md` — lint, build, test, doctor (strict), clean smoke, populated smoke, and a forced full walk of every manual gate. Use this for publishes that didn't go through a green PR, risky releases, or when you want belt-and-suspenders.
30
+ **With `--check` or `-ch` (any presence):** runs the full pre-flight from `pre-publish-rules.md` — lint, build, test, doctor (strict), local clean smoke, local populated smoke, and a forced full walk of every manual gate. Use this for publishes that didn't go through a green PR, risky releases, or when you want belt-and-suspenders on the local box in addition to the CI matrix.
31
31
 
32
32
  The flag is presence-only. `--check` and `-ch` both set it to true. There is no `--check=true` / `--check=false` syntax — absence means false.
33
33
 
@@ -103,14 +103,14 @@ Doctor is the only check with no CI equivalent — it inspects local state (daem
103
103
 
104
104
  **Never `--fix` on the publish path.** A release pipeline must fail fast on broken local state, not silently repair it; a doctor that auto-repairs masks the very signal we want — "something is off, stop and investigate before shipping." If `doctor --strict` fails, stop and run `flo healer --fix` (or `npx moflo doctor --fix`) interactively, verify the repair, then retry the publish.
105
105
 
106
- ### Step 6: Smoke Tests (only if `CHECK_MODE=true`)
106
+ ### Step 6: Local Smoke Tests (only if `CHECK_MODE=true`)
107
107
 
108
108
  ```bash
109
109
  npm run test:smoke
110
110
  npm run test:smoke:populated
111
111
  ```
112
112
 
113
- Skipped by default `consumer-install-smoke.yml` runs both profiles on Ubuntu/macOS/Windows on every PR + push to main. Run when `--check` is set.
113
+ Skipped by default. Local smoke covers the same harness as CI's Ubuntu leg, so it adds little signal over `release-smoke.yml`'s Ubuntu matrix entry (Step 8.5). Run only when `--check` is set — useful if you want belt-and-suspenders on the local box before dispatching the full CI matrix.
114
114
 
115
115
  ### Step 7: Manual Gate Walk (trigger-based, even in default mode)
116
116
 
@@ -144,6 +144,42 @@ git push origin main
144
144
 
145
145
  Only commit version-related files. Do not stage unrelated changes.
146
146
 
147
+ ### Step 8.5: Dispatch release-smoke and block until green
148
+
149
+ ```bash
150
+ # Capture the SHA we just pushed — release-smoke runs against this exact commit.
151
+ PUBLISH_SHA=$(git rev-parse HEAD)
152
+
153
+ # Trigger the full 3-OS × 2-harness matrix. `sha` is a required workflow input
154
+ # so the dispatched run is unambiguously bound to this commit.
155
+ gh workflow run release-smoke.yml --ref main -f sha="$PUBLISH_SHA"
156
+
157
+ # Give GitHub a moment to register the dispatched run.
158
+ sleep 10
159
+
160
+ # Find the run we just dispatched by matching head_sha. This is robust against
161
+ # concurrent dispatches (e.g. a manual UI retry from another tab) — never use
162
+ # `--limit 1` alone, which would race against any unrelated in-flight run.
163
+ RUN_ID=$(gh run list --workflow=release-smoke.yml --branch main \
164
+ --json databaseId,headSha \
165
+ --jq ".[] | select(.headSha == \"$PUBLISH_SHA\") | .databaseId" \
166
+ | head -1)
167
+
168
+ if [ -z "$RUN_ID" ]; then
169
+ echo "ERROR: no release-smoke run found for SHA $PUBLISH_SHA. Wait a few seconds and retry, or check https://github.com/eric-cielo/moflo/actions/workflows/release-smoke.yml" >&2
170
+ exit 1
171
+ fi
172
+
173
+ # Block until the run completes. Exits non-zero on failure.
174
+ gh run watch "$RUN_ID" --exit-status
175
+ ```
176
+
177
+ `release-smoke.yml` runs full consumer + populated smoke on Ubuntu/macOS/Windows, plus file-sync smoke on all three filesystems. Per-PR CI is intentionally Ubuntu-only to keep recurring cost manageable — the full cross-platform matrix is paid once here, at publish time, against the exact commit we're about to publish.
178
+
179
+ **Wall time expectations**: typical run is ~15-20 min once caches are warm. **The first dispatch after `release-smoke.yml` lands will have cold caches on every leg** (no prior runs of this workflow exist to populate the `consumer-warm` and `fastembed` caches per-OS). Expect ~25-35 min on that first dispatch — the macOS leg is the long pole. Subsequent dispatches reuse the cache and settle to the typical ~15 min.
180
+
181
+ **If `gh run watch` exits non-zero**: stop immediately. Inspect the failed leg with `gh run view "$RUN_ID" --log-failed`, fix the underlying issue, push the fix, then re-run this step (no need to rerun Steps 0–8 — the bump commit is already on main, and the SHA-filtered lookup will pick up the new dispatched run against the same SHA only after `cancel-in-progress: false` lets the previous run drain). Do not proceed to `npm publish` until release-smoke is green for the SHA you intend to publish.
182
+
147
183
  ### Step 9: Verify npm Authentication
148
184
 
149
185
  ```bash
@@ -223,8 +259,8 @@ Build: passed
223
259
  Tests: skipped (CI-covered) | <N> files passed, <N> tests passed
224
260
  Lint: skipped (CI-covered) | passed
225
261
  Doctor: <N> passed, <N> warnings
226
- Smoke (clean): skipped (CI-covered) | passed
227
- Smoke (popl): skipped (CI-covered) | passed
262
+ Smoke (local): skipped (CI-covered) | passed
263
+ Release-smoke: green (run <id>, 3-OS × 2-harness)
228
264
  Triggered gates: <list> | none
229
265
  Published: moflo@<new-version>
230
266
  Installed: moflo@<new-version> (devDependency)
@@ -257,5 +293,7 @@ For the common case — publishing right after a merged green PR — the default
257
293
  - `docs/BUILD.md` — Step-by-step build/publish process this skill mirrors
258
294
  - `.claude/guidance/internal/dogfooding.md` — Why we catch consumer-facing regressions first as our own dependency
259
295
  - `harness/consumer-smoke/README.md` — Smoke harness profiles (clean + populated) that prove a consumer install works
260
- - `.github/workflows/ci.yml` — Lint/build/test gates this skill skips by default
261
- - `.github/workflows/consumer-install-smoke.yml` — Smoke gates this skill skips by default
296
+ - `.github/workflows/ci.yml` — Lint/build/test gates this skill skips by default (ubuntu-only)
297
+ - `.github/workflows/consumer-install-smoke.yml` — Per-PR ubuntu-only smoke
298
+ - `.github/workflows/file-sync-smoke.yml` — Per-PR ubuntu-only file-sync smoke
299
+ - `.github/workflows/release-smoke.yml` — Full 3-OS × 2-harness matrix this skill dispatches at Step 8.5