moflo 4.9.0-rc.13 → 4.9.0-rc.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/statusline.cjs +66 -23
- package/.claude/helpers/subagent-bootstrap.json +3 -0
- package/.claude/helpers/subagent-start.cjs +58 -22
- package/.claude/skills/swarm-advanced/SKILL.md +77 -0
- package/README.md +9 -6
- package/bin/session-start-launcher.mjs +22 -20
- package/dist/src/cli/commands/doctor-checks-deep.js +6 -3
- package/dist/src/cli/commands/doctor-checks-swarm.js +433 -0
- package/dist/src/cli/commands/doctor.js +92 -18
- package/dist/src/cli/commands/status.js +3 -3
- package/dist/src/cli/embeddings/fastembed-inline/model-loader.js +27 -5
- package/dist/src/cli/init/executor.js +18 -18
- package/dist/src/cli/init/moflo-init.js +20 -12
- package/dist/src/cli/mcp-tools/agent-tools.js +332 -211
- package/dist/src/cli/mcp-tools/coordinator-views.js +23 -0
- package/dist/src/cli/mcp-tools/github-tools.js +2 -1
- package/dist/src/cli/mcp-tools/hive-mind-tools.js +145 -49
- package/dist/src/cli/mcp-tools/hooks-tools.js +3 -2
- package/dist/src/cli/mcp-tools/json-store.js +9 -6
- package/dist/src/cli/mcp-tools/neural-tools.js +2 -1
- package/dist/src/cli/mcp-tools/session-tools.js +11 -7
- package/dist/src/cli/mcp-tools/swarm-coordinator-singleton.js +119 -0
- package/dist/src/cli/mcp-tools/swarm-scale-handler.js +211 -0
- package/dist/src/cli/mcp-tools/swarm-tools.js +208 -27
- package/dist/src/cli/mcp-tools/task-tools.js +299 -166
- package/dist/src/cli/services/index.js +2 -0
- package/dist/src/cli/services/subagent-bootstrap.js +57 -0
- package/dist/src/cli/spells/core/platform-sandbox.js +80 -59
- package/dist/src/cli/spells/core/prerequisite-checker.js +8 -3
- package/dist/src/cli/spells/core/runner.js +1 -1
- package/dist/src/cli/swarm/swarm-persistence.js +144 -0
- package/dist/src/cli/swarm/unified-coordinator.js +260 -66
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -382,7 +382,7 @@ function getSwarmStatus() {
|
|
|
382
382
|
function getSystemMetrics() {
|
|
383
383
|
const memoryMB = Math.floor(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
384
384
|
const learning = getLearningStats();
|
|
385
|
-
const
|
|
385
|
+
const embeddings = getEmbeddingsStats();
|
|
386
386
|
|
|
387
387
|
// Intelligence from learning.json
|
|
388
388
|
const learningData = readJSON(path.join(CWD, '.moflo', 'metrics', 'learning.json'));
|
|
@@ -393,7 +393,7 @@ function getSystemMetrics() {
|
|
|
393
393
|
intelligencePct = Math.min(100, Math.floor(learningData.intelligence.score));
|
|
394
394
|
} else {
|
|
395
395
|
const fromPatterns = learning.patterns > 0 ? Math.min(100, Math.floor(learning.patterns / 10)) : 0;
|
|
396
|
-
const fromVectors =
|
|
396
|
+
const fromVectors = embeddings.vectorCount > 0 ? Math.min(100, Math.floor(embeddings.vectorCount / 100)) : 0;
|
|
397
397
|
intelligencePct = Math.max(fromPatterns, fromVectors);
|
|
398
398
|
}
|
|
399
399
|
|
|
@@ -423,7 +423,7 @@ function getSystemMetrics() {
|
|
|
423
423
|
subAgents = activityData.processes.estimated_agents;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
-
return { memoryMB, contextPct, intelligencePct, subAgents };
|
|
426
|
+
return { memoryMB, contextPct, intelligencePct, subAgents, embeddings };
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
// ADR status (count files only — don't read contents)
|
|
@@ -484,9 +484,9 @@ function getHooksStatus() {
|
|
|
484
484
|
return { enabled, total };
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
//
|
|
487
|
+
// Embeddings stats — reads from cache file written by embedding/memory ops.
|
|
488
488
|
// No subprocess spawning. Falls back to DB file size estimate if cache is missing.
|
|
489
|
-
function
|
|
489
|
+
function getEmbeddingsStats() {
|
|
490
490
|
let vectorCount = 0;
|
|
491
491
|
let dbSizeKB = 0;
|
|
492
492
|
let namespaces = 0;
|
|
@@ -601,20 +601,25 @@ function getIntegrationStatus() {
|
|
|
601
601
|
return { mcpServers, hasDatabase, hasApi };
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
// Upgrade notice (#636, #738, #743) — written by the session-start launcher
|
|
605
|
-
//
|
|
606
|
-
// work
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
604
|
+
// Upgrade notice (#636, #738, #743) — written by the session-start launcher.
|
|
605
|
+
// status='in-progress' — work is running; rendered with "(updating…)".
|
|
606
|
+
// status='completed' — work just finished; short-TTL post-upgrade badge so
|
|
607
|
+
// the user sees something on the very next render
|
|
608
|
+
// (Claude Code only paints the statusline AFTER the
|
|
609
|
+
// SessionStart hook returns, so the in-progress badge
|
|
610
|
+
// has effectively zero visibility window).
|
|
611
|
+
// Anything else is dropped (legacy "complete" pre-#738 files, zombie writes,
|
|
612
|
+
// future writer mistakes) so a stale notice can never turn the segment into a
|
|
613
|
+
// permanent column. Section 0-pre of the launcher also wipes any leftover at
|
|
614
|
+
// session start as a second line of defence.
|
|
611
615
|
function getUpgradeNotice() {
|
|
612
616
|
const data = readJSON(path.join(CWD, '.moflo', 'upgrade-notice.json'));
|
|
613
617
|
if (!data || typeof data !== 'object') return null;
|
|
614
|
-
if (data.status !== 'in-progress') return null;
|
|
618
|
+
if (data.status !== 'in-progress' && data.status !== 'completed') return null;
|
|
615
619
|
const expiresAt = data.expiresAt ? new Date(data.expiresAt).getTime() : 0;
|
|
616
620
|
if (!expiresAt || Date.now() > expiresAt) return null;
|
|
617
621
|
return {
|
|
622
|
+
status: data.status,
|
|
618
623
|
kind: data.kind === 'repair' ? 'repair' : 'upgrade',
|
|
619
624
|
from: typeof data.from === 'string' ? data.from : '',
|
|
620
625
|
to: typeof data.to === 'string' ? data.to : '',
|
|
@@ -623,14 +628,20 @@ function getUpgradeNotice() {
|
|
|
623
628
|
|
|
624
629
|
function formatUpgradeNoticeSegment(notice) {
|
|
625
630
|
if (!notice) return '';
|
|
626
|
-
const
|
|
631
|
+
const inFlight = notice.status === 'in-progress';
|
|
632
|
+
const suffix = inFlight ? ` ${c.dim}(updating…)${c.reset}` : '';
|
|
633
|
+
// Pick body text: repair > in-flight version range > completed "upgraded to"
|
|
634
|
+
// > bare "upgraded" fallback when no version is known.
|
|
635
|
+
let body;
|
|
627
636
|
if (notice.kind === 'repair') {
|
|
628
|
-
|
|
637
|
+
body = 'install repaired';
|
|
638
|
+
} else if (inFlight) {
|
|
639
|
+
body = notice.from && notice.to ? `${notice.from} → ${notice.to}` : (notice.to || 'upgraded');
|
|
640
|
+
} else {
|
|
641
|
+
const target = notice.to || notice.from || '';
|
|
642
|
+
body = target ? `upgraded to ${target}` : 'upgraded';
|
|
629
643
|
}
|
|
630
|
-
|
|
631
|
-
? `${notice.from} → ${notice.to}`
|
|
632
|
-
: (notice.to || 'upgraded');
|
|
633
|
-
return `${c.brightYellow}📦 ${versions}${c.reset}${suffix}`;
|
|
644
|
+
return `${c.brightYellow}📦 ${body}${c.reset}${suffix}`;
|
|
634
645
|
}
|
|
635
646
|
|
|
636
647
|
// Session stats (pure file reads)
|
|
@@ -784,6 +795,25 @@ function generateDashboard() {
|
|
|
784
795
|
);
|
|
785
796
|
}
|
|
786
797
|
|
|
798
|
+
// Embeddings line \u2014 vector store stats from .moflo/vector-stats.json.
|
|
799
|
+
// Reuses `system.embeddings` (already computed by getSystemMetrics()) instead
|
|
800
|
+
// of re-probing the cache file on every render.
|
|
801
|
+
{
|
|
802
|
+
const vec = system.embeddings;
|
|
803
|
+
if (vec.vectorCount > 0) {
|
|
804
|
+
const hnswInd = vec.hasHnsw ? `${c.brightGreen}\u26A1${c.reset}` : '';
|
|
805
|
+
const sizeDisp = vec.dbSizeKB >= 1024 ? `${(vec.dbSizeKB / 1024).toFixed(1)}MB` : `${vec.dbSizeKB}KB`;
|
|
806
|
+
const eParts = [
|
|
807
|
+
`${c.cyan}Vectors${c.reset} ${c.brightGreen}\u25CF${vec.vectorCount}${c.reset}${hnswInd}`,
|
|
808
|
+
`${c.cyan}Size${c.reset} ${c.brightWhite}${sizeDisp}${c.reset}`,
|
|
809
|
+
];
|
|
810
|
+
if (vec.namespaces > 0) {
|
|
811
|
+
eParts.push(`${c.cyan}NS${c.reset} ${c.brightWhite}${vec.namespaces}${c.reset}`);
|
|
812
|
+
}
|
|
813
|
+
lines.push(`${c.brightCyan}\uD83D\uDCCA Embeddings${c.reset} ${eParts.join(` ${c.dim}\u2502${c.reset} `)}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
787
817
|
// MCP line
|
|
788
818
|
if (SL_CONFIG.show_mcp) {
|
|
789
819
|
const parts = [];
|
|
@@ -795,7 +825,7 @@ function generateDashboard() {
|
|
|
795
825
|
}
|
|
796
826
|
if (integration.hasDatabase) parts.push(`${c.brightGreen}\u25C6${c.reset}DB`);
|
|
797
827
|
if (parts.length > 0) {
|
|
798
|
-
lines.push(`${c.brightCyan}\uD83D\
|
|
828
|
+
lines.push(`${c.brightCyan}\uD83D\uDD0C MCP${c.reset} ${parts.join(` ${c.dim}\u2502${c.reset} `)}`);
|
|
799
829
|
}
|
|
800
830
|
}
|
|
801
831
|
|
|
@@ -835,7 +865,7 @@ function generateCompactDashboard() {
|
|
|
835
865
|
pushUpgradeNoticeSegment(lines);
|
|
836
866
|
lines.push(header);
|
|
837
867
|
|
|
838
|
-
// Combined swarm + mcp line
|
|
868
|
+
// Combined swarm + embeddings + mcp line
|
|
839
869
|
const segments = [];
|
|
840
870
|
if (SL_CONFIG.show_swarm) {
|
|
841
871
|
const swarm = getSwarmStatus();
|
|
@@ -845,6 +875,18 @@ function generateCompactDashboard() {
|
|
|
845
875
|
`${c.brightYellow}\uD83E\uDD16${c.reset} ${swarmInd}[${agentsColor}${swarm.activeAgents}${c.reset}/${c.brightWhite}${swarm.maxAgents}${c.reset}]`
|
|
846
876
|
);
|
|
847
877
|
}
|
|
878
|
+
// Embeddings \u2014 always-on when vectorCount > 0; self-hides on a fresh install.
|
|
879
|
+
// Compact doesn't call getSystemMetrics() so this is the only probe per render.
|
|
880
|
+
{
|
|
881
|
+
const vec = getEmbeddingsStats();
|
|
882
|
+
if (vec.vectorCount > 0) {
|
|
883
|
+
const hnswInd = vec.hasHnsw ? '\u26A1' : '';
|
|
884
|
+
const sizeDisp = vec.dbSizeKB >= 1024 ? `${(vec.dbSizeKB / 1024).toFixed(1)}MB` : `${vec.dbSizeKB}KB`;
|
|
885
|
+
segments.push(
|
|
886
|
+
`${c.brightCyan}\uD83D\uDCCA${c.reset} ${c.brightGreen}${vec.vectorCount}${hnswInd}${c.reset} ${c.dim}(${sizeDisp})${c.reset}`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
848
890
|
if (SL_CONFIG.show_mcp) {
|
|
849
891
|
const integration = getIntegrationStatus();
|
|
850
892
|
if (integration.mcpServers.total > 0) {
|
|
@@ -863,15 +905,16 @@ function generateCompactDashboard() {
|
|
|
863
905
|
// JSON output
|
|
864
906
|
function generateJSON() {
|
|
865
907
|
const git = getGitInfo();
|
|
908
|
+
const system = getSystemMetrics();
|
|
866
909
|
return {
|
|
867
910
|
user: { name: git.name, gitBranch: git.gitBranch, modelName: getModelName() },
|
|
868
911
|
v3Progress: getV3Progress(),
|
|
869
912
|
security: getSecurityStatus(),
|
|
870
913
|
swarm: getSwarmStatus(),
|
|
871
|
-
system
|
|
914
|
+
system,
|
|
872
915
|
adrs: getADRStatus(),
|
|
873
916
|
hooks: getHooksStatus(),
|
|
874
|
-
|
|
917
|
+
embeddings: system.embeddings,
|
|
875
918
|
tests: getTestStats(),
|
|
876
919
|
git: { modified: git.modified, untracked: git.untracked, staged: git.staged, ahead: git.ahead, behind: git.behind },
|
|
877
920
|
upgradeNotice: getUpgradeNotice(),
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{
|
|
2
|
+
"directive": "MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/shipped/moflo-subagents.md` protocol."
|
|
3
|
+
}
|
|
@@ -1,22 +1,58 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* SubagentStart Hook — injects a directive into every subagent's context
|
|
4
|
-
* telling it to read the subagent protocol guidance before doing any work.
|
|
5
|
-
*
|
|
6
|
-
* Output format: JSON with additionalContext (Claude Code hook protocol).
|
|
7
|
-
* Exit 0 = allow (SubagentStart cannot block).
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SubagentStart Hook — injects a directive into every subagent's context
|
|
4
|
+
* telling it to read the subagent protocol guidance before doing any work.
|
|
5
|
+
*
|
|
6
|
+
* Output format: JSON with additionalContext (Claude Code hook protocol).
|
|
7
|
+
* Exit 0 = allow (SubagentStart cannot block).
|
|
8
|
+
*
|
|
9
|
+
* Source of truth: ./subagent-bootstrap.json (sibling). The TS export at
|
|
10
|
+
* `src/cli/services/subagent-bootstrap.ts` reads the same file so future
|
|
11
|
+
* agent_spawn surfaces (epic #798 stories 3 + 9) inject byte-identical text.
|
|
12
|
+
*
|
|
13
|
+
* Inline FALLBACK keeps the hook functional if the JSON sibling is ever
|
|
14
|
+
* missing — a SubagentStart that emits nothing leaves the memory-first gate
|
|
15
|
+
* un-announced and silently regresses subagent behavior.
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// Defense-in-depth copy of the canonical directive in subagent-bootstrap.json.
|
|
23
|
+
// Kept as a single-line literal so the parity test in tests/bin/subagent-start.test.ts
|
|
24
|
+
// can verify it matches the JSON via plain substring containment.
|
|
25
|
+
const FALLBACK_DIRECTIVE = 'MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/shipped/moflo-subagents.md` protocol.';
|
|
26
|
+
|
|
27
|
+
function loadDirective() {
|
|
28
|
+
const jsonPath = path.join(__dirname, 'subagent-bootstrap.json');
|
|
29
|
+
let raw;
|
|
30
|
+
try {
|
|
31
|
+
raw = fs.readFileSync(jsonPath, 'utf8');
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err && err.code !== 'ENOENT') {
|
|
34
|
+
process.stderr.write(`[subagent-start] read failed: ${err.message} — using inline fallback\n`);
|
|
35
|
+
}
|
|
36
|
+
return FALLBACK_DIRECTIVE;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const data = JSON.parse(raw);
|
|
40
|
+
if (typeof data.directive === 'string' && data.directive.length > 0) {
|
|
41
|
+
return data.directive;
|
|
42
|
+
}
|
|
43
|
+
process.stderr.write('[subagent-start] subagent-bootstrap.json missing string `directive` — using inline fallback\n');
|
|
44
|
+
} catch (err) {
|
|
45
|
+
process.stderr.write(`[subagent-start] subagent-bootstrap.json parse failed: ${err.message} — using inline fallback\n`);
|
|
46
|
+
}
|
|
47
|
+
return FALLBACK_DIRECTIVE;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const output = {
|
|
51
|
+
hookSpecificOutput: {
|
|
52
|
+
hookEventName: 'SubagentStart',
|
|
53
|
+
additionalContext: loadDirective(),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
process.stdout.write(JSON.stringify(output));
|
|
58
|
+
process.exit(0);
|
|
@@ -33,6 +33,83 @@ mcp__moflo__agent_spawn({ type: "researcher", name: "swarm-advanced" })
|
|
|
33
33
|
// 3. Orchestrate tasks
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
### Dynamic Scaling
|
|
37
|
+
Use `mcp__moflo__swarm_scale` to grow or shrink the agent pool to a target size
|
|
38
|
+
without re-initializing the swarm. Three strategies are available:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
// Burst-spawn 8 workers all at once (load test, big batch)
|
|
42
|
+
mcp__moflo__swarm_scale({
|
|
43
|
+
targetAgents: 8,
|
|
44
|
+
scaleStrategy: "immediate",
|
|
45
|
+
agentTypes: ["worker"],
|
|
46
|
+
reason: "load-test ramp"
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Rate-limited ramp (1 agent / 200ms) — gentler on the coordinator
|
|
50
|
+
mcp__moflo__swarm_scale({
|
|
51
|
+
targetAgents: 12,
|
|
52
|
+
scaleStrategy: "gradual",
|
|
53
|
+
agentTypes: ["coder", "tester"]
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Adaptive: chunks scale with current coordinator load
|
|
57
|
+
mcp__moflo__swarm_scale({ targetAgents: 4, scaleStrategy: "adaptive" })
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The tool returns `{ previousAgents, currentAgents, scalingStatus, addedAgents,
|
|
61
|
+
removedAgents }` so callers can verify the swarm reached the target. Scale-down
|
|
62
|
+
prefers idle agents first, then oldest by heartbeat.
|
|
63
|
+
|
|
64
|
+
### Task Orchestration
|
|
65
|
+
|
|
66
|
+
The `task_*` family talks to the same UnifiedSwarmCoordinator that
|
|
67
|
+
`swarm_init` / `agent_spawn` use, so tasks created here flow through the
|
|
68
|
+
same scoring scheduler that load-balances across idle agents.
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
// Single task — coordinator picks the lowest-workload agent automatically
|
|
72
|
+
mcp__moflo__task_create({
|
|
73
|
+
type: "coding",
|
|
74
|
+
description: "Implement OAuth refresh flow",
|
|
75
|
+
priority: "high"
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Direct dispatch to a known agent (skip the scheduler)
|
|
79
|
+
mcp__moflo__task_assign({
|
|
80
|
+
taskId: "task_swarm-…_3",
|
|
81
|
+
agentId: "agent-coder-…"
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Domain-routed dispatch (queen / security / core / integration / support)
|
|
85
|
+
mcp__moflo__task_assign({
|
|
86
|
+
taskId: "task_swarm-…_4",
|
|
87
|
+
domain: "security"
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Submit a batch — load-balanced across available agents in one call.
|
|
91
|
+
// 5 tasks across 3 idle agents → no agent ends up with more than 2.
|
|
92
|
+
mcp__moflo__task_orchestrate({
|
|
93
|
+
tasks: [
|
|
94
|
+
{ type: "coding", description: "endpoint A" },
|
|
95
|
+
{ type: "coding", description: "endpoint B" },
|
|
96
|
+
{ type: "testing", description: "tests for A" },
|
|
97
|
+
{ type: "testing", description: "tests for B" },
|
|
98
|
+
{ type: "review", description: "PR review" }
|
|
99
|
+
]
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Mark a task done and record its outcome
|
|
103
|
+
mcp__moflo__task_complete({
|
|
104
|
+
taskId: "task_swarm-…_3",
|
|
105
|
+
result: { ok: true, summary: "merged in PR #842" }
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`task_orchestrate` returns `{ submitted, assigned, queued, rejected, tasks,
|
|
110
|
+
errors }` — `assigned` is the count whose agents accepted them on submit;
|
|
111
|
+
the rest are queued and will be picked up as agents go idle.
|
|
112
|
+
|
|
36
113
|
## Core Concepts
|
|
37
114
|
|
|
38
115
|
### Swarm Topologies
|
package/README.md
CHANGED
|
@@ -293,16 +293,16 @@ For simple epics with independent stories, `/flo <epic>` is all you need. For co
|
|
|
293
293
|
`flo epic` is the robust epic runner — it adds persistent state, resume from failure, and per-story auto-merge on top of `/flo`. It takes a GitHub epic issue number:
|
|
294
294
|
|
|
295
295
|
```bash
|
|
296
|
-
flo epic
|
|
297
|
-
flo epic
|
|
298
|
-
flo epic
|
|
296
|
+
flo epic 42 # Fetch epic #42, run all stories sequentially
|
|
297
|
+
flo epic 42 --dry-run # Preview execution plan without running
|
|
298
|
+
flo epic 42 --strategy auto-merge # Per-story PRs with auto-merge between stories
|
|
299
299
|
flo epic status 42 # Check progress (which stories passed/failed)
|
|
300
300
|
flo epic reset 42 # Reset state for re-run
|
|
301
301
|
```
|
|
302
302
|
|
|
303
|
-
`flo epic` fetches the epic from GitHub, extracts child stories from checklists, numbered references, and `## Stories` / `## Tasks` sections, then runs each through `/flo` with state tracking. If a story fails, you can fix the issue and `flo epic
|
|
303
|
+
`flo epic` fetches the epic from GitHub, extracts child stories from checklists, numbered references, and `## Stories` / `## Tasks` sections, then runs each through `/flo` with state tracking. If a story fails, you can fix the issue and re-run `flo epic 42` — it resumes from where it left off, skipping already-passed stories. (`flo epic run 42` is an explicit alias for the same shorthand.)
|
|
304
304
|
|
|
305
|
-
| | `/flo <epic>` | `flo epic
|
|
305
|
+
| | `/flo <epic>` | `flo epic <epic>` |
|
|
306
306
|
|---|---|---|
|
|
307
307
|
| **State tracking** | No | Yes (`epic-state` memory namespace) |
|
|
308
308
|
| **Resume from failure** | No | Yes (skips passed stories) |
|
|
@@ -583,7 +583,7 @@ flo --version # Show version
|
|
|
583
583
|
|
|
584
584
|
### Hooks (enabled OOTB)
|
|
585
585
|
|
|
586
|
-
Hooks are shell commands that Claude Code runs automatically at specific points in its lifecycle. MoFlo installs
|
|
586
|
+
Hooks are shell commands that Claude Code runs automatically at specific points in its lifecycle. MoFlo installs 23 hook bindings across 8 lifecycle events. You don't invoke these — they fire automatically.
|
|
587
587
|
|
|
588
588
|
| Hook Event | What fires | What it does | Enabled OOTB |
|
|
589
589
|
|------------|-----------|-------------|:---:|
|
|
@@ -593,9 +593,12 @@ Hooks are shell commands that Claude Code runs automatically at specific points
|
|
|
593
593
|
| **PreToolUse: Bash** | `flo gate check-dangerous-command` | Safety check on shell commands | Yes |
|
|
594
594
|
| **PreToolUse: Bash** | `flo gate check-before-pr` | Validates PR readiness before `gh pr create` | Yes |
|
|
595
595
|
| **PostToolUse: Write/Edit** | `flo hooks post-edit` | Records edit outcome, optionally trains neural patterns | Yes |
|
|
596
|
+
| **PostToolUse: Write/Edit** | `flo gate reset-edit-gates` | Resets edit-related gate state after the write completes | Yes |
|
|
596
597
|
| **PostToolUse: Agent** | `flo hooks post-task` | Records task completion, feeds outcome into routing learner | Yes |
|
|
597
598
|
| **PostToolUse: TaskCreate** | `flo gate record-task-created` | Records that a task was registered (clears TaskCreate gate) | Yes |
|
|
598
599
|
| **PostToolUse: Bash** | `flo gate check-bash-memory` | Detects memory search commands in Bash (clears memory gate) | Yes |
|
|
600
|
+
| **PostToolUse: Bash** | `flo gate record-test-run` | Records test runs from Bash for the test-output gate | Yes |
|
|
601
|
+
| **PostToolUse: Skill** | `flo gate record-skill-run` | Records that a skill was invoked (clears skill-related gates) | Yes |
|
|
599
602
|
| **PostToolUse: memory_search** | `flo gate record-memory-searched` | Records that memory was searched (clears memory-first gate) | Yes |
|
|
600
603
|
| **PostToolUse: TaskUpdate** | `flo gate check-task-transition` | Validates task state transitions (prevents skipping states) | Yes |
|
|
601
604
|
| **PostToolUse: memory_store** | `flo gate record-learnings-stored` | Records that learnings were persisted to memory | Yes |
|
|
@@ -63,35 +63,36 @@ let upgradeNoticeContext = null;
|
|
|
63
63
|
let pendingVersionStampWrite = null;
|
|
64
64
|
|
|
65
65
|
// 5-min TTL is a safety net for zombie launchers (statusline ignores past-TTL
|
|
66
|
-
// files). The
|
|
67
|
-
//
|
|
66
|
+
// files). The 2-min "completed" TTL lets the user see the post-upgrade badge
|
|
67
|
+
// briefly in the next session render (Claude Code renders the statusline only
|
|
68
|
+
// AFTER the SessionStart hook returns, so the in-progress badge has effectively
|
|
69
|
+
// zero visibility window). The next session-start's section 0-pre wipes any
|
|
70
|
+
// leftover, so a stale completed notice can't linger past one session.
|
|
68
71
|
const UPGRADE_NOTICE_INPROGRESS_TTL_MS = 5 * 60 * 1000;
|
|
72
|
+
const UPGRADE_NOTICE_COMPLETED_TTL_MS = 2 * 60 * 1000;
|
|
69
73
|
const UPGRADE_NOTICE_PATH = () => join(mofloDir(projectRoot), 'upgrade-notice.json');
|
|
70
74
|
|
|
71
|
-
function
|
|
75
|
+
function writeUpgradeNotice(status) {
|
|
72
76
|
if (!upgradeNoticeContext) return;
|
|
77
|
+
const ttlMs = status === 'completed'
|
|
78
|
+
? UPGRADE_NOTICE_COMPLETED_TTL_MS
|
|
79
|
+
: UPGRADE_NOTICE_INPROGRESS_TTL_MS;
|
|
73
80
|
try {
|
|
74
81
|
mkdirSync(mofloDir(projectRoot), { recursive: true });
|
|
75
82
|
const now = Date.now();
|
|
76
83
|
const notice = {
|
|
77
|
-
status
|
|
84
|
+
status,
|
|
78
85
|
kind: upgradeNoticeContext.kind,
|
|
79
86
|
from: upgradeNoticeContext.from,
|
|
80
87
|
to: upgradeNoticeContext.to,
|
|
81
88
|
at: new Date(now).toISOString(),
|
|
82
|
-
expiresAt: new Date(now +
|
|
89
|
+
expiresAt: new Date(now + ttlMs).toISOString(),
|
|
83
90
|
changes: 0,
|
|
84
91
|
};
|
|
85
92
|
writeFileSync(UPGRADE_NOTICE_PATH(), JSON.stringify(notice, null, 2));
|
|
86
93
|
} catch { /* non-fatal — statusline just won't show the segment */ }
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
function clearUpgradeNotice() {
|
|
90
|
-
try {
|
|
91
|
-
unlinkSync(UPGRADE_NOTICE_PATH());
|
|
92
|
-
} catch { /* non-fatal — already gone or never existed */ }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
96
|
// ── 0-pre. Drop any stale upgrade notice (#738, #743) ───────────────────────
|
|
96
97
|
// `upgrade-notice.json` is a transient handshake between launcher and
|
|
97
98
|
// statusline — it should never survive past the launcher run that wrote it.
|
|
@@ -313,9 +314,9 @@ try {
|
|
|
313
314
|
}
|
|
314
315
|
// Surface a transient "(updating…)" badge in the statusline before the
|
|
315
316
|
// long-running upgrade work (manifest sync, daemon recycle, embeddings
|
|
316
|
-
// migration). See #738 —
|
|
317
|
-
//
|
|
318
|
-
|
|
317
|
+
// migration). See #738 — section 3f flips this to a 2-min "completed"
|
|
318
|
+
// badge once work finishes (TTL rationale at the constants above).
|
|
319
|
+
writeUpgradeNotice('in-progress');
|
|
319
320
|
const binDir = resolve(projectRoot, 'node_modules/moflo/bin');
|
|
320
321
|
|
|
321
322
|
// ── Manifest-based auto-update ──────────────────────────────────────
|
|
@@ -415,7 +416,9 @@ try {
|
|
|
415
416
|
resolve(projectRoot, 'node_modules/moflo/src/cli/.claude/helpers'),
|
|
416
417
|
];
|
|
417
418
|
const sourceHelperFiles = [
|
|
418
|
-
'auto-memory-hook.mjs', 'statusline.cjs', 'intelligence.cjs',
|
|
419
|
+
'auto-memory-hook.mjs', 'statusline.cjs', 'intelligence.cjs',
|
|
420
|
+
'subagent-start.cjs', 'subagent-bootstrap.json',
|
|
421
|
+
'pre-commit', 'post-commit',
|
|
419
422
|
];
|
|
420
423
|
for (const file of sourceHelperFiles) {
|
|
421
424
|
const dest = resolve(helpersDir, file);
|
|
@@ -855,12 +858,11 @@ try {
|
|
|
855
858
|
} catch { /* writing the failure itself must not throw */ }
|
|
856
859
|
}
|
|
857
860
|
|
|
858
|
-
// ── 3f.
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
// `additionalContext`); a lingering "you upgraded a while ago" badge is noise.
|
|
861
|
+
// ── 3f. Flip the upgrade notice to "completed" (#636, #738) ─────────────────
|
|
862
|
+
// See the TTL rationale at the constants above for why we switch to a
|
|
863
|
+
// short-TTL completed badge instead of clearing the file.
|
|
862
864
|
if (upgradeNoticeContext) {
|
|
863
|
-
|
|
865
|
+
writeUpgradeNotice('completed');
|
|
864
866
|
}
|
|
865
867
|
|
|
866
868
|
// ── 3g. Commit deferred version stamp (#730) ────────────────────────────────
|
|
@@ -23,7 +23,7 @@ import { errorDetail } from '../shared/utils/error-detail.js';
|
|
|
23
23
|
// Path Resolution
|
|
24
24
|
// ============================================================================
|
|
25
25
|
/** Convert an absolute path to a file:// URL for dynamic import() on Windows. */
|
|
26
|
-
function toImportUrl(absolutePath) {
|
|
26
|
+
export function toImportUrl(absolutePath) {
|
|
27
27
|
return pathToFileURL(absolutePath).href;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
@@ -37,7 +37,7 @@ export function getMofloRoot() {
|
|
|
37
37
|
/**
|
|
38
38
|
* Find the first existing .js module from paths relative to the moflo root.
|
|
39
39
|
*/
|
|
40
|
-
function findModule(...relativePaths) {
|
|
40
|
+
export function findModule(...relativePaths) {
|
|
41
41
|
const root = getMofloRoot();
|
|
42
42
|
if (!root)
|
|
43
43
|
return undefined;
|
|
@@ -79,7 +79,10 @@ export async function checkSubagentHealth() {
|
|
|
79
79
|
priority: 'low',
|
|
80
80
|
metadata: { purpose: 'doctor-health-check' },
|
|
81
81
|
}, {});
|
|
82
|
-
|
|
82
|
+
// `idle` is the AgentState status returned by the live coordinator (post
|
|
83
|
+
// #801); `active`/`spawned` were the legacy literal returns. Accept all
|
|
84
|
+
// three — matches the status-check enum 13 lines below.
|
|
85
|
+
if (!spawnResult?.agentId || !['active', 'idle', 'spawned'].includes(spawnResult.status)) {
|
|
83
86
|
return { name: 'Subagent Health', status: 'fail', message: `Spawn returned unexpected result: ${JSON.stringify(spawnResult)}` };
|
|
84
87
|
}
|
|
85
88
|
const agentId = spawnResult.agentId;
|