@mindfoldhq/trellis 0.6.7 → 0.6.9

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 (174) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +38 -0
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/channel/adapters/codex.d.ts +7 -1
  6. package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
  7. package/dist/commands/channel/adapters/codex.js +19 -2
  8. package/dist/commands/channel/adapters/codex.js.map +1 -1
  9. package/dist/commands/channel/adapters/index.d.ts +3 -1
  10. package/dist/commands/channel/adapters/index.d.ts.map +1 -1
  11. package/dist/commands/channel/adapters/index.js +1 -1
  12. package/dist/commands/channel/adapters/index.js.map +1 -1
  13. package/dist/commands/channel/agent-loader.d.ts +2 -2
  14. package/dist/commands/channel/agent-loader.d.ts.map +1 -1
  15. package/dist/commands/channel/agent-loader.js +9 -5
  16. package/dist/commands/channel/agent-loader.js.map +1 -1
  17. package/dist/commands/channel/context-loader.d.ts +1 -1
  18. package/dist/commands/channel/context-loader.d.ts.map +1 -1
  19. package/dist/commands/channel/context-loader.js +36 -15
  20. package/dist/commands/channel/context-loader.js.map +1 -1
  21. package/dist/commands/channel/context-trust.d.ts +31 -0
  22. package/dist/commands/channel/context-trust.d.ts.map +1 -0
  23. package/dist/commands/channel/context-trust.js +139 -0
  24. package/dist/commands/channel/context-trust.js.map +1 -0
  25. package/dist/commands/channel/index.d.ts.map +1 -1
  26. package/dist/commands/channel/index.js +4 -0
  27. package/dist/commands/channel/index.js.map +1 -1
  28. package/dist/commands/channel/spawn.d.ts +3 -0
  29. package/dist/commands/channel/spawn.d.ts.map +1 -1
  30. package/dist/commands/channel/spawn.js +5 -2
  31. package/dist/commands/channel/spawn.js.map +1 -1
  32. package/dist/commands/channel/supervisor.d.ts +3 -0
  33. package/dist/commands/channel/supervisor.d.ts.map +1 -1
  34. package/dist/commands/channel/supervisor.js +1 -0
  35. package/dist/commands/channel/supervisor.js.map +1 -1
  36. package/dist/commands/init.d.ts +3 -0
  37. package/dist/commands/init.d.ts.map +1 -1
  38. package/dist/commands/init.js.map +1 -1
  39. package/dist/commands/mem.d.ts.map +1 -1
  40. package/dist/commands/mem.js +30 -9
  41. package/dist/commands/mem.js.map +1 -1
  42. package/dist/commands/update.d.ts +12 -1
  43. package/dist/commands/update.d.ts.map +1 -1
  44. package/dist/commands/update.js +75 -7
  45. package/dist/commands/update.js.map +1 -1
  46. package/dist/configurators/codex.d.ts +36 -0
  47. package/dist/configurators/codex.d.ts.map +1 -1
  48. package/dist/configurators/codex.js +111 -13
  49. package/dist/configurators/codex.js.map +1 -1
  50. package/dist/configurators/grok.d.ts +22 -0
  51. package/dist/configurators/grok.d.ts.map +1 -0
  52. package/dist/configurators/grok.js +58 -0
  53. package/dist/configurators/grok.js.map +1 -0
  54. package/dist/configurators/index.d.ts.map +1 -1
  55. package/dist/configurators/index.js +17 -2
  56. package/dist/configurators/index.js.map +1 -1
  57. package/dist/configurators/kimi.d.ts +29 -0
  58. package/dist/configurators/kimi.d.ts.map +1 -0
  59. package/dist/configurators/kimi.js +81 -0
  60. package/dist/configurators/kimi.js.map +1 -0
  61. package/dist/configurators/pi.js +9 -9
  62. package/dist/configurators/pi.js.map +1 -1
  63. package/dist/configurators/shared.d.ts.map +1 -1
  64. package/dist/configurators/shared.js +12 -4
  65. package/dist/configurators/shared.js.map +1 -1
  66. package/dist/configurators/snow.d.ts +31 -0
  67. package/dist/configurators/snow.d.ts.map +1 -0
  68. package/dist/configurators/snow.js +95 -0
  69. package/dist/configurators/snow.js.map +1 -0
  70. package/dist/configurators/workflow.d.ts +14 -0
  71. package/dist/configurators/workflow.d.ts.map +1 -1
  72. package/dist/configurators/workflow.js +37 -1
  73. package/dist/configurators/workflow.js.map +1 -1
  74. package/dist/configurators/zcode.d.ts +9 -4
  75. package/dist/configurators/zcode.d.ts.map +1 -1
  76. package/dist/configurators/zcode.js +41 -12
  77. package/dist/configurators/zcode.js.map +1 -1
  78. package/dist/migrations/manifests/0.5.0-beta.0.json +1 -1
  79. package/dist/migrations/manifests/0.6.8.json +17 -0
  80. package/dist/migrations/manifests/0.6.9.json +9 -0
  81. package/dist/templates/codex/agents/trellis-check.toml +7 -12
  82. package/dist/templates/codex/agents/trellis-implement.toml +7 -12
  83. package/dist/templates/codex/agents/trellis-research.toml +15 -15
  84. package/dist/templates/codex/config.toml +11 -0
  85. package/dist/templates/codex/hooks/session-start.py +9 -3
  86. package/dist/templates/codex/hooks.json +12 -0
  87. package/dist/templates/codex/skills/brainstorm/SKILL.md +40 -13
  88. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +2 -2
  89. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +1 -1
  90. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +1 -0
  91. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +1 -1
  92. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +1 -1
  93. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +10 -4
  94. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +2 -1
  95. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
  96. package/dist/templates/common/skills/brainstorm.md +40 -13
  97. package/dist/templates/copilot/prompts/brainstorm.prompt.md +40 -13
  98. package/dist/templates/grok/agents/trellis-check.md +105 -0
  99. package/dist/templates/grok/agents/trellis-implement.md +116 -0
  100. package/dist/templates/grok/agents/trellis-research.md +146 -0
  101. package/dist/templates/grok/index.d.ts +16 -0
  102. package/dist/templates/grok/index.d.ts.map +1 -0
  103. package/dist/templates/grok/index.js +19 -0
  104. package/dist/templates/grok/index.js.map +1 -0
  105. package/dist/templates/kimi/agents/trellis-check.md +103 -0
  106. package/dist/templates/kimi/agents/trellis-implement.md +114 -0
  107. package/dist/templates/kimi/agents/trellis-research.md +144 -0
  108. package/dist/templates/kimi/index.d.ts +19 -0
  109. package/dist/templates/kimi/index.d.ts.map +1 -0
  110. package/dist/templates/kimi/index.js +22 -0
  111. package/dist/templates/kimi/index.js.map +1 -0
  112. package/dist/templates/omp/extensions/trellis/index.ts.txt +133 -9
  113. package/dist/templates/opencode/lib/session-utils.js +8 -2
  114. package/dist/templates/opencode/lib/trellis-context.js +263 -43
  115. package/dist/templates/opencode/plugins/inject-subagent-context.js +89 -35
  116. package/dist/templates/opencode/plugins/inject-workflow-state.js +90 -5
  117. package/dist/templates/pi/extensions/trellis/index.ts.txt +278 -24
  118. package/dist/templates/pi/settings.json +0 -3
  119. package/dist/templates/shared-hooks/index.d.ts +11 -5
  120. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  121. package/dist/templates/shared-hooks/index.js +16 -5
  122. package/dist/templates/shared-hooks/index.js.map +1 -1
  123. package/dist/templates/shared-hooks/inject-subagent-context.py +466 -104
  124. package/dist/templates/shared-hooks/inject-workflow-state.py +86 -29
  125. package/dist/templates/shared-hooks/session-start.py +26 -6
  126. package/dist/templates/snow/SNOW.md +86 -0
  127. package/dist/templates/snow/agents/trellis-check.md +135 -0
  128. package/dist/templates/snow/agents/trellis-implement.md +153 -0
  129. package/dist/templates/snow/agents/trellis-research.md +114 -0
  130. package/dist/templates/snow/hooks/beforeSubAgentStart.json +15 -0
  131. package/dist/templates/snow/hooks/onSessionStart.json +15 -0
  132. package/dist/templates/snow/hooks/onUserMessage.json +15 -0
  133. package/dist/templates/snow/hooks/write-trellis-context.py +644 -0
  134. package/dist/templates/snow/index.d.ts +26 -0
  135. package/dist/templates/snow/index.d.ts.map +1 -0
  136. package/dist/templates/snow/index.js +36 -0
  137. package/dist/templates/snow/index.js.map +1 -0
  138. package/dist/templates/template-utils.d.ts.map +1 -1
  139. package/dist/templates/template-utils.js +5 -1
  140. package/dist/templates/template-utils.js.map +1 -1
  141. package/dist/templates/trellis/config.yaml +55 -7
  142. package/dist/templates/trellis/gitattributes.txt +9 -0
  143. package/dist/templates/trellis/index.d.ts +3 -1
  144. package/dist/templates/trellis/index.d.ts.map +1 -1
  145. package/dist/templates/trellis/index.js +3 -1
  146. package/dist/templates/trellis/index.js.map +1 -1
  147. package/dist/templates/trellis/scripts/add_session.py +94 -28
  148. package/dist/templates/trellis/scripts/common/active_task.py +44 -13
  149. package/dist/templates/trellis/scripts/common/cli_adapter.py +69 -5
  150. package/dist/templates/trellis/scripts/common/config.py +105 -10
  151. package/dist/templates/trellis/scripts/common/git.py +36 -0
  152. package/dist/templates/trellis/scripts/common/io.py +16 -9
  153. package/dist/templates/trellis/scripts/common/task_context.py +101 -7
  154. package/dist/templates/trellis/scripts/common/task_store.py +149 -22
  155. package/dist/templates/trellis/scripts/common/task_utils.py +3 -1
  156. package/dist/templates/trellis/scripts/common/trellis_config.py +2 -1
  157. package/dist/templates/trellis/scripts/common/workflow_phase.py +19 -12
  158. package/dist/templates/trellis/scripts/task.py +106 -10
  159. package/dist/templates/trellis/workflow.md +20 -19
  160. package/dist/templates/zcode/agents/trellis-check.md +7 -0
  161. package/dist/templates/zcode/agents/trellis-implement.md +13 -4
  162. package/dist/templates/zcode/config.json +42 -0
  163. package/dist/templates/zcode/index.d.ts +10 -1
  164. package/dist/templates/zcode/index.d.ts.map +1 -1
  165. package/dist/templates/zcode/index.js +12 -2
  166. package/dist/templates/zcode/index.js.map +1 -1
  167. package/dist/types/ai-tools.d.ts +4 -4
  168. package/dist/types/ai-tools.d.ts.map +1 -1
  169. package/dist/types/ai-tools.js +114 -2
  170. package/dist/types/ai-tools.js.map +1 -1
  171. package/dist/utils/template-fetcher.d.ts.map +1 -1
  172. package/dist/utils/template-fetcher.js +0 -3
  173. package/dist/utils/template-fetcher.js.map +1 -1
  174. package/package.json +2 -2
@@ -0,0 +1,644 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Snow CLI Trellis context writer + additionalContext emitter.
4
+
5
+ Used by onSessionStart / onUserMessage / beforeSubAgentStart hooks.
6
+
7
+ 1. Writes a breadcrumb file agents/skills can Read:
8
+ .snow/log/trellis-context.txt
9
+ 2. Prints stdout JSON for the Snow inject protocol:
10
+ { "additionalContext": "...", "display": "..." }
11
+
12
+ Modes (compact vs full):
13
+ - argv: session | user | subagent (preferred)
14
+ - env: TRELLIS_SNOW_HOOK_MODE
15
+ - stdin JSON from Snow may include agentId/agentName/prompt/cwd/sessionId
16
+
17
+ Non-JSON hosts ignore stdout; Snow injects it into model context.
18
+ Fail-open: never raise out of main().
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from collections.abc import Mapping
24
+ import json
25
+ import os
26
+ import re
27
+ import subprocess
28
+ import sys
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ # Force UTF-8 on Windows consoles (cp936/cp1252 otherwise corrupt Chinese paths).
33
+ if sys.platform.startswith("win"):
34
+ import io as _io
35
+
36
+ for _stream_name in ("stdin", "stdout", "stderr"):
37
+ _stream = getattr(sys, _stream_name, None)
38
+ if _stream is None:
39
+ continue
40
+ if hasattr(_stream, "reconfigure"):
41
+ try:
42
+ _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
43
+ except Exception:
44
+ pass
45
+ elif hasattr(_stream, "detach"):
46
+ try:
47
+ setattr(
48
+ sys,
49
+ _stream_name,
50
+ _io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace"),
51
+ )
52
+ except Exception:
53
+ pass
54
+
55
+
56
+ DEFAULT_MAX_BYTES = 7500
57
+ COMPACT_MAX_BYTES = 2800
58
+
59
+
60
+ def _find_repo_root(start: Path) -> Path:
61
+ cur = start.resolve()
62
+ for candidate in [cur, *cur.parents]:
63
+ if (candidate / ".trellis").is_dir():
64
+ return candidate
65
+ return cur
66
+
67
+
68
+ def _run(cmd: list[str], cwd: Path) -> str:
69
+ try:
70
+ completed = subprocess.run(
71
+ cmd,
72
+ cwd=str(cwd),
73
+ capture_output=True,
74
+ text=True,
75
+ encoding="utf-8",
76
+ errors="replace",
77
+ # Keep below Snow user/subagent hook timeout (15s) so fail-open JSON
78
+ # can still emit if task.py stalls.
79
+ timeout=5,
80
+ check=False,
81
+ )
82
+ except Exception as exc: # noqa: BLE001 — hooks must never crash the host
83
+ return f"(command failed: {exc})"
84
+ out = (completed.stdout or "").strip()
85
+ if out:
86
+ return out
87
+ err = (completed.stderr or "").strip()
88
+ return err or "(no output)"
89
+
90
+
91
+ def _read_text(path: Path, limit: int = 4000) -> str:
92
+ try:
93
+ data = path.read_text(encoding="utf-8", errors="replace")
94
+ except Exception:
95
+ return ""
96
+ data = data.strip()
97
+ if len(data) > limit:
98
+ return data[: limit - 20] + "\n... (truncated)"
99
+ return data
100
+
101
+
102
+ def _count_jsonl_lines(path: Path) -> int | None:
103
+ if not path.is_file():
104
+ return None
105
+ try:
106
+ count = 0
107
+ with path.open("r", encoding="utf-8", errors="replace") as fh:
108
+ for line in fh:
109
+ if line.strip():
110
+ count += 1
111
+ return count
112
+ except Exception:
113
+ return None
114
+
115
+
116
+ def _jsonl_summaries(path: Path, max_items: int = 8, line_limit: int = 120) -> list[str]:
117
+ if max_items <= 0 or not path.is_file():
118
+ return []
119
+ items: list[str] = []
120
+ try:
121
+ with path.open("r", encoding="utf-8", errors="replace") as fh:
122
+ for raw in fh:
123
+ line = raw.strip()
124
+ if not line:
125
+ continue
126
+ summary = line
127
+ try:
128
+ obj = json.loads(line)
129
+ if isinstance(obj, dict):
130
+ for key in ("summary", "title", "id", "path", "message", "status"):
131
+ val = obj.get(key)
132
+ if isinstance(val, str) and val.strip():
133
+ summary = val.strip()
134
+ break
135
+ else:
136
+ summary = json.dumps(obj, ensure_ascii=False)
137
+ except Exception:
138
+ pass
139
+ if len(summary) > line_limit:
140
+ summary = summary[: line_limit - 3] + "..."
141
+ items.append(f"- {summary}")
142
+ if len(items) > max_items:
143
+ items.pop(0)
144
+ except Exception:
145
+ return []
146
+ return items
147
+
148
+
149
+ def _parse_active_task_path(current_out: str, repo: Path) -> Path | None:
150
+ # Prefer "Current task: .trellis/tasks/..." line from task.py --source.
151
+ for line in current_out.splitlines():
152
+ m = re.search(r"Current task:\s*(.+)\s*$", line.strip(), re.I)
153
+ if not m:
154
+ continue
155
+ raw = m.group(1).strip().strip('"').strip("'")
156
+ if not raw or raw.lower() in {"none", "(none)", "n/a"}:
157
+ return None
158
+ p = Path(raw)
159
+ if not p.is_absolute():
160
+ p = repo / p
161
+ return p
162
+ # Fallback: first path-like token containing tasks/
163
+ m = re.search(r"(\.trellis[/\\]tasks[/\\][^\s]+)", current_out)
164
+ if m:
165
+ return repo / m.group(1).replace("\\", "/")
166
+ return None
167
+
168
+
169
+ def _drain_stdin(timeout_sec: float = 0.5) -> str:
170
+ """Best-effort drain of host-piped hook context without hanging on TTY."""
171
+ chunks: list[str] = []
172
+ try:
173
+ if sys.stdin is None or sys.stdin.closed:
174
+ return ""
175
+ # Interactive terminal: do not block waiting for EOF.
176
+ if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
177
+ return ""
178
+
179
+ # Hosts (Snow) pipe JSON then close stdin. Manual CLI runs must not hang.
180
+ if sys.platform.startswith("win"):
181
+ import threading
182
+
183
+ box: list[str] = []
184
+
185
+ def _read() -> None:
186
+ try:
187
+ box.append(sys.stdin.read())
188
+ except Exception:
189
+ pass
190
+
191
+ t = threading.Thread(target=_read, daemon=True)
192
+ t.start()
193
+ t.join(timeout_sec)
194
+ return box[0] if box else ""
195
+
196
+ import select
197
+
198
+ while True:
199
+ ready, _, _ = select.select([sys.stdin], [], [], timeout_sec)
200
+ if not ready:
201
+ break
202
+ chunk = sys.stdin.read(4096)
203
+ if not chunk:
204
+ break
205
+ chunks.append(chunk)
206
+ except Exception:
207
+ pass
208
+ return "".join(chunks)
209
+
210
+
211
+ def _parse_stdin_json(raw: str) -> dict[str, Any]:
212
+ text = (raw or "").strip()
213
+ if not text:
214
+ return {}
215
+ try:
216
+ data = json.loads(text)
217
+ return data if isinstance(data, dict) else {}
218
+ except Exception:
219
+ # Some hosts may wrap payload; try first JSON object substring.
220
+ start = text.find("{")
221
+ end = text.rfind("}")
222
+ if start >= 0 and end > start:
223
+ try:
224
+ data = json.loads(text[start : end + 1])
225
+ return data if isinstance(data, dict) else {}
226
+ except Exception:
227
+ return {}
228
+ return {}
229
+
230
+
231
+ def _resolve_mode(
232
+ argv: list[str], env: Mapping[str, str], stdin_ctx: dict[str, Any]
233
+ ) -> str:
234
+ for arg in argv[1:]:
235
+ a = arg.strip().lower()
236
+ if a in {"session", "user", "subagent", "full", "compact"}:
237
+ if a == "full":
238
+ return "session"
239
+ if a == "compact":
240
+ return "user"
241
+ return a
242
+ env_mode = (env.get("TRELLIS_SNOW_HOOK_MODE") or "").strip().lower()
243
+ if env_mode in {"session", "user", "subagent"}:
244
+ return env_mode
245
+ # Infer from Snow stdin context shape.
246
+ if stdin_ctx.get("agentId") or stdin_ctx.get("agentName") or stdin_ctx.get("prompt"):
247
+ return "subagent"
248
+ if stdin_ctx.get("message") is not None or stdin_ctx.get("source") in {"normal", "pending"}:
249
+ return "user"
250
+ if stdin_ctx.get("isResume") is not None or stdin_ctx.get("messages") is not None:
251
+ return "session"
252
+ return "session"
253
+
254
+
255
+ def _agent_kind(stdin_ctx: dict[str, Any]) -> str:
256
+ blob = " ".join(
257
+ str(stdin_ctx.get(k) or "")
258
+ for k in ("agentId", "agentName", "prompt")
259
+ ).lower()
260
+ if "implement" in blob:
261
+ return "implement"
262
+ if "check" in blob:
263
+ return "check"
264
+ if "research" in blob:
265
+ return "research"
266
+ return "generic"
267
+
268
+
269
+ def _task_artifact_summary(task_dir: Path, *, detailed: bool, kind: str) -> list[str]:
270
+ lines: list[str] = [f"## Active task artifacts ({task_dir.as_posix()})"]
271
+ for name in ("prd.md", "design.md", "implement.md", "task.json"):
272
+ p = task_dir / name
273
+ if p.is_file():
274
+ try:
275
+ size = p.stat().st_size
276
+ except Exception:
277
+ size = -1
278
+ lines.append(f"- {name}: present ({size} bytes)")
279
+ else:
280
+ lines.append(f"- {name}: missing")
281
+
282
+ for jl in ("implement.jsonl", "check.jsonl"):
283
+ n = _count_jsonl_lines(task_dir / jl)
284
+ if n is None:
285
+ lines.append(f"- {jl}: missing")
286
+ else:
287
+ lines.append(f"- {jl}: {n} entries")
288
+
289
+ research_dir = task_dir / "research"
290
+ if research_dir.is_dir():
291
+ try:
292
+ research_files = [p.name for p in research_dir.iterdir() if p.is_file()]
293
+ except Exception:
294
+ research_files = []
295
+ lines.append(f"- research/: {len(research_files)} file(s)")
296
+ else:
297
+ lines.append("- research/: missing")
298
+
299
+ lines.append("")
300
+
301
+ if not detailed:
302
+ return lines
303
+
304
+ prd = task_dir / "prd.md"
305
+ if prd.is_file():
306
+ # Prefer first ~40 lines / ~1800 chars for full mode.
307
+ body = _read_text(prd, 1800)
308
+ if body:
309
+ lines.extend(["## prd.md summary", body, ""])
310
+
311
+ if kind in {"implement", "generic"}:
312
+ impl = task_dir / "implement.jsonl"
313
+ items = _jsonl_summaries(impl, max_items=10)
314
+ if items:
315
+ lines.append("## implement.jsonl (recent)")
316
+ lines.extend(items)
317
+ lines.append("")
318
+ design = task_dir / "design.md"
319
+ if design.is_file():
320
+ dbody = _read_text(design, 900)
321
+ if dbody:
322
+ lines.extend(["## design.md excerpt", dbody, ""])
323
+ implement_md = task_dir / "implement.md"
324
+ if implement_md.is_file():
325
+ ibody = _read_text(implement_md, 900)
326
+ if ibody:
327
+ lines.extend(["## implement.md excerpt", ibody, ""])
328
+
329
+ if kind in {"check", "generic"}:
330
+ lines.extend(
331
+ [
332
+ "## Check focus",
333
+ "- Run `git diff` / diagnostics for uncommitted changes.",
334
+ "- Compare against prd/design/implement and project specs.",
335
+ "- Self-fix issues; do not only report.",
336
+ "",
337
+ ]
338
+ )
339
+ check_items = _jsonl_summaries(task_dir / "check.jsonl", max_items=8)
340
+ if check_items:
341
+ lines.append("## check.jsonl (recent)")
342
+ lines.extend(check_items)
343
+ lines.append("")
344
+
345
+ if kind == "research":
346
+ lines.extend(
347
+ [
348
+ "## Research focus",
349
+ f"- Persist findings ONLY under `{task_dir.as_posix()}/research/`.",
350
+ "- Chat reply should list file paths + one-line summaries, not full dumps.",
351
+ "- Do not modify code outside the research directory.",
352
+ "",
353
+ ]
354
+ )
355
+
356
+ return lines
357
+
358
+
359
+ def _sanitize_session_key(value: str) -> str:
360
+ cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", (value or "").strip())
361
+ return cleaned.strip("._-")
362
+
363
+
364
+ def _current_session_ids(stdin_ctx: dict[str, Any] | None = None) -> list[str]:
365
+ # Prefer explicit Trellis context, then Snow session identity.
366
+ ids: list[str] = []
367
+ candidates = [
368
+ os.environ.get("TRELLIS_CONTEXT_ID"),
369
+ os.environ.get("SNOW_SESSION_ID"),
370
+ ]
371
+ if stdin_ctx:
372
+ candidates.extend(
373
+ [
374
+ stdin_ctx.get("sessionId"),
375
+ stdin_ctx.get("session_id"),
376
+ ]
377
+ )
378
+ for raw in candidates:
379
+ if not isinstance(raw, str):
380
+ continue
381
+ key = _sanitize_session_key(raw)
382
+ if key and key not in ids:
383
+ ids.append(key)
384
+ # Snow injects TRELLIS_CONTEXT_ID=snow-<sessionId>
385
+ if key.startswith("snow_"):
386
+ bare = key[5:]
387
+ if bare and bare not in ids:
388
+ ids.append(bare)
389
+ if key.startswith("snow-"):
390
+ bare = key[5:]
391
+ if bare and bare not in ids:
392
+ ids.append(bare)
393
+ return ids
394
+
395
+
396
+ def _resolve_runtime_session_file(
397
+ repo: Path,
398
+ stdin_ctx: dict[str, Any] | None = None,
399
+ ) -> Path | None:
400
+ # Resolve only the current session runtime file; never pick by mtime.
401
+ sessions_dir = repo / ".trellis" / ".runtime" / "sessions"
402
+ if not sessions_dir.is_dir():
403
+ return None
404
+ for sid in _current_session_ids(stdin_ctx):
405
+ for name in (f"{sid}.json", sid):
406
+ candidate = sessions_dir / name
407
+ if candidate.is_file():
408
+ return candidate
409
+ return None
410
+
411
+
412
+ def _workflow_phase_summary(
413
+ repo: Path,
414
+ stdin_ctx: dict[str, Any] | None = None,
415
+ ) -> list[str]:
416
+ lines: list[str] = []
417
+ workflow = repo / ".trellis" / "workflow.md"
418
+ if workflow.is_file():
419
+ body = _read_text(workflow, 900)
420
+ if body:
421
+ lines.extend(["## workflow.md excerpt", body, ""])
422
+
423
+ # Prefer classic path; fall back only to the *current* runtime session file.
424
+ session_md = repo / ".trellis" / "session" / "current.md"
425
+ if session_md.is_file():
426
+ body = _read_text(session_md, 1200)
427
+ if body:
428
+ lines.extend(["## .trellis/session/current.md", body, ""])
429
+ else:
430
+ current_session = _resolve_runtime_session_file(repo, stdin_ctx)
431
+ if current_session is not None:
432
+ body = _read_text(current_session, 900)
433
+ if body:
434
+ lines.extend(
435
+ [
436
+ f"## runtime session ({current_session.name})",
437
+ body,
438
+ "",
439
+ ]
440
+ )
441
+ return lines
442
+
443
+
444
+ def build_context(
445
+ repo: Path,
446
+ *,
447
+ mode: str,
448
+ stdin_ctx: dict[str, Any],
449
+ ) -> str:
450
+ compact = mode == "user"
451
+ kind = _agent_kind(stdin_ctx) if mode == "subagent" else "generic"
452
+
453
+ lines: list[str] = [
454
+ "# Trellis context (Snow CLI)",
455
+ "",
456
+ f"Mode: {mode}" + (f" | agentKind: {kind}" if mode == "subagent" else ""),
457
+ f"Repo: {repo}",
458
+ "",
459
+ ]
460
+
461
+ if stdin_ctx.get("sessionId"):
462
+ lines.append(f"Snow sessionId: {stdin_ctx.get('sessionId')}")
463
+ if stdin_ctx.get("cwd"):
464
+ lines.append(f"Hook cwd: {stdin_ctx.get('cwd')}")
465
+ if mode == "subagent":
466
+ if stdin_ctx.get("agentId") or stdin_ctx.get("agentName"):
467
+ lines.append(
468
+ f"Sub-agent: {stdin_ctx.get('agentId') or stdin_ctx.get('agentName')}"
469
+ )
470
+ prompt = stdin_ctx.get("prompt")
471
+ if isinstance(prompt, str) and prompt.strip():
472
+ p = prompt.strip()
473
+ if len(p) > 240:
474
+ p = p[:237] + "..."
475
+ lines.append(f"Dispatch prompt (truncated): {p}")
476
+ lines.append("")
477
+
478
+ task_py = repo / ".trellis" / "scripts" / "task.py"
479
+ current = ""
480
+ task_dir: Path | None = None
481
+ if task_py.is_file():
482
+ py = sys.executable or "python3"
483
+ current = _run([py, "-X", "utf8", str(task_py), "current", "--source"], repo)
484
+ lines.extend(["## task.py current --source", "```", current, "```", ""])
485
+ task_dir = _parse_active_task_path(current, repo)
486
+ else:
487
+ lines.append("(no .trellis/scripts/task.py — run trellis init first)")
488
+ lines.append("")
489
+
490
+ if task_dir and task_dir.exists():
491
+ lines.extend(
492
+ _task_artifact_summary(
493
+ task_dir,
494
+ detailed=not compact,
495
+ kind=kind if mode == "subagent" else "generic",
496
+ )
497
+ )
498
+ elif current:
499
+ lines.append("(could not resolve active task directory from task.py output)")
500
+ lines.append("")
501
+
502
+ if not compact:
503
+ lines.extend(_workflow_phase_summary(repo, stdin_ctx))
504
+
505
+ identity = repo / ".trellis" / "identity.md"
506
+ if identity.is_file():
507
+ body = _read_text(identity, 900 if mode == "subagent" else 1200)
508
+ if body:
509
+ lines.extend(["## .trellis/identity.md", body, ""])
510
+ else:
511
+ # Compact: short identity one-liner if file exists.
512
+ identity = repo / ".trellis" / "identity.md"
513
+ if identity.is_file():
514
+ body = _read_text(identity, 280)
515
+ if body:
516
+ first = body.splitlines()[0].strip()
517
+ lines.append(f"Identity: {first}")
518
+ lines.append("")
519
+
520
+ # Mode-specific checklist (keep short).
521
+ if mode == "subagent" and kind == "research":
522
+ task_hint = (
523
+ task_dir.as_posix()
524
+ if task_dir
525
+ else "<path from task.py current>"
526
+ )
527
+ lines.extend(
528
+ [
529
+ "## Sub-agent checklist (research)",
530
+ f"1. Active task: {task_hint}",
531
+ f"2. Write ONLY under `{task_hint}/research/`.",
532
+ "3. Return paths + one-line summaries to main session.",
533
+ "",
534
+ ]
535
+ )
536
+ elif mode == "subagent" and kind == "check":
537
+ lines.extend(
538
+ [
539
+ "## Sub-agent checklist (check)",
540
+ "1. `git diff` + diagnostics first.",
541
+ "2. Compare against prd/design/implement + specs.",
542
+ "3. Self-fix; no git commit/push/merge.",
543
+ "",
544
+ ]
545
+ )
546
+ elif mode == "subagent" and kind == "implement":
547
+ lines.extend(
548
+ [
549
+ "## Sub-agent checklist (implement)",
550
+ "1. Read prd + design/implement artifacts before coding.",
551
+ "2. Prefer Active task path from task.py / inject above.",
552
+ "3. No git commit/push/merge from implement agent.",
553
+ "",
554
+ ]
555
+ )
556
+ else:
557
+ lines.extend(
558
+ [
559
+ "## Main-session checklist",
560
+ "1. Session/user hooks auto-inject Trellis context; `/trellis-start` is optional."
561
+ if not compact
562
+ else "1. Compact inject — full context also at `.snow/log/trellis-context.txt`.",
563
+ "2. When dispatching implement/check/research, start the prompt with:",
564
+ " `Active task: <path from task.py current>`",
565
+ "3. Do not git commit/push/merge from Trellis implement/check agents.",
566
+ "",
567
+ ]
568
+ )
569
+
570
+ return "\n".join(lines).strip() + "\n"
571
+
572
+
573
+ def _truncate(text: str, max_bytes: int) -> str:
574
+ encoded = text.encode("utf-8")
575
+ if len(encoded) <= max_bytes:
576
+ return text
577
+ suffix = "\n... (truncated)\n"
578
+ budget = max(0, max_bytes - len(suffix.encode("utf-8")))
579
+ return encoded[:budget].decode("utf-8", errors="ignore") + suffix
580
+
581
+
582
+ def main() -> int:
583
+ try:
584
+ raw_stdin = _drain_stdin()
585
+ stdin_ctx = _parse_stdin_json(raw_stdin)
586
+ mode = _resolve_mode(sys.argv, os.environ, stdin_ctx)
587
+
588
+ cwd_hint = stdin_ctx.get("cwd") if isinstance(stdin_ctx.get("cwd"), str) else None
589
+ cwd = Path(cwd_hint or os.environ.get("SNOW_CWD") or os.getcwd())
590
+ repo = _find_repo_root(cwd)
591
+ context = build_context(repo, mode=mode, stdin_ctx=stdin_ctx)
592
+
593
+ log_dir = repo / ".snow" / "log"
594
+ try:
595
+ log_dir.mkdir(parents=True, exist_ok=True)
596
+ # Keep trellis-context.txt as the richest practical breadcrumb for
597
+ # pull-based reads. User-mode inject stays compact, but must not
598
+ # clobber a prior full snapshot.
599
+ log_path = log_dir / "trellis-context.txt"
600
+ if mode == "user":
601
+ full_context = build_context(repo, mode="session", stdin_ctx=stdin_ctx)
602
+ log_path.write_text(full_context, encoding="utf-8")
603
+ else:
604
+ log_path.write_text(context, encoding="utf-8")
605
+ except Exception as exc: # noqa: BLE001
606
+ sys.stderr.write(f"trellis-context write failed: {exc}\n")
607
+
608
+ max_bytes = COMPACT_MAX_BYTES if mode == "user" else DEFAULT_MAX_BYTES
609
+ inject = _truncate(context, max_bytes)
610
+ display = {
611
+ "session": "Trellis session context injected",
612
+ "user": "Trellis compact breadcrumb",
613
+ "subagent": f"Trellis sub-agent context ({_agent_kind(stdin_ctx)})",
614
+ }.get(mode, "Trellis context refreshed")
615
+ display = f"{display} (.snow/log/trellis-context.txt)"
616
+
617
+ payload = {
618
+ "additionalContext": inject,
619
+ "display": display,
620
+ }
621
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False))
622
+ sys.stdout.write("\n")
623
+ return 0
624
+ except Exception as exc: # noqa: BLE001 — absolute fail-open
625
+ try:
626
+ sys.stderr.write(f"write-trellis-context failed: {exc}\n")
627
+ # Still emit empty-safe JSON so host can continue.
628
+ sys.stdout.write(
629
+ json.dumps(
630
+ {
631
+ "additionalContext": "# Trellis context unavailable (hook error)\n",
632
+ "display": "Trellis context hook error (fail-open)",
633
+ },
634
+ ensure_ascii=False,
635
+ )
636
+ )
637
+ sys.stdout.write("\n")
638
+ except Exception:
639
+ pass
640
+ return 0
641
+
642
+
643
+ if __name__ == "__main__":
644
+ raise SystemExit(main())
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Snow CLI template module.
3
+ *
4
+ * Snow CLI is a class-1 platform:
5
+ * - Skills under `.snow/skills/<name>/SKILL.md`
6
+ * - Slash commands under `.snow/commands/trellis-*.json` (prompt type)
7
+ * - Project agents under `.snow/agents/<name>.md` (auto-discovered)
8
+ * - Inject hooks under `.snow/hooks/` (additionalContext + log breadcrumb)
9
+ * - Operator guide `.snow/SNOW.md`
10
+ *
11
+ * hasHooks=true: SessionStart/UserMessage/beforeSubAgentStart inject workflow
12
+ * context, so trellis-start is filtered out of skills/commands (optional
13
+ * recovery still documented in SNOW.md).
14
+ *
15
+ * Class-1: do NOT inject the class-2 "does NOT auto-inject" pull-based prelude.
16
+ * Hook inject (session/user/beforeSubAgentStart) is the primary context path.
17
+ * trellis-research remains standalone (no implement/check-specific prelude).
18
+ */
19
+ import { type AgentTemplate, type HookTemplate } from "../template-utils.js";
20
+ /** Sub-agent definitions (trellis-implement, trellis-check, trellis-research). */
21
+ export declare function getAllAgents(): AgentTemplate[];
22
+ /** Inject + breadcrumb hooks (onSessionStart / onUserMessage / beforeSubAgentStart). */
23
+ export declare function getAllHooks(): HookTemplate[];
24
+ /** Operator guide copied to `.snow/SNOW.md`. */
25
+ export declare function getSnowGuide(): string;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/snow/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,sBAAsB,CAAC;AAM9B,kFAAkF;AAClF,wBAAgB,YAAY,IAAI,aAAa,EAAE,CAE9C;AAED,wFAAwF;AACxF,wBAAgB,WAAW,IAAI,YAAY,EAAE,CAK5C;AAED,gDAAgD;AAChD,wBAAgB,YAAY,IAAI,MAAM,CAErC"}