Skip to content

Commit 3c03f98

Browse files
feat: port SDLC enhancements from aegis-daemon — --bare, Stop hook, --add-dir
- Add --bare flag to build_claude_cmd() for faster Claude startup (skips CLAUDE.md discovery, MCP init, auto-memory); hooks still load via --settings - Add --add-dir support: work_dir always passed first (required by --bare for CLAUDE.md re-discovery), plus dynamically detected cross-repo dirs - Add detect_cross_repo_dirs() that scans REPOS_DIR for repos referenced in the task prompt and passes them as additional --add-dir flags - Create hooks/stop-checkpoint.sh Stop hook: captures checkpoint on every stop, re-engages Claude if TypeScript errors detected, allows stop on second fire (stop_hook_active=true) to prevent infinite loops - Register Stop hook in ensure_hooks_settings() JSON template - Wire CC_CHECKPOINT_FILE env var into execute_task() around claude invocation - Warn when ANTHROPIC_API_KEY is unset (required by --bare mode) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 75aa64b commit 3c03f98

2 files changed

Lines changed: 141 additions & 4 deletions

File tree

hooks/stop-checkpoint.sh

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env bash
2+
# stop-checkpoint.sh — Stop hook for autonomous taskrunner sessions
3+
#
4+
# On natural stop: re-engage Claude if TypeScript errors are found.
5+
# On second fire (stop_hook_active=true): allows stop to prevent infinite loops.
6+
#
7+
# Writes checkpoint to $CC_CHECKPOINT_FILE (set by taskrunner) so the
8+
# taskrunner can include it in retry prompts.
9+
#
10+
# Copyright 2026 Stackbilt LLC — Apache 2.0
11+
12+
INPUT=$(cat)
13+
STOP_HOOK_ACTIVE=$(printf '%s' "$INPUT" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("stop_hook_active", False))' 2>/dev/null || echo "False")
14+
LAST_MSG=$(printf '%s' "$INPUT" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("last_assistant_message","")[:500])' 2>/dev/null || echo "")
15+
CWD=$(printf '%s' "$INPUT" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("cwd",""))' 2>/dev/null || echo "")
16+
17+
CHECKPOINT_FILE="${CC_CHECKPOINT_FILE:-}"
18+
19+
# ─── Always capture checkpoint ────────────────────────────────
20+
21+
capture_checkpoint() {
22+
[[ -n "$CHECKPOINT_FILE" ]] || return 0
23+
24+
local changed_files=""
25+
26+
if [[ -n "$CWD" ]] && git -C "$CWD" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
27+
changed_files=$(git -C "$CWD" diff --name-only 2>/dev/null | head -20)
28+
fi
29+
30+
CC_LAST_MSG="$LAST_MSG" CC_CHANGED="$changed_files" CC_CWD="$CWD" python3 - > "$CHECKPOINT_FILE" <<'PY'
31+
import json, os
32+
print(json.dumps({
33+
"last_message": os.environ.get("CC_LAST_MSG", "")[:500],
34+
"changed_files": [f for f in os.environ.get("CC_CHANGED", "").split("\n") if f],
35+
"cwd": os.environ.get("CC_CWD", ""),
36+
}))
37+
PY
38+
}
39+
40+
capture_checkpoint
41+
42+
# ─── If already re-engaged once, let it stop ─────────────────
43+
44+
if [[ "$STOP_HOOK_ACTIVE" == "True" ]]; then
45+
exit 0
46+
fi
47+
48+
# ─── On natural stop: check if TypeScript errors exist ────────
49+
50+
if [[ -z "$CWD" ]]; then
51+
exit 0
52+
fi
53+
54+
# Quick typecheck (most common failure mode)
55+
TC_EXIT=0
56+
TC_OUTPUT=""
57+
if [[ -f "$CWD/web/tsconfig.json" ]]; then
58+
TC_OUTPUT=$(cd "$CWD/web" && npx tsc --noEmit 2>&1 | tail -5)
59+
TC_EXIT=$?
60+
elif [[ -f "$CWD/tsconfig.json" ]]; then
61+
TC_OUTPUT=$(cd "$CWD" && npx tsc --noEmit 2>&1 | tail -5)
62+
TC_EXIT=$?
63+
fi
64+
65+
if [[ $TC_EXIT -ne 0 ]]; then
66+
# Re-engage: tell Claude to fix type errors before stopping
67+
TC_SNIPPET=$(echo "$TC_OUTPUT" | head -10 | tr '\n' ' ' | cut -c1-300)
68+
printf '{"decision":"block","reason":"TypeScript errors detected. Fix before completing:\\n%s"}' "$TC_SNIPPET"
69+
exit 0
70+
fi
71+
72+
# TypeScript clean — allow stop
73+
exit 0

taskrunner.sh

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,26 +246,66 @@ ensure_hooks_settings() {
246246
}
247247
]
248248
}
249+
],
250+
"Stop": [
251+
{
252+
"hooks": [
253+
{
254+
"type": "command",
255+
"command": "bash ${HOOKS_DIR}/stop-checkpoint.sh"
256+
}
257+
]
258+
}
249259
]
250260
}
251261
}
252262
SETTINGS
253263
fi
254264
}
255265

266+
# ─── Cross-repo dir detection ────────────────────────────────
267+
268+
detect_cross_repo_dirs() {
269+
# Scan task prompt for references to other repos under REPOS_DIR.
270+
# Returns newline-separated list of repo paths (excluding the primary repo).
271+
local prompt="$1" primary_repo="$2"
272+
273+
[[ -z "$REPOS_DIR" ]] && return 0
274+
275+
local dir dirname
276+
for dir in "$REPOS_DIR"/*/; do
277+
[[ -d "$dir" ]] || continue
278+
dirname=$(basename "$dir")
279+
[[ "$dirname" == "$primary_repo" ]] && continue
280+
if echo "$prompt" | grep -qi "$dirname" && git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
281+
echo "$dir"
282+
fi
283+
done
284+
}
285+
256286
# ─── Build Claude command ────────────────────────────────────
257287

258288
build_claude_cmd() {
259-
local prompt="$1" max_turns="$2"
289+
local prompt="$1" max_turns="$2" add_dirs="${3:-}"
260290

261291
local cmd=(
262292
claude
263293
-p "$prompt"
294+
--bare
264295
--dangerously-skip-permissions
265296
--output-format json
266297
--max-turns "$max_turns"
267298
--settings "$HOOKS_SETTINGS"
268299
)
300+
# --bare: skip CLAUDE.md discovery, MCP init, auto-memory for faster startup.
301+
# Hooks still load via explicit --settings. Work dir passed via --add-dir below.
302+
303+
# Cross-repo access and CLAUDE.md discovery via --add-dir
304+
if [[ -n "$add_dirs" ]]; then
305+
while IFS= read -r dir; do
306+
[[ -n "$dir" && -d "$dir" ]] && cmd+=(--add-dir "$dir")
307+
done <<< "$add_dirs"
308+
fi
269309

270310
printf '%q ' "${cmd[@]}"
271311
}
@@ -418,16 +458,40 @@ MISSION
418458
timeout 30 git ls-files --others --exclude-standard 2>/dev/null >> "$pre_snapshot"
419459

420460
# Execute
421-
local output_file exit_code=0
461+
local output_file checkpoint_file exit_code=0
422462
output_file=$(mktemp /tmp/cc-task-XXXXXX.json)
423-
trap "rm -f ${output_file} ${pre_snapshot}" RETURN
463+
checkpoint_file=$(mktemp /tmp/cc-task-checkpoint-XXXXXX.json)
464+
trap "rm -f ${output_file} ${pre_snapshot} ${checkpoint_file}" RETURN
465+
466+
# Detect cross-repo dirs for --add-dir (prompt may reference other repos)
467+
local cross_repo_dirs=""
468+
if [[ -n "$REPOS_DIR" ]]; then
469+
cross_repo_dirs=$(detect_cross_repo_dirs "$prompt $title" "$(basename "$repo_path")" 2>/dev/null || echo "")
470+
if [[ -n "$cross_repo_dirs" ]]; then
471+
log "│ Cross-repo access: $(echo "$cross_repo_dirs" | tr '\n' ',' | sed 's/,$//')"
472+
fi
473+
fi
474+
475+
# --bare requires explicit ANTHROPIC_API_KEY (OAuth/keychain disabled)
476+
if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
477+
log "│ WARNING: ANTHROPIC_API_KEY not set — --bare mode requires explicit API key"
478+
fi
479+
480+
# --bare skips CLAUDE.md auto-discovery; always pass repo_path as first --add-dir
481+
local all_dirs="$repo_path"
482+
if [[ -n "$cross_repo_dirs" ]]; then
483+
all_dirs="${all_dirs}
484+
${cross_repo_dirs}"
485+
fi
424486

425487
log "│ Starting Claude Code session..."
426488

427489
cd "$repo_path"
428490
unset CLAUDECODE CLAUDE_CODE_ENTRYPOINT 2>/dev/null || true
429-
eval "$(build_claude_cmd "$mission_prompt" "$max_turns")" \
491+
export CC_CHECKPOINT_FILE="$checkpoint_file"
492+
eval "$(build_claude_cmd "$mission_prompt" "$max_turns" "$all_dirs")" \
430493
< /dev/null > "$output_file" 2>&1 || exit_code=$?
494+
unset CC_CHECKPOINT_FILE
431495

432496
# Detect max_turns exceeded from JSON output (#15)
433497
local max_turns_exceeded=false

0 commit comments

Comments
 (0)