@mindfoldhq/trellis 0.4.0-beta.9 → 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.
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +132 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +14 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.4.0-rc.0.json +9 -0
- package/dist/templates/claude/hooks/ralph-loop.py +10 -9
- package/dist/templates/claude/hooks/session-start.py +29 -12
- package/dist/templates/claude/hooks/statusline.py +7 -0
- package/dist/templates/codex/hooks/session-start.py +29 -14
- package/dist/templates/copilot/hooks/session-start.py +29 -4
- package/dist/templates/iflow/hooks/session-start.py +29 -12
- package/dist/templates/opencode/lib/trellis-context.js +4 -248
- package/dist/templates/opencode/plugins/inject-subagent-context.js +71 -121
- package/dist/templates/opencode/plugins/session-start.js +143 -119
- package/dist/templates/trellis/workflow.md +17 -4
- package/package.json +1 -1
|
@@ -265,17 +265,18 @@ def main():
|
|
|
265
265
|
sys.exit(0)
|
|
266
266
|
|
|
267
267
|
# Get subagent info
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
# Field names per Claude Code SubagentStop event schema:
|
|
269
|
+
# agent_type, last_assistant_message, agent_id, agent_transcript_path, cwd
|
|
270
|
+
# The event does NOT carry a `prompt` field, so finish-phase detection
|
|
271
|
+
# based on a `[finish]` marker in the user prompt is no longer possible
|
|
272
|
+
# here; finish-phase skip logic should be reintroduced via task.json
|
|
273
|
+
# state (e.g. current_phase) in a follow-up.
|
|
274
|
+
agent_type = input_data.get("agent_type", "")
|
|
275
|
+
last_assistant_message = input_data.get("last_assistant_message", "")
|
|
271
276
|
cwd = input_data.get("cwd", os.getcwd())
|
|
272
277
|
|
|
273
278
|
# Only control check agent
|
|
274
|
-
if
|
|
275
|
-
sys.exit(0)
|
|
276
|
-
|
|
277
|
-
# Skip Ralph Loop for finish phase (already verified in check phase)
|
|
278
|
-
if "[finish]" in original_prompt.lower():
|
|
279
|
+
if agent_type != TARGET_AGENT:
|
|
279
280
|
sys.exit(0)
|
|
280
281
|
|
|
281
282
|
# Find repo root
|
|
@@ -357,7 +358,7 @@ def main():
|
|
|
357
358
|
else:
|
|
358
359
|
# No verify commands, fall back to completion markers
|
|
359
360
|
markers = get_completion_markers(repo_root, task_dir)
|
|
360
|
-
all_complete, missing = check_completion(
|
|
361
|
+
all_complete, missing = check_completion(last_assistant_message, markers)
|
|
361
362
|
|
|
362
363
|
if all_complete:
|
|
363
364
|
# All checks complete, allow stop
|
|
@@ -288,13 +288,38 @@ def _resolve_spec_scope(
|
|
|
288
288
|
return None # Unknown scope type: full scan
|
|
289
289
|
|
|
290
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
|
+
|
|
291
317
|
def main():
|
|
292
318
|
if should_skip_injection():
|
|
293
319
|
sys.exit(0)
|
|
294
320
|
|
|
295
321
|
project_dir = Path(os.environ.get("CLAUDE_PROJECT_DIR", ".")).resolve()
|
|
296
322
|
trellis_dir = project_dir / ".trellis"
|
|
297
|
-
claude_dir = project_dir / ".claude"
|
|
298
323
|
|
|
299
324
|
# Load config for scope filtering and legacy detection
|
|
300
325
|
is_mono, packages, scope_config, task_pkg, default_pkg = _load_trellis_config(trellis_dir)
|
|
@@ -320,8 +345,7 @@ Read and follow all instructions below carefully.
|
|
|
320
345
|
output.write("\n</current-state>\n\n")
|
|
321
346
|
|
|
322
347
|
output.write("<workflow>\n")
|
|
323
|
-
|
|
324
|
-
output.write(workflow_content)
|
|
348
|
+
output.write(_build_workflow_toc(trellis_dir / "workflow.md"))
|
|
325
349
|
output.write("\n</workflow>\n\n")
|
|
326
350
|
|
|
327
351
|
output.write("<guidelines>\n")
|
|
@@ -365,20 +389,13 @@ Read and follow all instructions below carefully.
|
|
|
365
389
|
|
|
366
390
|
output.write("</guidelines>\n\n")
|
|
367
391
|
|
|
368
|
-
output.write("<instructions>\n")
|
|
369
|
-
start_md = read_file(
|
|
370
|
-
claude_dir / "commands" / "trellis" / "start.md", "No start.md found"
|
|
371
|
-
)
|
|
372
|
-
output.write(start_md)
|
|
373
|
-
output.write("\n</instructions>\n\n")
|
|
374
|
-
|
|
375
392
|
# Check task status and inject structured tag
|
|
376
393
|
task_status = _get_task_status(trellis_dir)
|
|
377
394
|
output.write(f"<task-status>\n{task_status}\n</task-status>\n\n")
|
|
378
395
|
|
|
379
396
|
output.write("""<ready>
|
|
380
|
-
Context loaded.
|
|
381
|
-
|
|
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.
|
|
382
399
|
If there is an active task, ask whether to continue it.
|
|
383
400
|
</ready>""")
|
|
384
401
|
|
|
@@ -11,12 +11,19 @@ Info line: model · ctx% · branch · duration · developer · tasks · rate lim
|
|
|
11
11
|
"""
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import io
|
|
14
15
|
import json
|
|
15
16
|
import re
|
|
16
17
|
import subprocess
|
|
17
18
|
import sys
|
|
18
19
|
from pathlib import Path
|
|
19
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
|
+
|
|
20
27
|
|
|
21
28
|
def _read_text(path: Path) -> str:
|
|
22
29
|
try:
|
|
@@ -125,6 +125,32 @@ def _get_task_status(trellis_dir: Path) -> str:
|
|
|
125
125
|
return f"Status: READY\nTask: {task_title}\nNext: Continue with implement or check"
|
|
126
126
|
|
|
127
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
|
+
|
|
128
154
|
def main() -> None:
|
|
129
155
|
if should_skip_injection():
|
|
130
156
|
sys.exit(0)
|
|
@@ -137,7 +163,6 @@ def main() -> None:
|
|
|
137
163
|
project_dir = Path(".").resolve()
|
|
138
164
|
|
|
139
165
|
trellis_dir = project_dir / ".trellis"
|
|
140
|
-
codex_dir = project_dir / ".codex"
|
|
141
166
|
|
|
142
167
|
output = StringIO()
|
|
143
168
|
|
|
@@ -154,8 +179,7 @@ Read and follow all instructions below carefully.
|
|
|
154
179
|
output.write("\n</current-state>\n\n")
|
|
155
180
|
|
|
156
181
|
output.write("<workflow>\n")
|
|
157
|
-
|
|
158
|
-
output.write(workflow_content)
|
|
182
|
+
output.write(_build_workflow_toc(trellis_dir / "workflow.md"))
|
|
159
183
|
output.write("\n</workflow>\n\n")
|
|
160
184
|
|
|
161
185
|
output.write("<guidelines>\n")
|
|
@@ -193,21 +217,12 @@ Read and follow all instructions below carefully.
|
|
|
193
217
|
|
|
194
218
|
output.write("</guidelines>\n\n")
|
|
195
219
|
|
|
196
|
-
# Inject start skill as instructions (Codex uses skills, not slash commands)
|
|
197
|
-
start_skill = codex_dir / "skills" / "start" / "SKILL.md"
|
|
198
|
-
if not start_skill.is_file():
|
|
199
|
-
start_skill = project_dir / ".agents" / "skills" / "start" / "SKILL.md"
|
|
200
|
-
if start_skill.is_file():
|
|
201
|
-
output.write("<instructions>\n")
|
|
202
|
-
output.write(read_file(start_skill))
|
|
203
|
-
output.write("\n</instructions>\n\n")
|
|
204
|
-
|
|
205
220
|
task_status = _get_task_status(trellis_dir)
|
|
206
221
|
output.write(f"<task-status>\n{task_status}\n</task-status>\n\n")
|
|
207
222
|
|
|
208
223
|
output.write("""<ready>
|
|
209
|
-
Context loaded.
|
|
210
|
-
|
|
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.
|
|
211
226
|
If there is an active task, ask whether to continue it.
|
|
212
227
|
</ready>""")
|
|
213
228
|
|
|
@@ -125,6 +125,32 @@ def _get_task_status(trellis_dir: Path) -> str:
|
|
|
125
125
|
return f"Status: READY\nTask: {task_title}\nNext: Continue with implement or check"
|
|
126
126
|
|
|
127
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
|
+
|
|
128
154
|
def main() -> None:
|
|
129
155
|
if should_skip_injection():
|
|
130
156
|
sys.exit(0)
|
|
@@ -153,8 +179,7 @@ Read and follow all instructions below carefully.
|
|
|
153
179
|
output.write("\n</current-state>\n\n")
|
|
154
180
|
|
|
155
181
|
output.write("<workflow>\n")
|
|
156
|
-
|
|
157
|
-
output.write(workflow_content)
|
|
182
|
+
output.write(_build_workflow_toc(trellis_dir / "workflow.md"))
|
|
158
183
|
output.write("\n</workflow>\n\n")
|
|
159
184
|
|
|
160
185
|
output.write("<guidelines>\n")
|
|
@@ -196,8 +221,8 @@ Read and follow all instructions below carefully.
|
|
|
196
221
|
output.write(f"<task-status>\n{task_status}\n</task-status>\n\n")
|
|
197
222
|
|
|
198
223
|
output.write("""<ready>
|
|
199
|
-
Context loaded.
|
|
200
|
-
|
|
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.
|
|
201
226
|
If there is an active task, ask whether to continue it.
|
|
202
227
|
</ready>""")
|
|
203
228
|
|
|
@@ -278,6 +278,32 @@ def _resolve_spec_scope(
|
|
|
278
278
|
return None
|
|
279
279
|
|
|
280
280
|
|
|
281
|
+
def _build_workflow_toc(workflow_path: Path) -> str:
|
|
282
|
+
"""Build a compact section index for workflow.md (lazy-load the full file on demand).
|
|
283
|
+
|
|
284
|
+
Replaces full-file injection to keep additionalContext payload small.
|
|
285
|
+
The full file is accessible via: Read tool on .trellis/workflow.md
|
|
286
|
+
"""
|
|
287
|
+
content = read_file(workflow_path)
|
|
288
|
+
if not content:
|
|
289
|
+
return "No workflow.md found"
|
|
290
|
+
|
|
291
|
+
toc_lines = [
|
|
292
|
+
"# Development Workflow — Section Index",
|
|
293
|
+
"Full guide: .trellis/workflow.md (read on demand)",
|
|
294
|
+
"",
|
|
295
|
+
]
|
|
296
|
+
for line in content.splitlines():
|
|
297
|
+
if line.startswith("## "):
|
|
298
|
+
toc_lines.append(line)
|
|
299
|
+
|
|
300
|
+
toc_lines += [
|
|
301
|
+
"",
|
|
302
|
+
"To read a section: use the Read tool on .trellis/workflow.md",
|
|
303
|
+
]
|
|
304
|
+
return "\n".join(toc_lines)
|
|
305
|
+
|
|
306
|
+
|
|
281
307
|
def main():
|
|
282
308
|
if should_skip_injection():
|
|
283
309
|
sys.exit(0)
|
|
@@ -285,7 +311,6 @@ def main():
|
|
|
285
311
|
# iFlow don't have an env for project, use `.` instead
|
|
286
312
|
project_dir = Path(".").resolve()
|
|
287
313
|
trellis_dir = project_dir / ".trellis"
|
|
288
|
-
iflow_dir = project_dir / ".iflow"
|
|
289
314
|
|
|
290
315
|
# Load config for scope filtering and legacy detection
|
|
291
316
|
is_mono, packages, scope_config, task_pkg, default_pkg = _load_trellis_config(trellis_dir)
|
|
@@ -311,8 +336,7 @@ Read and follow all instructions below carefully.
|
|
|
311
336
|
output.write("\n</current-state>\n\n")
|
|
312
337
|
|
|
313
338
|
output.write("<workflow>\n")
|
|
314
|
-
|
|
315
|
-
output.write(workflow_content)
|
|
339
|
+
output.write(_build_workflow_toc(trellis_dir / "workflow.md"))
|
|
316
340
|
output.write("\n</workflow>\n\n")
|
|
317
341
|
|
|
318
342
|
output.write("<guidelines>\n")
|
|
@@ -354,20 +378,13 @@ Read and follow all instructions below carefully.
|
|
|
354
378
|
|
|
355
379
|
output.write("</guidelines>\n\n")
|
|
356
380
|
|
|
357
|
-
output.write("<instructions>\n")
|
|
358
|
-
start_md = read_file(
|
|
359
|
-
iflow_dir / "commands" / "trellis" / "start.md", "No start.md found"
|
|
360
|
-
)
|
|
361
|
-
output.write(start_md)
|
|
362
|
-
output.write("\n</instructions>\n\n")
|
|
363
|
-
|
|
364
381
|
# R2: Check task status and inject structured tag
|
|
365
382
|
task_status = _get_task_status(trellis_dir)
|
|
366
383
|
output.write(f"<task-status>\n{task_status}\n</task-status>\n\n")
|
|
367
384
|
|
|
368
385
|
output.write("""<ready>
|
|
369
|
-
Context loaded.
|
|
370
|
-
|
|
386
|
+
Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them.
|
|
387
|
+
Wait for the user's first message, then handle it following the workflow guide.
|
|
371
388
|
If there is an active task, ask whether to continue it.
|
|
372
389
|
</ready>""")
|
|
373
390
|
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Trellis Context Manager
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* import { TrellisContext } from "./trellis-context.js"
|
|
9
|
-
* const ctx = new TrellisContext(directory)
|
|
10
|
-
* if (ctx.shouldSkipHook("session-start")) return
|
|
4
|
+
* Utility class for OpenCode plugins providing file reading,
|
|
5
|
+
* JSONL parsing, and context building capabilities.
|
|
11
6
|
*/
|
|
12
7
|
|
|
13
8
|
import { existsSync, readFileSync, appendFileSync, readdirSync } from "fs"
|
|
14
9
|
import { isAbsolute, join } from "path"
|
|
15
|
-
import {
|
|
10
|
+
import { platform } from "os"
|
|
16
11
|
import { execSync } from "child_process"
|
|
17
12
|
|
|
18
|
-
// Python command: Windows uses 'python', macOS/Linux use 'python3'
|
|
19
13
|
const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
|
|
20
|
-
|
|
21
14
|
// Debug logging
|
|
22
15
|
const DEBUG_LOG = "/tmp/trellis-plugin-debug.log"
|
|
23
16
|
|
|
@@ -33,151 +26,17 @@ function debugLog(prefix, ...args) {
|
|
|
33
26
|
|
|
34
27
|
/**
|
|
35
28
|
* Trellis Context Manager
|
|
36
|
-
*
|
|
37
|
-
* Centralized logic for:
|
|
38
|
-
* - Detecting oh-my-opencode installation
|
|
39
|
-
* - Checking .claude/hooks/ presence
|
|
40
|
-
* - Determining which plugin should handle each hook
|
|
41
29
|
*/
|
|
42
30
|
export class TrellisContext {
|
|
43
31
|
constructor(directory) {
|
|
44
32
|
this.directory = directory
|
|
45
|
-
this._omoInstalled = null
|
|
46
|
-
this._omoHooksEnabled = null
|
|
47
|
-
this._claudeHooksPresent = {}
|
|
48
|
-
|
|
49
33
|
debugLog("context", "TrellisContext initialized", { directory })
|
|
50
34
|
}
|
|
51
35
|
|
|
52
|
-
// ============================================================
|
|
53
|
-
// oh-my-opencode Detection
|
|
54
|
-
// ============================================================
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Check if oh-my-opencode is installed
|
|
58
|
-
*
|
|
59
|
-
* Detection order:
|
|
60
|
-
* 1. Check if oh-my-opencode.json exists (most reliable)
|
|
61
|
-
* 2. Fallback: check opencode.json plugin list
|
|
62
|
-
*/
|
|
63
|
-
isOmoInstalled() {
|
|
64
|
-
if (this._omoInstalled !== null) {
|
|
65
|
-
return this._omoInstalled
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const configDir = join(homedir(), ".config", "opencode")
|
|
70
|
-
|
|
71
|
-
// Method 1: Check oh-my-opencode.json existence (omo-specific config)
|
|
72
|
-
const omoConfigPath = join(configDir, "oh-my-opencode.json")
|
|
73
|
-
if (existsSync(omoConfigPath)) {
|
|
74
|
-
this._omoInstalled = true
|
|
75
|
-
debugLog("context", "omo installed: oh-my-opencode.json exists")
|
|
76
|
-
return true
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Method 2: Fallback to plugin list check
|
|
80
|
-
const configPath = join(configDir, "opencode.json")
|
|
81
|
-
if (!existsSync(configPath)) {
|
|
82
|
-
this._omoInstalled = false
|
|
83
|
-
debugLog("context", "omo not installed: no config files")
|
|
84
|
-
return false
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const content = readFileSync(configPath, "utf-8")
|
|
88
|
-
const config = JSON.parse(content)
|
|
89
|
-
const plugins = config.plugin || []
|
|
90
|
-
|
|
91
|
-
this._omoInstalled = plugins.some(p =>
|
|
92
|
-
typeof p === "string" && p.toLowerCase().includes("oh-my-opencode")
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
debugLog("context", "omo installed (plugin list):", this._omoInstalled)
|
|
96
|
-
return this._omoInstalled
|
|
97
|
-
} catch (e) {
|
|
98
|
-
debugLog("context", "omo detection error:", e.message)
|
|
99
|
-
this._omoInstalled = false
|
|
100
|
-
return false
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Check if omo's claude_code.hooks is enabled
|
|
106
|
-
* Reads oh-my-opencode.json or defaults to true
|
|
107
|
-
*/
|
|
108
|
-
isOmoHooksEnabled() {
|
|
109
|
-
if (this._omoHooksEnabled !== null) {
|
|
110
|
-
return this._omoHooksEnabled
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (!this.isOmoInstalled()) {
|
|
114
|
-
this._omoHooksEnabled = false
|
|
115
|
-
return false
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
// Check global config
|
|
120
|
-
const globalConfig = join(homedir(), ".config", "opencode", "oh-my-opencode.json")
|
|
121
|
-
if (existsSync(globalConfig)) {
|
|
122
|
-
const content = readFileSync(globalConfig, "utf-8")
|
|
123
|
-
const config = JSON.parse(content)
|
|
124
|
-
if (config.claude_code?.hooks === false) {
|
|
125
|
-
this._omoHooksEnabled = false
|
|
126
|
-
debugLog("context", "omo hooks disabled in global config")
|
|
127
|
-
return false
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Check project config
|
|
132
|
-
const projectConfig = join(this.directory, "oh-my-opencode.json")
|
|
133
|
-
if (existsSync(projectConfig)) {
|
|
134
|
-
const content = readFileSync(projectConfig, "utf-8")
|
|
135
|
-
const config = JSON.parse(content)
|
|
136
|
-
if (config.claude_code?.hooks === false) {
|
|
137
|
-
this._omoHooksEnabled = false
|
|
138
|
-
debugLog("context", "omo hooks disabled in project config")
|
|
139
|
-
return false
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Default: enabled
|
|
144
|
-
this._omoHooksEnabled = true
|
|
145
|
-
debugLog("context", "omo hooks enabled (default)")
|
|
146
|
-
return true
|
|
147
|
-
} catch (e) {
|
|
148
|
-
debugLog("context", "omo hooks detection error:", e.message)
|
|
149
|
-
this._omoHooksEnabled = true // Default to enabled
|
|
150
|
-
return true
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ============================================================
|
|
155
|
-
// .claude/hooks/ Detection
|
|
156
|
-
// ============================================================
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Check if a specific .claude/hooks/ file exists
|
|
160
|
-
*/
|
|
161
|
-
hasClaudeHook(hookName) {
|
|
162
|
-
if (hookName in this._claudeHooksPresent) {
|
|
163
|
-
return this._claudeHooksPresent[hookName]
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const hookPath = join(this.directory, ".claude", "hooks", `${hookName}.py`)
|
|
167
|
-
const exists = existsSync(hookPath)
|
|
168
|
-
|
|
169
|
-
this._claudeHooksPresent[hookName] = exists
|
|
170
|
-
debugLog("context", `claude hook ${hookName}:`, exists)
|
|
171
|
-
return exists
|
|
172
|
-
}
|
|
173
|
-
|
|
174
36
|
// ============================================================
|
|
175
37
|
// Trellis Project Detection
|
|
176
38
|
// ============================================================
|
|
177
39
|
|
|
178
|
-
/**
|
|
179
|
-
* Check if this is a Trellis-managed project
|
|
180
|
-
*/
|
|
181
40
|
isTrellisProject() {
|
|
182
41
|
return existsSync(join(this.directory, ".trellis"))
|
|
183
42
|
}
|
|
@@ -237,54 +96,10 @@ export class TrellisContext {
|
|
|
237
96
|
return join(this.directory, ".trellis", "tasks", normalized)
|
|
238
97
|
}
|
|
239
98
|
|
|
240
|
-
// ============================================================
|
|
241
|
-
// Hook Decision Logic
|
|
242
|
-
// ============================================================
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Determine if our plugin should skip this hook
|
|
246
|
-
* (because omo will handle it via .claude/hooks/)
|
|
247
|
-
*
|
|
248
|
-
* @param {string} hookName - Hook name without extension (e.g., "session-start")
|
|
249
|
-
* @returns {boolean} - true if we should skip, false if we should handle
|
|
250
|
-
*/
|
|
251
|
-
shouldSkipHook(hookName) {
|
|
252
|
-
// Not a Trellis project? Skip.
|
|
253
|
-
if (!this.isTrellisProject()) {
|
|
254
|
-
debugLog("context", `shouldSkipHook(${hookName}): skip - not Trellis project`)
|
|
255
|
-
return true
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// omo not installed? We handle it.
|
|
259
|
-
if (!this.isOmoInstalled()) {
|
|
260
|
-
debugLog("context", `shouldSkipHook(${hookName}): handle - omo not installed`)
|
|
261
|
-
return false
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// omo installed but hooks disabled? We handle it.
|
|
265
|
-
if (!this.isOmoHooksEnabled()) {
|
|
266
|
-
debugLog("context", `shouldSkipHook(${hookName}): handle - omo hooks disabled`)
|
|
267
|
-
return false
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// omo installed + hooks enabled + .claude/hooks/ exists? Skip (omo handles).
|
|
271
|
-
if (this.hasClaudeHook(hookName)) {
|
|
272
|
-
debugLog("context", `shouldSkipHook(${hookName}): skip - omo will handle via .claude/hooks/`)
|
|
273
|
-
return true
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// omo installed but no .claude/hooks/ file? We handle it.
|
|
277
|
-
debugLog("context", `shouldSkipHook(${hookName}): handle - no .claude/hooks/ file`)
|
|
278
|
-
return false
|
|
279
|
-
}
|
|
280
|
-
|
|
281
99
|
// ============================================================
|
|
282
100
|
// File Reading Utilities
|
|
283
101
|
// ============================================================
|
|
284
102
|
|
|
285
|
-
/**
|
|
286
|
-
* Read a file, return null on error
|
|
287
|
-
*/
|
|
288
103
|
readFile(filePath) {
|
|
289
104
|
try {
|
|
290
105
|
if (existsSync(filePath)) {
|
|
@@ -296,16 +111,10 @@ export class TrellisContext {
|
|
|
296
111
|
return null
|
|
297
112
|
}
|
|
298
113
|
|
|
299
|
-
/**
|
|
300
|
-
* Read a file relative to project directory
|
|
301
|
-
*/
|
|
302
114
|
readProjectFile(relativePath) {
|
|
303
115
|
return this.readFile(join(this.directory, relativePath))
|
|
304
116
|
}
|
|
305
117
|
|
|
306
|
-
/**
|
|
307
|
-
* Run a Python script and return output
|
|
308
|
-
*/
|
|
309
118
|
runScript(scriptPath, cwd = null) {
|
|
310
119
|
try {
|
|
311
120
|
const result = execSync(`${PYTHON_CMD} "${scriptPath}"`, {
|
|
@@ -324,12 +133,6 @@ export class TrellisContext {
|
|
|
324
133
|
// JSONL Reading
|
|
325
134
|
// ============================================================
|
|
326
135
|
|
|
327
|
-
/**
|
|
328
|
-
* Read all .md files in a directory
|
|
329
|
-
* @param {string} dirPath - Directory path relative to project root
|
|
330
|
-
* @param {number} maxFiles - Max files to read (prevent huge directories)
|
|
331
|
-
* @returns {Array<{path: string, content: string}>}
|
|
332
|
-
*/
|
|
333
136
|
readDirectoryMdFiles(dirPath, maxFiles = 20) {
|
|
334
137
|
const results = []
|
|
335
138
|
const fullPath = join(this.directory, dirPath)
|
|
@@ -379,11 +182,9 @@ export class TrellisContext {
|
|
|
379
182
|
if (!file) continue
|
|
380
183
|
|
|
381
184
|
if (entryType === "directory") {
|
|
382
|
-
// Read all .md files in directory
|
|
383
185
|
const dirEntries = this.readDirectoryMdFiles(file)
|
|
384
186
|
results.push(...dirEntries)
|
|
385
187
|
} else {
|
|
386
|
-
// Read single file
|
|
387
188
|
const fullPath = join(this.directory, file)
|
|
388
189
|
const fileContent = this.readFile(fullPath)
|
|
389
190
|
if (fileContent) {
|
|
@@ -397,74 +198,29 @@ export class TrellisContext {
|
|
|
397
198
|
return results
|
|
398
199
|
}
|
|
399
200
|
|
|
400
|
-
/**
|
|
401
|
-
* Build context string from file entries
|
|
402
|
-
*/
|
|
403
201
|
buildContextFromEntries(entries) {
|
|
404
202
|
return entries.map(e => `=== ${e.path} ===\n${e.content}`).join("\n\n")
|
|
405
203
|
}
|
|
406
204
|
}
|
|
407
205
|
|
|
408
206
|
// ============================================================
|
|
409
|
-
// Context Collector (for
|
|
207
|
+
// Context Collector (for session deduplication)
|
|
410
208
|
// ============================================================
|
|
411
209
|
|
|
412
|
-
/**
|
|
413
|
-
* Simple context collector for cross-hook communication
|
|
414
|
-
* Similar to oh-my-opencode's contextCollector
|
|
415
|
-
*/
|
|
416
210
|
class ContextCollector {
|
|
417
211
|
constructor() {
|
|
418
|
-
this.pending = new Map()
|
|
419
212
|
this.processed = new Set()
|
|
420
213
|
}
|
|
421
214
|
|
|
422
|
-
/**
|
|
423
|
-
* Store context for a session
|
|
424
|
-
*/
|
|
425
|
-
store(sessionID, content) {
|
|
426
|
-
this.pending.set(sessionID, {
|
|
427
|
-
content,
|
|
428
|
-
timestamp: Date.now()
|
|
429
|
-
})
|
|
430
|
-
debugLog("collector", "stored context for session:", sessionID, "length:", content.length)
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Check if session has pending context
|
|
435
|
-
*/
|
|
436
|
-
hasPending(sessionID) {
|
|
437
|
-
return this.pending.has(sessionID)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Get and consume pending context
|
|
442
|
-
*/
|
|
443
|
-
consume(sessionID) {
|
|
444
|
-
const pending = this.pending.get(sessionID)
|
|
445
|
-
this.pending.delete(sessionID)
|
|
446
|
-
return pending
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Mark session as processed (for first-message-only injection)
|
|
451
|
-
*/
|
|
452
215
|
markProcessed(sessionID) {
|
|
453
216
|
this.processed.add(sessionID)
|
|
454
217
|
}
|
|
455
218
|
|
|
456
|
-
/**
|
|
457
|
-
* Check if session was already processed
|
|
458
|
-
*/
|
|
459
219
|
isProcessed(sessionID) {
|
|
460
220
|
return this.processed.has(sessionID)
|
|
461
221
|
}
|
|
462
222
|
|
|
463
|
-
/**
|
|
464
|
-
* Clear session state
|
|
465
|
-
*/
|
|
466
223
|
clear(sessionID) {
|
|
467
|
-
this.pending.delete(sessionID)
|
|
468
224
|
this.processed.delete(sessionID)
|
|
469
225
|
}
|
|
470
226
|
}
|