anvil-dev-framework 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- package/scripts/verify.sh +255 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
orient_fast.py - Fast-path orientation script for Claude Code sessions.
|
|
4
|
+
|
|
5
|
+
Runs all orientation checks in parallel to minimize latency, outputs structured
|
|
6
|
+
JSON that Claude can format without re-querying.
|
|
7
|
+
|
|
8
|
+
Performance target: <500ms for local checks, <2s with Linear API.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python3 global/lib/orient_fast.py # Full orientation
|
|
12
|
+
python3 global/lib/orient_fast.py --fast # Skip Linear (fastest)
|
|
13
|
+
python3 global/lib/orient_fast.py --json # Output raw JSON
|
|
14
|
+
|
|
15
|
+
Output format (JSON):
|
|
16
|
+
{
|
|
17
|
+
"linear_team": {"key": "ANV", "name": "Project Anvil"},
|
|
18
|
+
"git": {"branch": "main", "clean": true, "recent_commits": [...]},
|
|
19
|
+
"handoff": {"found": true, "path": "...", "summary": "..."},
|
|
20
|
+
"active_agents": [],
|
|
21
|
+
"open_prs": [...],
|
|
22
|
+
"in_progress_issues": [...],
|
|
23
|
+
"ready_work": [...],
|
|
24
|
+
"recommendations": [...]
|
|
25
|
+
}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
import time
|
|
33
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
34
|
+
from dataclasses import dataclass, asdict
|
|
35
|
+
from datetime import datetime, timedelta
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any, Dict, List, Optional
|
|
38
|
+
|
|
39
|
+
# Cache configuration
|
|
40
|
+
CACHE_DIR = Path.home() / ".anvil" / "cache"
|
|
41
|
+
LINEAR_CACHE_TTL = 60 # seconds
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class OrientResult:
|
|
46
|
+
"""Complete orientation result."""
|
|
47
|
+
linear_team: Optional[Dict[str, str]]
|
|
48
|
+
git: Dict[str, Any]
|
|
49
|
+
handoff: Optional[Dict[str, Any]]
|
|
50
|
+
active_agents: List[Dict[str, Any]]
|
|
51
|
+
open_prs: List[Dict[str, Any]]
|
|
52
|
+
in_progress_issues: List[Dict[str, Any]]
|
|
53
|
+
ready_work: List[Dict[str, Any]]
|
|
54
|
+
recommendations: List[str]
|
|
55
|
+
timing_ms: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_command(cmd: str, timeout: int = 5) -> tuple[str, bool]:
|
|
59
|
+
"""Run a shell command and return (output, success)."""
|
|
60
|
+
try:
|
|
61
|
+
result = subprocess.run(
|
|
62
|
+
cmd,
|
|
63
|
+
shell=True,
|
|
64
|
+
capture_output=True,
|
|
65
|
+
text=True,
|
|
66
|
+
timeout=timeout,
|
|
67
|
+
cwd=Path.cwd()
|
|
68
|
+
)
|
|
69
|
+
return result.stdout.strip(), result.returncode == 0
|
|
70
|
+
except subprocess.TimeoutExpired:
|
|
71
|
+
return "", False
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return str(e), False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def check_linear_yaml() -> Optional[Dict[str, str]]:
|
|
77
|
+
"""Read Linear configuration from .claude/linear.yaml."""
|
|
78
|
+
yaml_path = Path.cwd() / ".claude" / "linear.yaml"
|
|
79
|
+
if not yaml_path.exists():
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
content = yaml_path.read_text()
|
|
84
|
+
config = {}
|
|
85
|
+
for line in content.split("\n"):
|
|
86
|
+
line = line.strip()
|
|
87
|
+
if line.startswith("team_key:"):
|
|
88
|
+
config["key"] = line.split('"')[1] if '"' in line else line.split(":")[1].strip()
|
|
89
|
+
elif line.startswith("team_name:"):
|
|
90
|
+
config["name"] = line.split('"')[1] if '"' in line else line.split(":")[1].strip()
|
|
91
|
+
elif line.startswith("team_id:"):
|
|
92
|
+
config["id"] = line.split('"')[1] if '"' in line else line.split(":")[1].strip()
|
|
93
|
+
return config if config.get("key") else None
|
|
94
|
+
except Exception:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def check_git_state() -> Dict[str, Any]:
|
|
99
|
+
"""Get git status, branch, and recent commits."""
|
|
100
|
+
result = {"branch": "unknown", "clean": False, "recent_commits": []}
|
|
101
|
+
|
|
102
|
+
# Get branch
|
|
103
|
+
branch_out, ok = run_command("git branch --show-current")
|
|
104
|
+
if ok:
|
|
105
|
+
result["branch"] = branch_out or "main"
|
|
106
|
+
|
|
107
|
+
# Get status
|
|
108
|
+
status_out, ok = run_command("git status --porcelain")
|
|
109
|
+
if ok:
|
|
110
|
+
result["clean"] = len(status_out.strip()) == 0
|
|
111
|
+
|
|
112
|
+
# Get recent commits
|
|
113
|
+
log_out, ok = run_command("git log --oneline -5")
|
|
114
|
+
if ok and log_out:
|
|
115
|
+
result["recent_commits"] = log_out.split("\n")[:5]
|
|
116
|
+
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def check_handoff() -> Optional[Dict[str, Any]]:
|
|
121
|
+
"""Find and summarize most recent handoff."""
|
|
122
|
+
handoff_dir = Path.cwd() / ".claude" / "handoffs"
|
|
123
|
+
if not handoff_dir.exists():
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
# Find recent handoffs (last 48 hours)
|
|
127
|
+
cutoff = datetime.now() - timedelta(hours=48)
|
|
128
|
+
handoffs = []
|
|
129
|
+
|
|
130
|
+
for f in handoff_dir.glob("*.md"):
|
|
131
|
+
try:
|
|
132
|
+
mtime = datetime.fromtimestamp(f.stat().st_mtime)
|
|
133
|
+
if mtime > cutoff:
|
|
134
|
+
handoffs.append((mtime, f))
|
|
135
|
+
except Exception:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if not handoffs:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
# Get most recent
|
|
142
|
+
handoffs.sort(reverse=True)
|
|
143
|
+
latest_path = handoffs[0][1]
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
content = latest_path.read_text()
|
|
147
|
+
# Extract first 500 chars for summary
|
|
148
|
+
lines = content.split("\n")
|
|
149
|
+
summary_lines = []
|
|
150
|
+
for line in lines[:20]:
|
|
151
|
+
summary_lines.append(line)
|
|
152
|
+
if len("\n".join(summary_lines)) > 500:
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
"found": True,
|
|
157
|
+
"path": str(latest_path.relative_to(Path.cwd())),
|
|
158
|
+
"date": handoffs[0][0].strftime("%Y-%m-%d %H:%M"),
|
|
159
|
+
"summary": "\n".join(summary_lines)
|
|
160
|
+
}
|
|
161
|
+
except Exception:
|
|
162
|
+
return {"found": True, "path": str(latest_path), "date": "", "summary": ""}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def check_active_agents() -> List[Dict[str, Any]]:
|
|
166
|
+
"""Check agent registry for active agents."""
|
|
167
|
+
agents_file = Path.home() / ".anvil" / "agents.json"
|
|
168
|
+
if not agents_file.exists():
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
data = json.loads(agents_file.read_text())
|
|
173
|
+
active = []
|
|
174
|
+
now = time.time()
|
|
175
|
+
|
|
176
|
+
for agent_id, agent in data.get("agents", {}).items():
|
|
177
|
+
# Consider active if last seen within 2 minutes
|
|
178
|
+
last_seen = agent.get("last_seen", 0)
|
|
179
|
+
if now - last_seen < 120:
|
|
180
|
+
active.append({
|
|
181
|
+
"id": agent_id[:8],
|
|
182
|
+
"codename": agent.get("codename", "unknown"),
|
|
183
|
+
"project": agent.get("project", ""),
|
|
184
|
+
"phase": agent.get("phase", ""),
|
|
185
|
+
"issue": agent.get("active_issue", "")
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
return active
|
|
189
|
+
except Exception:
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def check_open_prs() -> List[Dict[str, Any]]:
|
|
194
|
+
"""Get open PRs from GitHub."""
|
|
195
|
+
out, ok = run_command("gh pr list --state open --limit 10 --json number,title,headRefName,state", timeout=10)
|
|
196
|
+
if not ok or not out:
|
|
197
|
+
return []
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
prs = json.loads(out)
|
|
201
|
+
return [{
|
|
202
|
+
"number": pr.get("number"),
|
|
203
|
+
"title": pr.get("title"),
|
|
204
|
+
"branch": pr.get("headRefName"),
|
|
205
|
+
"state": pr.get("state", "open")
|
|
206
|
+
} for pr in prs]
|
|
207
|
+
except json.JSONDecodeError:
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_cached_linear_data() -> Optional[Dict[str, Any]]:
|
|
212
|
+
"""Get Linear data from cache if fresh."""
|
|
213
|
+
cache_file = CACHE_DIR / "linear-issues.json"
|
|
214
|
+
if not cache_file.exists():
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
data = json.loads(cache_file.read_text())
|
|
219
|
+
cached_at = data.get("cached_at", 0)
|
|
220
|
+
if time.time() - cached_at < LINEAR_CACHE_TTL:
|
|
221
|
+
return data
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def fetch_linear_issues(team_id: str) -> Dict[str, Any]:
|
|
229
|
+
"""Fetch issues from Linear API using the linear-skill script."""
|
|
230
|
+
# First check cache
|
|
231
|
+
cached = get_cached_linear_data()
|
|
232
|
+
if cached:
|
|
233
|
+
return cached
|
|
234
|
+
|
|
235
|
+
# Check if LINEAR_API_KEY is available
|
|
236
|
+
if not os.environ.get("LINEAR_API_KEY"):
|
|
237
|
+
return {"in_progress": [], "ready": [], "blocked": [], "error": "No LINEAR_API_KEY"}
|
|
238
|
+
|
|
239
|
+
# Use the linear-skill script
|
|
240
|
+
linear_script = Path.home() / ".claude" / "skills" / "linear-skill" / "scripts" / "linear.py"
|
|
241
|
+
if not linear_script.exists():
|
|
242
|
+
return {"in_progress": [], "ready": [], "blocked": [], "error": "Linear skill not found"}
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
# Add the script directory to path and import
|
|
246
|
+
sys.path.insert(0, str(linear_script.parent))
|
|
247
|
+
from linear import LinearClient
|
|
248
|
+
|
|
249
|
+
client = LinearClient()
|
|
250
|
+
issues = client.list_issues(team_id=team_id, limit=50)
|
|
251
|
+
|
|
252
|
+
# Categorize issues by state type
|
|
253
|
+
in_progress = []
|
|
254
|
+
ready = [] # Todo + Backlog without blockers
|
|
255
|
+
blocked = []
|
|
256
|
+
|
|
257
|
+
for issue in issues:
|
|
258
|
+
state = issue.get("state", {})
|
|
259
|
+
state_type = state.get("type", "")
|
|
260
|
+
state_name = state.get("name", "").lower()
|
|
261
|
+
|
|
262
|
+
issue_data = {
|
|
263
|
+
"identifier": issue.get("identifier"),
|
|
264
|
+
"title": issue.get("title"),
|
|
265
|
+
"state": state_name,
|
|
266
|
+
"state_type": state_type,
|
|
267
|
+
"priority": issue.get("priority", 4),
|
|
268
|
+
"created_at": issue.get("createdAt", "")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if state_type == "started" or state_name == "in progress":
|
|
272
|
+
in_progress.append(issue_data)
|
|
273
|
+
elif state_type in ("unstarted", "backlog") or state_name in ("todo", "backlog"):
|
|
274
|
+
# For now, treat all unstarted as ready (blocker check would need more API calls)
|
|
275
|
+
ready.append(issue_data)
|
|
276
|
+
|
|
277
|
+
# Sort by priority (0 = urgent, 4 = none)
|
|
278
|
+
ready.sort(key=lambda x: (x.get("priority", 4), x.get("created_at", "")))
|
|
279
|
+
|
|
280
|
+
result = {
|
|
281
|
+
"in_progress": in_progress,
|
|
282
|
+
"ready": ready[:10], # Top 10 ready
|
|
283
|
+
"blocked": blocked
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Cache the result
|
|
287
|
+
save_linear_cache(result)
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
return {"in_progress": [], "ready": [], "blocked": [], "error": str(e)}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def save_linear_cache(data: Dict[str, Any]) -> None:
|
|
295
|
+
"""Save Linear data to cache."""
|
|
296
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
297
|
+
cache_file = CACHE_DIR / "linear-issues.json"
|
|
298
|
+
data["cached_at"] = time.time()
|
|
299
|
+
cache_file.write_text(json.dumps(data, indent=2))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def generate_recommendations(
|
|
303
|
+
handoff: Optional[Dict],
|
|
304
|
+
open_prs: List[Dict],
|
|
305
|
+
in_progress: List[Dict],
|
|
306
|
+
ready_work: List[Dict]
|
|
307
|
+
) -> List[str]:
|
|
308
|
+
"""Generate session recommendations based on state."""
|
|
309
|
+
recs = []
|
|
310
|
+
|
|
311
|
+
if handoff and handoff.get("found"):
|
|
312
|
+
recs.append(f"Recent handoff found from {handoff.get('date', 'recently')} - review for context")
|
|
313
|
+
|
|
314
|
+
if len(open_prs) >= 3:
|
|
315
|
+
recs.append(f"{len(open_prs)} open PRs - consider reviewing/merging before new work")
|
|
316
|
+
|
|
317
|
+
if ready_work:
|
|
318
|
+
top = ready_work[0]
|
|
319
|
+
recs.append(f"Top ready work: {top.get('identifier', 'unknown')} - {top.get('title', '')}")
|
|
320
|
+
|
|
321
|
+
if not recs:
|
|
322
|
+
recs.append("No specific recommendations - awaiting direction")
|
|
323
|
+
|
|
324
|
+
return recs
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def run_orient(skip_linear: bool = False) -> OrientResult:
|
|
328
|
+
"""Run all orientation checks in parallel."""
|
|
329
|
+
start = time.time()
|
|
330
|
+
|
|
331
|
+
# Run checks in parallel
|
|
332
|
+
with ThreadPoolExecutor(max_workers=6) as executor:
|
|
333
|
+
futures = {
|
|
334
|
+
executor.submit(check_linear_yaml): "linear_yaml",
|
|
335
|
+
executor.submit(check_git_state): "git",
|
|
336
|
+
executor.submit(check_handoff): "handoff",
|
|
337
|
+
executor.submit(check_active_agents): "agents",
|
|
338
|
+
executor.submit(check_open_prs): "prs",
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
results = {}
|
|
342
|
+
for future in as_completed(futures):
|
|
343
|
+
key = futures[future]
|
|
344
|
+
try:
|
|
345
|
+
results[key] = future.result()
|
|
346
|
+
except Exception as e:
|
|
347
|
+
results[key] = None
|
|
348
|
+
|
|
349
|
+
# Process Linear data (separate to allow caching)
|
|
350
|
+
linear_config = results.get("linear_yaml")
|
|
351
|
+
in_progress = []
|
|
352
|
+
ready_work = []
|
|
353
|
+
|
|
354
|
+
if not skip_linear and linear_config and linear_config.get("id"):
|
|
355
|
+
linear_data = fetch_linear_issues(linear_config["id"])
|
|
356
|
+
in_progress = linear_data.get("in_progress", [])
|
|
357
|
+
ready_work = linear_data.get("ready", [])
|
|
358
|
+
|
|
359
|
+
# Generate recommendations
|
|
360
|
+
recommendations = generate_recommendations(
|
|
361
|
+
results.get("handoff"),
|
|
362
|
+
results.get("prs", []),
|
|
363
|
+
in_progress,
|
|
364
|
+
ready_work
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
timing_ms = int((time.time() - start) * 1000)
|
|
368
|
+
|
|
369
|
+
return OrientResult(
|
|
370
|
+
linear_team=linear_config,
|
|
371
|
+
git=results.get("git", {}),
|
|
372
|
+
handoff=results.get("handoff"),
|
|
373
|
+
active_agents=results.get("agents", []),
|
|
374
|
+
open_prs=results.get("prs", []),
|
|
375
|
+
in_progress_issues=in_progress,
|
|
376
|
+
ready_work=ready_work,
|
|
377
|
+
recommendations=recommendations,
|
|
378
|
+
timing_ms=timing_ms
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def format_output(result: OrientResult, as_json: bool = False) -> str:
|
|
383
|
+
"""Format orientation result for display."""
|
|
384
|
+
if as_json:
|
|
385
|
+
return json.dumps(asdict(result), indent=2)
|
|
386
|
+
|
|
387
|
+
lines = []
|
|
388
|
+
lines.append("## Session Orientation (fast-path)\n")
|
|
389
|
+
|
|
390
|
+
# Linear team
|
|
391
|
+
if result.linear_team:
|
|
392
|
+
lines.append(f"**Linear Team**: {result.linear_team.get('key', '?')} - {result.linear_team.get('name', '?')}")
|
|
393
|
+
else:
|
|
394
|
+
lines.append("**Linear Team**: Not configured")
|
|
395
|
+
|
|
396
|
+
# Git state
|
|
397
|
+
git = result.git
|
|
398
|
+
clean_str = "clean" if git.get("clean") else "uncommitted changes"
|
|
399
|
+
lines.append(f"**Git**: {git.get('branch', '?')} ({clean_str})")
|
|
400
|
+
|
|
401
|
+
# Active agents
|
|
402
|
+
if result.active_agents:
|
|
403
|
+
lines.append(f"**Active Agents**: {len(result.active_agents)}")
|
|
404
|
+
for agent in result.active_agents:
|
|
405
|
+
lines.append(f" - {agent.get('codename', '?')}: {agent.get('issue', 'unknown')}")
|
|
406
|
+
else:
|
|
407
|
+
lines.append("**Active Agents**: None")
|
|
408
|
+
|
|
409
|
+
# Handoff
|
|
410
|
+
if result.handoff and result.handoff.get("found"):
|
|
411
|
+
lines.append(f"**Handoff**: Found ({result.handoff.get('date', 'recent')})")
|
|
412
|
+
else:
|
|
413
|
+
lines.append("**Handoff**: None")
|
|
414
|
+
|
|
415
|
+
# Open PRs
|
|
416
|
+
lines.append(f"**Open PRs**: {len(result.open_prs)}")
|
|
417
|
+
for pr in result.open_prs[:5]:
|
|
418
|
+
lines.append(f" - #{pr.get('number', '?')}: {pr.get('title', '?')}")
|
|
419
|
+
|
|
420
|
+
# Recommendations
|
|
421
|
+
lines.append("\n**Recommendations**:")
|
|
422
|
+
for rec in result.recommendations:
|
|
423
|
+
lines.append(f" - {rec}")
|
|
424
|
+
|
|
425
|
+
# Timing
|
|
426
|
+
lines.append(f"\n_Completed in {result.timing_ms}ms_")
|
|
427
|
+
|
|
428
|
+
return "\n".join(lines)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def main():
|
|
432
|
+
"""CLI entry point."""
|
|
433
|
+
import argparse
|
|
434
|
+
parser = argparse.ArgumentParser(description="Fast-path session orientation")
|
|
435
|
+
parser.add_argument("--fast", action="store_true", help="Skip Linear API calls")
|
|
436
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
437
|
+
args = parser.parse_args()
|
|
438
|
+
|
|
439
|
+
result = run_orient(skip_linear=args.fast)
|
|
440
|
+
output = format_output(result, as_json=args.json)
|
|
441
|
+
print(output)
|
|
442
|
+
|
|
443
|
+
# Also update state manager
|
|
444
|
+
try:
|
|
445
|
+
state_manager_path = Path.cwd() / "global" / "lib" / "state_manager.py"
|
|
446
|
+
if state_manager_path.exists():
|
|
447
|
+
subprocess.run(
|
|
448
|
+
[sys.executable, str(state_manager_path), "orient"],
|
|
449
|
+
capture_output=True,
|
|
450
|
+
timeout=5
|
|
451
|
+
)
|
|
452
|
+
except Exception:
|
|
453
|
+
pass # Non-critical
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
if __name__ == "__main__":
|
|
457
|
+
main()
|