Skip to content

Commit ad384fe

Browse files
authored
feat(brain): add memory, hooks, maxTurns; deprecate --initial-prompt (#635)
* Investigate issue #520: rename *Manager types to domain-role names * feat(brain): add memory, hooks, maxTurns to kild-brain agent; deprecate --initial-prompt - Add `memory: user`, `maxTurns: 200`, and agent-scoped `hooks:` to kild-brain frontmatter (PreToolUse bash guard + Stop fleet snapshot) - Create `.claude/hooks/brain-bash-guard.sh` to enforce "no source code access" constraint at the hook level - Replace ~40 lines of manual memory management with auto-memory note - Fix router skill to use create-then-inject instead of --initial-prompt - Deprecate --initial-prompt in CLI help text with runtime warnings - Update CLAUDE.md with deprecation notes and inject-based brain setup * fix: address review findings on brain hooks and deprecation warnings - Guard: switch from fragile grep+sed JSON parsing to jq, fail closed on parse failure, add ERR trap, block subshell invocations (bash -c, sh -c), remove overly broad src/ pattern, document advisory nature - Deprecation: eliminate contradictory double-warning for fleet sessions by branching into fleet-specific vs general paths (not both) - Deprecation: use color::warning/color::hint consistently in open.rs (was bare eprintln, now matches create.rs) - Deprecation: remove redundant initial_prompt_for_warning clone in create.rs, clone at use site instead - Logging: add structured error!() events for inbox fallback failures in both create.rs and open.rs - Stop hook: log stderr to file instead of /dev/null, ensure dir exists - SKILL.md: add session-active check after sleep 5 before injecting, warn user if session not ready instead of silently losing the message
1 parent 8aa9c5c commit ad384fe

7 files changed

Lines changed: 246 additions & 96 deletions

File tree

.claude/agents/kild-brain.md

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,22 @@ description: Honryū — KILD fleet supervisor. Manages parallel AI coding agent
44
model: opus
55
tools: Bash, Read, Write, Glob, Grep, Task
66
permissionMode: acceptEdits
7+
maxTurns: 200
8+
memory: user
79
skills:
810
- kild
911
- kild-wave-planner
12+
hooks:
13+
PreToolUse:
14+
- matcher: "Bash"
15+
hooks:
16+
- type: command
17+
command: ".claude/hooks/brain-bash-guard.sh"
18+
Stop:
19+
- hooks:
20+
- type: command
21+
command: "mkdir -p ~/.kild/brain && kild list --json > ~/.kild/brain/state.json 2>> ~/.kild/brain/stop-hook.log || echo \"$(date): kild list failed on Stop hook\" >> ~/.kild/brain/stop-hook.log"
22+
timeout: 10
1023
---
1124

1225
You are Honryū, the KILD fleet supervisor. You are the Tōryō's (human's) right hand — you do everything they would do from the CLI, but faster and in parallel. You coordinate a fleet of AI coding agents running in isolated git worktrees called kilds.
@@ -82,7 +95,7 @@ Communication uses two channels that work together. You don't need to think abou
8295

8396
### Sending instructions to workers
8497

85-
**Always use `kild inject` — never use `--initial-prompt`.**
98+
**Always use `kild inject`.** The `--initial-prompt` flag is deprecated.
8699

87100
```bash
88101
# Create a worker (no task yet — just boot the agent)
@@ -104,7 +117,7 @@ kild inject <branch> "Your next task: <instruction>"
104117

105118
For non-Claude agents (Codex, Kiro, etc.), inject writes to PTY stdin + dropbox.
106119

107-
**Do NOT use `--initial-prompt` on `kild create` or `kild open`.** The `--initial-prompt` flag has unreliable delivery for fleet sessions. Always create/open first, then inject separately.
120+
**Do NOT use `--initial-prompt` on `kild create` or `kild open`.** This flag is deprecated and will be removed in a future release. Always create/open first, wait for the agent to initialize, then inject separately.
108121

109122
**Never use `kild stop` to deliver messages.** Stopping kills the agent process.
110123

@@ -279,53 +292,36 @@ These files contain project-specific rules like forbidden file zones, required r
279292

280293
## Memory
281294

282-
### On startup — orient yourself
283-
```bash
284-
cat ~/.kild/brain/state.json 2>/dev/null # Last known fleet state
285-
tail -50 ~/.kild/brain/sessions/$(date +%Y-%m-%d).md 2>/dev/null # Today's log
286-
cat ~/.kild/brain/knowledge/MEMORY.md 2>/dev/null # Durable knowledge
287-
cat .kild/wave-plan.json 2>/dev/null # Pending wave plan
288-
```
295+
Your persistent memory is managed automatically by Claude Code at `~/.claude/agent-memory/kild-brain/MEMORY.md`. The first 200 lines are injected into your context on every startup. Use Read/Write/Edit tools to update it directly — no bash commands needed.
296+
297+
**What to remember:** Project patterns, worker quirks, conflict history, Tōryō preferences, lessons learned.
289298

290-
If a wave plan exists, mention it:
291-
> "There's a wave plan from {planned_at} with {N} kilds. Say 'start the wave' to execute, or 'plan a new wave' to replace it."
299+
**One-time migration:** If `~/.kild/brain/knowledge/MEMORY.md` exists and your auto-memory is empty, read the old file and copy relevant content to your auto-memory. Then note that migration is complete.
292300

293-
### After significant events — log
301+
### Session Logging
302+
303+
After significant events, log to the daily session file:
294304
```bash
295-
# Daily session log (append)
296305
cat >> ~/.kild/brain/sessions/$(date +%Y-%m-%d).md << 'EOF'
297306
## HH:MM — <event summary>
298307
Worker: <branch> | Decision: <what you decided> | Action: <what you did>
299308
EOF
300-
301-
# Fleet snapshot after major changes
302-
kild list --json > ~/.kild/brain/state.json
303309
```
304310

305-
### Your brain — persistent memory
311+
### On Startup
306312

307-
`~/.kild/brain/knowledge/MEMORY.md` is your long-term memory. It persists across restarts and sessions. This is where you store things you've learned so future-you remembers them:
308-
309-
- Project patterns: "kild project X has slow CI, allow 10min for checks"
310-
- Worker quirks: "codex agents need explicit test commands, they don't auto-run tests"
311-
- Conflict history: "sessions/ and git/ modules conflict frequently, never wave together"
312-
- Tōryō preferences: "prefers squash merges", "wants PR reviews before merge"
313-
- Lessons learned: "branch names with slashes break fleet inbox paths"
314-
315-
```bash
316-
# Write to your memory
317-
echo "- <learned fact>" >> ~/.kild/brain/knowledge/MEMORY.md
318-
319-
# Read your memory (do this on startup)
320-
cat ~/.kild/brain/knowledge/MEMORY.md 2>/dev/null
321-
```
313+
1. Your memory is auto-loaded (no action needed)
314+
2. Check fleet state: `kild list --json`
315+
3. Check today's log: `tail -50 ~/.kild/brain/sessions/$(date +%Y-%m-%d).md 2>/dev/null`
316+
4. Check wave plan: `cat .kild/wave-plan.json 2>/dev/null`
317+
5. Fleet state snapshot is auto-loaded from `~/.kild/brain/state.json` (saved on shutdown)
322318

323-
Update or remove memories that turn out to be wrong. Keep it concise — this is for facts, not session transcripts.
319+
If a wave plan exists, mention it to the Tōryō.
324320

325321
## Constraints
326322

327323
- **Never read project source code** — only `.kild/`, `~/.kild/`, and `~/.claude/teams/`
328-
- **Never `kild stop` to deliver a message** — use `kild inject` or `kild open --initial-prompt`
324+
- **Never `kild stop` to deliver a message** — use `kild inject`
329325
- **Never use `--no-attach`** unless the Tōryō explicitly asks for headless operation
330326
- **Never destroy a kild with an open PR** unless the Tōryō explicitly asks
331327
- **Never force-push** under any circumstances

.claude/hooks/brain-bash-guard.sh

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env bash
2+
#
3+
# brain-bash-guard.sh — PreToolUse hook for the kild-brain agent.
4+
# Blocks bash commands that access project source code.
5+
# The brain operates the fleet via the kild CLI, not by reading source.
6+
#
7+
# Exit 0 = allow, Exit 2 = block with reason on stderr.
8+
#
9+
# ADVISORY: This guard catches common direct access patterns but cannot
10+
# prevent all indirect execution (e.g., bash -c, sh -c, subshells).
11+
# It is a best-effort safety net, not a sandbox.
12+
13+
set -uo pipefail
14+
15+
# Fail closed on unexpected errors.
16+
trap 'echo "brain-bash-guard: unexpected error, blocking as safety measure" >&2; exit 2' ERR
17+
18+
# Extract the command from CLAUDE_CODE_TOOL_INPUT (JSON with "command" field).
19+
COMMAND="${CLAUDE_CODE_TOOL_INPUT:-}"
20+
if [ -z "$COMMAND" ]; then
21+
exit 0
22+
fi
23+
24+
# Extract the "command" field value from JSON.
25+
CMD=$(printf '%s' "$COMMAND" | jq -r '.command // empty' 2>/dev/null)
26+
27+
if [ -z "$CMD" ]; then
28+
# Parse failure or missing field — block rather than allow.
29+
echo "BLOCKED: could not parse tool input command field." >&2
30+
exit 2
31+
fi
32+
33+
# --- Blocklist: patterns that indicate source code access ---
34+
# NOTE: This is advisory — subshell invocations (bash -c, sh -c) can bypass
35+
# these checks. The brain agent instructions also prohibit source access.
36+
37+
# Source code paths (anchored to word boundary to reduce false positives)
38+
for pattern in "crates/" "target/" "tests/"; do
39+
if [[ "$CMD" == *"$pattern"* ]]; then
40+
echo "BLOCKED: Brain must not access source code (matched: $pattern)." >&2
41+
echo "Use kild CLI commands (kild diff, kild stats) instead of reading source directly." >&2
42+
exit 2
43+
fi
44+
done
45+
46+
# Build/compile commands
47+
for pattern in "^cargo " "^rustc" "^rustup"; do
48+
if echo "$CMD" | grep -qE "$pattern"; then
49+
echo "BLOCKED: Brain must not run build tools (matched: $pattern)." >&2
50+
echo "Workers handle builds. Use kild CLI to manage the fleet." >&2
51+
exit 2
52+
fi
53+
done
54+
55+
# Subshell invocations that could bypass the guard
56+
for pattern in "^bash -c" "^sh -c" "^env bash" "^env sh"; do
57+
if echo "$CMD" | grep -qE "$pattern"; then
58+
echo "BLOCKED: Brain must not use subshell invocations (matched: $pattern)." >&2
59+
echo "Run commands directly so the guard can inspect them." >&2
60+
exit 2
61+
fi
62+
done
63+
64+
# Raw git commands that have kild CLI equivalents
65+
if echo "$CMD" | grep -qE "^git diff"; then
66+
echo "BLOCKED: Use 'kild diff <branch>' instead of raw git diff." >&2
67+
exit 2
68+
fi
69+
70+
if echo "$CMD" | grep -qE "^git log"; then
71+
echo "BLOCKED: Use 'kild stats <branch>' instead of raw git log." >&2
72+
exit 2
73+
fi
74+
75+
# If no blocklist pattern matched, allow the command.
76+
exit 0

.claude/skills/kild-brain/SKILL.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,26 @@ case "$STATUS" in
4343
echo "Honryū is already running."
4444
;;
4545
stopped)
46-
kild open honryu --resume --initial-prompt "You've been restarted by the Tōryō. Orient yourself: check kild list --json, ~/.kild/brain/state.json, today's session log, and .kild/wave-plan.json (if it exists, mention it). Then greet the Tōryō and summarize the fleet state."
47-
echo "Honryū restarted."
46+
kild open honryu --resume
47+
echo "Waiting for Honryū to initialize..."
48+
sleep 5
49+
if kild list --json 2>/dev/null | jq -e '.sessions[] | select(.branch == "honryu" and .status == "active")' >/dev/null 2>&1; then
50+
kild inject honryu "You've been restarted by the Tōryō. Orient yourself: check kild list --json, today's session log, and .kild/wave-plan.json (if it exists, mention it). Then greet the Tōryō and summarize the fleet state."
51+
echo "Honryū restarted."
52+
else
53+
echo "Warning: honryu session not active after 5s. Run: kild inject honryu \"...\" once it starts." >&2
54+
fi
4855
;;
4956
*)
50-
kild create honryu --daemon --main --agent claude --yolo --note "Honryū fleet supervisor" --initial-prompt "You are Honryū, the KILD fleet supervisor. You have just been initialized by the Tōryō. Orient yourself: run kild list --json, read ~/.kild/brain/state.json, today's session log, and .kild/wave-plan.json if they exist. If a wave plan exists, mention it. Then greet the Tōryō and report fleet state. You are running on the main branch — do not create worktrees for yourself."
51-
echo "Honryū initialized."
57+
kild create honryu --daemon --main --agent claude --yolo --note "Honryū fleet supervisor"
58+
echo "Waiting for Honryū to initialize..."
59+
sleep 5
60+
if kild list --json 2>/dev/null | jq -e '.sessions[] | select(.branch == "honryu" and .status == "active")' >/dev/null 2>&1; then
61+
kild inject honryu "You are Honryū, the KILD fleet supervisor. You have just been initialized by the Tōryō. Orient yourself: run kild list --json, check today's session log, and .kild/wave-plan.json if they exist. If a wave plan exists, mention it. Then greet the Tōryō and report fleet state."
62+
echo "Honryū initialized."
63+
else
64+
echo "Warning: honryu session not active after 5s. Run: kild inject honryu \"...\" once it starts." >&2
65+
fi
5266
;;
5367
esac
5468
```

CLAUDE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,15 @@ cargo run -p kild -- create my-branch # Create kild with defaul
9191
cargo run -p kild -- create my-branch --note "Auth" # Create with description
9292
cargo run -p kild -- create my-branch --yolo # Create with autonomous mode
9393
cargo run -p kild -- create my-branch --main # Run from project root (no worktree)
94-
cargo run -p kild -- create my-branch --initial-prompt "Start with auth" # Inject prompt on startup
94+
cargo run -p kild -- create my-branch --initial-prompt "Start with auth" # [DEPRECATED] Use: create && sleep 5 && kild inject
9595
cargo run -p kild -- list # List all kilds
9696
cargo run -p kild -- list --json # JSON output
9797
cargo run -p kild -- open my-branch # Reopen agent in existing kild
9898
cargo run -p kild -- open --all # Open all stopped kilds
9999
cargo run -p kild -- open my-branch --resume # Resume previous conversation
100100
cargo run -p kild -- open my-branch --no-attach # Open daemon session without attach window
101101
cargo run -p kild -- open my-branch --no-attach --resume # Headless resume (brain reopening workers)
102-
cargo run -p kild -- open my-branch --initial-prompt "Next task: ..." # Inject prompt on reopen
102+
cargo run -p kild -- open my-branch --initial-prompt "Next task: ..." # [DEPRECATED] Use: open && sleep 5 && kild inject
103103
cargo run -p kild -- inject my-branch "do the thing" # Send to worker (inbox for claude, PTY for others)
104104
cargo run -p kild -- inject my-branch "msg" --inbox # Force Claude Code inbox protocol
105105
cargo run -p kild -- inject my-branch "msg" --queue # Queue task for later delivery (FIFO)
@@ -459,7 +459,10 @@ Inspect dropbox state with `kild inbox <branch>` (or `--all` for all fleet sessi
459459

460460
```
461461
kild create honryu --daemon --main # brain: runs from project root, no worktree
462+
sleep 5 # wait for agent init
463+
kild inject honryu "Orient yourself" # deliver initial task via inject
462464
kild create <worker> --daemon # worker: auto-joins fleet with team flags
465+
sleep 5
463466
kild inject <worker> "do the thing" # brain → worker message
464467
```
465468

crates/kild/src/app/session.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ pub fn create_command() -> Command {
9898
.arg(
9999
Arg::new("initial-prompt")
100100
.long("initial-prompt")
101-
.help("Write this text to the agent's PTY stdin immediately after startup (daemon sessions only)")
101+
.help("[DEPRECATED] Inject a prompt after agent starts. Use 'kild inject' instead — it's more reliable for fleet sessions")
102102
.value_name("TEXT")
103103
.conflicts_with("no-agent")
104104
.conflicts_with("no-daemon"),
@@ -185,7 +185,7 @@ pub fn open_command() -> Command {
185185
.arg(
186186
Arg::new("initial-prompt")
187187
.long("initial-prompt")
188-
.help("Write this text to the agent's PTY stdin immediately after startup (daemon sessions only)")
188+
.help("[DEPRECATED] Inject a prompt after agent starts. Use 'kild inject' instead — it's more reliable for fleet sessions")
189189
.value_name("TEXT")
190190
.conflicts_with("no-agent")
191191
.conflicts_with("no-daemon")

crates/kild/src/commands/create.rs

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ pub(crate) fn handle_create_command(
8787

8888
let use_main = matches.get_flag("main");
8989
let initial_prompt = matches.get_one::<String>("initial-prompt").cloned();
90-
let initial_prompt_for_warning = initial_prompt.clone();
9190
let issue = matches.get_one::<u32>("issue").copied();
9291

9392
let rows = matches.get_one::<u16>("rows").copied();
@@ -99,7 +98,7 @@ pub(crate) fn handle_create_command(
9998
.with_no_fetch(no_fetch)
10099
.with_runtime_mode(runtime_mode)
101100
.with_main_worktree(use_main)
102-
.with_initial_prompt(initial_prompt)
101+
.with_initial_prompt(initial_prompt.clone())
103102
.with_pty_size(rows, cols);
104103

105104
match session_ops::create_session(request, &config) {
@@ -137,41 +136,64 @@ pub(crate) fn handle_create_command(
137136
color::status(&status_str)
138137
);
139138

140-
// Warn fleet claude sessions about --initial-prompt deprecation.
141-
// Deliver the prompt via the reliable inbox path instead.
142-
if let Some(ref prompt) = initial_prompt_for_warning
143-
&& fleet::fleet_mode_active(&session.branch)
144-
&& fleet::is_claude_fleet_agent(&session.agent)
145-
{
146-
eprintln!();
147-
eprintln!(
148-
"{}",
149-
color::warning("Warning: --initial-prompt is unreliable for fleet sessions.")
150-
);
151-
eprintln!(
152-
" {}",
153-
color::hint(&format!(
154-
"Use instead: kild inject {} \"<your message>\"",
155-
session.branch
156-
))
157-
);
139+
// Deprecation warning for --initial-prompt.
140+
let is_fleet_claude = fleet::fleet_mode_active(&session.branch)
141+
&& fleet::is_claude_fleet_agent(&session.agent);
158142

159-
// Best-effort: deliver via inbox (the path that actually works).
160-
let safe_name = fleet::fleet_safe_name(&session.branch);
161-
match fleet::write_to_inbox(fleet::BRAIN_BRANCH, &safe_name, prompt) {
162-
Ok(()) => {
163-
eprintln!(" {} Delivered via inbox as fallback.", color::muted("→"));
164-
}
165-
Err(e) => {
166-
eprintln!(" {} Inbox fallback also failed: {}", color::error("✗"), e);
167-
eprintln!(
168-
" {}",
169-
color::hint(&format!(
170-
"Manually run: kild inject {} \"...\"",
171-
session.branch
172-
))
173-
);
143+
if let Some(ref prompt) = initial_prompt {
144+
eprintln!();
145+
if is_fleet_claude {
146+
// Fleet-specific warning with inbox fallback.
147+
eprintln!(
148+
"{}",
149+
color::warning(
150+
"Warning: --initial-prompt is deprecated. Fleet sessions: prompt delivered via inbox as fallback."
151+
)
152+
);
153+
eprintln!(
154+
" {}",
155+
color::hint(&format!(
156+
"Use instead: kild inject {} \"<your message>\"",
157+
session.branch
158+
))
159+
);
160+
161+
let safe_name = fleet::fleet_safe_name(&session.branch);
162+
match fleet::write_to_inbox(fleet::BRAIN_BRANCH, &safe_name, prompt) {
163+
Ok(()) => {
164+
eprintln!(" {} Delivered via inbox as fallback.", color::muted("→"));
165+
}
166+
Err(e) => {
167+
error!(
168+
event = "cli.create.inbox_fallback_failed",
169+
branch = %session.branch,
170+
error = %e
171+
);
172+
eprintln!(" {} Inbox fallback also failed: {}", color::error("✗"), e);
173+
eprintln!(
174+
" {}",
175+
color::hint(&format!(
176+
"Manually run: kild inject {} \"...\"",
177+
session.branch
178+
))
179+
);
180+
}
174181
}
182+
} else {
183+
// General deprecation warning for non-fleet sessions.
184+
eprintln!(
185+
"{}",
186+
color::warning(
187+
"Warning: --initial-prompt is deprecated and will be removed in a future release."
188+
)
189+
);
190+
eprintln!(
191+
" {}",
192+
color::hint(&format!(
193+
"Use instead: kild create {} && sleep 5 && kild inject {} \"...\"",
194+
session.branch, session.branch
195+
))
196+
);
175197
}
176198
}
177199

0 commit comments

Comments
 (0)