anvil-dev-framework 0.1.6 → 0.1.7

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 (48) hide show
  1. package/README.md +13 -12
  2. package/VERSION +1 -1
  3. package/docs/INSTALLATION.md +18 -0
  4. package/docs/command-reference.md +1 -1
  5. package/global/commands/orient.md +29 -0
  6. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  7. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  8. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  9. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  10. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  11. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  12. package/global/lib/__pycache__/git_utils.cpython-314.pyc +0 -0
  13. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  14. package/global/lib/__pycache__/handoff_generator.cpython-314.pyc +0 -0
  15. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  16. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  17. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  18. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  19. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  20. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  21. package/global/lib/__pycache__/orient_fast.cpython-314.pyc +0 -0
  22. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  23. package/global/lib/__pycache__/ralph_prompt_generator.cpython-314.pyc +0 -0
  24. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  25. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  26. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  27. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  28. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  29. package/global/lib/git_utils.py +267 -0
  30. package/global/lib/issue_models.py +28 -0
  31. package/global/lib/linear_provider.py +7 -0
  32. package/global/lib/orient_fast.py +24 -1
  33. package/global/tests/test_git_utils.py +160 -0
  34. package/global/tests/test_issue_models.py +40 -0
  35. package/global/tests/test_linear_provider.py +86 -0
  36. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +238 -1
  37. package/global/tools/anvil-memory/src/commands/ralph-iteration.ts +249 -0
  38. package/global/tools/anvil-memory/src/index.ts +2 -8
  39. package/package.json +1 -1
  40. package/scripts/anvil +7 -2
  41. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +0 -535
  42. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +0 -645
  43. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +0 -363
  44. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +0 -8
  45. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +0 -417
  46. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +0 -571
  47. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +0 -440
  48. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +0 -252
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ```
2
2
  ___ _ ___ _____ _
3
3
  / \ | \ | \ \ / /_ _| |
4
- / /_\ \ | \| |\ \ / / | || | v0.1.6.0 (alpha)
4
+ / /_\ \ | \| |\ \ / / | || | v0.1.7.0 (alpha)
5
5
  / _____ \| |\ | \ V / | || |___
6
6
  /_/ \_\_| \_| \_/ |___|_____|
7
7
 
@@ -10,7 +10,7 @@
10
10
  ══════════════════════════════════════════════════════════
11
11
  ```
12
12
 
13
- # Anvil Development Framework <sup>v0.1.6.0</sup>
13
+ # Anvil Development Framework <sup>v0.1.7.0</sup>
14
14
 
15
15
  > **A structured AI development system for solo builders who demand production-quality output.**
16
16
 
@@ -18,17 +18,18 @@ Anvil is a comprehensive framework for AI-assisted software development that com
18
18
 
19
19
  ---
20
20
 
21
- ## 📦 Latest Changes in v0.1.6.0
21
+ ## 📦 Latest Changes in v0.1.7.0
22
22
 
23
- *Released: 2026-01-07*
23
+ *Released: 2026-01-15*
24
24
 
25
- - **Ralph Wiggum Autonomous Execution** — Long-running unattended AI execution mode
26
- - `/ralph start` Initialize autonomous loop with task breakdown
27
- - Circuit breaker safety (stops stuck loops automatically)
28
- - Progress tracking to prevent repeated mistakes
29
- - Git checkpointing before each restart
30
- - **PostToolUse Formatting Hook** — Auto-format files after Edit/Write operations
31
- - **Usage Guidelines** — Clear guidance on when Ralph is appropriate vs standard workflow
25
+ - **npm Package Distribution** — Anvil now available via `npm install -g anvil-dev-framework`
26
+ - Also works with Bun: `bun install -g anvil-dev-framework`
27
+ - **Repository Hygiene System** Detect and clean up stale PRs, orphan branches, and accumulated stashes
28
+ - New `/cleanup` command with dry-run and force modes
29
+ - Integrated into `/healthcheck` and `/orient`
30
+ - **Context Checkpoint System Phase 1** — L1/L2/L3 visual indicators for context thresholds
31
+ - **Statusline v2 Fixes** — Per-agent costs, turns-until-compaction, heartbeat system
32
+ - **/sprint and /ready Integration** — Unified work prioritization commands
32
33
 
33
34
  See [CHANGELOG.md](CHANGELOG.md) for complete history.
34
35
 
@@ -154,7 +155,7 @@ npm install -g anvil-dev-framework
154
155
  anvil init
155
156
  ```
156
157
 
157
- **Option 3: Homebrew (macOS)**
158
+ **Option 3: Homebrew (macOS)** *(coming soon)*
158
159
  ```bash
159
160
  brew tap alexandercahiz/anvil
160
161
  brew install anvil
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.6.0
1
+ 0.1.7.1
@@ -16,6 +16,24 @@ A context engineering framework that transforms Claude Code from a reactive assi
16
16
 
17
17
  ---
18
18
 
19
+ ## Quick Install
20
+
21
+ For most users, the fastest way to get started:
22
+
23
+ ```bash
24
+ # Using bun (recommended)
25
+ bun install -g anvil-dev-framework
26
+ anvil init
27
+
28
+ # Or using npm
29
+ npm install -g anvil-dev-framework
30
+ anvil init
31
+ ```
32
+
33
+ This installs the CLI and initializes your project with the Anvil framework. For customization options and detailed setup, continue reading below.
34
+
35
+ ---
36
+
19
37
  ## Background: Research Foundation
20
38
 
21
39
  This framework synthesizes patterns from 15+ production systems:
@@ -104,7 +104,7 @@ bun install -g anvil-dev-framework
104
104
  npm install -g anvil-dev-framework
105
105
  ```
106
106
 
107
- **Option 3: Homebrew (macOS)**
107
+ **Option 3: Homebrew (macOS)** *(coming soon)*
108
108
  ```bash
109
109
  brew tap alexandercahiz/anvil
110
110
  brew install anvil
@@ -31,6 +31,35 @@ Options:
31
31
 
32
32
  ---
33
33
 
34
+ ## Post-Compaction Verification (CRITICAL)
35
+
36
+ **ALWAYS run this before any file reads or edits after session resume:**
37
+
38
+ ```bash
39
+ echo "=== Branch Verification ==="
40
+ git branch --show-current
41
+
42
+ echo "=== Working Tree Status ==="
43
+ git status --short
44
+
45
+ echo "=== Recent Changes ==="
46
+ git diff --stat HEAD~3..HEAD 2>/dev/null || echo "No recent commits"
47
+ ```
48
+
49
+ **Check results against expected context:**
50
+ - Is this the branch mentioned in the handoff/summary?
51
+ - Are there uncommitted changes that might conflict?
52
+ - Do recent commits match expected work?
53
+
54
+ **If mismatch detected:**
55
+ 1. STOP before making any edits
56
+ 2. Report discrepancy to user
57
+ 3. Switch branches or stash as needed
58
+
59
+ **Why this matters**: In 10/20 recent retros, work was done on the wrong branch after context compaction. This step prevents hours of rework from branch confusion.
60
+
61
+ ---
62
+
34
63
  ## Manual Execution Steps (Fallback)
35
64
 
36
65
  Use these if orient_fast.py is unavailable (e.g., on main before PR merge).
@@ -0,0 +1,267 @@
1
+ """
2
+ Git utility functions for safe repository operations.
3
+
4
+ Provides helpers for common git operations that handle edge cases gracefully,
5
+ such as untracked files blocking branch switches or divergent branches.
6
+
7
+ Usage:
8
+ from git_utils import safe_checkout, get_repo_status, stash_changes
9
+
10
+ # Safe branch switch (auto-stashes if needed)
11
+ success, message = safe_checkout('main')
12
+
13
+ # Check repository status
14
+ status = get_repo_status()
15
+ if status['has_changes']:
16
+ print(f"Uncommitted changes in: {status['changed_files']}")
17
+ """
18
+
19
+ import subprocess
20
+ from pathlib import Path
21
+ from typing import Optional
22
+
23
+
24
+ def run_git(*args: str, cwd: Optional[Path] = None) -> tuple[int, str, str]:
25
+ """
26
+ Run a git command and return (returncode, stdout, stderr).
27
+
28
+ Args:
29
+ *args: Git command arguments (e.g., 'status', '--porcelain')
30
+ cwd: Working directory (defaults to current directory)
31
+
32
+ Returns:
33
+ Tuple of (return_code, stdout, stderr)
34
+ """
35
+ result = subprocess.run(
36
+ ['git', *args],
37
+ capture_output=True,
38
+ text=True,
39
+ cwd=cwd
40
+ )
41
+ return result.returncode, result.stdout, result.stderr
42
+
43
+
44
+ def get_current_branch(cwd: Optional[Path] = None) -> Optional[str]:
45
+ """Get the current git branch name, or None if not in a repo."""
46
+ code, stdout, _ = run_git('branch', '--show-current', cwd=cwd)
47
+ if code == 0:
48
+ return stdout.strip()
49
+ return None
50
+
51
+
52
+ def get_repo_status(cwd: Optional[Path] = None) -> dict:
53
+ """
54
+ Get comprehensive repository status.
55
+
56
+ Returns:
57
+ Dictionary with:
58
+ - has_changes: bool - Any uncommitted changes
59
+ - has_staged: bool - Staged changes pending commit
60
+ - has_unstaged: bool - Modified files not staged
61
+ - has_untracked: bool - Untracked files present
62
+ - changed_files: list[str] - All changed file paths
63
+ - untracked_files: list[str] - Untracked file paths
64
+ - current_branch: str | None - Current branch name
65
+ """
66
+ code, stdout, _ = run_git('status', '--porcelain', cwd=cwd)
67
+
68
+ changed_files = []
69
+ untracked_files = []
70
+ has_staged = False
71
+ has_unstaged = False
72
+
73
+ if code == 0:
74
+ for line in stdout.splitlines():
75
+ if not line:
76
+ continue
77
+ status = line[:2]
78
+ filepath = line[3:]
79
+
80
+ changed_files.append(filepath)
81
+
82
+ if status.startswith('??'):
83
+ untracked_files.append(filepath)
84
+ else:
85
+ # First char is staged status, second is unstaged
86
+ if status[0] not in (' ', '?'):
87
+ has_staged = True
88
+ if status[1] not in (' ', '?'):
89
+ has_unstaged = True
90
+
91
+ return {
92
+ 'has_changes': bool(changed_files),
93
+ 'has_staged': has_staged,
94
+ 'has_unstaged': has_unstaged,
95
+ 'has_untracked': bool(untracked_files),
96
+ 'changed_files': changed_files,
97
+ 'untracked_files': untracked_files,
98
+ 'current_branch': get_current_branch(cwd),
99
+ }
100
+
101
+
102
+ def stash_changes(
103
+ message: Optional[str] = None,
104
+ include_untracked: bool = True,
105
+ cwd: Optional[Path] = None
106
+ ) -> tuple[bool, str]:
107
+ """
108
+ Stash current changes.
109
+
110
+ Args:
111
+ message: Optional stash message
112
+ include_untracked: Include untracked files in stash
113
+ cwd: Working directory
114
+
115
+ Returns:
116
+ Tuple of (success, message)
117
+ """
118
+ args = ['stash', 'push']
119
+ if include_untracked:
120
+ args.append('-u')
121
+ if message:
122
+ args.extend(['-m', message])
123
+
124
+ code, stdout, stderr = run_git(*args, cwd=cwd)
125
+
126
+ if code == 0:
127
+ return True, stdout.strip() or "Changes stashed"
128
+ return False, stderr.strip()
129
+
130
+
131
+ def safe_checkout(
132
+ branch: str,
133
+ stash_if_needed: bool = True,
134
+ cwd: Optional[Path] = None
135
+ ) -> tuple[bool, str]:
136
+ """
137
+ Switch branches safely, stashing changes if needed.
138
+
139
+ This function handles the common case where uncommitted or untracked
140
+ files would block a branch switch by automatically stashing them first.
141
+
142
+ Args:
143
+ branch: Target branch name
144
+ stash_if_needed: Auto-stash changes before checkout (default True)
145
+ cwd: Working directory
146
+
147
+ Returns:
148
+ Tuple of (success, message) where message describes what happened
149
+
150
+ Examples:
151
+ >>> success, msg = safe_checkout('main')
152
+ >>> print(msg)
153
+ 'Switched to main (changes stashed)'
154
+
155
+ >>> success, msg = safe_checkout('feature/new', stash_if_needed=False)
156
+ >>> print(msg) # If changes present
157
+ 'error: Your local changes would be overwritten...'
158
+ """
159
+ status = get_repo_status(cwd)
160
+ stash_created = False
161
+
162
+ if status['has_changes'] and stash_if_needed:
163
+ stash_msg = f"Auto-stash before checkout to {branch}"
164
+ success, stash_result = stash_changes(message=stash_msg, cwd=cwd)
165
+ if success:
166
+ stash_created = True
167
+ else:
168
+ return False, f"Failed to stash changes: {stash_result}"
169
+
170
+ # Attempt checkout
171
+ code, stdout, stderr = run_git('checkout', branch, cwd=cwd)
172
+
173
+ if code == 0:
174
+ msg = f"Switched to {branch}"
175
+ if stash_created:
176
+ msg += " (changes stashed)"
177
+ return True, msg
178
+ else:
179
+ # If stash was created but checkout failed, pop it back
180
+ if stash_created:
181
+ run_git('stash', 'pop', cwd=cwd)
182
+ return False, stderr.strip()
183
+
184
+
185
+ def get_divergence_status(cwd: Optional[Path] = None) -> dict:
186
+ """
187
+ Check if local branch has diverged from remote.
188
+
189
+ Returns:
190
+ Dictionary with:
191
+ - ahead: int - Commits ahead of remote
192
+ - behind: int - Commits behind remote
193
+ - diverged: bool - True if both ahead and behind
194
+ - remote_branch: str | None - Tracked remote branch
195
+ """
196
+ # Get tracking branch
197
+ code, stdout, _ = run_git(
198
+ 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}',
199
+ cwd=cwd
200
+ )
201
+
202
+ if code != 0:
203
+ return {
204
+ 'ahead': 0,
205
+ 'behind': 0,
206
+ 'diverged': False,
207
+ 'remote_branch': None,
208
+ }
209
+
210
+ remote_branch = stdout.strip()
211
+
212
+ # Get ahead/behind counts
213
+ code, stdout, _ = run_git(
214
+ 'rev-list', '--left-right', '--count', f'HEAD...{remote_branch}',
215
+ cwd=cwd
216
+ )
217
+
218
+ if code == 0:
219
+ parts = stdout.strip().split('\t')
220
+ ahead = int(parts[0]) if len(parts) > 0 else 0
221
+ behind = int(parts[1]) if len(parts) > 1 else 0
222
+ else:
223
+ ahead, behind = 0, 0
224
+
225
+ return {
226
+ 'ahead': ahead,
227
+ 'behind': behind,
228
+ 'diverged': ahead > 0 and behind > 0,
229
+ 'remote_branch': remote_branch,
230
+ }
231
+
232
+
233
+ def list_stashes(cwd: Optional[Path] = None) -> list[dict]:
234
+ """
235
+ List all stashes with metadata.
236
+
237
+ Returns:
238
+ List of dicts with 'index', 'branch', 'message' keys
239
+ """
240
+ code, stdout, _ = run_git('stash', 'list', cwd=cwd)
241
+
242
+ stashes = []
243
+ if code == 0:
244
+ for line in stdout.splitlines():
245
+ if not line:
246
+ continue
247
+ # Format: stash@{0}: On branch-name: message
248
+ parts = line.split(': ', 2)
249
+ if len(parts) >= 2:
250
+ index = parts[0] # stash@{0}
251
+ branch_part = parts[1] if len(parts) > 1 else ""
252
+ message = parts[2] if len(parts) > 2 else ""
253
+
254
+ # Extract branch name from "On branch-name" or "WIP on branch-name"
255
+ branch = ""
256
+ if branch_part.startswith("On "):
257
+ branch = branch_part[3:]
258
+ elif branch_part.startswith("WIP on "):
259
+ branch = branch_part[7:]
260
+
261
+ stashes.append({
262
+ 'index': index,
263
+ 'branch': branch,
264
+ 'message': message,
265
+ })
266
+
267
+ return stashes
@@ -255,3 +255,31 @@ class Issue:
255
255
 
256
256
  def __repr__(self) -> str:
257
257
  return f"Issue(identifier={self.identifier!r}, title={self.title!r}, status={self.status})"
258
+
259
+ def __getitem__(self, key: str):
260
+ """
261
+ Support dict-style access for ergonomic usage.
262
+
263
+ Example:
264
+ issue['identifier'] # Returns issue.identifier
265
+ issue['status'] # Returns issue.status
266
+
267
+ Raises:
268
+ KeyError: If the field doesn't exist on Issue
269
+ """
270
+ if hasattr(self, key):
271
+ return getattr(self, key)
272
+ raise KeyError(f"Issue has no field '{key}'")
273
+
274
+ def get(self, key: str, default=None):
275
+ """
276
+ Support safe dict-style access with default.
277
+
278
+ Example:
279
+ issue.get('identifier') # Returns issue.identifier
280
+ issue.get('unknown', 'fallback') # Returns 'fallback'
281
+ """
282
+ try:
283
+ return self[key]
284
+ except KeyError:
285
+ return default
@@ -109,6 +109,13 @@ class LinearProvider(BaseProvider):
109
109
  self._team_id = team_id
110
110
  self.api_key = api_key or os.getenv("LINEAR_API_KEY")
111
111
 
112
+ # Validate team context is provided
113
+ if not team_key and not team_id:
114
+ raise ValueError(
115
+ "LinearProvider requires team_key or team_id. "
116
+ "Example: LinearProvider(team_key='ANV')"
117
+ )
118
+
112
119
  if not self.api_key:
113
120
  self._available = False
114
121
  elif requests is None:
@@ -97,7 +97,12 @@ def check_linear_yaml() -> Optional[Dict[str, str]]:
97
97
 
98
98
  def check_git_state() -> Dict[str, Any]:
99
99
  """Get git status, branch, and recent commits."""
100
- result = {"branch": "unknown", "clean": False, "recent_commits": []}
100
+ result: Dict[str, Any] = {
101
+ "branch": "unknown",
102
+ "clean": False,
103
+ "recent_commits": [],
104
+ "uncommitted_files": [],
105
+ }
101
106
 
102
107
  # Get branch
103
108
  branch_out, ok = run_command("git branch --show-current")
@@ -108,12 +113,21 @@ def check_git_state() -> Dict[str, Any]:
108
113
  status_out, ok = run_command("git status --porcelain")
109
114
  if ok:
110
115
  result["clean"] = len(status_out.strip()) == 0
116
+ if not result["clean"]:
117
+ # Capture uncommitted file list for warning
118
+ result["uncommitted_files"] = [
119
+ line.strip() for line in status_out.split("\n") if line.strip()
120
+ ][:5] # Limit to 5 files
111
121
 
112
122
  # Get recent commits
113
123
  log_out, ok = run_command("git log --oneline -5")
114
124
  if ok and log_out:
115
125
  result["recent_commits"] = log_out.split("\n")[:5]
116
126
 
127
+ # Add compaction warning if uncommitted changes detected
128
+ if not result["clean"]:
129
+ result["warning"] = "Uncommitted changes detected - verify before proceeding"
130
+
117
131
  return result
118
132
 
119
133
 
@@ -398,6 +412,15 @@ def format_output(result: OrientResult, as_json: bool = False) -> str:
398
412
  clean_str = "clean" if git.get("clean") else "uncommitted changes"
399
413
  lines.append(f"**Git**: {git.get('branch', '?')} ({clean_str})")
400
414
 
415
+ # Post-compaction verification warning
416
+ if not git.get("clean"):
417
+ lines.append("\n**⚠️ Uncommitted Changes Detected**")
418
+ lines.append("Verify these are expected before proceeding with new work:")
419
+ for f in git.get("uncommitted_files", [])[:5]:
420
+ lines.append(f" - {f}")
421
+ if git.get("warning"):
422
+ lines.append(f"_Note: {git.get('warning')}_\n")
423
+
401
424
  # Active agents
402
425
  if result.active_agents:
403
426
  lines.append(f"**Active Agents**: {len(result.active_agents)}")