From 86fd1e6c25fa4c0a683e34782d6226b48f10f838 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Thu, 19 Feb 2026 18:46:51 -0800 Subject: [PATCH] Track Claude activity state via hook-based marker files --- src/cli.ts | 5 +++-- src/vm.ts | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 6c39087..8e862eb 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -233,10 +233,9 @@ program } // Determine status for each session in parallel - const activeWorktrees = await vm.activeWorktrees() const statusEntries = await Promise.all( sessions.map(async (s): Promise<[string, string]> => { - if (activeWorktrees.includes(s.worktree)) return [s.branch, "active"] + if (await vm.isClaudeActive(s.worktree, s.branch)) return [s.branch, "active"] const dirty = await git.isDirty(s.worktree) if (dirty) return [s.branch, "dirty"] const commits = await git.hasNewCommits(s.worktree) @@ -362,6 +361,8 @@ const closeAction = async (branch: string) => { const session = await state.getSession(root, branch) const worktreeAbs = session?.worktree ?? join(homedir(), '.sandlot', basename(root), branch) + await vm.clearActivity(worktreeAbs, branch) + await git.removeWorktree(worktreeAbs, root) .catch((e) => console.warn(`⚠ Failed to remove worktree: ${e.message}`)) diff --git a/src/vm.ts b/src/vm.ts index 3dd058f..d404225 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -1,5 +1,6 @@ import { $ } from "bun" import { homedir } from "os" +import { dirname } from "path" const CONTAINER_NAME = "sandlot" const USER = "ubuntu" @@ -73,9 +74,13 @@ export async function ensure(log?: (msg: string) => void): Promise { log?.("Warning: ANTHROPIC_API_KEY not found in ~/.env — claude will require manual login") } + const hooks = { + UserPromptSubmit: [{ hooks: [{ type: "command", command: "sandlot-activity active" }] }], + Stop: [{ hooks: [{ type: "command", command: "sandlot-activity idle" }] }], + } const settingsJson = JSON.stringify(apiKey - ? { apiKeyHelper: "~/.claude/api-key-helper.sh", skipDangerousModePermissionPrompt: true } - : { skipDangerousModePermissionPrompt: true }) + ? { apiKeyHelper: "~/.claude/api-key-helper.sh", skipDangerousModePermissionPrompt: true, hooks } + : { skipDangerousModePermissionPrompt: true, hooks }) const claudeJson = JSON.stringify({ hasCompletedOnboarding: true, effortCalloutDismissed: true }) // Write the helper script to a temp file and copy it in so the key @@ -88,6 +93,19 @@ export async function ensure(log?: (msg: string) => void): Promise { await Bun.file(tmp).unlink() } + // Install activity tracking hook script + const activityTmp = `${home}/.sandlot/.sandlot-activity.tmp` + await Bun.write(activityTmp, [ + '#!/bin/bash', + 'DIR=$(dirname "$CLAUDE_PROJECT_DIR")', + 'BRANCH=$(basename "$CLAUDE_PROJECT_DIR")', + 'echo "$1" > "$DIR/.activity-$BRANCH"', + '', + ].join('\n')) + await $`chmod +x ${activityTmp}`.quiet() + await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"cp /sandlot/.sandlot-activity.tmp ~/.local/bin/sandlot-activity"}`.quiet() + await Bun.file(activityTmp).unlink() + await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${` mkdir -p ~/.claude echo '${settingsJson}' > ~/.claude/settings.json @@ -178,6 +196,23 @@ export async function activeWorktrees(): Promise { }) } +/** Check if Claude is actively working in the given worktree (based on activity hook). */ +export async function isClaudeActive(worktree: string, branch: string): Promise { + const file = `${dirname(worktree)}/.activity-${branch}` + try { + const content = await Bun.file(file).text() + return content.trim() === "active" + } catch { + return false + } +} + +/** Remove the activity marker file for a worktree. */ +export async function clearActivity(worktree: string, branch: string): Promise { + const file = `${dirname(worktree)}/.activity-${branch}` + await Bun.file(file).unlink().catch(() => {}) +} + /** Stop the container. */ export async function stop(): Promise { await $`container stop ${CONTAINER_NAME}`.nothrow().quiet()