moflo 4.11.10-rc.9 → 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.
|
@@ -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.
|
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"
|