deepspider 0.1.0

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 (261) hide show
  1. package/.claude/agents/check.md +122 -0
  2. package/.claude/agents/debug.md +106 -0
  3. package/.claude/agents/dispatch.md +214 -0
  4. package/.claude/agents/implement.md +96 -0
  5. package/.claude/agents/plan.md +396 -0
  6. package/.claude/agents/research.md +120 -0
  7. package/.claude/commands/evolve/merge.md +80 -0
  8. package/.claude/commands/trellis/before-backend-dev.md +13 -0
  9. package/.claude/commands/trellis/before-frontend-dev.md +13 -0
  10. package/.claude/commands/trellis/break-loop.md +107 -0
  11. package/.claude/commands/trellis/check-backend.md +13 -0
  12. package/.claude/commands/trellis/check-cross-layer.md +153 -0
  13. package/.claude/commands/trellis/check-frontend.md +13 -0
  14. package/.claude/commands/trellis/create-command.md +154 -0
  15. package/.claude/commands/trellis/finish-work.md +129 -0
  16. package/.claude/commands/trellis/integrate-skill.md +219 -0
  17. package/.claude/commands/trellis/onboard.md +358 -0
  18. package/.claude/commands/trellis/parallel.md +193 -0
  19. package/.claude/commands/trellis/record-session.md +62 -0
  20. package/.claude/commands/trellis/start.md +280 -0
  21. package/.claude/commands/trellis/update-spec.md +213 -0
  22. package/.claude/hooks/inject-subagent-context.py +758 -0
  23. package/.claude/hooks/ralph-loop.py +374 -0
  24. package/.claude/hooks/session-start.py +126 -0
  25. package/.claude/settings.json +41 -0
  26. package/.claude/skills/deepagents-guide/SKILL.md +428 -0
  27. package/.cursor/commands/trellis-before-backend-dev.md +13 -0
  28. package/.cursor/commands/trellis-before-frontend-dev.md +13 -0
  29. package/.cursor/commands/trellis-break-loop.md +107 -0
  30. package/.cursor/commands/trellis-check-backend.md +13 -0
  31. package/.cursor/commands/trellis-check-cross-layer.md +153 -0
  32. package/.cursor/commands/trellis-check-frontend.md +13 -0
  33. package/.cursor/commands/trellis-create-command.md +154 -0
  34. package/.cursor/commands/trellis-finish-work.md +129 -0
  35. package/.cursor/commands/trellis-integrate-skill.md +219 -0
  36. package/.cursor/commands/trellis-onboard.md +358 -0
  37. package/.cursor/commands/trellis-record-session.md +62 -0
  38. package/.cursor/commands/trellis-start.md +156 -0
  39. package/.cursor/commands/trellis-update-spec.md +213 -0
  40. package/.env.example +11 -0
  41. package/.husky/pre-commit +1 -0
  42. package/.mcp.json +8 -0
  43. package/.trellis/.template-hashes.json +65 -0
  44. package/.trellis/.version +1 -0
  45. package/.trellis/scripts/add-session.sh +384 -0
  46. package/.trellis/scripts/common/developer.sh +129 -0
  47. package/.trellis/scripts/common/git-context.sh +263 -0
  48. package/.trellis/scripts/common/paths.sh +208 -0
  49. package/.trellis/scripts/common/phase.sh +150 -0
  50. package/.trellis/scripts/common/registry.sh +247 -0
  51. package/.trellis/scripts/common/task-queue.sh +142 -0
  52. package/.trellis/scripts/common/task-utils.sh +151 -0
  53. package/.trellis/scripts/common/worktree.sh +128 -0
  54. package/.trellis/scripts/create-bootstrap.sh +299 -0
  55. package/.trellis/scripts/get-context.sh +7 -0
  56. package/.trellis/scripts/get-developer.sh +15 -0
  57. package/.trellis/scripts/init-developer.sh +34 -0
  58. package/.trellis/scripts/multi-agent/cleanup.sh +396 -0
  59. package/.trellis/scripts/multi-agent/create-pr.sh +241 -0
  60. package/.trellis/scripts/multi-agent/plan.sh +207 -0
  61. package/.trellis/scripts/multi-agent/start.sh +310 -0
  62. package/.trellis/scripts/multi-agent/status.sh +828 -0
  63. package/.trellis/scripts/task.sh +1118 -0
  64. package/.trellis/spec/backend/deepagents-guide.md +337 -0
  65. package/.trellis/spec/backend/directory-structure.md +126 -0
  66. package/.trellis/spec/backend/examples/skills/deepagents-guide/README.md +11 -0
  67. package/.trellis/spec/backend/examples/skills/deepagents-guide/agent.js.template +20 -0
  68. package/.trellis/spec/backend/examples/skills/deepagents-guide/skills-config.js.template +13 -0
  69. package/.trellis/spec/backend/examples/skills/deepagents-guide/subagent.js.template +19 -0
  70. package/.trellis/spec/backend/hook-guidelines.md +178 -0
  71. package/.trellis/spec/backend/index.md +36 -0
  72. package/.trellis/spec/backend/quality-guidelines.md +201 -0
  73. package/.trellis/spec/backend/state-management.md +76 -0
  74. package/.trellis/spec/backend/tool-guidelines.md +144 -0
  75. package/.trellis/spec/backend/type-safety.md +71 -0
  76. package/.trellis/spec/guides/code-reuse-thinking-guide.md +92 -0
  77. package/.trellis/spec/guides/cross-layer-thinking-guide.md +94 -0
  78. package/.trellis/spec/guides/index.md +79 -0
  79. package/.trellis/tasks/archive/02-02-evolving-skills/prd.md +61 -0
  80. package/.trellis/tasks/archive/02-02-evolving-skills/task.json +29 -0
  81. package/.trellis/tasks/archive/2026-02/00-bootstrap-guidelines/prd.md +86 -0
  82. package/.trellis/tasks/archive/2026-02/00-bootstrap-guidelines/task.json +27 -0
  83. package/.trellis/tasks/archive/2026-02/02-02-skills-system/check.jsonl +3 -0
  84. package/.trellis/tasks/archive/2026-02/02-02-skills-system/debug.jsonl +2 -0
  85. package/.trellis/tasks/archive/2026-02/02-02-skills-system/implement.jsonl +5 -0
  86. package/.trellis/tasks/archive/2026-02/02-02-skills-system/prd.md +33 -0
  87. package/.trellis/tasks/archive/2026-02/02-02-skills-system/task.json +41 -0
  88. package/.trellis/workflow.md +407 -0
  89. package/.trellis/workspace/index.md +123 -0
  90. package/.trellis/workspace/pony/index.md +40 -0
  91. package/.trellis/workspace/pony/journal-1.md +7 -0
  92. package/.trellis/worktree.yaml +47 -0
  93. package/AGENTS.md +18 -0
  94. package/CLAUDE.md +292 -0
  95. package/README.md +134 -0
  96. package/agents/deepspider.md +142 -0
  97. package/docs/DEBUG.md +42 -0
  98. package/docs/GUIDE.md +334 -0
  99. package/docs/PROMPT.md +60 -0
  100. package/docs/USAGE.md +226 -0
  101. package/eslint.config.js +51 -0
  102. package/package.json +78 -0
  103. package/requirements-crypto.txt +14 -0
  104. package/src/agent/index.js +97 -0
  105. package/src/agent/logger.js +164 -0
  106. package/src/agent/middleware/filterTools.js +64 -0
  107. package/src/agent/middleware/report.js +79 -0
  108. package/src/agent/prompts/system.js +315 -0
  109. package/src/agent/run.js +575 -0
  110. package/src/agent/skills/anti-detect/SKILL.md +28 -0
  111. package/src/agent/skills/anti-detect/evolved.md +12 -0
  112. package/src/agent/skills/captcha/SKILL.md +37 -0
  113. package/src/agent/skills/captcha/evolved.md +12 -0
  114. package/src/agent/skills/config.js +30 -0
  115. package/src/agent/skills/crawler/SKILL.md +9 -0
  116. package/src/agent/skills/crawler/evolved.md +16 -0
  117. package/src/agent/skills/dynamic-analysis/SKILL.md +91 -0
  118. package/src/agent/skills/dynamic-analysis/evolved.md +12 -0
  119. package/src/agent/skills/env/SKILL.md +72 -0
  120. package/src/agent/skills/env/evolved.md +12 -0
  121. package/src/agent/skills/evolve.js +79 -0
  122. package/src/agent/skills/general/SKILL.md +12 -0
  123. package/src/agent/skills/general/evolved.md +12 -0
  124. package/src/agent/skills/js2python/SKILL.md +30 -0
  125. package/src/agent/skills/js2python/evolved.md +13 -0
  126. package/src/agent/skills/report/SKILL.md +21 -0
  127. package/src/agent/skills/report/evolved.md +12 -0
  128. package/src/agent/skills/sandbox/SKILL.md +22 -0
  129. package/src/agent/skills/sandbox/evolved.md +16 -0
  130. package/src/agent/skills/static-analysis/SKILL.md +93 -0
  131. package/src/agent/skills/static-analysis/evolved.md +12 -0
  132. package/src/agent/skills/xpath/SKILL.md +119 -0
  133. package/src/agent/subagents/anti-detect.js +45 -0
  134. package/src/agent/subagents/captcha.js +51 -0
  135. package/src/agent/subagents/crawler.js +138 -0
  136. package/src/agent/subagents/dynamic.js +64 -0
  137. package/src/agent/subagents/env-agent.js +82 -0
  138. package/src/agent/subagents/index.js +37 -0
  139. package/src/agent/subagents/js2python.js +72 -0
  140. package/src/agent/subagents/sandbox.js +55 -0
  141. package/src/agent/subagents/static.js +66 -0
  142. package/src/agent/tools/analysis.js +135 -0
  143. package/src/agent/tools/analyzer.js +85 -0
  144. package/src/agent/tools/anti-detect.js +89 -0
  145. package/src/agent/tools/antidebug.js +64 -0
  146. package/src/agent/tools/async.js +43 -0
  147. package/src/agent/tools/browser.js +324 -0
  148. package/src/agent/tools/captcha.js +223 -0
  149. package/src/agent/tools/capture.js +179 -0
  150. package/src/agent/tools/correlate.js +303 -0
  151. package/src/agent/tools/crawler.js +116 -0
  152. package/src/agent/tools/cryptohook.js +80 -0
  153. package/src/agent/tools/debug.js +246 -0
  154. package/src/agent/tools/deobfuscator.js +90 -0
  155. package/src/agent/tools/env.js +83 -0
  156. package/src/agent/tools/envdump.js +92 -0
  157. package/src/agent/tools/evolve.js +164 -0
  158. package/src/agent/tools/extract.js +114 -0
  159. package/src/agent/tools/extractor.js +54 -0
  160. package/src/agent/tools/file.js +224 -0
  161. package/src/agent/tools/hook.js +84 -0
  162. package/src/agent/tools/hookManager.js +178 -0
  163. package/src/agent/tools/index.js +137 -0
  164. package/src/agent/tools/nodejs.js +101 -0
  165. package/src/agent/tools/patch.js +46 -0
  166. package/src/agent/tools/preprocess.js +71 -0
  167. package/src/agent/tools/profile.js +122 -0
  168. package/src/agent/tools/python.js +627 -0
  169. package/src/agent/tools/report.js +124 -0
  170. package/src/agent/tools/runtime.js +132 -0
  171. package/src/agent/tools/sandbox.js +79 -0
  172. package/src/agent/tools/store.js +73 -0
  173. package/src/agent/tools/trace.js +74 -0
  174. package/src/agent/tools/tracing.js +201 -0
  175. package/src/agent/tools/utils.js +51 -0
  176. package/src/agent/tools/verify.js +184 -0
  177. package/src/agent/tools/webcrack.js +109 -0
  178. package/src/analyzer/ASTAnalyzer.js +387 -0
  179. package/src/analyzer/CallStackAnalyzer.js +379 -0
  180. package/src/analyzer/Deobfuscator.js +289 -0
  181. package/src/analyzer/EncryptionAnalyzer.js +99 -0
  182. package/src/analyzer/index.js +22 -0
  183. package/src/browser/EnvBridge.js +186 -0
  184. package/src/browser/cdp.js +168 -0
  185. package/src/browser/client.js +197 -0
  186. package/src/browser/collector.js +444 -0
  187. package/src/browser/collectors/RequestCryptoLinker.js +109 -0
  188. package/src/browser/collectors/ResponseSearcher.js +107 -0
  189. package/src/browser/collectors/ScriptCollector.js +158 -0
  190. package/src/browser/collectors/index.js +26 -0
  191. package/src/browser/defaultHooks.js +932 -0
  192. package/src/browser/hooks/crypto.js +55 -0
  193. package/src/browser/hooks/index.js +64 -0
  194. package/src/browser/hooks/native.js +9 -0
  195. package/src/browser/hooks/network.js +33 -0
  196. package/src/browser/index.js +42 -0
  197. package/src/browser/interceptors/NetworkInterceptor.js +116 -0
  198. package/src/browser/interceptors/ScriptInterceptor.js +76 -0
  199. package/src/browser/interceptors/index.js +6 -0
  200. package/src/browser/ui/analysisPanel.js +1782 -0
  201. package/src/browser/ui/confirmDialog.js +158 -0
  202. package/src/browser/ui/panel.html +152 -0
  203. package/src/browser/ui/selector.js +170 -0
  204. package/src/config/index.js +5 -0
  205. package/src/config/paths.js +71 -0
  206. package/src/config/patterns/crypto.js +36 -0
  207. package/src/config/profiles/chrome.json +71 -0
  208. package/src/config/profiles/firefox.json +44 -0
  209. package/src/config/profiles/safari.json +38 -0
  210. package/src/core/EnvMonitor.js +200 -0
  211. package/src/core/PatchGenerator.js +278 -0
  212. package/src/core/Sandbox.js +181 -0
  213. package/src/env/AntiAntiDebug.js +111 -0
  214. package/src/env/AsyncHook.js +68 -0
  215. package/src/env/BrowserAPIList.js +265 -0
  216. package/src/env/CookieHook.js +48 -0
  217. package/src/env/CryptoHook.js +205 -0
  218. package/src/env/EnvCodeGenerator.js +157 -0
  219. package/src/env/EnvDumper.js +356 -0
  220. package/src/env/EnvExtractor.js +220 -0
  221. package/src/env/HookBase.js +618 -0
  222. package/src/env/NetworkHook.js +159 -0
  223. package/src/env/modules/bom/history.js +29 -0
  224. package/src/env/modules/bom/location.js +26 -0
  225. package/src/env/modules/bom/navigator.js +70 -0
  226. package/src/env/modules/bom/screen.js +26 -0
  227. package/src/env/modules/bom/storage.js +23 -0
  228. package/src/env/modules/dom/document.js +110 -0
  229. package/src/env/modules/dom/event.js +51 -0
  230. package/src/env/modules/index.js +34 -0
  231. package/src/env/modules/webapi/fetch.js +46 -0
  232. package/src/env/modules/webapi/url.js +47 -0
  233. package/src/env/modules/webapi/xhr.js +48 -0
  234. package/src/index.js +27 -0
  235. package/src/mcp/server.js +89 -0
  236. package/src/store/DataStore.js +708 -0
  237. package/src/store/Store.js +158 -0
  238. package/src/store/Validator.js +24 -0
  239. package/test/analyze.test.js +90 -0
  240. package/test/envdump.test.js +74 -0
  241. package/test/flow.test.js +90 -0
  242. package/test/hooks.test.js +138 -0
  243. package/test/plugin.test.js +35 -0
  244. package/test/refactor-full.test.js +30 -0
  245. package/test/refactor.test.js +21 -0
  246. package/test/samples/obfuscated.js +61 -0
  247. package/test/samples/original.js +66 -0
  248. package/test/samples/v10_eval_chain.js +52 -0
  249. package/test/samples/v11_bytecode_vm.js +81 -0
  250. package/test/samples/v12_polymorphic.js +69 -0
  251. package/test/samples/v1_ob_basic.js +98 -0
  252. package/test/samples/v2_ob_advanced.js +99 -0
  253. package/test/samples/v3_jjencode.js +77 -0
  254. package/test/samples/v4_aaencode.js +73 -0
  255. package/test/samples/v5_control_flow.js +86 -0
  256. package/test/samples/v6_string_encryption.js +71 -0
  257. package/test/samples/v7_jsvmp.js +83 -0
  258. package/test/samples/v8_anti_debug.js +79 -0
  259. package/test/samples/v9_proxy_trap.js +49 -0
  260. package/test/samples.test.js +96 -0
  261. package/test/webcrack.test.js +55 -0
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Ralph Loop - SubagentStop Hook for Check Agent Loop Control
4
+
5
+ Based on the Ralph Wiggum technique for autonomous agent loops.
6
+ Uses completion promises to control when the check agent can stop.
7
+
8
+ Mechanism:
9
+ - Intercepts when check subagent tries to stop (SubagentStop event)
10
+ - If verify commands configured in worktree.yaml, runs them to verify
11
+ - Otherwise, reads check.jsonl to get dynamic completion markers ({reason}_FINISH)
12
+ - Blocks stopping until verification passes or all markers found
13
+ - Has max iterations as safety limit
14
+
15
+ State file: .trellis/.ralph-state.json
16
+ - Tracks current iteration count per session
17
+ - Resets when task changes
18
+ """
19
+
20
+ import json
21
+ import os
22
+ import subprocess
23
+ import sys
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+
27
+ # =============================================================================
28
+ # Configuration
29
+ # =============================================================================
30
+
31
+ MAX_ITERATIONS = 5 # Safety limit to prevent infinite loops
32
+ STATE_TIMEOUT_MINUTES = 30 # Reset state if older than this
33
+ STATE_FILE = ".trellis/.ralph-state.json"
34
+ WORKTREE_YAML = ".trellis/worktree.yaml"
35
+ DIR_WORKFLOW = ".trellis"
36
+ FILE_CURRENT_TASK = ".current-task"
37
+
38
+ # Only control loop for check agent
39
+ TARGET_AGENT = "check"
40
+
41
+
42
+ def find_repo_root(start_path: str) -> str | None:
43
+ """Find git repo root from start_path upwards"""
44
+ current = Path(start_path).resolve()
45
+ while current != current.parent:
46
+ if (current / ".git").exists():
47
+ return str(current)
48
+ current = current.parent
49
+ return None
50
+
51
+
52
+ def get_current_task(repo_root: str) -> str | None:
53
+ """Read current task directory path"""
54
+ current_task_file = os.path.join(repo_root, DIR_WORKFLOW, FILE_CURRENT_TASK)
55
+ if not os.path.exists(current_task_file):
56
+ return None
57
+
58
+ try:
59
+ with open(current_task_file, "r", encoding="utf-8") as f:
60
+ content = f.read().strip()
61
+ return content if content else None
62
+ except Exception:
63
+ return None
64
+
65
+
66
+ def get_verify_commands(repo_root: str) -> list[str]:
67
+ """
68
+ Read verify commands from worktree.yaml.
69
+
70
+ Returns list of commands to run, or empty list if not configured.
71
+ Uses simple YAML parsing without external dependencies.
72
+ """
73
+ yaml_path = os.path.join(repo_root, WORKTREE_YAML)
74
+ if not os.path.exists(yaml_path):
75
+ return []
76
+
77
+ try:
78
+ with open(yaml_path, "r", encoding="utf-8") as f:
79
+ content = f.read()
80
+
81
+ # Simple YAML parsing for verify section
82
+ # Look for "verify:" followed by list items
83
+ lines = content.split("\n")
84
+ in_verify_section = False
85
+ commands = []
86
+
87
+ for line in lines:
88
+ stripped = line.strip()
89
+
90
+ # Check for section start
91
+ if stripped.startswith("verify:"):
92
+ in_verify_section = True
93
+ continue
94
+
95
+ # Check for new section (not indented, ends with :)
96
+ if (
97
+ not line.startswith(" ")
98
+ and not line.startswith("\t")
99
+ and stripped.endswith(":")
100
+ and stripped != ""
101
+ ):
102
+ in_verify_section = False
103
+ continue
104
+
105
+ # If in verify section, look for list items
106
+ if in_verify_section:
107
+ # Skip comments and empty lines
108
+ if stripped.startswith("#") or stripped == "":
109
+ continue
110
+ # Parse list item (- command)
111
+ if stripped.startswith("- "):
112
+ cmd = stripped[2:].strip()
113
+ if cmd:
114
+ commands.append(cmd)
115
+
116
+ return commands
117
+ except Exception:
118
+ return []
119
+
120
+
121
+ def run_verify_commands(repo_root: str, commands: list[str]) -> tuple[bool, str]:
122
+ """
123
+ Run verify commands and return (success, message).
124
+
125
+ All commands must pass for success.
126
+ """
127
+ for cmd in commands:
128
+ try:
129
+ result = subprocess.run(
130
+ cmd,
131
+ shell=True,
132
+ cwd=repo_root,
133
+ capture_output=True,
134
+ timeout=120, # 2 minute timeout per command
135
+ )
136
+ if result.returncode != 0:
137
+ stderr = result.stderr.decode("utf-8", errors="replace")
138
+ stdout = result.stdout.decode("utf-8", errors="replace")
139
+ error_output = stderr or stdout
140
+ # Truncate long output
141
+ if len(error_output) > 500:
142
+ error_output = error_output[:500] + "..."
143
+ return False, f"Command failed: {cmd}\n{error_output}"
144
+ except subprocess.TimeoutExpired:
145
+ return False, f"Command timed out: {cmd}"
146
+ except Exception as e:
147
+ return False, f"Command error: {cmd} - {str(e)}"
148
+
149
+ return True, "All verify commands passed"
150
+
151
+
152
+ def get_completion_markers(repo_root: str, task_dir: str) -> list[str]:
153
+ """
154
+ Read check.jsonl and generate completion markers from reasons.
155
+
156
+ Each entry's "reason" field becomes {REASON}_FINISH marker.
157
+ Example: {"file": "...", "reason": "TypeCheck"} -> "TYPECHECK_FINISH"
158
+ """
159
+ check_jsonl_path = os.path.join(repo_root, task_dir, "check.jsonl")
160
+ markers = []
161
+
162
+ if not os.path.exists(check_jsonl_path):
163
+ # Fallback: if no check.jsonl, use default marker
164
+ return ["ALL_CHECKS_FINISH"]
165
+
166
+ try:
167
+ with open(check_jsonl_path, "r", encoding="utf-8") as f:
168
+ for line in f:
169
+ line = line.strip()
170
+ if not line:
171
+ continue
172
+ try:
173
+ item = json.loads(line)
174
+ reason = item.get("reason", "")
175
+ if reason:
176
+ # Convert to uppercase and add _FINISH suffix
177
+ marker = f"{reason.upper().replace(' ', '_')}_FINISH"
178
+ if marker not in markers:
179
+ markers.append(marker)
180
+ except json.JSONDecodeError:
181
+ continue
182
+ except Exception:
183
+ pass
184
+
185
+ # If no markers found, use default
186
+ if not markers:
187
+ markers = ["ALL_CHECKS_FINISH"]
188
+
189
+ return markers
190
+
191
+
192
+ def load_state(repo_root: str) -> dict:
193
+ """Load Ralph Loop state from file"""
194
+ state_path = os.path.join(repo_root, STATE_FILE)
195
+ if not os.path.exists(state_path):
196
+ return {"task": None, "iteration": 0, "started_at": None}
197
+
198
+ try:
199
+ with open(state_path, "r", encoding="utf-8") as f:
200
+ return json.load(f)
201
+ except Exception:
202
+ return {"task": None, "iteration": 0, "started_at": None}
203
+
204
+
205
+ def save_state(repo_root: str, state: dict) -> None:
206
+ """Save Ralph Loop state to file"""
207
+ state_path = os.path.join(repo_root, STATE_FILE)
208
+ try:
209
+ # Ensure directory exists
210
+ os.makedirs(os.path.dirname(state_path), exist_ok=True)
211
+ with open(state_path, "w", encoding="utf-8") as f:
212
+ json.dump(state, f, indent=2, ensure_ascii=False)
213
+ except Exception:
214
+ pass
215
+
216
+
217
+ def check_completion(agent_output: str, markers: list[str]) -> tuple[bool, list[str]]:
218
+ """
219
+ Check if all completion markers are present in agent output.
220
+
221
+ Returns:
222
+ (all_complete, missing_markers)
223
+ """
224
+ missing = []
225
+ for marker in markers:
226
+ if marker not in agent_output:
227
+ missing.append(marker)
228
+
229
+ return len(missing) == 0, missing
230
+
231
+
232
+ def main():
233
+ try:
234
+ input_data = json.load(sys.stdin)
235
+ except json.JSONDecodeError:
236
+ # If can't parse input, allow stop
237
+ sys.exit(0)
238
+
239
+ # Get event info
240
+ hook_event = input_data.get("hook_event_name", "")
241
+
242
+ # Only handle SubagentStop event
243
+ if hook_event != "SubagentStop":
244
+ sys.exit(0)
245
+
246
+ # Get subagent info
247
+ subagent_type = input_data.get("subagent_type", "")
248
+ agent_output = input_data.get("agent_output", "")
249
+ original_prompt = input_data.get("prompt", "")
250
+ cwd = input_data.get("cwd", os.getcwd())
251
+
252
+ # Only control check agent
253
+ if subagent_type != TARGET_AGENT:
254
+ sys.exit(0)
255
+
256
+ # Skip Ralph Loop for finish phase (already verified in check phase)
257
+ if "[finish]" in original_prompt.lower():
258
+ sys.exit(0)
259
+
260
+ # Find repo root
261
+ repo_root = find_repo_root(cwd)
262
+ if not repo_root:
263
+ sys.exit(0)
264
+
265
+ # Get current task
266
+ task_dir = get_current_task(repo_root)
267
+ if not task_dir:
268
+ sys.exit(0)
269
+
270
+ # Load state
271
+ state = load_state(repo_root)
272
+
273
+ # Reset state if task changed or state is too old
274
+ should_reset = False
275
+ if state.get("task") != task_dir:
276
+ should_reset = True
277
+ elif state.get("started_at"):
278
+ try:
279
+ started = datetime.fromisoformat(state["started_at"])
280
+ if (datetime.now() - started).total_seconds() > STATE_TIMEOUT_MINUTES * 60:
281
+ should_reset = True
282
+ except (ValueError, TypeError):
283
+ should_reset = True
284
+
285
+ if should_reset:
286
+ state = {
287
+ "task": task_dir,
288
+ "iteration": 0,
289
+ "started_at": datetime.now().isoformat(),
290
+ }
291
+
292
+ # Increment iteration
293
+ state["iteration"] = state.get("iteration", 0) + 1
294
+ current_iteration = state["iteration"]
295
+
296
+ # Save state
297
+ save_state(repo_root, state)
298
+
299
+ # Safety check: max iterations
300
+ if current_iteration >= MAX_ITERATIONS:
301
+ # Allow stop, reset state for next run
302
+ state["iteration"] = 0
303
+ save_state(repo_root, state)
304
+ output = {
305
+ "decision": "allow",
306
+ "reason": f"Max iterations ({MAX_ITERATIONS}) reached. Stopping to prevent infinite loop.",
307
+ }
308
+ print(json.dumps(output, ensure_ascii=False))
309
+ sys.exit(0)
310
+
311
+ # Check if verify commands are configured
312
+ verify_commands = get_verify_commands(repo_root)
313
+
314
+ if verify_commands:
315
+ # Use programmatic verification
316
+ passed, message = run_verify_commands(repo_root, verify_commands)
317
+
318
+ if passed:
319
+ # All verify commands passed, allow stop
320
+ state["iteration"] = 0
321
+ save_state(repo_root, state)
322
+ output = {
323
+ "decision": "allow",
324
+ "reason": "All verify commands passed. Check phase complete.",
325
+ }
326
+ print(json.dumps(output, ensure_ascii=False))
327
+ sys.exit(0)
328
+ else:
329
+ # Verification failed, block stop
330
+ output = {
331
+ "decision": "block",
332
+ "reason": f"Iteration {current_iteration}/{MAX_ITERATIONS}. Verification failed:\n{message}\n\nPlease fix the issues and try again.",
333
+ }
334
+ print(json.dumps(output, ensure_ascii=False))
335
+ sys.exit(0)
336
+ else:
337
+ # No verify commands, fall back to completion markers
338
+ markers = get_completion_markers(repo_root, task_dir)
339
+ all_complete, missing = check_completion(agent_output, markers)
340
+
341
+ if all_complete:
342
+ # All checks complete, allow stop
343
+ state["iteration"] = 0
344
+ save_state(repo_root, state)
345
+ output = {
346
+ "decision": "allow",
347
+ "reason": "All completion markers found. Check phase complete.",
348
+ }
349
+ print(json.dumps(output, ensure_ascii=False))
350
+ sys.exit(0)
351
+ else:
352
+ # Missing markers, block stop and continue
353
+ output = {
354
+ "decision": "block",
355
+ "reason": f"""Iteration {current_iteration}/{MAX_ITERATIONS}. Missing completion markers: {", ".join(missing)}.
356
+
357
+ IMPORTANT: You must ACTUALLY run the checks, not just output the markers.
358
+ - Did you run lint? What was the output?
359
+ - Did you run typecheck? What was the output?
360
+ - Did they actually pass with zero errors?
361
+
362
+ Only output a marker (e.g., LINT_FINISH) AFTER:
363
+ 1. You have executed the corresponding command
364
+ 2. The command completed with zero errors
365
+ 3. You have shown the command output in your response
366
+
367
+ Do NOT output markers just to escape the loop. The loop exists to ensure quality.""",
368
+ }
369
+ print(json.dumps(output, ensure_ascii=False))
370
+ sys.exit(0)
371
+
372
+
373
+ if __name__ == "__main__":
374
+ main()
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Session Start Hook - Inject structured context
4
+
5
+ Matcher: "startup" - only runs on normal startup (not resume/clear/compact)
6
+
7
+ This hook injects:
8
+ 1. Current state (git status, current task, task queue)
9
+ 2. Workflow guide
10
+ 3. Guidelines index (frontend/backend/guides)
11
+ 4. Session instructions (start.md)
12
+ 5. Action directive
13
+ """
14
+
15
+ import os
16
+ import subprocess
17
+ import sys
18
+ from pathlib import Path
19
+
20
+
21
+ def should_skip_injection() -> bool:
22
+ """
23
+ Determine if context injection should be skipped.
24
+
25
+ Multi-agent scripts (start.sh, plan.sh) set CLAUDE_NON_INTERACTIVE=1
26
+ to prevent duplicate context injection.
27
+ """
28
+ return os.environ.get("CLAUDE_NON_INTERACTIVE") == "1"
29
+
30
+
31
+ def read_file(path: Path, fallback: str = "") -> str:
32
+ """Read file content, return fallback if not found."""
33
+ try:
34
+ return path.read_text(encoding="utf-8")
35
+ except (FileNotFoundError, PermissionError):
36
+ return fallback
37
+
38
+
39
+ def run_script(script_path: Path) -> str:
40
+ """Run a script and return its output."""
41
+ try:
42
+ result = subprocess.run(
43
+ [str(script_path)],
44
+ capture_output=True,
45
+ text=True,
46
+ timeout=5,
47
+ cwd=script_path.parent.parent.parent, # repo root
48
+ )
49
+ return result.stdout if result.returncode == 0 else "No context available"
50
+ except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError):
51
+ return "No context available"
52
+
53
+
54
+ def main():
55
+ # Skip injection in non-interactive mode (multi-agent scripts set CLAUDE_NON_INTERACTIVE=1)
56
+ if should_skip_injection():
57
+ sys.exit(0)
58
+
59
+ project_dir = Path(os.environ.get("CLAUDE_PROJECT_DIR", ".")).resolve()
60
+ trellis_dir = project_dir / ".trellis"
61
+ claude_dir = project_dir / ".claude"
62
+
63
+ # 1. Header
64
+ print("""<session-context>
65
+ You are starting a new session in a Trellis-managed project.
66
+ Read and follow all instructions below carefully.
67
+ </session-context>
68
+ """)
69
+
70
+ # 2. Current Context (dynamic)
71
+ print("<current-state>")
72
+ context_script = trellis_dir / "scripts" / "get-context.sh"
73
+ print(run_script(context_script))
74
+ print("</current-state>")
75
+ print()
76
+
77
+ # 3. Workflow Guide
78
+ print("<workflow>")
79
+ workflow_content = read_file(trellis_dir / "workflow.md", "No workflow.md found")
80
+ print(workflow_content)
81
+ print("</workflow>")
82
+ print()
83
+
84
+ # 4. Guidelines Index
85
+ print("<guidelines>")
86
+
87
+ print("## Frontend")
88
+ frontend_index = read_file(
89
+ trellis_dir / "spec" / "frontend" / "index.md", "Not configured"
90
+ )
91
+ print(frontend_index)
92
+ print()
93
+
94
+ print("## Backend")
95
+ backend_index = read_file(
96
+ trellis_dir / "spec" / "backend" / "index.md", "Not configured"
97
+ )
98
+ print(backend_index)
99
+ print()
100
+
101
+ print("## Guides")
102
+ guides_index = read_file(
103
+ trellis_dir / "spec" / "guides" / "index.md", "Not configured"
104
+ )
105
+ print(guides_index)
106
+
107
+ print("</guidelines>")
108
+ print()
109
+
110
+ # 5. Session Instructions
111
+ print("<instructions>")
112
+ start_md = read_file(
113
+ claude_dir / "commands" / "trellis" / "start.md", "No start.md found"
114
+ )
115
+ print(start_md)
116
+ print("</instructions>")
117
+ print()
118
+
119
+ # 6. Final directive
120
+ print("""<ready>
121
+ Context loaded. Wait for user's first message, then follow <instructions> to handle their request.
122
+ </ready>""")
123
+
124
+
125
+ if __name__ == "__main__":
126
+ main()
@@ -0,0 +1,41 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "startup",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.py\"",
10
+ "timeout": 10
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "PreToolUse": [
16
+ {
17
+ "matcher": "Task",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject-subagent-context.py\"",
22
+ "timeout": 30
23
+ }
24
+ ]
25
+ }
26
+ ],
27
+ "SubagentStop": [
28
+ {
29
+ "matcher": "check",
30
+ "hooks": [
31
+ {
32
+ "type": "command",
33
+ "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/ralph-loop.py\"",
34
+ "timeout": 10
35
+ }
36
+ ]
37
+ }
38
+ ]
39
+ },
40
+ "enabledPlugins": {}
41
+ }