@mindfoldhq/trellis 0.6.6 → 0.6.8
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 +37 -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/guard.d.ts.map +1 -1
- package/dist/commands/channel/guard.js +7 -2
- package/dist/commands/channel/guard.js.map +1 -1
- 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 +1 -0
- package/dist/commands/channel/spawn.js.map +1 -1
- package/dist/commands/channel/store/paths.d.ts +14 -0
- package/dist/commands/channel/store/paths.d.ts.map +1 -1
- package/dist/commands/channel/store/paths.js +26 -0
- package/dist/commands/channel/store/paths.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 +2 -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/uninstall.d.ts +9 -0
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +67 -3
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts +40 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +126 -34
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +11 -12
- 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 +12 -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 +11 -4
- package/dist/configurators/shared.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.7.json +9 -0
- package/dist/migrations/manifests/0.6.8.json +17 -0
- package/dist/templates/codex/agents/trellis-check.toml +5 -12
- package/dist/templates/codex/agents/trellis-implement.toml +5 -12
- package/dist/templates/codex/agents/trellis-research.toml +13 -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 +8 -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 +140 -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 +19 -5
- package/dist/templates/opencode/lib/session-utils.js +8 -2
- package/dist/templates/pi/extensions/trellis/index.ts.txt +8 -2
- 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 +151 -17
- package/dist/templates/shared-hooks/inject-workflow-state.py +50 -28
- package/dist/templates/shared-hooks/session-start.py +26 -6
- package/dist/templates/trellis/config.yaml +9 -7
- package/dist/templates/trellis/scripts/common/active_task.py +40 -13
- package/dist/templates/trellis/scripts/common/cli_adapter.py +69 -5
- package/dist/templates/trellis/scripts/common/config.py +16 -8
- package/dist/templates/trellis/scripts/common/git.py +36 -0
- package/dist/templates/trellis/scripts/common/io.py +28 -4
- package/dist/templates/trellis/scripts/common/task_context.py +19 -1
- package/dist/templates/trellis/scripts/common/task_store.py +98 -21
- package/dist/templates/trellis/scripts/common/task_utils.py +24 -0
- package/dist/templates/trellis/scripts/common/workflow_phase.py +19 -12
- package/dist/templates/trellis/scripts/task.py +85 -8
- 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 +76 -2
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/utils/atomic-write.d.ts +12 -0
- package/dist/utils/atomic-write.d.ts.map +1 -0
- package/dist/utils/atomic-write.js +30 -0
- package/dist/utils/atomic-write.js.map +1 -0
- package/dist/utils/file-writer.d.ts.map +1 -1
- package/dist/utils/file-writer.js +6 -5
- package/dist/utils/file-writer.js.map +1 -1
- package/dist/utils/registry-config.d.ts.map +1 -1
- package/dist/utils/registry-config.js +3 -2
- package/dist/utils/registry-config.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +35 -6
- package/dist/utils/template-fetcher.js.map +1 -1
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +2 -1
- package/dist/utils/template-hash.js.map +1 -1
- package/package.json +2 -2
|
@@ -96,6 +96,9 @@ def _detect_platform(input_data: dict) -> str | None:
|
|
|
96
96
|
if isinstance(input_data.get("cursor_version"), str):
|
|
97
97
|
return "cursor"
|
|
98
98
|
env_map = {
|
|
99
|
+
# ZCode may set both ZCODE_PROJECT_DIR and CLAUDE_PROJECT_DIR; check
|
|
100
|
+
# ZCODE first so ZCode sessions aren't misdetected as claude.
|
|
101
|
+
"ZCODE_PROJECT_DIR": "zcode",
|
|
99
102
|
"CLAUDE_PROJECT_DIR": "claude",
|
|
100
103
|
"CURSOR_PROJECT_DIR": "cursor",
|
|
101
104
|
"CODEBUDDY_PROJECT_DIR": "codebuddy",
|
|
@@ -128,6 +131,8 @@ def _detect_platform(input_data: dict) -> str | None:
|
|
|
128
131
|
return "kiro"
|
|
129
132
|
if ".trae" in script_parts:
|
|
130
133
|
return "trae"
|
|
134
|
+
if ".zcode" in script_parts:
|
|
135
|
+
return "zcode"
|
|
131
136
|
return None
|
|
132
137
|
|
|
133
138
|
|
|
@@ -223,28 +228,50 @@ def _read_trellis_config(root: Path) -> dict:
|
|
|
223
228
|
return {}
|
|
224
229
|
|
|
225
230
|
|
|
226
|
-
def
|
|
227
|
-
"""
|
|
231
|
+
def _resolve_codex_dispatch_mode(config: dict) -> str:
|
|
232
|
+
"""Normalize `codex.dispatch_mode` from .trellis/config.yaml to "auto" or "inline".
|
|
228
233
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
Mode tells AI which dispatch protocol to follow; workflow-state tells
|
|
235
|
-
AI what step it's at.
|
|
234
|
+
Defaults to `auto`. The legacy `sub-agent` value is an alias for `auto`.
|
|
235
|
+
Any other explicit value (including invalid ones) falls back to `inline`
|
|
236
|
+
without per-turn warnings. Shared by `_codex_mode_banner` (the per-turn
|
|
237
|
+
banner) and `resolve_breadcrumb_key` (the breadcrumb tag key) so the two
|
|
238
|
+
stay in lockstep.
|
|
236
239
|
"""
|
|
237
|
-
mode = "
|
|
240
|
+
mode = "auto"
|
|
238
241
|
if isinstance(config, dict):
|
|
239
242
|
codex_cfg = config.get("codex")
|
|
240
243
|
if isinstance(codex_cfg, dict):
|
|
241
|
-
cfg_mode = codex_cfg.get("dispatch_mode")
|
|
242
|
-
if cfg_mode
|
|
243
|
-
mode =
|
|
244
|
-
|
|
244
|
+
cfg_mode = str(codex_cfg.get("dispatch_mode", mode)).strip().lower()
|
|
245
|
+
if cfg_mode == "inline":
|
|
246
|
+
mode = "inline"
|
|
247
|
+
elif cfg_mode in ("auto", "sub-agent"):
|
|
248
|
+
mode = "auto"
|
|
249
|
+
else:
|
|
250
|
+
mode = "inline"
|
|
251
|
+
return mode
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _codex_mode_banner(config: dict) -> str:
|
|
255
|
+
"""Emit a `<codex-mode>` banner for the additionalContext payload.
|
|
256
|
+
|
|
257
|
+
Reads `codex.dispatch_mode` from .trellis/config.yaml; defaults to
|
|
258
|
+
`auto`, which dispatches Trellis sub-agents using native Codex context
|
|
259
|
+
injection with a child-side fallback. This does not rely on inherited
|
|
260
|
+
parent transcripts: `fork_turns` remains caller-controlled, and
|
|
261
|
+
fresh-history sub-agents still receive their explicit delegated task and
|
|
262
|
+
inherited session configuration. `inline` is an explicit opt-out; the
|
|
263
|
+
legacy `sub-agent` value is an alias for `auto`. Invalid explicit values
|
|
264
|
+
fall back to `inline` without per-turn warnings. The banner makes the
|
|
265
|
+
active mode explicit to Codex AI per turn, complementing the workflow-state
|
|
266
|
+
body which is per-status. Mode tells AI which dispatch protocol to follow;
|
|
267
|
+
workflow-state tells AI what step it's at.
|
|
268
|
+
"""
|
|
269
|
+
mode = _resolve_codex_dispatch_mode(config)
|
|
270
|
+
if mode == "auto":
|
|
245
271
|
meaning = (
|
|
246
|
-
"
|
|
247
|
-
"
|
|
272
|
+
"auto: implement/check work defaults to Trellis sub-agents; native Codex "
|
|
273
|
+
"context injection is preferred and child-side loading is the fallback. "
|
|
274
|
+
"The main session still coordinates, clarifies, updates specs, commits, and finishes."
|
|
248
275
|
)
|
|
249
276
|
else:
|
|
250
277
|
meaning = (
|
|
@@ -259,22 +286,17 @@ def resolve_breadcrumb_key(
|
|
|
259
286
|
) -> str:
|
|
260
287
|
"""Pick the breadcrumb tag key based on Codex dispatch_mode.
|
|
261
288
|
|
|
262
|
-
Codex defaults to ``
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
289
|
+
Codex defaults to ``auto`` and therefore uses the ordinary ``<status>``
|
|
290
|
+
breadcrumb for native SubagentStart dispatch with child-side fallback;
|
|
291
|
+
it does not depend on an inherited parent transcript. ``inline`` selects
|
|
292
|
+
the parallel ``<status>-inline`` tag; ``sub-agent`` remains an alias for
|
|
293
|
+
``auto``. Invalid explicit values fall back to inline without per-turn
|
|
294
|
+
warnings.
|
|
267
295
|
|
|
268
296
|
Non-codex platforms return the plain status unchanged.
|
|
269
297
|
"""
|
|
270
298
|
if platform == "codex":
|
|
271
|
-
mode =
|
|
272
|
-
if isinstance(config, dict):
|
|
273
|
-
codex_cfg = config.get("codex")
|
|
274
|
-
if isinstance(codex_cfg, dict):
|
|
275
|
-
cfg_mode = codex_cfg.get("dispatch_mode")
|
|
276
|
-
if cfg_mode in ("inline", "sub-agent"):
|
|
277
|
-
mode = cfg_mode
|
|
299
|
+
mode = _resolve_codex_dispatch_mode(config)
|
|
278
300
|
return f"{status}-inline" if mode == "inline" else status
|
|
279
301
|
return status
|
|
280
302
|
|
|
@@ -68,8 +68,14 @@ def _normalize_windows_shell_path(path_str: str) -> str:
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
FIRST_REPLY_NOTICE = """<first-reply-notice>
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
On the first visible assistant reply in this session, briefly acknowledge that Trellis SessionStart context loaded.
|
|
72
|
+
Choose the acknowledgment language in this order:
|
|
73
|
+
1. Use the language of the user's current request (the user message that triggered this reply).
|
|
74
|
+
2. If that request has no clear natural language, use an explicitly established project communication language.
|
|
75
|
+
3. If neither provides a language, output the language-neutral fallback exactly: `Trellis SessionStart ✓`.
|
|
76
|
+
Continue directly with the user's request after the acknowledgment.
|
|
77
|
+
The acknowledgment must not alter the language used for the remainder of the response.
|
|
78
|
+
This notice is one-shot: do not repeat it after the first visible assistant reply in this session.
|
|
73
79
|
</first-reply-notice>"""
|
|
74
80
|
|
|
75
81
|
# Force UTF-8 on stdin/stdout/stderr on Windows. Default codepage there is
|
|
@@ -138,6 +144,7 @@ def should_skip_injection() -> bool:
|
|
|
138
144
|
"KIRO_NON_INTERACTIVE",
|
|
139
145
|
"COPILOT_NON_INTERACTIVE",
|
|
140
146
|
"TRAE_NON_INTERACTIVE",
|
|
147
|
+
"ZCODE_NON_INTERACTIVE",
|
|
141
148
|
]
|
|
142
149
|
return any(os.environ.get(var) == "1" for var in non_interactive_vars)
|
|
143
150
|
|
|
@@ -188,6 +195,9 @@ def _detect_platform(input_data: dict) -> str | None:
|
|
|
188
195
|
if isinstance(input_data.get("cursor_version"), str):
|
|
189
196
|
return "cursor"
|
|
190
197
|
env_map = {
|
|
198
|
+
# ZCode may set both ZCODE_PROJECT_DIR and CLAUDE_PROJECT_DIR; check
|
|
199
|
+
# ZCODE first so ZCode sessions aren't misdetected as claude.
|
|
200
|
+
"ZCODE_PROJECT_DIR": "zcode",
|
|
191
201
|
"CLAUDE_PROJECT_DIR": "claude",
|
|
192
202
|
"CURSOR_PROJECT_DIR": "cursor",
|
|
193
203
|
"CODEBUDDY_PROJECT_DIR": "codebuddy",
|
|
@@ -220,6 +230,8 @@ def _detect_platform(input_data: dict) -> str | None:
|
|
|
220
230
|
return "kiro"
|
|
221
231
|
if ".trae" in script_parts:
|
|
222
232
|
return "trae"
|
|
233
|
+
if ".zcode" in script_parts:
|
|
234
|
+
return "zcode"
|
|
223
235
|
return None
|
|
224
236
|
|
|
225
237
|
|
|
@@ -744,6 +756,7 @@ def main():
|
|
|
744
756
|
"KIRO_PROJECT_DIR",
|
|
745
757
|
"COPILOT_PROJECT_DIR",
|
|
746
758
|
"TRAE_PROJECT_DIR",
|
|
759
|
+
"ZCODE_PROJECT_DIR",
|
|
747
760
|
]
|
|
748
761
|
project_dir = None
|
|
749
762
|
for var in project_dir_env_vars:
|
|
@@ -826,15 +839,22 @@ Context loaded. Follow <task-status>. Load workflow/spec/task details only when
|
|
|
826
839
|
print(context_text, flush=True)
|
|
827
840
|
return
|
|
828
841
|
|
|
829
|
-
|
|
830
|
-
|
|
842
|
+
platform = _detect_platform(hook_input)
|
|
843
|
+
result: dict[str, object] = {
|
|
844
|
+
# Claude Code / Qoder / CodeBuddy / Droid / Gemini / Copilot / Trae /
|
|
845
|
+
# ZCode format.
|
|
831
846
|
"hookSpecificOutput": {
|
|
832
847
|
"hookEventName": "SessionStart",
|
|
833
848
|
"additionalContext": context_text,
|
|
834
849
|
},
|
|
835
|
-
# Cursor sessionStart format (top-level snake_case per Cursor docs)
|
|
836
|
-
"additional_context": context_text,
|
|
837
850
|
}
|
|
851
|
+
# Cursor sessionStart format (top-level snake_case per Cursor docs).
|
|
852
|
+
# ZCode reads BOTH `hookSpecificOutput.additionalContext` and top-level
|
|
853
|
+
# `additional_context` without deduplication, so emitting both keys would
|
|
854
|
+
# duplicate the context in the conversation. Keep the previous shared output
|
|
855
|
+
# shape for every other platform.
|
|
856
|
+
if platform != "zcode":
|
|
857
|
+
result["additional_context"] = context_text
|
|
838
858
|
|
|
839
859
|
# Output JSON - stdout is already configured for UTF-8
|
|
840
860
|
print(json.dumps(result, ensure_ascii=False), flush=True)
|
|
@@ -99,12 +99,14 @@ channel:
|
|
|
99
99
|
#-------------------------------------------------------------------------------
|
|
100
100
|
# Codex (dispatch behavior)
|
|
101
101
|
#-------------------------------------------------------------------------------
|
|
102
|
-
# Codex-only knob; other platforms ignore it. Default ("
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
102
|
+
# Codex-only knob; other platforms ignore it. Default ("auto") dispatches
|
|
103
|
+
# trellis-implement / trellis-check / trellis-research sub-agents. This does
|
|
104
|
+
# not rely on inherited parent transcripts: `fork_turns` remains
|
|
105
|
+
# caller-controlled, while Codex's native SubagentStart hook injects task
|
|
106
|
+
# context when trusted and child-side loading remains the fallback when it is
|
|
107
|
+
# unavailable. Set to "inline" only to keep implementation and checks in the
|
|
108
|
+
# main session. "sub-agent" remains a backwards-compatible alias for "auto".
|
|
109
|
+
# Invalid explicit values safely use inline mode.
|
|
108
110
|
#
|
|
109
111
|
# codex:
|
|
110
|
-
# dispatch_mode:
|
|
112
|
+
# dispatch_mode: auto # or "inline"; legacy alias: "sub-agent"
|
|
@@ -44,6 +44,9 @@ _KNOWN_PLATFORMS = {
|
|
|
44
44
|
"copilot",
|
|
45
45
|
"pi",
|
|
46
46
|
"trae",
|
|
47
|
+
"grok",
|
|
48
|
+
"kimi",
|
|
49
|
+
"zcode",
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
_ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
@@ -59,6 +62,11 @@ _ENV_SESSION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
|
59
62
|
("copilot", ("COPILOT_SESSION_ID", "COPILOT_SESSIONID")),
|
|
60
63
|
("pi", ("PI_SESSION_ID", "PI_SESSIONID")),
|
|
61
64
|
("trae", ("TRAE_SESSION_ID",)),
|
|
65
|
+
# ZCode reuses CLAUDE_SESSION_ID (it does not document a ZCODE_SESSION_ID).
|
|
66
|
+
# Platform-scoped lookup (_iter_env_keys filters by platform name), so this
|
|
67
|
+
# only fires when the resolver already detected "zcode" — no collision with
|
|
68
|
+
# the claude entry above.
|
|
69
|
+
("zcode", ("CLAUDE_SESSION_ID",)),
|
|
62
70
|
)
|
|
63
71
|
_ENV_CONVERSATION_KEYS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
64
72
|
("cursor", ("CURSOR_CONVERSATION_ID", "CURSOR_CONVERSATIONID")),
|
|
@@ -78,6 +86,12 @@ _ENV_PLATFORM_ALIASES = {
|
|
|
78
86
|
"factory-ai": "droid",
|
|
79
87
|
"github-copilot": "copilot",
|
|
80
88
|
}
|
|
89
|
+
# ZCode intentionally reuses CLAUDE_SESSION_ID. Hooks know the host is ZCode,
|
|
90
|
+
# while later shell commands see only the shared env name and resolve it through
|
|
91
|
+
# the Claude entry. Canonicalize both paths to one runtime filename.
|
|
92
|
+
_CONTEXT_KEY_PLATFORM_ALIASES = {
|
|
93
|
+
"zcode": "claude",
|
|
94
|
+
}
|
|
81
95
|
|
|
82
96
|
|
|
83
97
|
@dataclass(frozen=True)
|
|
@@ -191,6 +205,7 @@ def _detect_platform(platform_input: dict[str, Any] | None, platform: str | None
|
|
|
191
205
|
|
|
192
206
|
|
|
193
207
|
def _context_key(platform_name: str, kind: str, value: str) -> str:
|
|
208
|
+
platform_name = _CONTEXT_KEY_PLATFORM_ALIASES.get(platform_name, platform_name)
|
|
194
209
|
if kind == "transcript":
|
|
195
210
|
return f"{platform_name}_transcript_{_hash_value(value)}"
|
|
196
211
|
safe_value = _sanitize_key(value)
|
|
@@ -382,15 +397,18 @@ def _lookup_cursor_shell_ticket_context_key() -> str | None:
|
|
|
382
397
|
def resolve_context_key(
|
|
383
398
|
platform_input: dict[str, Any] | None = None,
|
|
384
399
|
platform: str | None = None,
|
|
400
|
+
*,
|
|
401
|
+
allow_environment_context: bool = True,
|
|
385
402
|
) -> str | None:
|
|
386
403
|
"""Resolve a stable session/window context key, if one is available.
|
|
387
404
|
|
|
388
405
|
`TRELLIS_CONTEXT_ID` is an explicit context-key override used by CLI
|
|
389
406
|
scripts and subprocesses. It does not store the task itself.
|
|
390
407
|
"""
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
408
|
+
if allow_environment_context:
|
|
409
|
+
override = _string_value(os.environ.get("TRELLIS_CONTEXT_ID"))
|
|
410
|
+
if override:
|
|
411
|
+
return _sanitize_key(override) or _hash_value(override)
|
|
394
412
|
|
|
395
413
|
data = _as_dict(platform_input)
|
|
396
414
|
platform_name = _detect_platform(data, platform) if data or platform else None
|
|
@@ -408,11 +426,12 @@ def resolve_context_key(
|
|
|
408
426
|
if transcript_path:
|
|
409
427
|
return _context_key(platform_name or "session", "transcript", transcript_path)
|
|
410
428
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
429
|
+
if allow_environment_context:
|
|
430
|
+
env_context_key = _lookup_env_context_key(platform_name)
|
|
431
|
+
if env_context_key:
|
|
432
|
+
return env_context_key
|
|
414
433
|
|
|
415
|
-
if platform_name in (None, "session", "cursor"):
|
|
434
|
+
if allow_environment_context and platform_name in (None, "session", "cursor"):
|
|
416
435
|
return _lookup_cursor_shell_ticket_context_key()
|
|
417
436
|
return None
|
|
418
437
|
|
|
@@ -471,17 +490,24 @@ def resolve_active_task(
|
|
|
471
490
|
repo_root: Path,
|
|
472
491
|
platform_input: dict[str, Any] | None = None,
|
|
473
492
|
platform: str | None = None,
|
|
493
|
+
*,
|
|
494
|
+
allow_single_session_fallback: bool = True,
|
|
495
|
+
allow_environment_context: bool = True,
|
|
474
496
|
) -> ActiveTask:
|
|
475
497
|
"""Resolve the active task from session runtime state only.
|
|
476
498
|
|
|
477
499
|
A stale session task is returned as stale. Missing context identity or a
|
|
478
500
|
missing/empty session context falls back to single-session inference: if
|
|
479
501
|
exactly one session file exists in the runtime, return its task with
|
|
480
|
-
source_type="session-fallback" — covers
|
|
481
|
-
copilot, gemini, qoder) that don't inherit the parent's session id. ≥2
|
|
502
|
+
source_type="session-fallback" — covers pull-based platform sub-agents
|
|
503
|
+
(copilot, gemini, qoder) that don't inherit the parent's session id. ≥2
|
|
482
504
|
files or 0 files yield ActiveTask(None) — refuses to guess across windows.
|
|
483
505
|
"""
|
|
484
|
-
context_key = resolve_context_key(
|
|
506
|
+
context_key = resolve_context_key(
|
|
507
|
+
platform_input,
|
|
508
|
+
platform,
|
|
509
|
+
allow_environment_context=allow_environment_context,
|
|
510
|
+
)
|
|
485
511
|
if context_key:
|
|
486
512
|
context = _read_json(_context_path(repo_root, context_key)) or {}
|
|
487
513
|
task_ref = _string_value(context.get("current_task"))
|
|
@@ -489,9 +515,10 @@ def resolve_active_task(
|
|
|
489
515
|
if active:
|
|
490
516
|
return active
|
|
491
517
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
518
|
+
if allow_single_session_fallback:
|
|
519
|
+
fallback = _resolve_single_session_fallback(repo_root)
|
|
520
|
+
if fallback is not None:
|
|
521
|
+
return fallback
|
|
495
522
|
|
|
496
523
|
return ActiveTask(None, "none", context_key)
|
|
497
524
|
|
|
@@ -20,6 +20,9 @@ Supported platforms:
|
|
|
20
20
|
- droid: Factory Droid (commands-based)
|
|
21
21
|
- pi: Pi Agent (extension-backed)
|
|
22
22
|
- trae: Trae IDE (IDE-only, hooks-based)
|
|
23
|
+
- omp: Oh My Pi
|
|
24
|
+
- grok: Grok Build (pull-based skills/agents; no hook context injection)
|
|
25
|
+
- kimi: Kimi Code (pull-based skills; commands delivered as skills; no hook context injection)
|
|
23
26
|
|
|
24
27
|
Usage:
|
|
25
28
|
from common.cli_adapter import CLIAdapter
|
|
@@ -56,6 +59,8 @@ Platform = Literal[
|
|
|
56
59
|
"pi",
|
|
57
60
|
"trae",
|
|
58
61
|
"omp",
|
|
62
|
+
"grok",
|
|
63
|
+
"kimi",
|
|
59
64
|
]
|
|
60
65
|
|
|
61
66
|
|
|
@@ -134,6 +139,10 @@ class CLIAdapter:
|
|
|
134
139
|
return ".trae"
|
|
135
140
|
elif self.platform == "omp":
|
|
136
141
|
return ".omp"
|
|
142
|
+
elif self.platform == "grok":
|
|
143
|
+
return ".grok"
|
|
144
|
+
elif self.platform == "kimi":
|
|
145
|
+
return ".kimi-code"
|
|
137
146
|
else:
|
|
138
147
|
return ".claude"
|
|
139
148
|
|
|
@@ -191,7 +200,8 @@ class CLIAdapter:
|
|
|
191
200
|
filename = filename[:-3]
|
|
192
201
|
return prompts_dir / f"trellis-{filename}.md"
|
|
193
202
|
return prompts_dir / Path(*parts)
|
|
194
|
-
|
|
203
|
+
# OMP and Grok: flat slash commands under .{platform}/commands/trellis-<name>.md
|
|
204
|
+
if self.platform in ("omp", "grok"):
|
|
195
205
|
commands_dir = self.get_config_dir(project_root) / "commands"
|
|
196
206
|
if not parts:
|
|
197
207
|
return commands_dir
|
|
@@ -202,6 +212,17 @@ class CLIAdapter:
|
|
|
202
212
|
return commands_dir / f"trellis-{filename}.md"
|
|
203
213
|
return commands_dir / Path(*parts)
|
|
204
214
|
|
|
215
|
+
# Kimi: commands are skills under .kimi-code/skills/trellis-<name>/SKILL.md
|
|
216
|
+
if self.platform == "kimi":
|
|
217
|
+
skills_dir = self.get_config_dir(project_root) / "skills"
|
|
218
|
+
if not parts:
|
|
219
|
+
return skills_dir
|
|
220
|
+
if len(parts) >= 2 and parts[0] == "trellis":
|
|
221
|
+
filename = parts[-1]
|
|
222
|
+
if filename.endswith(".md"):
|
|
223
|
+
filename = filename[:-3]
|
|
224
|
+
return skills_dir / f"trellis-{filename}" / "SKILL.md"
|
|
225
|
+
return skills_dir / Path(*parts)
|
|
205
226
|
|
|
206
227
|
if self.platform == "devin":
|
|
207
228
|
workflow_dir = self.get_config_dir(project_root) / "workflows"
|
|
@@ -286,8 +307,10 @@ class CLIAdapter:
|
|
|
286
307
|
return f".factory/commands/trellis/{name}.md"
|
|
287
308
|
elif self.platform == "pi":
|
|
288
309
|
return f".pi/prompts/trellis-{name}.md"
|
|
289
|
-
elif self.platform
|
|
290
|
-
return f".
|
|
310
|
+
elif self.platform in ("omp", "grok"):
|
|
311
|
+
return f"{self.config_dir_name}/commands/trellis-{name}.md"
|
|
312
|
+
elif self.platform == "kimi":
|
|
313
|
+
return f".kimi-code/skills/trellis-{name}/SKILL.md"
|
|
291
314
|
else:
|
|
292
315
|
return f"{self.config_dir_name}/commands/trellis/{name}.md"
|
|
293
316
|
|
|
@@ -329,6 +352,10 @@ class CLIAdapter:
|
|
|
329
352
|
return {}
|
|
330
353
|
elif self.platform == "omp":
|
|
331
354
|
return {}
|
|
355
|
+
elif self.platform == "grok":
|
|
356
|
+
return {}
|
|
357
|
+
elif self.platform == "kimi":
|
|
358
|
+
return {}
|
|
332
359
|
else:
|
|
333
360
|
return {"CLAUDE_NON_INTERACTIVE": "1"}
|
|
334
361
|
|
|
@@ -422,6 +449,13 @@ class CLIAdapter:
|
|
|
422
449
|
raise ValueError(
|
|
423
450
|
"OMP uses native task tool for agent runs; CLI agent run is not supported."
|
|
424
451
|
)
|
|
452
|
+
elif self.platform == "grok":
|
|
453
|
+
# Headless single-prompt; sub-agents use in-process spawn_subagent.
|
|
454
|
+
cmd = ["grok", "-p", prompt, "--yolo"]
|
|
455
|
+
elif self.platform == "kimi":
|
|
456
|
+
# Headless single-prompt with auto-approval; sub-agents are the
|
|
457
|
+
# built-in coder/explore/plan agents dispatched in-session.
|
|
458
|
+
cmd = ["kimi", "-p", prompt, "--yolo"]
|
|
425
459
|
|
|
426
460
|
else: # claude
|
|
427
461
|
cmd = ["claude", "-p"]
|
|
@@ -496,6 +530,10 @@ class CLIAdapter:
|
|
|
496
530
|
raise ValueError(
|
|
497
531
|
"OMP uses native task tool for agent runs; CLI resume is not supported."
|
|
498
532
|
)
|
|
533
|
+
elif self.platform == "grok":
|
|
534
|
+
return ["grok", "-c"]
|
|
535
|
+
elif self.platform == "kimi":
|
|
536
|
+
return ["kimi", "--session", session_id]
|
|
499
537
|
else:
|
|
500
538
|
return ["claude", "--resume", session_id]
|
|
501
539
|
|
|
@@ -574,6 +612,10 @@ class CLIAdapter:
|
|
|
574
612
|
return "trae"
|
|
575
613
|
elif self.platform == "omp":
|
|
576
614
|
return "omp"
|
|
615
|
+
elif self.platform == "grok":
|
|
616
|
+
return "grok"
|
|
617
|
+
elif self.platform == "kimi":
|
|
618
|
+
return "kimi"
|
|
577
619
|
else:
|
|
578
620
|
return "claude"
|
|
579
621
|
|
|
@@ -584,7 +626,15 @@ class CLIAdapter:
|
|
|
584
626
|
Claude Code, OpenCode, iFlow, and Codex support CLI agent execution.
|
|
585
627
|
Cursor is IDE-only and doesn't support CLI agents.
|
|
586
628
|
"""
|
|
587
|
-
return self.platform in (
|
|
629
|
+
return self.platform in (
|
|
630
|
+
"claude",
|
|
631
|
+
"opencode",
|
|
632
|
+
"iflow",
|
|
633
|
+
"codex",
|
|
634
|
+
"pi",
|
|
635
|
+
"grok",
|
|
636
|
+
"kimi",
|
|
637
|
+
)
|
|
588
638
|
|
|
589
639
|
@property
|
|
590
640
|
def requires_agent_definition_file(self) -> bool:
|
|
@@ -671,9 +721,11 @@ def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
|
|
|
671
721
|
"pi",
|
|
672
722
|
"trae",
|
|
673
723
|
"omp",
|
|
724
|
+
"grok",
|
|
725
|
+
"kimi",
|
|
674
726
|
):
|
|
675
727
|
raise ValueError(
|
|
676
|
-
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', 'trae', or '
|
|
728
|
+
f"Unsupported platform: {platform} (must be 'claude', 'opencode', 'cursor', 'iflow', 'codex', 'kilo', 'kiro', 'gemini', 'antigravity', 'devin', 'qoder', 'codebuddy', 'copilot', 'droid', 'pi', 'trae', 'omp', 'grok', or 'kimi')"
|
|
677
729
|
)
|
|
678
730
|
|
|
679
731
|
return CLIAdapter(platform=platform) # type: ignore
|
|
@@ -698,6 +750,8 @@ _ALL_PLATFORM_CONFIG_DIRS = (
|
|
|
698
750
|
".pi",
|
|
699
751
|
".trae",
|
|
700
752
|
".omp",
|
|
753
|
+
".grok",
|
|
754
|
+
".kimi-code",
|
|
701
755
|
)
|
|
702
756
|
"""Platform-specific config directory names used by detect_platform exclusion
|
|
703
757
|
checks. `.agents/skills/` is NOT listed here: it is a shared cross-platform
|
|
@@ -768,6 +822,8 @@ def detect_platform(project_root: Path) -> Platform:
|
|
|
768
822
|
"pi",
|
|
769
823
|
"trae",
|
|
770
824
|
"omp",
|
|
825
|
+
"grok",
|
|
826
|
+
"kimi",
|
|
771
827
|
):
|
|
772
828
|
return env_platform # type: ignore
|
|
773
829
|
|
|
@@ -852,6 +908,14 @@ def detect_platform(project_root: Path) -> Platform:
|
|
|
852
908
|
if (project_root / ".omp").is_dir():
|
|
853
909
|
return "omp"
|
|
854
910
|
|
|
911
|
+
# Check for .grok directory (Grok Build-specific)
|
|
912
|
+
if (project_root / ".grok").is_dir():
|
|
913
|
+
return "grok"
|
|
914
|
+
|
|
915
|
+
# Check for .kimi-code directory (Kimi Code-specific)
|
|
916
|
+
if (project_root / ".kimi-code").is_dir():
|
|
917
|
+
return "kimi"
|
|
918
|
+
|
|
855
919
|
# Fallback: checkout only has the Codex shared-skills layer
|
|
856
920
|
# (.agents/skills/trellis-* dirs) and no explicit platform config dir.
|
|
857
921
|
# Happens on fresh clones where .codex/ is gitignored/absent but the
|
|
@@ -167,7 +167,7 @@ def _next_content_line(lines: list[str], start: int) -> tuple[int, str]:
|
|
|
167
167
|
DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal"
|
|
168
168
|
DEFAULT_MAX_JOURNAL_LINES = 2000
|
|
169
169
|
DEFAULT_SESSION_AUTO_COMMIT = True
|
|
170
|
-
DEFAULT_CODEX_DISPATCH_MODE = "
|
|
170
|
+
DEFAULT_CODEX_DISPATCH_MODE = "auto"
|
|
171
171
|
|
|
172
172
|
CONFIG_FILE = "config.yaml"
|
|
173
173
|
|
|
@@ -247,8 +247,14 @@ def get_session_auto_commit(repo_root: Path | None = None) -> bool:
|
|
|
247
247
|
def get_codex_dispatch_mode(repo_root: Path | None = None) -> str:
|
|
248
248
|
"""Return Codex dispatch mode.
|
|
249
249
|
|
|
250
|
-
Default is ``
|
|
251
|
-
|
|
250
|
+
Default is ``auto``, which dispatches Trellis sub-agents and uses native
|
|
251
|
+
context injection with a child-side fallback. ``inline`` is an explicit
|
|
252
|
+
opt-out. ``sub-agent`` remains a backwards-compatible alias for ``auto``.
|
|
253
|
+
|
|
254
|
+
Invalid explicit configuration falls back to ``inline`` rather than
|
|
255
|
+
unexpectedly dispatching a sub-agent. This CLI-facing parser is the only
|
|
256
|
+
place that emits a warning for invalid values; hook readers fail safely
|
|
257
|
+
without producing per-turn warning noise.
|
|
252
258
|
"""
|
|
253
259
|
config = _load_config(repo_root)
|
|
254
260
|
codex = config.get("codex")
|
|
@@ -256,20 +262,22 @@ def get_codex_dispatch_mode(repo_root: Path | None = None) -> str:
|
|
|
256
262
|
return DEFAULT_CODEX_DISPATCH_MODE
|
|
257
263
|
if not isinstance(codex, dict):
|
|
258
264
|
print(
|
|
259
|
-
f"[WARN] invalid codex config: {codex!r}; using
|
|
265
|
+
f"[WARN] invalid codex config: {codex!r}; using inline",
|
|
260
266
|
file=sys.stderr,
|
|
261
267
|
)
|
|
262
|
-
return
|
|
268
|
+
return "inline"
|
|
263
269
|
|
|
264
270
|
raw = codex.get("dispatch_mode", DEFAULT_CODEX_DISPATCH_MODE)
|
|
265
271
|
mode = str(raw).strip().lower()
|
|
266
|
-
if mode in ("
|
|
272
|
+
if mode in ("auto", "inline"):
|
|
267
273
|
return mode
|
|
274
|
+
if mode == "sub-agent":
|
|
275
|
+
return "auto"
|
|
268
276
|
print(
|
|
269
|
-
f"[WARN] invalid codex.dispatch_mode value: {raw!r}; using
|
|
277
|
+
f"[WARN] invalid codex.dispatch_mode value: {raw!r}; using inline",
|
|
270
278
|
file=sys.stderr,
|
|
271
279
|
)
|
|
272
|
-
return
|
|
280
|
+
return "inline"
|
|
273
281
|
|
|
274
282
|
|
|
275
283
|
def get_hooks(event: str, repo_root: Path | None = None) -> list[str]:
|
|
@@ -29,3 +29,39 @@ def run_git(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
|
|
|
29
29
|
return result.returncode, result.stdout, result.stderr
|
|
30
30
|
except Exception as e:
|
|
31
31
|
return 1, "", str(e)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_default_branch(repo_root: Path) -> str | None:
|
|
35
|
+
"""Resolve the repository's default branch (origin/HEAD target).
|
|
36
|
+
|
|
37
|
+
Tries the local `refs/remotes/origin/HEAD` symbolic ref first (no
|
|
38
|
+
network access), then falls back to `git remote show origin` (which
|
|
39
|
+
may hit the network but also repairs a missing/stale symbolic-ref).
|
|
40
|
+
Returns None when neither resolves, so callers can fall back to their
|
|
41
|
+
own pre-existing behavior.
|
|
42
|
+
"""
|
|
43
|
+
rc, out, _ = run_git(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd=repo_root)
|
|
44
|
+
if rc == 0 and out.strip():
|
|
45
|
+
return out.strip().rsplit("/", 1)[-1]
|
|
46
|
+
|
|
47
|
+
rc, out, _ = run_git(["remote", "show", "origin"], cwd=repo_root)
|
|
48
|
+
if rc == 0:
|
|
49
|
+
for line in out.splitlines():
|
|
50
|
+
line = line.strip()
|
|
51
|
+
if line.startswith("HEAD branch:"):
|
|
52
|
+
branch = line.split(":", 1)[1].strip()
|
|
53
|
+
if branch and branch != "(unknown)":
|
|
54
|
+
return branch
|
|
55
|
+
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def branch_exists_locally(branch: str, repo_root: Path) -> bool:
|
|
60
|
+
"""Check whether a local branch ref exists in the repository."""
|
|
61
|
+
if not branch:
|
|
62
|
+
return False
|
|
63
|
+
rc, _, _ = run_git(
|
|
64
|
+
["rev-parse", "--verify", "--quiet", f"refs/heads/{branch}"],
|
|
65
|
+
cwd=repo_root,
|
|
66
|
+
)
|
|
67
|
+
return rc == 0
|
|
@@ -8,6 +8,8 @@ for JSON file operations across all Trellis scripts.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
|
|
13
15
|
|
|
@@ -25,13 +27,35 @@ def read_json(path: Path) -> dict | None:
|
|
|
25
27
|
def write_json(path: Path, data: dict) -> bool:
|
|
26
28
|
"""Write dict to JSON file with pretty formatting.
|
|
27
29
|
|
|
30
|
+
The write is atomic: content goes to a temp file in the same directory
|
|
31
|
+
and is then renamed over the target. A crash or Ctrl-C mid-write leaves
|
|
32
|
+
the existing file intact rather than truncated, so a corrupted task.json
|
|
33
|
+
can never make a task silently vanish from `task.py list`.
|
|
34
|
+
|
|
28
35
|
Returns True on success, False on error.
|
|
29
36
|
"""
|
|
37
|
+
payload = json.dumps(data, indent=2, ensure_ascii=False)
|
|
30
38
|
try:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
encoding="utf-8",
|
|
39
|
+
fd, tmp = tempfile.mkstemp(
|
|
40
|
+
dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp"
|
|
34
41
|
)
|
|
42
|
+
except OSError:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
try:
|
|
47
|
+
f = os.fdopen(fd, "w", encoding="utf-8")
|
|
48
|
+
except OSError:
|
|
49
|
+
# fdopen never took ownership of fd; close it ourselves.
|
|
50
|
+
os.close(fd)
|
|
51
|
+
raise
|
|
52
|
+
with f:
|
|
53
|
+
f.write(payload)
|
|
54
|
+
os.replace(tmp, path)
|
|
35
55
|
return True
|
|
36
|
-
except
|
|
56
|
+
except OSError:
|
|
57
|
+
try:
|
|
58
|
+
os.unlink(tmp)
|
|
59
|
+
except OSError:
|
|
60
|
+
pass
|
|
37
61
|
return False
|