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
|
@@ -10,6 +10,75 @@ import * as path from 'path';
|
|
|
10
10
|
import { LEGACY_SWARM_DIR, memoryDbCandidatePaths, mofloDir } from '../services/moflo-paths.js';
|
|
11
11
|
import { findProjectRoot } from '../services/project-root.js';
|
|
12
12
|
import { resolveStateRoot } from '../services/project-root.js';
|
|
13
|
+
/**
|
|
14
|
+
* Canonical path first, then the pre-#1168 `.swarm/state.json` fallback, so a
|
|
15
|
+
* consumer who initialised on an older moflo still resolves their swarm.
|
|
16
|
+
* Writes only ever go to `[0]`.
|
|
17
|
+
*/
|
|
18
|
+
function swarmStateFileCandidates() {
|
|
19
|
+
const root = findProjectRoot();
|
|
20
|
+
return [
|
|
21
|
+
path.join(mofloDir(root), 'swarm', 'state.json'),
|
|
22
|
+
path.join(root, LEGACY_SWARM_DIR, 'state.json'),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
function readSwarmState() {
|
|
26
|
+
for (const file of swarmStateFileCandidates()) {
|
|
27
|
+
if (!fs.existsSync(file))
|
|
28
|
+
continue;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// A malformed state file is treated as absent rather than fatal — the
|
|
34
|
+
// remedy is `flo swarm init`, which overwrites it.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function writeSwarmState(state) {
|
|
40
|
+
const [canonical] = swarmStateFileCandidates();
|
|
41
|
+
fs.mkdirSync(path.dirname(canonical), { recursive: true });
|
|
42
|
+
fs.writeFileSync(canonical, JSON.stringify(state, null, 2));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the recorded swarm, or print why it cannot be and return null —
|
|
46
|
+
* in which case the caller exits non-zero without doing anything.
|
|
47
|
+
*
|
|
48
|
+
* Shared by `stop` and `scale`: both must refuse an id that is not the active
|
|
49
|
+
* swarm rather than acting on it, which is half of what #1428 was about.
|
|
50
|
+
*/
|
|
51
|
+
function resolveActiveSwarm(swarmId, missingMessage) {
|
|
52
|
+
const existing = readSwarmState();
|
|
53
|
+
if (!existing) {
|
|
54
|
+
output.printError(missingMessage);
|
|
55
|
+
output.printInfo('Run "flo swarm status" to see what is recorded for this project.');
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (existing.id !== swarmId) {
|
|
59
|
+
output.printError(`Swarm ${swarmId} is not the active swarm.`);
|
|
60
|
+
output.printInfo(`Active swarm is ${existing.id} — run "flo swarm status" to confirm.`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return existing;
|
|
64
|
+
}
|
|
65
|
+
/** Remove both the canonical and legacy state files; returns true if any existed. */
|
|
66
|
+
function clearSwarmState() {
|
|
67
|
+
let removed = false;
|
|
68
|
+
for (const file of swarmStateFileCandidates()) {
|
|
69
|
+
if (!fs.existsSync(file))
|
|
70
|
+
continue;
|
|
71
|
+
try {
|
|
72
|
+
fs.rmSync(file, { force: true });
|
|
73
|
+
removed = true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Leave `removed` false — the caller reports the failure rather than
|
|
77
|
+
// claiming a stop that did not happen.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return removed;
|
|
81
|
+
}
|
|
13
82
|
// Get dynamic swarm status from memory/session files
|
|
14
83
|
function getSwarmStatus(swarmId) {
|
|
15
84
|
const projectRoot = findProjectRoot();
|
|
@@ -19,24 +88,11 @@ function getSwarmStatus(swarmId) {
|
|
|
19
88
|
// were removed — no current writer creates those directories, so they
|
|
20
89
|
// always produced 0 counts. The coordinator-backed MCP tools
|
|
21
90
|
// (agent_list / task_list) are the live source of truth.
|
|
22
|
-
const canonicalSwarmDir = path.join(mofloDir(projectRoot), 'swarm');
|
|
23
|
-
const legacySwarmDir = path.join(projectRoot, LEGACY_SWARM_DIR);
|
|
24
91
|
const sessionDir = path.join(process.cwd(), '.claude', 'sessions');
|
|
25
92
|
const memoryPaths = memoryDbCandidatePaths(resolveStateRoot());
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
swarmStateFile = path.join(legacySwarmDir, 'state.json');
|
|
30
|
-
}
|
|
31
|
-
let swarmState = null;
|
|
32
|
-
if (fs.existsSync(swarmStateFile)) {
|
|
33
|
-
try {
|
|
34
|
-
swarmState = JSON.parse(fs.readFileSync(swarmStateFile, 'utf-8'));
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
// Ignore parse errors
|
|
38
|
-
}
|
|
39
|
-
}
|
|
93
|
+
// Canonical first, then legacy — shared with init/start/stop so all four
|
|
94
|
+
// subcommands resolve the same file.
|
|
95
|
+
const swarmState = readSwarmState();
|
|
40
96
|
// agents/tasks counters: no file-store readers post-#1168. Coordinator
|
|
41
97
|
// MCP tools own the live counts; getSwarmStatus surfaces a static summary
|
|
42
98
|
// of the persisted state file plus session/memory rough indicators.
|
|
@@ -88,7 +144,10 @@ function getSwarmStatus(swarmId) {
|
|
|
88
144
|
status = 'completed';
|
|
89
145
|
}
|
|
90
146
|
else if (swarmState) {
|
|
91
|
-
|
|
147
|
+
// Honour what was recorded. `start` writes `running`; `init` writes
|
|
148
|
+
// `ready`. Pre-#1428 this collapsed both to `ready`, so a recorded
|
|
149
|
+
// objective was invisible here.
|
|
150
|
+
status = swarmState.status || 'ready';
|
|
92
151
|
}
|
|
93
152
|
return {
|
|
94
153
|
id: swarmId || swarmState?.id || 'no-active-swarm',
|
|
@@ -249,14 +308,9 @@ const initCommand = {
|
|
|
249
308
|
// Save swarm state locally for status command to read. Post-#1168 the
|
|
250
309
|
// canonical home is `<root>/.moflo/swarm/state.json`; the legacy
|
|
251
310
|
// `.swarm/state.json` path is preserved as a read-only fallback in
|
|
252
|
-
// `
|
|
253
|
-
const swarmDir = path.join(mofloDir(findProjectRoot()), 'swarm');
|
|
311
|
+
// `readSwarmState`.
|
|
254
312
|
try {
|
|
255
|
-
|
|
256
|
-
fs.mkdirSync(swarmDir, { recursive: true });
|
|
257
|
-
}
|
|
258
|
-
const stateFile = path.join(swarmDir, 'state.json');
|
|
259
|
-
fs.writeFileSync(stateFile, JSON.stringify({
|
|
313
|
+
writeSwarmState({
|
|
260
314
|
id: result.swarmId,
|
|
261
315
|
topology: result.topology,
|
|
262
316
|
maxAgents: result.config.maxAgents,
|
|
@@ -264,7 +318,7 @@ const initCommand = {
|
|
|
264
318
|
v3Mode,
|
|
265
319
|
initializedAt: result.initializedAt,
|
|
266
320
|
status: 'ready'
|
|
267
|
-
}
|
|
321
|
+
});
|
|
268
322
|
}
|
|
269
323
|
catch {
|
|
270
324
|
// Ignore errors writing state file
|
|
@@ -338,12 +392,22 @@ const startCommand = {
|
|
|
338
392
|
});
|
|
339
393
|
}
|
|
340
394
|
strategy = strategy || 'development';
|
|
395
|
+
// #1428 — refuse rather than narrate. Pre-fix this command printed "All
|
|
396
|
+
// agents deployed" and a `flo swarm status <id>` follow-up for an id it
|
|
397
|
+
// never persisted, so the advertised command reported either no active
|
|
398
|
+
// swarm or the state of an unrelated earlier `init`.
|
|
399
|
+
const existing = readSwarmState();
|
|
400
|
+
if (!existing) {
|
|
401
|
+
output.printError('No initialised swarm found.');
|
|
402
|
+
output.printInfo('Run "flo swarm init" first — it records the topology this objective attaches to.');
|
|
403
|
+
return { success: false, exitCode: 1 };
|
|
404
|
+
}
|
|
341
405
|
output.writeln();
|
|
342
406
|
output.printInfo(`Starting swarm with objective: ${output.highlight(objective)}`);
|
|
343
407
|
output.writeln();
|
|
344
408
|
// Compute agent deployment plan based on strategy
|
|
345
409
|
const agentPlan = getAgentPlan(strategy);
|
|
346
|
-
output.writeln(output.bold('Agent
|
|
410
|
+
output.writeln(output.bold('Planned Agent Roster'));
|
|
347
411
|
output.printTable({
|
|
348
412
|
columns: [
|
|
349
413
|
{ key: 'role', header: 'Role', width: 20 },
|
|
@@ -353,10 +417,11 @@ const startCommand = {
|
|
|
353
417
|
],
|
|
354
418
|
data: agentPlan
|
|
355
419
|
});
|
|
420
|
+
const plannedAgents = agentPlan.reduce((sum, a) => sum + a.count, 0);
|
|
356
421
|
// Confirm execution
|
|
357
422
|
if (ctx.interactive) {
|
|
358
423
|
const confirmed = await confirm({
|
|
359
|
-
message: `
|
|
424
|
+
message: `Record this objective for ${plannedAgents} agents?`,
|
|
360
425
|
default: true
|
|
361
426
|
});
|
|
362
427
|
if (!confirmed) {
|
|
@@ -364,26 +429,47 @@ const startCommand = {
|
|
|
364
429
|
return { success: true };
|
|
365
430
|
}
|
|
366
431
|
}
|
|
367
|
-
|
|
368
|
-
output.printInfo('Deploying agents...');
|
|
369
|
-
// Show deployment progress
|
|
370
|
-
const spinner = output.createSpinner({ text: 'Initializing agents...', spinner: 'dots' });
|
|
371
|
-
spinner.start();
|
|
372
|
-
// Brief delay for spinner animation
|
|
373
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
374
|
-
spinner.succeed('All agents deployed');
|
|
432
|
+
const startedAt = new Date().toISOString();
|
|
375
433
|
const executionState = {
|
|
376
|
-
|
|
434
|
+
// The persisted id, never a freshly-minted one — `flo swarm status`
|
|
435
|
+
// resolves ids out of this same file, so a local mint was guaranteed
|
|
436
|
+
// to be unresolvable.
|
|
437
|
+
swarmId: existing.id,
|
|
377
438
|
objective,
|
|
378
439
|
strategy,
|
|
379
440
|
status: 'running',
|
|
380
|
-
agents:
|
|
381
|
-
startedAt
|
|
441
|
+
agents: plannedAgents,
|
|
442
|
+
startedAt,
|
|
382
443
|
parallel: ctx.flags.parallel ?? true
|
|
383
444
|
};
|
|
445
|
+
try {
|
|
446
|
+
writeSwarmState({
|
|
447
|
+
...existing,
|
|
448
|
+
strategy,
|
|
449
|
+
objective,
|
|
450
|
+
plannedAgents,
|
|
451
|
+
startedAt,
|
|
452
|
+
status: 'running',
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
output.printError(`Failed to record objective: ${String(error)}`);
|
|
457
|
+
return { success: false, exitCode: 1 };
|
|
458
|
+
}
|
|
384
459
|
output.writeln();
|
|
385
|
-
output.printSuccess(
|
|
460
|
+
output.printSuccess(`Objective recorded against swarm ${executionState.swarmId}`);
|
|
386
461
|
output.writeln(output.dim(` Monitor: flo swarm status ${executionState.swarmId}`));
|
|
462
|
+
output.writeln();
|
|
463
|
+
// Say plainly what did NOT happen. The CLI reaches its MCP tools in-process
|
|
464
|
+
// (see SwarmStateFile above), so agents spawned here would not outlive this
|
|
465
|
+
// command — the long-lived coordinator is the one inside the MCP server.
|
|
466
|
+
output.printInfo('No agents were spawned by this command.');
|
|
467
|
+
output.writeln(output.dim(' The roster above is a plan. Agents run on the long-lived coordinator'));
|
|
468
|
+
output.writeln(output.dim(' in the MCP server — drive them from a Claude Code session with the'));
|
|
469
|
+
output.writeln(output.dim(' swarm_init / agent_spawn / task_orchestrate tools.'));
|
|
470
|
+
if (ctx.flags.format === 'json') {
|
|
471
|
+
output.printJson(executionState);
|
|
472
|
+
}
|
|
387
473
|
return { success: true, data: executionState };
|
|
388
474
|
}
|
|
389
475
|
};
|
|
@@ -491,9 +577,16 @@ const stopCommand = {
|
|
|
491
577
|
output.printInfo('Run "moflo swarm status" to find the active swarm ID');
|
|
492
578
|
return { success: false, exitCode: 1 };
|
|
493
579
|
}
|
|
580
|
+
// #1428 — `stop` used to print "Swarm <id> stopped" for any string at all,
|
|
581
|
+
// including an id that never existed, while leaving the state file in
|
|
582
|
+
// place. It is the counterpart to `start`: if it does not clear that file,
|
|
583
|
+
// `flo swarm status` reports the objective as running indefinitely.
|
|
584
|
+
const existing = resolveActiveSwarm(swarmId, 'No active swarm to stop.');
|
|
585
|
+
if (!existing)
|
|
586
|
+
return { success: false, exitCode: 1 };
|
|
494
587
|
if (ctx.interactive && !force) {
|
|
495
588
|
const confirmed = await confirm({
|
|
496
|
-
message: `Stop swarm ${swarmId}?
|
|
589
|
+
message: `Stop swarm ${swarmId}? Its recorded objective will be cleared.`,
|
|
497
590
|
default: false
|
|
498
591
|
});
|
|
499
592
|
if (!confirmed) {
|
|
@@ -502,13 +595,14 @@ const stopCommand = {
|
|
|
502
595
|
}
|
|
503
596
|
}
|
|
504
597
|
output.printInfo(`Stopping swarm ${swarmId}...`);
|
|
505
|
-
if (!
|
|
506
|
-
output.
|
|
507
|
-
|
|
508
|
-
output.writeln(output.dim(' Notifying agents...'));
|
|
509
|
-
output.writeln(output.dim(' Saving memory state...'));
|
|
598
|
+
if (!clearSwarmState()) {
|
|
599
|
+
output.printError(`Failed to clear swarm state for ${swarmId}.`);
|
|
600
|
+
return { success: false, exitCode: 1 };
|
|
510
601
|
}
|
|
511
602
|
output.printSuccess(`Swarm ${swarmId} stopped`);
|
|
603
|
+
// Only the recorded state is ours to clear — see SwarmStateFile.
|
|
604
|
+
output.writeln(output.dim(' Cleared the recorded swarm state. Agents running on the MCP'));
|
|
605
|
+
output.writeln(output.dim(' coordinator are unaffected — terminate those with agent_terminate.'));
|
|
512
606
|
return { success: true, data: { swarmId, stopped: true, force } };
|
|
513
607
|
}
|
|
514
608
|
};
|
|
@@ -544,88 +638,106 @@ const scaleCommand = {
|
|
|
544
638
|
output.printError('Target agent count required. Use --agents or -a');
|
|
545
639
|
return { success: false, exitCode: 1 };
|
|
546
640
|
}
|
|
547
|
-
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
641
|
+
// #1428 — same treatment as `start`, for the same structural reason. The
|
|
642
|
+
// wired `swarm_scale` MCP tool really does call coordinator.spawnAgent /
|
|
643
|
+
// terminateAgent, but calling it from here would drive a coordinator that
|
|
644
|
+
// dies with this process: the agents would be spawned and lost between one
|
|
645
|
+
// command and the next. Pre-fix this computed a delta against a hardcoded
|
|
646
|
+
// `currentAgents = 8` and announced "Swarm scaled to N agents".
|
|
647
|
+
const existing = resolveActiveSwarm(swarmId, 'No initialised swarm found.');
|
|
648
|
+
if (!existing)
|
|
649
|
+
return { success: false, exitCode: 1 };
|
|
650
|
+
// The recorded ceiling, which is a real number `init` wrote — not a guess.
|
|
651
|
+
const previous = existing.maxAgents;
|
|
652
|
+
if (previous === targetAgents) {
|
|
653
|
+
output.printInfo(`Recorded agent ceiling is already ${targetAgents}.`);
|
|
654
|
+
return { success: true, data: { swarmId, agents: targetAgents, changed: false } };
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
writeSwarmState({ ...existing, maxAgents: targetAgents });
|
|
553
658
|
}
|
|
554
|
-
|
|
555
|
-
output.
|
|
659
|
+
catch (error) {
|
|
660
|
+
output.printError(`Failed to record agent ceiling: ${String(error)}`);
|
|
661
|
+
return { success: false, exitCode: 1 };
|
|
556
662
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
663
|
+
output.printSuccess(`Recorded agent ceiling for ${swarmId}: ${previous ?? 'unset'} → ${targetAgents}`);
|
|
664
|
+
output.writeln();
|
|
665
|
+
output.printInfo('No agents were spawned or terminated by this command.');
|
|
666
|
+
output.writeln(output.dim(' This records the ceiling a later swarm_init will use. To scale a'));
|
|
667
|
+
output.writeln(output.dim(' running swarm, call the swarm_scale MCP tool from a Claude Code'));
|
|
668
|
+
output.writeln(output.dim(' session — that reaches the live coordinator, which this CLI cannot.'));
|
|
669
|
+
if (agentType) {
|
|
670
|
+
output.writeln(output.dim(` --type ${agentType} applies only to live scaling via swarm_scale.`));
|
|
560
671
|
}
|
|
561
|
-
|
|
562
|
-
|
|
672
|
+
return {
|
|
673
|
+
success: true,
|
|
674
|
+
data: { swarmId, agents: targetAgents, previousAgents: previous, changed: true },
|
|
675
|
+
};
|
|
563
676
|
}
|
|
564
677
|
};
|
|
565
|
-
|
|
678
|
+
/**
|
|
679
|
+
* The V3 roster `--v3-mode` is designed around. This is reference data, not
|
|
680
|
+
* observed state: no agent here is running, and nothing reads it back.
|
|
681
|
+
*
|
|
682
|
+
* #1428 — it used to carry a per-agent `status` of `primary`/`active`/
|
|
683
|
+
* `standby`, rendered in colour, which read as live telemetry for agents that
|
|
684
|
+
* did not exist. The roster itself is real (the topology it describes is what
|
|
685
|
+
* `swarm init --v3-mode` actually selects), so the fix is to drop the invented
|
|
686
|
+
* status column rather than the table.
|
|
687
|
+
*/
|
|
688
|
+
const V3_ROSTER = [
|
|
689
|
+
{ id: 1, role: 'Queen Coordinator', domain: 'Orchestration' },
|
|
690
|
+
{ id: 2, role: 'Security Architect', domain: 'Security' },
|
|
691
|
+
{ id: 3, role: 'Security Auditor', domain: 'Security' },
|
|
692
|
+
{ id: 4, role: 'Test Architect', domain: 'Security' },
|
|
693
|
+
{ id: 5, role: 'Core Architect', domain: 'Core' },
|
|
694
|
+
{ id: 6, role: 'Memory Specialist', domain: 'Core' },
|
|
695
|
+
{ id: 7, role: 'Swarm Specialist', domain: 'Core' },
|
|
696
|
+
{ id: 8, role: 'Integration Architect', domain: 'Integration' },
|
|
697
|
+
{ id: 9, role: 'Performance Engineer', domain: 'Integration' },
|
|
698
|
+
{ id: 10, role: 'CLI Developer', domain: 'Integration' },
|
|
699
|
+
{ id: 11, role: 'Hooks Developer', domain: 'Integration' },
|
|
700
|
+
{ id: 12, role: 'MCP Specialist', domain: 'Integration' },
|
|
701
|
+
{ id: 13, role: 'Project Coordinator', domain: 'Management' },
|
|
702
|
+
{ id: 14, role: 'Documentation Lead', domain: 'Management' },
|
|
703
|
+
{ id: 15, role: 'DevOps Engineer', domain: 'Management' },
|
|
704
|
+
];
|
|
705
|
+
// Reference display for the V3 roster. Named `coordinate` for backward
|
|
706
|
+
// compatibility — it coordinates nothing, and no longer claims to.
|
|
566
707
|
const coordinateCommand = {
|
|
567
708
|
name: 'coordinate',
|
|
568
|
-
description: '
|
|
709
|
+
description: 'Show the V3 15-agent hierarchical-mesh roster (reference only — starts nothing)',
|
|
569
710
|
options: [
|
|
570
711
|
{
|
|
571
712
|
name: 'agents',
|
|
572
|
-
description: 'Number of
|
|
713
|
+
description: 'Number of roster entries to show',
|
|
573
714
|
type: 'number',
|
|
574
715
|
default: 15
|
|
575
|
-
},
|
|
576
|
-
{
|
|
577
|
-
name: 'domains',
|
|
578
|
-
description: 'Domains to activate',
|
|
579
|
-
type: 'array'
|
|
580
716
|
}
|
|
581
717
|
],
|
|
582
718
|
action: async (ctx) => {
|
|
583
719
|
const agentCount = ctx.flags.agents || 15;
|
|
720
|
+
const roster = V3_ROSTER.slice(0, agentCount);
|
|
584
721
|
output.writeln();
|
|
585
|
-
output.writeln(output.bold('V3
|
|
722
|
+
output.writeln(output.bold('V3 Hierarchical-Mesh Roster (reference)'));
|
|
723
|
+
output.writeln(output.dim(' The layout `flo swarm init --v3-mode` is designed around.'));
|
|
586
724
|
output.writeln();
|
|
587
|
-
// V3 agent structure
|
|
588
|
-
const v3Agents = [
|
|
589
|
-
{ id: 1, role: 'Queen Coordinator', domain: 'Orchestration', status: 'primary' },
|
|
590
|
-
{ id: 2, role: 'Security Architect', domain: 'Security', status: 'active' },
|
|
591
|
-
{ id: 3, role: 'Security Auditor', domain: 'Security', status: 'active' },
|
|
592
|
-
{ id: 4, role: 'Test Architect', domain: 'Security', status: 'active' },
|
|
593
|
-
{ id: 5, role: 'Core Architect', domain: 'Core', status: 'active' },
|
|
594
|
-
{ id: 6, role: 'Memory Specialist', domain: 'Core', status: 'active' },
|
|
595
|
-
{ id: 7, role: 'Swarm Specialist', domain: 'Core', status: 'active' },
|
|
596
|
-
{ id: 8, role: 'Integration Architect', domain: 'Integration', status: 'active' },
|
|
597
|
-
{ id: 9, role: 'Performance Engineer', domain: 'Integration', status: 'active' },
|
|
598
|
-
{ id: 10, role: 'CLI Developer', domain: 'Integration', status: 'active' },
|
|
599
|
-
{ id: 11, role: 'Hooks Developer', domain: 'Integration', status: 'active' },
|
|
600
|
-
{ id: 12, role: 'MCP Specialist', domain: 'Integration', status: 'active' },
|
|
601
|
-
{ id: 13, role: 'Project Coordinator', domain: 'Management', status: 'active' },
|
|
602
|
-
{ id: 14, role: 'Documentation Lead', domain: 'Management', status: 'standby' },
|
|
603
|
-
{ id: 15, role: 'DevOps Engineer', domain: 'Management', status: 'standby' }
|
|
604
|
-
].slice(0, agentCount);
|
|
605
725
|
output.printTable({
|
|
606
726
|
columns: [
|
|
607
727
|
{ key: 'id', header: '#', width: 3, align: 'right' },
|
|
608
728
|
{ key: 'role', header: 'Role', width: 22 },
|
|
609
|
-
{ key: 'domain', header: 'Domain', width: 15 }
|
|
610
|
-
{ key: 'status', header: 'Status', width: 10, format: (v) => {
|
|
611
|
-
if (v === 'primary')
|
|
612
|
-
return output.highlight(String(v));
|
|
613
|
-
if (v === 'active')
|
|
614
|
-
return output.success(String(v));
|
|
615
|
-
return output.dim(String(v));
|
|
616
|
-
} }
|
|
729
|
+
{ key: 'domain', header: 'Domain', width: 15 }
|
|
617
730
|
],
|
|
618
|
-
data:
|
|
731
|
+
data: roster
|
|
619
732
|
});
|
|
620
733
|
output.writeln();
|
|
621
|
-
output.printInfo('
|
|
622
|
-
output.
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return { success: true, data: { agents: v3Agents, count: agentCount } };
|
|
734
|
+
output.printInfo('No agents were started, and none are running as a result of this command.');
|
|
735
|
+
output.writeln(output.dim(' Initialise the topology with: flo swarm init --v3-mode'));
|
|
736
|
+
output.writeln(output.dim(' Spawn real agents from a Claude Code session with agent_spawn.'));
|
|
737
|
+
if (ctx.flags.format === 'json') {
|
|
738
|
+
output.printJson({ roster, count: roster.length });
|
|
739
|
+
}
|
|
740
|
+
return { success: true, data: { agents: roster, count: roster.length } };
|
|
629
741
|
}
|
|
630
742
|
};
|
|
631
743
|
// Main swarm command
|
|
@@ -637,7 +749,7 @@ export const swarmCommand = {
|
|
|
637
749
|
examples: [
|
|
638
750
|
{ command: 'flo swarm init --v3-mode', description: 'Initialize V3 swarm' },
|
|
639
751
|
{ command: 'flo swarm start -o "Build API" -s development', description: 'Start development swarm' },
|
|
640
|
-
{ command: 'flo swarm coordinate --agents 15', description: 'V3
|
|
752
|
+
{ command: 'flo swarm coordinate --agents 15', description: 'Show the V3 roster (reference)' }
|
|
641
753
|
],
|
|
642
754
|
action: async (ctx) => {
|
|
643
755
|
output.writeln();
|
|
@@ -652,7 +764,7 @@ export const swarmCommand = {
|
|
|
652
764
|
`${output.highlight('status')} - Show swarm status`,
|
|
653
765
|
`${output.highlight('stop')} - Stop swarm execution`,
|
|
654
766
|
`${output.highlight('scale')} - Scale swarm agent count`,
|
|
655
|
-
`${output.highlight('coordinate')} - V3 15-agent
|
|
767
|
+
`${output.highlight('coordinate')} - Show the V3 15-agent roster (reference)`
|
|
656
768
|
]);
|
|
657
769
|
return { success: true };
|
|
658
770
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @module moflo/cli/guidance/headless
|
|
15
15
|
*/
|
|
16
|
+
import { escapeShellArg, NULL_DEVICE } from '../shared/utils/platform.js';
|
|
16
17
|
/**
|
|
17
18
|
* Default command executor using child_process
|
|
18
19
|
*/
|
|
@@ -162,14 +163,10 @@ export class HeadlessRunner {
|
|
|
162
163
|
* Build the Claude Code headless command
|
|
163
164
|
*/
|
|
164
165
|
buildCommand(task) {
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return `claude -p "${escapedPrompt}" --output-format json 2>${nd}`;
|
|
170
|
-
}
|
|
171
|
-
const escapedPrompt = task.prompt.replace(/'/g, "'\\''");
|
|
172
|
-
return `claude -p '${escapedPrompt}' --output-format json 2>${nd}`;
|
|
166
|
+
// Quoting and the null device both come from platform.ts — this used to
|
|
167
|
+
// branch on process.platform twice and hand-roll the Windows escape, which
|
|
168
|
+
// is where the trailing-backslash defect lived (#1419).
|
|
169
|
+
return `claude -p ${escapeShellArg(task.prompt)} --output-format json 2>${NULL_DEVICE}`;
|
|
173
170
|
}
|
|
174
171
|
/**
|
|
175
172
|
* Parse Claude Code JSON output
|
|
@@ -303,12 +303,7 @@ export class ShardRetriever {
|
|
|
303
303
|
matchGlob(path, glob) {
|
|
304
304
|
let re = this.globCache.get(glob);
|
|
305
305
|
if (!re) {
|
|
306
|
-
|
|
307
|
-
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
308
|
-
.replace(/\*/g, '[^/]*')
|
|
309
|
-
.replace(/{{GLOBSTAR}}/g, '.*')
|
|
310
|
-
.replace(/\//g, '\\/');
|
|
311
|
-
re = new RegExp(`^${pattern}$`);
|
|
306
|
+
re = globToRegExp(glob);
|
|
312
307
|
this.globCache.set(glob, re);
|
|
313
308
|
}
|
|
314
309
|
return re.test(path);
|
|
@@ -348,4 +343,30 @@ export class ShardRetriever {
|
|
|
348
343
|
export function createRetriever(embeddingProvider) {
|
|
349
344
|
return new ShardRetriever(embeddingProvider);
|
|
350
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Compile a repo-scope glob (`*`, `**`) into an anchored regex.
|
|
348
|
+
*
|
|
349
|
+
* Metacharacters are escaped FIRST, then the wildcards are substituted. Without
|
|
350
|
+
* the escape a glob containing `.`, `+`, `(` or `[` either matched the wrong
|
|
351
|
+
* thing — `docs/*.md` happily matching `docsXmd`, because the `.` was a
|
|
352
|
+
* wildcard — or threw from `new RegExp` on an unbalanced bracket. Guidance
|
|
353
|
+
* scopes carry `.md` routinely, so the silent-mismatch case was the live one
|
|
354
|
+
* (#1419).
|
|
355
|
+
*
|
|
356
|
+
* `*` is left unescaped so the substitutions can still see it. The
|
|
357
|
+
* `{{GLOBSTAR}}` placeholder cannot collide with a literal `{{GLOBSTAR}}` in the
|
|
358
|
+
* input because that input has already had its braces escaped to `\{\{…\}\}` by
|
|
359
|
+
* the time the placeholder is inserted.
|
|
360
|
+
*
|
|
361
|
+
* Exported for direct testing: the only caller is a private method reachable
|
|
362
|
+
* solely through a fully-loaded retriever.
|
|
363
|
+
*/
|
|
364
|
+
export function globToRegExp(glob) {
|
|
365
|
+
const pattern = glob
|
|
366
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
367
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
368
|
+
.replace(/\*/g, '[^/]*')
|
|
369
|
+
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
|
370
|
+
return new RegExp(`^${pattern}$`);
|
|
371
|
+
}
|
|
351
372
|
//# sourceMappingURL=retriever.js.map
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module moflo/cli/guidance/ruvbot-integration
|
|
18
18
|
*/
|
|
19
|
+
import { generateId } from '../shared/utils/id.js';
|
|
19
20
|
// ============================================================================
|
|
20
21
|
// Severity Mapping
|
|
21
22
|
// ============================================================================
|
|
@@ -587,7 +588,7 @@ export class RuvBotGuidanceBridge {
|
|
|
587
588
|
*/
|
|
588
589
|
async handleSessionCreate(...args) {
|
|
589
590
|
const data = (args[0] ?? {});
|
|
590
|
-
const sessionId = String(data['sessionId'] ?? data['id'] ??
|
|
591
|
+
const sessionId = String(data['sessionId'] ?? data['id'] ?? generateId('session'));
|
|
591
592
|
this.logEvent('session:create', { sessionId });
|
|
592
593
|
if (this.config.enableProofChain) {
|
|
593
594
|
if (!this.config.proofSigningKey) {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* @module moflo/cli/guidance/uncertainty
|
|
25
25
|
*/
|
|
26
26
|
import { randomUUID } from 'node:crypto';
|
|
27
|
+
import { generateId } from '../shared/utils/id.js';
|
|
27
28
|
// ============================================================================
|
|
28
29
|
// Default Configuration
|
|
29
30
|
// ============================================================================
|
|
@@ -294,7 +295,7 @@ export class UncertaintyLedger {
|
|
|
294
295
|
}
|
|
295
296
|
// Record the resolution as evidence
|
|
296
297
|
const resolutionEvidence = {
|
|
297
|
-
sourceId: `resolution:${beliefId}
|
|
298
|
+
sourceId: generateId(`resolution:${beliefId}`, { separator: ':' }),
|
|
298
299
|
sourceType: 'human-input',
|
|
299
300
|
supports: status === 'confirmed',
|
|
300
301
|
weight: 1.0,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @module v3/hooks/bridge/official-hooks-bridge
|
|
9
9
|
*/
|
|
10
10
|
import { HookEvent } from '../types.js';
|
|
11
|
+
import { generateId } from '../../shared/utils/id.js';
|
|
11
12
|
/**
|
|
12
13
|
* Mapping from V3 HookEvent to Official hook events
|
|
13
14
|
*/
|
|
@@ -100,7 +101,7 @@ export class OfficialHooksBridge {
|
|
|
100
101
|
// Add task information for Task tool
|
|
101
102
|
if (input.tool_name === 'Task') {
|
|
102
103
|
context.task = {
|
|
103
|
-
id:
|
|
104
|
+
id: generateId('task'),
|
|
104
105
|
description: input.tool_input?.prompt ?? '',
|
|
105
106
|
agent: input.tool_input?.subagent_type,
|
|
106
107
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* MCP tool definitions for hooks system integration.
|
|
5
5
|
* These tools provide programmatic access to hooks functionality.
|
|
6
6
|
*/
|
|
7
|
+
import { generateId } from '../../shared/utils/id.js';
|
|
7
8
|
/**
|
|
8
9
|
* Pre-edit hook MCP tool
|
|
9
10
|
*/
|
|
@@ -110,7 +111,7 @@ export const postEditTool = {
|
|
|
110
111
|
success,
|
|
111
112
|
recorded: true,
|
|
112
113
|
recordedAt: new Date().toISOString(),
|
|
113
|
-
patternId: success ?
|
|
114
|
+
patternId: success ? generateId('pattern') : undefined,
|
|
114
115
|
};
|
|
115
116
|
},
|
|
116
117
|
};
|
|
@@ -314,7 +315,7 @@ export const postCommandTool = {
|
|
|
314
315
|
const success = input.success;
|
|
315
316
|
return {
|
|
316
317
|
recorded: true,
|
|
317
|
-
patternId: success ?
|
|
318
|
+
patternId: success ? generateId('cmd') : undefined,
|
|
318
319
|
};
|
|
319
320
|
},
|
|
320
321
|
};
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { EventEmitter } from 'node:events';
|
|
16
16
|
import { TestDeterministicEmbedding } from './__mocks__/test-embedding-service.js';
|
|
17
17
|
import { MEMORY_DB_FILE, MOFLO_DIR } from '../../services/moflo-paths.js';
|
|
18
|
+
import { generateId } from '../../shared/utils/id.js';
|
|
18
19
|
// Dynamic imports for optional dependencies
|
|
19
20
|
let MofloDbAdapter = null;
|
|
20
21
|
let HNSWIndex = null;
|
|
@@ -244,7 +245,7 @@ export class ReasoningBank extends EventEmitter {
|
|
|
244
245
|
}
|
|
245
246
|
// Create new pattern
|
|
246
247
|
const pattern = {
|
|
247
|
-
id:
|
|
248
|
+
id: generateId('pat', { separator: '_' }),
|
|
248
249
|
strategy,
|
|
249
250
|
domain,
|
|
250
251
|
embedding,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Central registry for hook registration, management, and lookup.
|
|
5
5
|
* Provides priority-based execution ordering and filtering.
|
|
6
6
|
*/
|
|
7
|
+
import { generateId } from '../../shared/utils/id.js';
|
|
7
8
|
/**
|
|
8
9
|
* Hook Registry - manages hook registration and lookup
|
|
9
10
|
*/
|
|
@@ -189,7 +190,7 @@ export class HookRegistry {
|
|
|
189
190
|
* Generate unique hook ID
|
|
190
191
|
*/
|
|
191
192
|
generateId() {
|
|
192
|
-
return
|
|
193
|
+
return generateId('hook');
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
/**
|