@yan-geroge/omg 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 (59) hide show
  1. package/.claude/agents/trellis-check.md +109 -0
  2. package/.claude/agents/trellis-implement.md +109 -0
  3. package/.claude/agents/trellis-research.md +137 -0
  4. package/.claude/commands/trellis/continue.md +55 -0
  5. package/.claude/commands/trellis/finish-work.md +66 -0
  6. package/.claude/hooks/inject-subagent-context.py +749 -0
  7. package/.claude/hooks/inject-workflow-state.py +387 -0
  8. package/.claude/hooks/session-start.py +797 -0
  9. package/.claude/settings.json +73 -0
  10. package/.claude/skills/trellis-before-dev/SKILL.md +34 -0
  11. package/.claude/skills/trellis-brainstorm/SKILL.md +548 -0
  12. package/.claude/skills/trellis-break-loop/SKILL.md +130 -0
  13. package/.claude/skills/trellis-check/SKILL.md +92 -0
  14. package/.claude/skills/trellis-meta/SKILL.md +73 -0
  15. package/.claude/skills/trellis-meta/references/customize-local/add-project-local-conventions.md +83 -0
  16. package/.claude/skills/trellis-meta/references/customize-local/change-agents.md +54 -0
  17. package/.claude/skills/trellis-meta/references/customize-local/change-context-loading.md +81 -0
  18. package/.claude/skills/trellis-meta/references/customize-local/change-hooks.md +57 -0
  19. package/.claude/skills/trellis-meta/references/customize-local/change-skills-or-commands.md +78 -0
  20. package/.claude/skills/trellis-meta/references/customize-local/change-spec-structure.md +83 -0
  21. package/.claude/skills/trellis-meta/references/customize-local/change-task-lifecycle.md +90 -0
  22. package/.claude/skills/trellis-meta/references/customize-local/change-workflow.md +64 -0
  23. package/.claude/skills/trellis-meta/references/customize-local/overview.md +55 -0
  24. package/.claude/skills/trellis-meta/references/local-architecture/context-injection.md +68 -0
  25. package/.claude/skills/trellis-meta/references/local-architecture/generated-files.md +80 -0
  26. package/.claude/skills/trellis-meta/references/local-architecture/overview.md +51 -0
  27. package/.claude/skills/trellis-meta/references/local-architecture/spec-system.md +102 -0
  28. package/.claude/skills/trellis-meta/references/local-architecture/task-system.md +101 -0
  29. package/.claude/skills/trellis-meta/references/local-architecture/workflow.md +75 -0
  30. package/.claude/skills/trellis-meta/references/local-architecture/workspace-memory.md +71 -0
  31. package/.claude/skills/trellis-meta/references/platform-files/agents.md +79 -0
  32. package/.claude/skills/trellis-meta/references/platform-files/hooks-and-settings.md +69 -0
  33. package/.claude/skills/trellis-meta/references/platform-files/overview.md +59 -0
  34. package/.claude/skills/trellis-meta/references/platform-files/platform-map.md +74 -0
  35. package/.claude/skills/trellis-meta/references/platform-files/skills-and-commands.md +83 -0
  36. package/.claude/skills/trellis-spec-bootstarp/SKILL.md +41 -0
  37. package/.claude/skills/trellis-spec-bootstarp/references/mcp-setup.md +90 -0
  38. package/.claude/skills/trellis-spec-bootstarp/references/repository-analysis.md +59 -0
  39. package/.claude/skills/trellis-spec-bootstarp/references/spec-task-planning.md +61 -0
  40. package/.claude/skills/trellis-spec-bootstarp/references/spec-writing.md +70 -0
  41. package/.claude/skills/trellis-update-spec/SKILL.md +356 -0
  42. package/CLAUDE.md +88 -0
  43. package/agents/architect.md +80 -0
  44. package/agents/executor.md +79 -0
  45. package/agents/planner.md +76 -0
  46. package/agents/reviewer.md +81 -0
  47. package/agents/tdd-guide.md +95 -0
  48. package/agents/verifier.md +81 -0
  49. package/hooks/keyword-detector.mjs +115 -0
  50. package/hooks/lib/keyword-detector.js +89 -0
  51. package/hooks/session-start.mjs +104 -0
  52. package/marketplace.json +12 -0
  53. package/package.json +34 -0
  54. package/plugin.json +14 -0
  55. package/scripts/diagnose-marketplace.js +84 -0
  56. package/scripts/e2e-diagnose.mjs +118 -0
  57. package/scripts/validate-with-csc-schema.mjs +87 -0
  58. package/skills/costrict-autopilot/SKILL.md +53 -0
  59. package/skills/costrict-team/SKILL.md +65 -0
@@ -0,0 +1,749 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Multi-Platform Sub-Agent Context Injection Hook
5
+
6
+ Injects task-specific context when sub-agents (implement, check, research) are spawned.
7
+
8
+ Core Design Philosophy:
9
+ - Hook is responsible for injecting all context, subagent works autonomously with complete info
10
+ - Each agent has a dedicated jsonl file defining its context
11
+ - No resume needed, no segmentation, behavior controlled by code not prompt
12
+
13
+ Trigger: PreToolUse (before Task tool call)
14
+
15
+ Context Source: Trellis active task resolver points to task directory
16
+ - implement.jsonl - Implement agent dedicated context
17
+ - check.jsonl - Check agent dedicated context
18
+ - prd.md - Requirements document
19
+ - info.md - Technical design
20
+ - codex-review-output.txt - Code Review results
21
+ """
22
+ from __future__ import annotations
23
+
24
+ # IMPORTANT: Suppress all warnings FIRST
25
+ import warnings
26
+ warnings.filterwarnings("ignore")
27
+
28
+ import json
29
+ import os
30
+ import sys
31
+ from pathlib import Path
32
+ from typing import Any
33
+
34
+ # IMPORTANT: Force stdout to use UTF-8 on Windows
35
+ # This fixes UnicodeEncodeError when outputting non-ASCII characters
36
+ if sys.platform.startswith("win"):
37
+ import io as _io
38
+ if hasattr(sys.stdout, "reconfigure"):
39
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
40
+ elif hasattr(sys.stdout, "detach"):
41
+ sys.stdout = _io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8", errors="replace") # type: ignore[union-attr]
42
+
43
+
44
+ # =============================================================================
45
+ # Path Constants (change here to rename directories)
46
+ # =============================================================================
47
+
48
+ DIR_WORKFLOW = ".trellis"
49
+ DIR_SPEC = "spec"
50
+ FILE_TASK_JSON = "task.json"
51
+
52
+ # =============================================================================
53
+ # Subagent Constants (change here to rename subagent types)
54
+ # =============================================================================
55
+
56
+ AGENT_IMPLEMENT = "trellis-implement"
57
+ AGENT_CHECK = "trellis-check"
58
+ AGENT_RESEARCH = "trellis-research"
59
+
60
+ # Agents that require a task directory
61
+ AGENTS_REQUIRE_TASK = (AGENT_IMPLEMENT, AGENT_CHECK)
62
+ # All supported agents
63
+ AGENTS_ALL = (AGENT_IMPLEMENT, AGENT_CHECK, AGENT_RESEARCH)
64
+
65
+
66
+ def find_repo_root(start_path: str) -> str | None:
67
+ """
68
+ Find git repo root from start_path upwards
69
+
70
+ Returns:
71
+ Repo root path, or None if not found
72
+ """
73
+ current = Path(start_path).resolve()
74
+ while current != current.parent:
75
+ if (current / ".git").exists():
76
+ return str(current)
77
+ current = current.parent
78
+ return None
79
+
80
+
81
+ def _detect_platform(input_data: dict) -> str | None:
82
+ if isinstance(input_data.get("cursor_version"), str):
83
+ return "cursor"
84
+ env_map = {
85
+ "CLAUDE_PROJECT_DIR": "claude",
86
+ "CURSOR_PROJECT_DIR": "cursor",
87
+ "CODEBUDDY_PROJECT_DIR": "codebuddy",
88
+ "FACTORY_PROJECT_DIR": "droid",
89
+ "GEMINI_PROJECT_DIR": "gemini",
90
+ "QODER_PROJECT_DIR": "qoder",
91
+ "KIRO_PROJECT_DIR": "kiro",
92
+ "COPILOT_PROJECT_DIR": "copilot",
93
+ }
94
+ for env_name, platform in env_map.items():
95
+ if os.environ.get(env_name):
96
+ return platform
97
+ script_parts = set(Path(sys.argv[0]).parts)
98
+ if ".claude" in script_parts:
99
+ return "claude"
100
+ if ".cursor" in script_parts:
101
+ return "cursor"
102
+ if ".gemini" in script_parts:
103
+ return "gemini"
104
+ if ".qoder" in script_parts:
105
+ return "qoder"
106
+ if ".codebuddy" in script_parts:
107
+ return "codebuddy"
108
+ if ".factory" in script_parts:
109
+ return "droid"
110
+ if ".kiro" in script_parts:
111
+ return "kiro"
112
+ return None
113
+
114
+
115
+ def get_current_task(repo_root: str, input_data: dict) -> str | None:
116
+ """Resolve current task directory through the unified active task resolver."""
117
+ scripts_dir = Path(repo_root) / DIR_WORKFLOW / "scripts"
118
+ if str(scripts_dir) not in sys.path:
119
+ sys.path.insert(0, str(scripts_dir))
120
+ try:
121
+ from common.active_task import resolve_active_task # type: ignore[import-not-found]
122
+ except Exception:
123
+ return None
124
+
125
+ active = resolve_active_task(
126
+ Path(repo_root),
127
+ input_data,
128
+ platform=_detect_platform(input_data),
129
+ )
130
+ return active.task_path
131
+
132
+
133
+ def read_file_content(base_path: str, file_path: str) -> str | None:
134
+ """Read file content, return None if file doesn't exist"""
135
+ full_path = os.path.join(base_path, file_path)
136
+ if os.path.exists(full_path) and os.path.isfile(full_path):
137
+ try:
138
+ with open(full_path, "r", encoding="utf-8") as f:
139
+ return f.read()
140
+ except Exception:
141
+ return None
142
+ return None
143
+
144
+
145
+ def read_directory_contents(
146
+ base_path: str, dir_path: str, max_files: int = 20
147
+ ) -> list[tuple[str, str]]:
148
+ """
149
+ Read all .md files in a directory
150
+
151
+ Args:
152
+ base_path: Base path (usually repo_root)
153
+ dir_path: Directory relative path
154
+ max_files: Max files to read (prevent huge directories)
155
+
156
+ Returns:
157
+ [(file_path, content), ...]
158
+ """
159
+ full_path = os.path.join(base_path, dir_path)
160
+ if not os.path.exists(full_path) or not os.path.isdir(full_path):
161
+ return []
162
+
163
+ results = []
164
+ try:
165
+ # Only read .md files, sorted by filename
166
+ md_files = sorted(
167
+ [
168
+ f
169
+ for f in os.listdir(full_path)
170
+ if f.endswith(".md") and os.path.isfile(os.path.join(full_path, f))
171
+ ]
172
+ )
173
+
174
+ for filename in md_files[:max_files]:
175
+ file_full_path = os.path.join(full_path, filename)
176
+ relative_path = os.path.join(dir_path, filename)
177
+ try:
178
+ with open(file_full_path, "r", encoding="utf-8") as f:
179
+ content = f.read()
180
+ results.append((relative_path, content))
181
+ except Exception:
182
+ continue
183
+ except Exception:
184
+ pass
185
+
186
+ return results
187
+
188
+
189
+ def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]:
190
+ """
191
+ Read all file/directory contents referenced in jsonl file
192
+
193
+ Schema:
194
+ {"file": "path/to/file.md", "reason": "..."}
195
+ {"file": "path/to/dir/", "type": "directory", "reason": "..."}
196
+ {"_example": "..."} # seed row — skipped (no `file` field)
197
+
198
+ Rows without a ``file`` field (e.g. the self-describing seed line written
199
+ by ``task.py create`` before the agent has curated entries) are skipped
200
+ silently. If the resulting entry list is empty, a stderr warning is
201
+ emitted so the operator can debug missing context.
202
+
203
+ Returns:
204
+ [(path, content), ...]
205
+ """
206
+ full_path = os.path.join(base_path, jsonl_path)
207
+ if not os.path.exists(full_path):
208
+ print(
209
+ f"[inject-subagent-context] WARN: {jsonl_path} not found — "
210
+ f"sub-agent will receive only prd.md",
211
+ file=sys.stderr,
212
+ )
213
+ return []
214
+
215
+ results = []
216
+ saw_real_entry = False
217
+ try:
218
+ with open(full_path, "r", encoding="utf-8") as f:
219
+ for line in f:
220
+ line = line.strip()
221
+ if not line:
222
+ continue
223
+ try:
224
+ item = json.loads(line)
225
+ file_path = item.get("file") or item.get("path")
226
+ entry_type = item.get("type", "file")
227
+
228
+ if not file_path:
229
+ # Seed / comment row — skip silently
230
+ continue
231
+
232
+ saw_real_entry = True
233
+ if entry_type == "directory":
234
+ # Read all .md files in directory
235
+ dir_contents = read_directory_contents(base_path, file_path)
236
+ results.extend(dir_contents)
237
+ else:
238
+ # Read single file
239
+ content = read_file_content(base_path, file_path)
240
+ if content:
241
+ results.append((file_path, content))
242
+ except json.JSONDecodeError:
243
+ continue
244
+ except Exception:
245
+ pass
246
+
247
+ if not saw_real_entry:
248
+ print(
249
+ f"[inject-subagent-context] WARN: {jsonl_path} has no curated "
250
+ f"entries (only seed / empty) — sub-agent will receive only "
251
+ f"prd.md. See workflow.md Phase 1.3 for curation guidance.",
252
+ file=sys.stderr,
253
+ )
254
+
255
+ return results
256
+
257
+
258
+
259
+
260
+ def get_agent_context(repo_root: str, task_dir: str, agent_type: str) -> str:
261
+ """
262
+ Get context from {agent_type}.jsonl for the specified agent.
263
+ Only reads implement.jsonl or check.jsonl (the two JSONL files the task system creates).
264
+ """
265
+ context_parts = []
266
+
267
+ agent_jsonl = f"{task_dir}/{agent_type}.jsonl"
268
+ for file_path, content in read_jsonl_entries(repo_root, agent_jsonl):
269
+ context_parts.append(f"=== {file_path} ===\n{content}")
270
+
271
+ return "\n\n".join(context_parts)
272
+
273
+
274
+ def get_implement_context(repo_root: str, task_dir: str) -> str:
275
+ """
276
+ Complete context for Implement Agent
277
+
278
+ Read order:
279
+ 1. All files in implement.jsonl (dev specs)
280
+ 2. prd.md (requirements)
281
+ 3. info.md (technical design)
282
+ """
283
+ context_parts = []
284
+
285
+ # 1. Read implement.jsonl
286
+ base_context = get_agent_context(repo_root, task_dir, "implement")
287
+ if base_context:
288
+ context_parts.append(base_context)
289
+
290
+ # 2. Requirements document
291
+ prd_content = read_file_content(repo_root, f"{task_dir}/prd.md")
292
+ if prd_content:
293
+ context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}")
294
+
295
+ # 3. Technical design
296
+ info_content = read_file_content(repo_root, f"{task_dir}/info.md")
297
+ if info_content:
298
+ context_parts.append(
299
+ f"=== {task_dir}/info.md (Technical Design) ===\n{info_content}"
300
+ )
301
+
302
+ return "\n\n".join(context_parts)
303
+
304
+
305
+ def get_check_context(repo_root: str, task_dir: str) -> str:
306
+ """
307
+ Context for Check Agent: check.jsonl + prd.md
308
+ """
309
+ context_parts = []
310
+
311
+ for file_path, content in read_jsonl_entries(repo_root, f"{task_dir}/check.jsonl"):
312
+ context_parts.append(f"=== {file_path} ===\n{content}")
313
+
314
+ prd_content = read_file_content(repo_root, f"{task_dir}/prd.md")
315
+ if prd_content:
316
+ context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}")
317
+
318
+ return "\n\n".join(context_parts)
319
+
320
+
321
+ def get_finish_context(repo_root: str, task_dir: str) -> str:
322
+ """
323
+ Context for Finish phase: reuses check.jsonl + prd.md
324
+ (Finish is a final check, same context source.)
325
+ """
326
+ return get_check_context(repo_root, task_dir)
327
+
328
+
329
+
330
+ def build_implement_prompt(original_prompt: str, context: str) -> str:
331
+ """Build complete prompt for Implement"""
332
+ return f"""<!-- trellis-hook-injected -->
333
+ # Implement Agent Task
334
+
335
+ You are the Implement Agent in the Multi-Agent Pipeline.
336
+
337
+ ## Your Context
338
+
339
+ All the information you need has been prepared for you:
340
+
341
+ {context}
342
+
343
+ ---
344
+
345
+ ## Your Task
346
+
347
+ {original_prompt}
348
+
349
+ ---
350
+
351
+ ## Workflow
352
+
353
+ 1. **Understand specs** - All dev specs are injected above, understand them
354
+ 2. **Understand requirements** - Read requirements document and technical design
355
+ 3. **Implement feature** - Implement following specs and design
356
+ 4. **Self-check** - Ensure code quality against check specs
357
+
358
+ ## Important Constraints
359
+
360
+ - Do NOT execute git commit, only code modifications
361
+ - Follow all dev specs injected above
362
+ - Report list of modified/created files when done"""
363
+
364
+
365
+ def build_check_prompt(original_prompt: str, context: str) -> str:
366
+ """Build complete prompt for Check"""
367
+ return f"""<!-- trellis-hook-injected -->
368
+ # Check Agent Task
369
+
370
+ You are the Check Agent in the Multi-Agent Pipeline (code and cross-layer checker).
371
+
372
+ ## Your Context
373
+
374
+ All check specs and dev specs you need:
375
+
376
+ {context}
377
+
378
+ ---
379
+
380
+ ## Your Task
381
+
382
+ {original_prompt}
383
+
384
+ ---
385
+
386
+ ## Workflow
387
+
388
+ 1. **Get changes** - Run `git diff --name-only` and `git diff` to get code changes
389
+ 2. **Check against specs** - Check item by item against specs above
390
+ 3. **Self-fix** - Fix issues directly, don't just report
391
+ 4. **Run verification** - Run project's lint and typecheck commands
392
+
393
+ ## Important Constraints
394
+
395
+ - Fix issues yourself, don't just report
396
+ - Must execute complete checklist in check specs
397
+ - Pay special attention to impact radius analysis (L1-L5)"""
398
+
399
+
400
+ def build_finish_prompt(original_prompt: str, context: str) -> str:
401
+ """Build complete prompt for Finish (final check before PR)"""
402
+ return f"""<!-- trellis-hook-injected -->
403
+ # Finish Agent Task
404
+
405
+ You are performing the final check before creating a PR.
406
+
407
+ ## Your Context
408
+
409
+ Finish checklist and requirements:
410
+
411
+ {context}
412
+
413
+ ---
414
+
415
+ ## Your Task
416
+
417
+ {original_prompt}
418
+
419
+ ---
420
+
421
+ ## Workflow
422
+
423
+ 1. **Review changes** - Run `git diff --name-only` to see all changed files
424
+ 2. **Verify requirements** - Check each requirement in prd.md is implemented
425
+ 3. **Spec sync** - Analyze whether changes introduce new patterns, contracts, or conventions
426
+ - If new pattern/convention found: read target spec file → update it → update index.md if needed
427
+ - If infra/cross-layer change: follow the 7-section mandatory template from update-spec.md
428
+ - If pure code fix with no new patterns: skip this step
429
+ 4. **Run final checks** - Execute lint and typecheck
430
+ 5. **Confirm ready** - Ensure code is ready for PR
431
+
432
+ ## Important Constraints
433
+
434
+ - You MAY update spec files when gaps are detected (use update-spec.md as guide)
435
+ - MUST read the target spec file BEFORE editing (avoid duplicating existing content)
436
+ - Do NOT update specs for trivial changes (typos, formatting, obvious fixes)
437
+ - If critical CODE issues found, report them clearly (fix specs, not code)
438
+ - Verify all acceptance criteria in prd.md are met"""
439
+
440
+
441
+
442
+ def get_research_context(repo_root: str, task_dir: str | None) -> str:
443
+ """
444
+ Context for Research Agent — project structure overview for spec directories.
445
+
446
+ `task_dir` kept for signature parity with get_implement_context / get_check_context
447
+ so the dispatcher can call them uniformly.
448
+ """
449
+ _ = task_dir
450
+ context_parts = []
451
+
452
+ # 1. Project structure overview (dynamically discover spec directories)
453
+ spec_path = f"{DIR_WORKFLOW}/{DIR_SPEC}"
454
+ spec_root = Path(repo_root) / DIR_WORKFLOW / DIR_SPEC
455
+
456
+ # Build spec tree dynamically
457
+ tree_lines = [f"{spec_path}/"]
458
+ if spec_root.is_dir():
459
+ pkg_dirs = sorted(d for d in spec_root.iterdir() if d.is_dir())
460
+ for i, pkg_dir in enumerate(pkg_dirs):
461
+ is_last = i == len(pkg_dirs) - 1
462
+ prefix = "└── " if is_last else "├── "
463
+ layers = sorted(d.name for d in pkg_dir.iterdir() if d.is_dir())
464
+ layer_info = f" ({', '.join(layers)})" if layers else ""
465
+ tree_lines.append(f"{prefix}{pkg_dir.name}/{layer_info}")
466
+
467
+ spec_tree = "\n".join(tree_lines)
468
+
469
+ project_structure = f"""## Project Spec Directory Structure
470
+
471
+ ```
472
+ {spec_tree}
473
+ ```
474
+
475
+ To get structured package info, run: `python ./{DIR_WORKFLOW}/scripts/get_context.py --mode packages`
476
+
477
+ ## Search Tips
478
+
479
+ - Spec files: `{spec_path}/**/*.md`
480
+ - Code search: Use Glob and Grep tools
481
+ - Tech solutions: Use mcp__exa__web_search_exa or mcp__exa__get_code_context_exa"""
482
+
483
+ context_parts.append(project_structure)
484
+
485
+ return "\n\n".join(context_parts)
486
+
487
+
488
+ def build_research_prompt(original_prompt: str, context: str) -> str:
489
+ """Build complete prompt for Research"""
490
+ return f"""# Research Agent Task
491
+
492
+ You are the Research Agent in the Multi-Agent Pipeline (search researcher).
493
+
494
+ ## Core Principle
495
+
496
+ **You do one thing: find and explain information.**
497
+
498
+ You are a documenter, not a reviewer.
499
+
500
+ ## Project Info
501
+
502
+ {context}
503
+
504
+ ---
505
+
506
+ ## Your Task
507
+
508
+ {original_prompt}
509
+
510
+ ---
511
+
512
+ ## Workflow
513
+
514
+ 1. **Understand query** - Determine search type (internal/external) and scope
515
+ 2. **Plan search** - List search steps for complex queries
516
+ 3. **Execute search** - Execute multiple independent searches in parallel
517
+ 4. **Organize results** - Output structured report
518
+
519
+ ## Search Tools
520
+
521
+ | Tool | Purpose |
522
+ |------|---------|
523
+ | Glob | Search by filename pattern |
524
+ | Grep | Search by content |
525
+ | Read | Read file content |
526
+ | mcp__exa__web_search_exa | External web search |
527
+ | mcp__exa__get_code_context_exa | External code/doc search |
528
+
529
+ ## Strict Boundaries
530
+
531
+ **Only allowed**: Describe what exists, where it is, how it works
532
+
533
+ **Forbidden** (unless explicitly asked):
534
+ - Suggest improvements
535
+ - Criticize implementation
536
+ - Recommend refactoring
537
+ - Modify any files
538
+
539
+ ## Report Format
540
+
541
+ Provide structured search results including:
542
+ - List of files found (with paths)
543
+ - Code pattern analysis (if applicable)
544
+ - Related spec documents
545
+ - External references (if any)"""
546
+
547
+
548
+ def _string_value(value: Any) -> str:
549
+ if isinstance(value, str):
550
+ stripped = value.strip()
551
+ return stripped
552
+ return ""
553
+
554
+
555
+ def _extract_subagent_name(value: Any) -> str:
556
+ """Extract a sub-agent name from common platform encodings.
557
+
558
+ Cursor's native Task args encode custom sub-agents as a protobuf oneof,
559
+ which can appear in hook JSON as either ``{"custom": {"name": "..."}}``
560
+ or ``{"type": {"case": "custom", "value": {"name": "..."}}}``.
561
+ """
562
+ direct = _string_value(value)
563
+ if direct:
564
+ return direct
565
+
566
+ if not isinstance(value, dict):
567
+ return ""
568
+
569
+ for key in ("name", "subagent_type_name", "subagentTypeName"):
570
+ direct = _string_value(value.get(key))
571
+ if direct:
572
+ return direct
573
+
574
+ custom = value.get("custom")
575
+ if isinstance(custom, dict):
576
+ custom_name = _string_value(custom.get("name"))
577
+ if custom_name:
578
+ return custom_name
579
+
580
+ oneof = value.get("type")
581
+ if isinstance(oneof, dict):
582
+ case_name = _string_value(oneof.get("case"))
583
+ if case_name == "custom":
584
+ nested_value = oneof.get("value")
585
+ if isinstance(nested_value, dict):
586
+ custom_name = _string_value(nested_value.get("name"))
587
+ if custom_name:
588
+ return custom_name
589
+ if case_name:
590
+ return case_name
591
+
592
+ case_name = _string_value(value.get("case"))
593
+ if case_name == "custom":
594
+ nested_value = value.get("value")
595
+ if isinstance(nested_value, dict):
596
+ custom_name = _string_value(nested_value.get("name"))
597
+ if custom_name:
598
+ return custom_name
599
+ if case_name:
600
+ return case_name
601
+
602
+ for agent_name in AGENTS_ALL:
603
+ if agent_name in value:
604
+ return agent_name
605
+
606
+ return ""
607
+
608
+
609
+ def _extract_subagent_type(tool_input: dict) -> str:
610
+ for key in (
611
+ "subagent_type",
612
+ "subagentType",
613
+ "subagent_type_name",
614
+ "subagentTypeName",
615
+ "agent_type",
616
+ "agentType",
617
+ "name",
618
+ ):
619
+ agent_name = _extract_subagent_name(tool_input.get(key))
620
+ if agent_name:
621
+ return agent_name
622
+ return ""
623
+
624
+
625
+ def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]:
626
+ """Parse hook input across different platform formats.
627
+
628
+ Returns (subagent_type, original_prompt, tool_input).
629
+ Handles:
630
+ - Claude Code / Qoder / CodeBuddy / Droid: tool_name=Task|Agent, tool_input.subagent_type
631
+ - Cursor: tool_name=Task|Subagent, tool_input.subagent_type
632
+ - Copilot CLI: toolName=task (camelCase key, lowercase value)
633
+ - Gemini CLI: tool_name IS the agent name (BeforeTool matcher already filtered)
634
+ - Kiro: agentSpawn hook, agent_name field at top level
635
+ """
636
+ tool_input = input_data.get("tool_input", {})
637
+
638
+ # Standard format: Task/Agent tool with subagent_type
639
+ tool_name = input_data.get("tool_name", "") or input_data.get("toolName", "")
640
+ if tool_name.lower() in ("task", "agent", "subagent"):
641
+ return (
642
+ _extract_subagent_type(tool_input),
643
+ tool_input.get("prompt", ""),
644
+ tool_input,
645
+ )
646
+
647
+ # Kiro: agentSpawn hook passes agent_name at top level
648
+ agent_name = input_data.get("agent_name", "")
649
+ if agent_name:
650
+ return agent_name, tool_input.get("prompt", input_data.get("prompt", "")), tool_input
651
+
652
+ # Gemini CLI: BeforeTool where tool_name IS the agent name
653
+ # (matcher already ensured it's one of our agents)
654
+ if tool_name in AGENTS_ALL:
655
+ return tool_name, tool_input.get("prompt", ""), tool_input
656
+
657
+ # Copilot CLI: toolName field (camelCase), value might be the agent name
658
+ tool_name_camel = input_data.get("toolName", "")
659
+ if tool_name_camel in AGENTS_ALL:
660
+ return tool_name_camel, input_data.get("toolArgs", ""), tool_input
661
+
662
+ return "", "", tool_input
663
+
664
+
665
+ def main():
666
+ if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
667
+ sys.exit(0)
668
+
669
+ try:
670
+ input_data = json.load(sys.stdin)
671
+ except json.JSONDecodeError:
672
+ sys.exit(0)
673
+
674
+ subagent_type, original_prompt, tool_input = _parse_hook_input(input_data)
675
+ cwd = input_data.get("cwd", os.getcwd())
676
+
677
+ # Only handle subagent types we care about
678
+ if subagent_type not in AGENTS_ALL:
679
+ sys.exit(0)
680
+
681
+ # Find repo root
682
+ repo_root = find_repo_root(cwd)
683
+ if not repo_root:
684
+ sys.exit(0)
685
+
686
+ # Get current task directory (research doesn't require it)
687
+ task_dir = get_current_task(repo_root, input_data)
688
+
689
+ # implement/check need task directory
690
+ if subagent_type in AGENTS_REQUIRE_TASK:
691
+ if not task_dir:
692
+ sys.exit(0)
693
+ # Check if task directory exists
694
+ task_dir_full = os.path.join(repo_root, task_dir)
695
+ if not os.path.exists(task_dir_full):
696
+ sys.exit(0)
697
+
698
+ # Check for [finish] marker in prompt (check agent with finish context)
699
+ is_finish_phase = "[finish]" in original_prompt.lower()
700
+
701
+ # Get context and build prompt based on subagent type
702
+ if subagent_type == AGENT_IMPLEMENT:
703
+ assert task_dir is not None # validated above
704
+ context = get_implement_context(repo_root, task_dir)
705
+ new_prompt = build_implement_prompt(original_prompt, context)
706
+ elif subagent_type == AGENT_CHECK:
707
+ assert task_dir is not None # validated above
708
+ if is_finish_phase:
709
+ # Finish phase: use finish context (lighter, focused on final verification)
710
+ context = get_finish_context(repo_root, task_dir)
711
+ new_prompt = build_finish_prompt(original_prompt, context)
712
+ else:
713
+ # Regular check phase: use check context (full specs for self-fix loop)
714
+ context = get_check_context(repo_root, task_dir)
715
+ new_prompt = build_check_prompt(original_prompt, context)
716
+ elif subagent_type == AGENT_RESEARCH:
717
+ # Research can work without task directory
718
+ context = get_research_context(repo_root, task_dir)
719
+ new_prompt = build_research_prompt(original_prompt, context)
720
+ else:
721
+ sys.exit(0)
722
+
723
+ if not context:
724
+ sys.exit(0)
725
+
726
+ # Return updated input — use a multi-format output that covers all platforms.
727
+ # Most platforms ignore unrecognized fields, so we include multiple formats.
728
+ # The platform picks whichever fields it understands.
729
+ updated = {**tool_input, "prompt": new_prompt}
730
+ output = {
731
+ # Claude Code / Qoder / CodeBuddy / Droid format
732
+ "hookSpecificOutput": {
733
+ "hookEventName": "PreToolUse",
734
+ "permissionDecision": "allow",
735
+ "updatedInput": updated,
736
+ },
737
+ # Cursor format
738
+ "permission": "allow",
739
+ "updated_input": updated,
740
+ # Gemini format
741
+ "updatedInput": updated,
742
+ }
743
+
744
+ print(json.dumps(output, ensure_ascii=False))
745
+ sys.exit(0)
746
+
747
+
748
+ if __name__ == "__main__":
749
+ main()