diff --git a/agents/claude/anvillm-agent.md b/agents/claude/anvillm-agent.md new file mode 100644 index 0000000..401fbcf --- /dev/null +++ b/agents/claude/anvillm-agent.md @@ -0,0 +1,7 @@ +--- +name: anvillm-agent +description: Agent for AnviLLM multi-agent workflows with Claude Code backend +permissionMode: bypassPermissions +--- + +You are an AI assistant for AnviLLM multi-agent workflows. You have full access to all tools and operate without permission restrictions. diff --git a/agents/anvillm-agent.json b/agents/kiro-cli/anvillm-agent.json similarity index 100% rename from agents/anvillm-agent.json rename to agents/kiro-cli/anvillm-agent.json diff --git a/cfg/backends/claude.yaml b/cfg/backends/claude.yaml index ae7cb25..d66004e 100644 --- a/cfg/backends/claude.yaml +++ b/cfg/backends/claude.yaml @@ -4,7 +4,10 @@ filesystem: - "{HOME}/.claude" - "{HOME}/.claude.json" - "{HOME}/.config/anvillm" + rwx: + - "{HOME}/.claude/hooks" network: unrestricted: true env: - ANTHROPIC_API_KEY + - CLAUDE_AGENT_NAME=anvillm-agent diff --git a/claude/hooks/anvillm-state-idle.sh b/claude/hooks/anvillm-state-idle.sh new file mode 100644 index 0000000..85b6601 --- /dev/null +++ b/claude/hooks/anvillm-state-idle.sh @@ -0,0 +1,31 @@ +#!/bin/sh +echo "$(date '+%Y-%m-%d %H:%M:%S') - Hook fired: Stop (PPID=$PPID)" >> /tmp/claude-hooks.log +cat > /dev/null +pid=$PPID +while [ $pid -gt 1 ]; do + if ps -p $pid -o comm= | grep -q claude; then + break + fi + pid=$(ps -p $pid -o ppid= | tr -d ' ') +done +if [ $pid -le 1 ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Claude PID not found" >> /tmp/claude-hooks.log + exit 0 +fi +echo "$(date '+%Y-%m-%d %H:%M:%S') - Found Claude PID: $pid" >> /tmp/claude-hooks.log +# Read session ID from filesystem directly (9p read doesn't work in sandbox) +ns_path="/tmp/ns.$USER.:0/anvillm-session-id-$pid" +if [ ! -f "$ns_path" ]; then + ns_path="/tmp/ns.$USER/anvillm-session-id-$pid" +fi +session=$(cat "$ns_path" 2>/dev/null) +if [ -z "$session" ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Session ID not found for PID $pid" >> /tmp/claude-hooks.log + exit 0 +fi +echo "$(date '+%Y-%m-%d %H:%M:%S') - Found session ID: $session" >> /tmp/claude-hooks.log +if echo idle | 9p write agent/$session/state 2>>/tmp/claude-hooks.log; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - SUCCESS: Wrote 'idle' to agent/$session/state" >> /tmp/claude-hooks.log +else + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Failed to write to agent/$session/state" >> /tmp/claude-hooks.log +fi diff --git a/claude/hooks/anvillm-state-running.sh b/claude/hooks/anvillm-state-running.sh new file mode 100644 index 0000000..acfd8de --- /dev/null +++ b/claude/hooks/anvillm-state-running.sh @@ -0,0 +1,31 @@ +#!/bin/sh +echo "$(date '+%Y-%m-%d %H:%M:%S') - Hook fired: UserPromptSubmit (PPID=$PPID)" >> /tmp/claude-hooks.log +cat > /dev/null +pid=$PPID +while [ $pid -gt 1 ]; do + if ps -p $pid -o comm= | grep -q claude; then + break + fi + pid=$(ps -p $pid -o ppid= | tr -d ' ') +done +if [ $pid -le 1 ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Claude PID not found" >> /tmp/claude-hooks.log + exit 0 +fi +echo "$(date '+%Y-%m-%d %H:%M:%S') - Found Claude PID: $pid" >> /tmp/claude-hooks.log +# Read session ID from filesystem directly (9p read doesn't work in sandbox) +ns_path="/tmp/ns.$USER.:0/anvillm-session-id-$pid" +if [ ! -f "$ns_path" ]; then + ns_path="/tmp/ns.$USER/anvillm-session-id-$pid" +fi +session=$(cat "$ns_path" 2>/dev/null) +if [ -z "$session" ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Session ID not found for PID $pid" >> /tmp/claude-hooks.log + exit 0 +fi +echo "$(date '+%Y-%m-%d %H:%M:%S') - Found session ID: $session" >> /tmp/claude-hooks.log +if echo running | 9p write agent/$session/state 2>>/tmp/claude-hooks.log; then + echo "$(date '+%Y-%m-%d %H:%M:%S') - SUCCESS: Wrote 'running' to agent/$session/state" >> /tmp/claude-hooks.log +else + echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Failed to write to agent/$session/state" >> /tmp/claude-hooks.log +fi diff --git a/claude/settings.json b/claude/settings.json new file mode 100644 index 0000000..2bc1212 --- /dev/null +++ b/claude/settings.json @@ -0,0 +1,24 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "$HOME/.claude/hooks/anvillm-state-running.sh" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "$HOME/.claude/hooks/anvillm-state-idle.sh" + } + ] + } + ] + } +} diff --git a/internal/backend/tmux/tmux.go b/internal/backend/tmux/tmux.go index a818fc2..230c894 100644 --- a/internal/backend/tmux/tmux.go +++ b/internal/backend/tmux/tmux.go @@ -349,12 +349,15 @@ func (b *Backend) CreateSession(ctx context.Context, opts backend.SessionOptions } sess.idleCond = sync.NewCond(&sess.mu) - // Write session ID to file for hook access (if kiro-cli backend) - if b.cfg.Name == "kiro-cli" && pid != 0 { + // Write session ID to file for hook access (if kiro-cli or claude backend) + if (b.cfg.Name == "kiro-cli" || b.cfg.Name == "claude") && pid != 0 { ns := client.Namespace() sessionIDFile := filepath.Join(ns, fmt.Sprintf("anvillm-session-id-%d", pid)) - os.WriteFile(sessionIDFile, []byte(id), 0644) - debug.Log("[session %s] wrote session ID file: %s", id, sessionIDFile) + if err := os.WriteFile(sessionIDFile, []byte(id), 0644); err != nil { + debug.Log("[session %s] failed to write session ID file: %v", id, err) + } else { + debug.Log("[session %s] wrote session ID file: %s", id, sessionIDFile) + } } // 10. Start reader goroutine diff --git a/internal/backends/claude.go b/internal/backends/claude.go index dcc32b0..0bed443 100644 --- a/internal/backends/claude.go +++ b/internal/backends/claude.go @@ -25,20 +25,15 @@ func NewClaude(nsSuffix string) backend.Backend { return newClaudeWithCommand([]string{"claude", "--dangerously-skip-permissions"}, nsSuffix) } -// NewClaudeWithContinue creates a backend that continues the most recent conversation -func NewClaudeWithContinue(nsSuffix string) backend.Backend { - return newClaudeWithCommand([]string{"claude", "--dangerously-skip-permissions", "--continue"}, nsSuffix) -} - -// NewClaudeWithResume creates a backend that resumes a specific session by ID -func NewClaudeWithResume(sessionID string, nsSuffix string) backend.Backend { - return newClaudeWithCommand([]string{"claude", "--dangerously-skip-permissions", "--resume", sessionID}, nsSuffix) -} - func newClaudeWithCommand(command []string, nsSuffix string) backend.Backend { + agentName := os.Getenv("CLAUDE_AGENT_NAME") + if agentName == "" { + agentName = "anvillm-agent" + } + return tmux.New(tmux.Config{ Name: "claude", - Command: command, + Command: append(command, "--agent", agentName), Environment: map[string]string{ "TERM": "xterm-256color", }, @@ -99,17 +94,12 @@ func (h *claudeCommands) Execute(ctx context.Context, command string) (string, e // // NOTE: Save/Load operations are NOT supported for Claude backend. // - Claude auto-saves all conversations to ~/.claude/projects/