moflo 4.11.10-rc.9 → 4.12.1
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/.claude/helpers/gate.cjs +105 -9
- package/.claude/skills/fl/SKILL.md +35 -14
- package/.claude/skills/fl/sdd.md +4 -2
- package/bin/gate.cjs +105 -9
- package/dist/src/cli/commands/doctor-checks-sdd.js +138 -44
- package/dist/src/cli/commands/doctor-fixes.js +71 -0
- package/dist/src/cli/commands/sdd.js +119 -1
- package/dist/src/cli/init/helpers-generator.js +88 -6
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
package/.claude/helpers/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,6 +129,7 @@ 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'];
|
|
@@ -288,16 +302,71 @@ function detectFlMode(promptText) {
|
|
|
288
302
|
return null;
|
|
289
303
|
}
|
|
290
304
|
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
305
|
+
// Resolve the FULL set of /flo run modifiers from the prompt + moflo.yaml — the
|
|
306
|
+
// single source of truth for both gate arming (#1297) and the authoritative
|
|
307
|
+
// announcement emitted by prompt-reminder. Two consumers, one resolver: a second
|
|
308
|
+
// implementation is exactly how `sdd.default: true` got silently ignored (the
|
|
309
|
+
// skill's prompt carried `let sddMode = false` and the model executed the literal).
|
|
310
|
+
//
|
|
311
|
+
// Precedence per key: explicit --no-X > explicit -x/--X > moflo.yaml > built-in.
|
|
312
|
+
// NOTE the differing built-in defaults: sdd is opt-IN (false), verify is opt-OUT
|
|
313
|
+
// (true, #1294), merge is opt-IN (false, #1285).
|
|
314
|
+
//
|
|
315
|
+
// `-sd` is a distinct token from `-s` (swarm): the `d` sits on the word boundary
|
|
316
|
+
// so `-s\b` never matches `-sd`.
|
|
317
|
+
function resolveFloRun(promptText) {
|
|
296
318
|
var p = promptText || '';
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (
|
|
300
|
-
|
|
319
|
+
var out = { isFlo: false, workflow: 'full', sdd: false, verify: false, merge: false,
|
|
320
|
+
sddSrc: 'default', verifySrc: 'default', mergeSrc: 'default' };
|
|
321
|
+
if (!/^\s*\/(?:fl|flo)\b/i.test(p)) return out;
|
|
322
|
+
out.isFlo = true;
|
|
323
|
+
|
|
324
|
+
// Workflow mode — decides which modifiers are even applicable.
|
|
325
|
+
if (/(?:^|\s)(?:-wf|--workflow)\b/.test(p)) out.workflow = 'spell-engine';
|
|
326
|
+
else if (/(?:^|\s)(?:-r|--research)\b/.test(p)) out.workflow = 'research';
|
|
327
|
+
else if (/(?:^|\s)(?:-t|--ticket)\b/.test(p)) out.workflow = 'ticket';
|
|
328
|
+
var epicBranch = /(?:^|\s)--epic-branch\b/.test(p);
|
|
329
|
+
|
|
330
|
+
if (/(?:^|\s)--no-sdd\b/.test(p)) { out.sdd = false; out.sddSrc = 'flag'; }
|
|
331
|
+
else if (/(?:^|\s)(?:-sd|--sdd)\b/.test(p)) { out.sdd = true; out.sddSrc = 'flag'; }
|
|
332
|
+
else if (sddConf.default) { out.sdd = true; out.sddSrc = 'moflo.yaml sdd.default'; }
|
|
333
|
+
|
|
334
|
+
if (/(?:^|\s)--no-verify\b/.test(p)) { out.verify = false; out.verifySrc = 'flag'; }
|
|
335
|
+
else if (/(?:^|\s)(?:-v|--verify)\b/.test(p)) { out.verify = true; out.verifySrc = 'flag'; }
|
|
336
|
+
else if (!config.verify_before_done) { out.verify = false; out.verifySrc = 'moflo.yaml gates.verify_before_done'; }
|
|
337
|
+
else { out.verify = true; out.verifySrc = 'default'; }
|
|
338
|
+
// --sdd implies --verify: a spec/plan without an enforced verify step drifts.
|
|
339
|
+
if (out.sdd && !out.verify && out.verifySrc !== 'flag') out.verify = true;
|
|
340
|
+
|
|
341
|
+
if (/(?:^|\s)--no-merge\b/.test(p)) { out.merge = false; out.mergeSrc = 'flag'; }
|
|
342
|
+
else if (/(?:^|\s)(?:-m|--merge)\b/.test(p)) { out.merge = true; out.mergeSrc = 'flag'; }
|
|
343
|
+
else if (mergeConf.auto) { out.merge = true; out.mergeSrc = 'moflo.yaml merge.auto'; }
|
|
344
|
+
|
|
345
|
+
// Applicability: -t/-r never implement, so verify is a no-op there; -r produces
|
|
346
|
+
// no artifacts, so sdd is a no-op too (in -t the spec/plan goes INTO the ticket,
|
|
347
|
+
// so sdd stays on). Only a full non-epic-branch run opens a PR to merge.
|
|
348
|
+
// Re-attribute anything applicability turned off, so a false never carries the
|
|
349
|
+
// source of the value it no longer has. This whole change exists to stop modes
|
|
350
|
+
// being reported inaccurately — the attribution has to be honest too.
|
|
351
|
+
if (out.workflow === 'ticket' || out.workflow === 'research') {
|
|
352
|
+
if (out.verify) out.verifySrc = out.workflow + ' mode does not implement';
|
|
353
|
+
out.verify = false;
|
|
354
|
+
}
|
|
355
|
+
if (out.workflow === 'research' || out.workflow === 'spell-engine') {
|
|
356
|
+
if (out.sdd) out.sddSrc = out.workflow + ' mode produces no spec artifacts';
|
|
357
|
+
out.sdd = false;
|
|
358
|
+
}
|
|
359
|
+
if (out.workflow !== 'full' || epicBranch) {
|
|
360
|
+
if (out.merge) out.mergeSrc = epicBranch ? '--epic-branch owns merging' : out.workflow + ' mode opens no PR';
|
|
361
|
+
out.merge = false;
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// #1297 — arm the SDD implement gate from the user prompt. Thin wrapper so the
|
|
367
|
+
// arming decision and the announced decision can never disagree.
|
|
368
|
+
function detectSddMode(promptText) {
|
|
369
|
+
return resolveFloRun(promptText).sdd;
|
|
301
370
|
}
|
|
302
371
|
|
|
303
372
|
// Resolve the absolute specs root the same way TS specsRoot does (#1294): split
|
|
@@ -981,6 +1050,33 @@ switch (command) {
|
|
|
981
1050
|
applyPromptStateReset(s, prompt);
|
|
982
1051
|
s.interactionCount = (s.interactionCount || 0) + 1;
|
|
983
1052
|
writeState(s);
|
|
1053
|
+
// Announce the resolved /flo run modifiers. The gate already parsed
|
|
1054
|
+
// moflo.yaml in THIS process (fresh per prompt — a git pull or a mid-session
|
|
1055
|
+
// yaml edit is picked up automatically, no cache to invalidate), so this
|
|
1056
|
+
// costs no extra read. Emitting it is what closes the loop: the skill used
|
|
1057
|
+
// to carry `let sddMode = false` as a literal and the model executed it,
|
|
1058
|
+
// silently ignoring `sdd.default: true`. Only fires on /fl|/flo prompts.
|
|
1059
|
+
var floRun = resolveFloRun(prompt);
|
|
1060
|
+
if (floRun.isFlo) {
|
|
1061
|
+
console.log(
|
|
1062
|
+
'[moflo] /flo run modes (AUTHORITATIVE — use verbatim; do NOT re-derive from the skill defaults): ' +
|
|
1063
|
+
'sdd=' + (floRun.sdd ? 'ON' : 'off') +
|
|
1064
|
+
' verify=' + (floRun.verify ? 'ON' : 'off') +
|
|
1065
|
+
' merge=' + (floRun.merge ? 'ON' : 'off') +
|
|
1066
|
+
' [workflow=' + floRun.workflow + ']'
|
|
1067
|
+
);
|
|
1068
|
+
// Spell out the surprising case: a project default the user did not type.
|
|
1069
|
+
if (floRun.sdd && floRun.sddSrc !== 'flag') {
|
|
1070
|
+
console.log(
|
|
1071
|
+
'[moflo] sdd is ON via ' + floRun.sddSrc + ' — the spec→plan→implement→verify cycle is ' +
|
|
1072
|
+
'MANDATORY this run. Author the spec before editing source (the sdd_gate blocks source ' +
|
|
1073
|
+
'Write/Edit until a reviewed plan exists). One-off opt out: re-run with --no-sdd.'
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
if (floRun.merge && floRun.mergeSrc !== 'flag') {
|
|
1077
|
+
console.log('[moflo] merge is ON via ' + floRun.mergeSrc + ' — the PR will be auto-merged. Opt out: --no-merge.');
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
984
1080
|
if (config.context_tracking) {
|
|
985
1081
|
var ic = s.interactionCount;
|
|
986
1082
|
if (ic > 30) console.log('Context: CRITICAL. Commit, store learnings, suggest new session.');
|
|
@@ -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` (
|
|
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)
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
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
|
|
package/.claude/skills/fl/sdd.md
CHANGED
|
@@ -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`
|
|
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
|
-
|
|
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
|
|
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,6 +129,7 @@ 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'];
|
|
@@ -288,16 +302,71 @@ function detectFlMode(promptText) {
|
|
|
288
302
|
return null;
|
|
289
303
|
}
|
|
290
304
|
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
305
|
+
// Resolve the FULL set of /flo run modifiers from the prompt + moflo.yaml — the
|
|
306
|
+
// single source of truth for both gate arming (#1297) and the authoritative
|
|
307
|
+
// announcement emitted by prompt-reminder. Two consumers, one resolver: a second
|
|
308
|
+
// implementation is exactly how `sdd.default: true` got silently ignored (the
|
|
309
|
+
// skill's prompt carried `let sddMode = false` and the model executed the literal).
|
|
310
|
+
//
|
|
311
|
+
// Precedence per key: explicit --no-X > explicit -x/--X > moflo.yaml > built-in.
|
|
312
|
+
// NOTE the differing built-in defaults: sdd is opt-IN (false), verify is opt-OUT
|
|
313
|
+
// (true, #1294), merge is opt-IN (false, #1285).
|
|
314
|
+
//
|
|
315
|
+
// `-sd` is a distinct token from `-s` (swarm): the `d` sits on the word boundary
|
|
316
|
+
// so `-s\b` never matches `-sd`.
|
|
317
|
+
function resolveFloRun(promptText) {
|
|
296
318
|
var p = promptText || '';
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (
|
|
300
|
-
|
|
319
|
+
var out = { isFlo: false, workflow: 'full', sdd: false, verify: false, merge: false,
|
|
320
|
+
sddSrc: 'default', verifySrc: 'default', mergeSrc: 'default' };
|
|
321
|
+
if (!/^\s*\/(?:fl|flo)\b/i.test(p)) return out;
|
|
322
|
+
out.isFlo = true;
|
|
323
|
+
|
|
324
|
+
// Workflow mode — decides which modifiers are even applicable.
|
|
325
|
+
if (/(?:^|\s)(?:-wf|--workflow)\b/.test(p)) out.workflow = 'spell-engine';
|
|
326
|
+
else if (/(?:^|\s)(?:-r|--research)\b/.test(p)) out.workflow = 'research';
|
|
327
|
+
else if (/(?:^|\s)(?:-t|--ticket)\b/.test(p)) out.workflow = 'ticket';
|
|
328
|
+
var epicBranch = /(?:^|\s)--epic-branch\b/.test(p);
|
|
329
|
+
|
|
330
|
+
if (/(?:^|\s)--no-sdd\b/.test(p)) { out.sdd = false; out.sddSrc = 'flag'; }
|
|
331
|
+
else if (/(?:^|\s)(?:-sd|--sdd)\b/.test(p)) { out.sdd = true; out.sddSrc = 'flag'; }
|
|
332
|
+
else if (sddConf.default) { out.sdd = true; out.sddSrc = 'moflo.yaml sdd.default'; }
|
|
333
|
+
|
|
334
|
+
if (/(?:^|\s)--no-verify\b/.test(p)) { out.verify = false; out.verifySrc = 'flag'; }
|
|
335
|
+
else if (/(?:^|\s)(?:-v|--verify)\b/.test(p)) { out.verify = true; out.verifySrc = 'flag'; }
|
|
336
|
+
else if (!config.verify_before_done) { out.verify = false; out.verifySrc = 'moflo.yaml gates.verify_before_done'; }
|
|
337
|
+
else { out.verify = true; out.verifySrc = 'default'; }
|
|
338
|
+
// --sdd implies --verify: a spec/plan without an enforced verify step drifts.
|
|
339
|
+
if (out.sdd && !out.verify && out.verifySrc !== 'flag') out.verify = true;
|
|
340
|
+
|
|
341
|
+
if (/(?:^|\s)--no-merge\b/.test(p)) { out.merge = false; out.mergeSrc = 'flag'; }
|
|
342
|
+
else if (/(?:^|\s)(?:-m|--merge)\b/.test(p)) { out.merge = true; out.mergeSrc = 'flag'; }
|
|
343
|
+
else if (mergeConf.auto) { out.merge = true; out.mergeSrc = 'moflo.yaml merge.auto'; }
|
|
344
|
+
|
|
345
|
+
// Applicability: -t/-r never implement, so verify is a no-op there; -r produces
|
|
346
|
+
// no artifacts, so sdd is a no-op too (in -t the spec/plan goes INTO the ticket,
|
|
347
|
+
// so sdd stays on). Only a full non-epic-branch run opens a PR to merge.
|
|
348
|
+
// Re-attribute anything applicability turned off, so a false never carries the
|
|
349
|
+
// source of the value it no longer has. This whole change exists to stop modes
|
|
350
|
+
// being reported inaccurately — the attribution has to be honest too.
|
|
351
|
+
if (out.workflow === 'ticket' || out.workflow === 'research') {
|
|
352
|
+
if (out.verify) out.verifySrc = out.workflow + ' mode does not implement';
|
|
353
|
+
out.verify = false;
|
|
354
|
+
}
|
|
355
|
+
if (out.workflow === 'research' || out.workflow === 'spell-engine') {
|
|
356
|
+
if (out.sdd) out.sddSrc = out.workflow + ' mode produces no spec artifacts';
|
|
357
|
+
out.sdd = false;
|
|
358
|
+
}
|
|
359
|
+
if (out.workflow !== 'full' || epicBranch) {
|
|
360
|
+
if (out.merge) out.mergeSrc = epicBranch ? '--epic-branch owns merging' : out.workflow + ' mode opens no PR';
|
|
361
|
+
out.merge = false;
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// #1297 — arm the SDD implement gate from the user prompt. Thin wrapper so the
|
|
367
|
+
// arming decision and the announced decision can never disagree.
|
|
368
|
+
function detectSddMode(promptText) {
|
|
369
|
+
return resolveFloRun(promptText).sdd;
|
|
301
370
|
}
|
|
302
371
|
|
|
303
372
|
// Resolve the absolute specs root the same way TS specsRoot does (#1294): split
|
|
@@ -981,6 +1050,33 @@ switch (command) {
|
|
|
981
1050
|
applyPromptStateReset(s, prompt);
|
|
982
1051
|
s.interactionCount = (s.interactionCount || 0) + 1;
|
|
983
1052
|
writeState(s);
|
|
1053
|
+
// Announce the resolved /flo run modifiers. The gate already parsed
|
|
1054
|
+
// moflo.yaml in THIS process (fresh per prompt — a git pull or a mid-session
|
|
1055
|
+
// yaml edit is picked up automatically, no cache to invalidate), so this
|
|
1056
|
+
// costs no extra read. Emitting it is what closes the loop: the skill used
|
|
1057
|
+
// to carry `let sddMode = false` as a literal and the model executed it,
|
|
1058
|
+
// silently ignoring `sdd.default: true`. Only fires on /fl|/flo prompts.
|
|
1059
|
+
var floRun = resolveFloRun(prompt);
|
|
1060
|
+
if (floRun.isFlo) {
|
|
1061
|
+
console.log(
|
|
1062
|
+
'[moflo] /flo run modes (AUTHORITATIVE — use verbatim; do NOT re-derive from the skill defaults): ' +
|
|
1063
|
+
'sdd=' + (floRun.sdd ? 'ON' : 'off') +
|
|
1064
|
+
' verify=' + (floRun.verify ? 'ON' : 'off') +
|
|
1065
|
+
' merge=' + (floRun.merge ? 'ON' : 'off') +
|
|
1066
|
+
' [workflow=' + floRun.workflow + ']'
|
|
1067
|
+
);
|
|
1068
|
+
// Spell out the surprising case: a project default the user did not type.
|
|
1069
|
+
if (floRun.sdd && floRun.sddSrc !== 'flag') {
|
|
1070
|
+
console.log(
|
|
1071
|
+
'[moflo] sdd is ON via ' + floRun.sddSrc + ' — the spec→plan→implement→verify cycle is ' +
|
|
1072
|
+
'MANDATORY this run. Author the spec before editing source (the sdd_gate blocks source ' +
|
|
1073
|
+
'Write/Edit until a reviewed plan exists). One-off opt out: re-run with --no-sdd.'
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
if (floRun.merge && floRun.mergeSrc !== 'flag') {
|
|
1077
|
+
console.log('[moflo] merge is ON via ' + floRun.mergeSrc + ' — the PR will be auto-merged. Opt out: --no-merge.');
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
984
1080
|
if (config.context_tracking) {
|
|
985
1081
|
var ic = s.interactionCount;
|
|
986
1082
|
if (ic > 30) console.log('Context: CRITICAL. Commit, store learnings, suggest new session.');
|
|
@@ -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.
|
|
@@ -16,12 +16,14 @@
|
|
|
16
16
|
* flo sdd path <slug> [spec|plan] Print the artifact path (for skill shell-out)
|
|
17
17
|
* flo sdd embed <slug> Print spec+plan as a PR-body block (#1297)
|
|
18
18
|
* flo sdd index Re-index specs into memory now (also runs at session start)
|
|
19
|
+
* flo sdd mode --args "<flo args>" Resolve sdd/verify/merge for a /flo run as JSON
|
|
19
20
|
*/
|
|
20
21
|
import { spawnSync } from 'node:child_process';
|
|
21
22
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
22
23
|
import { dirname, join } from 'node:path';
|
|
23
24
|
import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
|
|
24
25
|
import { findProjectRoot } from '../services/project-root.js';
|
|
26
|
+
import { loadMofloConfig } from '../config/moflo-config.js';
|
|
25
27
|
import { locateMofloRootPath } from '../services/moflo-require.js';
|
|
26
28
|
import { artifactPath, assertReviewed, listSpecs, newArtifact, readArtifact, slugify, specExists, validateArtifact, writeArtifact, } from '../sdd/index.js';
|
|
27
29
|
import { defaultPlanBody, defaultSpecBody } from '../sdd/templates.js';
|
|
@@ -226,6 +228,119 @@ function cmdList(ctx) {
|
|
|
226
228
|
}
|
|
227
229
|
return { success: true, data: specs };
|
|
228
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Resolve the effective `/flo` run modifiers from an argument string + moflo.yaml
|
|
233
|
+
* and print them as JSON. This is the FALLBACK path for the authoritative
|
|
234
|
+
* announcement that `bin/gate.cjs prompt-reminder` normally injects on every
|
|
235
|
+
* `/flo` prompt — consumers with hooks disabled (or a gate.cjs predating this
|
|
236
|
+
* change) still get a deterministic answer by shelling out to this.
|
|
237
|
+
*
|
|
238
|
+
* Exists because the `/flo` skill used to carry `let sddMode = false` as a
|
|
239
|
+
* literal beside a comment saying "seed from moflo.yaml"; the model executed the
|
|
240
|
+
* literal and `sdd.default: true` was silently ignored. Config-derived run modes
|
|
241
|
+
* must be COMPUTED, never described in prose next to a contradicting default.
|
|
242
|
+
*
|
|
243
|
+
* Precedence per key: `--no-X` > `-x`/`--X` > moflo.yaml > built-in. Built-in
|
|
244
|
+
* defaults differ deliberately: sdd is opt-in (false), verify is opt-out (true,
|
|
245
|
+
* #1294), merge is opt-in (false, #1285).
|
|
246
|
+
*
|
|
247
|
+
* SYNC: mirrors `resolveFloRun` in bin/gate.cjs (and its copy in
|
|
248
|
+
* init/helpers-generator.ts). Any precedence change MUST land in all three.
|
|
249
|
+
*/
|
|
250
|
+
function cmdMode(ctx) {
|
|
251
|
+
// MUST be passed as `--args=<value>`, not `--args <value>`: the flag parser
|
|
252
|
+
// only consumes a following token as a value when it does not start with `-`,
|
|
253
|
+
// so `--args "-sd 42"` would silently degrade to `args=true` and resolve every
|
|
254
|
+
// run to the bare config default. The `=` form bypasses that lookahead.
|
|
255
|
+
// parseValue may coerce a bare numeric ("42") to a number — coerce back.
|
|
256
|
+
const flagArgs = ctx.flags?.args;
|
|
257
|
+
const raw = (typeof flagArgs === 'string' || typeof flagArgs === 'number')
|
|
258
|
+
? String(flagArgs)
|
|
259
|
+
: (ctx.args ?? []).slice(1).join(' ');
|
|
260
|
+
// Tokenize on whitespace so `-sd` is matched as a whole token — it is NOT
|
|
261
|
+
// `-s` (swarm) + `d`.
|
|
262
|
+
const tokens = raw.trim().split(/\s+/).filter(Boolean);
|
|
263
|
+
const has = (...names) => names.some((n) => tokens.includes(n));
|
|
264
|
+
const cfg = loadMofloConfig(projectRoot(ctx));
|
|
265
|
+
let workflow = 'full';
|
|
266
|
+
if (has('-wf', '--workflow'))
|
|
267
|
+
workflow = 'spell-engine';
|
|
268
|
+
else if (has('-r', '--research'))
|
|
269
|
+
workflow = 'research';
|
|
270
|
+
else if (has('-t', '--ticket'))
|
|
271
|
+
workflow = 'ticket';
|
|
272
|
+
const epicBranch = has('--epic-branch');
|
|
273
|
+
let sdd = false;
|
|
274
|
+
let sddSrc = 'default';
|
|
275
|
+
if (has('--no-sdd')) {
|
|
276
|
+
sdd = false;
|
|
277
|
+
sddSrc = 'flag';
|
|
278
|
+
}
|
|
279
|
+
else if (has('-sd', '--sdd')) {
|
|
280
|
+
sdd = true;
|
|
281
|
+
sddSrc = 'flag';
|
|
282
|
+
}
|
|
283
|
+
else if (cfg.sdd.default) {
|
|
284
|
+
sdd = true;
|
|
285
|
+
sddSrc = 'moflo.yaml sdd.default';
|
|
286
|
+
}
|
|
287
|
+
let verify;
|
|
288
|
+
let verifySrc = 'default';
|
|
289
|
+
if (has('--no-verify')) {
|
|
290
|
+
verify = false;
|
|
291
|
+
verifySrc = 'flag';
|
|
292
|
+
}
|
|
293
|
+
else if (has('-v', '--verify')) {
|
|
294
|
+
verify = true;
|
|
295
|
+
verifySrc = 'flag';
|
|
296
|
+
}
|
|
297
|
+
else if (!cfg.gates.verify_before_done) {
|
|
298
|
+
verify = false;
|
|
299
|
+
verifySrc = 'moflo.yaml gates.verify_before_done';
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
verify = true;
|
|
303
|
+
}
|
|
304
|
+
// --sdd implies --verify: a spec/plan without an enforced verify step drifts.
|
|
305
|
+
if (sdd && !verify && verifySrc !== 'flag')
|
|
306
|
+
verify = true;
|
|
307
|
+
let merge = false;
|
|
308
|
+
let mergeSrc = 'default';
|
|
309
|
+
if (has('--no-merge')) {
|
|
310
|
+
merge = false;
|
|
311
|
+
mergeSrc = 'flag';
|
|
312
|
+
}
|
|
313
|
+
else if (has('-m', '--merge')) {
|
|
314
|
+
merge = true;
|
|
315
|
+
mergeSrc = 'flag';
|
|
316
|
+
}
|
|
317
|
+
else if (cfg.merge.auto) {
|
|
318
|
+
merge = true;
|
|
319
|
+
mergeSrc = 'moflo.yaml merge.auto';
|
|
320
|
+
}
|
|
321
|
+
// Applicability: -t/-r never implement, so verify is a no-op there; -r and -wf
|
|
322
|
+
// produce no spec artifacts (in -t the spec/plan goes INTO the ticket, so sdd
|
|
323
|
+
// stays on). Only a full, non-epic-branch run opens a PR to merge.
|
|
324
|
+
// Re-attribute anything applicability turned off — a false must never carry the
|
|
325
|
+
// source of the value it no longer has. SYNC: mirrors bin/gate.cjs.
|
|
326
|
+
if (workflow === 'ticket' || workflow === 'research') {
|
|
327
|
+
if (verify)
|
|
328
|
+
verifySrc = `${workflow} mode does not implement`;
|
|
329
|
+
verify = false;
|
|
330
|
+
}
|
|
331
|
+
if (workflow === 'research' || workflow === 'spell-engine') {
|
|
332
|
+
if (sdd)
|
|
333
|
+
sddSrc = `${workflow} mode produces no spec artifacts`;
|
|
334
|
+
sdd = false;
|
|
335
|
+
}
|
|
336
|
+
if (workflow !== 'full' || epicBranch) {
|
|
337
|
+
if (merge)
|
|
338
|
+
mergeSrc = epicBranch ? '--epic-branch owns merging' : `${workflow} mode opens no PR`;
|
|
339
|
+
merge = false;
|
|
340
|
+
}
|
|
341
|
+
console.log(JSON.stringify({ workflow, sdd, verify, merge, sddSrc, verifySrc, mergeSrc }, null, 2));
|
|
342
|
+
return { success: true };
|
|
343
|
+
}
|
|
229
344
|
function cmdPath(ctx) {
|
|
230
345
|
const root = projectRoot(ctx);
|
|
231
346
|
const slug = slugify(ctx.args[1] || '');
|
|
@@ -294,7 +409,8 @@ Spec-Driven Development artifacts (.moflo/specs/<slug>/{spec,plan}.md):
|
|
|
294
409
|
list List every spec slug
|
|
295
410
|
path <slug> [spec|plan] Print the artifact path
|
|
296
411
|
embed <slug> Print spec+plan as a collapsible block for the PR body
|
|
297
|
-
index Re-index specs into memory now
|
|
412
|
+
index Re-index specs into memory now
|
|
413
|
+
mode --args "<flo args>" Resolve sdd/verify/merge for a /flo run as JSON (moflo.yaml + flags)`;
|
|
298
414
|
const sddCommand = {
|
|
299
415
|
name: 'sdd',
|
|
300
416
|
description: 'Spec-Driven Development artifacts (spec → plan → implement → verify)',
|
|
@@ -322,6 +438,8 @@ const sddCommand = {
|
|
|
322
438
|
return cmdStatus(ctx);
|
|
323
439
|
case 'list':
|
|
324
440
|
return cmdList(ctx);
|
|
441
|
+
case 'mode':
|
|
442
|
+
return cmdMode(ctx);
|
|
325
443
|
case 'path':
|
|
326
444
|
return cmdPath(ctx);
|
|
327
445
|
case 'embed':
|
|
@@ -293,6 +293,18 @@ function loadSddConfig() {
|
|
|
293
293
|
return out;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
// #1285 — parse the top-level merge: block. Block-scoped like loadSddConfig.
|
|
297
|
+
// SYNC: mirrors bin/gate.cjs loadMergeConfig.
|
|
298
|
+
function loadMergeConfig() {
|
|
299
|
+
var out = { auto: false };
|
|
300
|
+
var content = MOFLO_YAML;
|
|
301
|
+
if (!content) return out;
|
|
302
|
+
var block = content.match(/^merge:[ \\t]*\\r?\\n((?:[ \\t]+.*(?:\\r?\\n|$))*)/m);
|
|
303
|
+
if (!block) return out;
|
|
304
|
+
if (/^\\s*auto:\\s*true\\b/im.test(block[1])) out.auto = true;
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
|
|
296
308
|
// #1297 — read moflo.yaml once (both loaders parse it; gate fires on every
|
|
297
309
|
// Write/Edit). SYNC: mirrors bin/gate.cjs readMofloYaml.
|
|
298
310
|
function readMofloYaml() {
|
|
@@ -303,6 +315,7 @@ var MOFLO_YAML = readMofloYaml();
|
|
|
303
315
|
|
|
304
316
|
var config = loadGateConfig();
|
|
305
317
|
var sddConf = loadSddConfig();
|
|
318
|
+
var mergeConf = loadMergeConfig();
|
|
306
319
|
var command = process.argv[2];
|
|
307
320
|
|
|
308
321
|
var EXEMPT = ['.claude/', '.claude\\\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules', 'moflo.yaml'];
|
|
@@ -388,13 +401,58 @@ function detectFlMode(promptText) {
|
|
|
388
401
|
return null;
|
|
389
402
|
}
|
|
390
403
|
|
|
391
|
-
//
|
|
392
|
-
|
|
404
|
+
// Resolve ALL /flo run modifiers from the prompt + moflo.yaml. Single source of
|
|
405
|
+
// truth for gate arming AND the authoritative announcement below — a second
|
|
406
|
+
// implementation is how sdd.default got silently ignored. Precedence per key:
|
|
407
|
+
// --no-X > -x/--X > moflo.yaml > built-in (sdd opt-in, verify opt-out, merge
|
|
408
|
+
// opt-in). SYNC: mirrors bin/gate.cjs resolveFloRun.
|
|
409
|
+
function resolveFloRun(promptText) {
|
|
393
410
|
var p = promptText || '';
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (
|
|
397
|
-
|
|
411
|
+
var out = { isFlo: false, workflow: 'full', sdd: false, verify: false, merge: false,
|
|
412
|
+
sddSrc: 'default', verifySrc: 'default', mergeSrc: 'default' };
|
|
413
|
+
if (!/^\\s*\\/(?:fl|flo)\\b/i.test(p)) return out;
|
|
414
|
+
out.isFlo = true;
|
|
415
|
+
|
|
416
|
+
if (/(?:^|\\s)(?:-wf|--workflow)\\b/.test(p)) out.workflow = 'spell-engine';
|
|
417
|
+
else if (/(?:^|\\s)(?:-r|--research)\\b/.test(p)) out.workflow = 'research';
|
|
418
|
+
else if (/(?:^|\\s)(?:-t|--ticket)\\b/.test(p)) out.workflow = 'ticket';
|
|
419
|
+
var epicBranch = /(?:^|\\s)--epic-branch\\b/.test(p);
|
|
420
|
+
|
|
421
|
+
if (/(?:^|\\s)--no-sdd\\b/.test(p)) { out.sdd = false; out.sddSrc = 'flag'; }
|
|
422
|
+
else if (/(?:^|\\s)(?:-sd|--sdd)\\b/.test(p)) { out.sdd = true; out.sddSrc = 'flag'; }
|
|
423
|
+
else if (sddConf.default) { out.sdd = true; out.sddSrc = 'moflo.yaml sdd.default'; }
|
|
424
|
+
|
|
425
|
+
if (/(?:^|\\s)--no-verify\\b/.test(p)) { out.verify = false; out.verifySrc = 'flag'; }
|
|
426
|
+
else if (/(?:^|\\s)(?:-v|--verify)\\b/.test(p)) { out.verify = true; out.verifySrc = 'flag'; }
|
|
427
|
+
else if (!config.verify_before_done) { out.verify = false; out.verifySrc = 'moflo.yaml gates.verify_before_done'; }
|
|
428
|
+
else { out.verify = true; out.verifySrc = 'default'; }
|
|
429
|
+
if (out.sdd && !out.verify && out.verifySrc !== 'flag') out.verify = true;
|
|
430
|
+
|
|
431
|
+
if (/(?:^|\\s)--no-merge\\b/.test(p)) { out.merge = false; out.mergeSrc = 'flag'; }
|
|
432
|
+
else if (/(?:^|\\s)(?:-m|--merge)\\b/.test(p)) { out.merge = true; out.mergeSrc = 'flag'; }
|
|
433
|
+
else if (mergeConf.auto) { out.merge = true; out.mergeSrc = 'moflo.yaml merge.auto'; }
|
|
434
|
+
|
|
435
|
+
// Re-attribute anything applicability turned off — a false must never carry
|
|
436
|
+
// the source of the value it no longer has. SYNC: mirrors bin/gate.cjs.
|
|
437
|
+
if (out.workflow === 'ticket' || out.workflow === 'research') {
|
|
438
|
+
if (out.verify) out.verifySrc = out.workflow + ' mode does not implement';
|
|
439
|
+
out.verify = false;
|
|
440
|
+
}
|
|
441
|
+
if (out.workflow === 'research' || out.workflow === 'spell-engine') {
|
|
442
|
+
if (out.sdd) out.sddSrc = out.workflow + ' mode produces no spec artifacts';
|
|
443
|
+
out.sdd = false;
|
|
444
|
+
}
|
|
445
|
+
if (out.workflow !== 'full' || epicBranch) {
|
|
446
|
+
if (out.merge) out.mergeSrc = epicBranch ? '--epic-branch owns merging' : out.workflow + ' mode opens no PR';
|
|
447
|
+
out.merge = false;
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// #1297 — arm the SDD implement gate from a /flo prompt. Thin wrapper so the
|
|
453
|
+
// armed decision and the announced decision can never disagree.
|
|
454
|
+
function detectSddMode(promptText) {
|
|
455
|
+
return resolveFloRun(promptText).sdd;
|
|
398
456
|
}
|
|
399
457
|
|
|
400
458
|
// SDD specs-root resolution + artifact helpers for check-before-implement.
|
|
@@ -779,6 +837,30 @@ switch (command) {
|
|
|
779
837
|
applyPromptStateReset(s, prompt);
|
|
780
838
|
s.interactionCount = (s.interactionCount || 0) + 1;
|
|
781
839
|
writeState(s);
|
|
840
|
+
// Announce the resolved /flo run modifiers. moflo.yaml was already parsed in
|
|
841
|
+
// THIS process (fresh per prompt — a git pull or mid-session yaml edit is
|
|
842
|
+
// picked up automatically, no cache to invalidate), so this costs no extra
|
|
843
|
+
// read. SYNC: mirrors bin/gate.cjs prompt-reminder.
|
|
844
|
+
var floRun = resolveFloRun(prompt);
|
|
845
|
+
if (floRun.isFlo) {
|
|
846
|
+
console.log(
|
|
847
|
+
'[moflo] /flo run modes (AUTHORITATIVE — use verbatim; do NOT re-derive from the skill defaults): ' +
|
|
848
|
+
'sdd=' + (floRun.sdd ? 'ON' : 'off') +
|
|
849
|
+
' verify=' + (floRun.verify ? 'ON' : 'off') +
|
|
850
|
+
' merge=' + (floRun.merge ? 'ON' : 'off') +
|
|
851
|
+
' [workflow=' + floRun.workflow + ']'
|
|
852
|
+
);
|
|
853
|
+
if (floRun.sdd && floRun.sddSrc !== 'flag') {
|
|
854
|
+
console.log(
|
|
855
|
+
'[moflo] sdd is ON via ' + floRun.sddSrc + ' — the spec→plan→implement→verify cycle is ' +
|
|
856
|
+
'MANDATORY this run. Author the spec before editing source (the sdd_gate blocks source ' +
|
|
857
|
+
'Write/Edit until a reviewed plan exists). One-off opt out: re-run with --no-sdd.'
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
if (floRun.merge && floRun.mergeSrc !== 'flag') {
|
|
861
|
+
console.log('[moflo] merge is ON via ' + floRun.mergeSrc + ' — the PR will be auto-merged. Opt out: --no-merge.');
|
|
862
|
+
}
|
|
863
|
+
}
|
|
782
864
|
if (config.context_tracking) {
|
|
783
865
|
var ic = s.interactionCount;
|
|
784
866
|
if (ic > 30) console.log('Context: CRITICAL. Commit, store learnings, suggest new session.');
|
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.1",
|
|
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.
|
|
98
|
+
"moflo": "^4.12.0",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|