moflo 4.12.4 → 4.12.6

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.
Files changed (110) hide show
  1. package/.claude/guidance/shipped/moflo-agent-rules.md +1 -0
  2. package/.claude/guidance/shipped/moflo-claude-swarm-cohesion.md +1 -1
  3. package/.claude/guidance/shipped/moflo-cli-reference.md +4 -4
  4. package/.claude/guidance/shipped/moflo-core-guidance.md +4 -4
  5. package/.claude/guidance/shipped/moflo-guidance-rules.md +21 -0
  6. package/.claude/guidance/shipped/moflo-inline-fixes.md +98 -0
  7. package/.claude/guidance/shipped/moflo-root-cause-discipline.md +10 -10
  8. package/.claude/guidance/shipped/moflo-sdd.md +5 -5
  9. package/.claude/guidance/shipped/moflo-spell-engine.md +1 -1
  10. package/.claude/guidance/shipped/moflo-yaml-reference.md +8 -8
  11. package/.claude/helpers/gate.cjs +46 -7
  12. package/.claude/helpers/pr-create-command.cjs +440 -0
  13. package/.claude/skills/eldar/SKILL.md +1 -1
  14. package/.claude/skills/fl/SKILL.md +6 -6
  15. package/.claude/skills/fl/execution-modes.md +3 -3
  16. package/.claude/skills/fl/phases.md +8 -8
  17. package/.claude/skills/fl/sdd.md +6 -6
  18. package/.claude/skills/guidance/SKILL.md +3 -0
  19. package/.claude/skills/verify/SKILL.md +4 -4
  20. package/bin/gate.cjs +46 -7
  21. package/bin/hooks.mjs +4 -1
  22. package/bin/lib/retired-files.mjs +38 -6
  23. package/bin/lib/shipped-scripts.json +2 -1
  24. package/bin/lib/skill-categories.mjs +81 -17
  25. package/bin/pr-create-command.cjs +440 -0
  26. package/bin/session-start-launcher.mjs +11 -6
  27. package/dist/src/cli/aidefence/domain/entities/threat.js +2 -1
  28. package/dist/src/cli/commands/doctor-checks-deep.js +3 -2
  29. package/dist/src/cli/commands/doctor-checks-memory-access.js +3 -2
  30. package/dist/src/cli/commands/doctor-checks-swarm.js +3 -2
  31. package/dist/src/cli/commands/doctor-fixes.js +119 -20
  32. package/dist/src/cli/commands/hooks.js +2 -1
  33. package/dist/src/cli/commands/mcp.js +3 -2
  34. package/dist/src/cli/commands/memory.js +2 -1
  35. package/dist/src/cli/commands/performance.js +2 -1
  36. package/dist/src/cli/commands/retire.js +1 -1
  37. package/dist/src/cli/commands/spell-schedule.js +2 -1
  38. package/dist/src/cli/commands/swarm.js +216 -104
  39. package/dist/src/cli/guidance/headless.js +5 -8
  40. package/dist/src/cli/guidance/retriever.js +27 -6
  41. package/dist/src/cli/guidance/ruvbot-integration.js +2 -1
  42. package/dist/src/cli/guidance/uncertainty.js +2 -1
  43. package/dist/src/cli/hooks/bridge/official-hooks-bridge.js +2 -1
  44. package/dist/src/cli/hooks/mcp/index.js +3 -2
  45. package/dist/src/cli/hooks/reasoningbank/index.js +2 -1
  46. package/dist/src/cli/hooks/registry/index.js +2 -1
  47. package/dist/src/cli/hooks/swarm/index.js +6 -5
  48. package/dist/src/cli/init/claudemd-generator.js +2 -1
  49. package/dist/src/cli/init/helpers-generator.js +1 -1
  50. package/dist/src/cli/init/moflo-yaml-template.js +4 -4
  51. package/dist/src/cli/mcp-client.js +5 -2
  52. package/dist/src/cli/mcp-tools/agent-tools.js +4 -1
  53. package/dist/src/cli/mcp-tools/hive-mind-tools.js +4 -3
  54. package/dist/src/cli/mcp-tools/hooks-tools.js +6 -5
  55. package/dist/src/cli/mcp-tools/json-store.js +3 -2
  56. package/dist/src/cli/mcp-tools/neural-tools.js +2 -1
  57. package/dist/src/cli/mcp-tools/performance-tools.js +2 -1
  58. package/dist/src/cli/mcp-tools/session-tools.js +4 -2
  59. package/dist/src/cli/mcp-tools/spell-tools.js +2 -1
  60. package/dist/src/cli/memory/bridge-core.js +8 -2
  61. package/dist/src/cli/memory/controllers/_shared.js +8 -2
  62. package/dist/src/cli/memory/controllers/nightly-learner.js +2 -1
  63. package/dist/src/cli/memory/domain/services/memory-domain-service.js +2 -1
  64. package/dist/src/cli/memory/entries-write.js +2 -1
  65. package/dist/src/cli/memory/intelligence.js +2 -1
  66. package/dist/src/cli/memory/persistent-sona.js +1 -3
  67. package/dist/src/cli/memory/types.js +2 -3
  68. package/dist/src/cli/memory/verify.js +3 -2
  69. package/dist/src/cli/movector/graph-analyzer.js +14 -3
  70. package/dist/src/cli/neural/pattern-learner.js +4 -2
  71. package/dist/src/cli/neural/reasoning-bank.js +2 -1
  72. package/dist/src/cli/neural/reasoningbank-adapter.js +2 -1
  73. package/dist/src/cli/neural/sona-manager.js +4 -3
  74. package/dist/src/cli/production/monitoring.js +2 -1
  75. package/dist/src/cli/services/daemon-service.js +57 -46
  76. package/dist/src/cli/services/daemon-spell-executor.js +3 -2
  77. package/dist/src/cli/services/headless-worker-executor.js +4 -2
  78. package/dist/src/cli/services/learning-service.js +2 -1
  79. package/dist/src/cli/services/project-root.js +17 -1
  80. package/dist/src/cli/services/worker-daemon.js +2 -1
  81. package/dist/src/cli/shared/hooks/example-usage.js +3 -2
  82. package/dist/src/cli/shared/hooks/safety/git-commit.js +11 -2
  83. package/dist/src/cli/shared/hooks/session-hooks.js +3 -2
  84. package/dist/src/cli/shared/hooks/task-hooks.js +2 -1
  85. package/dist/src/cli/shared/mcp/session-manager.js +2 -1
  86. package/dist/src/cli/shared/plugins/official/hive-mind-plugin.js +2 -1
  87. package/dist/src/cli/shared/plugins/official/maestro-plugin.js +3 -2
  88. package/dist/src/cli/shared/security/index.js +6 -2
  89. package/dist/src/cli/shared/utils/atomic-file-write.js +9 -1
  90. package/dist/src/cli/shared/utils/id.js +54 -0
  91. package/dist/src/cli/shared/utils/platform.js +36 -5
  92. package/dist/src/cli/spells/commands/destructive-pattern-checker.js +6 -1
  93. package/dist/src/cli/spells/core/dry-run-validator.js +2 -1
  94. package/dist/src/cli/spells/core/interpolation.js +8 -3
  95. package/dist/src/cli/spells/core/permission-resolver.js +2 -2
  96. package/dist/src/cli/spells/core/runner.js +2 -1
  97. package/dist/src/cli/spells/core/shell.js +7 -8
  98. package/dist/src/cli/spells/factory/runner-bridge.js +3 -2
  99. package/dist/src/cli/spells/factory/runner-factory.js +3 -2
  100. package/dist/src/cli/spells/scheduler/scheduler.js +2 -1
  101. package/dist/src/cli/swarm/consensus/gossip.js +2 -1
  102. package/dist/src/cli/swarm/federation-hub.js +5 -4
  103. package/dist/src/cli/swarm/queen-coordinator.js +5 -4
  104. package/dist/src/cli/swarm/shared/events.js +2 -1
  105. package/dist/src/cli/swarm/unified-coordinator.js +3 -2
  106. package/dist/src/cli/version.js +1 -1
  107. package/package.json +2 -2
  108. package/retired-files.json +0 -72
  109. package/dist/src/cli/mcp-tools/coordination-tools.js +0 -86
  110. package/dist/src/cli/shared/security/secure-random.js +0 -142
@@ -0,0 +1,440 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Does this Bash command actually invoke `gh pr create`?
5
+ *
6
+ * Issue #1410. The gates in `gate.cjs` (`check-before-pr`, `check-before-done`)
7
+ * used to answer this with a single regex applied to raw command text:
8
+ *
9
+ * /(?:^|&&\s*|\|\|\s*|;\s*)\s*(?:[A-Z_][A-Z0-9_]*=\S+\s+)*gh\s+pr\s+create\b/
10
+ *
11
+ * That was wrong in both directions at once. It MISSED shapes people routinely
12
+ * type — a newline-separated multi-line command, `cat body.md | gh pr create
13
+ * --body-file -`, `( gh pr create )` — each of which opened a PR with every gate
14
+ * silently skipped. And it FIRED on commands that merely quote the literal — a
15
+ * `git commit -m "...gh pr create..."`, a `node -e '...'` probe, a heredoc body —
16
+ * blocking work that has nothing to do with opening a PR.
17
+ *
18
+ * One cause, both symptoms: a regex over raw text cannot tell a command from a
19
+ * string that quotes one. That is why the obvious repair (widen the separator
20
+ * set to include newline and `|`) makes the over-match strictly worse — every
21
+ * heredoc body line starting with the literal would become a match.
22
+ *
23
+ * So: two passes.
24
+ *
25
+ * 1. `sanitizeShellData` blanks the regions that are data rather than command
26
+ * — comments, heredoc bodies, all quote flavours, escapes — replacing them
27
+ * with spaces and PRESERVING LENGTH, so nothing outside a blanked region
28
+ * shifts or merges.
29
+ * 2. `isPrCreateCommand` searches the sanitised text for the literal, then
30
+ * walks BACKWARDS from each hit to confirm it starts a command.
31
+ *
32
+ * Four traps, three of which are silent BYPASSES (the gate stops blocking with
33
+ * no signal at all), are handled deliberately below and are marked `TRAP` at
34
+ * their sites:
35
+ *
36
+ * - `<<<` is a herestring, not a heredoc. Reading it as a heredoc opener
37
+ * means the delimiter is never found, the "body" blanks the rest of the
38
+ * input, and a real invocation on the next line is swallowed.
39
+ * - Heredoc headers are matched with a STICKY regex, never against a
40
+ * fixed-size `slice(i, i + N)` window. A window truncates a bare delimiter
41
+ * longer than it, registers the truncated prefix as the delimiter, and the
42
+ * real terminator line then never matches — body blanks to EOF.
43
+ * - `\` + newline is a line continuation, not a separator: the words after it
44
+ * are arguments, and heredoc bodies queued on that line do not start there.
45
+ * - Left-context stays OUT of the regex. `(?:^|[\n;&|(){}])\s*(?:NAME=val\s+)*gh…`
46
+ * is quadratic on sanitised input (long runs of spaces that `\s*` consumes,
47
+ * fails on, and backtracks through at every position). Searching for the
48
+ * literal first and walking back by hand is flat.
49
+ *
50
+ * Mirrored verbatim into `.claude/helpers/pr-create-command.cjs` — the dogfood
51
+ * parity guard enforces byte-identity. Dependency-free CommonJS, `node:`
52
+ * builtins only (it uses none), so it loads from either location.
53
+ */
54
+
55
+ /**
56
+ * Chars that can immediately precede a command word. `<` and `>` are absent on
57
+ * purpose: `> out.txt gh pr create` is a redirect target followed by arguments
58
+ * in the shapes we care about, not a fresh command.
59
+ *
60
+ * The backtick is present — and is deliberately NOT blanked as data the way
61
+ * quotes are. #1410's write-up lists backticks with the quote flavours, but an
62
+ * UNQUOTED backtick is command substitution: blanking it would make
63
+ * `` `gh pr create` `` stop blocking, i.e. exactly the silent-bypass class the
64
+ * issue is about. Backticks inside `"…"` are still blanked with the rest of the
65
+ * quoted run, which is where the over-match risk actually lives.
66
+ */
67
+ var SEPARATOR_CHARS = '\n;&|(){}`';
68
+
69
+ /**
70
+ * Words that may sit between a separator and the command without changing the
71
+ * fact that a command follows. Kept short and literal — anything not listed
72
+ * (e.g. `echo`) correctly makes the walk fail, because its following words are
73
+ * arguments, not a command.
74
+ */
75
+ var PREFIX_WORDS = ['!', 'then', 'else', 'elif', 'do', 'time', 'command', 'builtin', 'exec', 'env', 'nohup', 'xargs', 'sudo'];
76
+
77
+ /** `NAME=value` prefix, e.g. `GH_TOKEN=x gh pr create`. Bounded to one word. */
78
+ var ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=\S*$/;
79
+
80
+ /**
81
+ * Heredoc header, matched STICKY at a known index. Alternatives, in order:
82
+ * `'DELIM'`, `"DELIM"`, `$'DELIM'`, bare DELIM. The quoted forms come first so
83
+ * a quoted delimiter is never mis-read as a bare one starting with a quote.
84
+ */
85
+ var HEREDOC_RE = /<<(-?)[ \t]*(?:'([^'\n]*)'|"([^"\n]*)"|\$'([^'\n]*)'|([^\s;&|<>()'"`]+))/y;
86
+
87
+ /** The invocation itself. No left-context — see the fourth trap above. */
88
+ var PR_CREATE_RE = /gh\s+pr\s+create\b/g;
89
+
90
+ /**
91
+ * Pre-sanitise reject: `gh` as a whole token. A bare `indexOf('gh')` is far too
92
+ * weak — "gh" is a common English digraph, so `highlight`, `github`, `though`,
93
+ * and `flight` all fall through to the full two-pass scan on a hook that runs as
94
+ * a fresh process on every Bash call, at both gate call sites.
95
+ *
96
+ * Safe against false negatives: `PR_CREATE_RE` requires whitespace after `gh`,
97
+ * and sanitising only ever replaces characters with spaces — it never turns a
98
+ * non-word character into a word one — so a `gh` that survives to match here
99
+ * cannot have had a word character in front of it in the raw text.
100
+ *
101
+ * No quantifiers, so the scan is linear with no backtracking.
102
+ */
103
+ var GH_TOKEN_RE = /(?:^|[^A-Za-z0-9_])gh(?![A-Za-z0-9_])/;
104
+
105
+ /** Horizontal whitespace only. `\n` is a separator and must not be skipped as blank. */
106
+ function isBlank(ch) {
107
+ return ch === ' ' || ch === '\t' || ch === '\r';
108
+ }
109
+
110
+ /** A `#` opens a comment only when it begins a word. */
111
+ function startsWord(cmd, i) {
112
+ if (i === 0) return true;
113
+ var prev = cmd.charAt(i - 1);
114
+ return isBlank(prev) || SEPARATOR_CHARS.indexOf(prev) !== -1;
115
+ }
116
+
117
+ /** Index of the first character of the whitespace/separator-delimited token containing `from`. */
118
+ function tokenStartAt(s, from) {
119
+ var k = from;
120
+ while (k > 0 && !isBlank(s.charAt(k - 1)) && SEPARATOR_CHARS.indexOf(s.charAt(k - 1)) === -1) k--;
121
+ return k;
122
+ }
123
+
124
+ /**
125
+ * How deep `$(…)` inside `"…"` inside `$(…)` is followed before giving up and
126
+ * leaving the region blanked. Nothing real nests anywhere near this; the cap
127
+ * exists so a pathological input cannot recurse into a stack overflow.
128
+ */
129
+ var MAX_SUBSTITUTION_DEPTH = 8;
130
+
131
+ /**
132
+ * Replace every data region with spaces, preserving length. What remains is
133
+ * command text — the only thing safe to match a command against.
134
+ *
135
+ * Exported for tests: asserting on the sanitised string is how the blanking
136
+ * rules are pinned independently of the matcher on top of them.
137
+ *
138
+ * @param {string} cmd
139
+ * @param {number} [depth] internal — substitution recursion depth
140
+ * @returns {string} same length as `cmd`
141
+ */
142
+ function sanitizeShellData(cmd, depth) {
143
+ var n = cmd.length;
144
+ var out = cmd.split('');
145
+ var pending = [];
146
+ var level = depth || 0;
147
+ var i = 0;
148
+
149
+ function blank(from, to) {
150
+ var stop = to > n ? n : to;
151
+ for (var k = from; k < stop; k++) out[k] = ' ';
152
+ }
153
+
154
+ /** Copy `raw`'s chars back into `out` at `from` — the inverse of `blank`. */
155
+ function restore(from, raw) {
156
+ for (var k = 0; k < raw.length; k++) out[from + k] = raw.charAt(k);
157
+ }
158
+
159
+ /** Scan a quoted run from `i` to its closing `close`, honouring `\` escapes if asked. */
160
+ function closingIndex(from, close, escapes) {
161
+ var j = from;
162
+ while (j < n) {
163
+ var ch = cmd.charAt(j);
164
+ if (escapes && ch === '\\') { j += 2; continue; }
165
+ if (ch === close) return j + 1;
166
+ j++;
167
+ }
168
+ return n;
169
+ }
170
+
171
+ /**
172
+ * Index of the `)` closing the `(` at `openIdx`, or -1 before `limit`.
173
+ * Skips quoted runs so a `)` inside a string doesn't close the substitution.
174
+ */
175
+ function matchingParen(openIdx, limit) {
176
+ var d = 0;
177
+ var k = openIdx;
178
+ while (k < limit) {
179
+ var ch = cmd.charAt(k);
180
+ if (ch === '\\') { k += 2; continue; }
181
+ if (ch === "'") { k = closingIndex(k + 1, "'", false); continue; }
182
+ if (ch === '"') { k = doubleQuoteEnd(k + 1); continue; }
183
+ if (ch === '(') d++;
184
+ else if (ch === ')') { d--; if (d === 0) return k; }
185
+ k++;
186
+ }
187
+ return -1;
188
+ }
189
+
190
+ /**
191
+ * Index just past the `"` that closes the run opened before `from`.
192
+ *
193
+ * Not `closingIndex(from, '"', true)`: quoting RESTARTS inside a command
194
+ * substitution, so in `"a $(cmd "b") c"` the quotes around `b` belong to the
195
+ * substitution and do not end the outer run. Scanning for the next unescaped
196
+ * `"` would cut the run short at `b`'s opening quote and leave the second half
197
+ * — including anything the substitution runs — mis-parsed.
198
+ *
199
+ * Mutually recursive with `matchingParen`; both only ever move forward, so the
200
+ * pair terminates.
201
+ */
202
+ function doubleQuoteEnd(from) {
203
+ var j = from;
204
+ while (j < n) {
205
+ var ch = cmd.charAt(j);
206
+ if (ch === '\\') { j += 2; continue; }
207
+ if (ch === '"') return j + 1;
208
+ if (ch === '$' && cmd.charAt(j + 1) === '(') {
209
+ var close = matchingParen(j + 1, n);
210
+ if (close !== -1) { j = close + 1; continue; }
211
+ j += 2;
212
+ continue;
213
+ }
214
+ if (ch === '`') {
215
+ var bt = cmd.indexOf('`', j + 1);
216
+ if (bt !== -1) { j = bt + 1; continue; }
217
+ }
218
+ j++;
219
+ }
220
+ return n;
221
+ }
222
+
223
+ /**
224
+ * A double-quoted run and an unquoted-delimiter heredoc body are data —
225
+ * EXCEPT for the command substitutions inside them, which the shell still
226
+ * runs. Blanking those wholesale would leave `echo "$(gh pr create)"` and its
227
+ * heredoc equivalent as silent bypasses, so restore each `$(…)` / `` `…` ``
228
+ * region over the already-blanked range: its delimiters (both of which the
229
+ * backwards walk reads as separators) plus its body, sanitised in its own
230
+ * right so quoting INSIDE the substitution still applies.
231
+ *
232
+ * `'…'`, `$'…'` and quoted-delimiter heredocs (`<<'EOF'`) are left blanked —
233
+ * the shell does not expand those, so there is nothing to restore.
234
+ */
235
+ function restoreSubstitutions(from, to) {
236
+ if (level >= MAX_SUBSTITUTION_DEPTH) return;
237
+ var k = from;
238
+ while (k < to) {
239
+ var ch = cmd.charAt(k);
240
+ if (ch === '\\') { k += 2; continue; }
241
+ var openLen = 0;
242
+ var close = -1;
243
+ if (ch === '$' && cmd.charAt(k + 1) === '(') {
244
+ openLen = 2;
245
+ close = matchingParen(k + 1, to);
246
+ } else if (ch === '`') {
247
+ openLen = 1;
248
+ close = cmd.indexOf('`', k + 1);
249
+ if (close >= to) close = -1;
250
+ }
251
+ if (close === -1) { k++; continue; }
252
+ restore(k, cmd.slice(k, k + openLen));
253
+ restore(close, cmd.charAt(close));
254
+ restore(k + openLen, sanitizeShellData(cmd.slice(k + openLen, close), level + 1));
255
+ k = close + 1;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Blank one heredoc body, starting at `start` (the first char of the line
261
+ * after the newline that ended the header's logical line).
262
+ *
263
+ * Blanks each body line's content and the terminator line's content, leaving
264
+ * every newline in place — so the text after the heredoc still begins after a
265
+ * real separator, and offsets outside the body are untouched.
266
+ *
267
+ * Returns the index to resume scanning from. An unterminated heredoc blanks to
268
+ * end of input, which is what the shell does with it too.
269
+ */
270
+ function blankHeredocBody(start, delim, stripTabs, expands) {
271
+ var pos = start;
272
+ while (pos < n) {
273
+ var eol = cmd.indexOf('\n', pos);
274
+ var lineEnd = eol === -1 ? n : eol;
275
+ var line = cmd.slice(pos, lineEnd);
276
+ // CRLF: the terminator is the line's content, not its line ending.
277
+ if (line.charAt(line.length - 1) === '\r') line = line.slice(0, -1);
278
+ var candidate = stripTabs ? line.replace(/^\t+/, '') : line;
279
+ blank(pos, lineEnd);
280
+ if (expands) restoreSubstitutions(pos, lineEnd);
281
+ if (candidate === delim) return eol === -1 ? n : eol + 1;
282
+ if (eol === -1) return n;
283
+ pos = eol + 1;
284
+ }
285
+ return n;
286
+ }
287
+
288
+ while (i < n) {
289
+ var c = cmd.charAt(i);
290
+
291
+ // TRAP: `\` + newline is a line continuation. Blanking BOTH characters keeps
292
+ // the logical line going, so the words after it stay arguments — otherwise
293
+ // `cat <<EOF \` + newline + `gh pr create` reads as a command when the shell
294
+ // reads it as arguments to `cat`. Any other `\x` is an escape; blanking the
295
+ // pair likewise stops `\;` and friends from reading as separators.
296
+ if (c === '\\') {
297
+ blank(i, i + 2);
298
+ i += 2;
299
+ continue;
300
+ }
301
+
302
+ if (c === '#' && startsWord(cmd, i)) {
303
+ var eol = cmd.indexOf('\n', i);
304
+ if (eol === -1) eol = n;
305
+ blank(i, eol);
306
+ i = eol;
307
+ continue;
308
+ }
309
+
310
+ if (c === '<' && cmd.charAt(i + 1) === '<') {
311
+ // TRAP: `<<<` is a herestring. Skip the operator — its word is handled by
312
+ // the ordinary quote/plain-text rules below.
313
+ if (cmd.charAt(i + 2) === '<') { i += 3; continue; }
314
+ // TRAP: sticky match at `i`, no window, no substring copy.
315
+ HEREDOC_RE.lastIndex = i;
316
+ var h = HEREDOC_RE.exec(cmd);
317
+ if (h) {
318
+ var delim = h[2] !== undefined ? h[2] : h[3] !== undefined ? h[3] : h[4] !== undefined ? h[4] : h[5];
319
+ // A BARE delimiter (`<<EOF`) expands `$(…)` in the body; any quoted form
320
+ // (`<<'EOF'`, `<<"EOF"`, `<<$'EOF'`) suppresses expansion entirely.
321
+ pending.push({ delim: delim, stripTabs: h[1] === '-', expands: h[5] !== undefined });
322
+ blank(i, i + h[0].length);
323
+ i += h[0].length;
324
+ continue;
325
+ }
326
+ // Not a header we recognise (`<< ;`, `<<` at end of input) — treat as text.
327
+ i += 2;
328
+ continue;
329
+ }
330
+
331
+ // `$'...'` processes `\` escapes; plain `'...'` does not.
332
+ if (c === '$' && cmd.charAt(i + 1) === "'") {
333
+ var ansiEnd = closingIndex(i + 2, "'", true);
334
+ blank(i, ansiEnd);
335
+ i = ansiEnd;
336
+ continue;
337
+ }
338
+
339
+ if (c === "'") {
340
+ var sqEnd = closingIndex(i + 1, "'", false);
341
+ blank(i, sqEnd);
342
+ i = sqEnd;
343
+ continue;
344
+ }
345
+
346
+ if (c === '"') {
347
+ var dqEnd = doubleQuoteEnd(i + 1);
348
+ blank(i, dqEnd);
349
+ // Substitutions inside the run still execute — see restoreSubstitutions.
350
+ restoreSubstitutions(i + 1, dqEnd - 1);
351
+ i = dqEnd;
352
+ continue;
353
+ }
354
+
355
+ if (c === '\n') {
356
+ i++;
357
+ // Bodies for every heredoc announced on the logical line just ended, in
358
+ // the order they were announced (`cat <<A <<B` reads A's body then B's).
359
+ while (pending.length > 0) {
360
+ var hd = pending.shift();
361
+ i = blankHeredocBody(i, hd.delim, hd.stripTabs, hd.expands);
362
+ }
363
+ continue;
364
+ }
365
+
366
+ i++;
367
+ }
368
+
369
+ return out.join('');
370
+ }
371
+
372
+ /**
373
+ * Does the token at `idx` in sanitised text `s` begin a command? True when what
374
+ * precedes it is the start of input, a separator, or a run of `NAME=value` /
375
+ * command-prefix words that itself bottoms out at one of those.
376
+ */
377
+ function startsCommand(s, idx) {
378
+ // The hit must be the whole command word. `PR_CREATE_RE` carries no leading
379
+ // `\b`, so it can land mid-token — and the backwards walk below would then
380
+ // read a TRUNCATED prefix as a command prefix: `dogh pr create` yields `do`,
381
+ // which is in PREFIX_WORDS, and `FOO=bargh pr create` yields `FOO=bar`, which
382
+ // matches ASSIGNMENT_RE. Both are one unrelated token; both used to over-match.
383
+ //
384
+ // A leading `\b` would not fix it on its own — `\b` admits `=`, so the command
385
+ // in `FOO=gh pr create` (which is `pr`, not `gh`) would still walk back over
386
+ // `FOO=` and report a command start.
387
+ //
388
+ // Checking the whole token rather than just the character before it also keeps
389
+ // an invocation BY PATH working: `/usr/bin/gh pr create`, `./gh pr create`, and
390
+ // on Windows `C:/tools/gh` or `/c/tools/gh` — the forms that are valid command
391
+ // words for the shell the Bash tool runs.
392
+ //
393
+ // Only `/` is accepted as the separator, deliberately. A `\` is an ESCAPE here
394
+ // and is blanked in pass 1, so it can never reach this token; accepting it too
395
+ // would be unreachable code. The cost is that a backslash-escaped invocation
396
+ // (`C:\tools\gh`, or `\gh`) is not recognised — a miss, not an over-block, and
397
+ // the same miss the pre-#1410 regex had. Blanking the escape is what makes
398
+ // `echo a\; gh pr create` correctly NOT read as a command, which is worth more.
399
+ var tokenStart = tokenStartAt(s, idx);
400
+ var token = s.slice(tokenStart, idx + 2);
401
+ if (token !== 'gh' && !/\/gh$/.test(token)) return false;
402
+
403
+ var i = tokenStart;
404
+ for (;;) {
405
+ while (i > 0 && isBlank(s.charAt(i - 1))) i--;
406
+ if (i === 0) return true;
407
+ if (SEPARATOR_CHARS.indexOf(s.charAt(i - 1)) !== -1) return true;
408
+ var end = i;
409
+ i = tokenStartAt(s, i);
410
+ if (i === end) return false;
411
+ var word = s.slice(i, end);
412
+ if (!ASSIGNMENT_RE.test(word) && PREFIX_WORDS.indexOf(word) === -1) return false;
413
+ }
414
+ }
415
+
416
+ /**
417
+ * True when `cmd` invokes `gh pr create` as a command — not when it merely
418
+ * contains those words inside a string, comment, or heredoc body.
419
+ *
420
+ * @param {string} cmd raw Bash command text (Claude Code's `tool_input.command`)
421
+ * @returns {boolean}
422
+ */
423
+ function isPrCreateCommand(cmd) {
424
+ // Fast reject before sanitising. These gates run on EVERY Bash call, and a
425
+ // large non-`gh` command would otherwise pay the full two-pass cost. `indexOf`
426
+ // first because it is the cheapest possible screen; the token check then
427
+ // discards the digraph false positives (`highlight`, `github`, `though`) that
428
+ // `indexOf` alone lets through.
429
+ if (typeof cmd !== 'string' || cmd.indexOf('gh') === -1) return false;
430
+ if (!GH_TOKEN_RE.test(cmd)) return false;
431
+ var s = sanitizeShellData(cmd);
432
+ PR_CREATE_RE.lastIndex = 0;
433
+ var m;
434
+ while ((m = PR_CREATE_RE.exec(s)) !== null) {
435
+ if (startsCommand(s, m.index)) return true;
436
+ }
437
+ return false;
438
+ }
439
+
440
+ module.exports = { isPrCreateCommand: isPrCreateCommand, sanitizeShellData: sanitizeShellData };
@@ -121,7 +121,7 @@ mcp__moflo__memory_stats — { namespace: "learnings" }
121
121
 
122
122
  Flag empty `learnings` as `info` (project hasn't accumulated decisions yet — fine for new projects). Flag empty `guidance` as `warn` (no indexed guidance means semantic search is degraded).
123
123
 
124
- **Legacy `doc-*` residue (#1053 S4)** — moflo retired whole-document indexing in favor of chunk-only RAG. The `purge-doc-entries` migration runs on session-start; if any `doc-*` rows linger, the migration didn't fire (ran with no DB, errored, or the install is below the migration's introduction).
124
+ **Legacy `doc-*` residue** — moflo retired whole-document indexing in favor of chunk-only RAG. The `purge-doc-entries` migration runs on session-start; if any `doc-*` rows linger, the migration didn't fire (ran with no DB, errored, or the install is below the migration's introduction).
125
125
 
126
126
  ```
127
127
  mcp__moflo__memory_search — { query: "doc-", namespace: "guidance", threshold: 0, limit: 5 }
@@ -62,19 +62,19 @@ Under `--sdd` (or any run that authors or consults a spec), add these **spec-tar
62
62
 
63
63
  Worktree isolation is orthogonal to every other flag: it changes *where* the branch is created and the work happens, not *what* runs. All other arguments (mode, execution style, issue/title) apply unchanged. Ignored (with a one-line note) when `--epic-branch` is set — the epic orchestrator owns branch/worktree layout — and in `-r`/`--research` and `-t`/`--ticket` modes, which never touch a branch.
64
64
 
65
- ## SDD & verification (Epic #1269)
65
+ ## SDD & verification
66
66
 
67
67
  Two **independent** modifiers, orthogonal to execution mode (`-n`/`-s`/`-h`) and `--worktree`. Verify is deliberately separable from SDD — you can get the completion gate without the spec ceremony.
68
68
 
69
69
  | Flag | Long | Effect |
70
70
  |------|------|--------|
71
71
  | `-sd` | `--sdd` | Run the full **spec → plan → (review) → implement → verify** cycle. Short is `-sd`, **not** `-s` (swarm) — follows the two-letter convention (`-wf`, `-wt`). Implies `--verify`. |
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`. |
72
+ | `-v` | `--verify` | **Verify-before-done** — a normal run plus the `/verify` skill (the completion gate), no spec/plan front-half. **On by default** — 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` (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`.
75
+ Defaults seed from `moflo.yaml` — `sdd.default` (built-in **off**) and `gates.verify_before_done` (built-in **on**). 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
- ## Auto-merge (#1285)
77
+ ## Auto-merge
78
78
 
79
79
  | Flag | Long | Effect |
80
80
  |------|------|--------|
@@ -164,7 +164,7 @@ let epicBranch = null;
164
164
  let issueNumber = null;
165
165
  let titleWords = [];
166
166
 
167
- // SDD / verify / merge modifiers (Epic #1269, #1294, #1285) — DO NOT resolve
167
+ // SDD / verify / merge modifiers — DO NOT resolve
168
168
  // these here. They are config-derived, and the values below are placeholders,
169
169
  // NOT defaults. Take them from the "Resolved run modes" step above; the loop
170
170
  // below only applies what the user typed on top of that resolution.
@@ -230,7 +230,7 @@ if (useWorktree && (epicBranch || workflowMode !== "full")) {
230
230
 
231
231
  // SDD/verify are implementation-time modifiers. -t (ticket) and -r (research)
232
232
  // never implement, so verify is a no-op there — clear it. Verify is on by
233
- // default now (#1294), so only surface the "ignored" note when the user
233
+ // default now, so only surface the "ignored" note when the user
234
234
  // EXPLICITLY passed -v/--verify — otherwise clearing the default is silent.
235
235
  // --sdd in -t writes the spec/plan INTO the ticket (see ./sdd.md); in -r ignored.
236
236
  if (workflowMode === "ticket" || workflowMode === "research") {
@@ -6,12 +6,12 @@ The SDD/verify modifiers (`-sd`/`--sdd`, `-v`/`--verify` — see `./sdd.md`) are
6
6
 
7
7
  ## SWARM mode (`-s`, `--swarm`)
8
8
 
9
- > **MANDATORY when `-s` is passed.** Your first Execute-phase action MUST be `mcp__moflo__swarm_init`, followed by `mcp__moflo__agent_spawn` for each role. Spawning subagents via `Agent` (or `Task`) without first registering the swarm is a violation of issue #952. The `Agent` PreToolUse gate will BLOCK the call until `swarm_init` runs. Even when you also use `Agent` for parallelism, the moflo swarm IS the registration surface — call it first. See CLAUDE.md "⛔ Protected functionality — swarm + hive-mind".
9
+ > **MANDATORY when `-s` is passed.** Your first Execute-phase action MUST be `mcp__moflo__swarm_init`, followed by `mcp__moflo__agent_spawn` for each role. Spawning subagents via `Agent` (or `Task`) without first registering the swarm is a violation of the swarm-registration contract. The `Agent` PreToolUse gate will BLOCK the call until `swarm_init` runs. Even when you also use `Agent` for parallelism, the moflo swarm IS the registration surface — call it first. See CLAUDE.md "⛔ Protected functionality — swarm + hive-mind".
10
10
 
11
11
  > **`-s` is the user's request for agents.** An ambient "don't call the Agent
12
12
  > tool unless the user requested it" is satisfied by the flag — dispatch.
13
13
  > `swarm_init`/`agent_spawn` are MCP tools, so a suppressed run still registers a
14
- > swarm and passes the #952 gate while dispatching nobody. If you cannot spawn
14
+ > swarm and passes the registration gate while dispatching nobody. If you cannot spawn
15
15
  > `Agent`, say so and stop.
16
16
 
17
17
  Swarm mode coordinates agents through the moflo swarm coordinator, then spawns workers via the `Agent` tool.
@@ -79,4 +79,4 @@ Single Claude execution without spawning sub-agents.
79
79
 
80
80
  ## Why these are MANDATORY
81
81
 
82
- Swarm and hive-mind are headline moflo product surface (CLAUDE.md "⛔ Protected functionality"). When the user explicitly opts in via `-s`/`-h`, the protected MCP surface MUST be exercised — falling back to "Claude-native parallelism" via `Agent` tool calls without coordinator registration is the failure mode that prompted issue #952. The PreToolUse gate enforces this; opt-out is `gates.swarm_invocation_gate: false` in `moflo.yaml`.
82
+ Swarm and hive-mind are headline moflo product surface (CLAUDE.md "⛔ Protected functionality"). When the user explicitly opts in via `-s`/`-h`, the protected MCP surface MUST be exercised — falling back to "Claude-native parallelism" via `Agent` tool calls without coordinator registration is the failure mode this rule exists to prevent. The PreToolUse gate enforces this; opt-out is `gates.swarm_invocation_gate: false` in `moflo.yaml`.
@@ -4,7 +4,7 @@ Phase-by-phase notes for the full `/flo <issue>` run. Phase 2 (Ticket) lives in
4
4
 
5
5
  ## Phase 0: Record run start (Flo Runs dashboard)
6
6
 
7
- Before research, open a run record so the Luminarium "Flo Runs" tab shows this run live and after the next session restart (#968), and so Phase 5.5 can price it. Skip this phase ONLY when `--epic-branch` is set — the epic orchestrator owns the parent record and the per-story spell engine writes its own row.
7
+ Before research, open a run record so the Luminarium "Flo Runs" tab shows this run live and after the next session restart, and so Phase 5.5 can price it. Skip this phase ONLY when `--epic-branch` is set — the epic orchestrator owns the parent record and the per-story spell engine writes its own row.
8
8
 
9
9
  One command. It builds the record, derives the run id, and picks up the session id `gate.cjs` stamped on this prompt:
10
10
 
@@ -20,7 +20,7 @@ Flag the mode when it isn't a plain ticket run: `--research` (`-r`), `--new-tick
20
20
 
21
21
  `sessionId: null` means no session id was stamped (the gate has not seen a prompt yet, or you are outside Claude Code). The run still records; only its token cost will be unmeasurable.
22
22
 
23
- Do **not** hand-write a `memory_store` call for this. The record shape lives in `storeFloRunRecord` (`src/cli/services/daemon-dashboard.ts`) and `flo runs start` is its only caller — #1333 replaced a copy of the schema that lived here in prose, which produced exactly one record across the whole retained corpus because it depended on remembering to perform it.
23
+ Do **not** hand-write a `memory_store` call for this. The record shape lives in `storeFloRunRecord` (`src/cli/services/daemon-dashboard.ts`) and `flo runs start` is its only caller — it replaced a copy of the schema that once lived here in prose, which produced exactly one record across the whole retained corpus because it depended on remembering to perform it.
24
24
 
25
25
  The session-start launcher retains the most recent ~200 tasklist rows, so this record outlives the session and renders in the Flo Runs tab on subsequent restarts.
26
26
 
@@ -63,7 +63,7 @@ flo memory search --query "<domain keywords>" --namespace guidance
63
63
  Or via MCP: `mcp__moflo__memory_search`.
64
64
 
65
65
  ### 1.4 Reach guidance via memory (only if memory was thin)
66
- If the §1.3 search above (the memory-first step named `SKILL.md` Step 0) returned fewer than three relevant results, widen the query — **still through `mcp__moflo__memory_search` (namespace `guidance`)**, not a direct `Read` of `.claude/guidance/*.md`. Guidance docs are indexed; search returns the relevant chunk, and `mcp__moflo__memory_get_neighbors` traverses adjacent context far more cheaply than reading the whole doc (this is the #1292 fix — don't bulk-read `.claude/guidance/moflo-sdd.md` et al.). Angle the query by change type:
66
+ If the §1.3 search above (the memory-first step named `SKILL.md` Step 0) returned fewer than three relevant results, widen the query — **still through `mcp__moflo__memory_search` (namespace `guidance`)**, not a direct `Read` of `.claude/guidance/*.md`. Guidance docs are indexed; search returns the relevant chunk, and `mcp__moflo__memory_get_neighbors` traverses adjacent context far more cheaply than reading the whole doc (don't bulk-read `.claude/guidance/moflo-sdd.md` et al.). Angle the query by change type:
67
67
  - Bug → testing patterns, error handling
68
68
  - Feature → domain model, architecture
69
69
  - UI → frontend patterns, components
@@ -165,13 +165,13 @@ Closes #<issue-number>"
165
165
  ```
166
166
 
167
167
  **No attribution trailer.** Do not add `Co-Authored-By:`, `Generated with …`, or any
168
- other tool-attribution line to commits or PR bodies (#1398). This is the consumer's
168
+ other tool-attribution line to commits or PR bodies. This is the consumer's
169
169
  repository and their git history is permanent — moflo does not sign their commits.
170
170
 
171
171
  ### 5.1b Verify-before-done (default; skipped only with `--no-verify`)
172
172
  **Delegate to the `/verify` skill** — `Skill({ skill: "verify" })`, passing the issue number or spec slug. That skill owns the mechanics (locate acceptance criteria → reuse Phase 4's already-green tests, no double verify → map each criterion → run only uncovered checks → record the outcome). Don't restate them here or verify in prose — *invoking* `/verify` is what records the run and satisfies the `check-before-done` gate.
173
173
 
174
- **When it runs:** by default (`verify_before_done` now defaults true, #1294) and always under `--sdd`; `--no-verify` skips it for one run. See `./sdd.md` for triggers and `.claude/skills/verify/SKILL.md` for how verification is performed.
174
+ **When it runs:** by default (`verify_before_done` now defaults true) and always under `--sdd`; `--no-verify` skips it for one run. See `./sdd.md` for triggers and `.claude/skills/verify/SKILL.md` for how verification is performed.
175
175
 
176
176
  ### 5.2 Store learnings
177
177
  Before opening the PR, call `mcp__moflo__memory_store` with what was learned. The `check-before-pr` gate blocks `gh pr create` until this has run.
@@ -208,7 +208,7 @@ gh pr create --title "type(scope): description" --body "## Summary
208
208
  Closes #<issue-number>"
209
209
  ```
210
210
 
211
- **Under `--sdd` (#1297):** when `sdd.embed_in_pr` is true (default), append the spec+plan to the PR body so the reasoning is reviewable in the PR even when specs stay local/gitignored. Run `flo sdd embed <slug>`, read its printed output from the tool result, and paste it as literal text at the end of the PR body — do **not** use shell command substitution (`$(...)`) or heredoc/`printf`, none of which are portable (Rule #1). The robust path on every OS: write the full body (summary + embed block) to a file with the Write tool, then `gh pr create --body-file <path>`. See `./sdd.md` step 7.
211
+ **Under `--sdd`:** when `sdd.embed_in_pr` is true (default), append the spec+plan to the PR body so the reasoning is reviewable in the PR even when specs stay local/gitignored. Run `flo sdd embed <slug>`, read its printed output from the tool result, and paste it as literal text at the end of the PR body — do **not** use shell command substitution (`$(...)`) or heredoc/`printf`, none of which are portable (Rule #1). The robust path on every OS: write the full body (summary + embed block) to a file with the Write tool, then `gh pr create --body-file <path>`. See `./sdd.md` step 7.
212
212
 
213
213
  ### 5.4 Update issue status
214
214
  ```bash
@@ -226,7 +226,7 @@ Idempotent and safe to skip when there is no back-reference. `--epic-branch` run
226
226
 
227
227
  ### 5.5 Close the run record
228
228
 
229
- Close the record Phase 0 opened, using the `runId` it printed. This is what snapshots the run's token cost — `finalize` reads the session transcript for the run's window and stores the rollup **in the same record**, so cost and outcome finally share a key (#1333):
229
+ Close the record Phase 0 opened, using the `runId` it printed. This is what snapshots the run's token cost — `finalize` reads the session transcript for the run's window and stores the rollup **in the same record**, so cost and outcome finally share a key:
230
230
 
231
231
  ```bash
232
232
  flo runs finalize --run-id <runId> # or: --status failed --error "<summary>"
@@ -236,7 +236,7 @@ Run it after the PR is open (5.3) so the rollup covers the whole run. Skipped un
236
236
 
237
237
  Snapshot rather than compute-on-read because Claude Code prunes `~/.claude/projects/**` — measured at roughly two days of history, so a cost joined at read time would be correct today and zero next week.
238
238
 
239
- `tokens.transcripts: 0` in the stored record means the run's cost could **not** be measured (no transcript, or no session id was stamped), which is not the same as a run that cost nothing. `success` means the run reached a terminal state without reporting an error — it is not a verification that the work was correct, and nothing here should be described as one (#1322).
239
+ `tokens.transcripts: 0` in the stored record means the run's cost could **not** be measured (no transcript, or no session id was stamped), which is not the same as a run that cost nothing. `success` means the run reached a terminal state without reporting an error — it is not a verification that the work was correct, and nothing here should be described as one.
240
240
 
241
241
  ### 5.3b Auto-merge the PR (`mergeMode` / `merge.auto`)
242
242
 
@@ -1,9 +1,9 @@
1
1
  # SDD & Verification (`-sd` / `--sdd`, `-v` / `--verify`)
2
2
 
3
- Spec-Driven Development for `/flo` — Epic #1269. Two independent modifiers:
3
+ Spec-Driven Development for `/flo`. Two independent modifiers:
4
4
 
5
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
- - **`-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.
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) — `-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
9
 
@@ -11,13 +11,13 @@ Precedence: per-run flags and `--no-sdd` / `--no-verify` override `moflo.yaml`,
11
11
 
12
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.
13
13
 
14
- > **Memory-first for SDD mechanics (do not bulk-read the guidance doc).** The authoritative SDD rules live in the **indexed** guidance doc `.claude/guidance/moflo-sdd.md`. Reach the slice you need via `mcp__moflo__memory_search { namespace: "guidance", query: "sdd <topic>" }` and traverse chunks with `mcp__moflo__memory_get_neighbors` — do **not** `Read` the whole `moflo-sdd.md` to find a rule (that is the anti-pattern #1292 fixes: with `-sd` enabled the operator read the entire doc instead of searching for the part it needed). **This file** (`./sdd.md`) is the skill's own companion runbook — it lives under `.claude/skills/`, is *not* indexed, and so is `Read` directly. See `SKILL.md` Step 0.
14
+ > **Memory-first for SDD mechanics (do not bulk-read the guidance doc).** The authoritative SDD rules live in the **indexed** guidance doc `.claude/guidance/moflo-sdd.md`. Reach the slice you need via `mcp__moflo__memory_search { namespace: "guidance", query: "sdd <topic>" }` and traverse chunks with `mcp__moflo__memory_get_neighbors` — do **not** `Read` the whole `moflo-sdd.md` to find a rule (the anti-pattern: with `-sd` enabled, reading the entire doc instead of searching for the part you need). **This file** (`./sdd.md`) is the skill's own companion runbook — it lives under `.claude/skills/`, is *not* indexed, and so is `Read` directly. See `SKILL.md` Step 0.
15
15
 
16
16
  ## The `--sdd` cycle
17
17
 
18
- Artifacts live at `<specs_dir>/<slug>/{spec,plan}.md` — default `.moflo/specs`, which is **gitignored** (local, not committed). By default (`sdd.embed_in_pr: true`) the spec + plan are appended to the PR body, so the reasoning is **reviewable in the PR even while specs stay local**. To source-control the artifacts instead (or as well), set `moflo.yaml sdd.specs_dir` to a tracked path (e.g. `docs/specs`) and commit them (#1294). Drive them with the `flo sdd` CLI; never hand-write the paths.
18
+ Artifacts live at `<specs_dir>/<slug>/{spec,plan}.md` — default `.moflo/specs`, which is **gitignored** (local, not committed). By default (`sdd.embed_in_pr: true`) the spec + plan are appended to the PR body, so the reasoning is **reviewable in the PR even while specs stay local**. To source-control the artifacts instead (or as well), set `moflo.yaml sdd.specs_dir` to a tracked path (e.g. `docs/specs`) and commit them. Drive them with the `flo sdd` CLI; never hand-write the paths.
19
19
 
20
- **The front half is enforced, not advisory (#1297).** When a run is armed for SDD (`-sd`/`--sdd` or `sdd.default`), the `check-before-implement` gate **blocks every source `Write`/`Edit`** until a spec exists for the active slug and its plan is `reviewed`. Skipping straight to implementation is not possible — do the spec→plan steps first. `flo sdd spec` stamps the active slug so the gate knows which unit this run is building. One-off escape hatch: re-run with `--no-sdd`; per-project off-switch: `gates: sdd_gate: false`.
20
+ **The front half is enforced, not advisory.** When a run is armed for SDD (`-sd`/`--sdd` or `sdd.default`), the `check-before-implement` gate **blocks every source `Write`/`Edit`** until a spec exists for the active slug and its plan is `reviewed`. Skipping straight to implementation is not possible — do the spec→plan steps first. `flo sdd spec` stamps the active slug so the gate knows which unit this run is building. One-off escape hatch: re-run with `--no-sdd`; per-project off-switch: `gates: sdd_gate: false`.
21
21
 
22
22
  1. **Spec** — capture the *what* + acceptance criteria:
23
23
  ```bash
@@ -52,7 +52,7 @@ Artifacts live at `<specs_dir>/<slug>/{spec,plan}.md` — default `.moflo/specs`
52
52
 
53
53
  Runs at step 8 of the full-mode flow, before the PR — **by default** and always under `--sdd`; `--no-verify` skips it for one run.
54
54
 
55
- **Delegate to the `/verify` skill** — `Skill({ skill: "verify" })`, passing the issue number or spec slug. It owns the mechanics (single source of truth — don't restate them here): locate the acceptance criteria (plan, else ticket) → reuse the Tests-phase run (no double verify) → map each criterion to evidence → run only uncovered checks → record its own outcome to memory (`verify`, `verify:<slug-or-issue>`) → return a per-criterion PASS/FAIL. **The verdict is the point, not the invocation** (#1332). Invoking `/verify` trips `record-verify-run`, but that alone no longer opens `check-before-done` — the gate requires the recorded verdict to be `PASS`. A run returning FAIL leaves `gh pr create` blocked, which is the intended behaviour: the change did not meet its acceptance criteria. The verdict reaches the gate from the structured record `/verify` Step 5 writes to `memory_store` under `metadata.overall` (#1328), so a run that stores prose only is treated as *no verdict* and also blocks. Describing verification in prose has never satisfied the gate. A source edit after verifying invalidates both the flag and the verdict — re-run `/verify`. Full how-to: `.claude/skills/verify/SKILL.md`.
55
+ **Delegate to the `/verify` skill** — `Skill({ skill: "verify" })`, passing the issue number or spec slug. It owns the mechanics (single source of truth — don't restate them here): locate the acceptance criteria (plan, else ticket) → reuse the Tests-phase run (no double verify) → map each criterion to evidence → run only uncovered checks → record its own outcome to memory (`verify`, `verify:<slug-or-issue>`) → return a per-criterion PASS/FAIL. **The verdict is the point, not the invocation.** Invoking `/verify` trips `record-verify-run`, but that alone no longer opens `check-before-done` — the gate requires the recorded verdict to be `PASS`. A run returning FAIL leaves `gh pr create` blocked, which is the intended behaviour: the change did not meet its acceptance criteria. The verdict reaches the gate from the structured record `/verify` Step 5 writes to `memory_store` under `metadata.overall`, so a run that stores prose only is treated as *no verdict* and also blocks. Describing verification in prose has never satisfied the gate. A source edit after verifying invalidates both the flag and the verdict — re-run `/verify`. Full how-to: `.claude/skills/verify/SKILL.md`.
56
56
 
57
57
  `/ward` and `/quicken` stay targeted audits, not the completion gate.
58
58