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.
- package/README.md +13 -12
- package/VERSION +1 -1
- package/docs/INSTALLATION.md +18 -0
- package/docs/command-reference.md +1 -1
- package/global/commands/orient.md +29 -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__/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__/git_utils.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/handoff_generator.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__/orient_fast.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_prompt_generator.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/git_utils.py +267 -0
- package/global/lib/issue_models.py +28 -0
- package/global/lib/linear_provider.py +7 -0
- package/global/lib/orient_fast.py +24 -1
- package/global/tests/test_git_utils.py +160 -0
- package/global/tests/test_issue_models.py +40 -0
- package/global/tests/test_linear_provider.py +86 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +238 -1
- package/global/tools/anvil-memory/src/commands/ralph-iteration.ts +249 -0
- package/global/tools/anvil-memory/src/index.ts +2 -8
- package/package.json +1 -1
- package/scripts/anvil +7 -2
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +0 -535
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +0 -645
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +0 -363
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +0 -8
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +0 -417
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +0 -571
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +0 -440
- 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.
|
|
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.
|
|
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.
|
|
21
|
+
## 📦 Latest Changes in v0.1.7.0
|
|
22
22
|
|
|
23
|
-
*Released: 2026-01-
|
|
23
|
+
*Released: 2026-01-15*
|
|
24
24
|
|
|
25
|
-
- **
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
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.
|
|
1
|
+
0.1.7.1
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -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:
|
|
@@ -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).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
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)}")
|