@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
|
@@ -80,9 +80,12 @@ def find_repo_root(start_path: str) -> str | None:
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def _detect_platform(input_data: dict) -> str | None:
|
|
83
|
+
if _hook_event_name(input_data) == "SubagentStart":
|
|
84
|
+
return "codex"
|
|
83
85
|
if isinstance(input_data.get("cursor_version"), str):
|
|
84
86
|
return "cursor"
|
|
85
87
|
env_map = {
|
|
88
|
+
"ZCODE_PROJECT_DIR": "zcode",
|
|
86
89
|
"CLAUDE_PROJECT_DIR": "claude",
|
|
87
90
|
"CURSOR_PROJECT_DIR": "cursor",
|
|
88
91
|
"CODEBUDDY_PROJECT_DIR": "codebuddy",
|
|
@@ -110,10 +113,20 @@ def _detect_platform(input_data: dict) -> str | None:
|
|
|
110
113
|
return "droid"
|
|
111
114
|
if ".kiro" in script_parts:
|
|
112
115
|
return "kiro"
|
|
116
|
+
if ".zcode" in script_parts:
|
|
117
|
+
return "zcode"
|
|
113
118
|
return None
|
|
114
119
|
|
|
115
120
|
|
|
116
|
-
def get_current_task(
|
|
121
|
+
def get_current_task(
|
|
122
|
+
repo_root: str,
|
|
123
|
+
input_data: dict,
|
|
124
|
+
*,
|
|
125
|
+
platform: str | None = None,
|
|
126
|
+
allow_single_session_fallback: bool = True,
|
|
127
|
+
allow_environment_context: bool = True,
|
|
128
|
+
require_existing: bool = False,
|
|
129
|
+
) -> str | None:
|
|
117
130
|
"""Resolve current task directory through the unified active task resolver."""
|
|
118
131
|
scripts_dir = Path(repo_root) / DIR_WORKFLOW / "scripts"
|
|
119
132
|
if str(scripts_dir) not in sys.path:
|
|
@@ -126,70 +139,220 @@ def get_current_task(repo_root: str, input_data: dict) -> str | None:
|
|
|
126
139
|
active = resolve_active_task(
|
|
127
140
|
Path(repo_root),
|
|
128
141
|
input_data,
|
|
129
|
-
platform=_detect_platform(input_data),
|
|
142
|
+
platform=platform or _detect_platform(input_data),
|
|
143
|
+
allow_single_session_fallback=allow_single_session_fallback,
|
|
144
|
+
allow_environment_context=allow_environment_context,
|
|
130
145
|
)
|
|
146
|
+
if require_existing and active.stale:
|
|
147
|
+
return None
|
|
131
148
|
return active.task_path
|
|
132
149
|
|
|
133
150
|
|
|
134
|
-
|
|
135
|
-
|
|
151
|
+
# =============================================================================
|
|
152
|
+
# Context Injection Limits (issue #441)
|
|
153
|
+
#
|
|
154
|
+
# Notice text and behavior mirrored byte-for-byte in the Pi TS extension
|
|
155
|
+
# (templates/pi/extensions/trellis/index.ts.txt). Changing wording here
|
|
156
|
+
# requires changing it there too.
|
|
157
|
+
# =============================================================================
|
|
158
|
+
|
|
159
|
+
DEFAULT_MAX_FILE_BYTES = 32768
|
|
160
|
+
DEFAULT_MAX_ARTIFACT_BYTES = 65536
|
|
161
|
+
DEFAULT_MAX_TOTAL_BYTES = 131072
|
|
162
|
+
|
|
163
|
+
DEFAULT_LIMITS: dict[str, int] = {
|
|
164
|
+
"max_file_bytes": DEFAULT_MAX_FILE_BYTES,
|
|
165
|
+
"max_artifact_bytes": DEFAULT_MAX_ARTIFACT_BYTES,
|
|
166
|
+
"max_total_bytes": DEFAULT_MAX_TOTAL_BYTES,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _get_limits(repo_root: str) -> dict[str, int]:
|
|
171
|
+
"""Load context-injection byte limits from config.yaml, with safe fallback."""
|
|
172
|
+
scripts_dir = Path(repo_root) / DIR_WORKFLOW / "scripts"
|
|
173
|
+
if str(scripts_dir) not in sys.path:
|
|
174
|
+
sys.path.insert(0, str(scripts_dir))
|
|
175
|
+
try:
|
|
176
|
+
from common.config import get_context_injection_limits # type: ignore[import-not-found]
|
|
177
|
+
|
|
178
|
+
return get_context_injection_limits(Path(repo_root))
|
|
179
|
+
except Exception:
|
|
180
|
+
return dict(DEFAULT_LIMITS)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def truncate_utf8(data: bytes, cap: int) -> bytes:
|
|
184
|
+
"""Truncate ``data`` to at most ``cap`` bytes without splitting a UTF-8
|
|
185
|
+
multi-byte sequence.
|
|
186
|
+
|
|
187
|
+
``cap <= 0`` means "no limit" — returns ``data`` unchanged.
|
|
188
|
+
"""
|
|
189
|
+
if cap <= 0 or len(data) <= cap:
|
|
190
|
+
return data
|
|
191
|
+
|
|
192
|
+
truncated = data[:cap]
|
|
193
|
+
i = len(truncated)
|
|
194
|
+
# Back off over continuation bytes (10xxxxxx) to find the lead byte.
|
|
195
|
+
while i > 0 and (truncated[i - 1] & 0xC0) == 0x80:
|
|
196
|
+
i -= 1
|
|
197
|
+
if i == 0:
|
|
198
|
+
return b""
|
|
199
|
+
|
|
200
|
+
lead = truncated[i - 1]
|
|
201
|
+
if lead & 0x80:
|
|
202
|
+
if (lead & 0xE0) == 0xC0:
|
|
203
|
+
seq_len = 2
|
|
204
|
+
elif (lead & 0xF0) == 0xE0:
|
|
205
|
+
seq_len = 3
|
|
206
|
+
elif (lead & 0xF8) == 0xF0:
|
|
207
|
+
seq_len = 4
|
|
208
|
+
else:
|
|
209
|
+
seq_len = 1
|
|
210
|
+
# Drop the lead byte too if its full sequence didn't fit.
|
|
211
|
+
if (i - 1) + seq_len > len(truncated):
|
|
212
|
+
i -= 1
|
|
213
|
+
|
|
214
|
+
return truncated[:i]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class _Budget:
|
|
218
|
+
"""Tracks the running total of bytes emitted into the sub-agent context."""
|
|
219
|
+
|
|
220
|
+
def __init__(self, max_total_bytes: int) -> None:
|
|
221
|
+
self.max_total_bytes = max_total_bytes
|
|
222
|
+
self.used = 0
|
|
223
|
+
|
|
224
|
+
def has_room(self, size: int) -> bool:
|
|
225
|
+
if self.max_total_bytes <= 0:
|
|
226
|
+
return True
|
|
227
|
+
return self.used + size <= self.max_total_bytes
|
|
228
|
+
|
|
229
|
+
def add(self, size: int) -> None:
|
|
230
|
+
self.used += size
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _read_file_bytes(base_path: str, file_path: str) -> bytes | None:
|
|
234
|
+
"""Read raw file bytes, return None if file doesn't exist."""
|
|
136
235
|
full_path = os.path.join(base_path, file_path)
|
|
137
236
|
if os.path.exists(full_path) and os.path.isfile(full_path):
|
|
138
237
|
try:
|
|
139
|
-
with open(full_path, "
|
|
238
|
+
with open(full_path, "rb") as f:
|
|
140
239
|
return f.read()
|
|
141
240
|
except Exception:
|
|
142
241
|
return None
|
|
143
242
|
return None
|
|
144
243
|
|
|
145
244
|
|
|
146
|
-
def
|
|
147
|
-
|
|
148
|
-
) -> list[tuple[str, str]]:
|
|
149
|
-
"""
|
|
150
|
-
Read all .md files in a directory
|
|
245
|
+
def _truncate_notice(path: str, cap: int) -> str:
|
|
246
|
+
return f"\n[Trellis: truncated at {cap} bytes — read {path} for the full content]"
|
|
151
247
|
|
|
152
|
-
Args:
|
|
153
|
-
base_path: Base path (usually repo_root)
|
|
154
|
-
dir_path: Directory relative path
|
|
155
|
-
max_files: Max files to read (prevent huge directories)
|
|
156
248
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
""
|
|
249
|
+
def _is_binary_content(data: bytes) -> bool:
|
|
250
|
+
"""Return True when raw bytes should not be decoded into model context."""
|
|
251
|
+
if b"\x00" in data:
|
|
252
|
+
return True
|
|
253
|
+
try:
|
|
254
|
+
data.decode("utf-8", errors="strict")
|
|
255
|
+
except UnicodeDecodeError:
|
|
256
|
+
return True
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _binary_notice(path: str, size: int, reason: str) -> str:
|
|
261
|
+
return (
|
|
262
|
+
f"[Trellis: not inlined (binary file) — "
|
|
263
|
+
f"{path} ({size} bytes): {reason}]"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _index_notice(path: str, size: int, reason: str) -> str:
|
|
268
|
+
return (
|
|
269
|
+
f"[Trellis: not inlined (total context limit reached) — "
|
|
270
|
+
f"{path} ({size} bytes): {reason}]"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _budgeted_block(
|
|
275
|
+
budget: _Budget,
|
|
276
|
+
header: str,
|
|
277
|
+
plain_path: str,
|
|
278
|
+
content: str,
|
|
279
|
+
reason: str,
|
|
280
|
+
size_for_index: int,
|
|
281
|
+
) -> str:
|
|
282
|
+
"""Return an inlined ``=== header ===`` block, or degrade to an index
|
|
283
|
+
notice once the total context budget is exhausted."""
|
|
284
|
+
block = f"=== {header} ===\n{content}"
|
|
285
|
+
block_bytes = len(block.encode("utf-8"))
|
|
286
|
+
if not budget.has_room(block_bytes):
|
|
287
|
+
notice = _index_notice(plain_path, size_for_index, reason)
|
|
288
|
+
budget.add(len(notice.encode("utf-8")))
|
|
289
|
+
return notice
|
|
290
|
+
budget.add(block_bytes)
|
|
291
|
+
return block
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _materialize_file(
|
|
295
|
+
base_path: str,
|
|
296
|
+
file_path: str,
|
|
297
|
+
reason: str,
|
|
298
|
+
limits: dict[str, int],
|
|
299
|
+
budget: _Budget,
|
|
300
|
+
) -> str | None:
|
|
301
|
+
"""Read a JSONL-referenced file, apply the per-file cap, then budget it."""
|
|
302
|
+
data = _read_file_bytes(base_path, file_path)
|
|
303
|
+
if data is None:
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
size = len(data)
|
|
307
|
+
if _is_binary_content(data):
|
|
308
|
+
notice = _binary_notice(file_path, size, reason)
|
|
309
|
+
budget.add(len(notice.encode("utf-8")))
|
|
310
|
+
return notice
|
|
311
|
+
|
|
312
|
+
cap = limits["max_file_bytes"]
|
|
313
|
+
truncated_bytes = truncate_utf8(data, cap)
|
|
314
|
+
content = truncated_bytes.decode("utf-8", errors="replace")
|
|
315
|
+
if len(truncated_bytes) < size:
|
|
316
|
+
content += _truncate_notice(file_path, cap)
|
|
317
|
+
|
|
318
|
+
return _budgeted_block(budget, file_path, file_path, content, reason, size)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _materialize_directory(
|
|
322
|
+
base_path: str,
|
|
323
|
+
dir_path: str,
|
|
324
|
+
reason: str,
|
|
325
|
+
limits: dict[str, int],
|
|
326
|
+
budget: _Budget,
|
|
327
|
+
max_files: int = 20,
|
|
328
|
+
) -> list[str]:
|
|
329
|
+
"""Read all .md files in a directory, applying the same per-file and
|
|
330
|
+
total caps as a single-file JSONL entry."""
|
|
160
331
|
full_path = os.path.join(base_path, dir_path)
|
|
161
332
|
if not os.path.exists(full_path) or not os.path.isdir(full_path):
|
|
162
333
|
return []
|
|
163
334
|
|
|
164
|
-
|
|
335
|
+
blocks: list[str] = []
|
|
165
336
|
try:
|
|
166
|
-
# Only read .md files, sorted by filename
|
|
167
337
|
md_files = sorted(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if f.endswith(".md") and os.path.isfile(os.path.join(full_path, f))
|
|
172
|
-
]
|
|
338
|
+
f
|
|
339
|
+
for f in os.listdir(full_path)
|
|
340
|
+
if f.endswith(".md") and os.path.isfile(os.path.join(full_path, f))
|
|
173
341
|
)
|
|
174
|
-
|
|
175
342
|
for filename in md_files[:max_files]:
|
|
176
|
-
file_full_path = os.path.join(full_path, filename)
|
|
177
343
|
relative_path = os.path.join(dir_path, filename)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
results.append((relative_path, content))
|
|
182
|
-
except Exception:
|
|
183
|
-
continue
|
|
344
|
+
block = _materialize_file(base_path, relative_path, reason, limits, budget)
|
|
345
|
+
if block:
|
|
346
|
+
blocks.append(block)
|
|
184
347
|
except Exception:
|
|
185
348
|
pass
|
|
186
349
|
|
|
187
|
-
return
|
|
350
|
+
return blocks
|
|
188
351
|
|
|
189
352
|
|
|
190
|
-
def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[
|
|
353
|
+
def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[dict]:
|
|
191
354
|
"""
|
|
192
|
-
|
|
355
|
+
Parse all file/directory entries referenced in a jsonl context file.
|
|
193
356
|
|
|
194
357
|
Schema:
|
|
195
358
|
{"file": "path/to/file.md", "reason": "..."}
|
|
@@ -202,7 +365,7 @@ def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]
|
|
|
202
365
|
emitted so the operator can debug missing context.
|
|
203
366
|
|
|
204
367
|
Returns:
|
|
205
|
-
[
|
|
368
|
+
[{"file": path, "type": "file" | "directory", "reason": reason}, ...]
|
|
206
369
|
"""
|
|
207
370
|
full_path = os.path.join(base_path, jsonl_path)
|
|
208
371
|
if not os.path.exists(full_path):
|
|
@@ -213,7 +376,7 @@ def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]
|
|
|
213
376
|
)
|
|
214
377
|
return []
|
|
215
378
|
|
|
216
|
-
|
|
379
|
+
entries: list[dict] = []
|
|
217
380
|
saw_real_entry = False
|
|
218
381
|
try:
|
|
219
382
|
with open(full_path, "r", encoding="utf-8") as f:
|
|
@@ -224,22 +387,19 @@ def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]
|
|
|
224
387
|
try:
|
|
225
388
|
item = json.loads(line)
|
|
226
389
|
file_path = item.get("file") or item.get("path")
|
|
227
|
-
entry_type = item.get("type", "file")
|
|
228
390
|
|
|
229
391
|
if not file_path:
|
|
230
392
|
# Seed / comment row — skip silently
|
|
231
393
|
continue
|
|
232
394
|
|
|
233
395
|
saw_real_entry = True
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if content:
|
|
242
|
-
results.append((file_path, content))
|
|
396
|
+
entries.append(
|
|
397
|
+
{
|
|
398
|
+
"file": file_path,
|
|
399
|
+
"type": item.get("type", "file"),
|
|
400
|
+
"reason": item.get("reason") or "-",
|
|
401
|
+
}
|
|
402
|
+
)
|
|
243
403
|
except json.JSONDecodeError:
|
|
244
404
|
continue
|
|
245
405
|
except Exception:
|
|
@@ -253,23 +413,69 @@ def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]
|
|
|
253
413
|
file=sys.stderr,
|
|
254
414
|
)
|
|
255
415
|
|
|
256
|
-
return
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
416
|
+
return entries
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _materialize_jsonl_entries(
|
|
420
|
+
base_path: str, jsonl_path: str, limits: dict[str, int], budget: _Budget
|
|
421
|
+
) -> list[str]:
|
|
422
|
+
"""Materialize every entry in a jsonl context file into context blocks,
|
|
423
|
+
applying per-file and total budget caps."""
|
|
424
|
+
blocks: list[str] = []
|
|
425
|
+
for entry in read_jsonl_entries(base_path, jsonl_path):
|
|
426
|
+
if entry["type"] == "directory":
|
|
427
|
+
blocks.extend(
|
|
428
|
+
_materialize_directory(
|
|
429
|
+
base_path, entry["file"], entry["reason"], limits, budget
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
else:
|
|
433
|
+
block = _materialize_file(
|
|
434
|
+
base_path, entry["file"], entry["reason"], limits, budget
|
|
435
|
+
)
|
|
436
|
+
if block:
|
|
437
|
+
blocks.append(block)
|
|
438
|
+
return blocks
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def get_agent_context(
|
|
442
|
+
repo_root: str,
|
|
443
|
+
task_dir: str,
|
|
444
|
+
agent_type: str,
|
|
445
|
+
limits: dict[str, int],
|
|
446
|
+
budget: _Budget,
|
|
447
|
+
) -> str:
|
|
262
448
|
"""
|
|
263
449
|
Get context from {agent_type}.jsonl for the specified agent.
|
|
264
450
|
Only reads implement.jsonl or check.jsonl (the two JSONL files the task system creates).
|
|
265
451
|
"""
|
|
266
|
-
context_parts = []
|
|
267
|
-
|
|
268
452
|
agent_jsonl = f"{task_dir}/{agent_type}.jsonl"
|
|
269
|
-
|
|
270
|
-
|
|
453
|
+
blocks = _materialize_jsonl_entries(repo_root, agent_jsonl, limits, budget)
|
|
454
|
+
return "\n\n".join(blocks)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _materialize_artifact(
|
|
458
|
+
base_path: str,
|
|
459
|
+
file_path: str,
|
|
460
|
+
header_label: str,
|
|
461
|
+
reason: str,
|
|
462
|
+
limits: dict[str, int],
|
|
463
|
+
budget: _Budget,
|
|
464
|
+
) -> str | None:
|
|
465
|
+
"""Read a task artifact (prd/design/implement.md), apply the per-artifact
|
|
466
|
+
cap, then budget it."""
|
|
467
|
+
data = _read_file_bytes(base_path, file_path)
|
|
468
|
+
if data is None:
|
|
469
|
+
return None
|
|
271
470
|
|
|
272
|
-
|
|
471
|
+
size = len(data)
|
|
472
|
+
cap = limits["max_artifact_bytes"]
|
|
473
|
+
truncated_bytes = truncate_utf8(data, cap)
|
|
474
|
+
content = truncated_bytes.decode("utf-8", errors="replace")
|
|
475
|
+
if len(truncated_bytes) < size:
|
|
476
|
+
content += _truncate_notice(file_path, cap)
|
|
477
|
+
|
|
478
|
+
return _budgeted_block(budget, header_label, file_path, content, reason, size)
|
|
273
479
|
|
|
274
480
|
|
|
275
481
|
def get_implement_context(repo_root: str, task_dir: str) -> str:
|
|
@@ -282,31 +488,50 @@ def get_implement_context(repo_root: str, task_dir: str) -> str:
|
|
|
282
488
|
3. design.md if present (technical design)
|
|
283
489
|
4. implement.md if present (execution plan)
|
|
284
490
|
"""
|
|
491
|
+
limits = _get_limits(repo_root)
|
|
492
|
+
budget = _Budget(limits["max_total_bytes"])
|
|
285
493
|
context_parts = []
|
|
286
494
|
|
|
287
495
|
# 1. Read implement.jsonl
|
|
288
|
-
base_context = get_agent_context(repo_root, task_dir, "implement")
|
|
496
|
+
base_context = get_agent_context(repo_root, task_dir, "implement", limits, budget)
|
|
289
497
|
if base_context:
|
|
290
498
|
context_parts.append(base_context)
|
|
291
499
|
|
|
292
500
|
# 2. Requirements document
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
501
|
+
prd_block = _materialize_artifact(
|
|
502
|
+
repo_root,
|
|
503
|
+
f"{task_dir}/prd.md",
|
|
504
|
+
f"{task_dir}/prd.md (Requirements)",
|
|
505
|
+
"Requirements document",
|
|
506
|
+
limits,
|
|
507
|
+
budget,
|
|
508
|
+
)
|
|
509
|
+
if prd_block:
|
|
510
|
+
context_parts.append(prd_block)
|
|
296
511
|
|
|
297
512
|
# 3. Technical design for complex tasks
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
513
|
+
design_block = _materialize_artifact(
|
|
514
|
+
repo_root,
|
|
515
|
+
f"{task_dir}/design.md",
|
|
516
|
+
f"{task_dir}/design.md (Technical Design)",
|
|
517
|
+
"Technical design document",
|
|
518
|
+
limits,
|
|
519
|
+
budget,
|
|
520
|
+
)
|
|
521
|
+
if design_block:
|
|
522
|
+
context_parts.append(design_block)
|
|
303
523
|
|
|
304
524
|
# 4. Execution plan for complex tasks
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
525
|
+
implement_plan_block = _materialize_artifact(
|
|
526
|
+
repo_root,
|
|
527
|
+
f"{task_dir}/implement.md",
|
|
528
|
+
f"{task_dir}/implement.md (Execution Plan)",
|
|
529
|
+
"Execution plan document",
|
|
530
|
+
limits,
|
|
531
|
+
budget,
|
|
532
|
+
)
|
|
533
|
+
if implement_plan_block:
|
|
534
|
+
context_parts.append(implement_plan_block)
|
|
310
535
|
|
|
311
536
|
return "\n\n".join(context_parts)
|
|
312
537
|
|
|
@@ -315,26 +540,46 @@ def get_check_context(repo_root: str, task_dir: str) -> str:
|
|
|
315
540
|
"""
|
|
316
541
|
Context for Check Agent: check.jsonl + task artifacts.
|
|
317
542
|
"""
|
|
543
|
+
limits = _get_limits(repo_root)
|
|
544
|
+
budget = _Budget(limits["max_total_bytes"])
|
|
318
545
|
context_parts = []
|
|
319
546
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
prd_content = read_file_content(repo_root, f"{task_dir}/prd.md")
|
|
324
|
-
if prd_content:
|
|
325
|
-
context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}")
|
|
326
|
-
|
|
327
|
-
design_content = read_file_content(repo_root, f"{task_dir}/design.md")
|
|
328
|
-
if design_content:
|
|
329
|
-
context_parts.append(
|
|
330
|
-
f"=== {task_dir}/design.md (Technical Design) ===\n{design_content}"
|
|
331
|
-
)
|
|
547
|
+
base_context = get_agent_context(repo_root, task_dir, "check", limits, budget)
|
|
548
|
+
if base_context:
|
|
549
|
+
context_parts.append(base_context)
|
|
332
550
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
551
|
+
prd_block = _materialize_artifact(
|
|
552
|
+
repo_root,
|
|
553
|
+
f"{task_dir}/prd.md",
|
|
554
|
+
f"{task_dir}/prd.md (Requirements)",
|
|
555
|
+
"Requirements document",
|
|
556
|
+
limits,
|
|
557
|
+
budget,
|
|
558
|
+
)
|
|
559
|
+
if prd_block:
|
|
560
|
+
context_parts.append(prd_block)
|
|
561
|
+
|
|
562
|
+
design_block = _materialize_artifact(
|
|
563
|
+
repo_root,
|
|
564
|
+
f"{task_dir}/design.md",
|
|
565
|
+
f"{task_dir}/design.md (Technical Design)",
|
|
566
|
+
"Technical design document",
|
|
567
|
+
limits,
|
|
568
|
+
budget,
|
|
569
|
+
)
|
|
570
|
+
if design_block:
|
|
571
|
+
context_parts.append(design_block)
|
|
572
|
+
|
|
573
|
+
implement_plan_block = _materialize_artifact(
|
|
574
|
+
repo_root,
|
|
575
|
+
f"{task_dir}/implement.md",
|
|
576
|
+
f"{task_dir}/implement.md (Execution Plan)",
|
|
577
|
+
"Execution plan document",
|
|
578
|
+
limits,
|
|
579
|
+
budget,
|
|
580
|
+
)
|
|
581
|
+
if implement_plan_block:
|
|
582
|
+
context_parts.append(implement_plan_block)
|
|
338
583
|
|
|
339
584
|
return "\n\n".join(context_parts)
|
|
340
585
|
|
|
@@ -574,6 +819,98 @@ def _string_value(value: Any) -> str:
|
|
|
574
819
|
return ""
|
|
575
820
|
|
|
576
821
|
|
|
822
|
+
def _hook_event_name(input_data: dict) -> str:
|
|
823
|
+
"""Return a hook event name from the documented snake/camel-case fields."""
|
|
824
|
+
return _string_value(
|
|
825
|
+
input_data.get("hook_event_name") or input_data.get("hookEventName")
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _codex_subagent_type(input_data: dict) -> str:
|
|
830
|
+
"""Return a Trellis Codex agent type only for a native start event."""
|
|
831
|
+
if _hook_event_name(input_data) != "SubagentStart":
|
|
832
|
+
return ""
|
|
833
|
+
agent_type = _string_value(
|
|
834
|
+
input_data.get("agent_type") or input_data.get("agentType")
|
|
835
|
+
)
|
|
836
|
+
return agent_type if agent_type in AGENTS_ALL else ""
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def build_codex_subagent_context(
|
|
840
|
+
subagent_type: str,
|
|
841
|
+
task_dir: str,
|
|
842
|
+
context: str,
|
|
843
|
+
) -> str:
|
|
844
|
+
"""Build developer context for a native, already-dispatched Codex role."""
|
|
845
|
+
role = subagent_type.removeprefix("trellis-")
|
|
846
|
+
return f"""<!-- trellis-hook-injected -->
|
|
847
|
+
# Trellis Native {role.title()} Subagent
|
|
848
|
+
|
|
849
|
+
You are the dispatched `{subagent_type}` role for this task. Perform that role
|
|
850
|
+
directly; do not follow main-session dispatch or wait instructions, and do not
|
|
851
|
+
spawn another Trellis subagent.
|
|
852
|
+
|
|
853
|
+
Active task: {task_dir}
|
|
854
|
+
|
|
855
|
+
## Curated Context
|
|
856
|
+
|
|
857
|
+
{context}"""
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def _handle_codex_subagent_start(input_data: dict) -> None:
|
|
861
|
+
"""Emit Codex developer context for a recognised native Trellis subagent.
|
|
862
|
+
|
|
863
|
+
The event supplies the parent session id. Disabling the generic
|
|
864
|
+
single-session fallback is essential here: native starts must never borrow
|
|
865
|
+
a task from another Codex window when that parent id is absent or stale.
|
|
866
|
+
"""
|
|
867
|
+
subagent_type = _codex_subagent_type(input_data)
|
|
868
|
+
parent_session_id = _string_value(input_data.get("session_id"))
|
|
869
|
+
if not subagent_type or not parent_session_id:
|
|
870
|
+
return
|
|
871
|
+
|
|
872
|
+
cwd = _string_value(input_data.get("cwd")) or os.getcwd()
|
|
873
|
+
repo_root = find_repo_root(cwd)
|
|
874
|
+
if not repo_root:
|
|
875
|
+
return
|
|
876
|
+
|
|
877
|
+
task_dir = get_current_task(
|
|
878
|
+
repo_root,
|
|
879
|
+
{"session_id": parent_session_id},
|
|
880
|
+
platform="codex",
|
|
881
|
+
allow_single_session_fallback=False,
|
|
882
|
+
allow_environment_context=False,
|
|
883
|
+
require_existing=True,
|
|
884
|
+
)
|
|
885
|
+
if not task_dir:
|
|
886
|
+
return
|
|
887
|
+
|
|
888
|
+
if subagent_type in AGENTS_REQUIRE_TASK:
|
|
889
|
+
task_dir_full = Path(repo_root) / task_dir
|
|
890
|
+
if not task_dir_full.is_dir():
|
|
891
|
+
return
|
|
892
|
+
|
|
893
|
+
if subagent_type == AGENT_IMPLEMENT:
|
|
894
|
+
context = get_implement_context(repo_root, task_dir)
|
|
895
|
+
elif subagent_type == AGENT_CHECK:
|
|
896
|
+
context = get_check_context(repo_root, task_dir)
|
|
897
|
+
else:
|
|
898
|
+
context = get_research_context(repo_root, task_dir)
|
|
899
|
+
|
|
900
|
+
if not context:
|
|
901
|
+
return
|
|
902
|
+
|
|
903
|
+
output = {
|
|
904
|
+
"hookSpecificOutput": {
|
|
905
|
+
"hookEventName": "SubagentStart",
|
|
906
|
+
"additionalContext": build_codex_subagent_context(
|
|
907
|
+
subagent_type, task_dir, context
|
|
908
|
+
),
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
print(json.dumps(output, ensure_ascii=False))
|
|
912
|
+
|
|
913
|
+
|
|
577
914
|
def _extract_subagent_name(value: Any) -> str:
|
|
578
915
|
"""Extract a sub-agent name from common platform encodings.
|
|
579
916
|
|
|
@@ -652,10 +989,15 @@ def _parse_hook_input(input_data: dict) -> tuple[str, str, dict]:
|
|
|
652
989
|
- Claude Code / Qoder / CodeBuddy / Droid: tool_name=Task|Agent, tool_input.subagent_type
|
|
653
990
|
- Cursor: tool_name=Task|Subagent, tool_input.subagent_type
|
|
654
991
|
- Copilot CLI: toolName=task (camelCase key, lowercase value)
|
|
992
|
+
- ZCode: toolName=Agent, toolInput/tool_input.subagent_type
|
|
655
993
|
- Gemini CLI: tool_name IS the agent name (BeforeTool matcher already filtered)
|
|
656
994
|
- Kiro: agentSpawn hook, agent_name field at top level
|
|
657
995
|
"""
|
|
658
996
|
tool_input = input_data.get("tool_input", {})
|
|
997
|
+
if not isinstance(tool_input, dict):
|
|
998
|
+
tool_input = input_data.get("toolInput", {})
|
|
999
|
+
if not isinstance(tool_input, dict):
|
|
1000
|
+
tool_input = {}
|
|
659
1001
|
|
|
660
1002
|
# Standard format: Task/Agent tool with subagent_type
|
|
661
1003
|
tool_name = input_data.get("tool_name", "") or input_data.get("toolName", "")
|
|
@@ -692,6 +1034,17 @@ def main():
|
|
|
692
1034
|
input_data = json.load(sys.stdin)
|
|
693
1035
|
except json.JSONDecodeError:
|
|
694
1036
|
sys.exit(0)
|
|
1037
|
+
if not isinstance(input_data, dict):
|
|
1038
|
+
sys.exit(0)
|
|
1039
|
+
|
|
1040
|
+
if _hook_event_name(input_data) == "SubagentStart":
|
|
1041
|
+
try:
|
|
1042
|
+
_handle_codex_subagent_start(input_data)
|
|
1043
|
+
except Exception:
|
|
1044
|
+
# A native context hook must never prevent Codex from spawning the
|
|
1045
|
+
# requested child when its runtime state is unavailable or stale.
|
|
1046
|
+
pass
|
|
1047
|
+
sys.exit(0)
|
|
695
1048
|
|
|
696
1049
|
subagent_type, original_prompt, tool_input = _parse_hook_input(input_data)
|
|
697
1050
|
cwd = input_data.get("cwd", os.getcwd())
|
|
@@ -745,23 +1098,32 @@ def main():
|
|
|
745
1098
|
if not context:
|
|
746
1099
|
sys.exit(0)
|
|
747
1100
|
|
|
748
|
-
# Return updated input
|
|
749
|
-
#
|
|
750
|
-
#
|
|
1101
|
+
# Return updated input. Most platforms ignore unrecognized fields, so we
|
|
1102
|
+
# include multiple formats. ZCode is stricter; live probing confirmed the
|
|
1103
|
+
# nested Claude-compatible shape below reaches the sub-agent prompt.
|
|
751
1104
|
updated = {**tool_input, "prompt": new_prompt}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1105
|
+
if _detect_platform(input_data) == "zcode":
|
|
1106
|
+
output = {
|
|
1107
|
+
"hookSpecificOutput": {
|
|
1108
|
+
"hookEventName": "PreToolUse",
|
|
1109
|
+
"permissionDecision": "allow",
|
|
1110
|
+
"updatedInput": updated,
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
else:
|
|
1114
|
+
output = {
|
|
1115
|
+
# Claude Code / Qoder / CodeBuddy / Droid format
|
|
1116
|
+
"hookSpecificOutput": {
|
|
1117
|
+
"hookEventName": "PreToolUse",
|
|
1118
|
+
"permissionDecision": "allow",
|
|
1119
|
+
"updatedInput": updated,
|
|
1120
|
+
},
|
|
1121
|
+
# Cursor format
|
|
1122
|
+
"permission": "allow",
|
|
1123
|
+
"updated_input": updated,
|
|
1124
|
+
# Gemini format
|
|
757
1125
|
"updatedInput": updated,
|
|
758
|
-
}
|
|
759
|
-
# Cursor format
|
|
760
|
-
"permission": "allow",
|
|
761
|
-
"updated_input": updated,
|
|
762
|
-
# Gemini format
|
|
763
|
-
"updatedInput": updated,
|
|
764
|
-
}
|
|
1126
|
+
}
|
|
765
1127
|
|
|
766
1128
|
print(json.dumps(output, ensure_ascii=False))
|
|
767
1129
|
sys.exit(0)
|