moflo 4.12.4 → 4.12.5
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/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/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 };
|
|
@@ -1334,13 +1334,18 @@ try {
|
|
|
1334
1334
|
// prune when the consumer's file matches a known-shipped hash —
|
|
1335
1335
|
// customized files (different hash) get preserved with a one-line
|
|
1336
1336
|
// notice the user can act on.
|
|
1337
|
+
//
|
|
1338
|
+
// The package root is passed so entries can be cross-checked against
|
|
1339
|
+
// what moflo actually ships (#1414). A path can be retired and later
|
|
1340
|
+
// RESTORED under the same name; the stale entry then carries only
|
|
1341
|
+
// pre-deletion hashes, so the copy Mechanism A just synced matches
|
|
1342
|
+
// nothing and gets reported as a "customized retired file" on every
|
|
1343
|
+
// session — inviting the user to delete an agent the CLI still routes to.
|
|
1337
1344
|
let prunedRetiredPaths = new Set();
|
|
1338
1345
|
try {
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
);
|
|
1343
|
-
const report = applyRetiredPrune(projectRoot, retiredManifestPath);
|
|
1346
|
+
const mofloPackageRoot = resolve(projectRoot, 'node_modules', 'moflo');
|
|
1347
|
+
const retiredManifestPath = resolve(mofloPackageRoot, 'retired-files.json');
|
|
1348
|
+
const report = applyRetiredPrune(projectRoot, retiredManifestPath, mofloPackageRoot);
|
|
1344
1349
|
prunedRetiredPaths = new Set(report.pruned);
|
|
1345
1350
|
if (report.pruned.length > 0) {
|
|
1346
1351
|
emitMutation(
|
|
@@ -1365,7 +1370,7 @@ try {
|
|
|
1365
1370
|
if (report.preservedDetails.length > 0) {
|
|
1366
1371
|
try {
|
|
1367
1372
|
recordVersion = JSON.parse(readFileSync(
|
|
1368
|
-
resolve(
|
|
1373
|
+
resolve(mofloPackageRoot, 'package.json'), 'utf-8',
|
|
1369
1374
|
)).version;
|
|
1370
1375
|
} catch { /* record just omits the version */ }
|
|
1371
1376
|
}
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Represents a detected security threat from AI manipulation attempts.
|
|
5
5
|
*/
|
|
6
|
+
import { generateId } from '../../../shared/utils/id.js';
|
|
6
7
|
/**
|
|
7
8
|
* Factory function to create a Threat entity
|
|
8
9
|
*/
|
|
9
10
|
export function createThreat(params) {
|
|
10
11
|
return {
|
|
11
|
-
id:
|
|
12
|
+
id: generateId('threat'),
|
|
12
13
|
type: params.type,
|
|
13
14
|
severity: params.severity,
|
|
14
15
|
confidence: params.confidence,
|
|
@@ -19,6 +19,7 @@ import { pathToFileURL } from 'url';
|
|
|
19
19
|
import { findProjectRoot as findConsumerProjectDir } from '../services/project-root.js';
|
|
20
20
|
import { findMofloPackageRoot } from '../services/moflo-require.js';
|
|
21
21
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
22
|
+
import { generateId } from '../shared/utils/id.js';
|
|
22
23
|
// ============================================================================
|
|
23
24
|
// Path Resolution
|
|
24
25
|
// ============================================================================
|
|
@@ -75,7 +76,7 @@ export async function checkSubagentHealth() {
|
|
|
75
76
|
// Spawn a test agent
|
|
76
77
|
const spawnResult = await spawnTool.handler({
|
|
77
78
|
agentType: 'tester',
|
|
78
|
-
id:
|
|
79
|
+
id: generateId('doctor-probe'),
|
|
79
80
|
priority: 'low',
|
|
80
81
|
metadata: { purpose: 'doctor-health-check' },
|
|
81
82
|
}, {});
|
|
@@ -149,7 +150,7 @@ steps:
|
|
|
149
150
|
output: result
|
|
150
151
|
`;
|
|
151
152
|
const result = await runSpellFromContent(minimalSpell, 'doctor-probe.yaml', {
|
|
152
|
-
spellId:
|
|
153
|
+
spellId: generateId('doctor-probe'),
|
|
153
154
|
timeout: 10_000,
|
|
154
155
|
});
|
|
155
156
|
if (!result) {
|
|
@@ -28,6 +28,7 @@ import { memoryDbPath } from '../services/moflo-paths.js';
|
|
|
28
28
|
import { findProjectRoot } from '../services/project-root.js';
|
|
29
29
|
import { purgeMemoryProbeNamespaces } from '../services/ephemeral-namespace-purge.js';
|
|
30
30
|
import { loadToolArrays, getTool, pushDetail, summarizeFunctional, } from './doctor-checks-functional-shared.js';
|
|
31
|
+
import { randomSuffix } from '../shared/utils/id.js';
|
|
31
32
|
const MEMORY_ACCESS_CHECK = 'Memory Access Functional';
|
|
32
33
|
const MEMORY_ACCESS_FAIL_FIX = 'Run `flo doctor --json` for per-subcheck details. Common fixes: ensure fastembed installed (memory_store.hasEmbedding=false), explicit threshold:0 honored (#837), or rebuild HNSW index (`flo memory rebuild-index`)';
|
|
33
34
|
async function invokeOrThrow(tools, name, input) {
|
|
@@ -49,7 +50,7 @@ export async function _runMemoryRoundTripForTest(ctx) {
|
|
|
49
50
|
return runMemoryRoundTrip(ctx);
|
|
50
51
|
}
|
|
51
52
|
async function runMemoryRoundTrip(ctx) {
|
|
52
|
-
const stamp = `${Date.now()}-${
|
|
53
|
+
const stamp = `${Date.now()}-${randomSuffix()}`;
|
|
53
54
|
const key = `doctor-memprobe-${ctx.persona}-${stamp}`;
|
|
54
55
|
const namespace = `doctor-memprobe-${ctx.persona}`;
|
|
55
56
|
const sentinel = `memprobe-${ctx.persona}-${stamp}`;
|
|
@@ -286,7 +287,7 @@ async function probeMemoryGetNeighbors(memoryTools, details) {
|
|
|
286
287
|
observed: { registered: true },
|
|
287
288
|
expected: 'memory_get_neighbors registered',
|
|
288
289
|
});
|
|
289
|
-
const stamp = `${Date.now()}-${
|
|
290
|
+
const stamp = `${Date.now()}-${randomSuffix()}`;
|
|
290
291
|
const namespace = `doctor-neighbors-${stamp}`;
|
|
291
292
|
const prefix = `chunk-doctor-neighbors-${stamp}`;
|
|
292
293
|
const chunkKeys = [`${prefix}-0`, `${prefix}-1`, `${prefix}-2`];
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
15
15
|
import { loadToolArrays, getTool, safeInvoke, summarizeFunctional, } from './doctor-checks-functional-shared.js';
|
|
16
|
+
import { generateId } from '../shared/utils/id.js';
|
|
16
17
|
const SWARM_FUNCTIONAL_CHECK = 'Swarm Functional';
|
|
17
18
|
const HIVE_FUNCTIONAL_CHECK = 'Hive-Mind Functional';
|
|
18
19
|
const SWARM_FAIL_FIX = 'Run `flo doctor --json` for per-subcheck details; investigate any handler that disconnected from UnifiedSwarmCoordinator (epic #798)';
|
|
@@ -450,8 +451,8 @@ export async function checkHiveMindFunctional() {
|
|
|
450
451
|
// 6. hive-mind_memory roundtrips via Memory DB write-through.
|
|
451
452
|
// Memory backend unavailable (e.g. fresh smoke fixture before `memory init`)
|
|
452
453
|
// is environmental, not a regression — softFailMessage downgrades to warn.
|
|
453
|
-
const probeKey =
|
|
454
|
-
const sentinel =
|
|
454
|
+
const probeKey = generateId('doctor-probe');
|
|
455
|
+
const sentinel = generateId('hive-doctor');
|
|
455
456
|
const setOut = await safeInvoke(hiveMindTools, 'hive-mind_memory', {
|
|
456
457
|
action: 'set', key: probeKey, value: { sentinel },
|
|
457
458
|
}, details, {
|
|
@@ -11,6 +11,7 @@ import { errorDetail } from '../shared/utils/error-detail.js';
|
|
|
11
11
|
// #1315 — the intelligence/maturity probes below read `.moflo/` state; anchor
|
|
12
12
|
// them at the resolved root so a subdirectory invocation doesn't report zeroes.
|
|
13
13
|
import { resolveStateRoot } from '../services/project-root.js';
|
|
14
|
+
import { generateId } from '../shared/utils/id.js';
|
|
14
15
|
// Hook types
|
|
15
16
|
const HOOK_TYPES = [
|
|
16
17
|
{ value: 'pre-edit', label: 'Pre-Edit', hint: 'Get context before editing files' },
|
|
@@ -1204,7 +1205,7 @@ const postTaskCommand = {
|
|
|
1204
1205
|
],
|
|
1205
1206
|
action: async (ctx) => {
|
|
1206
1207
|
// Auto-generate task ID if not provided
|
|
1207
|
-
const taskId = ctx.flags.taskId ||
|
|
1208
|
+
const taskId = ctx.flags.taskId || generateId('task', { separator: '_' });
|
|
1208
1209
|
// Default success to true for backward compatibility
|
|
1209
1210
|
const success = ctx.flags.success !== undefined ? ctx.flags.success : true;
|
|
1210
1211
|
output.printInfo(`Recording outcome for task: ${output.highlight(taskId)}`);
|
|
@@ -11,6 +11,7 @@ import { getServerManager, getMCPServerStatus, } from '../mcp-server.js';
|
|
|
11
11
|
import { listMCPTools, callMCPTool, hasTool } from '../mcp-client.js';
|
|
12
12
|
import { acquireDaemonLock, releaseDaemonLock, getDaemonLockHolder } from '../services/daemon-lock.js';
|
|
13
13
|
import { readMofloEnv } from '../services/env-compat.js';
|
|
14
|
+
import { generateId } from '../shared/utils/id.js';
|
|
14
15
|
// MCP tool categories offered by `flo mcp tools --category`.
|
|
15
16
|
//
|
|
16
17
|
// #1353 removed the `github` entry along with the four tools that were the
|
|
@@ -494,8 +495,8 @@ const execCommand = {
|
|
|
494
495
|
}
|
|
495
496
|
const startTime = performance.now();
|
|
496
497
|
const result = await callMCPTool(tool, params, {
|
|
497
|
-
sessionId:
|
|
498
|
-
requestId:
|
|
498
|
+
sessionId: generateId('cli', { base36Time: true }),
|
|
499
|
+
requestId: generateId('exec'),
|
|
499
500
|
});
|
|
500
501
|
const duration = performance.now() - startTime;
|
|
501
502
|
output.writeln();
|
|
@@ -13,6 +13,7 @@ import { errorDetail } from '../shared/utils/error-detail.js';
|
|
|
13
13
|
import { memoryDbPath } from '../services/moflo-paths.js';
|
|
14
14
|
import { resolveBridgeDbPath } from '../memory/bridge-core.js';
|
|
15
15
|
import { findProjectRoot } from '../services/project-root.js';
|
|
16
|
+
import { generateId } from '../shared/utils/id.js';
|
|
16
17
|
// Memory backends
|
|
17
18
|
const BACKENDS = [
|
|
18
19
|
{ value: 'agentdb', label: 'AgentDB', hint: 'Vector database with HNSW approximate-nearest-neighbor (ANN) indexing' },
|
|
@@ -1327,7 +1328,7 @@ function saveAndCloseDb(db, _dbPath) {
|
|
|
1327
1328
|
db.close();
|
|
1328
1329
|
}
|
|
1329
1330
|
function batchGenerateId() {
|
|
1330
|
-
return
|
|
1331
|
+
return generateId('mem', { separator: '_' });
|
|
1331
1332
|
}
|
|
1332
1333
|
function batchStoreEntry(db, key, namespace, content, metadata = {}, tags = [], embedding, embeddingModel, embeddingDimensions) {
|
|
1333
1334
|
const now = Date.now();
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { output } from '../output.js';
|
|
8
8
|
import { readLoadAverage, NOT_MEASURED } from '../shared/utils/load-average.js';
|
|
9
|
+
import { generateId } from '../shared/utils/id.js';
|
|
9
10
|
// Benchmark subcommand - REAL measurements
|
|
10
11
|
const benchmarkCommand = {
|
|
11
12
|
name: 'benchmark',
|
|
@@ -165,7 +166,7 @@ const benchmarkCommand = {
|
|
|
165
166
|
for (let i = 0; i < Math.min(iterations, 20); i++) {
|
|
166
167
|
const start = performance.now();
|
|
167
168
|
await storeEntry({
|
|
168
|
-
key:
|
|
169
|
+
key: generateId('bench', { separator: '_' }),
|
|
169
170
|
value: `Benchmark test entry ${i} with some content for testing storage performance`,
|
|
170
171
|
namespace: 'benchmark',
|
|
171
172
|
generateEmbeddingFlag: true,
|
|
@@ -57,7 +57,7 @@ export const retireCommand = {
|
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
name: 'rebuild-hashes',
|
|
60
|
-
description: 'Recompute knownContentHashes[] for every
|
|
60
|
+
description: 'Recompute knownContentHashes[] for every entry from full git history, and drop entries whose path is back in the tree (#1133 backfill, #1414 heal)',
|
|
61
61
|
type: 'boolean',
|
|
62
62
|
default: false,
|
|
63
63
|
},
|
|
@@ -19,6 +19,7 @@ import { checkScheduleAcceptance } from '../services/schedule-acceptance-check.j
|
|
|
19
19
|
import { reconcileDaemonAutostart } from '../services/daemon-autostart-lifecycle.js';
|
|
20
20
|
import { isDaemonInstalled } from '../services/daemon-service.js';
|
|
21
21
|
import { validateSchedule, computeNextRun } from '../spells/scheduler/cron-parser.js';
|
|
22
|
+
import { randomSuffix } from '../shared/utils/id.js';
|
|
22
23
|
const NAMESPACE_SCHEDULES = 'scheduled-spells';
|
|
23
24
|
const NAMESPACE_EXECUTIONS = 'schedule-executions';
|
|
24
25
|
const DEFAULT_EXECUTIONS_LIMIT = 10;
|
|
@@ -135,7 +136,7 @@ const createCommand = {
|
|
|
135
136
|
output.printWarning(acceptance.message);
|
|
136
137
|
}
|
|
137
138
|
// Always create the schedule, regardless of daemon state
|
|
138
|
-
const id = `sched-adhoc-${now}-${
|
|
139
|
+
const id = `sched-adhoc-${now}-${randomSuffix()}`;
|
|
139
140
|
const record = {
|
|
140
141
|
id,
|
|
141
142
|
spellName: name,
|