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.
Files changed (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. 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()