moflo 4.12.0 → 4.12.2

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: base-template-generator
3
- description: Use this agent when you need to create foundational templates, boilerplate code, or starter configurations for new projects, components, or features. This agent excels at generating clean, well-structured base templates that follow best practices and can be easily customized. Examples: <example>Context: User needs to start a new React component and wants a solid foundation. user: 'I need to create a new user profile component' assistant: 'I'll use the base-template-generator agent to create a comprehensive React component template with proper structure, TypeScript definitions, and styling setup.' <commentary>Since the user needs a foundational template for a new component, use the base-template-generator agent to create a well-structured starting point.</commentary></example> <example>Context: User is setting up a new API endpoint and needs a template. user: 'Can you help me set up a new REST API endpoint for user management?' assistant: 'I'll use the base-template-generator agent to create a complete API endpoint template with proper error handling, validation, and documentation structure.' <commentary>The user needs a foundational template for an API endpoint, so use the base-template-generator agent to provide a comprehensive starting point.</commentary></example>
3
+ description: Create foundational templates, boilerplate code, and starter configurations for new projects, components, or features. Use for scaffolding a new component, API endpoint, model, config, or test suite from scratch.
4
4
  color: orange
5
5
  ---
6
6
 
@@ -19,6 +19,26 @@ On chunk hits where `navigation` is non-null, traverse via `mcp__moflo__memory_g
19
19
 
20
20
  You are a Base Template Generator, an expert architect specializing in creating clean, well-structured foundational templates and boilerplate code. Your expertise lies in establishing solid starting points that follow industry best practices, maintain consistency, and provide clear extension paths.
21
21
 
22
+ ## When you get dispatched
23
+
24
+ Worked examples of the requests that route here — these live in the body, not the
25
+ frontmatter `description`, because the description is always-resident routing
26
+ context while this body only loads on dispatch.
27
+
28
+ <example>
29
+ Context: User needs to start a new React component and wants a solid foundation.
30
+ user: 'I need to create a new user profile component'
31
+ assistant: 'I'll use the base-template-generator agent to create a comprehensive React component template with proper structure, TypeScript definitions, and styling setup.'
32
+ <commentary>Since the user needs a foundational template for a new component, use the base-template-generator agent to create a well-structured starting point.</commentary>
33
+ </example>
34
+
35
+ <example>
36
+ Context: User is setting up a new API endpoint and needs a template.
37
+ user: 'Can you help me set up a new REST API endpoint for user management?'
38
+ assistant: 'I'll use the base-template-generator agent to create a complete API endpoint template with proper error handling, validation, and documentation structure.'
39
+ <commentary>The user needs a foundational template for an API endpoint, so use the base-template-generator agent to provide a comprehensive starting point.</commentary>
40
+ </example>
41
+
22
42
  Your core responsibilities:
23
43
  - Generate comprehensive base templates for components, modules, APIs, configurations, and project structures
24
44
  - Ensure all templates follow established coding standards and best practices from the project's CLAUDE.md guidelines
@@ -123,14 +123,54 @@ auto_update:
123
123
  helpers: true # Sync .claude/helpers/ from moflo source
124
124
  hook_block_drift: regenerate # warn | regenerate | off (default: regenerate since #1227)
125
125
  claudemd_injection_drift: regenerate # warn | regenerate | off
126
+
127
+ # Which skill categories to install (OPTIONAL — omit to get every skill)
128
+ skills:
129
+ categories: [core, memory, spells] # core | memory | spells
126
130
  ```
127
131
 
128
132
  If your `moflo.yaml` predates the `sandbox:` or `auto_update:` blocks, they are auto-appended on the next session start — you never need to re-run `moflo init` after a version bump.
129
133
 
134
+ ### Narrowing Installed Skills with `skills.categories`
135
+
136
+ **Omit the `skills:` block entirely to receive every moflo skill — that is the default and the recommended setting.** Add `skills.categories` only when you want a smaller always-resident skill listing, and expect to lose the commands in the categories you drop.
137
+
138
+ Every installed skill's name and description sit in Claude's context in every session, so a project that will never author a spell can reclaim that space. The cost is real but small — the full set is roughly 2,000 tokens.
139
+
140
+ | Category | Skills | Drop it when |
141
+ |----------|--------|--------------|
142
+ | `core` | `/fl`, `/healer`, `/verify`, `/flo-simplify`, `/eldar`, `/quicken`, `/ward`, `/divine`, `/meditate`, `/guidance`, `/commune`, `/luminarium` | Never — these are the everyday commands |
143
+ | `memory` | `/memory-patterns`, `/vector-search`, `/memory-optimization`, `/memory-team`, `/memory-worktree` | You are not building on moflo's memory stack and have finished any team/worktree sharing setup |
144
+ | `spells` | `/spell-builder`, `/spell-schedule`, `/connector-builder` | You do not author or schedule spells in this project |
145
+
146
+ Both YAML list styles work:
147
+
148
+ ```yaml
149
+ skills:
150
+ categories: [core, memory]
151
+ ```
152
+
153
+ ```yaml
154
+ skills:
155
+ categories:
156
+ - core
157
+ - memory
158
+ ```
159
+
160
+ **Key behaviors:**
161
+
162
+ - **Omitting `skills:` means no restriction.** Every shipped skill syncs on every session start, exactly as before this option existed.
163
+ - **`/flo` and `/fl` are never gated.** The ticket spell is the primary entry point and installs regardless of selection.
164
+ - **An unknown category name is ignored, not fatal.** A typo narrows nothing rather than silently stripping your skills.
165
+ - **Narrowing stops future syncing; it does not delete.** Skills already on disk stay until you remove them — moflo will not re-add them once excluded, so delete the directories under `.claude/skills/` to reclaim the context.
166
+ - **Re-widening is instant.** Add the category back and the next session start restores its skills.
167
+
130
168
  ### Key Behaviors
131
169
 
132
170
  | Config | Effect |
133
171
  |--------|--------|
172
+ | `skills.categories` omitted | Install every skill (default) |
173
+ | `skills.categories: [core]` | Install only core skills; `memory` + `spells` stop syncing |
134
174
  | `auto_index.guidance: false` | Skip guidance indexing on session start |
135
175
  | `auto_index.code_map: false` | Skip code map generation on session start |
136
176
  | `auto_meditate.enabled: false` | Opt out of automatic session-lesson distillation (`/meditate` still works manually) |
@@ -104,6 +104,19 @@ function loadSddConfig() {
104
104
  return out;
105
105
  }
106
106
 
107
+ // Parse the top-level `merge:` block from moflo.yaml (#1285). Block-scoped for
108
+ // the same reason as loadSddConfig: a bare `auto:` key can appear under other
109
+ // sections. Cross-platform: tolerates CRLF.
110
+ function loadMergeConfig() {
111
+ var out = { auto: false };
112
+ var content = MOFLO_YAML;
113
+ if (!content) return out;
114
+ var block = content.match(/^merge:[ \t]*\r?\n((?:[ \t]+.*(?:\r?\n|$))*)/m);
115
+ if (!block) return out;
116
+ if (/^\s*auto:\s*true\b/im.test(block[1])) out.auto = true;
117
+ return out;
118
+ }
119
+
107
120
  // Read moflo.yaml exactly once per gate process (#1297 review): loadGateConfig
108
121
  // and loadSddConfig both parse it, and the gate fires on every Write/Edit — a
109
122
  // second read is wasted syscalls. Single readFileSync in try/catch (no existsSync
@@ -116,10 +129,18 @@ var MOFLO_YAML = readMofloYaml();
116
129
 
117
130
  var config = loadGateConfig();
118
131
  var sddConf = loadSddConfig();
132
+ var mergeConf = loadMergeConfig();
119
133
  var command = process.argv[2];
120
134
 
121
135
  var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules', 'moflo.yaml'];
122
136
 
137
+ // Appended to every memory-first denial. Without it, a context audit of Claude
138
+ // Code's denial telemetry reads moflo's gate enforcements as permission-setup
139
+ // failures and "fixes" them with allow rules — which cannot override a hook
140
+ // block at all (#1307 finding 5). Naming the gate makes the cause self-evident.
141
+ var GATE_ORIGIN_NOTE = 'This is a moflo hook, not a Claude Code permission rule — allow-rules cannot override it.';
142
+ var GATE_DISABLE_NOTE = 'Disable per-gate via moflo.yaml: gates: memory_first: false';
143
+
123
144
  // #1294 Finding 3 — reads/scans of EPHEMERAL files under the OS temp dir
124
145
  // (background-task output/transcripts, agent scratchpads) are transient tool
125
146
  // I/O and never carry indexable project knowledge, so they must not trip the
@@ -288,16 +309,71 @@ function detectFlMode(promptText) {
288
309
  return null;
289
310
  }
290
311
 
291
- // #1297 arm the SDD implement gate from the user prompt. Only /fl or /flo runs
292
- // can arm it. Explicit -sd/--sdd wins; --no-sdd disarms; otherwise honor the
293
- // sdd.default config. `-sd` is a distinct token from `-s` (swarm): the `d` sits
294
- // on the word boundary so `-s\b` never matches `-sd`.
295
- function detectSddMode(promptText) {
312
+ // Resolve the FULL set of /flo run modifiers from the prompt + moflo.yaml the
313
+ // single source of truth for both gate arming (#1297) and the authoritative
314
+ // announcement emitted by prompt-reminder. Two consumers, one resolver: a second
315
+ // implementation is exactly how `sdd.default: true` got silently ignored (the
316
+ // skill's prompt carried `let sddMode = false` and the model executed the literal).
317
+ //
318
+ // Precedence per key: explicit --no-X > explicit -x/--X > moflo.yaml > built-in.
319
+ // NOTE the differing built-in defaults: sdd is opt-IN (false), verify is opt-OUT
320
+ // (true, #1294), merge is opt-IN (false, #1285).
321
+ //
322
+ // `-sd` is a distinct token from `-s` (swarm): the `d` sits on the word boundary
323
+ // so `-s\b` never matches `-sd`.
324
+ function resolveFloRun(promptText) {
296
325
  var p = promptText || '';
297
- if (!/^\s*\/(?:fl|flo)\b/i.test(p)) return false;
298
- if (/(?:^|\s)--no-sdd\b/.test(p)) return false;
299
- if (/(?:^|\s)(?:-sd|--sdd)\b/.test(p)) return true;
300
- return !!sddConf.default;
326
+ var out = { isFlo: false, workflow: 'full', sdd: false, verify: false, merge: false,
327
+ sddSrc: 'default', verifySrc: 'default', mergeSrc: 'default' };
328
+ if (!/^\s*\/(?:fl|flo)\b/i.test(p)) return out;
329
+ out.isFlo = true;
330
+
331
+ // Workflow mode — decides which modifiers are even applicable.
332
+ if (/(?:^|\s)(?:-wf|--workflow)\b/.test(p)) out.workflow = 'spell-engine';
333
+ else if (/(?:^|\s)(?:-r|--research)\b/.test(p)) out.workflow = 'research';
334
+ else if (/(?:^|\s)(?:-t|--ticket)\b/.test(p)) out.workflow = 'ticket';
335
+ var epicBranch = /(?:^|\s)--epic-branch\b/.test(p);
336
+
337
+ if (/(?:^|\s)--no-sdd\b/.test(p)) { out.sdd = false; out.sddSrc = 'flag'; }
338
+ else if (/(?:^|\s)(?:-sd|--sdd)\b/.test(p)) { out.sdd = true; out.sddSrc = 'flag'; }
339
+ else if (sddConf.default) { out.sdd = true; out.sddSrc = 'moflo.yaml sdd.default'; }
340
+
341
+ if (/(?:^|\s)--no-verify\b/.test(p)) { out.verify = false; out.verifySrc = 'flag'; }
342
+ else if (/(?:^|\s)(?:-v|--verify)\b/.test(p)) { out.verify = true; out.verifySrc = 'flag'; }
343
+ else if (!config.verify_before_done) { out.verify = false; out.verifySrc = 'moflo.yaml gates.verify_before_done'; }
344
+ else { out.verify = true; out.verifySrc = 'default'; }
345
+ // --sdd implies --verify: a spec/plan without an enforced verify step drifts.
346
+ if (out.sdd && !out.verify && out.verifySrc !== 'flag') out.verify = true;
347
+
348
+ if (/(?:^|\s)--no-merge\b/.test(p)) { out.merge = false; out.mergeSrc = 'flag'; }
349
+ else if (/(?:^|\s)(?:-m|--merge)\b/.test(p)) { out.merge = true; out.mergeSrc = 'flag'; }
350
+ else if (mergeConf.auto) { out.merge = true; out.mergeSrc = 'moflo.yaml merge.auto'; }
351
+
352
+ // Applicability: -t/-r never implement, so verify is a no-op there; -r produces
353
+ // no artifacts, so sdd is a no-op too (in -t the spec/plan goes INTO the ticket,
354
+ // so sdd stays on). Only a full non-epic-branch run opens a PR to merge.
355
+ // Re-attribute anything applicability turned off, so a false never carries the
356
+ // source of the value it no longer has. This whole change exists to stop modes
357
+ // being reported inaccurately — the attribution has to be honest too.
358
+ if (out.workflow === 'ticket' || out.workflow === 'research') {
359
+ if (out.verify) out.verifySrc = out.workflow + ' mode does not implement';
360
+ out.verify = false;
361
+ }
362
+ if (out.workflow === 'research' || out.workflow === 'spell-engine') {
363
+ if (out.sdd) out.sddSrc = out.workflow + ' mode produces no spec artifacts';
364
+ out.sdd = false;
365
+ }
366
+ if (out.workflow !== 'full' || epicBranch) {
367
+ if (out.merge) out.mergeSrc = epicBranch ? '--epic-branch owns merging' : out.workflow + ' mode opens no PR';
368
+ out.merge = false;
369
+ }
370
+ return out;
371
+ }
372
+
373
+ // #1297 — arm the SDD implement gate from the user prompt. Thin wrapper so the
374
+ // arming decision and the announced decision can never disagree.
375
+ function detectSddMode(promptText) {
376
+ return resolveFloRun(promptText).sdd;
301
377
  }
302
378
 
303
379
  // Resolve the absolute specs root the same way TS specsRoot does (#1294): split
@@ -644,7 +720,7 @@ switch (command) {
644
720
  var target = (process.env.TOOL_INPUT_pattern || '') + ' ' + (process.env.TOOL_INPUT_path || '');
645
721
  if (isEphemeralPath(process.env.TOOL_INPUT_path)) break;
646
722
  if (EXEMPT.some(function(p) { return target.indexOf(p) >= 0; })) break;
647
- process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
723
+ process.stderr.write('BLOCKED [moflo memory_first gate]: Search memory before exploring files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n' + GATE_ORIGIN_NOTE + '\n' + GATE_DISABLE_NOTE + '\n');
648
724
  process.exit(2);
649
725
  }
650
726
  case 'check-before-read': {
@@ -657,7 +733,7 @@ switch (command) {
657
733
  if (isEphemeralPath(fp)) break;
658
734
  var isGuidance = fp.indexOf('.claude/guidance/') >= 0 || fp.indexOf('.claude\\guidance\\') >= 0;
659
735
  if (!isGuidance && EXEMPT.some(function(p) { return fp.indexOf(p) >= 0; })) break;
660
- process.stderr.write('BLOCKED: Search memory before reading files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
736
+ process.stderr.write('BLOCKED [moflo memory_first gate]: Search memory before reading files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n' + GATE_ORIGIN_NOTE + '\n' + GATE_DISABLE_NOTE + '\n');
661
737
  process.exit(2);
662
738
  }
663
739
  case 'record-task-created': {
@@ -706,11 +782,12 @@ switch (command) {
706
782
  // "Memory namespace hint: ..." sentence so the BLOCK message stays uniform.
707
783
  var hint = s2.lastNamespaceHint || classifyBashNamespaceHint(cmd) || '';
708
784
  process.stderr.write(
709
- 'BLOCKED: Search memory before reading files via Bash.\n' +
785
+ 'BLOCKED [moflo memory_first gate]: Search memory before reading files via Bash.\n' +
710
786
  'Example: mcp__moflo__memory_search { query: "<topic>", namespace: "<one of: guidance | code-map | patterns | learnings | tests>" }\n' +
711
787
  (hint ? hint + '\n' : '') +
712
788
  'On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n' +
713
- 'Disable per-gate via moflo.yaml: gates: memory_first: false\n'
789
+ GATE_ORIGIN_NOTE + '\n' +
790
+ GATE_DISABLE_NOTE + '\n'
714
791
  );
715
792
  process.exit(2);
716
793
  break;
@@ -981,6 +1058,33 @@ switch (command) {
981
1058
  applyPromptStateReset(s, prompt);
982
1059
  s.interactionCount = (s.interactionCount || 0) + 1;
983
1060
  writeState(s);
1061
+ // Announce the resolved /flo run modifiers. The gate already parsed
1062
+ // moflo.yaml in THIS process (fresh per prompt — a git pull or a mid-session
1063
+ // yaml edit is picked up automatically, no cache to invalidate), so this
1064
+ // costs no extra read. Emitting it is what closes the loop: the skill used
1065
+ // to carry `let sddMode = false` as a literal and the model executed it,
1066
+ // silently ignoring `sdd.default: true`. Only fires on /fl|/flo prompts.
1067
+ var floRun = resolveFloRun(prompt);
1068
+ if (floRun.isFlo) {
1069
+ console.log(
1070
+ '[moflo] /flo run modes (AUTHORITATIVE — use verbatim; do NOT re-derive from the skill defaults): ' +
1071
+ 'sdd=' + (floRun.sdd ? 'ON' : 'off') +
1072
+ ' verify=' + (floRun.verify ? 'ON' : 'off') +
1073
+ ' merge=' + (floRun.merge ? 'ON' : 'off') +
1074
+ ' [workflow=' + floRun.workflow + ']'
1075
+ );
1076
+ // Spell out the surprising case: a project default the user did not type.
1077
+ if (floRun.sdd && floRun.sddSrc !== 'flag') {
1078
+ console.log(
1079
+ '[moflo] sdd is ON via ' + floRun.sddSrc + ' — the spec→plan→implement→verify cycle is ' +
1080
+ 'MANDATORY this run. Author the spec before editing source (the sdd_gate blocks source ' +
1081
+ 'Write/Edit until a reviewed plan exists). One-off opt out: re-run with --no-sdd.'
1082
+ );
1083
+ }
1084
+ if (floRun.merge && floRun.mergeSrc !== 'flag') {
1085
+ console.log('[moflo] merge is ON via ' + floRun.mergeSrc + ' — the PR will be auto-merged. Opt out: --no-merge.');
1086
+ }
1087
+ }
984
1088
  if (config.context_tracking) {
985
1089
  var ic = s.interactionCount;
986
1090
  if (ic > 30) console.log('Context: CRITICAL. Commit, store learnings, suggest new session.');
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: distill
3
- description: Alias for /flo-simplify. Review changed code for reuse, quality, and efficiency, then fix any issues found effort scales to the diff size.
3
+ description: Alias for /flo-simplify — see that skill's description.
4
4
  ---
5
5
 
6
6
  # /distill is an alias for /flo-simplify
@@ -72,7 +72,7 @@ Two **independent** modifiers, orthogonal to execution mode (`-n`/`-s`/`-h`) and
72
72
  | `-v` | `--verify` | **Verify-before-done** — a normal run plus the `/verify` skill (the completion gate), no spec/plan front-half. **On by default** (#1294) — this flag only forces it back on for a project that set `gates.verify_before_done: false`. |
73
73
  | `--no-sdd`, `--no-verify` | | Opt a single run out. `--no-verify` skips the (default-on) verify step. |
74
74
 
75
- Defaults seed from `moflo.yaml` — `sdd.default` (default **off**) and `gates.verify_before_done` (default **on** since #1294). So the SDD spec/plan ceremony is opt-in, but **verify-before-done runs by default**; per-run flags override (`--no-verify` to skip). `--sdd` implies `--verify` (a spec/plan without an enforced verify step drifts). In `-t`/`-r` modes (no implementation) verify is a no-op — cleared silently, with the one-line ignored note only when the user explicitly passed `-v`/`--verify`; `--sdd` in `-t` writes the spec/plan **into the ticket** rather than scaffolding artifacts. Full mechanics in `./sdd.md`.
75
+ Defaults seed from `moflo.yaml` — `sdd.default` (built-in **off**) and `gates.verify_before_done` (built-in **on** since #1294). So the SDD spec/plan ceremony is opt-in *by default*, but **a project can turn it on for every run** — never assume it is off; resolve it per the "Resolved run modes" section below. Verify-before-done runs by default; per-run flags override (`--no-verify` to skip). `--sdd` implies `--verify` (a spec/plan without an enforced verify step drifts). In `-t`/`-r` modes (no implementation) verify is a no-op — cleared silently, with the one-line ignored note only when the user explicitly passed `-v`/`--verify`; `--sdd` in `-t` writes the spec/plan **into the ticket** rather than scaffolding artifacts. Full mechanics in `./sdd.md`.
76
76
 
77
77
  ## Auto-merge (#1285)
78
78
 
@@ -124,6 +124,35 @@ Read the relevant file before executing that part of the run.
124
124
  | `./spell-engine.md` | `-wf` invocations (list, info, execute) |
125
125
  | `./sdd.md` | `-sd`/`--sdd` and `-v`/`--verify` — the spec→plan→implement→verify cycle |
126
126
 
127
+ ## Resolved run modes — read this BEFORE parsing arguments
128
+
129
+ `sddMode`, `verifyMode`, and `mergeMode` are **config-derived**. Their project defaults live in `moflo.yaml` (`sdd.default`, `gates.verify_before_done`, `merge.auto`), so they cannot be known from this document — you MUST obtain them, never assume them. A run that assumes `sdd=off` on a project with `sdd.default: true` silently skips the entire spec→plan cycle, which is the exact failure this step exists to prevent.
130
+
131
+ **Primary source — the injected resolution line.** moflo's `prompt-reminder` hook resolves all three from `moflo.yaml` on every `/flo` prompt (fresh process per prompt, so a `git pull` or a mid-session edit to `moflo.yaml` is picked up automatically) and emits them into your context immediately above the user's message:
132
+
133
+ ```
134
+ [moflo] /flo run modes (AUTHORITATIVE — use verbatim; do NOT re-derive from the skill defaults): sdd=ON verify=ON merge=off [workflow=full]
135
+ ```
136
+
137
+ Scan for that `[moflo] /flo run modes` line and assign `sddMode` / `verifyMode` / `mergeMode` from it **verbatim**. It already accounts for flags the user typed, `moflo.yaml` defaults, `--sdd` implying `--verify`, and mode applicability — do not second-guess it.
138
+
139
+ **Fallback — no such line in context** (hooks disabled, or a consumer on a gate.cjs predating this). Shell out once; the CLI resolves from the same precedence rules:
140
+
141
+ ```bash
142
+ flo sdd mode --args="$ARGUMENTS"
143
+ ```
144
+
145
+ Use `--args=` (with the `=`), **not** `--args <value>` — a value starting with `-` would otherwise be parsed as flags and you would silently get the bare config default back.
146
+
147
+ ```json
148
+ { "workflow": "full", "sdd": true, "verify": true, "merge": false,
149
+ "sddSrc": "moflo.yaml sdd.default", "verifySrc": "default", "mergeSrc": "default" }
150
+ ```
151
+
152
+ **Never skip both.** If the injected line is absent and the CLI call fails, say so explicitly to the user and stop rather than proceeding on a guessed mode — a wrongly-assumed `sdd=off` is invisible to the user until the PR lands without a spec.
153
+
154
+ When `sdd` resolved ON from config rather than a typed flag, state that in your first message (e.g. *"SDD is on via `moflo.yaml sdd.default` — running the spec→plan→implement→verify cycle."*) so the user can `--no-sdd` out before work starts.
155
+
127
156
  ## Argument parsing
128
157
 
129
158
  ```javascript
@@ -135,21 +164,13 @@ let epicBranch = null;
135
164
  let issueNumber = null;
136
165
  let titleWords = [];
137
166
 
138
- // SDD/verify modifiers (Epic #1269, #1294). Seed from moflo.yaml BEFORE parsing
139
- // so a project default applies unless a per-run flag overrides it. Read the two
140
- // keys from moflo.yaml at the project root NOTE the different absent-defaults:
141
- // sddMode ← `sdd.default` (absent false spec/plan is opt-in)
142
- // verifyMode `gates.verify_before_done` (absent TRUE verify is on by default, #1294)
143
- let sddMode = false; // -sd / --sdd (full spec→plan→implement→verify)
144
- let verifyMode = true; // -v / --verify (on by default; --no-verify opts out)
167
+ // SDD / verify / merge modifiers (Epic #1269, #1294, #1285) DO NOT resolve
168
+ // these here. They are config-derived, and the values below are placeholders,
169
+ // NOT defaults. Take them from the "Resolved run modes" step above; the loop
170
+ // below only applies what the user typed on top of that resolution.
171
+ let sddMode, verifyMode, mergeMode; // assigned from the resolution, never guessed
145
172
  let verifyExplicit = false; // did the user actually type -v/--verify? (drives the -t/-r note only, so it doesn't fire on the default)
146
173
 
147
- // Auto-merge modifier (#1285). Seed from moflo.yaml BEFORE parsing, same as
148
- // sddMode/verifyMode: read `merge.auto` at the project root (absent ⇒ false).
149
- // When true, a full run awaits the PR's merge preconditions and merges it
150
- // (Phase 5.3b) instead of stopping at "PR opened".
151
- let mergeMode = false; // -m / --merge ← moflo.yaml merge.auto
152
-
153
174
  let wfName = null, wfSubcommand = null;
154
175
  let wfArgs = [], wfNamedArgs = {};
155
176
 
@@ -2,10 +2,12 @@
2
2
 
3
3
  Spec-Driven Development for `/flo` — Epic #1269. Two independent modifiers:
4
4
 
5
- - **`-sd` / `--sdd`** — run the full **spec → plan → (review) → implement → verify** cycle. Opt-in (`sdd.default` defaults false). Implies verify.
5
+ - **`-sd` / `--sdd`** — run the full **spec → plan → (review) → implement → verify** cycle. Opt-in *by built-in default* (`sdd.default` = false), but **a project can set `sdd.default: true` and turn it on for every run**. Implies verify.
6
6
  - **`-v` / `--verify`** — verify-before-done: a normal run plus the completion gate, no spec/plan front-half. **On by default** (`gates.verify_before_done` defaults true, #1294) — `-v` is explicit, `--no-verify` opts out for one run.
7
7
 
8
- Defaults seed from `moflo.yaml` (`sdd.default` = false, `gates.verify_before_done` = true); per-run flags and `--no-sdd` / `--no-verify` override. Both are orthogonal to execution mode (`-n`/`-s`/`-h`) and `--worktree`, so `--sdd -s -wt 42` runs the SDD cycle in a swarm inside a worktree.
8
+ **Never infer whether SDD is on from these built-in defaults.** They are project-configurable, so the effective value is only knowable at run time. Resolve it as `SKILL.md` § "Resolved run modes" describes — the `[moflo] /flo run modes` line injected into context, or `flo sdd mode --args="$ARGUMENTS"`. Assuming `sdd=off` on a project with `sdd.default: true` silently skips the whole spec/plan cycle and is invisible to the user until the PR lands without a spec.
9
+
10
+ Precedence: per-run flags and `--no-sdd` / `--no-verify` override `moflo.yaml`, which overrides the built-ins. Both are orthogonal to execution mode (`-n`/`-s`/`-h`) and `--worktree`, so `--sdd -s -wt 42` runs the SDD cycle in a swarm inside a worktree.
9
11
 
10
12
  The artifact model, paths, and CLI live in `src/cli/sdd/` (`flo sdd …`). The constitution layer (CLAUDE.md + `.claude/guidance/`) is referenced by every stage — never restated in a spec.
11
13
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: perf-audit
3
- description: Alias for /quicken. Ad-hoc performance audit reveals N+1 queries, needless re-renders, missing caching, memory leaks, and redundant computation in your diff (or a named path), and prescribes the fix inline.
3
+ description: Alias for /quicken — see that skill's description.
4
4
  arguments: "[path or area | --all]"
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: test-gaps
3
- description: Alias for /ward. Ad-hoc test-gap audit reveals untested functions, uncovered edge cases, and missing error-handling/integration tests in your diff (or a named path), and conjures ready-to-paste test skeletons inline.
3
+ description: Alias for /ward — see that skill's description.
4
4
  arguments: "[path or area | --all]"
5
5
  ---
6
6
 
package/bin/gate.cjs CHANGED
@@ -104,6 +104,19 @@ function loadSddConfig() {
104
104
  return out;
105
105
  }
106
106
 
107
+ // Parse the top-level `merge:` block from moflo.yaml (#1285). Block-scoped for
108
+ // the same reason as loadSddConfig: a bare `auto:` key can appear under other
109
+ // sections. Cross-platform: tolerates CRLF.
110
+ function loadMergeConfig() {
111
+ var out = { auto: false };
112
+ var content = MOFLO_YAML;
113
+ if (!content) return out;
114
+ var block = content.match(/^merge:[ \t]*\r?\n((?:[ \t]+.*(?:\r?\n|$))*)/m);
115
+ if (!block) return out;
116
+ if (/^\s*auto:\s*true\b/im.test(block[1])) out.auto = true;
117
+ return out;
118
+ }
119
+
107
120
  // Read moflo.yaml exactly once per gate process (#1297 review): loadGateConfig
108
121
  // and loadSddConfig both parse it, and the gate fires on every Write/Edit — a
109
122
  // second read is wasted syscalls. Single readFileSync in try/catch (no existsSync
@@ -116,10 +129,18 @@ var MOFLO_YAML = readMofloYaml();
116
129
 
117
130
  var config = loadGateConfig();
118
131
  var sddConf = loadSddConfig();
132
+ var mergeConf = loadMergeConfig();
119
133
  var command = process.argv[2];
120
134
 
121
135
  var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules', 'moflo.yaml'];
122
136
 
137
+ // Appended to every memory-first denial. Without it, a context audit of Claude
138
+ // Code's denial telemetry reads moflo's gate enforcements as permission-setup
139
+ // failures and "fixes" them with allow rules — which cannot override a hook
140
+ // block at all (#1307 finding 5). Naming the gate makes the cause self-evident.
141
+ var GATE_ORIGIN_NOTE = 'This is a moflo hook, not a Claude Code permission rule — allow-rules cannot override it.';
142
+ var GATE_DISABLE_NOTE = 'Disable per-gate via moflo.yaml: gates: memory_first: false';
143
+
123
144
  // #1294 Finding 3 — reads/scans of EPHEMERAL files under the OS temp dir
124
145
  // (background-task output/transcripts, agent scratchpads) are transient tool
125
146
  // I/O and never carry indexable project knowledge, so they must not trip the
@@ -288,16 +309,71 @@ function detectFlMode(promptText) {
288
309
  return null;
289
310
  }
290
311
 
291
- // #1297 arm the SDD implement gate from the user prompt. Only /fl or /flo runs
292
- // can arm it. Explicit -sd/--sdd wins; --no-sdd disarms; otherwise honor the
293
- // sdd.default config. `-sd` is a distinct token from `-s` (swarm): the `d` sits
294
- // on the word boundary so `-s\b` never matches `-sd`.
295
- function detectSddMode(promptText) {
312
+ // Resolve the FULL set of /flo run modifiers from the prompt + moflo.yaml the
313
+ // single source of truth for both gate arming (#1297) and the authoritative
314
+ // announcement emitted by prompt-reminder. Two consumers, one resolver: a second
315
+ // implementation is exactly how `sdd.default: true` got silently ignored (the
316
+ // skill's prompt carried `let sddMode = false` and the model executed the literal).
317
+ //
318
+ // Precedence per key: explicit --no-X > explicit -x/--X > moflo.yaml > built-in.
319
+ // NOTE the differing built-in defaults: sdd is opt-IN (false), verify is opt-OUT
320
+ // (true, #1294), merge is opt-IN (false, #1285).
321
+ //
322
+ // `-sd` is a distinct token from `-s` (swarm): the `d` sits on the word boundary
323
+ // so `-s\b` never matches `-sd`.
324
+ function resolveFloRun(promptText) {
296
325
  var p = promptText || '';
297
- if (!/^\s*\/(?:fl|flo)\b/i.test(p)) return false;
298
- if (/(?:^|\s)--no-sdd\b/.test(p)) return false;
299
- if (/(?:^|\s)(?:-sd|--sdd)\b/.test(p)) return true;
300
- return !!sddConf.default;
326
+ var out = { isFlo: false, workflow: 'full', sdd: false, verify: false, merge: false,
327
+ sddSrc: 'default', verifySrc: 'default', mergeSrc: 'default' };
328
+ if (!/^\s*\/(?:fl|flo)\b/i.test(p)) return out;
329
+ out.isFlo = true;
330
+
331
+ // Workflow mode — decides which modifiers are even applicable.
332
+ if (/(?:^|\s)(?:-wf|--workflow)\b/.test(p)) out.workflow = 'spell-engine';
333
+ else if (/(?:^|\s)(?:-r|--research)\b/.test(p)) out.workflow = 'research';
334
+ else if (/(?:^|\s)(?:-t|--ticket)\b/.test(p)) out.workflow = 'ticket';
335
+ var epicBranch = /(?:^|\s)--epic-branch\b/.test(p);
336
+
337
+ if (/(?:^|\s)--no-sdd\b/.test(p)) { out.sdd = false; out.sddSrc = 'flag'; }
338
+ else if (/(?:^|\s)(?:-sd|--sdd)\b/.test(p)) { out.sdd = true; out.sddSrc = 'flag'; }
339
+ else if (sddConf.default) { out.sdd = true; out.sddSrc = 'moflo.yaml sdd.default'; }
340
+
341
+ if (/(?:^|\s)--no-verify\b/.test(p)) { out.verify = false; out.verifySrc = 'flag'; }
342
+ else if (/(?:^|\s)(?:-v|--verify)\b/.test(p)) { out.verify = true; out.verifySrc = 'flag'; }
343
+ else if (!config.verify_before_done) { out.verify = false; out.verifySrc = 'moflo.yaml gates.verify_before_done'; }
344
+ else { out.verify = true; out.verifySrc = 'default'; }
345
+ // --sdd implies --verify: a spec/plan without an enforced verify step drifts.
346
+ if (out.sdd && !out.verify && out.verifySrc !== 'flag') out.verify = true;
347
+
348
+ if (/(?:^|\s)--no-merge\b/.test(p)) { out.merge = false; out.mergeSrc = 'flag'; }
349
+ else if (/(?:^|\s)(?:-m|--merge)\b/.test(p)) { out.merge = true; out.mergeSrc = 'flag'; }
350
+ else if (mergeConf.auto) { out.merge = true; out.mergeSrc = 'moflo.yaml merge.auto'; }
351
+
352
+ // Applicability: -t/-r never implement, so verify is a no-op there; -r produces
353
+ // no artifacts, so sdd is a no-op too (in -t the spec/plan goes INTO the ticket,
354
+ // so sdd stays on). Only a full non-epic-branch run opens a PR to merge.
355
+ // Re-attribute anything applicability turned off, so a false never carries the
356
+ // source of the value it no longer has. This whole change exists to stop modes
357
+ // being reported inaccurately — the attribution has to be honest too.
358
+ if (out.workflow === 'ticket' || out.workflow === 'research') {
359
+ if (out.verify) out.verifySrc = out.workflow + ' mode does not implement';
360
+ out.verify = false;
361
+ }
362
+ if (out.workflow === 'research' || out.workflow === 'spell-engine') {
363
+ if (out.sdd) out.sddSrc = out.workflow + ' mode produces no spec artifacts';
364
+ out.sdd = false;
365
+ }
366
+ if (out.workflow !== 'full' || epicBranch) {
367
+ if (out.merge) out.mergeSrc = epicBranch ? '--epic-branch owns merging' : out.workflow + ' mode opens no PR';
368
+ out.merge = false;
369
+ }
370
+ return out;
371
+ }
372
+
373
+ // #1297 — arm the SDD implement gate from the user prompt. Thin wrapper so the
374
+ // arming decision and the announced decision can never disagree.
375
+ function detectSddMode(promptText) {
376
+ return resolveFloRun(promptText).sdd;
301
377
  }
302
378
 
303
379
  // Resolve the absolute specs root the same way TS specsRoot does (#1294): split
@@ -644,7 +720,7 @@ switch (command) {
644
720
  var target = (process.env.TOOL_INPUT_pattern || '') + ' ' + (process.env.TOOL_INPUT_path || '');
645
721
  if (isEphemeralPath(process.env.TOOL_INPUT_path)) break;
646
722
  if (EXEMPT.some(function(p) { return target.indexOf(p) >= 0; })) break;
647
- process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
723
+ process.stderr.write('BLOCKED [moflo memory_first gate]: Search memory before exploring files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n' + GATE_ORIGIN_NOTE + '\n' + GATE_DISABLE_NOTE + '\n');
648
724
  process.exit(2);
649
725
  }
650
726
  case 'check-before-read': {
@@ -657,7 +733,7 @@ switch (command) {
657
733
  if (isEphemeralPath(fp)) break;
658
734
  var isGuidance = fp.indexOf('.claude/guidance/') >= 0 || fp.indexOf('.claude\\guidance\\') >= 0;
659
735
  if (!isGuidance && EXEMPT.some(function(p) { return fp.indexOf(p) >= 0; })) break;
660
- process.stderr.write('BLOCKED: Search memory before reading files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
736
+ process.stderr.write('BLOCKED [moflo memory_first gate]: Search memory before reading files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n' + GATE_ORIGIN_NOTE + '\n' + GATE_DISABLE_NOTE + '\n');
661
737
  process.exit(2);
662
738
  }
663
739
  case 'record-task-created': {
@@ -706,11 +782,12 @@ switch (command) {
706
782
  // "Memory namespace hint: ..." sentence so the BLOCK message stays uniform.
707
783
  var hint = s2.lastNamespaceHint || classifyBashNamespaceHint(cmd) || '';
708
784
  process.stderr.write(
709
- 'BLOCKED: Search memory before reading files via Bash.\n' +
785
+ 'BLOCKED [moflo memory_first gate]: Search memory before reading files via Bash.\n' +
710
786
  'Example: mcp__moflo__memory_search { query: "<topic>", namespace: "<one of: guidance | code-map | patterns | learnings | tests>" }\n' +
711
787
  (hint ? hint + '\n' : '') +
712
788
  'On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n' +
713
- 'Disable per-gate via moflo.yaml: gates: memory_first: false\n'
789
+ GATE_ORIGIN_NOTE + '\n' +
790
+ GATE_DISABLE_NOTE + '\n'
714
791
  );
715
792
  process.exit(2);
716
793
  break;
@@ -981,6 +1058,33 @@ switch (command) {
981
1058
  applyPromptStateReset(s, prompt);
982
1059
  s.interactionCount = (s.interactionCount || 0) + 1;
983
1060
  writeState(s);
1061
+ // Announce the resolved /flo run modifiers. The gate already parsed
1062
+ // moflo.yaml in THIS process (fresh per prompt — a git pull or a mid-session
1063
+ // yaml edit is picked up automatically, no cache to invalidate), so this
1064
+ // costs no extra read. Emitting it is what closes the loop: the skill used
1065
+ // to carry `let sddMode = false` as a literal and the model executed it,
1066
+ // silently ignoring `sdd.default: true`. Only fires on /fl|/flo prompts.
1067
+ var floRun = resolveFloRun(prompt);
1068
+ if (floRun.isFlo) {
1069
+ console.log(
1070
+ '[moflo] /flo run modes (AUTHORITATIVE — use verbatim; do NOT re-derive from the skill defaults): ' +
1071
+ 'sdd=' + (floRun.sdd ? 'ON' : 'off') +
1072
+ ' verify=' + (floRun.verify ? 'ON' : 'off') +
1073
+ ' merge=' + (floRun.merge ? 'ON' : 'off') +
1074
+ ' [workflow=' + floRun.workflow + ']'
1075
+ );
1076
+ // Spell out the surprising case: a project default the user did not type.
1077
+ if (floRun.sdd && floRun.sddSrc !== 'flag') {
1078
+ console.log(
1079
+ '[moflo] sdd is ON via ' + floRun.sddSrc + ' — the spec→plan→implement→verify cycle is ' +
1080
+ 'MANDATORY this run. Author the spec before editing source (the sdd_gate blocks source ' +
1081
+ 'Write/Edit until a reviewed plan exists). One-off opt out: re-run with --no-sdd.'
1082
+ );
1083
+ }
1084
+ if (floRun.merge && floRun.mergeSrc !== 'flag') {
1085
+ console.log('[moflo] merge is ON via ' + floRun.mergeSrc + ' — the PR will be auto-merged. Opt out: --no-merge.');
1086
+ }
1087
+ }
984
1088
  if (config.context_tracking) {
985
1089
  var ic = s.interactionCount;
986
1090
  if (ic > 30) console.log('Context: CRITICAL. Commit, store learnings, suggest new session.');