@mindfoldhq/trellis 0.5.0-beta.14 → 0.5.0-beta.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +24 -20
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +44 -13
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/claude.js +1 -1
- package/dist/configurators/claude.js.map +1 -1
- package/dist/configurators/codebuddy.js +1 -1
- package/dist/configurators/codebuddy.js.map +1 -1
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +3 -6
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/copilot.d.ts.map +1 -1
- package/dist/configurators/copilot.js +4 -11
- package/dist/configurators/copilot.js.map +1 -1
- package/dist/configurators/cursor.js +1 -1
- package/dist/configurators/cursor.js.map +1 -1
- package/dist/configurators/droid.js +1 -1
- package/dist/configurators/droid.js.map +1 -1
- package/dist/configurators/gemini.d.ts.map +1 -1
- package/dist/configurators/gemini.js +1 -3
- package/dist/configurators/gemini.js.map +1 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +24 -38
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/kiro.js +1 -1
- package/dist/configurators/kiro.js.map +1 -1
- package/dist/configurators/opencode.d.ts.map +1 -1
- package/dist/configurators/opencode.js +4 -1
- package/dist/configurators/opencode.js.map +1 -1
- package/dist/configurators/pi.d.ts +3 -0
- package/dist/configurators/pi.d.ts.map +1 -0
- package/dist/configurators/pi.js +39 -0
- package/dist/configurators/pi.js.map +1 -0
- package/dist/configurators/qoder.d.ts.map +1 -1
- package/dist/configurators/qoder.js +1 -3
- package/dist/configurators/qoder.js.map +1 -1
- package/dist/configurators/shared.d.ts +2 -4
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +6 -9
- package/dist/configurators/shared.js.map +1 -1
- package/dist/migrations/manifests/0.5.0-beta.15.json +116 -0
- package/dist/migrations/manifests/0.5.0-beta.16.json +9 -0
- package/dist/templates/claude/agents/trellis-research.md +1 -1
- package/dist/templates/claude/settings.json +0 -4
- package/dist/templates/codebuddy/agents/trellis-research.md +1 -1
- package/dist/templates/codex/agents/trellis-check.toml +0 -16
- package/dist/templates/codex/agents/trellis-implement.toml +0 -16
- package/dist/templates/codex/agents/trellis-research.toml +3 -2
- package/dist/templates/codex/hooks/session-start.py +51 -21
- package/dist/templates/codex/skills/start/SKILL.md +1 -1
- package/dist/templates/copilot/hooks/session-start.py +51 -21
- package/dist/templates/copilot/prompts/start.prompt.md +1 -1
- package/dist/templates/cursor/agents/trellis-check.md +1 -1
- package/dist/templates/cursor/agents/trellis-implement.md +1 -1
- package/dist/templates/cursor/agents/trellis-research.md +2 -2
- package/dist/templates/cursor/hooks.json +7 -1
- package/dist/templates/droid/droids/trellis-research.md +1 -1
- package/dist/templates/extract.d.ts +6 -0
- package/dist/templates/extract.d.ts.map +1 -1
- package/dist/templates/extract.js +14 -0
- package/dist/templates/extract.js.map +1 -1
- package/dist/templates/gemini/agents/trellis-research.md +1 -1
- package/dist/templates/kiro/agents/trellis-research.json +1 -1
- package/dist/templates/markdown/agents.md +11 -12
- package/dist/templates/markdown/gitignore.txt +3 -0
- package/dist/templates/opencode/agents/trellis-check.md +1 -1
- package/dist/templates/opencode/agents/trellis-implement.md +1 -1
- package/dist/templates/opencode/agents/trellis-research.md +2 -2
- package/dist/templates/opencode/lib/trellis-context.js +100 -13
- package/dist/templates/opencode/plugins/inject-subagent-context.js +54 -4
- package/dist/templates/opencode/plugins/inject-workflow-state.js +48 -25
- package/dist/templates/opencode/plugins/session-start.js +29 -16
- package/dist/templates/pi/agents/trellis-check.md +28 -0
- package/dist/templates/pi/agents/trellis-implement.md +33 -0
- package/dist/templates/pi/agents/trellis-research.md +25 -0
- package/dist/templates/pi/extensions/trellis/index.ts.txt +549 -0
- package/dist/templates/pi/index.d.ts +5 -0
- package/dist/templates/pi/index.d.ts.map +1 -0
- package/dist/templates/pi/index.js +12 -0
- package/dist/templates/pi/index.js.map +1 -0
- package/dist/templates/pi/settings.json +12 -0
- package/dist/templates/qoder/agents/trellis-research.md +1 -1
- package/dist/templates/shared-hooks/index.d.ts +31 -0
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +59 -0
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-shell-session-context.py +180 -0
- package/dist/templates/shared-hooks/inject-subagent-context.py +128 -26
- package/dist/templates/shared-hooks/inject-workflow-state.py +99 -62
- package/dist/templates/shared-hooks/session-start.py +139 -24
- package/dist/templates/trellis/gitignore.txt +3 -0
- package/dist/templates/trellis/index.d.ts +1 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +2 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/__init__.py +8 -0
- package/dist/templates/trellis/scripts/common/active_task.py +593 -0
- package/dist/templates/trellis/scripts/common/cli_adapter.py +43 -8
- package/dist/templates/trellis/scripts/common/paths.py +61 -58
- package/dist/templates/trellis/scripts/common/session_context.py +12 -0
- package/dist/templates/trellis/scripts/common/task_store.py +6 -8
- package/dist/templates/trellis/scripts/task.py +59 -17
- package/dist/templates/trellis/workflow.md +30 -26
- package/dist/types/ai-tools.d.ts +3 -3
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +16 -0
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/utils/posix.d.ts +13 -0
- package/dist/utils/posix.d.ts.map +1 -0
- package/dist/utils/posix.js +15 -0
- package/dist/utils/posix.js.map +1 -0
- package/dist/utils/template-fetcher.d.ts +22 -6
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +405 -27
- package/dist/utils/template-fetcher.js.map +1 -1
- package/dist/utils/template-hash.d.ts +22 -3
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +80 -18
- package/dist/utils/template-hash.js.map +1 -1
- package/package.json +1 -1
- package/dist/templates/shared-hooks/statusline.py +0 -219
|
@@ -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;
|
|
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,7 +12,7 @@ Core Design Philosophy:
|
|
|
12
12
|
|
|
13
13
|
Trigger: PreToolUse (before Task tool call)
|
|
14
14
|
|
|
15
|
-
Context Source:
|
|
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
|
|
@@ -29,6 +29,7 @@ import json
|
|
|
29
29
|
import os
|
|
30
30
|
import sys
|
|
31
31
|
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
32
33
|
|
|
33
34
|
# IMPORTANT: Force stdout to use UTF-8 on Windows
|
|
34
35
|
# This fixes UnicodeEncodeError when outputting non-ASCII characters
|
|
@@ -46,7 +47,6 @@ if sys.platform.startswith("win"):
|
|
|
46
47
|
|
|
47
48
|
DIR_WORKFLOW = ".trellis"
|
|
48
49
|
DIR_SPEC = "spec"
|
|
49
|
-
FILE_CURRENT_TASK = ".current-task"
|
|
50
50
|
FILE_TASK_JSON = "task.json"
|
|
51
51
|
|
|
52
52
|
# =============================================================================
|
|
@@ -78,32 +78,57 @@ def find_repo_root(start_path: str) -> str | None:
|
|
|
78
78
|
return None
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
def
|
|
82
|
-
""
|
|
83
|
-
|
|
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
|
|
84
113
|
|
|
85
|
-
Returns:
|
|
86
|
-
Task directory relative path (relative to repo_root)
|
|
87
|
-
None if not set
|
|
88
|
-
"""
|
|
89
|
-
current_task_file = os.path.join(repo_root, DIR_WORKFLOW, FILE_CURRENT_TASK)
|
|
90
|
-
if not os.path.exists(current_task_file):
|
|
91
|
-
return None
|
|
92
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))
|
|
93
120
|
try:
|
|
94
|
-
|
|
95
|
-
content = f.read().strip()
|
|
96
|
-
if not content:
|
|
97
|
-
return None
|
|
98
|
-
normalized = content.replace("\\", "/")
|
|
99
|
-
while normalized.startswith("./"):
|
|
100
|
-
normalized = normalized[2:]
|
|
101
|
-
if normalized.startswith("tasks/"):
|
|
102
|
-
normalized = f".trellis/{normalized}"
|
|
103
|
-
return normalized
|
|
121
|
+
from common.active_task import resolve_active_task # type: ignore[import-not-found]
|
|
104
122
|
except Exception:
|
|
105
123
|
return None
|
|
106
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
|
+
|
|
107
132
|
|
|
108
133
|
def read_file_content(base_path: str, file_path: str) -> str | None:
|
|
109
134
|
"""Read file content, return None if file doesn't exist"""
|
|
@@ -517,13 +542,90 @@ Provide structured search results including:
|
|
|
517
542
|
- External references (if any)"""
|
|
518
543
|
|
|
519
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
|
+
|
|
520
622
|
def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]:
|
|
521
623
|
"""Parse hook input across different platform formats.
|
|
522
624
|
|
|
523
625
|
Returns (subagent_type, original_prompt, tool_input).
|
|
524
626
|
Handles:
|
|
525
627
|
- Claude Code / Qoder / CodeBuddy / Droid: tool_name=Task|Agent, tool_input.subagent_type
|
|
526
|
-
- Cursor: tool_name=Task, tool_input.subagent_type
|
|
628
|
+
- Cursor: tool_name=Task|Subagent, tool_input.subagent_type
|
|
527
629
|
- Copilot CLI: toolName=task (camelCase key, lowercase value)
|
|
528
630
|
- Gemini CLI: tool_name IS the agent name (BeforeTool matcher already filtered)
|
|
529
631
|
- Kiro: agentSpawn hook, agent_name field at top level
|
|
@@ -532,9 +634,9 @@ def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]:
|
|
|
532
634
|
|
|
533
635
|
# Standard format: Task/Agent tool with subagent_type
|
|
534
636
|
tool_name = input_data.get("tool_name", "") or input_data.get("toolName", "")
|
|
535
|
-
if tool_name.lower() in ("task", "agent"):
|
|
637
|
+
if tool_name.lower() in ("task", "agent", "subagent"):
|
|
536
638
|
return (
|
|
537
|
-
tool_input
|
|
639
|
+
_extract_subagent_type(tool_input),
|
|
538
640
|
tool_input.get("prompt", ""),
|
|
539
641
|
tool_input,
|
|
540
642
|
)
|
|
@@ -576,7 +678,7 @@ def main():
|
|
|
576
678
|
sys.exit(0)
|
|
577
679
|
|
|
578
680
|
# Get current task directory (research doesn't require it)
|
|
579
|
-
task_dir = get_current_task(repo_root)
|
|
681
|
+
task_dir = get_current_task(repo_root, input_data)
|
|
580
682
|
|
|
581
683
|
# implement/check need task directory
|
|
582
684
|
if subagent_type in AGENTS_REQUIRE_TASK:
|