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.
Files changed (34) hide show
  1. package/.claude/helpers/statusline.cjs +66 -23
  2. package/.claude/helpers/subagent-bootstrap.json +3 -0
  3. package/.claude/helpers/subagent-start.cjs +58 -22
  4. package/.claude/skills/swarm-advanced/SKILL.md +77 -0
  5. package/README.md +9 -6
  6. package/bin/session-start-launcher.mjs +22 -20
  7. package/dist/src/cli/commands/doctor-checks-deep.js +6 -3
  8. package/dist/src/cli/commands/doctor-checks-swarm.js +433 -0
  9. package/dist/src/cli/commands/doctor.js +92 -18
  10. package/dist/src/cli/commands/status.js +3 -3
  11. package/dist/src/cli/embeddings/fastembed-inline/model-loader.js +27 -5
  12. package/dist/src/cli/init/executor.js +18 -18
  13. package/dist/src/cli/init/moflo-init.js +20 -12
  14. package/dist/src/cli/mcp-tools/agent-tools.js +332 -211
  15. package/dist/src/cli/mcp-tools/coordinator-views.js +23 -0
  16. package/dist/src/cli/mcp-tools/github-tools.js +2 -1
  17. package/dist/src/cli/mcp-tools/hive-mind-tools.js +145 -49
  18. package/dist/src/cli/mcp-tools/hooks-tools.js +3 -2
  19. package/dist/src/cli/mcp-tools/json-store.js +9 -6
  20. package/dist/src/cli/mcp-tools/neural-tools.js +2 -1
  21. package/dist/src/cli/mcp-tools/session-tools.js +11 -7
  22. package/dist/src/cli/mcp-tools/swarm-coordinator-singleton.js +119 -0
  23. package/dist/src/cli/mcp-tools/swarm-scale-handler.js +211 -0
  24. package/dist/src/cli/mcp-tools/swarm-tools.js +208 -27
  25. package/dist/src/cli/mcp-tools/task-tools.js +299 -166
  26. package/dist/src/cli/services/index.js +2 -0
  27. package/dist/src/cli/services/subagent-bootstrap.js +57 -0
  28. package/dist/src/cli/spells/core/platform-sandbox.js +80 -59
  29. package/dist/src/cli/spells/core/prerequisite-checker.js +8 -3
  30. package/dist/src/cli/spells/core/runner.js +1 -1
  31. package/dist/src/cli/swarm/swarm-persistence.js +144 -0
  32. package/dist/src/cli/swarm/unified-coordinator.js +260 -66
  33. package/dist/src/cli/version.js +1 -1
  34. 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 agentdb = getAgentDBStats();
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 = agentdb.vectorCount > 0 ? Math.min(100, Math.floor(agentdb.vectorCount / 100)) : 0;
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
- // AgentDB stats — reads from cache file written by embedding/memory operations.
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 getAgentDBStats() {
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
- // ONLY while upgrade work is in flight; the launcher deletes the file when
606
- // work completes. We render it strictly for status='in-progress' so a stale
607
- // notice (legacy "complete" file from pre-#738 launchers, zombie write from
608
- // an aborted launcher, future writer mistakes) cannot turn the statusline
609
- // segment into a permanent column. The launcher's section 0-pre also drops
610
- // any leftover file at session start as a second line of defence.
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 suffix = ` ${c.dim}(updating…)${c.reset}`;
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
- return `${c.brightYellow}📦 install repaired${c.reset}${suffix}`;
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
- const versions = notice.from && notice.to
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\uDCCA MCP${c.reset} ${parts.join(` ${c.dim}\u2502${c.reset} `)}`);
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: getSystemMetrics(),
914
+ system,
872
915
  adrs: getADRStatus(),
873
916
  hooks: getHooksStatus(),
874
- agentdb: getAgentDBStats(),
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
- 'use strict';
10
-
11
- const output = {
12
- hookSpecificOutput: {
13
- hookEventName: 'SubagentStart',
14
- additionalContext:
15
- 'MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). ' +
16
- 'The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. ' +
17
- 'After memory search, follow `.claude/guidance/shipped/moflo-subagents.md` protocol.',
18
- },
19
- };
20
-
21
- process.stdout.write(JSON.stringify(output));
22
- process.exit(0);
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 run 42 # Fetch epic #42, run all stories sequentially
297
- flo epic run 42 --dry-run # Preview execution plan without running
298
- flo epic run 42 --strategy auto-merge # Per-story PRs with auto-merge between stories
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 run 42` again — it resumes from where it left off, skipping already-passed stories.
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 run <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 20 hook bindings across 8 lifecycle events. You don't invoke these — they fire automatically.
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 launcher deletes the notice when upgrade work finishes no
67
- // "complete" state lingers, see #738.
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 writeInProgressUpgradeNotice() {
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: 'in-progress',
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 + UPGRADE_NOTICE_INPROGRESS_TTL_MS).toISOString(),
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 — the launcher clears this file after work
317
- // completes, so the badge naturally disappears once the user is unblocked.
318
- writeInProgressUpgradeNotice();
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', 'subagent-start.cjs', 'pre-commit', 'post-commit',
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. Clear the in-progress upgrade notice (#636, #738) ───────────────────
859
- // Upgrade work is finished; drop the notice so the statusline badge disappears
860
- // immediately. Change summary is already in stdout emits (Claude's
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
- clearUpgradeNotice();
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
- if (!spawnResult?.agentId || !['active', 'spawned'].includes(spawnResult.status)) {
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;