Wakes an idle session when agent-mailbox mail arrives, without typing into the terminal

Version: 2.18.1 (RuVector Integration) Last Updated: 2025-12-05
Package Metrics:
AI agent orchestration with semantic codebase search, Redis coordination, and multi-loop consensus validation (CFN Loop).
npx claude-flow-noviceCFN_REDIS_HOST=localhost for non-Docker environments (see CLI Mode Redis Configuration)/custom-routing-activate for 95-98% total cost savings when using CLI mode/cfn-loop-task: Task mode execution (Main Chat → Task() agents)/cfn-loop-cli: CLI mode execution (Main Chat → coordinator → CLI agents)--mode=mvp|standard|enterprise with different confidence thresholds# Install and initialize
npm install claude-flow-novice
npx cfn-init
# Execute CFN Loop - Main Chat spawns Task() agents directly
/cfn-loop-task "Implement JWT authentication" --mode=standard
# Custom routing available for 5x cost reduction
/custom-routing-activate
# Install and initialize
npm install claude-flow-novice
npx cfn-init
# Configure Redis for non-Docker environments
export CFN_REDIS_HOST=localhost
export CFN_REDIS_PORT=6379
# Execute CFN Loop - Coordinator spawns CLI agents in background
/cfn-loop-cli "Implement JWT authentication" --mode=standard
# Initialize swarm with skills
npx claude-flow-novice swarm "Task Description" \
--skills=redis-coordination,agent-spawning \
--strategy development
.cfn-manifest.json tracks CFN vs custom files# Index codebase
./.claude/skills/cfn-ruvector-codebase-index/index.sh --full
# Search
./.claude/skills/cfn-ruvector-codebase-index/search.sh "authentication" --top 5
# Incremental reindex
/cfn-ruvector:codebase-reindex
v1 → v2 Changes:
v2.14 Changes:
v2.16.0 Changes (Integration Standardization - PR #16):
See CHANGELOG.md and CFN Loop Task Mode Guide for migration details.
CFN_REDIS_HOST=localhost)Test Details: See docs/ALL_3_MODES_VERIFIED_WORKING.md
# TDD compliance tests
bash tests/tdd-compliance/test-*.sh
# CLI mode tests
bash tests/cli-mode/test-*.sh
# Bug #6 Redis validation
bash tests/docker/validation/validate-bug6-redis-vars.sh
Centralized Index: ~/.local/share/ruvector/index_v2.db
RuVector provides dual-mode code intelligence with a centralized index shared across all projects:
| Schema | Purpose | Query Type | Use Case |
|---|---|---|---|
| V1 (embeddings, files) | Semantic similarity | Vector distance | "Find code similar to X" |
| V2 (entities, refs, modules) | Structured relationships | SQL joins | "Who calls this function?" |
V1 - Semantic Search:
V2 - Code Intelligence:
# Build RuVector
cd .claude/skills/cfn-local-ruvector-accelerator
cargo build --release
# Initialize centralized index
./target/release/local-ruvector init
# Index current project (all file types)
./target/release/local-ruvector index --path . --types rs,ts,js,json,md,sh
# Semantic search across all indexed projects
./target/release/local-ruvector query --pattern "authentication middleware"
# SQL queries on code structure
sqlite3 ~/.local/share/ruvector/index_v2.db "
SELECT * FROM refs WHERE target_name = 'MyFunction';
"
Supported Files:
.artifacts/ (logs/reports)Benefits:
.claude/skills/*/SKILL.md filesSee LICENSE file in repository root.
hooks/register.ts 124 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// Agent mailbox listener. Runs `mailbox.sh listen` for the session's life; each line it prints is
4// one new message (metadata only, never the body). Mail that should wake the session starts a turn
5// of its own with $.prompt.submit once the session is idle: nothing is typed into the terminal, so
6// a half-typed draft in the prompt box is never touched, and it works in tmux, a plain terminal
7// tab, VS Code and on the server alike. The mail hook (cfn-mailbox-hook.sh) then shows the mail
8// on that prompt. Other mail only shows a toast and waits for the next prompt.
9// Needs mailbox.sh listen/unread (mailbox stage 6 listener work).
10
11// Must equal NUDGE_TEXT in mailbox.sh: the hook keeps the outside-mail guard up for this exact
12// prompt, since no person typed it. tests/test-cfn-mailbox-listener-mod.sh checks they match.
13export const NUDGE_TEXT = 'You have mail: run /mail read'
14const COOLDOWN_MS = 60_000
15const SCRIPT = '$HOME/.claude/skills/cfn-mailbox/mailbox.sh'
16
17export type MailLine =
18 | { event: 'ready'; session: string }
19 | { event: 'mail'; from: string; kind: string; wake: boolean; addr_kind?: string }
20 | { event: 'down'; reason?: string }
21 | { event: 'up' }
22
23// Module state: restarts with the module (a reload re-runs session.start, which restarts the child).
24const st = { isBusy: false, isPending: false, lastWake: 0, session: '' }
25
26export function parseLines(buffer: string): { lines: MailLine[]; rest: string } {
27 const parts = buffer.split('\n')
28 const rest = parts.pop() ?? ''
29 const lines: MailLine[] = []
30 for (const p of parts) {
31 const s = p.trim()
32 if (!s.startsWith('{')) continue
33 try {
34 const v = JSON.parse(s)
35 if (v && typeof v.event === 'string') lines.push(v as MailLine)
36 } catch {
37 // a partial or foreign line: ignore it
38 }
39 }
40 return { lines, rest }
41}
42
43function mailboxArgv(sub: string, sid: string, cwd: string): string[] {
44 return ['sh', '-c', `exec bash "${SCRIPT}" ${sub} --sid "$1" --cwd "$2"`, 'sh', sid, cwd]
45}
46
47async function wake($: EngineInterface): Promise<void> {
48 if (st.isBusy) {
49 st.isPending = true
50 return
51 }
52 const now = await $.clock.now()
53 const wait = st.lastWake + COOLDOWN_MS - now
54 if (wait > 0) {
55 st.isPending = true
56 $.clock.after(wait, () => void flush($))
57 return
58 }
59 st.isPending = false
60 st.lastWake = now
61 // The hook may already have shown it (a tool call ran in between): wake only if mail waits.
62 const sid = await $.session.id()
63 const cwd = await $.session.cwd()
64 const ran = await $.process.run(mailboxArgv('unread', sid, cwd), { timeoutMs: 15_000 })
65 if (ran.exitCode === 0 && Number.parseInt(ran.stdout.trim(), 10) === 0) return
66 void $.prompt.submit({ text: NUDGE_TEXT })
67}
68
69async function flush($: EngineInterface): Promise<void> {
70 if (st.isPending && !st.isBusy) await wake($)
71}
72
73async function handleLine($: EngineInterface, line: MailLine): Promise<void> {
74 if (line.event === 'ready') {
75 st.session = line.session
76 $.ui.status(undefined)
77 } else if (line.event === 'down') {
78 $.ui.status('mail: offline')
79 } else if (line.event === 'up') {
80 $.ui.status(undefined)
81 } else if (line.wake) {
82 await wake($)
83 } else {
84 $.ui.toast(`Mail from ${line.from} (${line.kind}); shown at your next prompt`)
85 }
86}
87
88async function listen($: EngineInterface, sid: string, cwd: string): Promise<void> {
89 const child = $.process.spawn({ argv: mailboxArgv('listen', sid, cwd) })
90 let buffer = ''
91 for await (const piece of child) {
92 if (piece.stream !== 'stdout') continue
93 const parsed = parseLines(buffer + piece.text)
94 buffer = parsed.rest
95 for (const line of parsed.lines) await handleLine($, line)
96 }
97 $.ui.status(st.session ? 'mail: listener stopped' : 'mail: listener did not start')
98}
99
100export const register: Register = on => {
101 on('session.start', async ($, e, next) => {
102 const started = await next(e)
103 if (!e.isInteractive) return started
104 const sid = await $.session.id()
105 void listen($, sid, e.cwd)
106 return started
107 })
108
109 // Bookkeeping only: a failure here must never block a prompt.
110 on('prompt.submit', ($, e, next) => {
111 st.isBusy = true
112 return next(e)
113 }).catch(($, e, next) => next(e))
114
115 on('turn.complete', async ($, e, next) => {
116 const done = await next(e)
117 if (e.agentId === undefined) {
118 st.isBusy = false
119 await flush($)
120 }
121 return done
122 })
123}
124