anvil-dev-framework 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- package/scripts/verify.sh +255 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
parse_transcript.py - Lightweight bash wrapper for transcript parsing
|
|
4
|
+
|
|
5
|
+
Returns JSON output suitable for consumption by bash scripts (statusline.sh).
|
|
6
|
+
Designed for minimal latency with caching.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
# Get current todo for statusline
|
|
10
|
+
python3 parse_transcript.py todo /path/to/transcript.jsonl
|
|
11
|
+
|
|
12
|
+
# Get tool activity for HUD
|
|
13
|
+
python3 parse_transcript.py tools /path/to/transcript.jsonl
|
|
14
|
+
|
|
15
|
+
# Get both as combined JSON
|
|
16
|
+
python3 parse_transcript.py all /path/to/transcript.jsonl
|
|
17
|
+
|
|
18
|
+
Output (todo):
|
|
19
|
+
{"content": "Writing auth tests", "completed": 3, "total": 7}
|
|
20
|
+
or empty {} if no todos
|
|
21
|
+
|
|
22
|
+
Output (tools):
|
|
23
|
+
{"running": [{"name": "Edit", "target": "..."}], "completed": {"Edit": 4, "Read": 2}}
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
# Add lib directory to path
|
|
31
|
+
lib_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "lib")
|
|
32
|
+
sys.path.insert(0, lib_dir)
|
|
33
|
+
|
|
34
|
+
from transcript_parser import TranscriptParser # noqa: E402
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_todo_output(parser: TranscriptParser, transcript_path: str) -> dict:
|
|
38
|
+
"""Format todo state for statusline consumption."""
|
|
39
|
+
todos = parser.get_todos(transcript_path)
|
|
40
|
+
|
|
41
|
+
if not todos.in_progress and todos.total == 0:
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
result = {
|
|
45
|
+
"total": todos.total,
|
|
46
|
+
"completed": todos.completed,
|
|
47
|
+
"pending": todos.pending,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if todos.in_progress:
|
|
51
|
+
result["content"] = todos.in_progress.content
|
|
52
|
+
result["activeForm"] = todos.in_progress.active_form or todos.in_progress.content
|
|
53
|
+
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def format_tools_output(parser: TranscriptParser, transcript_path: str) -> dict:
|
|
58
|
+
"""Format tool activity for HUD consumption."""
|
|
59
|
+
activity = parser.parse(transcript_path)
|
|
60
|
+
|
|
61
|
+
result = {
|
|
62
|
+
"running": [],
|
|
63
|
+
"completed": {},
|
|
64
|
+
"errors": activity.error_count,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for tool in activity.running:
|
|
68
|
+
result["running"].append({
|
|
69
|
+
"name": tool.name,
|
|
70
|
+
"target": tool.target,
|
|
71
|
+
"started_at": tool.started_at,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
result["completed"] = dict(activity.get_top_completed(limit=6))
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main():
|
|
80
|
+
if len(sys.argv) < 3:
|
|
81
|
+
print(json.dumps({"error": "Usage: parse_transcript.py <todo|tools|all> <path>"}))
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
command = sys.argv[1]
|
|
85
|
+
transcript_path = sys.argv[2]
|
|
86
|
+
|
|
87
|
+
# Validate path
|
|
88
|
+
if not os.path.exists(transcript_path):
|
|
89
|
+
print(json.dumps({}))
|
|
90
|
+
sys.exit(0)
|
|
91
|
+
|
|
92
|
+
parser = TranscriptParser(cache_ttl=1.0)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
if command == "todo":
|
|
96
|
+
result = format_todo_output(parser, transcript_path)
|
|
97
|
+
elif command == "tools":
|
|
98
|
+
result = format_tools_output(parser, transcript_path)
|
|
99
|
+
elif command == "all":
|
|
100
|
+
result = {
|
|
101
|
+
"todo": format_todo_output(parser, transcript_path),
|
|
102
|
+
"tools": format_tools_output(parser, transcript_path),
|
|
103
|
+
}
|
|
104
|
+
else:
|
|
105
|
+
result = {"error": f"Unknown command: {command}"}
|
|
106
|
+
|
|
107
|
+
print(json.dumps(result))
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
# Graceful degradation - return empty on any error
|
|
111
|
+
print(json.dumps({"error": str(e)}))
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
main()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# post_merge_cleanup.sh - Post-Merge Cleanup Suggestions (ANV-242)
|
|
4
|
+
# =============================================================================
|
|
5
|
+
#
|
|
6
|
+
# Suggests cleanup for related branches and stashes after a PR is merged.
|
|
7
|
+
# This script is called after gh pr merge to help maintain repository hygiene.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# ./post_merge_cleanup.sh [branch_name]
|
|
11
|
+
# Called automatically via PostToolUse hook after gh pr merge
|
|
12
|
+
#
|
|
13
|
+
# Arguments:
|
|
14
|
+
# branch_name - The branch that was merged (optional, detected from git if not provided)
|
|
15
|
+
#
|
|
16
|
+
# Output:
|
|
17
|
+
# Cleanup suggestions for related artifacts (non-blocking)
|
|
18
|
+
#
|
|
19
|
+
# Exit Codes:
|
|
20
|
+
# 0 - Always (suggestions are non-blocking)
|
|
21
|
+
#
|
|
22
|
+
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# Configuration
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# Functions
|
|
33
|
+
# =============================================================================
|
|
34
|
+
|
|
35
|
+
# Extract issue references from text (e.g., ANV-123)
|
|
36
|
+
extract_issue_refs() {
|
|
37
|
+
local text="$1"
|
|
38
|
+
echo "$text" | grep -oE 'ANV-[0-9]+' | sort -u || true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Check if a local branch exists
|
|
42
|
+
branch_exists() {
|
|
43
|
+
local branch="$1"
|
|
44
|
+
git show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Get stashes related to an issue reference
|
|
48
|
+
get_related_stashes() {
|
|
49
|
+
local issue_ref="$1"
|
|
50
|
+
git stash list 2>/dev/null | grep -i "$issue_ref" || true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Get the most recently merged branch from reflog
|
|
54
|
+
get_recently_merged_branch() {
|
|
55
|
+
# Look for recent merge commits in reflog
|
|
56
|
+
local merge_info
|
|
57
|
+
merge_info=$(git reflog --pretty=format:"%gs" -n 20 | grep -m1 "merge" || true)
|
|
58
|
+
|
|
59
|
+
if [[ -n "$merge_info" ]]; then
|
|
60
|
+
# Extract branch name from merge message
|
|
61
|
+
echo "$merge_info" | sed -n 's/.*merge \([^ ]*\).*/\1/p' | head -1
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# =============================================================================
|
|
66
|
+
# Main
|
|
67
|
+
# =============================================================================
|
|
68
|
+
|
|
69
|
+
main() {
|
|
70
|
+
local merged_branch="${1:-}"
|
|
71
|
+
|
|
72
|
+
# If no branch provided, try to detect from recent merge
|
|
73
|
+
if [[ -z "$merged_branch" ]]; then
|
|
74
|
+
merged_branch=$(get_recently_merged_branch)
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
if [[ -z "$merged_branch" ]]; then
|
|
78
|
+
# No merged branch detected, exit silently
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Extract issue references from branch name
|
|
83
|
+
local issue_refs
|
|
84
|
+
issue_refs=$(extract_issue_refs "$merged_branch")
|
|
85
|
+
|
|
86
|
+
if [[ -z "$issue_refs" ]]; then
|
|
87
|
+
# No issue references found, exit silently
|
|
88
|
+
exit 0
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Check for cleanup opportunities
|
|
92
|
+
local has_suggestions=false
|
|
93
|
+
local suggestions=""
|
|
94
|
+
|
|
95
|
+
# Check if local branch still exists
|
|
96
|
+
if branch_exists "$merged_branch"; then
|
|
97
|
+
has_suggestions=true
|
|
98
|
+
suggestions+="
|
|
99
|
+
- **Local branch**: \`$merged_branch\` still exists
|
|
100
|
+
Delete with: \`git branch -D $merged_branch\`"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Check for related stashes
|
|
104
|
+
for issue_ref in $issue_refs; do
|
|
105
|
+
local related_stashes
|
|
106
|
+
related_stashes=$(get_related_stashes "$issue_ref")
|
|
107
|
+
|
|
108
|
+
if [[ -n "$related_stashes" ]]; then
|
|
109
|
+
has_suggestions=true
|
|
110
|
+
local stash_count
|
|
111
|
+
stash_count=$(echo "$related_stashes" | wc -l | tr -d ' ')
|
|
112
|
+
suggestions+="
|
|
113
|
+
- **Stashes**: $stash_count stash(es) reference $issue_ref
|
|
114
|
+
Review with: \`git stash list | grep -i $issue_ref\`"
|
|
115
|
+
fi
|
|
116
|
+
done
|
|
117
|
+
|
|
118
|
+
# Output suggestions if any
|
|
119
|
+
if [[ "$has_suggestions" == true ]]; then
|
|
120
|
+
echo ""
|
|
121
|
+
echo "## Cleanup Suggestions for $merged_branch"
|
|
122
|
+
echo ""
|
|
123
|
+
echo "The following artifacts may be obsolete after this merge:"
|
|
124
|
+
echo "$suggestions"
|
|
125
|
+
echo ""
|
|
126
|
+
echo "Run \`/cleanup\` to address these issues."
|
|
127
|
+
echo ""
|
|
128
|
+
fi
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Run main with all arguments
|
|
132
|
+
main "$@"
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# post_tool_format.sh - PostToolUse Formatting Hook (ANV-143)
|
|
4
|
+
# =============================================================================
|
|
5
|
+
#
|
|
6
|
+
# Automatically formats code files after every Edit or Write operation.
|
|
7
|
+
# This catches the "last 10%" of formatting issues that Claude often misses.
|
|
8
|
+
#
|
|
9
|
+
# Usage: Called automatically by Claude Code on PostToolUse events
|
|
10
|
+
#
|
|
11
|
+
# Environment Variables:
|
|
12
|
+
# CLAUDE_FILE_PATH - Path to the file that was edited/written
|
|
13
|
+
# ANVIL_FORMAT_LOG - Set to "true" to enable logging
|
|
14
|
+
# ANVIL_FORMAT_TIMEOUT - Timeout in seconds (default: 5)
|
|
15
|
+
#
|
|
16
|
+
# Exit Codes:
|
|
17
|
+
# 0 - Always (formatting should never block operations)
|
|
18
|
+
#
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# Configuration
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
FILE_PATH="${CLAUDE_FILE_PATH:-}"
|
|
25
|
+
ENABLE_LOGGING="${ANVIL_FORMAT_LOG:-false}"
|
|
26
|
+
TIMEOUT_SECONDS="${ANVIL_FORMAT_TIMEOUT:-5}"
|
|
27
|
+
LOG_FILE=".claude/logs/format.log"
|
|
28
|
+
|
|
29
|
+
# Start timing
|
|
30
|
+
START_TIME=$(date +%s%N 2>/dev/null || date +%s)
|
|
31
|
+
|
|
32
|
+
# =============================================================================
|
|
33
|
+
# Logging
|
|
34
|
+
# =============================================================================
|
|
35
|
+
|
|
36
|
+
log_event() {
|
|
37
|
+
local message="$1"
|
|
38
|
+
if [[ "$ENABLE_LOGGING" == "true" ]]; then
|
|
39
|
+
local timestamp
|
|
40
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
41
|
+
mkdir -p "$(dirname "$LOG_FILE")"
|
|
42
|
+
echo "[$timestamp] $message" >> "$LOG_FILE"
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
log_timing() {
|
|
47
|
+
if [[ "$ENABLE_LOGGING" == "true" ]]; then
|
|
48
|
+
local end_time
|
|
49
|
+
end_time=$(date +%s%N 2>/dev/null || date +%s)
|
|
50
|
+
local duration_ms
|
|
51
|
+
if [[ ${#START_TIME} -gt 10 ]]; then
|
|
52
|
+
# Nanoseconds available
|
|
53
|
+
duration_ms=$(( (end_time - START_TIME) / 1000000 ))
|
|
54
|
+
else
|
|
55
|
+
# Fallback to seconds
|
|
56
|
+
duration_ms=$(( (end_time - START_TIME) * 1000 ))
|
|
57
|
+
fi
|
|
58
|
+
log_event "Formatting completed in ${duration_ms}ms"
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# =============================================================================
|
|
63
|
+
# Path Validation
|
|
64
|
+
# =============================================================================
|
|
65
|
+
|
|
66
|
+
# Skip if no file path provided
|
|
67
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Skip non-existent files
|
|
72
|
+
if [[ ! -f "$FILE_PATH" ]]; then
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Skip binary and excluded paths
|
|
77
|
+
is_excluded() {
|
|
78
|
+
case "$FILE_PATH" in
|
|
79
|
+
# Package managers
|
|
80
|
+
*/node_modules/*|*/vendor/*|*/.venv/*|*/venv/*)
|
|
81
|
+
return 0
|
|
82
|
+
;;
|
|
83
|
+
# Version control
|
|
84
|
+
*/.git/*|*/.svn/*|*/.hg/*)
|
|
85
|
+
return 0
|
|
86
|
+
;;
|
|
87
|
+
# Build outputs
|
|
88
|
+
*/dist/*|*/build/*|*/out/*|*/.next/*|*/__pycache__/*)
|
|
89
|
+
return 0
|
|
90
|
+
;;
|
|
91
|
+
# Lock files
|
|
92
|
+
*/package-lock.json|*/yarn.lock|*/pnpm-lock.yaml|*/Cargo.lock|*/poetry.lock)
|
|
93
|
+
return 0
|
|
94
|
+
;;
|
|
95
|
+
# Binary files
|
|
96
|
+
*.png|*.jpg|*.jpeg|*.gif|*.ico|*.svg|*.webp)
|
|
97
|
+
return 0
|
|
98
|
+
;;
|
|
99
|
+
*.woff|*.woff2|*.ttf|*.eot|*.otf)
|
|
100
|
+
return 0
|
|
101
|
+
;;
|
|
102
|
+
*.pdf|*.zip|*.tar|*.gz|*.bz2)
|
|
103
|
+
return 0
|
|
104
|
+
;;
|
|
105
|
+
# Generated files
|
|
106
|
+
*.min.js|*.min.css|*.bundle.js|*.map)
|
|
107
|
+
return 0
|
|
108
|
+
;;
|
|
109
|
+
*)
|
|
110
|
+
return 1
|
|
111
|
+
;;
|
|
112
|
+
esac
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if is_excluded; then
|
|
116
|
+
exit 0
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
log_event "Formatting: $FILE_PATH"
|
|
120
|
+
|
|
121
|
+
# =============================================================================
|
|
122
|
+
# Formatter Execution with Timeout
|
|
123
|
+
# =============================================================================
|
|
124
|
+
|
|
125
|
+
run_formatter() {
|
|
126
|
+
local formatter_cmd="$1"
|
|
127
|
+
|
|
128
|
+
# Use timeout if available
|
|
129
|
+
if command -v timeout &> /dev/null; then
|
|
130
|
+
timeout "${TIMEOUT_SECONDS}s" bash -c "$formatter_cmd" 2>/dev/null || true
|
|
131
|
+
else
|
|
132
|
+
# macOS fallback: use perl for timeout
|
|
133
|
+
perl -e 'alarm shift; exec @ARGV' "$TIMEOUT_SECONDS" bash -c "$formatter_cmd" 2>/dev/null || true
|
|
134
|
+
fi
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# =============================================================================
|
|
138
|
+
# Format Based on File Type
|
|
139
|
+
# =============================================================================
|
|
140
|
+
|
|
141
|
+
case "$FILE_PATH" in
|
|
142
|
+
# TypeScript/JavaScript/Web files (Prettier)
|
|
143
|
+
*.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs)
|
|
144
|
+
if command -v npx &> /dev/null; then
|
|
145
|
+
run_formatter "npx prettier --write '$FILE_PATH'"
|
|
146
|
+
log_event "Formatted with prettier (JS/TS)"
|
|
147
|
+
fi
|
|
148
|
+
;;
|
|
149
|
+
|
|
150
|
+
*.json|*.md|*.css|*.scss|*.less|*.html|*.vue|*.svelte)
|
|
151
|
+
if command -v npx &> /dev/null; then
|
|
152
|
+
run_formatter "npx prettier --write '$FILE_PATH'"
|
|
153
|
+
log_event "Formatted with prettier (web)"
|
|
154
|
+
fi
|
|
155
|
+
;;
|
|
156
|
+
|
|
157
|
+
# Python (black or ruff)
|
|
158
|
+
*.py)
|
|
159
|
+
if command -v black &> /dev/null; then
|
|
160
|
+
run_formatter "black --quiet '$FILE_PATH'"
|
|
161
|
+
log_event "Formatted with black"
|
|
162
|
+
elif command -v ruff &> /dev/null; then
|
|
163
|
+
run_formatter "ruff format --quiet '$FILE_PATH'"
|
|
164
|
+
log_event "Formatted with ruff"
|
|
165
|
+
fi
|
|
166
|
+
;;
|
|
167
|
+
|
|
168
|
+
# Go (gofmt)
|
|
169
|
+
*.go)
|
|
170
|
+
if command -v gofmt &> /dev/null; then
|
|
171
|
+
run_formatter "gofmt -w '$FILE_PATH'"
|
|
172
|
+
log_event "Formatted with gofmt"
|
|
173
|
+
fi
|
|
174
|
+
;;
|
|
175
|
+
|
|
176
|
+
# Shell scripts (shfmt)
|
|
177
|
+
*.sh|*.bash)
|
|
178
|
+
if command -v shfmt &> /dev/null; then
|
|
179
|
+
run_formatter "shfmt -w '$FILE_PATH'"
|
|
180
|
+
log_event "Formatted with shfmt"
|
|
181
|
+
fi
|
|
182
|
+
;;
|
|
183
|
+
|
|
184
|
+
# Rust (rustfmt)
|
|
185
|
+
*.rs)
|
|
186
|
+
if command -v rustfmt &> /dev/null; then
|
|
187
|
+
run_formatter "rustfmt --quiet '$FILE_PATH'"
|
|
188
|
+
log_event "Formatted with rustfmt"
|
|
189
|
+
fi
|
|
190
|
+
;;
|
|
191
|
+
|
|
192
|
+
# YAML (prettier or yamlfmt)
|
|
193
|
+
*.yaml|*.yml)
|
|
194
|
+
if command -v npx &> /dev/null; then
|
|
195
|
+
run_formatter "npx prettier --write '$FILE_PATH'"
|
|
196
|
+
log_event "Formatted with prettier (yaml)"
|
|
197
|
+
fi
|
|
198
|
+
;;
|
|
199
|
+
|
|
200
|
+
# TOML (taplo)
|
|
201
|
+
*.toml)
|
|
202
|
+
if command -v taplo &> /dev/null; then
|
|
203
|
+
run_formatter "taplo format '$FILE_PATH'"
|
|
204
|
+
log_event "Formatted with taplo"
|
|
205
|
+
fi
|
|
206
|
+
;;
|
|
207
|
+
|
|
208
|
+
*)
|
|
209
|
+
# Unknown file type - skip silently
|
|
210
|
+
log_event "Skipped: unknown file type"
|
|
211
|
+
;;
|
|
212
|
+
esac
|
|
213
|
+
|
|
214
|
+
log_timing
|
|
215
|
+
exit 0
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""
|
|
7
|
+
ralph_context_monitor.py - CCS Context Monitor for Ralph Mode (ANV-199)
|
|
8
|
+
|
|
9
|
+
Monitors context percentage during Ralph autonomous execution and triggers
|
|
10
|
+
checkpoints when thresholds are exceeded.
|
|
11
|
+
|
|
12
|
+
Called as a PostToolUse hook during Ralph sessions. Reads context data from
|
|
13
|
+
stdin (JSON from Claude Code) and checks against CCS thresholds.
|
|
14
|
+
|
|
15
|
+
Thresholds:
|
|
16
|
+
- L1 (70-84%): Warning only, no checkpoint
|
|
17
|
+
- L2 (85-94%): Trigger checkpoint, allow current edit to complete
|
|
18
|
+
- L3 (95%+): Emergency checkpoint, signal immediate stop
|
|
19
|
+
|
|
20
|
+
Output signals (parsed by Claude Code):
|
|
21
|
+
- CCS_WARNING|L1|<percent>|<message>
|
|
22
|
+
- CCS_CHECKPOINT_TRIGGERED|L2|<percent>|<handoff_file>
|
|
23
|
+
- CCS_EMERGENCY_STOP|L3|<percent>|<handoff_file>
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
# Add global lib to path for ralph_state
|
|
33
|
+
_global_lib = Path(__file__).parent.parent / "lib"
|
|
34
|
+
if _global_lib.exists():
|
|
35
|
+
sys.path.insert(0, str(_global_lib))
|
|
36
|
+
|
|
37
|
+
# CCS Thresholds
|
|
38
|
+
THRESHOLD_L1 = 70
|
|
39
|
+
THRESHOLD_L2 = 85
|
|
40
|
+
THRESHOLD_L3 = 95
|
|
41
|
+
|
|
42
|
+
# State file location
|
|
43
|
+
DEFAULT_STATE_FILE = ".claude/ralph-state.json"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_ralph_active() -> bool:
|
|
47
|
+
"""Check if Ralph mode is active by looking for state file."""
|
|
48
|
+
return Path(DEFAULT_STATE_FILE).exists()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_ralph_state() -> dict:
|
|
52
|
+
"""Load Ralph state from JSON file."""
|
|
53
|
+
try:
|
|
54
|
+
with open(DEFAULT_STATE_FILE) as f:
|
|
55
|
+
return json.load(f)
|
|
56
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def save_ralph_state(state: dict) -> None:
|
|
61
|
+
"""Save Ralph state to JSON file."""
|
|
62
|
+
Path(DEFAULT_STATE_FILE).parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
with open(DEFAULT_STATE_FILE, "w") as f:
|
|
64
|
+
json.dump(state, f, indent=2)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_context_percent(input_data: dict) -> int:
|
|
68
|
+
"""Extract context percentage from Claude Code JSON data.
|
|
69
|
+
|
|
70
|
+
Claude Code provides context window info in the input:
|
|
71
|
+
{
|
|
72
|
+
"context_window": {
|
|
73
|
+
"current_usage": {
|
|
74
|
+
"input_tokens": N,
|
|
75
|
+
"output_tokens": N,
|
|
76
|
+
"cache_read_input_tokens": N,
|
|
77
|
+
"cache_creation_input_tokens": N
|
|
78
|
+
},
|
|
79
|
+
"context_window_size": 200000
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
"""
|
|
83
|
+
context_window = input_data.get("context_window", {})
|
|
84
|
+
current_usage = context_window.get("current_usage", {})
|
|
85
|
+
context_limit = context_window.get("context_window_size", 200000)
|
|
86
|
+
|
|
87
|
+
if context_limit == 0:
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
# Context usage = input + cache write (what counts toward limit)
|
|
91
|
+
tokens_input = current_usage.get("input_tokens", 0)
|
|
92
|
+
tokens_cache_write = current_usage.get("cache_creation_input_tokens", 0)
|
|
93
|
+
context_usage = tokens_input + tokens_cache_write
|
|
94
|
+
|
|
95
|
+
return int((context_usage / context_limit) * 100)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_level(percent: int) -> str:
|
|
99
|
+
"""Determine CCS level from percentage."""
|
|
100
|
+
if percent >= THRESHOLD_L3:
|
|
101
|
+
return "L3"
|
|
102
|
+
elif percent >= THRESHOLD_L2:
|
|
103
|
+
return "L2"
|
|
104
|
+
elif percent >= THRESHOLD_L1:
|
|
105
|
+
return "L1"
|
|
106
|
+
return "L0"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def generate_handoff_filename() -> str:
|
|
110
|
+
"""Generate timestamped handoff filename."""
|
|
111
|
+
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M")
|
|
112
|
+
return f".claude/handoffs/{timestamp}-checkpoint.md"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def trigger_checkpoint(level: str, percent: int, state: dict) -> str:
|
|
116
|
+
"""Trigger a CCS checkpoint within Ralph iteration.
|
|
117
|
+
|
|
118
|
+
Updates state and returns the handoff file path.
|
|
119
|
+
"""
|
|
120
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
121
|
+
handoff_file = generate_handoff_filename()
|
|
122
|
+
|
|
123
|
+
# Get current todo item if available
|
|
124
|
+
todo_items = state.get("todo_items", [])
|
|
125
|
+
current_item = todo_items[0] if todo_items else ""
|
|
126
|
+
|
|
127
|
+
# Initialize or update context_checkpoint
|
|
128
|
+
state["context_checkpoint"] = {
|
|
129
|
+
"active": True,
|
|
130
|
+
"level": level,
|
|
131
|
+
"percent_at_checkpoint": percent,
|
|
132
|
+
"timestamp": timestamp,
|
|
133
|
+
"handoff_file": handoff_file,
|
|
134
|
+
"resume_summary": "", # Filled by handoff generation
|
|
135
|
+
"files_in_progress": [],
|
|
136
|
+
"current_todo_item": current_item,
|
|
137
|
+
"progress_on_item": "checkpoint triggered"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Add to context history
|
|
141
|
+
if "context_history" not in state:
|
|
142
|
+
state["context_history"] = []
|
|
143
|
+
|
|
144
|
+
state["context_history"].append({
|
|
145
|
+
"iteration": state.get("iteration", 0),
|
|
146
|
+
"peak_percent": percent,
|
|
147
|
+
"checkpoint": True,
|
|
148
|
+
"level": level,
|
|
149
|
+
"timestamp": timestamp
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
save_ralph_state(state)
|
|
153
|
+
return handoff_file
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def update_context_history(percent: int, state: dict) -> None:
|
|
157
|
+
"""Update context history without triggering checkpoint."""
|
|
158
|
+
if "context_history" not in state:
|
|
159
|
+
state["context_history"] = []
|
|
160
|
+
|
|
161
|
+
iteration = state.get("iteration", 0)
|
|
162
|
+
|
|
163
|
+
# Check if we already have an entry for this iteration
|
|
164
|
+
if state["context_history"]:
|
|
165
|
+
last_entry = state["context_history"][-1]
|
|
166
|
+
if last_entry.get("iteration") == iteration:
|
|
167
|
+
# Update peak if higher
|
|
168
|
+
if percent > last_entry.get("peak_percent", 0):
|
|
169
|
+
last_entry["peak_percent"] = percent
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
# Add new entry for this iteration (non-checkpoint)
|
|
173
|
+
state["context_history"].append({
|
|
174
|
+
"iteration": iteration,
|
|
175
|
+
"peak_percent": percent,
|
|
176
|
+
"checkpoint": False
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def main():
|
|
181
|
+
"""Main entry point for the hook."""
|
|
182
|
+
# Skip if not in Ralph mode
|
|
183
|
+
if not is_ralph_active():
|
|
184
|
+
sys.exit(0)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
# Read JSON from stdin (same format as statusline)
|
|
188
|
+
input_data = json.load(sys.stdin)
|
|
189
|
+
|
|
190
|
+
# Get context percentage
|
|
191
|
+
percent = get_context_percent(input_data)
|
|
192
|
+
|
|
193
|
+
# Load current Ralph state
|
|
194
|
+
state = load_ralph_state()
|
|
195
|
+
|
|
196
|
+
# Check if checkpoint already active (avoid re-triggering)
|
|
197
|
+
checkpoint = state.get("context_checkpoint", {})
|
|
198
|
+
if checkpoint.get("active", False):
|
|
199
|
+
# Already in checkpoint mode, don't re-trigger
|
|
200
|
+
sys.exit(0)
|
|
201
|
+
|
|
202
|
+
# Determine level
|
|
203
|
+
level = get_level(percent)
|
|
204
|
+
|
|
205
|
+
# Handle based on level
|
|
206
|
+
if level == "L3":
|
|
207
|
+
# Emergency: trigger checkpoint and signal immediate stop
|
|
208
|
+
handoff_file = trigger_checkpoint("L3", percent, state)
|
|
209
|
+
print(f"CCS_EMERGENCY_STOP|L3|{percent}|{handoff_file}")
|
|
210
|
+
sys.exit(1) # Signal emergency stop
|
|
211
|
+
|
|
212
|
+
elif level == "L2":
|
|
213
|
+
# Critical: trigger checkpoint, allow current edit to complete
|
|
214
|
+
handoff_file = trigger_checkpoint("L2", percent, state)
|
|
215
|
+
print(f"CCS_CHECKPOINT_TRIGGERED|L2|{percent}|{handoff_file}")
|
|
216
|
+
# Don't exit - let current operation complete
|
|
217
|
+
|
|
218
|
+
elif level == "L1":
|
|
219
|
+
# Warning: just log, no checkpoint
|
|
220
|
+
update_context_history(percent, state)
|
|
221
|
+
save_ralph_state(state)
|
|
222
|
+
print(f"CCS_WARNING|L1|{percent}|Context at {percent}%, approaching threshold")
|
|
223
|
+
|
|
224
|
+
else:
|
|
225
|
+
# L0: Normal operation, track history
|
|
226
|
+
update_context_history(percent, state)
|
|
227
|
+
save_ralph_state(state)
|
|
228
|
+
|
|
229
|
+
except json.JSONDecodeError:
|
|
230
|
+
# Invalid JSON input, skip silently
|
|
231
|
+
pass
|
|
232
|
+
except Exception:
|
|
233
|
+
# Don't crash Ralph on monitor errors
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
sys.exit(0)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
main()
|