From f8310c3a5865fad03138d89f42b5f0dfb2ef4114 Mon Sep 17 00:00:00 2001 From: deimagjas Date: Tue, 31 Mar 2026 23:52:00 -0500 Subject: [PATCH 1/5] refactor: extract entrypoint functions with main() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract linear entrypoint.sh into discrete functions (parse_args, copy_credentials, create_worktree, setup_agent_perms, run_agent, run_interactive) called from a main() entry point. Prepares for adding monitoring logic without growing an unmaintainable script. Zero behavior change — all 49 shellspec tests pass unchanged. Co-Authored-By: Claude Opus 4.6 --- config/entrypoint.sh | 113 ++++++++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 44 deletions(-) diff --git a/config/entrypoint.sh b/config/entrypoint.sh index 5b01d67..52f4c87 100644 --- a/config/entrypoint.sh +++ b/config/entrypoint.sh @@ -22,25 +22,28 @@ AGENT_TASK="" PROJECT_NAME="" PASSTHROUGH_ARGS=() -# ── Parse agent mode flags ──────────────────────────────────────────────────── -while [[ $# -gt 0 ]]; do - case "$1" in - --worktree) WORKTREE_BRANCH="$2"; shift 2 ;; - --task) AGENT_TASK="$2"; shift 2 ;; - --project) PROJECT_NAME="$2"; shift 2 ;; - *) PASSTHROUGH_ARGS+=("$1"); shift ;; - esac -done - -# ── Copy credentials from host mounts ───────────────────────────────────────── -echo "[entrypoint] Copying credentials..." -cp /root/.claudenew.json /root/.claude.json -mkdir -p /root/.claude -cp -r /root/.claudenew/. /root/.claude/ -echo "[entrypoint] Credentials ready." - -# ── Agent mode: worktree + headless ─────────────────────────────────────────── -if [[ -n "$WORKTREE_BRANCH" ]]; then +# ── Functions ───────────────────────────────────────────────────────────────── + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --worktree) WORKTREE_BRANCH="$2"; shift 2 ;; + --task) AGENT_TASK="$2"; shift 2 ;; + --project) PROJECT_NAME="$2"; shift 2 ;; + *) PASSTHROUGH_ARGS+=("$1"); shift ;; + esac + done +} + +copy_credentials() { + echo "[entrypoint] Copying credentials..." + cp /root/.claudenew.json /root/.claude.json + mkdir -p /root/.claude + cp -r /root/.claudenew/. /root/.claude/ + echo "[entrypoint] Credentials ready." +} + +create_worktree() { WORKTREE_PATH="/worktrees/${WORKTREE_BRANCH}" echo "[entrypoint] Creating worktree: ${WORKTREE_BRANCH} → ${WORKTREE_PATH}" @@ -60,31 +63,53 @@ if [[ -n "$WORKTREE_BRANCH" ]]; then cd "$WORKTREE_PATH" echo "[entrypoint] Working directory: $(pwd)" +} - if [[ -n "$AGENT_TASK" ]]; then - echo "[entrypoint] Starting Claude agent (headless)..." - echo "[entrypoint] Task: ${AGENT_TASK}" - echo "---" - # Make claude's install path traversable for non-root users (installed under /root/) - # go+x required: agent is in group root (gid=0), so group bits apply, not others bits - chmod go+x /root /root/.local /root/.local/share 2>/dev/null || true - find /root/.local/share/claude -type d -exec chmod go+x {} + 2>/dev/null || true - find /root/.local/share/claude/versions -maxdepth 1 -type f -exec chmod 755 {} + 2>/dev/null || true - # Copy credentials to agent user's home (claude requires non-root for --dangerously-skip-permissions) - cp -r /root/.claude/. /home/agent/.claude/ 2>/dev/null || true - cp /root/.claude.json /home/agent/.claude.json 2>/dev/null || true - chown -R agent:agent /home/agent/.claude /home/agent/.claude.json 2>/dev/null || true - chown -R agent:agent "$WORKTREE_PATH" - exec su-exec agent env HOME=/home/agent claude --dangerously-skip-permissions -p "$AGENT_TASK" - else - # Worktree ready but no task: interactive shell in the worktree +setup_agent_perms() { + echo "[entrypoint] Starting Claude agent (headless)..." + echo "[entrypoint] Task: ${AGENT_TASK}" + echo "---" + # Make claude's install path traversable for non-root users (installed under /root/) + # go+x required: agent is in group root (gid=0), so group bits apply, not others bits + chmod go+x /root /root/.local /root/.local/share 2>/dev/null || true + find /root/.local/share/claude -type d -exec chmod go+x {} + 2>/dev/null || true + find /root/.local/share/claude/versions -maxdepth 1 -type f -exec chmod 755 {} + 2>/dev/null || true + # Copy credentials to agent user's home (claude requires non-root for --dangerously-skip-permissions) + cp -r /root/.claude/. /home/agent/.claude/ 2>/dev/null || true + cp /root/.claude.json /home/agent/.claude.json 2>/dev/null || true + chown -R agent:agent /home/agent/.claude /home/agent/.claude.json 2>/dev/null || true + chown -R agent:agent "$WORKTREE_PATH" +} + +run_agent() { + exec su-exec agent env HOME=/home/agent claude --dangerously-skip-permissions -p "$AGENT_TASK" +} + +run_interactive() { + if [[ ${#PASSTHROUGH_ARGS[@]} -eq 0 ]]; then exec /bin/bash --login + else + exec "${PASSTHROUGH_ARGS[@]}" fi -fi - -# ── Interactive mode (original behavior) ────────────────────────────────────── -if [[ ${#PASSTHROUGH_ARGS[@]} -eq 0 ]]; then - exec /bin/bash --login -else - exec "${PASSTHROUGH_ARGS[@]}" -fi +} + +# ── Main ────────────────────────────────────────────────────────────────────── + +main() { + parse_args "$@" + copy_credentials + + if [[ -n "$WORKTREE_BRANCH" ]]; then + create_worktree + if [[ -n "$AGENT_TASK" ]]; then + setup_agent_perms + run_agent + else + run_interactive + fi + else + run_interactive + fi +} + +main "$@" From 657cf0a56ddcb102dc24b8572366c3e026183295 Mon Sep 17 00:00:00 2001 From: deimagjas Date: Tue, 31 Mar 2026 23:57:14 -0500 Subject: [PATCH 2/5] feat: add agent monitoring with status.json, log persistence, and lifecycle markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add structured monitoring to the entrypoint agent mode: - Create .agent/ dir in worktree with status.json (phase, branch, task, timestamps, exit code, commit count) - Persist full agent output to .agent/agent.log via tee - Emit [agent:status] tagged markers at lifecycle transitions (starting → working → completed/errored) - Capture post-run metrics (commit count, duration, last commit) - Add .agent/ to worktree .gitignore automatically The su-exec call no longer uses exec, allowing post-processing to run after claude exits. Test mocks updated accordingly with 6 new test cases. Co-Authored-By: Claude Opus 4.6 --- config/entrypoint.sh | 71 ++++++++++++++++++++++++++- config/spec/entrypoint_spec.sh | 87 ++++++++++++++++++++++++++++++++-- 2 files changed, 152 insertions(+), 6 deletions(-) diff --git a/config/entrypoint.sh b/config/entrypoint.sh index 52f4c87..782e463 100644 --- a/config/entrypoint.sh +++ b/config/entrypoint.sh @@ -81,8 +81,77 @@ setup_agent_perms() { chown -R agent:agent "$WORKTREE_PATH" } +write_status() { + local phase="$1"; shift + local now + now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + ( printf '{"phase":"%s","branch":"%s","task":"%s","started_at":"%s"}\n' \ + "$phase" "$WORKTREE_BRANCH" "$AGENT_TASK" "${AGENT_STARTED_AT:-${now}}" \ + > "${AGENT_DIR}/status.json" ) 2>/dev/null || true +} + +emit_marker() { + local phase="$1"; shift + echo "[agent:status] PHASE=${phase} BRANCH=${WORKTREE_BRANCH} $*" +} + run_agent() { - exec su-exec agent env HOME=/home/agent claude --dangerously-skip-permissions -p "$AGENT_TASK" + AGENT_DIR="${WORKTREE_PATH}/.agent" + mkdir -p "$AGENT_DIR" + chown -R agent:agent "$AGENT_DIR" + + # Add .agent/ to worktree .gitignore (safe if dir doesn't exist yet) + if ! grep -qxF '.agent/' "${WORKTREE_PATH}/.gitignore" 2>/dev/null; then + echo '.agent/' >> "${WORKTREE_PATH}/.gitignore" 2>/dev/null || true + fi + + AGENT_STARTED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + local start_epoch + start_epoch=$(date +%s) + + write_status "starting" + emit_marker "starting" + + write_status "working" + emit_marker "working" + + # Run claude with tee to persist logs; capture exit code through pipe + set +e + su-exec agent env HOME=/home/agent claude --dangerously-skip-permissions \ + -p "$AGENT_TASK" 2>&1 | tee "$AGENT_DIR/agent.log" + local exit_code=${PIPESTATUS[0]} + set -e + + # Collect post-run metrics + local commit_count last_commit finished_at end_epoch duration_secs + commit_count=$(git -C "$WORKTREE_PATH" rev-list --count HEAD 2>/dev/null || echo 0) + last_commit=$(git -C "$WORKTREE_PATH" log --oneline -1 2>/dev/null || echo "none") + finished_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + end_epoch=$(date +%s) + duration_secs=$((end_epoch - start_epoch)) + + local final_phase="completed" + [ "$exit_code" -ne 0 ] && final_phase="errored" + + # Write final status.json + ( printf '{ + "phase": "%s", + "branch": "%s", + "task": "%s", + "started_at": "%s", + "finished_at": "%s", + "duration_secs": %d, + "exit_code": %d, + "commits": %s, + "last_commit": "%s" +}\n' "$final_phase" "$WORKTREE_BRANCH" "$AGENT_TASK" \ + "$AGENT_STARTED_AT" "$finished_at" "$duration_secs" \ + "$exit_code" "$commit_count" "$last_commit" \ + > "$AGENT_DIR/status.json" ) 2>/dev/null || true + + emit_marker "$final_phase" "EXIT_CODE=${exit_code}" "COMMITS=${commit_count}" "DURATION=${duration_secs}s" + + exit "$exit_code" } run_interactive() { diff --git a/config/spec/entrypoint_spec.sh b/config/spec/entrypoint_spec.sh index 29e243c..879f2b8 100644 --- a/config/spec/entrypoint_spec.sh +++ b/config/spec/entrypoint_spec.sh @@ -28,6 +28,32 @@ find() { echo "[MOCK] find $*"; } chown() { echo "[MOCK] chown $*"; } dirname() { command dirname "$@"; } pwd() { echo "/mocked/pwd"; } +date() { + if [[ "$*" == *"+%s"* ]]; then + echo "1000000" + else + echo "2026-01-01T00:00:00Z" + fi +} +tee() { command cat; } +grep() { + # For .gitignore check, pretend .agent/ is already there + if [[ "$*" == *".gitignore"* ]]; then + return 0 + fi + command grep "$@" +} +cat() { + # Intercept heredoc writes to status.json (cat > file) + if [[ "${1:-}" == ">" ]]; then + echo "[MOCK] cat > $2" + command cat > /dev/null + return 0 + fi + command cat "$@" +} + +su-exec() { echo "[MOCK] su-exec $*"; } git() { echo "[MOCK] git $*" @@ -35,6 +61,12 @@ git() { [[ "$GIT_WORKTREE_NEW_BRANCH_SUCCEEDS" == "true" ]] && return 0 || return 1 elif [[ "$*" == *"worktree add"* ]]; then [[ "$GIT_WORKTREE_EXISTING_BRANCH_SUCCEEDS" == "true" ]] && return 0 || return 1 + elif [[ "$*" == *"rev-list --count"* ]]; then + echo "3" + return 0 + elif [[ "$*" == *"log --oneline -1"* ]]; then + echo "abc1234 test commit" + return 0 fi return 0 } @@ -49,7 +81,7 @@ exec() { exit 0 } -export -f cp mkdir chmod find chown git cd exec pwd dirname +export -f cp mkdir chmod find chown git cd exec pwd dirname su-exec date tee grep cat source "$ENTRYPOINT_SH" "$@" WRAPPER_EOF @@ -305,9 +337,9 @@ WRAPPER_EOF The status should equal 0 End - It "execs su-exec with correct claude command" + It "runs su-exec with correct claude command" When run run_entrypoint --worktree agent-br --task "do work" - The output should include "[EXEC] exec su-exec agent env HOME=/home/agent claude --dangerously-skip-permissions -p do work" + The output should include "[MOCK] su-exec agent env HOME=/home/agent claude --dangerously-skip-permissions -p do work" The status should equal 0 End @@ -322,7 +354,7 @@ WRAPPER_EOF It "tolerates chmod failures via || true" When run run_entrypoint --worktree agent-br --task "work" The status should equal 0 - The output should include "[EXEC] exec su-exec" + The output should include "[MOCK] su-exec" End It "passes task with special characters" @@ -345,7 +377,7 @@ WRAPPER_EOF It "does not invoke su-exec without task" When run run_entrypoint --worktree my-branch The output should not include "[MOCK] chown" - The output should not include "[EXEC] exec su-exec" + The output should not include "[MOCK] su-exec" The status should equal 0 End @@ -414,4 +446,49 @@ WRAPPER_EOF End End + # ═══════════════════════════════════════════════════════════════════════════ + # 8. AGENT MONITORING + # ═══════════════════════════════════════════════════════════════════════════ + Describe "Agent Monitoring" + It "emits starting lifecycle marker" + When run run_entrypoint --worktree agent-br --task "do work" + The output should include "[agent:status] PHASE=starting BRANCH=agent-br" + The status should equal 0 + End + + It "emits working lifecycle marker" + When run run_entrypoint --worktree agent-br --task "do work" + The output should include "[agent:status] PHASE=working BRANCH=agent-br" + The status should equal 0 + End + + It "emits completed lifecycle marker on success" + When run run_entrypoint --worktree agent-br --task "do work" + The output should include "[agent:status] PHASE=completed BRANCH=agent-br" + The output should include "EXIT_CODE=0" + The output should include "COMMITS=" + The output should include "DURATION=" + The status should equal 0 + End + + It "creates .agent directory in worktree" + When run run_entrypoint --worktree agent-br --task "do work" + The output should include "[MOCK] mkdir -p /worktrees/agent-br/.agent" + The status should equal 0 + End + + It "runs su-exec without exec (allows post-processing)" + When run run_entrypoint --worktree agent-br --task "do work" + The output should include "[MOCK] su-exec agent env HOME=/home/agent claude" + The output should not include "[EXEC] exec su-exec" + The status should equal 0 + End + + It "collects commit count after agent finishes" + When run run_entrypoint --worktree agent-br --task "do work" + The output should include "[MOCK] git -C /worktrees/agent-br rev-list --count HEAD" + The status should equal 0 + End + End + End From 01eeaaf0373e39ae01481fe96c94c277b7001b78 Mon Sep 17 00:00:00 2001 From: deimagjas Date: Wed, 1 Apr 2026 00:01:51 -0500 Subject: [PATCH 3/5] feat: add Makefile monitoring targets with graceful fallback Add new targets and enhance existing ones: - status-agent: reads status.json from worktree (works post-exit) - summary-agent: extracts [agent:status] markers from logs - list-agents: now shows phase from each worktree's status.json - logs-agent: falls back to .agent/agent.log when container is gone - follow-agent: same fallback behavior All fallback paths return exit 0 with contextual messages instead of silent failures, fixing the UX issue where finished agents appeared as errors. Co-Authored-By: Claude Opus 4.6 --- config/Makefile | 69 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/config/Makefile b/config/Makefile index 85d4c52..bc96562 100644 --- a/config/Makefile +++ b/config/Makefile @@ -60,7 +60,7 @@ CONTAINER_BRANCH := $(shell echo "$(BRANCH)" | tr '/_ ' '-' | tr '[:upper:]' '[: # Host env var holding the OAuth token (avoids collision with host Claude instance) HOST_TOKEN_VAR := CLAUDE_CONTAINER_OAUTH_TOKEN -.PHONY: build network run shell spawn list-agents logs-agent follow-agent stop-agent clean clean-network clean-all help +.PHONY: build network run shell spawn list-agents logs-agent follow-agent stop-agent status-agent summary-agent clean clean-network clean-all help # ── Build ───────────────────────────────────────────────────────────────────── @@ -131,13 +131,74 @@ list-agents: @container list 2>/dev/null | grep "$(PROJECT_NAME)" || echo " (none)" @echo "" @echo "[agents] Worktrees in $(WORKTREES_DIR):" - @ls -la "$(WORKTREES_DIR)" 2>/dev/null || echo " (none yet)" + @if [ -d "$(WORKTREES_DIR)" ]; then \ + for dir in $(WORKTREES_DIR)/*/; do \ + [ -d "$$dir" ] || continue; \ + branch=$$(basename "$$dir"); \ + status_file="$$dir/.agent/status.json"; \ + if [ -f "$$status_file" ] && command -v jq >/dev/null 2>&1; then \ + phase=$$(jq -r '.phase // "unknown"' "$$status_file" 2>/dev/null || echo "unknown"); \ + printf " %-30s %s\n" "$$branch" "$$phase"; \ + else \ + printf " %-30s %s\n" "$$branch" "(no status)"; \ + fi; \ + done; \ + else \ + echo " (none yet)"; \ + fi logs-agent: - container logs $(PROJECT_NAME)-$(CONTAINER_BRANCH) + @container logs $(PROJECT_NAME)-$(CONTAINER_BRANCH) 2>/dev/null \ + || { \ + LOG_FILE="$(WORKTREES_DIR)/$(BRANCH)/.agent/agent.log"; \ + if [ -f "$$LOG_FILE" ]; then \ + echo "[logs] Container $(PROJECT_NAME)-$(CONTAINER_BRANCH) no longer running (agent finished)."; \ + echo "[logs] Showing saved logs from $$LOG_FILE"; \ + echo "---"; \ + cat "$$LOG_FILE"; \ + else \ + echo "[logs] Container $(PROJECT_NAME)-$(CONTAINER_BRANCH) not found and no saved logs at $$LOG_FILE"; \ + fi; \ + } follow-agent: - container logs -f $(PROJECT_NAME)-$(CONTAINER_BRANCH) + @container logs -f $(PROJECT_NAME)-$(CONTAINER_BRANCH) 2>/dev/null \ + || { \ + LOG_FILE="$(WORKTREES_DIR)/$(BRANCH)/.agent/agent.log"; \ + if [ -f "$$LOG_FILE" ]; then \ + echo "[logs] Container $(PROJECT_NAME)-$(CONTAINER_BRANCH) no longer running (agent finished)."; \ + echo "[logs] Showing saved logs from $$LOG_FILE"; \ + echo "---"; \ + cat "$$LOG_FILE"; \ + else \ + echo "[logs] Container $(PROJECT_NAME)-$(CONTAINER_BRANCH) not found and no saved logs at $$LOG_FILE"; \ + fi; \ + } + +status-agent: + @STATUS_FILE="$(WORKTREES_DIR)/$(BRANCH)/.agent/status.json"; \ + if [ -f "$$STATUS_FILE" ]; then \ + if command -v jq >/dev/null 2>&1; then \ + jq '.' "$$STATUS_FILE"; \ + else \ + cat "$$STATUS_FILE"; \ + fi; \ + else \ + echo "[status] No status file found for branch '$(BRANCH)'."; \ + echo "[status] Expected at: $$STATUS_FILE"; \ + fi + +summary-agent: + @container logs $(PROJECT_NAME)-$(CONTAINER_BRANCH) 2>/dev/null \ + | grep '^\[agent:' \ + || { \ + LOG_FILE="$(WORKTREES_DIR)/$(BRANCH)/.agent/agent.log"; \ + if [ -f "$$LOG_FILE" ]; then \ + grep '^\[agent:' "$$LOG_FILE" || echo "(no structured events found)"; \ + else \ + echo "(no logs available for branch '$(BRANCH)')"; \ + fi; \ + } stop-agent: @container stop $(PROJECT_NAME)-$(CONTAINER_BRANCH) 2>/dev/null \ From 07ca9d8ec85baf2e569ff9690bd8820ab25bb842 Mon Sep 17 00:00:00 2001 From: deimagjas Date: Wed, 1 Apr 2026 00:05:17 -0500 Subject: [PATCH 4/5] feat: add CLI status/summary commands and log fallback Add new commands to the q CLI: - q agents status --branch X: reads status.json directly from filesystem (no container needed, works post-exit) - q agents summary --branch X: shows structured lifecycle events The logs and follow commands now delegate to Makefile targets that include graceful fallback to persisted .agent/agent.log when the container no longer exists. Co-Authored-By: Claude Opus 4.6 --- app/cli/src/container_cli/commands/agents.py | 36 +++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/app/cli/src/container_cli/commands/agents.py b/app/cli/src/container_cli/commands/agents.py index 1759a0d..1b5c90c 100644 --- a/app/cli/src/container_cli/commands/agents.py +++ b/app/cli/src/container_cli/commands/agents.py @@ -1,12 +1,23 @@ +import json +import os +from pathlib import Path from typing import Annotated import typer -from container_cli.utils import check_token, run_make +from container_cli.utils import check_token, find_git_root, run_make app = typer.Typer(help="Agent lifecycle commands") +def _agents_home() -> Path: + """Resolve AGENTS_HOME, falling back to sibling .worktrees/ directory.""" + env_val = os.environ.get("AGENTS_HOME") + if env_val: + return Path(env_val) + return find_git_root().parent / ".worktrees" + + @app.command() def spawn( branch: Annotated[str, typer.Option("--branch", help="Git branch for the agent worktree")], @@ -55,3 +66,26 @@ def stop( ) -> None: """Stop a branch agent container.""" run_make("stop-agent", {"BRANCH": branch}) + + +@app.command() +def status( + branch: Annotated[str, typer.Option("--branch", help="Agent branch name")], +) -> None: + """Show agent status from persisted status.json file.""" + status_file = _agents_home() / branch / ".agent" / "status.json" + if not status_file.exists(): + typer.echo(f"[status] No status file found for branch '{branch}'.") + typer.echo(f"[status] Expected at: {status_file}") + raise typer.Exit(1) + + data = json.loads(status_file.read_text()) + typer.echo(json.dumps(data, indent=2)) + + +@app.command() +def summary( + branch: Annotated[str, typer.Option("--branch", help="Agent branch name")], +) -> None: + """Show structured lifecycle events for a branch agent.""" + run_make("summary-agent", {"BRANCH": branch}) From dc5ae9215d1ec944879e42d29f2abe681dfb2296 Mon Sep 17 00:00:00 2001 From: deimagjas Date: Wed, 1 Apr 2026 00:11:17 -0500 Subject: [PATCH 5/5] docs: update skill, docs, and evals for agent monitoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update spawn-agent skill with decision flow for monitoring: status.json → agent.log → container logs. Add guidance for post-exit scenarios where container is gone. Update docs: - spawn-agent-skill.md: new monitoring commands and post-exit status - cli.md: document q agents status/summary and log fallback behavior Update evals: - list_and_monitor.md: add part 3 for status post-exit scenario - evals.json: update eval 4 to prefer status.json, add evals 7 (status post-exit) and 8 (logs fallback) Co-Authored-By: Claude Opus 4.6 --- .claude/skills/spawn-agent/SKILL.md | 44 ++++++++++++++++--- .claude/skills/spawn-agent/evals/evals.json | 32 ++++++++++++-- .../spawn-agent/evals/list_and_monitor.md | 27 +++++++++--- docs/agents/cli.md | 25 +++++++++++ docs/agents/spawn-agent-skill.md | 24 ++++++++-- 5 files changed, 134 insertions(+), 18 deletions(-) diff --git a/.claude/skills/spawn-agent/SKILL.md b/.claude/skills/spawn-agent/SKILL.md index 83d34e3..d20bd79 100644 --- a/.claude/skills/spawn-agent/SKILL.md +++ b/.claude/skills/spawn-agent/SKILL.md @@ -168,24 +168,56 @@ ls -la "${AGENTS_HOME}" 2>/dev/null || echo "(no worktrees yet at $AGENTS_HOME)" Show the user a readable table with both container status and worktree list. -## Reading agent output (context) +## Reading agent output -`container logs` captures everything the agent prints (Claude's reasoning, -tool calls, results). Use this to pass context back to the user: +Agents persist structured monitoring data in the worktree at +`${AGENTS_HOME}//.agent/`. Use the right source for each question: + +### Quick status (preferred for "what is agent X doing?") + +```bash +# Read status.json — works even after container exits +cat "${AGENTS_HOME}/${BRANCH}/.agent/status.json" 2>/dev/null +``` + +Returns JSON with phase, branch, task, timestamps, exit code, and commit count. +Phases: `starting` → `working` → `completed` | `errored`. + +### Structured lifecycle events ```bash CONTAINER_NAME="${PROJECT_NAME}-${CONTAINER_BRANCH}" -# Last 100 lines (good for summary) -container logs -n 100 "${CONTAINER_NAME}" +# From live container +container logs "${CONTAINER_NAME}" 2>/dev/null | grep '^\[agent:' + +# From persisted logs (after container exits) +grep '^\[agent:' "${AGENTS_HOME}/${BRANCH}/.agent/agent.log" 2>/dev/null +``` + +### Full logs -# Follow live output (for running agents) +```bash +# Live container (while running) +container logs -n 100 "${CONTAINER_NAME}" container logs -f "${CONTAINER_NAME}" + +# Persisted logs (after container exits) +tail -100 "${AGENTS_HOME}/${BRANCH}/.agent/agent.log" ``` +### Decision flow + +- **"What is agent X doing?"** → read `status.json` (instant, always works) +- **"What did agent X do?"** → read `agent.log` (persisted, works post-exit) +- **"Show me live output"** → `container logs -f` (only while running) + Read the logs and **summarize the agent's progress** — don't just dump raw output. Tell the user: what the agent is working on, what it has done, what step it's at. +**Important:** When the container is gone (agent finished), do NOT attempt +`container logs` — it will fail. Use the persisted files in `.agent/` instead. + ## Integrating agent work When an agent finishes, its commits already exist in the **host repo** — the diff --git a/.claude/skills/spawn-agent/evals/evals.json b/.claude/skills/spawn-agent/evals/evals.json index 6722cd5..4623dd5 100644 --- a/.claude/skills/spawn-agent/evals/evals.json +++ b/.claude/skills/spawn-agent/evals/evals.json @@ -44,15 +44,39 @@ { "id": 4, "prompt": "Check what the feat/oauth2 agent is doing right now. Give me a summary of its progress.", - "expected_output": "Claude runs container logs for the feat/oauth2 agent (sanitized to feat-oauth2 in name), reads the output, and provides a plain-language summary of the agent's progress — not just a raw log dump.", + "expected_output": "Claude checks status.json from the worktree for quick status, and/or reads container logs (sanitized name feat-oauth2). Provides a plain-language summary — not just a raw log dump.", "files": [], "expectations": [ - "Runs `container logs` (not container run or container list)", - "Uses correct sanitized container name: includes feat-oauth2", - "Provides a summary or interpretation of logs, not just raw output", + "Reads status.json from $AGENTS_HOME/feat/oauth2/.agent/ OR runs `container logs`", + "Uses correct sanitized container name if using container logs: includes feat-oauth2", + "Provides a summary or interpretation, not just raw output", "Does NOT spawn a new container" ] }, + { + "id": 7, + "prompt": "The feat/oauth2 agent finished a while ago. What was the result? Did it succeed?", + "expected_output": "Claude reads status.json from the persisted worktree directory. Reports the phase (completed/errored), exit code, commit count, and duration. Does NOT attempt container logs on a stopped container.", + "files": [], + "expectations": [ + "Reads status.json from $AGENTS_HOME/feat/oauth2/.agent/status.json", + "Reports phase, exit code, and commit count from the status file", + "Does NOT attempt `container logs` on a container that no longer exists", + "Does NOT show error messages about missing containers" + ] + }, + { + "id": 8, + "prompt": "Show me the full logs from the feat/oauth2 agent. It finished already.", + "expected_output": "Claude reads the persisted agent.log from the worktree directory. Shows the saved logs with a note that these are persisted logs from a finished agent.", + "files": [], + "expectations": [ + "Reads from $AGENTS_HOME/feat/oauth2/.agent/agent.log (persisted logs)", + "Does NOT attempt `container logs` as the primary source for a finished agent", + "Indicates these are saved/persisted logs", + "Does NOT show confusing error output about missing containers" + ] + }, { "id": 5, "prompt": "The feat/oauth2 agent finished. Merge its work into my current branch.", diff --git a/.claude/skills/spawn-agent/evals/list_and_monitor.md b/.claude/skills/spawn-agent/evals/list_and_monitor.md index 5aec038..84f873e 100644 --- a/.claude/skills/spawn-agent/evals/list_and_monitor.md +++ b/.claude/skills/spawn-agent/evals/list_and_monitor.md @@ -14,7 +14,7 @@ User says: 1. Skill triggers 2. Runs: `container list 2>/dev/null | grep "qubits-team"` -3. Also shows worktrees on disk: `ls -la /.worktrees/` +3. Also shows worktrees on disk (with status from `status.json` if available) 4. Presents output in a readable format to the user --- @@ -27,12 +27,29 @@ User says: ## Expected behavior (monitor) 1. Skill triggers -2. Sanitizes: `feat/jwt-auth` → `qubits-team-feat-jwt-auth` -3. Runs: `container logs -n 100 qubits-team-feat-jwt-auth` -4. **Reads and summarizes** the logs — does NOT just dump raw output -5. Tells user: agent is working on X, currently at step Y, last action was Z +2. Reads `status.json` from `$AGENTS_HOME/feat/jwt-auth/.agent/status.json` for quick status +3. If more detail needed, reads container logs or persisted `.agent/agent.log` +4. Sanitizes container name correctly: `feat/jwt-auth` → `qubits-team-feat-jwt-auth` +5. **Reads and summarizes** the output — does NOT just dump raw logs +6. Tells user: agent is working on X, currently at step Y, last action was Z + +--- + +## Input (part 3 — status post-exit) + +User says: +> "What happened with the feat/jwt-auth agent?" + +## Expected behavior (status post-exit) + +1. Skill triggers +2. Reads `status.json` from `$AGENTS_HOME/feat/jwt-auth/.agent/status.json` +3. Reports phase (completed/errored), exit code, commit count, duration +4. Does NOT attempt `container logs` on a stopped container +5. If user wants full logs, reads from `.agent/agent.log` (persisted) ## Must NOT do - Must not just print raw container logs without summarizing - Must not confuse container name sanitization (/ → -) +- Must not show errors when container is gone (use persisted files instead) diff --git a/docs/agents/cli.md b/docs/agents/cli.md index f40c950..b8f4b8a 100644 --- a/docs/agents/cli.md +++ b/docs/agents/cli.md @@ -111,6 +111,25 @@ Follows live streaming logs (hands off TTY via `os.execvp`). q agents follow --branch feat-oauth2 ``` +#### `q agents status` + +Shows agent status from the persisted `status.json` file. Works even after the +container has exited — reads directly from the worktree filesystem. + +```bash +q agents status --branch feat-oauth2 +``` + +Output includes: phase, branch, task, timestamps, duration, exit code, commit count. + +#### `q agents summary` + +Shows structured lifecycle events (`[agent:status]` markers) for a branch agent. + +```bash +q agents summary --branch feat-oauth2 +``` + #### `q agents stop` Stops a branch agent container. @@ -119,6 +138,12 @@ Stops a branch agent container. q agents stop --branch feat-oauth2 ``` +#### Log fallback behavior + +When the container no longer exists (agent finished, `--rm` removed it), `logs` and +`follow` automatically fall back to the persisted `.agent/agent.log` in the worktree +directory with a contextual message. This avoids confusing error output. + --- ### Cleanup commands diff --git a/docs/agents/spawn-agent-skill.md b/docs/agents/spawn-agent-skill.md index d01a46a..f7b20d7 100644 --- a/docs/agents/spawn-agent-skill.md +++ b/docs/agents/spawn-agent-skill.md @@ -256,10 +256,16 @@ container list | grep "stackai-feat-oauth2" ### 3. Monitor progress ```bash -# Last 100 lines (snapshot) +# Quick status (works even after container exits) +cat "$AGENTS_HOME/feat/oauth2/.agent/status.json" + +# Structured lifecycle events +container logs stackai-feat-oauth2 2>/dev/null | grep '^\[agent:' + +# Full logs — last 100 lines container logs -n 100 stackai-feat-oauth2 -# Real time +# Real time (while running) container logs -f stackai-feat-oauth2 ``` @@ -272,6 +278,7 @@ On completion, the agent will have: - Implemented OAuth2 + JWT on the branch - Made a commit with a descriptive message - Exited (container automatically removed) +- Left `status.json` and `agent.log` in `$AGENTS_HOME/feat/oauth2/.agent/` The worktree persists in `$AGENTS_HOME/feat/oauth2/` so you can review the code. @@ -314,7 +321,18 @@ ls -la "${AGENTS_HOME}" "What is the feat/oauth2 agent doing?" ``` -Claude executes `container logs -n 100 stackai-feat-oauth2` and gives you a natural language summary. +Claude reads `$AGENTS_HOME/feat/oauth2/.agent/status.json` for quick status. If more +detail is needed, reads `container logs` (live) or `.agent/agent.log` (post-exit) and +gives a natural language summary. + +### Check agent status (post-exit) + +``` +"What happened with the feat/oauth2 agent?" +``` + +Claude reads the persisted `status.json` from the worktree. This works even after the +container has been removed (which happens automatically with `--rm`). ### Stop an agent