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.
- package/.claude/guidance/shipped/moflo-agent-rules.md +1 -0
- package/.claude/guidance/shipped/moflo-claude-swarm-cohesion.md +1 -1
- package/.claude/guidance/shipped/moflo-cli-reference.md +4 -4
- package/.claude/guidance/shipped/moflo-core-guidance.md +4 -4
- package/.claude/guidance/shipped/moflo-guidance-rules.md +21 -0
- package/.claude/guidance/shipped/moflo-inline-fixes.md +98 -0
- package/.claude/guidance/shipped/moflo-root-cause-discipline.md +10 -10
- package/.claude/guidance/shipped/moflo-sdd.md +5 -5
- package/.claude/guidance/shipped/moflo-spell-engine.md +1 -1
- package/.claude/guidance/shipped/moflo-yaml-reference.md +8 -8
- package/.claude/helpers/gate.cjs +46 -7
- package/.claude/helpers/pr-create-command.cjs +440 -0
- package/.claude/skills/eldar/SKILL.md +1 -1
- package/.claude/skills/fl/SKILL.md +6 -6
- package/.claude/skills/fl/execution-modes.md +3 -3
- package/.claude/skills/fl/phases.md +8 -8
- package/.claude/skills/fl/sdd.md +6 -6
- package/.claude/skills/guidance/SKILL.md +3 -0
- package/.claude/skills/verify/SKILL.md +4 -4
- package/bin/gate.cjs +46 -7
- package/bin/hooks.mjs +4 -1
- package/bin/lib/retired-files.mjs +38 -6
- package/bin/lib/shipped-scripts.json +2 -1
- package/bin/lib/skill-categories.mjs +81 -17
- package/bin/pr-create-command.cjs +440 -0
- package/bin/session-start-launcher.mjs +11 -6
- package/dist/src/cli/aidefence/domain/entities/threat.js +2 -1
- package/dist/src/cli/commands/doctor-checks-deep.js +3 -2
- package/dist/src/cli/commands/doctor-checks-memory-access.js +3 -2
- package/dist/src/cli/commands/doctor-checks-swarm.js +3 -2
- package/dist/src/cli/commands/doctor-fixes.js +119 -20
- package/dist/src/cli/commands/hooks.js +2 -1
- package/dist/src/cli/commands/mcp.js +3 -2
- package/dist/src/cli/commands/memory.js +2 -1
- package/dist/src/cli/commands/performance.js +2 -1
- package/dist/src/cli/commands/retire.js +1 -1
- package/dist/src/cli/commands/spell-schedule.js +2 -1
- package/dist/src/cli/commands/swarm.js +216 -104
- package/dist/src/cli/guidance/headless.js +5 -8
- package/dist/src/cli/guidance/retriever.js +27 -6
- package/dist/src/cli/guidance/ruvbot-integration.js +2 -1
- package/dist/src/cli/guidance/uncertainty.js +2 -1
- package/dist/src/cli/hooks/bridge/official-hooks-bridge.js +2 -1
- package/dist/src/cli/hooks/mcp/index.js +3 -2
- package/dist/src/cli/hooks/reasoningbank/index.js +2 -1
- package/dist/src/cli/hooks/registry/index.js +2 -1
- package/dist/src/cli/hooks/swarm/index.js +6 -5
- package/dist/src/cli/init/claudemd-generator.js +2 -1
- package/dist/src/cli/init/helpers-generator.js +1 -1
- package/dist/src/cli/init/moflo-yaml-template.js +4 -4
- package/dist/src/cli/mcp-client.js +5 -2
- package/dist/src/cli/mcp-tools/agent-tools.js +4 -1
- package/dist/src/cli/mcp-tools/hive-mind-tools.js +4 -3
- package/dist/src/cli/mcp-tools/hooks-tools.js +6 -5
- package/dist/src/cli/mcp-tools/json-store.js +3 -2
- package/dist/src/cli/mcp-tools/neural-tools.js +2 -1
- package/dist/src/cli/mcp-tools/performance-tools.js +2 -1
- package/dist/src/cli/mcp-tools/session-tools.js +4 -2
- package/dist/src/cli/mcp-tools/spell-tools.js +2 -1
- package/dist/src/cli/memory/bridge-core.js +8 -2
- package/dist/src/cli/memory/controllers/_shared.js +8 -2
- package/dist/src/cli/memory/controllers/nightly-learner.js +2 -1
- package/dist/src/cli/memory/domain/services/memory-domain-service.js +2 -1
- package/dist/src/cli/memory/entries-write.js +2 -1
- package/dist/src/cli/memory/intelligence.js +2 -1
- package/dist/src/cli/memory/persistent-sona.js +1 -3
- package/dist/src/cli/memory/types.js +2 -3
- package/dist/src/cli/memory/verify.js +3 -2
- package/dist/src/cli/movector/graph-analyzer.js +14 -3
- package/dist/src/cli/neural/pattern-learner.js +4 -2
- package/dist/src/cli/neural/reasoning-bank.js +2 -1
- package/dist/src/cli/neural/reasoningbank-adapter.js +2 -1
- package/dist/src/cli/neural/sona-manager.js +4 -3
- package/dist/src/cli/production/monitoring.js +2 -1
- package/dist/src/cli/services/daemon-service.js +57 -46
- package/dist/src/cli/services/daemon-spell-executor.js +3 -2
- package/dist/src/cli/services/headless-worker-executor.js +4 -2
- package/dist/src/cli/services/learning-service.js +2 -1
- package/dist/src/cli/services/project-root.js +17 -1
- package/dist/src/cli/services/worker-daemon.js +2 -1
- package/dist/src/cli/shared/hooks/example-usage.js +3 -2
- package/dist/src/cli/shared/hooks/safety/git-commit.js +11 -2
- package/dist/src/cli/shared/hooks/session-hooks.js +3 -2
- package/dist/src/cli/shared/hooks/task-hooks.js +2 -1
- package/dist/src/cli/shared/mcp/session-manager.js +2 -1
- package/dist/src/cli/shared/plugins/official/hive-mind-plugin.js +2 -1
- package/dist/src/cli/shared/plugins/official/maestro-plugin.js +3 -2
- package/dist/src/cli/shared/security/index.js +6 -2
- package/dist/src/cli/shared/utils/atomic-file-write.js +9 -1
- package/dist/src/cli/shared/utils/id.js +54 -0
- package/dist/src/cli/shared/utils/platform.js +36 -5
- package/dist/src/cli/spells/commands/destructive-pattern-checker.js +6 -1
- package/dist/src/cli/spells/core/dry-run-validator.js +2 -1
- package/dist/src/cli/spells/core/interpolation.js +8 -3
- package/dist/src/cli/spells/core/permission-resolver.js +2 -2
- package/dist/src/cli/spells/core/runner.js +2 -1
- package/dist/src/cli/spells/core/shell.js +7 -8
- package/dist/src/cli/spells/factory/runner-bridge.js +3 -2
- package/dist/src/cli/spells/factory/runner-factory.js +3 -2
- package/dist/src/cli/spells/scheduler/scheduler.js +2 -1
- package/dist/src/cli/swarm/consensus/gossip.js +2 -1
- package/dist/src/cli/swarm/federation-hub.js +5 -4
- package/dist/src/cli/swarm/queen-coordinator.js +5 -4
- package/dist/src/cli/swarm/shared/events.js +2 -1
- package/dist/src/cli/swarm/unified-coordinator.js +3 -2
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/retired-files.json +0 -72
- package/dist/src/cli/mcp-tools/coordination-tools.js +0 -86
- 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
|
|
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
|
|
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**
|
|
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**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 —
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/.claude/skills/fl/sdd.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SDD & Verification (`-sd` / `--sdd`, `-v` / `--verify`)
|
|
2
2
|
|
|
3
|
-
Spec-Driven Development for `/flo
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|