@mindfoldhq/trellis 0.5.0-beta.8 → 0.5.0-rc.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 (210) hide show
  1. package/README.md +60 -95
  2. package/dist/cli/index.js +7 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/init.d.ts +13 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +474 -210
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +295 -54
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/configurators/antigravity.d.ts.map +1 -1
  12. package/dist/configurators/antigravity.js +2 -8
  13. package/dist/configurators/antigravity.js.map +1 -1
  14. package/dist/configurators/claude.d.ts.map +1 -1
  15. package/dist/configurators/claude.js +4 -10
  16. package/dist/configurators/claude.js.map +1 -1
  17. package/dist/configurators/codebuddy.d.ts.map +1 -1
  18. package/dist/configurators/codebuddy.js +3 -3
  19. package/dist/configurators/codebuddy.js.map +1 -1
  20. package/dist/configurators/codex.d.ts.map +1 -1
  21. package/dist/configurators/codex.js +5 -13
  22. package/dist/configurators/codex.js.map +1 -1
  23. package/dist/configurators/copilot.d.ts.map +1 -1
  24. package/dist/configurators/copilot.js +5 -19
  25. package/dist/configurators/copilot.js.map +1 -1
  26. package/dist/configurators/cursor.d.ts.map +1 -1
  27. package/dist/configurators/cursor.js +3 -3
  28. package/dist/configurators/cursor.js.map +1 -1
  29. package/dist/configurators/droid.d.ts.map +1 -1
  30. package/dist/configurators/droid.js +3 -3
  31. package/dist/configurators/droid.js.map +1 -1
  32. package/dist/configurators/gemini.d.ts.map +1 -1
  33. package/dist/configurators/gemini.js +3 -5
  34. package/dist/configurators/gemini.js.map +1 -1
  35. package/dist/configurators/index.d.ts.map +1 -1
  36. package/dist/configurators/index.js +44 -55
  37. package/dist/configurators/index.js.map +1 -1
  38. package/dist/configurators/kilo.d.ts.map +1 -1
  39. package/dist/configurators/kilo.js +2 -8
  40. package/dist/configurators/kilo.js.map +1 -1
  41. package/dist/configurators/kiro.d.ts.map +1 -1
  42. package/dist/configurators/kiro.js +3 -3
  43. package/dist/configurators/kiro.js.map +1 -1
  44. package/dist/configurators/opencode.d.ts.map +1 -1
  45. package/dist/configurators/opencode.js +7 -4
  46. package/dist/configurators/opencode.js.map +1 -1
  47. package/dist/configurators/pi.d.ts +3 -0
  48. package/dist/configurators/pi.d.ts.map +1 -0
  49. package/dist/configurators/pi.js +44 -0
  50. package/dist/configurators/pi.js.map +1 -0
  51. package/dist/configurators/qoder.d.ts +7 -6
  52. package/dist/configurators/qoder.d.ts.map +1 -1
  53. package/dist/configurators/qoder.js +18 -12
  54. package/dist/configurators/qoder.js.map +1 -1
  55. package/dist/configurators/shared.d.ts +30 -6
  56. package/dist/configurators/shared.d.ts.map +1 -1
  57. package/dist/configurators/shared.js +65 -15
  58. package/dist/configurators/shared.js.map +1 -1
  59. package/dist/configurators/windsurf.d.ts.map +1 -1
  60. package/dist/configurators/windsurf.js +2 -8
  61. package/dist/configurators/windsurf.js.map +1 -1
  62. package/dist/constants/paths.d.ts +2 -0
  63. package/dist/constants/paths.d.ts.map +1 -1
  64. package/dist/constants/paths.js +2 -0
  65. package/dist/constants/paths.js.map +1 -1
  66. package/dist/migrations/manifests/0.5.0-beta.0.json +2 -0
  67. package/dist/migrations/manifests/0.5.0-beta.10.json +9 -0
  68. package/dist/migrations/manifests/0.5.0-beta.11.json +9 -0
  69. package/dist/migrations/manifests/0.5.0-beta.12.json +9 -0
  70. package/dist/migrations/manifests/0.5.0-beta.13.json +9 -0
  71. package/dist/migrations/manifests/0.5.0-beta.14.json +9 -0
  72. package/dist/migrations/manifests/0.5.0-beta.15.json +116 -0
  73. package/dist/migrations/manifests/0.5.0-beta.16.json +9 -0
  74. package/dist/migrations/manifests/0.5.0-beta.17.json +9 -0
  75. package/dist/migrations/manifests/0.5.0-beta.18.json +9 -0
  76. package/dist/migrations/manifests/0.5.0-beta.19.json +9 -0
  77. package/dist/migrations/manifests/0.5.0-beta.5.json +2 -0
  78. package/dist/migrations/manifests/0.5.0-beta.9.json +48 -0
  79. package/dist/migrations/manifests/0.5.0-rc.0.json +9 -0
  80. package/dist/templates/claude/agents/trellis-research.md +1 -1
  81. package/dist/templates/claude/settings.json +0 -4
  82. package/dist/templates/codebuddy/agents/trellis-research.md +1 -1
  83. package/dist/templates/codex/agents/trellis-research.toml +3 -2
  84. package/dist/templates/codex/hooks/session-start.py +126 -26
  85. package/dist/templates/codex/skills/finish-work/SKILL.md +41 -109
  86. package/dist/templates/codex/skills/start/SKILL.md +12 -9
  87. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +73 -0
  88. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +83 -0
  89. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +54 -0
  90. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +81 -0
  91. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-hooks.md +57 -0
  92. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +78 -0
  93. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +83 -0
  94. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +90 -0
  95. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +64 -0
  96. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +55 -0
  97. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +68 -0
  98. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +80 -0
  99. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/overview.md +51 -0
  100. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +102 -0
  101. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +101 -0
  102. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +75 -0
  103. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workspace-memory.md +71 -0
  104. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +79 -0
  105. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +69 -0
  106. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +59 -0
  107. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +74 -0
  108. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +83 -0
  109. package/dist/templates/common/commands/continue.md +9 -5
  110. package/dist/templates/common/commands/finish-work.md +34 -10
  111. package/dist/templates/common/index.d.ts +22 -2
  112. package/dist/templates/common/index.d.ts.map +1 -1
  113. package/dist/templates/common/index.js +53 -4
  114. package/dist/templates/common/index.js.map +1 -1
  115. package/dist/templates/common/skills/brainstorm.md +50 -4
  116. package/dist/templates/copilot/hooks/session-start.py +127 -30
  117. package/dist/templates/copilot/prompts/finish-work.prompt.md +44 -112
  118. package/dist/templates/copilot/prompts/start.prompt.md +12 -9
  119. package/dist/templates/cursor/agents/trellis-check.md +1 -1
  120. package/dist/templates/cursor/agents/trellis-implement.md +1 -1
  121. package/dist/templates/cursor/agents/trellis-research.md +2 -2
  122. package/dist/templates/cursor/hooks.json +7 -1
  123. package/dist/templates/droid/droids/trellis-research.md +1 -1
  124. package/dist/templates/extract.d.ts +6 -0
  125. package/dist/templates/extract.d.ts.map +1 -1
  126. package/dist/templates/extract.js +14 -0
  127. package/dist/templates/extract.js.map +1 -1
  128. package/dist/templates/gemini/agents/trellis-research.md +1 -1
  129. package/dist/templates/kiro/agents/trellis-research.json +1 -1
  130. package/dist/templates/markdown/agents.md +19 -12
  131. package/dist/templates/markdown/gitignore.txt +3 -0
  132. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +24 -0
  133. package/dist/templates/opencode/agents/trellis-check.md +1 -1
  134. package/dist/templates/opencode/agents/trellis-implement.md +7 -4
  135. package/dist/templates/opencode/agents/trellis-research.md +2 -2
  136. package/dist/templates/opencode/lib/trellis-context.js +100 -13
  137. package/dist/templates/opencode/plugins/inject-subagent-context.js +70 -5
  138. package/dist/templates/opencode/plugins/inject-workflow-state.js +38 -44
  139. package/dist/templates/opencode/plugins/session-start.js +76 -31
  140. package/dist/templates/pi/agents/trellis-check.md +28 -0
  141. package/dist/templates/pi/agents/trellis-implement.md +33 -0
  142. package/dist/templates/pi/agents/trellis-research.md +25 -0
  143. package/dist/templates/pi/extensions/trellis/index.ts.txt +997 -0
  144. package/dist/templates/pi/index.d.ts +5 -0
  145. package/dist/templates/pi/index.d.ts.map +1 -0
  146. package/dist/templates/pi/index.js +12 -0
  147. package/dist/templates/pi/index.js.map +1 -0
  148. package/dist/templates/pi/settings.json +12 -0
  149. package/dist/templates/qoder/agents/trellis-research.md +1 -1
  150. package/dist/templates/shared-hooks/index.d.ts +31 -0
  151. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  152. package/dist/templates/shared-hooks/index.js +59 -0
  153. package/dist/templates/shared-hooks/index.js.map +1 -1
  154. package/dist/templates/shared-hooks/inject-shell-session-context.py +180 -0
  155. package/dist/templates/shared-hooks/inject-subagent-context.py +156 -27
  156. package/dist/templates/shared-hooks/inject-workflow-state.py +85 -92
  157. package/dist/templates/shared-hooks/session-start.py +232 -36
  158. package/dist/templates/trellis/config.yaml +6 -0
  159. package/dist/templates/trellis/gitignore.txt +3 -0
  160. package/dist/templates/trellis/index.d.ts +1 -1
  161. package/dist/templates/trellis/index.d.ts.map +1 -1
  162. package/dist/templates/trellis/index.js +2 -2
  163. package/dist/templates/trellis/index.js.map +1 -1
  164. package/dist/templates/trellis/scripts/common/__init__.py +8 -0
  165. package/dist/templates/trellis/scripts/common/active_task.py +593 -0
  166. package/dist/templates/trellis/scripts/common/cli_adapter.py +72 -14
  167. package/dist/templates/trellis/scripts/common/paths.py +61 -58
  168. package/dist/templates/trellis/scripts/common/session_context.py +12 -0
  169. package/dist/templates/trellis/scripts/common/task_context.py +27 -194
  170. package/dist/templates/trellis/scripts/common/task_store.py +102 -26
  171. package/dist/templates/trellis/scripts/common/tasks.py +4 -1
  172. package/dist/templates/trellis/scripts/common/types.py +0 -2
  173. package/dist/templates/trellis/scripts/common/workflow_phase.py +15 -3
  174. package/dist/templates/trellis/scripts/task.py +99 -34
  175. package/dist/templates/trellis/workflow.md +332 -64
  176. package/dist/types/ai-tools.d.ts +12 -3
  177. package/dist/types/ai-tools.d.ts.map +1 -1
  178. package/dist/types/ai-tools.js +29 -0
  179. package/dist/types/ai-tools.js.map +1 -1
  180. package/dist/utils/file-writer.d.ts.map +1 -1
  181. package/dist/utils/file-writer.js +7 -2
  182. package/dist/utils/file-writer.js.map +1 -1
  183. package/dist/utils/posix.d.ts +13 -0
  184. package/dist/utils/posix.d.ts.map +1 -0
  185. package/dist/utils/posix.js +15 -0
  186. package/dist/utils/posix.js.map +1 -0
  187. package/dist/utils/project-detector.d.ts +2 -0
  188. package/dist/utils/project-detector.d.ts.map +1 -1
  189. package/dist/utils/project-detector.js +120 -11
  190. package/dist/utils/project-detector.js.map +1 -1
  191. package/dist/utils/task-json.d.ts +46 -0
  192. package/dist/utils/task-json.d.ts.map +1 -0
  193. package/dist/utils/task-json.js +49 -0
  194. package/dist/utils/task-json.js.map +1 -0
  195. package/dist/utils/template-fetcher.d.ts +22 -6
  196. package/dist/utils/template-fetcher.d.ts.map +1 -1
  197. package/dist/utils/template-fetcher.js +405 -27
  198. package/dist/utils/template-fetcher.js.map +1 -1
  199. package/dist/utils/template-hash.d.ts +22 -3
  200. package/dist/utils/template-hash.d.ts.map +1 -1
  201. package/dist/utils/template-hash.js +99 -19
  202. package/dist/utils/template-hash.js.map +1 -1
  203. package/package.json +7 -7
  204. package/dist/templates/markdown/spec/backend/directory-structure.md +0 -292
  205. package/dist/templates/markdown/spec/backend/index.md +0 -40
  206. package/dist/templates/markdown/spec/backend/script-conventions.md +0 -742
  207. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md +0 -118
  208. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md +0 -394
  209. package/dist/templates/shared-hooks/statusline.py +0 -218
  210. package/dist/templates/trellis/scripts/create_bootstrap.py +0 -298
@@ -0,0 +1,5 @@
1
+ import { type AgentTemplate, type HookTemplate } from "../template-utils.js";
2
+ export declare function getAllAgents(): AgentTemplate[];
3
+ export declare function getSettingsTemplate(): HookTemplate;
4
+ export declare function getExtensionTemplate(): string;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/pi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,sBAAsB,CAAC;AAM9B,wBAAgB,YAAY,IAAI,aAAa,EAAE,CAE9C;AAED,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
@@ -0,0 +1,12 @@
1
+ import { createTemplateReader, } from "../template-utils.js";
2
+ const { listMdAgents, getSettings, readTemplate } = createTemplateReader(import.meta.url);
3
+ export function getAllAgents() {
4
+ return listMdAgents();
5
+ }
6
+ export function getSettingsTemplate() {
7
+ return getSettings();
8
+ }
9
+ export function getExtensionTemplate() {
10
+ return readTemplate("extensions/trellis/index.ts.txt");
11
+ }
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/pi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,GAGrB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,oBAAoB,CACtE,MAAM,CAAC,IAAI,CAAC,GAAG,CAChB,CAAC;AAEF,MAAM,UAAU,YAAY;IAC1B,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,WAAW,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,YAAY,CAAC,iCAAiC,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,12 @@
1
+ {
2
+ "enableSkillCommands": true,
3
+ "extensions": [
4
+ "./extensions/trellis/index.ts"
5
+ ],
6
+ "skills": [
7
+ "./skills"
8
+ ],
9
+ "prompts": [
10
+ "./prompts"
11
+ ]
12
+ }
@@ -29,7 +29,7 @@ Conversations get compacted; files don't. Every research output MUST end up as a
29
29
 
30
30
  ### Step 1: Resolve Current Task
31
31
 
32
- Read `.trellis/.current-task` → task directory (e.g. `.trellis/tasks/04-17-foo/`). If empty or missing, ask the user where to write output; do NOT guess.
32
+ Run `python3 ./.trellis/scripts/task.py current --source` → active task path. If no active task is set, ask the user where to write output; do NOT guess.
33
33
 
34
34
  Ensure `{TASK_DIR}/research/` exists:
35
35
 
@@ -11,9 +11,40 @@ export interface HookScript {
11
11
  /** Script content — no placeholders, ready to write directly */
12
12
  content: string;
13
13
  }
14
+ export type SharedHookName = "session-start.py" | "inject-shell-session-context.py" | "inject-workflow-state.py" | "inject-subagent-context.py";
15
+ export type SharedHookPlatform = "claude" | "cursor" | "codex" | "gemini" | "qoder" | "copilot" | "codebuddy" | "droid" | "kiro";
16
+ /**
17
+ * Which shared hooks each platform actually invokes. Single source of truth
18
+ * for shared-hook distribution — both `writeSharedHooks` (runtime install)
19
+ * and `collectSharedHooks` (`trellis update` diff) read from this table.
20
+ *
21
+ * Routing rules encoded here:
22
+ * - `session-start.py` — shipped by every platform with a SessionStart
23
+ * hook event *except* codex + copilot, which bundle a platform-specific
24
+ * session-start.py under their own template dirs.
25
+ * - `inject-workflow-state.py` — every platform with a UserPromptSubmit
26
+ * (or equivalent) event. Kiro + codex self-included; platforms without
27
+ * per-turn main-session hooks are excluded.
28
+ * - `inject-subagent-context.py` — class-1 (push-based) platforms only.
29
+ * Class-2 (pull-based) platforms (codex, copilot, gemini, qoder) can't
30
+ * have hooks mutate sub-agent prompts — their sub-agents load context
31
+ * via a prelude instead.
32
+ * - Kiro supports only `agentSpawn` (no SessionStart / UserPromptSubmit
33
+ * event), so it takes just `inject-subagent-context.py`.
34
+ * - Claude Code `statusLine` is intentionally not installed by default.
35
+ * Users can add their own statusLine command in `.claude/settings.json`
36
+ * without Trellis owning a generated hook file.
37
+ */
38
+ export declare const SHARED_HOOKS_BY_PLATFORM: Record<SharedHookPlatform, readonly SharedHookName[]>;
14
39
  /**
15
40
  * Get all shared hook scripts. Content is platform-independent and can be
16
41
  * written directly without placeholder resolution.
17
42
  */
18
43
  export declare function getSharedHookScripts(): HookScript[];
44
+ /**
45
+ * Get the shared hook scripts that a given platform actually registers.
46
+ * Drives both `writeSharedHooks` and `collectSharedHooks` so distribution
47
+ * never drifts from the per-platform capability declared above.
48
+ */
49
+ export declare function getSharedHookScriptsForPlatform(platform: SharedHookPlatform): HookScript[];
19
50
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,iCAAiC,GACjC,0BAA0B,GAC1B,4BAA4B,CAAC;AAEjC,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,OAAO,GACP,SAAS,GACT,WAAW,GACX,OAAO,GACP,MAAM,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAC3C,kBAAkB,EAClB,SAAS,cAAc,EAAE,CA4B1B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,kBAAkB,GAC3B,UAAU,EAAE,CAGd"}
@@ -13,6 +13,56 @@ const __dirname = dirname(__filename);
13
13
  function readTemplate(relativePath) {
14
14
  return readFileSync(join(__dirname, relativePath), "utf-8");
15
15
  }
16
+ /**
17
+ * Which shared hooks each platform actually invokes. Single source of truth
18
+ * for shared-hook distribution — both `writeSharedHooks` (runtime install)
19
+ * and `collectSharedHooks` (`trellis update` diff) read from this table.
20
+ *
21
+ * Routing rules encoded here:
22
+ * - `session-start.py` — shipped by every platform with a SessionStart
23
+ * hook event *except* codex + copilot, which bundle a platform-specific
24
+ * session-start.py under their own template dirs.
25
+ * - `inject-workflow-state.py` — every platform with a UserPromptSubmit
26
+ * (or equivalent) event. Kiro + codex self-included; platforms without
27
+ * per-turn main-session hooks are excluded.
28
+ * - `inject-subagent-context.py` — class-1 (push-based) platforms only.
29
+ * Class-2 (pull-based) platforms (codex, copilot, gemini, qoder) can't
30
+ * have hooks mutate sub-agent prompts — their sub-agents load context
31
+ * via a prelude instead.
32
+ * - Kiro supports only `agentSpawn` (no SessionStart / UserPromptSubmit
33
+ * event), so it takes just `inject-subagent-context.py`.
34
+ * - Claude Code `statusLine` is intentionally not installed by default.
35
+ * Users can add their own statusLine command in `.claude/settings.json`
36
+ * without Trellis owning a generated hook file.
37
+ */
38
+ export const SHARED_HOOKS_BY_PLATFORM = {
39
+ claude: [
40
+ "session-start.py",
41
+ "inject-workflow-state.py",
42
+ "inject-subagent-context.py",
43
+ ],
44
+ cursor: [
45
+ "session-start.py",
46
+ "inject-shell-session-context.py",
47
+ "inject-workflow-state.py",
48
+ "inject-subagent-context.py",
49
+ ],
50
+ codex: ["inject-workflow-state.py"],
51
+ gemini: ["session-start.py", "inject-workflow-state.py"],
52
+ qoder: ["session-start.py", "inject-workflow-state.py"],
53
+ copilot: ["inject-workflow-state.py"],
54
+ codebuddy: [
55
+ "session-start.py",
56
+ "inject-workflow-state.py",
57
+ "inject-subagent-context.py",
58
+ ],
59
+ droid: [
60
+ "session-start.py",
61
+ "inject-workflow-state.py",
62
+ "inject-subagent-context.py",
63
+ ],
64
+ kiro: ["inject-subagent-context.py"],
65
+ };
16
66
  /**
17
67
  * Get all shared hook scripts. Content is platform-independent and can be
18
68
  * written directly without placeholder resolution.
@@ -27,4 +77,13 @@ export function getSharedHookScripts() {
27
77
  }
28
78
  return scripts;
29
79
  }
80
+ /**
81
+ * Get the shared hook scripts that a given platform actually registers.
82
+ * Drives both `writeSharedHooks` and `collectSharedHooks` so distribution
83
+ * never drifts from the per-platform capability declared above.
84
+ */
85
+ export function getSharedHookScriptsForPlatform(platform) {
86
+ const allowed = new Set(SHARED_HOOKS_BY_PLATFORM[platform]);
87
+ return getSharedHookScripts().filter((h) => allowed.has(h.name));
88
+ }
30
89
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AASD;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;SACjC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SAChC,IAAI,EAAE,CAAC;IAEV,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AA0BD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAGjC;IACF,MAAM,EAAE;QACN,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,MAAM,EAAE;QACN,kBAAkB;QAClB,iCAAiC;QACjC,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,KAAK,EAAE,CAAC,0BAA0B,CAAC;IACnC,MAAM,EAAE,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;IACxD,KAAK,EAAE,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;IACvD,OAAO,EAAE,CAAC,0BAA0B,CAAC;IACrC,SAAS,EAAE;QACT,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,KAAK,EAAE;QACL,kBAAkB;QAClB,0BAA0B;QAC1B,4BAA4B;KAC7B;IACD,IAAI,EAAE,CAAC,4BAA4B,CAAC;CACrC,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;SACjC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SAChC,IAAI,EAAE,CAAC;IAEV,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAA4B;IAE5B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,wBAAwB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,OAAO,oBAAoB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACnE,CAAC"}
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ """Cursor beforeShellExecution hook: bridge conversation identity to task.py.
3
+
4
+ Cursor's shell command environment does not inherit SessionStart data. This
5
+ hook writes a short-lived runtime ticket before Cursor runs a shell command
6
+ that calls `task.py start/current/finish`. The task script then consumes the
7
+ ticket only when it has no native session environment.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import os
14
+ import shlex
15
+ import sys
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+
21
+ DIR_WORKFLOW = ".trellis"
22
+ DIR_RUNTIME = ".runtime"
23
+ DIR_CURSOR_SHELL = "cursor-shell"
24
+ SESSION_SUBCOMMANDS = {"start", "current", "finish"}
25
+ TICKET_TTL_SECONDS = 30
26
+ CONTEXT_IDENTITY_KEYS = (
27
+ "session_id",
28
+ "sessionId",
29
+ "sessionID",
30
+ "conversation_id",
31
+ "conversationId",
32
+ "conversationID",
33
+ "transcript_path",
34
+ "transcriptPath",
35
+ "transcript",
36
+ )
37
+
38
+
39
+ def _string_value(value: Any) -> str | None:
40
+ if isinstance(value, str):
41
+ stripped = value.strip()
42
+ return stripped or None
43
+ return None
44
+
45
+
46
+ def _find_trellis_root(start: Path) -> Path | None:
47
+ current = start.resolve()
48
+ while True:
49
+ if (current / DIR_WORKFLOW).is_dir():
50
+ return current
51
+ if current == current.parent:
52
+ return None
53
+ current = current.parent
54
+
55
+
56
+ def _runtime_ticket_dir(root: Path) -> Path:
57
+ return root / DIR_WORKFLOW / DIR_RUNTIME / DIR_CURSOR_SHELL
58
+
59
+
60
+ def _load_active_task_resolver(root: Path):
61
+ scripts_dir = root / DIR_WORKFLOW / "scripts"
62
+ if str(scripts_dir) not in sys.path:
63
+ sys.path.insert(0, str(scripts_dir))
64
+ from common.active_task import resolve_context_key # type: ignore[import-not-found]
65
+
66
+ return resolve_context_key
67
+
68
+
69
+ def _extract_task_subcommands(command: str) -> list[dict[str, str]]:
70
+ try:
71
+ tokens = shlex.split(command, posix=os.name != "nt")
72
+ except ValueError:
73
+ return []
74
+
75
+ subcommands: list[dict[str, str]] = []
76
+ for index, token in enumerate(tokens[:-1]):
77
+ if Path(token.strip("\"'")).name != "task.py":
78
+ continue
79
+ name = tokens[index + 1]
80
+ if name not in SESSION_SUBCOMMANDS:
81
+ continue
82
+ item = {"name": name}
83
+ if name == "start" and index + 2 < len(tokens):
84
+ item["task_ref"] = tokens[index + 2]
85
+ subcommands.append(item)
86
+ return subcommands
87
+
88
+
89
+ def _cleanup_expired_tickets(ticket_dir: Path, now: float) -> None:
90
+ if not ticket_dir.is_dir():
91
+ return
92
+ for ticket_path in ticket_dir.glob("*.json"):
93
+ try:
94
+ data = json.loads(ticket_path.read_text(encoding="utf-8"))
95
+ except (json.JSONDecodeError, OSError):
96
+ continue
97
+ expires_at = data.get("expires_at_epoch")
98
+ if isinstance(expires_at, (int, float)) and expires_at < now:
99
+ try:
100
+ ticket_path.unlink()
101
+ except OSError:
102
+ pass
103
+
104
+
105
+ def _has_context_identity(hook_input: dict[str, Any]) -> bool:
106
+ return any(_string_value(hook_input.get(key)) for key in CONTEXT_IDENTITY_KEYS)
107
+
108
+
109
+ def _write_ticket(
110
+ root: Path,
111
+ hook_input: dict[str, Any],
112
+ context_key: str,
113
+ subcommands: list[dict[str, str]],
114
+ ) -> None:
115
+ now = time.time()
116
+ ticket_dir = _runtime_ticket_dir(root)
117
+ ticket_dir.mkdir(parents=True, exist_ok=True)
118
+ _cleanup_expired_tickets(ticket_dir, now)
119
+
120
+ command = _string_value(hook_input.get("command")) or ""
121
+ digest = hashlib.sha256(
122
+ f"{context_key}\0{command}\0{now}".encode("utf-8"),
123
+ ).hexdigest()[:16]
124
+ ticket_path = ticket_dir / f"{int(now * 1000)}-{digest}.json"
125
+
126
+ payload = {
127
+ "platform": "cursor",
128
+ "context_key": context_key,
129
+ "conversation_id": _string_value(hook_input.get("conversation_id")),
130
+ "session_id": _string_value(hook_input.get("session_id")),
131
+ "generation_id": _string_value(hook_input.get("generation_id")),
132
+ "cwd": _string_value(hook_input.get("cwd")),
133
+ "command": command,
134
+ "subcommands": subcommands,
135
+ "created_at_epoch": now,
136
+ "expires_at_epoch": now + TICKET_TTL_SECONDS,
137
+ }
138
+ ticket_path.write_text(
139
+ json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
140
+ encoding="utf-8",
141
+ )
142
+
143
+
144
+ def main() -> int:
145
+ try:
146
+ hook_input = json.loads(sys.stdin.read())
147
+ except (json.JSONDecodeError, ValueError):
148
+ hook_input = {}
149
+ if not isinstance(hook_input, dict):
150
+ hook_input = {}
151
+
152
+ command = _string_value(hook_input.get("command")) or ""
153
+ subcommands = _extract_task_subcommands(command)
154
+ if not subcommands:
155
+ return 0
156
+
157
+ cwd = Path(_string_value(hook_input.get("cwd")) or os.getcwd())
158
+ root = _find_trellis_root(cwd)
159
+ if root is None:
160
+ return 0
161
+
162
+ if not _has_context_identity(hook_input):
163
+ return 0
164
+
165
+ resolve_context_key = _load_active_task_resolver(root)
166
+ context_key = resolve_context_key(hook_input, platform="cursor")
167
+ if not context_key:
168
+ return 0
169
+
170
+ try:
171
+ _write_ticket(root, hook_input, context_key, subcommands)
172
+ except OSError:
173
+ return 0
174
+
175
+ print(json.dumps({"permission": "allow"}, ensure_ascii=False))
176
+ return 0
177
+
178
+
179
+ if __name__ == "__main__":
180
+ sys.exit(main())
@@ -12,13 +12,14 @@ Core Design Philosophy:
12
12
 
13
13
  Trigger: PreToolUse (before Task tool call)
14
14
 
15
- Context Source: .trellis/.current-task points to task directory
15
+ Context Source: Trellis active task resolver points to task directory
16
16
  - implement.jsonl - Implement agent dedicated context
17
17
  - check.jsonl - Check agent dedicated context
18
18
  - prd.md - Requirements document
19
19
  - info.md - Technical design
20
20
  - codex-review-output.txt - Code Review results
21
21
  """
22
+ from __future__ import annotations
22
23
 
23
24
  # IMPORTANT: Suppress all warnings FIRST
24
25
  import warnings
@@ -28,10 +29,11 @@ import json
28
29
  import os
29
30
  import sys
30
31
  from pathlib import Path
32
+ from typing import Any
31
33
 
32
34
  # IMPORTANT: Force stdout to use UTF-8 on Windows
33
35
  # This fixes UnicodeEncodeError when outputting non-ASCII characters
34
- if sys.platform == "win32":
36
+ if sys.platform.startswith("win"):
35
37
  import io as _io
36
38
  if hasattr(sys.stdout, "reconfigure"):
37
39
  sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
@@ -45,7 +47,6 @@ if sys.platform == "win32":
45
47
 
46
48
  DIR_WORKFLOW = ".trellis"
47
49
  DIR_SPEC = "spec"
48
- FILE_CURRENT_TASK = ".current-task"
49
50
  FILE_TASK_JSON = "task.json"
50
51
 
51
52
  # =============================================================================
@@ -77,32 +78,57 @@ def find_repo_root(start_path: str) -> str | None:
77
78
  return None
78
79
 
79
80
 
80
- def get_current_task(repo_root: str) -> str | None:
81
- """
82
- Read current task directory path from .trellis/.current-task
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
83
113
 
84
- Returns:
85
- Task directory relative path (relative to repo_root)
86
- None if not set
87
- """
88
- current_task_file = os.path.join(repo_root, DIR_WORKFLOW, FILE_CURRENT_TASK)
89
- if not os.path.exists(current_task_file):
90
- return None
91
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))
92
120
  try:
93
- with open(current_task_file, "r", encoding="utf-8") as f:
94
- content = f.read().strip()
95
- if not content:
96
- return None
97
- normalized = content.replace("\\", "/")
98
- while normalized.startswith("./"):
99
- normalized = normalized[2:]
100
- if normalized.startswith("tasks/"):
101
- normalized = f".trellis/{normalized}"
102
- return normalized
121
+ from common.active_task import resolve_active_task # type: ignore[import-not-found]
103
122
  except Exception:
104
123
  return None
105
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
+
106
132
 
107
133
  def read_file_content(base_path: str, file_path: str) -> str | None:
108
134
  """Read file content, return None if file doesn't exist"""
@@ -167,15 +193,27 @@ def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]
167
193
  Schema:
168
194
  {"file": "path/to/file.md", "reason": "..."}
169
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.
170
202
 
171
203
  Returns:
172
204
  [(path, content), ...]
173
205
  """
174
206
  full_path = os.path.join(base_path, jsonl_path)
175
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
+ )
176
213
  return []
177
214
 
178
215
  results = []
216
+ saw_real_entry = False
179
217
  try:
180
218
  with open(full_path, "r", encoding="utf-8") as f:
181
219
  for line in f:
@@ -188,8 +226,10 @@ def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]
188
226
  entry_type = item.get("type", "file")
189
227
 
190
228
  if not file_path:
229
+ # Seed / comment row — skip silently
191
230
  continue
192
231
 
232
+ saw_real_entry = True
193
233
  if entry_type == "directory":
194
234
  # Read all .md files in directory
195
235
  dir_contents = read_directory_contents(base_path, file_path)
@@ -204,6 +244,14 @@ def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]
204
244
  except Exception:
205
245
  pass
206
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
+
207
255
  return results
208
256
 
209
257
 
@@ -391,7 +439,11 @@ Finish checklist and requirements:
391
439
  def get_research_context(repo_root: str, task_dir: str | None) -> str:
392
440
  """
393
441
  Context for Research Agent — project structure overview for spec directories.
442
+
443
+ `task_dir` kept for signature parity with get_implement_context / get_check_context
444
+ so the dispatcher can call them uniformly.
394
445
  """
446
+ _ = task_dir
395
447
  context_parts = []
396
448
 
397
449
  # 1. Project structure overview (dynamically discover spec directories)
@@ -490,13 +542,90 @@ Provide structured search results including:
490
542
  - External references (if any)"""
491
543
 
492
544
 
545
+ def _string_value(value: Any) -> str:
546
+ if isinstance(value, str):
547
+ stripped = value.strip()
548
+ return stripped
549
+ return ""
550
+
551
+
552
+ def _extract_subagent_name(value: Any) -> str:
553
+ """Extract a sub-agent name from common platform encodings.
554
+
555
+ Cursor's native Task args encode custom sub-agents as a protobuf oneof,
556
+ which can appear in hook JSON as either ``{"custom": {"name": "..."}}``
557
+ or ``{"type": {"case": "custom", "value": {"name": "..."}}}``.
558
+ """
559
+ direct = _string_value(value)
560
+ if direct:
561
+ return direct
562
+
563
+ if not isinstance(value, dict):
564
+ return ""
565
+
566
+ for key in ("name", "subagent_type_name", "subagentTypeName"):
567
+ direct = _string_value(value.get(key))
568
+ if direct:
569
+ return direct
570
+
571
+ custom = value.get("custom")
572
+ if isinstance(custom, dict):
573
+ custom_name = _string_value(custom.get("name"))
574
+ if custom_name:
575
+ return custom_name
576
+
577
+ oneof = value.get("type")
578
+ if isinstance(oneof, dict):
579
+ case_name = _string_value(oneof.get("case"))
580
+ if case_name == "custom":
581
+ nested_value = oneof.get("value")
582
+ if isinstance(nested_value, dict):
583
+ custom_name = _string_value(nested_value.get("name"))
584
+ if custom_name:
585
+ return custom_name
586
+ if case_name:
587
+ return case_name
588
+
589
+ case_name = _string_value(value.get("case"))
590
+ if case_name == "custom":
591
+ nested_value = value.get("value")
592
+ if isinstance(nested_value, dict):
593
+ custom_name = _string_value(nested_value.get("name"))
594
+ if custom_name:
595
+ return custom_name
596
+ if case_name:
597
+ return case_name
598
+
599
+ for agent_name in AGENTS_ALL:
600
+ if agent_name in value:
601
+ return agent_name
602
+
603
+ return ""
604
+
605
+
606
+ def _extract_subagent_type(tool_input: dict) -> str:
607
+ for key in (
608
+ "subagent_type",
609
+ "subagentType",
610
+ "subagent_type_name",
611
+ "subagentTypeName",
612
+ "agent_type",
613
+ "agentType",
614
+ "name",
615
+ ):
616
+ agent_name = _extract_subagent_name(tool_input.get(key))
617
+ if agent_name:
618
+ return agent_name
619
+ return ""
620
+
621
+
493
622
  def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]:
494
623
  """Parse hook input across different platform formats.
495
624
 
496
625
  Returns (subagent_type, original_prompt, tool_input).
497
626
  Handles:
498
627
  - Claude Code / Qoder / CodeBuddy / Droid: tool_name=Task|Agent, tool_input.subagent_type
499
- - Cursor: tool_name=Task, tool_input.subagent_type
628
+ - Cursor: tool_name=Task|Subagent, tool_input.subagent_type
500
629
  - Copilot CLI: toolName=task (camelCase key, lowercase value)
501
630
  - Gemini CLI: tool_name IS the agent name (BeforeTool matcher already filtered)
502
631
  - Kiro: agentSpawn hook, agent_name field at top level
@@ -505,9 +634,9 @@ def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]:
505
634
 
506
635
  # Standard format: Task/Agent tool with subagent_type
507
636
  tool_name = input_data.get("tool_name", "") or input_data.get("toolName", "")
508
- if tool_name.lower() in ("task", "agent"):
637
+ if tool_name.lower() in ("task", "agent", "subagent"):
509
638
  return (
510
- tool_input.get("subagent_type", ""),
639
+ _extract_subagent_type(tool_input),
511
640
  tool_input.get("prompt", ""),
512
641
  tool_input,
513
642
  )
@@ -549,7 +678,7 @@ def main():
549
678
  sys.exit(0)
550
679
 
551
680
  # Get current task directory (research doesn't require it)
552
- task_dir = get_current_task(repo_root)
681
+ task_dir = get_current_task(repo_root, input_data)
553
682
 
554
683
  # implement/check need task directory
555
684
  if subagent_type in AGENTS_REQUIRE_TASK: