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.
- package/dist/src/cli/commands/doctor-checks-config.js +52 -0
- package/dist/src/cli/commands/doctor-checks-sdd.js +138 -44
- package/dist/src/cli/commands/doctor-fixes.js +100 -0
- package/dist/src/cli/commands/doctor-registry.js +8 -1
- package/dist/src/cli/init/settings-generator.js +5 -1
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -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
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
46
|
-
//
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
83
|
+
for (const c of requiredGateCases) {
|
|
84
|
+
if (!gate.includes(`case '${c}'`))
|
|
85
|
+
missingGateCases.push(c);
|
|
86
|
+
}
|
|
53
87
|
}
|
|
54
88
|
catch (e) {
|
|
55
|
-
|
|
89
|
+
structural.push(`cannot read gate.cjs: ${errorDetail(e)}`);
|
|
56
90
|
}
|
|
57
91
|
}
|
|
58
|
-
else {
|
|
59
|
-
|
|
92
|
+
else if (!uninitialised) {
|
|
93
|
+
structural.push('.claude/helpers/gate.cjs not found');
|
|
60
94
|
}
|
|
61
|
-
//
|
|
95
|
+
// settings.json — the hook wiring plus the lock sentinel.
|
|
62
96
|
if (existsSync(settingsPath)) {
|
|
63
97
|
try {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
112
|
+
structural.push(`cannot read settings.json: ${errorDetail(e)}`);
|
|
71
113
|
}
|
|
72
114
|
}
|
|
73
|
-
else {
|
|
74
|
-
|
|
115
|
+
else if (!uninitialised) {
|
|
116
|
+
structural.push('.claude/settings.json not found');
|
|
75
117
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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:
|
|
87
|
-
message:
|
|
88
|
-
fix: 'npx moflo init
|
|
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: '
|
|
94
|
-
message: `SDD/verify
|
|
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
|
-
|
|
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)',
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
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"
|