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,435 @@
1
+ #!/bin/bash
2
+ # anvil-state.sh - Session state management library for Anvil framework
3
+ # Version: 1.0.0
4
+ #
5
+ # This library provides functions for reading and writing anvil-state.json,
6
+ # which tracks workflow phase, active specs/plans/issues, and cached data.
7
+ #
8
+ # Usage:
9
+ # source "path/to/anvil-state.sh"
10
+ # anvil_state_init
11
+ # anvil_state_set_phase "implement"
12
+ # anvil_state_set_issue "ANV-42"
13
+ #
14
+ # IMPORTANT: Add .claude/anvil-state.json to your project's .gitignore
15
+ # This file contains session-specific state and should not be committed.
16
+
17
+ set -euo pipefail
18
+
19
+ # ============================================================================
20
+ # Configuration
21
+ # ============================================================================
22
+
23
+ ANVIL_STATE_FILE="${ANVIL_STATE_FILE:-.claude/anvil-state.json}"
24
+ ANVIL_STATE_VERSION="1.0"
25
+ ANVIL_GIT_CACHE_TTL="${ANVIL_GIT_CACHE_TTL:-5}" # seconds
26
+
27
+ # ============================================================================
28
+ # Dependency Check
29
+ # ============================================================================
30
+
31
+ # Check if jq is installed and provide helpful error message
32
+ anvil_check_jq() {
33
+ if ! command -v jq &> /dev/null; then
34
+ echo "ERROR: jq is required but not installed." >&2
35
+ echo "" >&2
36
+ echo "Install jq:" >&2
37
+ echo " macOS: brew install jq" >&2
38
+ echo " Ubuntu: sudo apt-get install jq" >&2
39
+ echo " Fedora: sudo dnf install jq" >&2
40
+ echo " Windows: choco install jq" >&2
41
+ echo "" >&2
42
+ echo "For more info: https://stedolan.github.io/jq/download/" >&2
43
+ return 1
44
+ fi
45
+ return 0
46
+ }
47
+
48
+ # ============================================================================
49
+ # State File Management
50
+ # ============================================================================
51
+
52
+ # Get the state file path (creates .claude directory if needed)
53
+ anvil_state_path() {
54
+ local state_dir
55
+ state_dir="$(dirname "$ANVIL_STATE_FILE")"
56
+
57
+ if [[ ! -d "$state_dir" ]]; then
58
+ mkdir -p "$state_dir"
59
+ fi
60
+
61
+ echo "$ANVIL_STATE_FILE"
62
+ }
63
+
64
+ # Initialize state file with default structure if it doesn't exist
65
+ anvil_state_init() {
66
+ anvil_check_jq || return 1
67
+
68
+ local state_file
69
+ state_file="$(anvil_state_path)"
70
+
71
+ if [[ ! -f "$state_file" ]]; then
72
+ jq -n \
73
+ --arg version "$ANVIL_STATE_VERSION" \
74
+ --arg time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
75
+ '{
76
+ version: $version,
77
+ session: {
78
+ phase: null,
79
+ activeSpec: null,
80
+ activePlan: null,
81
+ activeIssue: null,
82
+ lastCommand: null,
83
+ lastCommandAt: null
84
+ },
85
+ cache: {
86
+ git: {
87
+ branch: null,
88
+ dirty: null,
89
+ cachedAt: null,
90
+ ttlSeconds: 5
91
+ }
92
+ },
93
+ meta: {
94
+ createdAt: $time,
95
+ updatedAt: $time
96
+ }
97
+ }' > "$state_file"
98
+ fi
99
+
100
+ echo "$state_file"
101
+ }
102
+
103
+ # Read the entire state file
104
+ anvil_state_read() {
105
+ anvil_check_jq || return 1
106
+
107
+ local state_file
108
+ state_file="$(anvil_state_path)"
109
+
110
+ if [[ -f "$state_file" ]]; then
111
+ cat "$state_file"
112
+ else
113
+ # Return empty state structure if file doesn't exist
114
+ jq -n '{version: "1.0", session: {}, cache: {}}'
115
+ fi
116
+ }
117
+
118
+ # Read a specific field from state (dot notation: session.phase)
119
+ anvil_state_get() {
120
+ local field="$1"
121
+ anvil_check_jq || return 1
122
+
123
+ local state_file
124
+ state_file="$(anvil_state_path)"
125
+
126
+ if [[ -f "$state_file" ]]; then
127
+ jq -r ".$field // empty" "$state_file" 2>/dev/null || echo ""
128
+ else
129
+ echo ""
130
+ fi
131
+ }
132
+
133
+ # ============================================================================
134
+ # State Update Functions
135
+ # ============================================================================
136
+
137
+ # Update a single field in state (creates file if needed)
138
+ anvil_state_set() {
139
+ local field="$1"
140
+ local value="$2"
141
+ anvil_check_jq || return 1
142
+
143
+ local state_file
144
+ state_file="$(anvil_state_init)"
145
+
146
+ local tmp_file
147
+ tmp_file="$(mktemp)"
148
+
149
+ # Update the field and the updatedAt timestamp
150
+ jq --arg value "$value" \
151
+ --arg time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
152
+ ".$field = \$value | .meta.updatedAt = \$time" \
153
+ "$state_file" > "$tmp_file" && mv "$tmp_file" "$state_file"
154
+ }
155
+
156
+ # Update multiple session fields at once
157
+ anvil_state_update() {
158
+ local phase="${1:-}"
159
+ local spec="${2:-}"
160
+ local plan="${3:-}"
161
+ local issue="${4:-}"
162
+ local command="${5:-}"
163
+
164
+ anvil_check_jq || return 1
165
+
166
+ local state_file
167
+ state_file="$(anvil_state_init)"
168
+
169
+ local tmp_file
170
+ tmp_file="$(mktemp)"
171
+
172
+ local time
173
+ time="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
174
+
175
+ # Build jq filter dynamically based on provided values
176
+ local jq_filter=".meta.updatedAt = \$time"
177
+ local jq_args=(--arg time "$time")
178
+
179
+ if [[ -n "$phase" ]]; then
180
+ jq_filter="$jq_filter | .session.phase = \$phase"
181
+ jq_args+=(--arg phase "$phase")
182
+ fi
183
+
184
+ if [[ -n "$spec" ]]; then
185
+ if [[ "$spec" == "null" || "$spec" == "clear" ]]; then
186
+ jq_filter="$jq_filter | .session.activeSpec = null"
187
+ else
188
+ jq_filter="$jq_filter | .session.activeSpec = \$spec"
189
+ jq_args+=(--arg spec "$spec")
190
+ fi
191
+ fi
192
+
193
+ if [[ -n "$plan" ]]; then
194
+ if [[ "$plan" == "null" || "$plan" == "clear" ]]; then
195
+ jq_filter="$jq_filter | .session.activePlan = null"
196
+ else
197
+ jq_filter="$jq_filter | .session.activePlan = \$plan"
198
+ jq_args+=(--arg plan "$plan")
199
+ fi
200
+ fi
201
+
202
+ if [[ -n "$issue" ]]; then
203
+ if [[ "$issue" == "null" || "$issue" == "clear" ]]; then
204
+ jq_filter="$jq_filter | .session.activeIssue = null"
205
+ else
206
+ jq_filter="$jq_filter | .session.activeIssue = \$issue"
207
+ jq_args+=(--arg issue "$issue")
208
+ fi
209
+ fi
210
+
211
+ if [[ -n "$command" ]]; then
212
+ jq_filter="$jq_filter | .session.lastCommand = \$command | .session.lastCommandAt = \$time"
213
+ jq_args+=(--arg command "$command")
214
+ fi
215
+
216
+ jq "${jq_args[@]}" "$jq_filter" "$state_file" > "$tmp_file" && mv "$tmp_file" "$state_file"
217
+ }
218
+
219
+ # ============================================================================
220
+ # Convenience Functions for Common Operations
221
+ # ============================================================================
222
+
223
+ # Set current workflow phase
224
+ anvil_state_set_phase() {
225
+ local phase="$1"
226
+ anvil_state_set "session.phase" "$phase"
227
+ }
228
+
229
+ # Set active spec ID
230
+ anvil_state_set_spec() {
231
+ local spec="$1"
232
+ anvil_state_set "session.activeSpec" "$spec"
233
+ }
234
+
235
+ # Set active plan ID
236
+ anvil_state_set_plan() {
237
+ local plan="$1"
238
+ anvil_state_set "session.activePlan" "$plan"
239
+ }
240
+
241
+ # Set active issue key
242
+ anvil_state_set_issue() {
243
+ local issue="$1"
244
+ anvil_state_set "session.activeIssue" "$issue"
245
+ }
246
+
247
+ # Record command execution
248
+ anvil_state_set_command() {
249
+ local command="$1"
250
+ local time
251
+ time="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
252
+
253
+ anvil_check_jq || return 1
254
+
255
+ local state_file
256
+ state_file="$(anvil_state_init)"
257
+
258
+ local tmp_file
259
+ tmp_file="$(mktemp)"
260
+
261
+ jq --arg cmd "$command" --arg time "$time" \
262
+ '.session.lastCommand = $cmd | .session.lastCommandAt = $time | .meta.updatedAt = $time' \
263
+ "$state_file" > "$tmp_file" && mv "$tmp_file" "$state_file"
264
+ }
265
+
266
+ # Clear session state (for /orient reset)
267
+ anvil_state_clear_session() {
268
+ anvil_check_jq || return 1
269
+
270
+ local state_file
271
+ state_file="$(anvil_state_init)"
272
+
273
+ local tmp_file
274
+ tmp_file="$(mktemp)"
275
+
276
+ local time
277
+ time="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
278
+
279
+ jq --arg time "$time" \
280
+ '.session.phase = null | .session.activeSpec = null | .session.activePlan = null | .session.activeIssue = null | .meta.updatedAt = $time' \
281
+ "$state_file" > "$tmp_file" && mv "$tmp_file" "$state_file"
282
+ }
283
+
284
+ # ============================================================================
285
+ # Git Cache Functions
286
+ # ============================================================================
287
+
288
+ # Check if git cache is still valid (within TTL)
289
+ anvil_git_cache_valid() {
290
+ anvil_check_jq || return 1
291
+
292
+ local state_file
293
+ state_file="$(anvil_state_path)"
294
+
295
+ if [[ ! -f "$state_file" ]]; then
296
+ return 1
297
+ fi
298
+
299
+ local cached_at
300
+ cached_at="$(jq -r '.cache.git.cachedAt // empty' "$state_file" 2>/dev/null)"
301
+
302
+ if [[ -z "$cached_at" ]]; then
303
+ return 1
304
+ fi
305
+
306
+ local ttl
307
+ ttl="$(jq -r '.cache.git.ttlSeconds // 5' "$state_file" 2>/dev/null)"
308
+
309
+ # Convert ISO timestamp to epoch seconds
310
+ local cached_epoch
311
+ local current_epoch
312
+
313
+ if [[ "$(uname)" == "Darwin" ]]; then
314
+ # macOS: Use TZ=UTC to ensure proper UTC parsing
315
+ cached_epoch="$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$cached_at" "+%s" 2>/dev/null || echo 0)"
316
+ else
317
+ # Linux: date -d handles ISO format natively with UTC suffix
318
+ cached_epoch="$(date -d "$cached_at" "+%s" 2>/dev/null || echo 0)"
319
+ fi
320
+ current_epoch="$(date -u "+%s")"
321
+
322
+ local age=$((current_epoch - cached_epoch))
323
+
324
+ if [[ $age -lt $ttl ]]; then
325
+ return 0 # Cache is valid
326
+ else
327
+ return 1 # Cache expired
328
+ fi
329
+ }
330
+
331
+ # Get git branch (from cache if valid, otherwise fresh)
332
+ anvil_git_branch() {
333
+ # Try cache first
334
+ if anvil_git_cache_valid; then
335
+ local cached_branch
336
+ cached_branch="$(anvil_state_get 'cache.git.branch')"
337
+ if [[ -n "$cached_branch" ]]; then
338
+ echo "$cached_branch"
339
+ return 0
340
+ fi
341
+ fi
342
+
343
+ # Get fresh value
344
+ local branch
345
+ branch="$(git branch --show-current 2>/dev/null || echo "")"
346
+
347
+ # Update cache
348
+ anvil_git_cache_update "$branch"
349
+
350
+ echo "$branch"
351
+ }
352
+
353
+ # Update git cache with fresh values
354
+ anvil_git_cache_update() {
355
+ local branch="${1:-}"
356
+
357
+ anvil_check_jq || return 1
358
+
359
+ # Get branch if not provided
360
+ if [[ -z "$branch" ]]; then
361
+ branch="$(git branch --show-current 2>/dev/null || echo "")"
362
+ fi
363
+
364
+ # Check if working tree is dirty
365
+ local dirty="false"
366
+ if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
367
+ dirty="true"
368
+ fi
369
+
370
+ local state_file
371
+ state_file="$(anvil_state_init)"
372
+
373
+ local tmp_file
374
+ tmp_file="$(mktemp)"
375
+
376
+ local time
377
+ time="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
378
+
379
+ jq --arg branch "$branch" \
380
+ --argjson dirty "$dirty" \
381
+ --arg time "$time" \
382
+ --argjson ttl "$ANVIL_GIT_CACHE_TTL" \
383
+ '.cache.git.branch = $branch | .cache.git.dirty = $dirty | .cache.git.cachedAt = $time | .cache.git.ttlSeconds = $ttl | .meta.updatedAt = $time' \
384
+ "$state_file" > "$tmp_file" && mv "$tmp_file" "$state_file"
385
+ }
386
+
387
+ # ============================================================================
388
+ # Utility Functions
389
+ # ============================================================================
390
+
391
+ # Pretty print the current state (for debugging)
392
+ anvil_state_debug() {
393
+ anvil_check_jq || return 1
394
+
395
+ local state_file
396
+ state_file="$(anvil_state_path)"
397
+
398
+ if [[ -f "$state_file" ]]; then
399
+ echo "=== Anvil State ($state_file) ==="
400
+ jq '.' "$state_file"
401
+ else
402
+ echo "State file does not exist: $state_file"
403
+ fi
404
+ }
405
+
406
+ # Check if state file exists and is valid JSON
407
+ anvil_state_valid() {
408
+ local state_file
409
+ state_file="$(anvil_state_path)"
410
+
411
+ if [[ ! -f "$state_file" ]]; then
412
+ return 1
413
+ fi
414
+
415
+ if jq empty "$state_file" 2>/dev/null; then
416
+ return 0
417
+ else
418
+ return 1
419
+ fi
420
+ }
421
+
422
+ # Repair corrupted state file by recreating it
423
+ anvil_state_repair() {
424
+ local state_file
425
+ state_file="$(anvil_state_path)"
426
+
427
+ if [[ -f "$state_file" ]]; then
428
+ local backup="${state_file}.bak"
429
+ mv "$state_file" "$backup"
430
+ echo "Backed up corrupted state to: $backup" >&2
431
+ fi
432
+
433
+ anvil_state_init
434
+ echo "Created fresh state file: $state_file" >&2
435
+ }