@mindfoldhq/trellis 0.6.7 → 0.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +38 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/channel/adapters/codex.d.ts +7 -1
- package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
- package/dist/commands/channel/adapters/codex.js +19 -2
- package/dist/commands/channel/adapters/codex.js.map +1 -1
- package/dist/commands/channel/adapters/index.d.ts +3 -1
- package/dist/commands/channel/adapters/index.d.ts.map +1 -1
- package/dist/commands/channel/adapters/index.js +1 -1
- package/dist/commands/channel/adapters/index.js.map +1 -1
- package/dist/commands/channel/agent-loader.d.ts +2 -2
- package/dist/commands/channel/agent-loader.d.ts.map +1 -1
- package/dist/commands/channel/agent-loader.js +9 -5
- package/dist/commands/channel/agent-loader.js.map +1 -1
- package/dist/commands/channel/context-loader.d.ts +1 -1
- package/dist/commands/channel/context-loader.d.ts.map +1 -1
- package/dist/commands/channel/context-loader.js +36 -15
- package/dist/commands/channel/context-loader.js.map +1 -1
- package/dist/commands/channel/context-trust.d.ts +31 -0
- package/dist/commands/channel/context-trust.d.ts.map +1 -0
- package/dist/commands/channel/context-trust.js +139 -0
- package/dist/commands/channel/context-trust.js.map +1 -0
- package/dist/commands/channel/index.d.ts.map +1 -1
- package/dist/commands/channel/index.js +4 -0
- package/dist/commands/channel/index.js.map +1 -1
- package/dist/commands/channel/spawn.d.ts +3 -0
- package/dist/commands/channel/spawn.d.ts.map +1 -1
- package/dist/commands/channel/spawn.js +5 -2
- package/dist/commands/channel/spawn.js.map +1 -1
- package/dist/commands/channel/supervisor.d.ts +3 -0
- package/dist/commands/channel/supervisor.d.ts.map +1 -1
- package/dist/commands/channel/supervisor.js +1 -0
- package/dist/commands/channel/supervisor.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mem.d.ts.map +1 -1
- package/dist/commands/mem.js +30 -9
- package/dist/commands/mem.js.map +1 -1
- package/dist/commands/update.d.ts +12 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +75 -7
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/codex.d.ts +36 -0
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +111 -13
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/grok.d.ts +22 -0
- package/dist/configurators/grok.d.ts.map +1 -0
- package/dist/configurators/grok.js +58 -0
- package/dist/configurators/grok.js.map +1 -0
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +17 -2
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/kimi.d.ts +29 -0
- package/dist/configurators/kimi.d.ts.map +1 -0
- package/dist/configurators/kimi.js +81 -0
- package/dist/configurators/kimi.js.map +1 -0
- package/dist/configurators/pi.js +9 -9
- package/dist/configurators/pi.js.map +1 -1
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +12 -4
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/snow.d.ts +31 -0
- package/dist/configurators/snow.d.ts.map +1 -0
- package/dist/configurators/snow.js +95 -0
- package/dist/configurators/snow.js.map +1 -0
- package/dist/configurators/workflow.d.ts +14 -0
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +37 -1
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/configurators/zcode.d.ts +9 -4
- package/dist/configurators/zcode.d.ts.map +1 -1
- package/dist/configurators/zcode.js +41 -12
- package/dist/configurators/zcode.js.map +1 -1
- package/dist/migrations/manifests/0.5.0-beta.0.json +1 -1
- package/dist/migrations/manifests/0.6.8.json +17 -0
- package/dist/migrations/manifests/0.6.9.json +9 -0
- package/dist/templates/codex/agents/trellis-check.toml +7 -12
- package/dist/templates/codex/agents/trellis-implement.toml +7 -12
- package/dist/templates/codex/agents/trellis-research.toml +15 -15
- package/dist/templates/codex/config.toml +11 -0
- package/dist/templates/codex/hooks/session-start.py +9 -3
- package/dist/templates/codex/hooks.json +12 -0
- package/dist/templates/codex/skills/brainstorm/SKILL.md +40 -13
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +1 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +10 -4
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +2 -1
- package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
- package/dist/templates/common/skills/brainstorm.md +40 -13
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +40 -13
- package/dist/templates/grok/agents/trellis-check.md +105 -0
- package/dist/templates/grok/agents/trellis-implement.md +116 -0
- package/dist/templates/grok/agents/trellis-research.md +146 -0
- package/dist/templates/grok/index.d.ts +16 -0
- package/dist/templates/grok/index.d.ts.map +1 -0
- package/dist/templates/grok/index.js +19 -0
- package/dist/templates/grok/index.js.map +1 -0
- package/dist/templates/kimi/agents/trellis-check.md +103 -0
- package/dist/templates/kimi/agents/trellis-implement.md +114 -0
- package/dist/templates/kimi/agents/trellis-research.md +144 -0
- package/dist/templates/kimi/index.d.ts +19 -0
- package/dist/templates/kimi/index.d.ts.map +1 -0
- package/dist/templates/kimi/index.js +22 -0
- package/dist/templates/kimi/index.js.map +1 -0
- package/dist/templates/omp/extensions/trellis/index.ts.txt +133 -9
- package/dist/templates/opencode/lib/session-utils.js +8 -2
- package/dist/templates/opencode/lib/trellis-context.js +263 -43
- package/dist/templates/opencode/plugins/inject-subagent-context.js +89 -35
- package/dist/templates/opencode/plugins/inject-workflow-state.js +90 -5
- package/dist/templates/pi/extensions/trellis/index.ts.txt +278 -24
- package/dist/templates/pi/settings.json +0 -3
- package/dist/templates/shared-hooks/index.d.ts +11 -5
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +16 -5
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-subagent-context.py +466 -104
- package/dist/templates/shared-hooks/inject-workflow-state.py +86 -29
- package/dist/templates/shared-hooks/session-start.py +26 -6
- package/dist/templates/snow/SNOW.md +86 -0
- package/dist/templates/snow/agents/trellis-check.md +135 -0
- package/dist/templates/snow/agents/trellis-implement.md +153 -0
- package/dist/templates/snow/agents/trellis-research.md +114 -0
- package/dist/templates/snow/hooks/beforeSubAgentStart.json +15 -0
- package/dist/templates/snow/hooks/onSessionStart.json +15 -0
- package/dist/templates/snow/hooks/onUserMessage.json +15 -0
- package/dist/templates/snow/hooks/write-trellis-context.py +644 -0
- package/dist/templates/snow/index.d.ts +26 -0
- package/dist/templates/snow/index.d.ts.map +1 -0
- package/dist/templates/snow/index.js +36 -0
- package/dist/templates/snow/index.js.map +1 -0
- package/dist/templates/template-utils.d.ts.map +1 -1
- package/dist/templates/template-utils.js +5 -1
- package/dist/templates/template-utils.js.map +1 -1
- package/dist/templates/trellis/config.yaml +55 -7
- package/dist/templates/trellis/gitattributes.txt +9 -0
- package/dist/templates/trellis/index.d.ts +3 -1
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +3 -1
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +94 -28
- package/dist/templates/trellis/scripts/common/active_task.py +44 -13
- package/dist/templates/trellis/scripts/common/cli_adapter.py +69 -5
- package/dist/templates/trellis/scripts/common/config.py +105 -10
- package/dist/templates/trellis/scripts/common/git.py +36 -0
- package/dist/templates/trellis/scripts/common/io.py +16 -9
- package/dist/templates/trellis/scripts/common/task_context.py +101 -7
- package/dist/templates/trellis/scripts/common/task_store.py +149 -22
- package/dist/templates/trellis/scripts/common/task_utils.py +3 -1
- package/dist/templates/trellis/scripts/common/trellis_config.py +2 -1
- package/dist/templates/trellis/scripts/common/workflow_phase.py +19 -12
- package/dist/templates/trellis/scripts/task.py +106 -10
- package/dist/templates/trellis/workflow.md +20 -19
- package/dist/templates/zcode/agents/trellis-check.md +7 -0
- package/dist/templates/zcode/agents/trellis-implement.md +13 -4
- package/dist/templates/zcode/config.json +42 -0
- package/dist/templates/zcode/index.d.ts +10 -1
- package/dist/templates/zcode/index.d.ts.map +1 -1
- package/dist/templates/zcode/index.js +12 -2
- package/dist/templates/zcode/index.js.map +1 -1
- package/dist/types/ai-tools.d.ts +4 -4
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +114 -2
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +0 -3
- package/dist/utils/template-fetcher.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
@@ -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 = "
|
|
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 ``
|
|
251
|
-
|
|
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
|
|
266
|
+
f"[WARN] invalid codex config: {codex!r}; using inline",
|
|
260
267
|
file=sys.stderr,
|
|
261
268
|
)
|
|
262
|
-
return
|
|
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 ("
|
|
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
|
|
278
|
+
f"[WARN] invalid codex.dispatch_mode value: {raw!r}; using inline",
|
|
270
279
|
file=sys.stderr,
|
|
271
280
|
)
|
|
272
|
-
return
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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)}")
|