@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.
Files changed (152) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +37 -0
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/channel/adapters/codex.d.ts +7 -1
  6. package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
  7. package/dist/commands/channel/adapters/codex.js +19 -2
  8. package/dist/commands/channel/adapters/codex.js.map +1 -1
  9. package/dist/commands/channel/adapters/index.d.ts +3 -1
  10. package/dist/commands/channel/adapters/index.d.ts.map +1 -1
  11. package/dist/commands/channel/adapters/index.js +1 -1
  12. package/dist/commands/channel/adapters/index.js.map +1 -1
  13. package/dist/commands/channel/guard.d.ts.map +1 -1
  14. package/dist/commands/channel/guard.js +7 -2
  15. package/dist/commands/channel/guard.js.map +1 -1
  16. package/dist/commands/channel/index.d.ts.map +1 -1
  17. package/dist/commands/channel/index.js +4 -0
  18. package/dist/commands/channel/index.js.map +1 -1
  19. package/dist/commands/channel/spawn.d.ts +3 -0
  20. package/dist/commands/channel/spawn.d.ts.map +1 -1
  21. package/dist/commands/channel/spawn.js +1 -0
  22. package/dist/commands/channel/spawn.js.map +1 -1
  23. package/dist/commands/channel/store/paths.d.ts +14 -0
  24. package/dist/commands/channel/store/paths.d.ts.map +1 -1
  25. package/dist/commands/channel/store/paths.js +26 -0
  26. package/dist/commands/channel/store/paths.js.map +1 -1
  27. package/dist/commands/channel/supervisor.d.ts +3 -0
  28. package/dist/commands/channel/supervisor.d.ts.map +1 -1
  29. package/dist/commands/channel/supervisor.js +1 -0
  30. package/dist/commands/channel/supervisor.js.map +1 -1
  31. package/dist/commands/init.d.ts +2 -0
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js.map +1 -1
  34. package/dist/commands/mem.d.ts.map +1 -1
  35. package/dist/commands/mem.js +30 -9
  36. package/dist/commands/mem.js.map +1 -1
  37. package/dist/commands/uninstall.d.ts +9 -0
  38. package/dist/commands/uninstall.d.ts.map +1 -1
  39. package/dist/commands/uninstall.js +67 -3
  40. package/dist/commands/uninstall.js.map +1 -1
  41. package/dist/commands/update.d.ts +40 -1
  42. package/dist/commands/update.d.ts.map +1 -1
  43. package/dist/commands/update.js +126 -34
  44. package/dist/commands/update.js.map +1 -1
  45. package/dist/configurators/codex.d.ts.map +1 -1
  46. package/dist/configurators/codex.js +11 -12
  47. package/dist/configurators/codex.js.map +1 -1
  48. package/dist/configurators/grok.d.ts +22 -0
  49. package/dist/configurators/grok.d.ts.map +1 -0
  50. package/dist/configurators/grok.js +58 -0
  51. package/dist/configurators/grok.js.map +1 -0
  52. package/dist/configurators/index.d.ts.map +1 -1
  53. package/dist/configurators/index.js +12 -2
  54. package/dist/configurators/index.js.map +1 -1
  55. package/dist/configurators/kimi.d.ts +29 -0
  56. package/dist/configurators/kimi.d.ts.map +1 -0
  57. package/dist/configurators/kimi.js +81 -0
  58. package/dist/configurators/kimi.js.map +1 -0
  59. package/dist/configurators/pi.js +9 -9
  60. package/dist/configurators/pi.js.map +1 -1
  61. package/dist/configurators/shared.d.ts.map +1 -1
  62. package/dist/configurators/shared.js +11 -4
  63. package/dist/configurators/shared.js.map +1 -1
  64. package/dist/configurators/zcode.d.ts +9 -4
  65. package/dist/configurators/zcode.d.ts.map +1 -1
  66. package/dist/configurators/zcode.js +41 -12
  67. package/dist/configurators/zcode.js.map +1 -1
  68. package/dist/migrations/manifests/0.5.0-beta.0.json +1 -1
  69. package/dist/migrations/manifests/0.6.7.json +9 -0
  70. package/dist/migrations/manifests/0.6.8.json +17 -0
  71. package/dist/templates/codex/agents/trellis-check.toml +5 -12
  72. package/dist/templates/codex/agents/trellis-implement.toml +5 -12
  73. package/dist/templates/codex/agents/trellis-research.toml +13 -15
  74. package/dist/templates/codex/config.toml +11 -0
  75. package/dist/templates/codex/hooks/session-start.py +9 -3
  76. package/dist/templates/codex/hooks.json +12 -0
  77. package/dist/templates/codex/skills/brainstorm/SKILL.md +40 -13
  78. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +2 -2
  79. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +1 -1
  80. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +1 -0
  81. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +1 -1
  82. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +1 -1
  83. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +8 -4
  84. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +2 -1
  85. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
  86. package/dist/templates/common/skills/brainstorm.md +40 -13
  87. package/dist/templates/copilot/prompts/brainstorm.prompt.md +40 -13
  88. package/dist/templates/grok/agents/trellis-check.md +105 -0
  89. package/dist/templates/grok/agents/trellis-implement.md +116 -0
  90. package/dist/templates/grok/agents/trellis-research.md +146 -0
  91. package/dist/templates/grok/index.d.ts +16 -0
  92. package/dist/templates/grok/index.d.ts.map +1 -0
  93. package/dist/templates/grok/index.js +19 -0
  94. package/dist/templates/grok/index.js.map +1 -0
  95. package/dist/templates/kimi/agents/trellis-check.md +103 -0
  96. package/dist/templates/kimi/agents/trellis-implement.md +114 -0
  97. package/dist/templates/kimi/agents/trellis-research.md +140 -0
  98. package/dist/templates/kimi/index.d.ts +19 -0
  99. package/dist/templates/kimi/index.d.ts.map +1 -0
  100. package/dist/templates/kimi/index.js +22 -0
  101. package/dist/templates/kimi/index.js.map +1 -0
  102. package/dist/templates/omp/extensions/trellis/index.ts.txt +19 -5
  103. package/dist/templates/opencode/lib/session-utils.js +8 -2
  104. package/dist/templates/pi/extensions/trellis/index.ts.txt +8 -2
  105. package/dist/templates/pi/settings.json +0 -3
  106. package/dist/templates/shared-hooks/index.d.ts +11 -5
  107. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  108. package/dist/templates/shared-hooks/index.js +16 -5
  109. package/dist/templates/shared-hooks/index.js.map +1 -1
  110. package/dist/templates/shared-hooks/inject-subagent-context.py +151 -17
  111. package/dist/templates/shared-hooks/inject-workflow-state.py +50 -28
  112. package/dist/templates/shared-hooks/session-start.py +26 -6
  113. package/dist/templates/trellis/config.yaml +9 -7
  114. package/dist/templates/trellis/scripts/common/active_task.py +40 -13
  115. package/dist/templates/trellis/scripts/common/cli_adapter.py +69 -5
  116. package/dist/templates/trellis/scripts/common/config.py +16 -8
  117. package/dist/templates/trellis/scripts/common/git.py +36 -0
  118. package/dist/templates/trellis/scripts/common/io.py +28 -4
  119. package/dist/templates/trellis/scripts/common/task_context.py +19 -1
  120. package/dist/templates/trellis/scripts/common/task_store.py +98 -21
  121. package/dist/templates/trellis/scripts/common/task_utils.py +24 -0
  122. package/dist/templates/trellis/scripts/common/workflow_phase.py +19 -12
  123. package/dist/templates/trellis/scripts/task.py +85 -8
  124. package/dist/templates/trellis/workflow.md +20 -19
  125. package/dist/templates/zcode/agents/trellis-check.md +7 -0
  126. package/dist/templates/zcode/agents/trellis-implement.md +13 -4
  127. package/dist/templates/zcode/config.json +42 -0
  128. package/dist/templates/zcode/index.d.ts +10 -1
  129. package/dist/templates/zcode/index.d.ts.map +1 -1
  130. package/dist/templates/zcode/index.js +12 -2
  131. package/dist/templates/zcode/index.js.map +1 -1
  132. package/dist/types/ai-tools.d.ts +4 -4
  133. package/dist/types/ai-tools.d.ts.map +1 -1
  134. package/dist/types/ai-tools.js +76 -2
  135. package/dist/types/ai-tools.js.map +1 -1
  136. package/dist/utils/atomic-write.d.ts +12 -0
  137. package/dist/utils/atomic-write.d.ts.map +1 -0
  138. package/dist/utils/atomic-write.js +30 -0
  139. package/dist/utils/atomic-write.js.map +1 -0
  140. package/dist/utils/file-writer.d.ts.map +1 -1
  141. package/dist/utils/file-writer.js +6 -5
  142. package/dist/utils/file-writer.js.map +1 -1
  143. package/dist/utils/registry-config.d.ts.map +1 -1
  144. package/dist/utils/registry-config.js +3 -2
  145. package/dist/utils/registry-config.js.map +1 -1
  146. package/dist/utils/template-fetcher.d.ts.map +1 -1
  147. package/dist/utils/template-fetcher.js +35 -6
  148. package/dist/utils/template-fetcher.js.map +1 -1
  149. package/dist/utils/template-hash.d.ts.map +1 -1
  150. package/dist/utils/template-hash.js +2 -1
  151. package/dist/utils/template-hash.js.map +1 -1
  152. 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 _codex_mode_banner(config: dict) -> str:
227
- """Emit a `<codex-mode>` banner for the additionalContext payload.
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
- Reads `codex.dispatch_mode` from .trellis/config.yaml; defaults to
230
- `inline` when missing or invalid because Codex sub-agents run with
231
- `fork_turns="none"` isolation and can't inherit the parent session's
232
- task context. The banner makes the active mode explicit to Codex AI
233
- per turn, complementing the workflow-state body which is per-status.
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 = "inline"
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 in ("inline", "sub-agent"):
243
- mode = cfg_mode
244
- if mode == "sub-agent":
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
- "sub-agent: implement/check work defaults to Trellis sub-agents; "
247
- "the main session still coordinates, clarifies, updates specs, commits, and finishes."
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 ``inline`` because sub-agents run with ``fork_turns="none"``
263
- isolation and can't inherit the parent session's task context. Users can
264
- opt into ``codex.dispatch_mode: sub-agent`` in ``.trellis/config.yaml``
265
- to use the parallel ``<status>-inline`` tag ``<status>`` flip. Invalid
266
- or missing values fall back to inline.
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 = "inline"
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
- First visible reply: say once in Chinese that Trellis SessionStart context is loaded, then answer directly.
72
- This notice is one-shot: do not repeat it after the first assistant reply in the same session.
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
- result = {
830
- # Claude Code / Qoder / CodeBuddy / Droid / Gemini / Copilot format
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 ("inline") makes the
103
- # main Codex agent edit code directly because Codex sub-agents run with
104
- # `fork_turns="none"` isolation and can't inherit the parent session's
105
- # task context. Set to "sub-agent" to opt into the legacy dispatch model
106
- # (main agent spawns trellis-implement / trellis-check / trellis-research
107
- # sub-agents).
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: inline # or "sub-agent" to dispatch trellis-* sub-agents
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
- override = _string_value(os.environ.get("TRELLIS_CONTEXT_ID"))
392
- if override:
393
- return _sanitize_key(override) or _hash_value(override)
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
- env_context_key = _lookup_env_context_key(platform_name)
412
- if env_context_key:
413
- return env_context_key
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 class-2 platform sub-agents (codex,
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(platform_input, platform)
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
- fallback = _resolve_single_session_fallback(repo_root)
493
- if fallback is not None:
494
- return fallback
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
- if self.platform == "omp":
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 == "omp":
290
- return f".omp/commands/trellis-{name}.md"
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 ("claude", "opencode", "iflow", "codex", "pi")
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 'omp')"
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 = "inline"
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 ``inline``. ``sub-agent`` is an explicit opt-in because Codex
251
- sub-agents do not inherit the parent session context.
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 {DEFAULT_CODEX_DISPATCH_MODE}",
265
+ f"[WARN] invalid codex config: {codex!r}; using inline",
260
266
  file=sys.stderr,
261
267
  )
262
- return DEFAULT_CODEX_DISPATCH_MODE
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 ("inline", "sub-agent"):
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 {DEFAULT_CODEX_DISPATCH_MODE}",
277
+ f"[WARN] invalid codex.dispatch_mode value: {raw!r}; using inline",
270
278
  file=sys.stderr,
271
279
  )
272
- return DEFAULT_CODEX_DISPATCH_MODE
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
- path.write_text(
32
- json.dumps(data, indent=2, ensure_ascii=False),
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 (OSError, IOError):
56
+ except OSError:
57
+ try:
58
+ os.unlink(tmp)
59
+ except OSError:
60
+ pass
37
61
  return False