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,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()