Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/agents/framework-engineer.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ When processing "Incorporate Learnings" work, each action item from a LEARNINGS.

### 4. Cross-File Consistency

When updating a count ("seven engagement modes" → "eight"), search for ALL occurrences across the codebase, not just the one you know about. When adding a table row, verify the row matches the column schema of existing rows. When adding an escalation path, verify both the "from" and "to" modes acknowledge the path.
When updating a count ("eight engagement modes" → "nine"), search for ALL occurrences across the codebase, not just the one you know about. When adding a table row, verify the row matches the column schema of existing rows. When adding an escalation path, verify both the "from" and "to" modes acknowledge the path.

Use `Grep` proactively to find all mentions of related terms before declaring a change complete.

Expand Down Expand Up @@ -295,7 +295,7 @@ Return findings in this structure:

You MAY deviate without asking for:
- **RULE 1:** Fixing obvious typos, broken links, or formatting inconsistencies in files you're already editing
- **RULE 2:** Updating count words ("seven" → "eight") discovered via consistency checks, even if not explicitly scoped
- **RULE 2:** Updating count words ("eight" → "nine") discovered via consistency checks, even if not explicitly scoped
- **RULE 3:** Adding missing cross-references when a registration point clearly requires one

You MUST ask before:
Expand Down Expand Up @@ -368,7 +368,7 @@ Awaiting guidance before proceeding.
**This engineering task is INCOMPLETE if:**
- Any mandatory integration checklist item is unaddressed
- A new artifact is missing required template sections
- Count words are stale (e.g., "seven modes" when eight exist)
- Count words are stale (e.g., "eight modes" when nine exist)
- Cross-references point to non-existent files or sections
- The output report omits files that were created or modified

Expand Down
295 changes: 196 additions & 99 deletions .claude/hooks/archive-session.sh

Large diffs are not rendered by default.

162 changes: 162 additions & 0 deletions .claude/hooks/recover-session-logs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#!/bin/bash
# recover-session-logs.sh -- SessionStart hook: activity logging + crash recovery
#
# Replaces the previous inline SessionStart command. Performs:
# 1. Original work: mkdir -p + activity.log append
# 2. Background recovery: reconciles Claude Code's raw transcript storage
# against DAAF's archive directory, archiving any missed sessions
#
# Recovery approach:
# - Derives the Claude Code transcript directory from the current session's
# transcript_path (a common field available in all hook events)
# - Builds an index of already-archived session IDs with their file sizes
# - Scans for raw transcripts newer than the last recovery timestamp
# - For each unarchived (or grown) transcript, pipes a synthesized JSON
# payload to archive-session.sh, reusing all existing archiving logic
# - archive-session.sh's idempotency guard handles concurrent sessions:
# if a session is still running and its transcript grows, the next
# archive invocation (SessionEnd or recovery) will re-archive it
#
# Also processes stale pending log collection markers left by
# collect_session_logs.sh if archive-session.sh didn't process them
# (e.g., the session that wrote the marker crashed before SessionEnd).
#
# Performance:
# - Timestamp-gated: only processes transcripts modified since last recovery
# - Index-based matching: O(n+m) via bash associative array, not O(n*m) globs
# - Background execution: recovery runs in a detached subprocess so session
# startup is never blocked (foreground work completes in <1s)
#
# Exit codes:
# 0 = always (observability hook, must never block session start)
#
# Hook event: SessionStart (matcher: "")
# Registered in: .claude/settings.json

# Fail OPEN: session start is observability, not a security gate
trap '' ERR

INPUT=$(cat)

# Parse session metadata from JSON input
mapfile -t _meta < <(
printf '%s' "$INPUT" | jq -r '
(.session_id // "unknown"),
(.transcript_path // ""),
(.cwd // "unknown")
' 2>/dev/null
)
SESSION_ID="${_meta[0]:-unknown}"
TRANSCRIPT_PATH="${_meta[1]:-}"
CWD="${_meta[2]:-unknown}"

PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
LOG_DIR="$PROJECT_DIR/.claude/logs"
ARCHIVE_DIR="$LOG_DIR/sessions"

# --- Section 1: Original SessionStart work ---
mkdir -p "$LOG_DIR"
mkdir -p "$ARCHIVE_DIR"
DAAF_VERSION=$(git -C "$PROJECT_DIR" describe --always --dirty 2>/dev/null || echo "unknown")
echo "Session started: $(date '+%Y-%m-%d %H:%M:%S') | DAAF: $DAAF_VERSION | Session: ${SESSION_ID:0:8}" >> "$LOG_DIR/activity.log"

# --- Section 2: Background recovery ---
# Only attempt recovery if we have a transcript_path to derive the source directory
if [ -n "$TRANSCRIPT_PATH" ]; then
CURRENT_SHORT="${SESSION_ID:0:8}"

(
TRANSCRIPT_DIR=$(dirname "$TRANSCRIPT_PATH")

# Bail if transcript directory doesn't exist
[ -d "$TRANSCRIPT_DIR" ] || exit 0

# Build index of already-archived session shorts with their file sizes
# This is a single directory read -- O(m) where m = archived sessions
declare -A ARCHIVED_SIZE
for f in "$ARCHIVE_DIR"/*_orchestrator.jsonl; do
[ -f "$f" ] || continue
short=$(basename "$f" | sed 's/.*_\([a-f0-9]\{8\}\)_orchestrator\.jsonl/\1/')
[ -n "$short" ] || continue
ARCHIVED_SIZE["$short"]=$(stat -c%s "$f" 2>/dev/null || echo 0)
done

# Timestamp-gated scan: only process transcripts newer than last recovery
LAST_RECOVERY="$LOG_DIR/.last_recovery"
FIND_ARGS=(-maxdepth 1 -name "*.jsonl")
[ -f "$LAST_RECOVERY" ] && FIND_ARGS+=(-newer "$LAST_RECOVERY")

RECOVERED=0
SKIPPED=0

while IFS= read -r raw; do
[ -f "$raw" ] || continue
uuid=$(basename "$raw" .jsonl)
short="${uuid:0:8}"

# Skip the current session -- it just started
[ "$short" = "$CURRENT_SHORT" ] && continue

RAW_SIZE=$(stat -c%s "$raw" 2>/dev/null || echo 0)

# Skip if already archived with same or larger size
if [ -n "${ARCHIVED_SIZE[$short]+x}" ]; then
if [ "$RAW_SIZE" -le "${ARCHIVED_SIZE[$short]}" ] 2>/dev/null; then
SKIPPED=$((SKIPPED + 1))
continue
fi
fi

# Archive this session by piping synthesized JSON to archive-session.sh
# archive-session.sh's own idempotency guard provides a second safety net
printf '{"session_id":"%s","transcript_path":"%s","cwd":"%s","reason":"recovered"}' \
"$uuid" "$raw" "$PROJECT_DIR" \
| "$PROJECT_DIR/.claude/hooks/archive-session.sh" 2>/dev/null

RECOVERED=$((RECOVERED + 1))
done < <(find "$TRANSCRIPT_DIR" "${FIND_ARGS[@]}" 2>/dev/null)

# Update recovery timestamp
touch "$LAST_RECOVERY"

# Log recovery activity
if [ $RECOVERED -gt 0 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') RECOVERY: archived $RECOVERED session(s), skipped $SKIPPED" >> "$LOG_DIR/activity.log"
fi

# --- Process stale pending log collection markers ---
PENDING_FILE="$LOG_DIR/pending_log_collection.jsonl"
if [ -f "$PENDING_FILE" ]; then
PENDING_TMP="${PENDING_FILE}.recovery.$$"
if mv "$PENDING_FILE" "$PENDING_TMP" 2>/dev/null; then
while IFS= read -r entry; do
[ -z "$entry" ] && continue
P_BASENAME=$(printf '%s' "$entry" | jq -r '.project_basename // empty' 2>/dev/null)
P_PATH=$(printf '%s' "$entry" | jq -r '.project_path // empty' 2>/dev/null)
[ -z "$P_BASENAME" ] || [ -z "$P_PATH" ] || [ ! -d "$P_PATH" ] && continue

DEST="$P_PATH/logs"
mkdir -p "$DEST"

# Search all archived orchestrator transcripts for this project
for arc in "$ARCHIVE_DIR"/*_orchestrator.jsonl; do
[ -f "$arc" ] || continue
if grep -q -- "$P_BASENAME" "$arc" 2>/dev/null; then
arc_short=$(basename "$arc" | sed 's/.*_\([a-f0-9]\{8\}\)_orchestrator\.jsonl/\1/')
for src in "$ARCHIVE_DIR"/*_${arc_short}_*.jsonl "$ARCHIVE_DIR"/*_${arc_short}_*.md; do
[ -f "$src" ] || continue
tgt="$DEST/$(basename "$src")"
[ -f "$tgt" ] || cp "$src" "$tgt" 2>/dev/null
done
fi
done
done < "$PENDING_TMP"
rm -f "$PENDING_TMP" 2>/dev/null
fi
fi

) </dev/null >/dev/null 2>&1 &
disown
fi

exit 0
84 changes: 0 additions & 84 deletions .claude/hooks/subagent-registry.sh

This file was deleted.

14 changes: 1 addition & 13 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,18 +196,6 @@
]
}
],
"SubagentStop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/subagent-registry.sh",
"timeout": 10
}
]
}
],
"SessionEnd": [
{
"matcher": "",
Expand All @@ -226,7 +214,7 @@
"hooks": [
{
"type": "command",
"command": "mkdir -p \"$CLAUDE_PROJECT_DIR\"/.claude/logs && echo \"Session started: $(date '+%Y-%m-%d %H:%M:%S') | DAAF: $(git -C \"$CLAUDE_PROJECT_DIR\" describe --always --dirty 2>/dev/null || echo 'unknown')\" >> \"$CLAUDE_PROJECT_DIR\"/.claude/logs/activity.log",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/recover-session-logs.sh",
"timeout": 5
}
]
Expand Down
Loading