Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
54 changes: 54 additions & 0 deletions .claude/hooks/snapshot-pre-bash.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# snapshot-pre-bash.sh — PreToolUse hook for Bash tool calls
# Snapshots `git status --porcelain` to a temp file before each Bash call so
# that track-bash-writes.sh (PostToolUse) can diff the before/after state and
# log files newly modified by the command to .claude/session-edits.log.
# Always exits 0 (informational only, never blocks).

set -euo pipefail

INPUT=$(cat)

# Extract the command from tool_input JSON
COMMAND=$(echo "$INPUT" | node -e "
let d='';
process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>{
const p=JSON.parse(d).tool_input?.command||'';
if(p)process.stdout.write(p);
});
" 2>/dev/null) || true

if [ -z "$COMMAND" ]; then
exit 0
fi

# Skip read-only commands that can never write files — reduces snapshot overhead
# for the most common Bash calls (ls, cat, grep, git log, git status, etc.).
# sed is intentionally NOT in this list because `sed -i` modifies files in-place.
if echo "$COMMAND" | grep -qE '^\s*(ls|cat|head|tail|grep|find|git\s+(log|status|diff|show|branch|remote|fetch|rev-parse|stash\s+list|ls-files|blame|describe|tag|config\s+--get)|gh\s+(pr|issue|repo)\s+(view|list|status)|echo|printf|pwd|which|node\s+-e|node\s+-p|npx\s+--version|wc|sort|uniq|awk)\b'; then
exit 0
fi
Comment thread
carlos-alm marked this conversation as resolved.
Outdated
Comment thread
carlos-alm marked this conversation as resolved.
Outdated

# Resolve the project root (worktree-aware — each worktree has its own .claude/)
PROJECT_DIR=$(git rev-parse --show-toplevel 2>/dev/null) || PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"

# Key the snapshot file to the project root so parallel worktrees don't collide.
# Use a simple hash of the path — just enough to be unique per worktree.
PROJECT_HASH=$(echo "$PROJECT_DIR" | node -e "
const crypto = require('crypto');
let d='';
process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>{
process.stdout.write(crypto.createHash('sha1').update(d.trim()).digest('hex').slice(0,8));
});
" 2>/dev/null) || PROJECT_HASH="default"

SNAPSHOT_FILE="/tmp/claude-bash-snapshot-${PROJECT_HASH}.txt"

# Capture current git status --porcelain.
# Lines look like: "XY filename" or "XY orig -> dest" (rename).
# We only care about the status marker and path — porcelain is stable across git versions.
git -C "$PROJECT_DIR" status --porcelain 2>/dev/null > "$SNAPSHOT_FILE" || true
Comment thread
carlos-alm marked this conversation as resolved.
Outdated

exit 0
119 changes: 119 additions & 0 deletions .claude/hooks/track-bash-writes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# track-bash-writes.sh — PostToolUse hook for Bash tool calls
# Compares `git status --porcelain` against the snapshot taken by
# snapshot-pre-bash.sh (PreToolUse) to detect files newly modified or
# created by the Bash command, then appends them to .claude/session-edits.log
# so that guard-git.sh can validate commits correctly.
# Always exits 0 (informational only, never blocks).

set -euo pipefail

INPUT=$(cat)

# Extract the command from tool_input JSON
COMMAND=$(echo "$INPUT" | node -e "
let d='';
process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>{
const p=JSON.parse(d).tool_input?.command||'';
if(p)process.stdout.write(p);
});
" 2>/dev/null) || true

if [ -z "$COMMAND" ]; then
exit 0
fi

# Resolve the project root (worktree-aware — each worktree has its own .claude/)
PROJECT_DIR=$(git rev-parse --show-toplevel 2>/dev/null) || PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"

# Reproduce the same project hash used by snapshot-pre-bash.sh
PROJECT_HASH=$(echo "$PROJECT_DIR" | node -e "
const crypto = require('crypto');
let d='';
process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>{
process.stdout.write(crypto.createHash('sha1').update(d.trim()).digest('hex').slice(0,8));
});
" 2>/dev/null) || PROJECT_HASH="default"

SNAPSHOT_FILE="/tmp/claude-bash-snapshot-${PROJECT_HASH}.txt"

# If there is no snapshot (hook was not installed yet, or the pre-hook was
# skipped for a read-only command) we have no baseline — exit cleanly.
if [ ! -f "$SNAPSHOT_FILE" ]; then
exit 0
fi

# Capture current state after the command ran
AFTER=$(git -C "$PROJECT_DIR" status --porcelain 2>/dev/null) || true

# Read the before-state
BEFORE=$(cat "$SNAPSHOT_FILE") || true

# Clean up the snapshot so it doesn't pollute the next command's pre-hook
rm -f "$SNAPSHOT_FILE"

# Build the set of paths that existed (as dirty) before the command ran.
# porcelain format: "XY path" or "XY original -> new" (rename).
# We extract every path token after the two-char status code.
parse_paths() {
local status_output="$1"
echo "$status_output" | awk '
/^[ MADRCU?!]{2} / {
# Drop the two-char status + space
rest = substr($0, 4)
# Handle rename: "old -> new"
if (index(rest, " -> ") > 0) {
n = split(rest, parts, " -> ")
for (i = 1; i <= n; i++) {
p = parts[i]
gsub(/^"/, "", p); gsub(/"$/, "", p)
if (p != "") print p
}
} else {
gsub(/^"/, "", rest); gsub(/"$/, "", rest)
if (rest != "") print rest
}
}
'
}

BEFORE_PATHS=$(parse_paths "$BEFORE" | sort)
AFTER_PATHS=$(parse_paths "$AFTER" | sort)

if [ -z "$AFTER_PATHS" ]; then
exit 0
fi

# Find paths present in AFTER but not in BEFORE — these were newly dirtied
# (modified, created, or renamed-to) by the Bash command.
NEW_PATHS=$(comm -13 <(echo "$BEFORE_PATHS") <(echo "$AFTER_PATHS")) || true

if [ -z "$NEW_PATHS" ]; then
exit 0
fi

# Also exclude paths that were already tracked by track-edits.sh or other hooks
# (i.e. already in the session-edits.log) so we don't double-log.
LOG_FILE="$PROJECT_DIR/.claude/session-edits.log"
ALREADY_LOGGED=""
if [ -f "$LOG_FILE" ] && [ -s "$LOG_FILE" ]; then
ALREADY_LOGGED=$(awk '{print $2}' "$LOG_FILE" | sort -u)
fi

mkdir -p "$(dirname "$LOG_FILE")"
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)

while IFS= read -r rel_path; do
if [ -z "$rel_path" ]; then
continue
fi
# Skip if already in the log from a prior hook (Edit/Write/track-moves)
if [ -n "$ALREADY_LOGGED" ] && echo "$ALREADY_LOGGED" | grep -qxF "$rel_path"; then
continue
fi
echo "$TS $rel_path" >> "$LOG_FILE"
done <<< "$NEW_PATHS"

exit 0
10 changes: 10 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "p=\"${CLAUDE_PROJECT_DIR}\"; [ -d \"$p/.claude/hooks\" ] || p=\"$(git rev-parse --show-toplevel 2>/dev/null)\"; [ -d \"$p/.claude/hooks\" ] || exit 0; bash \"$p/.claude/hooks/snapshot-pre-bash.sh\"",
"timeout": 5
},
{
"type": "command",
"command": "p=\"${CLAUDE_PROJECT_DIR}\"; [ -d \"$p/.claude/hooks\" ] || p=\"$(git rev-parse --show-toplevel 2>/dev/null)\"; [ -d \"$p/.claude/hooks\" ] || exit 0; bash \"$p/.claude/hooks/check-readme.sh\"",
Expand Down Expand Up @@ -79,6 +84,11 @@
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "p=\"${CLAUDE_PROJECT_DIR}\"; [ -d \"$p/.claude/hooks\" ] || p=\"$(git rev-parse --show-toplevel 2>/dev/null)\"; [ -d \"$p/.claude/hooks\" ] || exit 0; bash \"$p/.claude/hooks/track-bash-writes.sh\"",
"timeout": 5
},
{
"type": "command",
"command": "p=\"${CLAUDE_PROJECT_DIR}\"; [ -d \"$p/.claude/hooks\" ] || p=\"$(git rev-parse --show-toplevel 2>/dev/null)\"; [ -d \"$p/.claude/hooks\" ] || exit 0; bash \"$p/.claude/hooks/track-moves.sh\"",
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/titan-run/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: titan-run
description: Run the full Titan Paradigm pipeline end-to-end by dispatching each phase to sub-agents with fresh context windows. Orchestrates recon → gauntlet → sync → forge → grind (+ repo-provided parity audit) automatically.
argument-hint: <path (default: .)> <--skip-recon> <--skip-gauntlet> <--start-from recon|gauntlet|sync|forge|grind|parity> <--gauntlet-batch-size 5> <--yes>
argument-hint: <path (default: .)> <--skip-recon> <--skip-gauntlet> <--start-from recon|gauntlet|sync|forge|grind|parity|close> <--gauntlet-batch-size 5> <--yes>
allowed-tools: Agent, Read, Bash, Glob, Write, Edit
---

Expand Down Expand Up @@ -50,7 +50,7 @@ You are the **orchestrator** for the full Titan Paradigm pipeline. Your job is t
node -e "const fs=require('fs');const s=JSON.parse(fs.readFileSync('.codegraph/titan/titan-state.json','utf8'));s.phaseTimestamps=s.phaseTimestamps||{};s.phaseTimestamps['<PHASE>']=s.phaseTimestamps['<PHASE>']||{};s.phaseTimestamps['<PHASE>'].completedAt=new Date().toISOString();fs.writeFileSync('.codegraph/titan/titan-state.json',JSON.stringify(s,null,2));"
```

Replace `<PHASE>` with `recon`, `gauntlet`, `sync`, `forge`, `parity`, or `close`. **Run the start command immediately before dispatching each phase's first sub-agent, and the completion command immediately after post-phase validation passes.** If resuming a phase (e.g., gauntlet loop iteration 2+), do NOT overwrite `startedAt` — only set it if it doesn't already exist.
Replace `<PHASE>` with `recon`, `gauntlet`, `sync`, `forge`, `grind`, `parity`, or `close`. **Run the start command immediately before dispatching each phase's first sub-agent, and the completion command immediately after post-phase validation passes.** If resuming a phase (e.g., gauntlet loop iteration 2+), do NOT overwrite `startedAt` — only set it if it doesn't already exist.

**Timestamp validation:** After recording `completedAt` for any phase, verify `startedAt < completedAt`:
```bash
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ dist/
coverage/
.env
grammars/*.wasm
crates/codegraph-core/index.js
crates/codegraph-core/index.d.ts
crates/codegraph-core/*.node
.claude/session-edits.log
.claude/worktrees/
generated/DEPENDENCIES.md
Expand Down
1 change: 0 additions & 1 deletion tests/unit/visitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import { describe, expect, it } from 'vitest';

// We need a tree-sitter tree to test. Use the JS parser.
// biome-ignore lint/suspicious/noExplicitAny: tree-sitter parser type is complex and not worth typing for tests
let parse: any;

async function ensureParser() {
Expand Down
Loading