@mindfoldhq/trellis 0.6.7 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +38 -0
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/channel/adapters/codex.d.ts +7 -1
  6. package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
  7. package/dist/commands/channel/adapters/codex.js +19 -2
  8. package/dist/commands/channel/adapters/codex.js.map +1 -1
  9. package/dist/commands/channel/adapters/index.d.ts +3 -1
  10. package/dist/commands/channel/adapters/index.d.ts.map +1 -1
  11. package/dist/commands/channel/adapters/index.js +1 -1
  12. package/dist/commands/channel/adapters/index.js.map +1 -1
  13. package/dist/commands/channel/agent-loader.d.ts +2 -2
  14. package/dist/commands/channel/agent-loader.d.ts.map +1 -1
  15. package/dist/commands/channel/agent-loader.js +9 -5
  16. package/dist/commands/channel/agent-loader.js.map +1 -1
  17. package/dist/commands/channel/context-loader.d.ts +1 -1
  18. package/dist/commands/channel/context-loader.d.ts.map +1 -1
  19. package/dist/commands/channel/context-loader.js +36 -15
  20. package/dist/commands/channel/context-loader.js.map +1 -1
  21. package/dist/commands/channel/context-trust.d.ts +31 -0
  22. package/dist/commands/channel/context-trust.d.ts.map +1 -0
  23. package/dist/commands/channel/context-trust.js +139 -0
  24. package/dist/commands/channel/context-trust.js.map +1 -0
  25. package/dist/commands/channel/index.d.ts.map +1 -1
  26. package/dist/commands/channel/index.js +4 -0
  27. package/dist/commands/channel/index.js.map +1 -1
  28. package/dist/commands/channel/spawn.d.ts +3 -0
  29. package/dist/commands/channel/spawn.d.ts.map +1 -1
  30. package/dist/commands/channel/spawn.js +5 -2
  31. package/dist/commands/channel/spawn.js.map +1 -1
  32. package/dist/commands/channel/supervisor.d.ts +3 -0
  33. package/dist/commands/channel/supervisor.d.ts.map +1 -1
  34. package/dist/commands/channel/supervisor.js +1 -0
  35. package/dist/commands/channel/supervisor.js.map +1 -1
  36. package/dist/commands/init.d.ts +3 -0
  37. package/dist/commands/init.d.ts.map +1 -1
  38. package/dist/commands/init.js.map +1 -1
  39. package/dist/commands/mem.d.ts.map +1 -1
  40. package/dist/commands/mem.js +30 -9
  41. package/dist/commands/mem.js.map +1 -1
  42. package/dist/commands/update.d.ts +12 -1
  43. package/dist/commands/update.d.ts.map +1 -1
  44. package/dist/commands/update.js +75 -7
  45. package/dist/commands/update.js.map +1 -1
  46. package/dist/configurators/codex.d.ts +36 -0
  47. package/dist/configurators/codex.d.ts.map +1 -1
  48. package/dist/configurators/codex.js +111 -13
  49. package/dist/configurators/codex.js.map +1 -1
  50. package/dist/configurators/grok.d.ts +22 -0
  51. package/dist/configurators/grok.d.ts.map +1 -0
  52. package/dist/configurators/grok.js +58 -0
  53. package/dist/configurators/grok.js.map +1 -0
  54. package/dist/configurators/index.d.ts.map +1 -1
  55. package/dist/configurators/index.js +17 -2
  56. package/dist/configurators/index.js.map +1 -1
  57. package/dist/configurators/kimi.d.ts +29 -0
  58. package/dist/configurators/kimi.d.ts.map +1 -0
  59. package/dist/configurators/kimi.js +81 -0
  60. package/dist/configurators/kimi.js.map +1 -0
  61. package/dist/configurators/pi.js +9 -9
  62. package/dist/configurators/pi.js.map +1 -1
  63. package/dist/configurators/shared.d.ts.map +1 -1
  64. package/dist/configurators/shared.js +12 -4
  65. package/dist/configurators/shared.js.map +1 -1
  66. package/dist/configurators/snow.d.ts +31 -0
  67. package/dist/configurators/snow.d.ts.map +1 -0
  68. package/dist/configurators/snow.js +95 -0
  69. package/dist/configurators/snow.js.map +1 -0
  70. package/dist/configurators/workflow.d.ts +14 -0
  71. package/dist/configurators/workflow.d.ts.map +1 -1
  72. package/dist/configurators/workflow.js +37 -1
  73. package/dist/configurators/workflow.js.map +1 -1
  74. package/dist/configurators/zcode.d.ts +9 -4
  75. package/dist/configurators/zcode.d.ts.map +1 -1
  76. package/dist/configurators/zcode.js +41 -12
  77. package/dist/configurators/zcode.js.map +1 -1
  78. package/dist/migrations/manifests/0.5.0-beta.0.json +1 -1
  79. package/dist/migrations/manifests/0.6.8.json +17 -0
  80. package/dist/migrations/manifests/0.6.9.json +9 -0
  81. package/dist/templates/codex/agents/trellis-check.toml +7 -12
  82. package/dist/templates/codex/agents/trellis-implement.toml +7 -12
  83. package/dist/templates/codex/agents/trellis-research.toml +15 -15
  84. package/dist/templates/codex/config.toml +11 -0
  85. package/dist/templates/codex/hooks/session-start.py +9 -3
  86. package/dist/templates/codex/hooks.json +12 -0
  87. package/dist/templates/codex/skills/brainstorm/SKILL.md +40 -13
  88. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +2 -2
  89. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +1 -1
  90. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +1 -0
  91. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +1 -1
  92. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +1 -1
  93. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +10 -4
  94. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +2 -1
  95. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
  96. package/dist/templates/common/skills/brainstorm.md +40 -13
  97. package/dist/templates/copilot/prompts/brainstorm.prompt.md +40 -13
  98. package/dist/templates/grok/agents/trellis-check.md +105 -0
  99. package/dist/templates/grok/agents/trellis-implement.md +116 -0
  100. package/dist/templates/grok/agents/trellis-research.md +146 -0
  101. package/dist/templates/grok/index.d.ts +16 -0
  102. package/dist/templates/grok/index.d.ts.map +1 -0
  103. package/dist/templates/grok/index.js +19 -0
  104. package/dist/templates/grok/index.js.map +1 -0
  105. package/dist/templates/kimi/agents/trellis-check.md +103 -0
  106. package/dist/templates/kimi/agents/trellis-implement.md +114 -0
  107. package/dist/templates/kimi/agents/trellis-research.md +144 -0
  108. package/dist/templates/kimi/index.d.ts +19 -0
  109. package/dist/templates/kimi/index.d.ts.map +1 -0
  110. package/dist/templates/kimi/index.js +22 -0
  111. package/dist/templates/kimi/index.js.map +1 -0
  112. package/dist/templates/omp/extensions/trellis/index.ts.txt +133 -9
  113. package/dist/templates/opencode/lib/session-utils.js +8 -2
  114. package/dist/templates/opencode/lib/trellis-context.js +263 -43
  115. package/dist/templates/opencode/plugins/inject-subagent-context.js +89 -35
  116. package/dist/templates/opencode/plugins/inject-workflow-state.js +90 -5
  117. package/dist/templates/pi/extensions/trellis/index.ts.txt +278 -24
  118. package/dist/templates/pi/settings.json +0 -3
  119. package/dist/templates/shared-hooks/index.d.ts +11 -5
  120. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  121. package/dist/templates/shared-hooks/index.js +16 -5
  122. package/dist/templates/shared-hooks/index.js.map +1 -1
  123. package/dist/templates/shared-hooks/inject-subagent-context.py +466 -104
  124. package/dist/templates/shared-hooks/inject-workflow-state.py +86 -29
  125. package/dist/templates/shared-hooks/session-start.py +26 -6
  126. package/dist/templates/snow/SNOW.md +86 -0
  127. package/dist/templates/snow/agents/trellis-check.md +135 -0
  128. package/dist/templates/snow/agents/trellis-implement.md +153 -0
  129. package/dist/templates/snow/agents/trellis-research.md +114 -0
  130. package/dist/templates/snow/hooks/beforeSubAgentStart.json +15 -0
  131. package/dist/templates/snow/hooks/onSessionStart.json +15 -0
  132. package/dist/templates/snow/hooks/onUserMessage.json +15 -0
  133. package/dist/templates/snow/hooks/write-trellis-context.py +644 -0
  134. package/dist/templates/snow/index.d.ts +26 -0
  135. package/dist/templates/snow/index.d.ts.map +1 -0
  136. package/dist/templates/snow/index.js +36 -0
  137. package/dist/templates/snow/index.js.map +1 -0
  138. package/dist/templates/template-utils.d.ts.map +1 -1
  139. package/dist/templates/template-utils.js +5 -1
  140. package/dist/templates/template-utils.js.map +1 -1
  141. package/dist/templates/trellis/config.yaml +55 -7
  142. package/dist/templates/trellis/gitattributes.txt +9 -0
  143. package/dist/templates/trellis/index.d.ts +3 -1
  144. package/dist/templates/trellis/index.d.ts.map +1 -1
  145. package/dist/templates/trellis/index.js +3 -1
  146. package/dist/templates/trellis/index.js.map +1 -1
  147. package/dist/templates/trellis/scripts/add_session.py +94 -28
  148. package/dist/templates/trellis/scripts/common/active_task.py +44 -13
  149. package/dist/templates/trellis/scripts/common/cli_adapter.py +69 -5
  150. package/dist/templates/trellis/scripts/common/config.py +105 -10
  151. package/dist/templates/trellis/scripts/common/git.py +36 -0
  152. package/dist/templates/trellis/scripts/common/io.py +16 -9
  153. package/dist/templates/trellis/scripts/common/task_context.py +101 -7
  154. package/dist/templates/trellis/scripts/common/task_store.py +149 -22
  155. package/dist/templates/trellis/scripts/common/task_utils.py +3 -1
  156. package/dist/templates/trellis/scripts/common/trellis_config.py +2 -1
  157. package/dist/templates/trellis/scripts/common/workflow_phase.py +19 -12
  158. package/dist/templates/trellis/scripts/task.py +106 -10
  159. package/dist/templates/trellis/workflow.md +20 -19
  160. package/dist/templates/zcode/agents/trellis-check.md +7 -0
  161. package/dist/templates/zcode/agents/trellis-implement.md +13 -4
  162. package/dist/templates/zcode/config.json +42 -0
  163. package/dist/templates/zcode/index.d.ts +10 -1
  164. package/dist/templates/zcode/index.d.ts.map +1 -1
  165. package/dist/templates/zcode/index.js +12 -2
  166. package/dist/templates/zcode/index.js.map +1 -1
  167. package/dist/types/ai-tools.d.ts +4 -4
  168. package/dist/types/ai-tools.d.ts.map +1 -1
  169. package/dist/types/ai-tools.js +114 -2
  170. package/dist/types/ai-tools.js.map +1 -1
  171. package/dist/utils/template-fetcher.d.ts.map +1 -1
  172. package/dist/utils/template-fetcher.js +0 -3
  173. package/dist/utils/template-fetcher.js.map +1 -1
  174. package/package.json +2 -2
@@ -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
@@ -117,11 +117,12 @@ def _parse_yaml_block(
117
117
  key, _, value = stripped.partition(":")
118
118
  key = key.strip()
119
119
  value = _strip_inline_comment(value).strip()
120
+ was_quoted = len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'")
120
121
  value = _unquote(value)
121
122
  current_list = None
122
123
 
123
- if value:
124
- # key: value
124
+ if value or was_quoted:
125
+ # key: value (an explicit quoted "" is a value, not "no value")
125
126
  target[key] = value
126
127
  i += 1
127
128
  else:
@@ -167,7 +168,7 @@ def _next_content_line(lines: list[str], start: int) -> tuple[int, str]:
167
168
  DEFAULT_SESSION_COMMIT_MESSAGE = "chore: record journal"
168
169
  DEFAULT_MAX_JOURNAL_LINES = 2000
169
170
  DEFAULT_SESSION_AUTO_COMMIT = True
170
- DEFAULT_CODEX_DISPATCH_MODE = "inline"
171
+ DEFAULT_CODEX_DISPATCH_MODE = "auto"
171
172
 
172
173
  CONFIG_FILE = "config.yaml"
173
174
 
@@ -247,8 +248,14 @@ def get_session_auto_commit(repo_root: Path | None = None) -> bool:
247
248
  def get_codex_dispatch_mode(repo_root: Path | None = None) -> str:
248
249
  """Return Codex dispatch mode.
249
250
 
250
- Default is ``inline``. ``sub-agent`` is an explicit opt-in because Codex
251
- sub-agents do not inherit the parent session context.
251
+ Default is ``auto``, which dispatches Trellis sub-agents and uses native
252
+ context injection with a child-side fallback. ``inline`` is an explicit
253
+ opt-out. ``sub-agent`` remains a backwards-compatible alias for ``auto``.
254
+
255
+ Invalid explicit configuration falls back to ``inline`` rather than
256
+ unexpectedly dispatching a sub-agent. This CLI-facing parser is the only
257
+ place that emits a warning for invalid values; hook readers fail safely
258
+ without producing per-turn warning noise.
252
259
  """
253
260
  config = _load_config(repo_root)
254
261
  codex = config.get("codex")
@@ -256,20 +263,108 @@ def get_codex_dispatch_mode(repo_root: Path | None = None) -> str:
256
263
  return DEFAULT_CODEX_DISPATCH_MODE
257
264
  if not isinstance(codex, dict):
258
265
  print(
259
- f"[WARN] invalid codex config: {codex!r}; using {DEFAULT_CODEX_DISPATCH_MODE}",
266
+ f"[WARN] invalid codex config: {codex!r}; using inline",
260
267
  file=sys.stderr,
261
268
  )
262
- return DEFAULT_CODEX_DISPATCH_MODE
269
+ return "inline"
263
270
 
264
271
  raw = codex.get("dispatch_mode", DEFAULT_CODEX_DISPATCH_MODE)
265
272
  mode = str(raw).strip().lower()
266
- if mode in ("inline", "sub-agent"):
273
+ if mode in ("auto", "inline"):
267
274
  return mode
275
+ if mode == "sub-agent":
276
+ return "auto"
268
277
  print(
269
- f"[WARN] invalid codex.dispatch_mode value: {raw!r}; using {DEFAULT_CODEX_DISPATCH_MODE}",
278
+ f"[WARN] invalid codex.dispatch_mode value: {raw!r}; using inline",
270
279
  file=sys.stderr,
271
280
  )
272
- return DEFAULT_CODEX_DISPATCH_MODE
281
+ return "inline"
282
+
283
+
284
+ DEFAULT_CONTEXT_INJECTION_MAX_FILE_BYTES = 32768
285
+ DEFAULT_CONTEXT_INJECTION_MAX_ARTIFACT_BYTES = 65536
286
+ DEFAULT_CONTEXT_INJECTION_MAX_TOTAL_BYTES = 131072
287
+
288
+
289
+ def get_context_injection_limits(repo_root: Path | None = None) -> dict[str, int]:
290
+ """Return sub-agent context injection byte limits.
291
+
292
+ Reads the ``context_injection:`` section of ``.trellis/config.yaml``:
293
+
294
+ context_injection:
295
+ max_file_bytes: 32768
296
+ max_artifact_bytes: 65536
297
+ max_total_bytes: 131072
298
+
299
+ ``0`` disables the corresponding limit. Missing keys use their default;
300
+ invalid (non-int or negative) values fall back to the default for that
301
+ key with a stderr warning.
302
+ """
303
+ defaults = {
304
+ "max_file_bytes": DEFAULT_CONTEXT_INJECTION_MAX_FILE_BYTES,
305
+ "max_artifact_bytes": DEFAULT_CONTEXT_INJECTION_MAX_ARTIFACT_BYTES,
306
+ "max_total_bytes": DEFAULT_CONTEXT_INJECTION_MAX_TOTAL_BYTES,
307
+ }
308
+
309
+ config = _load_config(repo_root)
310
+ section = config.get("context_injection")
311
+ if not isinstance(section, dict):
312
+ return defaults
313
+
314
+ result = dict(defaults)
315
+ for key, default_value in defaults.items():
316
+ if key not in section:
317
+ continue
318
+ raw = section[key]
319
+ try:
320
+ value = int(raw)
321
+ except (TypeError, ValueError):
322
+ print(
323
+ f"[WARN] invalid context_injection.{key} value: {raw!r}; "
324
+ f"using default {default_value}",
325
+ file=sys.stderr,
326
+ )
327
+ continue
328
+ if value < 0:
329
+ print(
330
+ f"[WARN] invalid context_injection.{key} value: {raw!r}; "
331
+ f"using default {default_value}",
332
+ file=sys.stderr,
333
+ )
334
+ continue
335
+ result[key] = value
336
+
337
+ return result
338
+
339
+
340
+ DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD = "no-trellis"
341
+
342
+
343
+ def get_prompt_injection_config(repo_root: Path | None = None) -> dict[str, str]:
344
+ """Return per-turn prompt injection config.
345
+
346
+ Reads the ``prompt_injection:`` section of ``.trellis/config.yaml``:
347
+
348
+ prompt_injection:
349
+ skip_keyword: "no-trellis" # "" disables the escape hatch entirely
350
+
351
+ ``skip_keyword`` is the word-boundary, case-insensitive keyword that, when
352
+ present in the user's prompt, makes the per-turn workflow-state injection
353
+ emit nothing for that turn. Defaults to ``"no-trellis"``. A non-string
354
+ value falls back to the default.
355
+ """
356
+ defaults = {"skip_keyword": DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD}
357
+
358
+ config = _load_config(repo_root)
359
+ section = config.get("prompt_injection")
360
+ if not isinstance(section, dict):
361
+ return defaults
362
+
363
+ result = dict(defaults)
364
+ raw = section.get("skip_keyword", DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD)
365
+ if isinstance(raw, str):
366
+ result["skip_keyword"] = raw
367
+ return result
273
368
 
274
369
 
275
370
  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
@@ -39,16 +39,23 @@ def write_json(path: Path, data: dict) -> bool:
39
39
  fd, tmp = tempfile.mkstemp(
40
40
  dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp"
41
41
  )
42
+ except OSError:
43
+ return False
44
+
45
+ try:
42
46
  try:
43
- with os.fdopen(fd, "w", encoding="utf-8") as f:
44
- f.write(payload)
45
- os.replace(tmp, path)
46
- except BaseException:
47
- try:
48
- os.unlink(tmp)
49
- except OSError:
50
- pass
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
51
  raise
52
+ with f:
53
+ f.write(payload)
54
+ os.replace(tmp, path)
52
55
  return True
53
- except (OSError, IOError):
56
+ except OSError:
57
+ try:
58
+ os.unlink(tmp)
59
+ except OSError:
60
+ pass
54
61
  return False
@@ -21,10 +21,36 @@ import argparse
21
21
  import json
22
22
  from pathlib import Path
23
23
 
24
+ from .config import get_context_injection_limits
25
+ from .git import branch_exists_locally
26
+ from .io import read_json
24
27
  from .log import Colors, colored
25
- from .paths import get_repo_root
28
+ from .paths import FILE_TASK_JSON, get_repo_root
26
29
  from .task_utils import resolve_task_dir
27
30
 
31
+ # Extensions that look like code rather than spec/research docs. Entries with
32
+ # one of these extensions outside .trellis/spec/, docs/docs-site, or the
33
+ # task's own directory get a hygiene warning in `task.py validate` — the
34
+ # reader is a sub-agent, not a human, so code paths belong in the diff the
35
+ # agent reads itself, not in implement.jsonl / check.jsonl.
36
+ _CODE_FILE_EXTENSIONS = {
37
+ ".ts",
38
+ ".tsx",
39
+ ".js",
40
+ ".jsx",
41
+ ".mjs",
42
+ ".cjs",
43
+ ".py",
44
+ ".go",
45
+ ".rs",
46
+ ".java",
47
+ ".rb",
48
+ ".c",
49
+ ".cc",
50
+ ".cpp",
51
+ ".h",
52
+ }
53
+
28
54
 
29
55
  # =============================================================================
30
56
  # Command: add-context
@@ -97,10 +123,26 @@ def cmd_validate(args: argparse.Namespace) -> int:
97
123
  print(f"Target dir: {target_dir}")
98
124
  print()
99
125
 
126
+ # Warn (don't fail validation) when the recorded branch is stale — it
127
+ # was likely already merged and deleted (#399 item 2).
128
+ task_json_path = target_dir / FILE_TASK_JSON
129
+ if task_json_path.is_file():
130
+ task_data = read_json(task_json_path)
131
+ stored_branch = task_data.get("branch") if task_data else None
132
+ if stored_branch and not branch_exists_locally(stored_branch, repo_root):
133
+ print(
134
+ colored(
135
+ f"Warning: recorded branch '{stored_branch}' no longer exists locally "
136
+ "(likely merged and deleted).",
137
+ Colors.YELLOW,
138
+ )
139
+ )
140
+ print()
141
+
100
142
  total_errors = 0
101
143
  for jsonl_name in ["implement.jsonl", "check.jsonl"]:
102
144
  jsonl_file = target_dir / jsonl_name
103
- errors = _validate_jsonl(jsonl_file, repo_root)
145
+ errors = _validate_jsonl(jsonl_file, repo_root, target_dir)
104
146
  total_errors += errors
105
147
 
106
148
  print()
@@ -112,11 +154,33 @@ def cmd_validate(args: argparse.Namespace) -> int:
112
154
  return 1
113
155
 
114
156
 
115
- def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
157
+ def _is_exempt_from_code_file_warning(file_path: str, task_rel: str) -> bool:
158
+ """Whether a jsonl entry path is exempt from the code-file hygiene warning.
159
+
160
+ Exempt: spec docs (``.trellis/spec/``), documentation (``docs``,
161
+ ``docs-site``), and the task's own directory (execution plans, generated
162
+ artifacts, etc. legitimately live there).
163
+ """
164
+ posix_path = file_path.replace("\\", "/").lstrip("/")
165
+ exempt_prefixes = (".trellis/spec/", "docs/", "docs-site/")
166
+ if posix_path.startswith(exempt_prefixes):
167
+ return True
168
+ if task_rel and (posix_path == task_rel or posix_path.startswith(f"{task_rel}/")):
169
+ return True
170
+ return False
171
+
172
+
173
+ def _validate_jsonl(jsonl_file: Path, repo_root: Path, task_dir: Path | None = None) -> int:
116
174
  """Validate a single JSONL file.
117
175
 
118
176
  Seed rows (no ``file`` field — typically ``{"_example": "..."}``) are
119
177
  skipped silently; they are self-describing comments, not real entries.
178
+
179
+ Beyond hard errors (missing file/dir, invalid JSON), this also prints
180
+ non-blocking hygiene warnings (never counted in ``errors``, never change
181
+ the exit code): entries that look like code files rather than
182
+ spec/research docs, and entries whose file size exceeds the configured
183
+ sub-agent context injection cap (``context_injection.max_file_bytes``).
120
184
  """
121
185
  file_name = jsonl_file.name
122
186
  errors = 0
@@ -125,6 +189,15 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
125
189
  print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}")
126
190
  return 0
127
191
 
192
+ task_rel = ""
193
+ if task_dir is not None:
194
+ try:
195
+ task_rel = task_dir.resolve().relative_to(repo_root.resolve()).as_posix()
196
+ except ValueError:
197
+ task_rel = ""
198
+
199
+ max_file_bytes = get_context_injection_limits(repo_root).get("max_file_bytes", 0)
200
+
128
201
  line_num = 0
129
202
  real_entries = 0
130
203
  for line in jsonl_file.read_text(encoding="utf-8").splitlines():
@@ -152,10 +225,31 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
152
225
  if not full_path.is_dir():
153
226
  print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}")
154
227
  errors += 1
155
- else:
156
- if not full_path.is_file():
157
- print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
158
- errors += 1
228
+ continue
229
+
230
+ if not full_path.is_file():
231
+ print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
232
+ errors += 1
233
+ continue
234
+
235
+ extension = Path(file_path).suffix.lower()
236
+ if extension in _CODE_FILE_EXTENSIONS and not _is_exempt_from_code_file_warning(
237
+ file_path, task_rel
238
+ ):
239
+ print(
240
+ f" {colored(f'{file_name}:{line_num}: Warning: {file_path} looks like a code file — '
241
+ 'implement/check.jsonl should reference spec/research docs; '
242
+ 'agents read code themselves', Colors.YELLOW)}"
243
+ )
244
+
245
+ if max_file_bytes:
246
+ size = full_path.stat().st_size
247
+ if size > max_file_bytes:
248
+ print(
249
+ f" {colored(f'{file_name}:{line_num}: Warning: {file_path} is {size} bytes, '
250
+ f'exceeds context_injection.max_file_bytes ({max_file_bytes}); '
251
+ 'injection will truncate it', Colors.YELLOW)}"
252
+ )
159
253
 
160
254
  if errors == 0:
161
255
  print(f" {colored(f'{file_name}: ✓ ({real_entries} entries)', Colors.GREEN)}")