@mindfoldhq/trellis 0.4.0-beta.8 → 0.4.0-rc.0

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 (99) hide show
  1. package/README.md +10 -5
  2. package/dist/cli/index.js +2 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/init.d.ts +2 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +165 -13
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +14 -2
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/configurators/codex.d.ts.map +1 -1
  12. package/dist/configurators/codex.js +2 -1
  13. package/dist/configurators/codex.js.map +1 -1
  14. package/dist/configurators/copilot.d.ts +9 -0
  15. package/dist/configurators/copilot.d.ts.map +1 -0
  16. package/dist/configurators/copilot.js +34 -0
  17. package/dist/configurators/copilot.js.map +1 -0
  18. package/dist/configurators/index.d.ts.map +1 -1
  19. package/dist/configurators/index.js +32 -1
  20. package/dist/configurators/index.js.map +1 -1
  21. package/dist/configurators/windsurf.d.ts +8 -0
  22. package/dist/configurators/windsurf.d.ts.map +1 -0
  23. package/dist/configurators/windsurf.js +18 -0
  24. package/dist/configurators/windsurf.js.map +1 -0
  25. package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
  26. package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
  27. package/dist/migrations/manifests/0.4.0-rc.0.json +9 -0
  28. package/dist/templates/claude/hooks/inject-subagent-context.py +8 -1
  29. package/dist/templates/claude/hooks/ralph-loop.py +18 -10
  30. package/dist/templates/claude/hooks/session-start.py +60 -19
  31. package/dist/templates/claude/hooks/statusline.py +218 -0
  32. package/dist/templates/claude/settings.json +4 -0
  33. package/dist/templates/codex/hooks/session-start.py +60 -21
  34. package/dist/templates/codex/hooks.json +1 -1
  35. package/dist/templates/copilot/hooks/session-start.py +243 -0
  36. package/dist/templates/copilot/hooks.json +11 -0
  37. package/dist/templates/copilot/index.d.ts +23 -0
  38. package/dist/templates/copilot/index.d.ts.map +1 -0
  39. package/dist/templates/copilot/index.js +54 -0
  40. package/dist/templates/copilot/index.js.map +1 -0
  41. package/dist/templates/copilot/prompts/before-dev.prompt.md +33 -0
  42. package/dist/templates/copilot/prompts/brainstorm.prompt.md +491 -0
  43. package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
  44. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +157 -0
  45. package/dist/templates/copilot/prompts/check.prompt.md +29 -0
  46. package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
  47. package/dist/templates/copilot/prompts/finish-work.prompt.md +157 -0
  48. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
  49. package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
  50. package/dist/templates/copilot/prompts/parallel.prompt.md +196 -0
  51. package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
  52. package/dist/templates/copilot/prompts/start.prompt.md +397 -0
  53. package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
  54. package/dist/templates/extract.d.ts +18 -0
  55. package/dist/templates/extract.d.ts.map +1 -1
  56. package/dist/templates/extract.js +32 -0
  57. package/dist/templates/extract.js.map +1 -1
  58. package/dist/templates/iflow/hooks/inject-subagent-context.py +8 -1
  59. package/dist/templates/iflow/hooks/ralph-loop.py +8 -1
  60. package/dist/templates/iflow/hooks/session-start.py +60 -19
  61. package/dist/templates/markdown/spec/backend/directory-structure.md +1 -1
  62. package/dist/templates/opencode/agents/dispatch.md +20 -19
  63. package/dist/templates/opencode/lib/trellis-context.js +35 -239
  64. package/dist/templates/opencode/plugins/inject-subagent-context.js +71 -121
  65. package/dist/templates/opencode/plugins/session-start.js +150 -146
  66. package/dist/templates/trellis/scripts/add_session.py +6 -1
  67. package/dist/templates/trellis/scripts/common/__init__.py +2 -0
  68. package/dist/templates/trellis/scripts/common/cli_adapter.py +87 -9
  69. package/dist/templates/trellis/scripts/common/paths.py +57 -6
  70. package/dist/templates/trellis/scripts/common/task_store.py +6 -4
  71. package/dist/templates/trellis/scripts/common/task_utils.py +14 -8
  72. package/dist/templates/trellis/scripts/multi_agent/start.py +9 -5
  73. package/dist/templates/trellis/scripts/task.py +1 -1
  74. package/dist/templates/trellis/workflow.md +17 -4
  75. package/dist/templates/windsurf/index.d.ts +21 -0
  76. package/dist/templates/windsurf/index.d.ts.map +1 -0
  77. package/dist/templates/windsurf/index.js +44 -0
  78. package/dist/templates/windsurf/index.js.map +1 -0
  79. package/dist/templates/windsurf/workflows/trellis-before-dev.md +31 -0
  80. package/dist/templates/windsurf/workflows/trellis-brainstorm.md +491 -0
  81. package/dist/templates/windsurf/workflows/trellis-break-loop.md +111 -0
  82. package/dist/templates/windsurf/workflows/trellis-check-cross-layer.md +157 -0
  83. package/dist/templates/windsurf/workflows/trellis-check.md +27 -0
  84. package/dist/templates/windsurf/workflows/trellis-create-command.md +154 -0
  85. package/dist/templates/windsurf/workflows/trellis-finish-work.md +147 -0
  86. package/dist/templates/windsurf/workflows/trellis-integrate-skill.md +220 -0
  87. package/dist/templates/windsurf/workflows/trellis-onboard.md +362 -0
  88. package/dist/templates/windsurf/workflows/trellis-record-session.md +66 -0
  89. package/dist/templates/windsurf/workflows/trellis-start.md +373 -0
  90. package/dist/templates/windsurf/workflows/trellis-update-spec.md +358 -0
  91. package/dist/types/ai-tools.d.ts +5 -3
  92. package/dist/types/ai-tools.d.ts.map +1 -1
  93. package/dist/types/ai-tools.js +21 -1
  94. package/dist/types/ai-tools.js.map +1 -1
  95. package/dist/utils/template-fetcher.d.ts +17 -4
  96. package/dist/utils/template-fetcher.d.ts.map +1 -1
  97. package/dist/utils/template-fetcher.js +94 -12
  98. package/dist/utils/template-fetcher.js.map +1 -1
  99. package/package.json +1 -1
@@ -65,23 +65,47 @@ def run_script(script_path: Path) -> str:
65
65
  return "No context available"
66
66
 
67
67
 
68
+ def _normalize_task_ref(task_ref: str) -> str:
69
+ normalized = task_ref.strip()
70
+ if not normalized:
71
+ return ""
72
+
73
+ path_obj = Path(normalized)
74
+ if path_obj.is_absolute():
75
+ return str(path_obj)
76
+
77
+ normalized = normalized.replace("\\", "/")
78
+ while normalized.startswith("./"):
79
+ normalized = normalized[2:]
80
+
81
+ if normalized.startswith("tasks/"):
82
+ return f".trellis/{normalized}"
83
+
84
+ return normalized
85
+
86
+
87
+ def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path:
88
+ normalized = _normalize_task_ref(task_ref)
89
+ path_obj = Path(normalized)
90
+ if path_obj.is_absolute():
91
+ return path_obj
92
+ if normalized.startswith(".trellis/"):
93
+ return trellis_dir.parent / path_obj
94
+ return trellis_dir / "tasks" / path_obj
95
+
96
+
68
97
  def _get_task_status(trellis_dir: Path) -> str:
69
98
  """Check current task status and return structured status string."""
70
99
  current_task_file = trellis_dir / ".current-task"
71
100
  if not current_task_file.is_file():
72
101
  return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
73
102
 
74
- task_ref = current_task_file.read_text(encoding="utf-8").strip()
103
+ task_ref = _normalize_task_ref(current_task_file.read_text(encoding="utf-8").strip())
75
104
  if not task_ref:
76
105
  return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
77
106
 
78
107
  # Resolve task directory
79
- if Path(task_ref).is_absolute():
80
- task_dir = Path(task_ref)
81
- elif task_ref.startswith(".trellis/"):
82
- task_dir = trellis_dir.parent / task_ref
83
- else:
84
- task_dir = trellis_dir / "tasks" / task_ref
108
+ task_dir = _resolve_task_dir(trellis_dir, task_ref)
85
109
  if not task_dir.is_dir():
86
110
  return f"Status: STALE POINTER\nTask: {task_ref}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish"
87
111
 
@@ -264,13 +288,38 @@ def _resolve_spec_scope(
264
288
  return None # Unknown scope type: full scan
265
289
 
266
290
 
291
+ def _build_workflow_toc(workflow_path: Path) -> str:
292
+ """Build a compact section index for workflow.md (lazy-load the full file on demand).
293
+
294
+ Replaces full-file injection to keep additionalContext payload small.
295
+ The full file is accessible via: Read tool on .trellis/workflow.md
296
+ """
297
+ content = read_file(workflow_path)
298
+ if not content:
299
+ return "No workflow.md found"
300
+
301
+ toc_lines = [
302
+ "# Development Workflow — Section Index",
303
+ "Full guide: .trellis/workflow.md (read on demand)",
304
+ "",
305
+ ]
306
+ for line in content.splitlines():
307
+ if line.startswith("## "):
308
+ toc_lines.append(line)
309
+
310
+ toc_lines += [
311
+ "",
312
+ "To read a section: use the Read tool on .trellis/workflow.md",
313
+ ]
314
+ return "\n".join(toc_lines)
315
+
316
+
267
317
  def main():
268
318
  if should_skip_injection():
269
319
  sys.exit(0)
270
320
 
271
321
  project_dir = Path(os.environ.get("CLAUDE_PROJECT_DIR", ".")).resolve()
272
322
  trellis_dir = project_dir / ".trellis"
273
- claude_dir = project_dir / ".claude"
274
323
 
275
324
  # Load config for scope filtering and legacy detection
276
325
  is_mono, packages, scope_config, task_pkg, default_pkg = _load_trellis_config(trellis_dir)
@@ -296,8 +345,7 @@ Read and follow all instructions below carefully.
296
345
  output.write("\n</current-state>\n\n")
297
346
 
298
347
  output.write("<workflow>\n")
299
- workflow_content = read_file(trellis_dir / "workflow.md", "No workflow.md found")
300
- output.write(workflow_content)
348
+ output.write(_build_workflow_toc(trellis_dir / "workflow.md"))
301
349
  output.write("\n</workflow>\n\n")
302
350
 
303
351
  output.write("<guidelines>\n")
@@ -341,20 +389,13 @@ Read and follow all instructions below carefully.
341
389
 
342
390
  output.write("</guidelines>\n\n")
343
391
 
344
- output.write("<instructions>\n")
345
- start_md = read_file(
346
- claude_dir / "commands" / "trellis" / "start.md", "No start.md found"
347
- )
348
- output.write(start_md)
349
- output.write("\n</instructions>\n\n")
350
-
351
392
  # Check task status and inject structured tag
352
393
  task_status = _get_task_status(trellis_dir)
353
394
  output.write(f"<task-status>\n{task_status}\n</task-status>\n\n")
354
395
 
355
396
  output.write("""<ready>
356
- Context loaded. Steps 1-3 (workflow, context, guidelines) are already injected above — do NOT re-read them.
357
- Start from Step 4. Wait for user's first message, then follow <instructions> to handle their request.
397
+ Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them.
398
+ Wait for the user's first message, then handle it following the workflow guide.
358
399
  If there is an active task, ask whether to continue it.
359
400
  </ready>""")
360
401
 
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Trellis StatusLine — project-level status display for Claude Code.
5
+
6
+ Reads Claude Code session JSON from stdin + Trellis task data from filesystem.
7
+ Outputs 1-2 lines:
8
+ With active task: [P1] Task title (status) + info line
9
+ Without task: info line only
10
+ Info line: model · ctx% · branch · duration · developer · tasks · rate limits
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import io
15
+ import json
16
+ import re
17
+ import subprocess
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ # Fix: Windows Python defaults to GBK encoding, which corrupts UTF-8
22
+ # characters like the middle dot (·). Wrap stdout/stderr with UTF-8.
23
+ if sys.platform == "win32":
24
+ sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8")
25
+ sys.stderr = io.TextIOWrapper(sys.stderr.detach(), encoding="utf-8")
26
+
27
+
28
+ def _read_text(path: Path) -> str:
29
+ try:
30
+ return path.read_text(encoding="utf-8").strip()
31
+ except (FileNotFoundError, PermissionError, OSError):
32
+ return ""
33
+
34
+
35
+ def _read_json(path: Path) -> dict:
36
+ text = _read_text(path)
37
+ if not text:
38
+ return {}
39
+ try:
40
+ return json.loads(text)
41
+ except (json.JSONDecodeError, ValueError):
42
+ return {}
43
+
44
+
45
+ def _normalize_task_ref(task_ref: str) -> str:
46
+ normalized = task_ref.strip()
47
+ if not normalized:
48
+ return ""
49
+
50
+ path_obj = Path(normalized)
51
+ if path_obj.is_absolute():
52
+ return str(path_obj)
53
+
54
+ normalized = normalized.replace("\\", "/")
55
+ while normalized.startswith("./"):
56
+ normalized = normalized[2:]
57
+
58
+ if normalized.startswith("tasks/"):
59
+ return f".trellis/{normalized}"
60
+
61
+ return normalized
62
+
63
+
64
+ def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path:
65
+ normalized = _normalize_task_ref(task_ref)
66
+ path_obj = Path(normalized)
67
+ if path_obj.is_absolute():
68
+ return path_obj
69
+ if normalized.startswith(".trellis/"):
70
+ return trellis_dir.parent / path_obj
71
+ return trellis_dir / "tasks" / path_obj
72
+
73
+
74
+ def _find_trellis_dir() -> Path | None:
75
+ """Walk up from cwd to find .trellis/ directory."""
76
+ current = Path.cwd()
77
+ for parent in [current, *current.parents]:
78
+ candidate = parent / ".trellis"
79
+ if candidate.is_dir():
80
+ return candidate
81
+ return None
82
+
83
+
84
+ def _get_current_task(trellis_dir: Path) -> dict | None:
85
+ """Load current task info. Returns dict with title/status/priority or None."""
86
+ task_ref = _normalize_task_ref(_read_text(trellis_dir / ".current-task"))
87
+ if not task_ref:
88
+ return None
89
+
90
+ # Resolve task directory
91
+ task_path = _resolve_task_dir(trellis_dir, task_ref)
92
+ task_data = _read_json(task_path / "task.json")
93
+ if not task_data:
94
+ return None
95
+
96
+ return {
97
+ "title": task_data.get("title") or task_data.get("name") or "unknown",
98
+ "status": task_data.get("status", "unknown"),
99
+ "priority": task_data.get("priority", "P2"),
100
+ }
101
+
102
+
103
+ def _count_active_tasks(trellis_dir: Path) -> int:
104
+ """Count non-archived task directories with valid task.json."""
105
+ tasks_dir = trellis_dir / "tasks"
106
+ if not tasks_dir.is_dir():
107
+ return 0
108
+ count = 0
109
+ for d in tasks_dir.iterdir():
110
+ if d.is_dir() and d.name != "archive" and (d / "task.json").is_file():
111
+ count += 1
112
+ return count
113
+
114
+
115
+ def _get_developer(trellis_dir: Path) -> str:
116
+ content = _read_text(trellis_dir / ".developer")
117
+ if not content:
118
+ return "unknown"
119
+ for line in content.splitlines():
120
+ if line.startswith("name="):
121
+ return line[5:].strip()
122
+ return content.splitlines()[0].strip() or "unknown"
123
+
124
+
125
+ def _get_git_branch() -> str:
126
+ try:
127
+ result = subprocess.run(
128
+ ["git", "branch", "--show-current"],
129
+ capture_output=True, text=True, timeout=3,
130
+ )
131
+ return result.stdout.strip() if result.returncode == 0 else ""
132
+ except (FileNotFoundError, subprocess.TimeoutExpired):
133
+ return ""
134
+
135
+
136
+ def _format_ctx_size(size: int) -> str:
137
+ if size >= 1_000_000:
138
+ return f"{size // 1_000_000}M"
139
+ if size >= 1_000:
140
+ return f"{size // 1_000}K"
141
+ return str(size)
142
+
143
+
144
+ def _format_duration(ms: int) -> str:
145
+ secs = ms // 1000
146
+ hours, remainder = divmod(secs, 3600)
147
+ mins = remainder // 60
148
+ if hours > 0:
149
+ return f"{hours}h{mins}m"
150
+ return f"{mins}m"
151
+
152
+
153
+ def main() -> None:
154
+ # Read Claude Code session JSON from stdin
155
+ try:
156
+ cc_data = json.loads(sys.stdin.read())
157
+ except (json.JSONDecodeError, ValueError):
158
+ cc_data = {}
159
+
160
+ trellis_dir = _find_trellis_dir()
161
+ SEP = " \033[90m·\033[0m "
162
+
163
+ # --- Trellis data ---
164
+ task = _get_current_task(trellis_dir) if trellis_dir else None
165
+ dev = _get_developer(trellis_dir) if trellis_dir else ""
166
+ task_count = _count_active_tasks(trellis_dir) if trellis_dir else 0
167
+
168
+ # --- CC session data ---
169
+ model = cc_data.get("model", {}).get("display_name", "?")
170
+ ctx_pct = int(cc_data.get("context_window", {}).get("used_percentage") or 0)
171
+ ctx_size = _format_ctx_size(cc_data.get("context_window", {}).get("context_window_size") or 0)
172
+ duration = _format_duration(cc_data.get("cost", {}).get("total_duration_ms") or 0)
173
+ branch = _get_git_branch()
174
+
175
+ # Avoid "Opus 4.6 (1M context) (1M)"
176
+ if re.search(r"\d+[KMG]\b", model, re.IGNORECASE):
177
+ model_label = model
178
+ else:
179
+ model_label = f"{model} ({ctx_size})"
180
+
181
+ # Context % with color
182
+ if ctx_pct >= 90:
183
+ ctx_color = "\033[31m"
184
+ elif ctx_pct >= 70:
185
+ ctx_color = "\033[33m"
186
+ else:
187
+ ctx_color = "\033[32m"
188
+
189
+ # Build info line: model · ctx · branch · duration · dev · tasks [· rate limits]
190
+ parts = [
191
+ model_label,
192
+ f"ctx {ctx_color}{ctx_pct}%\033[0m",
193
+ ]
194
+ if branch:
195
+ parts.append(f"\033[35m{branch}\033[0m")
196
+ parts.append(duration)
197
+ if dev:
198
+ parts.append(f"\033[32m{dev}\033[0m")
199
+ if task_count:
200
+ parts.append(f"{task_count} task(s)")
201
+
202
+ five_hr = cc_data.get("rate_limits", {}).get("five_hour", {}).get("used_percentage")
203
+ if five_hr is not None:
204
+ parts.append(f"5h {int(five_hr)}%")
205
+ seven_day = cc_data.get("rate_limits", {}).get("seven_day", {}).get("used_percentage")
206
+ if seven_day is not None:
207
+ parts.append(f"7d {int(seven_day)}%")
208
+
209
+ info_line = SEP.join(parts)
210
+
211
+ # Output: task line (only if active) + info line
212
+ if task:
213
+ print(f"\033[36m[{task['priority']}]\033[0m {task['title']} \033[33m({task['status']})\033[0m")
214
+ print(info_line)
215
+
216
+
217
+ if __name__ == "__main__":
218
+ main()
@@ -1,4 +1,8 @@
1
1
  {
2
+ "statusLine": {
3
+ "type": "command",
4
+ "command": "{{PYTHON_CMD}} .claude/hooks/statusline.py"
5
+ },
2
6
  "hooks": {
3
7
  "SessionStart": [
4
8
  {
@@ -51,21 +51,45 @@ def run_script(script_path: Path) -> str:
51
51
  return "No context available"
52
52
 
53
53
 
54
+ def _normalize_task_ref(task_ref: str) -> str:
55
+ normalized = task_ref.strip()
56
+ if not normalized:
57
+ return ""
58
+
59
+ path_obj = Path(normalized)
60
+ if path_obj.is_absolute():
61
+ return str(path_obj)
62
+
63
+ normalized = normalized.replace("\\", "/")
64
+ while normalized.startswith("./"):
65
+ normalized = normalized[2:]
66
+
67
+ if normalized.startswith("tasks/"):
68
+ return f".trellis/{normalized}"
69
+
70
+ return normalized
71
+
72
+
73
+ def _resolve_task_dir(trellis_dir: Path, task_ref: str) -> Path:
74
+ normalized = _normalize_task_ref(task_ref)
75
+ path_obj = Path(normalized)
76
+ if path_obj.is_absolute():
77
+ return path_obj
78
+ if normalized.startswith(".trellis/"):
79
+ return trellis_dir.parent / path_obj
80
+ return trellis_dir / "tasks" / path_obj
81
+
82
+
54
83
  def _get_task_status(trellis_dir: Path) -> str:
55
84
  current_task_file = trellis_dir / ".current-task"
56
85
  if not current_task_file.is_file():
57
86
  return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
58
87
 
59
- task_ref = current_task_file.read_text(encoding="utf-8").strip()
88
+ task_ref = _normalize_task_ref(current_task_file.read_text(encoding="utf-8").strip())
60
89
  if not task_ref:
61
90
  return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
62
91
 
63
- if Path(task_ref).is_absolute():
64
- task_dir = Path(task_ref)
65
- elif task_ref.startswith(".trellis/"):
66
- task_dir = trellis_dir.parent / task_ref
67
- else:
68
- task_dir = trellis_dir / "tasks" / task_ref
92
+ task_dir = _resolve_task_dir(trellis_dir, task_ref)
69
93
  if not task_dir.is_dir():
70
94
  return f"Status: STALE POINTER\nTask: {task_ref}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish"
71
95
 
@@ -101,6 +125,32 @@ def _get_task_status(trellis_dir: Path) -> str:
101
125
  return f"Status: READY\nTask: {task_title}\nNext: Continue with implement or check"
102
126
 
103
127
 
128
+ def _build_workflow_toc(workflow_path: Path) -> str:
129
+ """Build a compact section index for workflow.md (lazy-load the full file on demand).
130
+
131
+ Replaces full-file injection to keep additionalContext payload small.
132
+ The full file is accessible via: Read tool on .trellis/workflow.md
133
+ """
134
+ content = read_file(workflow_path)
135
+ if not content:
136
+ return "No workflow.md found"
137
+
138
+ toc_lines = [
139
+ "# Development Workflow — Section Index",
140
+ "Full guide: .trellis/workflow.md (read on demand)",
141
+ "",
142
+ ]
143
+ for line in content.splitlines():
144
+ if line.startswith("## "):
145
+ toc_lines.append(line)
146
+
147
+ toc_lines += [
148
+ "",
149
+ "To read a section: use the Read tool on .trellis/workflow.md",
150
+ ]
151
+ return "\n".join(toc_lines)
152
+
153
+
104
154
  def main() -> None:
105
155
  if should_skip_injection():
106
156
  sys.exit(0)
@@ -113,7 +163,6 @@ def main() -> None:
113
163
  project_dir = Path(".").resolve()
114
164
 
115
165
  trellis_dir = project_dir / ".trellis"
116
- codex_dir = project_dir / ".codex"
117
166
 
118
167
  output = StringIO()
119
168
 
@@ -130,8 +179,7 @@ Read and follow all instructions below carefully.
130
179
  output.write("\n</current-state>\n\n")
131
180
 
132
181
  output.write("<workflow>\n")
133
- workflow_content = read_file(trellis_dir / "workflow.md", "No workflow.md found")
134
- output.write(workflow_content)
182
+ output.write(_build_workflow_toc(trellis_dir / "workflow.md"))
135
183
  output.write("\n</workflow>\n\n")
136
184
 
137
185
  output.write("<guidelines>\n")
@@ -169,21 +217,12 @@ Read and follow all instructions below carefully.
169
217
 
170
218
  output.write("</guidelines>\n\n")
171
219
 
172
- # Inject start skill as instructions (Codex uses skills, not slash commands)
173
- start_skill = codex_dir / "skills" / "start" / "SKILL.md"
174
- if not start_skill.is_file():
175
- start_skill = project_dir / ".agents" / "skills" / "start" / "SKILL.md"
176
- if start_skill.is_file():
177
- output.write("<instructions>\n")
178
- output.write(read_file(start_skill))
179
- output.write("\n</instructions>\n\n")
180
-
181
220
  task_status = _get_task_status(trellis_dir)
182
221
  output.write(f"<task-status>\n{task_status}\n</task-status>\n\n")
183
222
 
184
223
  output.write("""<ready>
185
- Context loaded. Steps 1-3 (workflow, context, guidelines) are already injected above — do NOT re-read them.
186
- Start from Step 4. Wait for user's first message, then follow <instructions> to handle their request.
224
+ Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them.
225
+ Wait for the user's first message, then handle it following the workflow guide.
187
226
  If there is an active task, ask whether to continue it.
188
227
  </ready>""")
189
228
 
@@ -5,7 +5,7 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": "python3 .codex/hooks/session-start.py",
8
+ "command": "{{PYTHON_CMD}} .codex/hooks/session-start.py",
9
9
  "timeout": 15,
10
10
  "statusMessage": "Loading Trellis context..."
11
11
  }