@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.
- package/README.md +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +38 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/channel/adapters/codex.d.ts +7 -1
- package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
- package/dist/commands/channel/adapters/codex.js +19 -2
- package/dist/commands/channel/adapters/codex.js.map +1 -1
- package/dist/commands/channel/adapters/index.d.ts +3 -1
- package/dist/commands/channel/adapters/index.d.ts.map +1 -1
- package/dist/commands/channel/adapters/index.js +1 -1
- package/dist/commands/channel/adapters/index.js.map +1 -1
- package/dist/commands/channel/agent-loader.d.ts +2 -2
- package/dist/commands/channel/agent-loader.d.ts.map +1 -1
- package/dist/commands/channel/agent-loader.js +9 -5
- package/dist/commands/channel/agent-loader.js.map +1 -1
- package/dist/commands/channel/context-loader.d.ts +1 -1
- package/dist/commands/channel/context-loader.d.ts.map +1 -1
- package/dist/commands/channel/context-loader.js +36 -15
- package/dist/commands/channel/context-loader.js.map +1 -1
- package/dist/commands/channel/context-trust.d.ts +31 -0
- package/dist/commands/channel/context-trust.d.ts.map +1 -0
- package/dist/commands/channel/context-trust.js +139 -0
- package/dist/commands/channel/context-trust.js.map +1 -0
- package/dist/commands/channel/index.d.ts.map +1 -1
- package/dist/commands/channel/index.js +4 -0
- package/dist/commands/channel/index.js.map +1 -1
- package/dist/commands/channel/spawn.d.ts +3 -0
- package/dist/commands/channel/spawn.d.ts.map +1 -1
- package/dist/commands/channel/spawn.js +5 -2
- package/dist/commands/channel/spawn.js.map +1 -1
- package/dist/commands/channel/supervisor.d.ts +3 -0
- package/dist/commands/channel/supervisor.d.ts.map +1 -1
- package/dist/commands/channel/supervisor.js +1 -0
- package/dist/commands/channel/supervisor.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mem.d.ts.map +1 -1
- package/dist/commands/mem.js +30 -9
- package/dist/commands/mem.js.map +1 -1
- package/dist/commands/update.d.ts +12 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +75 -7
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/codex.d.ts +36 -0
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +111 -13
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/grok.d.ts +22 -0
- package/dist/configurators/grok.d.ts.map +1 -0
- package/dist/configurators/grok.js +58 -0
- package/dist/configurators/grok.js.map +1 -0
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +17 -2
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/kimi.d.ts +29 -0
- package/dist/configurators/kimi.d.ts.map +1 -0
- package/dist/configurators/kimi.js +81 -0
- package/dist/configurators/kimi.js.map +1 -0
- package/dist/configurators/pi.js +9 -9
- package/dist/configurators/pi.js.map +1 -1
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +12 -4
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/snow.d.ts +31 -0
- package/dist/configurators/snow.d.ts.map +1 -0
- package/dist/configurators/snow.js +95 -0
- package/dist/configurators/snow.js.map +1 -0
- package/dist/configurators/workflow.d.ts +14 -0
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +37 -1
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/configurators/zcode.d.ts +9 -4
- package/dist/configurators/zcode.d.ts.map +1 -1
- package/dist/configurators/zcode.js +41 -12
- package/dist/configurators/zcode.js.map +1 -1
- package/dist/migrations/manifests/0.5.0-beta.0.json +1 -1
- package/dist/migrations/manifests/0.6.8.json +17 -0
- package/dist/migrations/manifests/0.6.9.json +9 -0
- package/dist/templates/codex/agents/trellis-check.toml +7 -12
- package/dist/templates/codex/agents/trellis-implement.toml +7 -12
- package/dist/templates/codex/agents/trellis-research.toml +15 -15
- package/dist/templates/codex/config.toml +11 -0
- package/dist/templates/codex/hooks/session-start.py +9 -3
- package/dist/templates/codex/hooks.json +12 -0
- package/dist/templates/codex/skills/brainstorm/SKILL.md +40 -13
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +1 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +10 -4
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +2 -1
- package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
- package/dist/templates/common/skills/brainstorm.md +40 -13
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +40 -13
- package/dist/templates/grok/agents/trellis-check.md +105 -0
- package/dist/templates/grok/agents/trellis-implement.md +116 -0
- package/dist/templates/grok/agents/trellis-research.md +146 -0
- package/dist/templates/grok/index.d.ts +16 -0
- package/dist/templates/grok/index.d.ts.map +1 -0
- package/dist/templates/grok/index.js +19 -0
- package/dist/templates/grok/index.js.map +1 -0
- package/dist/templates/kimi/agents/trellis-check.md +103 -0
- package/dist/templates/kimi/agents/trellis-implement.md +114 -0
- package/dist/templates/kimi/agents/trellis-research.md +144 -0
- package/dist/templates/kimi/index.d.ts +19 -0
- package/dist/templates/kimi/index.d.ts.map +1 -0
- package/dist/templates/kimi/index.js +22 -0
- package/dist/templates/kimi/index.js.map +1 -0
- package/dist/templates/omp/extensions/trellis/index.ts.txt +133 -9
- package/dist/templates/opencode/lib/session-utils.js +8 -2
- package/dist/templates/opencode/lib/trellis-context.js +263 -43
- package/dist/templates/opencode/plugins/inject-subagent-context.js +89 -35
- package/dist/templates/opencode/plugins/inject-workflow-state.js +90 -5
- package/dist/templates/pi/extensions/trellis/index.ts.txt +278 -24
- package/dist/templates/pi/settings.json +0 -3
- package/dist/templates/shared-hooks/index.d.ts +11 -5
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +16 -5
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-subagent-context.py +466 -104
- package/dist/templates/shared-hooks/inject-workflow-state.py +86 -29
- package/dist/templates/shared-hooks/session-start.py +26 -6
- package/dist/templates/snow/SNOW.md +86 -0
- package/dist/templates/snow/agents/trellis-check.md +135 -0
- package/dist/templates/snow/agents/trellis-implement.md +153 -0
- package/dist/templates/snow/agents/trellis-research.md +114 -0
- package/dist/templates/snow/hooks/beforeSubAgentStart.json +15 -0
- package/dist/templates/snow/hooks/onSessionStart.json +15 -0
- package/dist/templates/snow/hooks/onUserMessage.json +15 -0
- package/dist/templates/snow/hooks/write-trellis-context.py +644 -0
- package/dist/templates/snow/index.d.ts +26 -0
- package/dist/templates/snow/index.d.ts.map +1 -0
- package/dist/templates/snow/index.js +36 -0
- package/dist/templates/snow/index.js.map +1 -0
- package/dist/templates/template-utils.d.ts.map +1 -1
- package/dist/templates/template-utils.js +5 -1
- package/dist/templates/template-utils.js.map +1 -1
- package/dist/templates/trellis/config.yaml +55 -7
- package/dist/templates/trellis/gitattributes.txt +9 -0
- package/dist/templates/trellis/index.d.ts +3 -1
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +3 -1
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +94 -28
- package/dist/templates/trellis/scripts/common/active_task.py +44 -13
- package/dist/templates/trellis/scripts/common/cli_adapter.py +69 -5
- package/dist/templates/trellis/scripts/common/config.py +105 -10
- package/dist/templates/trellis/scripts/common/git.py +36 -0
- package/dist/templates/trellis/scripts/common/io.py +16 -9
- package/dist/templates/trellis/scripts/common/task_context.py +101 -7
- package/dist/templates/trellis/scripts/common/task_store.py +149 -22
- package/dist/templates/trellis/scripts/common/task_utils.py +3 -1
- package/dist/templates/trellis/scripts/common/trellis_config.py +2 -1
- package/dist/templates/trellis/scripts/common/workflow_phase.py +19 -12
- package/dist/templates/trellis/scripts/task.py +106 -10
- package/dist/templates/trellis/workflow.md +20 -19
- package/dist/templates/zcode/agents/trellis-check.md +7 -0
- package/dist/templates/zcode/agents/trellis-implement.md +13 -4
- package/dist/templates/zcode/config.json +42 -0
- package/dist/templates/zcode/index.d.ts +10 -1
- package/dist/templates/zcode/index.d.ts.map +1 -1
- package/dist/templates/zcode/index.js +12 -2
- package/dist/templates/zcode/index.js.map +1 -1
- package/dist/types/ai-tools.d.ts +4 -4
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +114 -2
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +0 -3
- package/dist/utils/template-fetcher.js.map +1 -1
- 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"}
|