moflo 4.11.10-rc.8 → 4.12.0

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.
@@ -68,6 +68,58 @@ export async function checkStatusLine() {
68
68
  return { name: 'Status Line', status: 'fail', message: 'Failed to parse .claude/settings.json', fix: 'Fix JSON syntax in .claude/settings.json' };
69
69
  }
70
70
  }
71
+ /**
72
+ * #1300 — the generated `.claude/settings.json` must auto-approve every moflo
73
+ * MCP tool via the bare `mcp__moflo` server prefix.
74
+ *
75
+ * Claude Code does NOT support wildcards in MCP permission rules. The bare
76
+ * `mcp__<server>` prefix is the only "all tools from this server" form;
77
+ * `mcp__server__tool` names a single tool. The settings-generator historically
78
+ * emitted `mcp__moflo__:*`, a wildcard attempt that matches no real tool name,
79
+ * so every `mcp__moflo__…` call fell through to a permission prompt in every
80
+ * consumer install (the generator is fixed going forward, but already-generated
81
+ * settings.json files still carry the stale rule until repaired).
82
+ *
83
+ * Detection is deliberately narrow — it fires ONLY on a malformed pattern-style
84
+ * rule (`mcp__moflo__` followed by a `:` or `*` wildcard), never on a valid
85
+ * exact-tool rule like `mcp__moflo__memory_store` (which a consumer may list
86
+ * intentionally). The companion auto-fix (`fixMcpToolPermissions` in
87
+ * doctor-fixes.ts) drops the malformed rule(s) and ensures the bare prefix.
88
+ *
89
+ * Status semantics:
90
+ * - pass — no `.claude/settings.json` (fresh fixture; nothing we own to
91
+ * assert), OR the file carries no malformed `mcp__moflo__*` pattern rule.
92
+ * - warn — a malformed `mcp__moflo__…` wildcard rule is present. Auto-fixable.
93
+ */
94
+ const MALFORMED_MOFLO_MCP_RULE = /^mcp__moflo__.*[:*]/;
95
+ export async function checkMcpToolPermissions(cwd = process.cwd()) {
96
+ const name = 'MCP Tool Permissions';
97
+ const settingsPath = join(cwd, '.claude', 'settings.json');
98
+ if (!existsSync(settingsPath)) {
99
+ return { name, status: 'pass', message: 'No .claude/settings.json (nothing to verify)' };
100
+ }
101
+ let allow;
102
+ try {
103
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
104
+ const raw = settings.permissions?.allow;
105
+ allow = Array.isArray(raw) ? raw.filter((r) => typeof r === 'string') : [];
106
+ }
107
+ catch {
108
+ // Malformed settings.json is owned by checkStatusLine — don't double-report.
109
+ return { name, status: 'pass', message: 'settings.json unreadable (reported by Status Line)' };
110
+ }
111
+ const malformed = allow.filter(rule => MALFORMED_MOFLO_MCP_RULE.test(rule));
112
+ if (malformed.length > 0) {
113
+ return {
114
+ name,
115
+ status: 'warn',
116
+ message: `Malformed moflo MCP permission rule (Claude Code has no MCP wildcards): ${malformed.join(', ')} — ` +
117
+ `matches no real tool name, so every moflo MCP call prompts. Use the bare \`mcp__moflo\` prefix.`,
118
+ fix: 'flo healer --fix -c mcp-permissions',
119
+ };
120
+ }
121
+ return { name, status: 'pass', message: 'moflo MCP tools auto-approved (no malformed rule)' };
122
+ }
71
123
  // Delegates to daemon-lock module for proper PID + command-line verification
72
124
  // (avoids Windows PID-recycling false positives).
73
125
  export async function checkDaemonStatus() {
@@ -7,31 +7,49 @@
7
7
  * (via the doctor registry) and, transitively, by `/eldar` (which renders the
8
8
  * healer's JSON).
9
9
  *
10
+ * #1301 — the check used to demand ALL three gate tokens whenever EITHER toggle
11
+ * was on. Because `gates.verify_before_done` defaults to `true`, a consumer who
12
+ * opted OUT of SDD (`sdd.default: false`) was still forced to carry the SDD
13
+ * implement-gate (`check-before-implement`) and failed spuriously. The required
14
+ * set is now decoupled per feature (see `computeSddVerifyRequirements`), and a
15
+ * LOCKED hook block (`moflo.hooks.locked: true`) — which moflo deliberately
16
+ * won't rewrite — is reported as a warn with actionable reconciliations rather
17
+ * than a permanently-red fail that `init --fix` can never clear.
18
+ *
10
19
  * Cross-platform (Rule #1): all paths via path.join; no shelling out.
11
20
  */
12
21
  import { existsSync, readFileSync } from 'node:fs';
13
22
  import { join } from 'node:path';
14
23
  import { findProjectRoot } from '../services/project-root.js';
15
24
  import { loadMofloConfig } from '../config/moflo-config.js';
25
+ import { isHookBlockLocked } from '../services/hook-block-hash.js';
16
26
  import { errorDetail } from '../shared/utils/error-detail.js';
17
27
  const NAME = 'SDD + Verify Wiring';
18
- /** The gate cases + hook subcommands that must be present for the feature to work. */
19
- const REQUIRED_GATE_CASES = ['check-before-done', 'record-verify-run', 'check-before-implement'];
20
- const REQUIRED_HOOK_TOKENS = ['check-before-done', 'record-verify-run', 'check-before-implement'];
21
- export async function checkSddVerifyWiring() {
22
- const projectDir = findProjectRoot();
28
+ /**
29
+ * The SDD implement-gate — required only when `sdd.default === true`. This is
30
+ * the gate that pauses `/flo` at the implement step for a spec/plan checkpoint.
31
+ */
32
+ const SDD_GATE_CASES = ['check-before-implement'];
33
+ const SDD_HOOK_TOKENS = ['check-before-implement'];
34
+ /**
35
+ * The verify-before-done gate — required only when
36
+ * `gates.verify_before_done === true`. `check-before-done` blocks `gh pr create`
37
+ * until a `/verify` run is recorded; `record-verify-run` is the PostToolUse
38
+ * half that records it.
39
+ */
40
+ const VERIFY_GATE_CASES = ['check-before-done', 'record-verify-run'];
41
+ const VERIFY_HOOK_TOKENS = ['check-before-done', 'record-verify-run'];
42
+ /**
43
+ * Single source of truth for what SDD/verify wiring the consumer's toggles
44
+ * require and what is actually present. Shared by the health check and the
45
+ * `--fix` handler so the fixer can honestly re-verify its own work (#1301 — the
46
+ * old fixer reported `applied: true` on a locked block that it never touched).
47
+ *
48
+ * Pure read-only inspection — no file writes.
49
+ */
50
+ export function computeSddVerifyRequirements(projectDir = findProjectRoot()) {
23
51
  const helperGate = join(projectDir, '.claude', 'helpers', 'gate.cjs');
24
52
  const settingsPath = join(projectDir, '.claude', 'settings.json');
25
- // Uninitialised project — defer to `flo init`, don't hard-fail.
26
- if (!existsSync(helperGate) && !existsSync(settingsPath)) {
27
- return {
28
- name: NAME,
29
- status: 'warn',
30
- message: '.claude/ not initialised — SDD/verify wiring absent',
31
- fix: 'npx moflo init',
32
- };
33
- }
34
- // Read the two opt-in toggles (best-effort; default false).
35
53
  let sddDefault = false;
36
54
  let verifyGate = false;
37
55
  try {
@@ -42,56 +60,132 @@ export async function checkSddVerifyWiring() {
42
60
  catch {
43
61
  /* config unreadable — treat as defaults (both off) */
44
62
  }
45
- const issues = [];
46
- // 1. Gate logic present in the installed helper gate.cjs.
63
+ // Decouple the required set per feature (#1301). SDD's implement-gate is
64
+ // required only under sdd.default; the verify hooks only under
65
+ // verify_before_done. A verify-only consumer no longer drags in the SDD gate.
66
+ const requiredHookTokens = [
67
+ ...(sddDefault ? SDD_HOOK_TOKENS : []),
68
+ ...(verifyGate ? VERIFY_HOOK_TOKENS : []),
69
+ ];
70
+ const requiredGateCases = [
71
+ ...(sddDefault ? SDD_GATE_CASES : []),
72
+ ...(verifyGate ? VERIFY_GATE_CASES : []),
73
+ ];
74
+ const uninitialised = !existsSync(helperGate) && !existsSync(settingsPath);
75
+ const structural = [];
76
+ const missingHooks = [];
77
+ const missingGateCases = [];
78
+ let locked = false;
79
+ // gate.cjs — the installed helper must carry the cases for enabled features.
47
80
  if (existsSync(helperGate)) {
48
81
  try {
49
82
  const gate = readFileSync(helperGate, 'utf8');
50
- const missing = REQUIRED_GATE_CASES.filter((c) => !gate.includes(`case '${c}'`));
51
- if (missing.length > 0)
52
- issues.push(`gate.cjs missing: ${missing.join(', ')}`);
83
+ for (const c of requiredGateCases) {
84
+ if (!gate.includes(`case '${c}'`))
85
+ missingGateCases.push(c);
86
+ }
53
87
  }
54
88
  catch (e) {
55
- issues.push(`cannot read gate.cjs: ${errorDetail(e)}`);
89
+ structural.push(`cannot read gate.cjs: ${errorDetail(e)}`);
56
90
  }
57
91
  }
58
- else {
59
- issues.push('.claude/helpers/gate.cjs not found');
92
+ else if (!uninitialised) {
93
+ structural.push('.claude/helpers/gate.cjs not found');
60
94
  }
61
- // 2. Hooks wired in settings.json.
95
+ // settings.json the hook wiring plus the lock sentinel.
62
96
  if (existsSync(settingsPath)) {
63
97
  try {
64
- const settings = readFileSync(settingsPath, 'utf8');
65
- const missing = REQUIRED_HOOK_TOKENS.filter((t) => !settings.includes(t));
66
- if (missing.length > 0)
67
- issues.push(`settings.json missing hooks: ${missing.join(', ')}`);
98
+ const raw = readFileSync(settingsPath, 'utf8');
99
+ for (const t of requiredHookTokens) {
100
+ if (!raw.includes(t))
101
+ missingHooks.push(t);
102
+ }
103
+ try {
104
+ locked = isHookBlockLocked(JSON.parse(raw));
105
+ }
106
+ catch {
107
+ /* unparseable JSON — leave locked=false; structural check below flags it */
108
+ structural.push('.claude/settings.json is not valid JSON');
109
+ }
68
110
  }
69
111
  catch (e) {
70
- issues.push(`cannot read settings.json: ${errorDetail(e)}`);
112
+ structural.push(`cannot read settings.json: ${errorDetail(e)}`);
71
113
  }
72
114
  }
73
- else {
74
- issues.push('.claude/settings.json not found');
115
+ else if (!uninitialised) {
116
+ structural.push('.claude/settings.json not found');
75
117
  }
76
- const toggles = `sdd.default=${sddDefault}, gates.verify_before_done=${verifyGate}`;
77
- if (issues.length > 0) {
78
- // Wiring is broken. Severity depends on whether the consumer opted in: a
79
- // missing gate when verify is ON blocks nothing (fail-open) but breaks the
80
- // promised enforcement, so it's a real fault. When both toggles are off the
81
- // wiring is still expected (it ships inert), but a missing case only bites
82
- // once someone opts in — warn.
83
- const enforced = verifyGate || sddDefault;
118
+ return {
119
+ sddDefault,
120
+ verifyGate,
121
+ locked,
122
+ enforced: sddDefault || verifyGate,
123
+ uninitialised,
124
+ requiredHookTokens,
125
+ requiredGateCases,
126
+ missingHooks,
127
+ missingGateCases,
128
+ structural,
129
+ };
130
+ }
131
+ export async function checkSddVerifyWiring() {
132
+ const req = computeSddVerifyRequirements(findProjectRoot());
133
+ // Uninitialised project — defer to `flo init`, don't hard-fail.
134
+ if (req.uninitialised) {
84
135
  return {
85
136
  name: NAME,
86
- status: enforced ? 'fail' : 'warn',
87
- message: `SDD/verify wiring incomplete (${toggles}): ${issues.join('; ')}`,
88
- fix: 'npx moflo init --fix # re-syncs gate.cjs + settings.json hooks',
137
+ status: 'warn',
138
+ message: '.claude/ not initialised — SDD/verify wiring absent',
139
+ fix: 'npx moflo init',
140
+ };
141
+ }
142
+ const toggles = `sdd.default=${req.sddDefault}, gates.verify_before_done=${req.verifyGate}`;
143
+ const issues = [...req.structural];
144
+ if (req.missingGateCases.length > 0)
145
+ issues.push(`gate.cjs missing: ${req.missingGateCases.join(', ')}`);
146
+ if (req.missingHooks.length > 0)
147
+ issues.push(`settings.json missing hooks: ${req.missingHooks.join(', ')}`);
148
+ if (issues.length === 0) {
149
+ return {
150
+ name: NAME,
151
+ status: 'pass',
152
+ message: `SDD/verify wired (${toggles})${req.verifyGate ? ' — verify-before-done ENFORCED' : ''}`,
153
+ };
154
+ }
155
+ // Locked hook block: moflo won't rewrite it (init + launcher both skip on
156
+ // `isHookBlockLocked`), so pointing at `init --fix` is a dead-end and a hard
157
+ // fail is a permanently-red check the healer can never clear (#1301). When the
158
+ // ONLY outstanding problem is missing hook tokens in that locked block,
159
+ // downgrade to warn and hand over the two real reconciliations. gate.cjs and
160
+ // structural faults are unaffected by the lock, so they still fail normally.
161
+ if (req.locked &&
162
+ req.missingHooks.length > 0 &&
163
+ req.missingGateCases.length === 0 &&
164
+ req.structural.length === 0) {
165
+ const optOut = req.sddDefault && !req.verifyGate
166
+ ? 'set sdd.default: false'
167
+ : req.verifyGate && !req.sddDefault
168
+ ? 'set gates.verify_before_done: false'
169
+ : 'disable the SDD/verify toggles';
170
+ return {
171
+ name: NAME,
172
+ status: 'warn',
173
+ message: `SDD/verify wiring incomplete (${toggles}): settings.json missing hooks: ` +
174
+ `${req.missingHooks.join(', ')} — hook block is LOCKED (moflo.hooks.locked=true), ` +
175
+ `so moflo will not wire it`,
176
+ fix: `Reconcile: ${optOut} in moflo.yaml to match the locked block, OR remove ` +
177
+ `moflo.hooks.locked from .claude/settings.json and run npx moflo init --fix`,
89
178
  };
90
179
  }
180
+ // A missing required token means a feature the consumer turned ON isn't
181
+ // enforced — a real fault. With both toggles off, `requiredHookTokens`/
182
+ // `requiredGateCases` are empty, so only structural faults can reach here;
183
+ // those warn (the wiring ships inert until someone opts in).
91
184
  return {
92
185
  name: NAME,
93
- status: 'pass',
94
- message: `SDD/verify wired (${toggles})${verifyGate ? ' — verify-before-done ENFORCED' : ''}`,
186
+ status: req.enforced ? 'fail' : 'warn',
187
+ message: `SDD/verify wiring incomplete (${toggles}): ${issues.join('; ')}`,
188
+ fix: 'npx moflo init --fix # re-syncs gate.cjs + settings.json hooks',
95
189
  };
96
190
  }
97
191
  //# sourceMappingURL=doctor-checks-sdd.js.map
@@ -639,6 +639,77 @@ export async function autoFixCheck(check) {
639
639
  'Gate Health': async () => {
640
640
  return fixGateHealthHooks();
641
641
  },
642
+ // #1301 — SDD/verify hook wiring. The generic fall-through used to run
643
+ // `npx moflo init --fix` and report `applied: true` on its exit code alone,
644
+ // even when the hook block was LOCKED and init injected nothing — a false
645
+ // positive over a permanently-red check. This handler:
646
+ // 1. refuses (returns false) when the block is locked — moflo won't
647
+ // rewrite it, so claiming a fix would be a lie;
648
+ // 2. grafts ONLY the missing SDD/verify reference entries additively
649
+ // (`check-before-implement` lives in the reference block but NOT in
650
+ // repairHookWiring's REQUIRED_HOOK_WIRING, so the generic repair could
651
+ // never add it); and
652
+ // 3. re-verifies from disk and returns the honest result.
653
+ 'SDD + Verify Wiring': async () => {
654
+ const projectDir = findProjectRoot();
655
+ const { computeSddVerifyRequirements } = await import('./doctor-checks-sdd.js');
656
+ const before = computeSddVerifyRequirements(projectDir);
657
+ // Uninitialised `.claude/` — the graft path below assumes settings.json +
658
+ // gate.cjs already exist, and every `missing*` array is empty when nothing
659
+ // is on disk. Run the check's own remediation (`npx moflo init`) instead,
660
+ // preserving the pre-#1301 generic-fallthrough behavior this named handler
661
+ // now intercepts.
662
+ if (before.uninitialised) {
663
+ return runFixCommand('npx moflo init');
664
+ }
665
+ if (before.missingHooks.length === 0 && before.missingGateCases.length === 0 && before.structural.length === 0) {
666
+ return true; // nothing outstanding
667
+ }
668
+ if (before.locked && before.missingHooks.length > 0) {
669
+ output.writeln(output.warning(' Hook block is LOCKED (moflo.hooks.locked=true) — moflo will not wire SDD/verify hooks. ' +
670
+ 'Set the matching toggle to false in moflo.yaml, or remove the lock and re-run.'));
671
+ // If the locked hooks are the only gap, there is genuinely nothing we
672
+ // can do — report honestly rather than a false success.
673
+ if (before.missingGateCases.length === 0 && before.structural.length === 0)
674
+ return false;
675
+ }
676
+ // settings.json hook wiring — additively graft the missing SDD/verify
677
+ // reference entries (skip when locked; the block above already reported it).
678
+ const settingsPath = join(projectDir, '.claude', 'settings.json');
679
+ if (!before.locked && before.missingHooks.length > 0 && existsSync(settingsPath)) {
680
+ try {
681
+ const { computeHookBlockDrift, applyAdditiveRegeneration } = await import('../services/hook-block-hash.js');
682
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
683
+ const report = computeHookBlockDrift((settings.hooks ?? {}));
684
+ // Scope the graft to the tokens this check owns — leave unrelated
685
+ // drift to the Hook Block Drift check/fixer.
686
+ const wanted = before.requiredHookTokens;
687
+ const scoped = {
688
+ ...report,
689
+ missing: report.missing.filter((m) => wanted.some((t) => m.command.includes(t))),
690
+ };
691
+ if (scoped.missing.length > 0) {
692
+ applyAdditiveRegeneration(settings, scoped);
693
+ // Atomic swap — settings.json is actively re-read by Claude Code, so
694
+ // a concurrent reader must never see a truncated file (#1015).
695
+ atomicWriteFileSync(settingsPath, JSON.stringify(settings, null, 2));
696
+ }
697
+ }
698
+ catch (e) {
699
+ output.writeln(output.warning(` settings.json hook repair failed: ${errorDetail(e)}`));
700
+ return false;
701
+ }
702
+ }
703
+ // gate.cjs case drift — reuse the gate-health repair (mirrors bin/helper).
704
+ if (before.missingGateCases.length > 0) {
705
+ await fixGateHealthHooks();
706
+ }
707
+ // Truth check — re-read from disk and confirm the required wiring landed.
708
+ const after = computeSddVerifyRequirements(projectDir);
709
+ return after.missingHooks.length === 0
710
+ && after.missingGateCases.length === 0
711
+ && after.structural.length === 0;
712
+ },
642
713
  // Refresh the consumer's CLAUDE.md MoFlo block in place using the
643
714
  // shared `applyInjectionReplacement` service. Idempotent: a re-run sees
644
715
  // `state === 'in-sync'` and the autoFix dispatcher skips this entry.
@@ -764,6 +835,35 @@ export async function autoFixCheck(check) {
764
835
  'Nested .moflo/ Islands': async () => {
765
836
  return fixNestedMofloIslands();
766
837
  },
838
+ // #1300 — rewrite a malformed moflo MCP permission rule in
839
+ // .claude/settings.json. Claude Code has no MCP wildcards, so the stale
840
+ // `mcp__moflo__:*` (and any `mcp__moflo__…[:*]` pattern) matched no tool and
841
+ // every moflo MCP call prompted. Drop the malformed rule(s), ensure the bare
842
+ // `mcp__moflo` prefix (approves all tools), and preserve any valid exact-tool
843
+ // rules the consumer added. Atomic swap so Claude Code re-reading settings
844
+ // mid-fix never sees a truncated file.
845
+ 'MCP Tool Permissions': async () => {
846
+ const settingsPath = join(process.cwd(), '.claude', 'settings.json');
847
+ if (!existsSync(settingsPath))
848
+ return false;
849
+ try {
850
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
851
+ const perms = (settings.permissions ??= {});
852
+ const rawAllow = Array.isArray(perms.allow) ? perms.allow : [];
853
+ const malformed = /^mcp__moflo__.*[:*]/;
854
+ const cleaned = rawAllow.filter((r) => typeof r === 'string' && !malformed.test(r));
855
+ if (!cleaned.includes('mcp__moflo'))
856
+ cleaned.push('mcp__moflo');
857
+ perms.allow = cleaned;
858
+ atomicWriteFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
859
+ output.writeln(output.dim(' Rewrote moflo MCP permission to the bare `mcp__moflo` prefix.'));
860
+ return true;
861
+ }
862
+ catch (e) {
863
+ output.writeln(output.warning(` MCP permission repair failed: ${errorDetail(e)}`));
864
+ return false;
865
+ }
866
+ },
767
867
  'Status Line': async () => {
768
868
  const settingsPath = join(process.cwd(), '.claude', 'settings.json');
769
869
  if (!existsSync(settingsPath))
@@ -13,7 +13,7 @@ import { checkSharedFullDb } from './doctor-checks-shared-db.js';
13
13
  import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
14
14
  import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
15
15
  import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
16
- import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMemoryFirstGate, checkMofloYamlCompliance, checkNestedMofloIslands, checkStatusLine, checkSwarmResidue, checkTestDirs, } from './doctor-checks-config.js';
16
+ import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMcpToolPermissions, checkMemoryDatabase, checkMemoryDbIntegrity, checkMemoryFirstGate, checkMofloYamlCompliance, checkNestedMofloIslands, checkStatusLine, checkSwarmResidue, checkTestDirs, } from './doctor-checks-config.js';
17
17
  import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
18
18
  import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
19
19
  import { checkIntelligence } from './doctor-checks-intelligence.js';
@@ -39,6 +39,10 @@ export const allChecks = [
39
39
  // #1229 — loud tripwire for a silently-disabled memory_first gate.
40
40
  checkMemoryFirstGate,
41
41
  checkStatusLine,
42
+ // #1300 — standing detector for a malformed moflo MCP permission rule in
43
+ // .claude/settings.json (the `mcp__moflo__:*` wildcard that Claude Code can't
44
+ // match, so every moflo MCP call prompts). Cheap: one settings.json read.
45
+ checkMcpToolPermissions,
42
46
  checkDaemonStatus,
43
47
  checkDaemonVersionSkew,
44
48
  checkDaemonIdentity,
@@ -146,6 +150,9 @@ export const componentMap = {
146
150
  'hygiene': checkEmbeddingHygiene,
147
151
  'git': checkGit,
148
152
  'mcp': checkMcpServers,
153
+ 'mcp-permissions': checkMcpToolPermissions,
154
+ 'mcp-perms': checkMcpToolPermissions,
155
+ 'mcp-tool-permissions': checkMcpToolPermissions,
149
156
  'disk': checkDiskSpace,
150
157
  'typescript': checkBuildTools,
151
158
  'tests': checkTestDirs,
@@ -26,7 +26,11 @@ export function generateSettings(options) {
26
26
  'MultiEdit(*)',
27
27
  'Glob(*)',
28
28
  'Grep(*)',
29
- 'mcp__moflo__:*',
29
+ // Approve every moflo MCP tool. Claude Code does NOT support wildcards in
30
+ // MCP permission rules — the bare `mcp__<server>` prefix is the canonical
31
+ // "all tools from this server" form. The prior `mcp__moflo__:*` matched no
32
+ // real tool name, so every mcp__moflo__ call fell through to a prompt.
33
+ 'mcp__moflo',
30
34
  ],
31
35
  deny: [
32
36
  'Read(./.env)',
@@ -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.11.10-rc.8';
5
+ export const VERSION = '4.12.0';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.11.10-rc.8",
3
+ "version": "4.12.0",
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",
@@ -95,7 +95,7 @@
95
95
  "@typescript-eslint/eslint-plugin": "^7.18.0",
96
96
  "@typescript-eslint/parser": "^7.18.0",
97
97
  "eslint": "^8.0.0",
98
- "moflo": "^4.11.10-rc.7",
98
+ "moflo": "^4.11.10-rc.10",
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3",
101
101
  "vitest": "^4.0.0"