@mindfoldhq/trellis 0.4.0-beta.9 → 0.4.0-rc.1
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 +3 -3
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- 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/configurators/droid.d.ts +5 -0
- package/dist/configurators/droid.d.ts.map +1 -0
- package/dist/configurators/droid.js +48 -0
- package/dist/configurators/droid.js.map +1 -0
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +13 -0
- package/dist/configurators/index.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/migrations/manifests/0.4.0-rc.1.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/droid/commands/trellis/before-dev.md +33 -0
- package/dist/templates/droid/commands/trellis/brainstorm.md +491 -0
- package/dist/templates/droid/commands/trellis/break-loop.md +111 -0
- package/dist/templates/droid/commands/trellis/check-cross-layer.md +157 -0
- package/dist/templates/droid/commands/trellis/check.md +29 -0
- package/dist/templates/droid/commands/trellis/create-command.md +158 -0
- package/dist/templates/droid/commands/trellis/finish-work.md +147 -0
- package/dist/templates/droid/commands/trellis/integrate-skill.md +223 -0
- package/dist/templates/droid/commands/trellis/onboard.md +362 -0
- package/dist/templates/droid/commands/trellis/record-session.md +66 -0
- package/dist/templates/droid/commands/trellis/start.md +377 -0
- package/dist/templates/droid/commands/trellis/update-spec.md +358 -0
- package/dist/templates/droid/index.d.ts +27 -0
- package/dist/templates/droid/index.d.ts.map +1 -0
- package/dist/templates/droid/index.js +47 -0
- package/dist/templates/droid/index.js.map +1 -0
- package/dist/templates/extract.d.ts +11 -0
- package/dist/templates/extract.d.ts.map +1 -1
- package/dist/templates/extract.js +19 -0
- package/dist/templates/extract.js.map +1 -1
- 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/scripts/common/cli_adapter.py +27 -2
- package/dist/templates/trellis/workflow.md +17 -4
- package/dist/types/ai-tools.d.ts +3 -3
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +9 -1
- package/dist/types/ai-tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Injects context when Task tool is called with supported subagent types.
|
|
5
5
|
* Uses OpenCode's tool.execute.before hook.
|
|
6
|
-
*
|
|
7
|
-
* Compatibility:
|
|
8
|
-
* - If oh-my-opencode handles via .claude/hooks/, this plugin skips
|
|
9
|
-
* - Otherwise, this plugin handles injection
|
|
10
6
|
*/
|
|
11
7
|
|
|
12
8
|
import { existsSync, writeFileSync, readdirSync } from "fs"
|
|
@@ -36,21 +32,18 @@ function updateCurrentPhase(ctx, taskDir, subagentType) {
|
|
|
36
32
|
const currentPhase = taskData.current_phase || 0
|
|
37
33
|
const nextActions = taskData.next_action || []
|
|
38
34
|
|
|
39
|
-
// Map action names to subagent types
|
|
40
35
|
const actionToAgent = {
|
|
41
36
|
"implement": "implement",
|
|
42
37
|
"check": "check",
|
|
43
|
-
"finish": "check"
|
|
38
|
+
"finish": "check"
|
|
44
39
|
}
|
|
45
40
|
|
|
46
|
-
// Find the next phase that matches this subagent_type
|
|
47
41
|
let newPhase = null
|
|
48
42
|
for (const action of nextActions) {
|
|
49
43
|
const phaseNum = action.phase || 0
|
|
50
44
|
const actionName = action.action || ""
|
|
51
45
|
const expectedAgent = actionToAgent[actionName]
|
|
52
46
|
|
|
53
|
-
// Only consider phases after current_phase
|
|
54
47
|
if (phaseNum > currentPhase && expectedAgent === subagentType) {
|
|
55
48
|
newPhase = phaseNum
|
|
56
49
|
break
|
|
@@ -73,12 +66,10 @@ function updateCurrentPhase(ctx, taskDir, subagentType) {
|
|
|
73
66
|
function getImplementContext(ctx, taskDir) {
|
|
74
67
|
const parts = []
|
|
75
68
|
|
|
76
|
-
// 1. Read implement.jsonl (or fallback to spec.jsonl)
|
|
77
69
|
let jsonlPath = join(ctx.directory, taskDir, "implement.jsonl")
|
|
78
70
|
let entries = ctx.readJsonlWithFiles(jsonlPath)
|
|
79
71
|
|
|
80
72
|
if (entries.length === 0) {
|
|
81
|
-
// Fallback to spec.jsonl
|
|
82
73
|
jsonlPath = join(ctx.directory, taskDir, "spec.jsonl")
|
|
83
74
|
entries = ctx.readJsonlWithFiles(jsonlPath)
|
|
84
75
|
}
|
|
@@ -87,13 +78,11 @@ function getImplementContext(ctx, taskDir) {
|
|
|
87
78
|
parts.push(ctx.buildContextFromEntries(entries))
|
|
88
79
|
}
|
|
89
80
|
|
|
90
|
-
// 2. Requirements document
|
|
91
81
|
const prd = ctx.readProjectFile(join(taskDir, "prd.md"))
|
|
92
82
|
if (prd) {
|
|
93
83
|
parts.push(`=== ${taskDir}/prd.md (Requirements) ===\n${prd}`)
|
|
94
84
|
}
|
|
95
85
|
|
|
96
|
-
// 3. Technical design
|
|
97
86
|
const info = ctx.readProjectFile(join(taskDir, "info.md"))
|
|
98
87
|
if (info) {
|
|
99
88
|
parts.push(`=== ${taskDir}/info.md (Technical Design) ===\n${info}`)
|
|
@@ -108,14 +97,12 @@ function getImplementContext(ctx, taskDir) {
|
|
|
108
97
|
function getCheckContext(ctx, taskDir) {
|
|
109
98
|
const parts = []
|
|
110
99
|
|
|
111
|
-
// 1. Read check.jsonl
|
|
112
100
|
const jsonlPath = join(ctx.directory, taskDir, "check.jsonl")
|
|
113
101
|
const entries = ctx.readJsonlWithFiles(jsonlPath)
|
|
114
102
|
|
|
115
103
|
if (entries.length > 0) {
|
|
116
104
|
parts.push(ctx.buildContextFromEntries(entries))
|
|
117
105
|
} else {
|
|
118
|
-
// Fallback: hardcoded check files + spec.jsonl
|
|
119
106
|
const checkFiles = [
|
|
120
107
|
[".opencode/commands/trellis/finish-work.md", "Finish work checklist"],
|
|
121
108
|
[".opencode/commands/trellis/check-cross-layer.md", "Cross-layer check spec"],
|
|
@@ -128,7 +115,6 @@ function getCheckContext(ctx, taskDir) {
|
|
|
128
115
|
}
|
|
129
116
|
}
|
|
130
117
|
|
|
131
|
-
// Add spec.jsonl
|
|
132
118
|
const specJsonlPath = join(ctx.directory, taskDir, "spec.jsonl")
|
|
133
119
|
const specEntries = ctx.readJsonlWithFiles(specJsonlPath)
|
|
134
120
|
for (const entry of specEntries) {
|
|
@@ -136,7 +122,6 @@ function getCheckContext(ctx, taskDir) {
|
|
|
136
122
|
}
|
|
137
123
|
}
|
|
138
124
|
|
|
139
|
-
// 2. Requirements document
|
|
140
125
|
const prd = ctx.readProjectFile(join(taskDir, "prd.md"))
|
|
141
126
|
if (prd) {
|
|
142
127
|
parts.push(`=== ${taskDir}/prd.md (Requirements - for understanding intent) ===\n${prd}`)
|
|
@@ -151,27 +136,23 @@ function getCheckContext(ctx, taskDir) {
|
|
|
151
136
|
function getFinishContext(ctx, taskDir) {
|
|
152
137
|
const parts = []
|
|
153
138
|
|
|
154
|
-
// 1. Try finish.jsonl first
|
|
155
139
|
const jsonlPath = join(ctx.directory, taskDir, "finish.jsonl")
|
|
156
140
|
const entries = ctx.readJsonlWithFiles(jsonlPath)
|
|
157
141
|
|
|
158
142
|
if (entries.length > 0) {
|
|
159
143
|
parts.push(ctx.buildContextFromEntries(entries))
|
|
160
144
|
} else {
|
|
161
|
-
// Fallback: only finish-work.md (lightweight)
|
|
162
145
|
const finishWork = ctx.readProjectFile(".opencode/commands/trellis/finish-work.md")
|
|
163
146
|
if (finishWork) {
|
|
164
147
|
parts.push(`=== .opencode/commands/trellis/finish-work.md (Finish checklist) ===\n${finishWork}`)
|
|
165
148
|
}
|
|
166
149
|
}
|
|
167
150
|
|
|
168
|
-
// 2. Spec update process (for active spec sync)
|
|
169
151
|
const updateSpec = ctx.readProjectFile(".opencode/commands/trellis/update-spec.md")
|
|
170
152
|
if (updateSpec) {
|
|
171
153
|
parts.push(`=== .opencode/commands/trellis/update-spec.md (Spec update process) ===\n${updateSpec}`)
|
|
172
154
|
}
|
|
173
155
|
|
|
174
|
-
// 3. Requirements document (for verifying requirements are met)
|
|
175
156
|
const prd = ctx.readProjectFile(join(taskDir, "prd.md"))
|
|
176
157
|
if (prd) {
|
|
177
158
|
parts.push(`=== ${taskDir}/prd.md (Requirements - verify all met) ===\n${prd}`)
|
|
@@ -186,14 +167,12 @@ function getFinishContext(ctx, taskDir) {
|
|
|
186
167
|
function getDebugContext(ctx, taskDir) {
|
|
187
168
|
const parts = []
|
|
188
169
|
|
|
189
|
-
// 1. Read debug.jsonl (or fallback to spec.jsonl + check files)
|
|
190
170
|
const jsonlPath = join(ctx.directory, taskDir, "debug.jsonl")
|
|
191
171
|
const entries = ctx.readJsonlWithFiles(jsonlPath)
|
|
192
172
|
|
|
193
173
|
if (entries.length > 0) {
|
|
194
174
|
parts.push(ctx.buildContextFromEntries(entries))
|
|
195
175
|
} else {
|
|
196
|
-
// Fallback: use spec.jsonl + hardcoded check files
|
|
197
176
|
const specJsonlPath = join(ctx.directory, taskDir, "spec.jsonl")
|
|
198
177
|
const specEntries = ctx.readJsonlWithFiles(specJsonlPath)
|
|
199
178
|
for (const entry of specEntries) {
|
|
@@ -212,7 +191,6 @@ function getDebugContext(ctx, taskDir) {
|
|
|
212
191
|
}
|
|
213
192
|
}
|
|
214
193
|
|
|
215
|
-
// 2. Codex review output (if exists)
|
|
216
194
|
const codex = ctx.readProjectFile(join(taskDir, "codex-review-output.txt"))
|
|
217
195
|
if (codex) {
|
|
218
196
|
parts.push(`=== ${taskDir}/codex-review-output.txt (Codex Review Results) ===\n${codex}`)
|
|
@@ -240,11 +218,9 @@ function getResearchContext(ctx, taskDir) {
|
|
|
240
218
|
|
|
241
219
|
for (const entry of entries) {
|
|
242
220
|
const entryPath = join(specFull, entry.name)
|
|
243
|
-
// Check if this is a direct spec layer (has index.md)
|
|
244
221
|
if (existsSync(join(entryPath, "index.md"))) {
|
|
245
222
|
structureLines.push(`├── ${entry.name}/`)
|
|
246
223
|
} else {
|
|
247
|
-
// Check for nested package dirs (monorepo)
|
|
248
224
|
try {
|
|
249
225
|
const nested = readdirSync(entryPath, { withFileTypes: true })
|
|
250
226
|
.filter(d => d.isDirectory() && existsSync(join(entryPath, d.name, "index.md")))
|
|
@@ -448,117 +424,91 @@ ${originalPrompt}
|
|
|
448
424
|
return templates[agentType] || originalPrompt
|
|
449
425
|
}
|
|
450
426
|
|
|
451
|
-
export default
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// ==========================================================================
|
|
457
|
-
// ⚠️ KNOWN LIMITATION: OpenCode project-level plugins cannot intercept subagents
|
|
458
|
-
//
|
|
459
|
-
// This hook will NOT be triggered because:
|
|
460
|
-
// 1. Project-level plugins (.opencode/plugin/) don't support tool.execute.before
|
|
461
|
-
// 2. Only global plugins (npm packages) have full hook permissions
|
|
462
|
-
// 3. This is a known OpenCode architecture limitation (see Issue #5894)
|
|
463
|
-
//
|
|
464
|
-
// SOLUTION: Trellis + OpenCode users must install oh-my-opencode (omo)
|
|
465
|
-
// - omo is a global plugin with full hook permissions
|
|
466
|
-
// - omo reads .claude/settings.json and executes Python hooks
|
|
467
|
-
// - .claude/hooks/inject-subagent-context.py handles the actual injection
|
|
468
|
-
//
|
|
469
|
-
// References:
|
|
470
|
-
// - https://github.com/sst/opencode/issues/5894 (plugin hooks don't intercept subagent)
|
|
471
|
-
// - https://github.com/sst/opencode/issues/2588 (subagent inherit context)
|
|
472
|
-
// ==========================================================================
|
|
473
|
-
"tool.execute.before": async (input, output) => {
|
|
474
|
-
try {
|
|
475
|
-
debugLog("inject", "tool.execute.before called, tool:", input?.tool)
|
|
476
|
-
|
|
477
|
-
// Only handle Task tool
|
|
478
|
-
const toolName = input?.tool?.toLowerCase()
|
|
479
|
-
if (toolName !== "task") {
|
|
480
|
-
return
|
|
481
|
-
}
|
|
427
|
+
export default {
|
|
428
|
+
id: "trellis.inject-subagent-context",
|
|
429
|
+
server: async ({ directory }) => {
|
|
430
|
+
const ctx = new TrellisContext(directory)
|
|
431
|
+
debugLog("inject", "Plugin loaded, directory:", directory)
|
|
482
432
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
433
|
+
return {
|
|
434
|
+
"tool.execute.before": async (input, output) => {
|
|
435
|
+
try {
|
|
436
|
+
debugLog("inject", "tool.execute.before called, tool:", input?.tool)
|
|
486
437
|
|
|
487
|
-
|
|
438
|
+
const toolName = input?.tool?.toLowerCase()
|
|
439
|
+
if (toolName !== "task") {
|
|
440
|
+
return
|
|
441
|
+
}
|
|
488
442
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
debugLog("inject", "Skipping - unsupported subagent_type")
|
|
492
|
-
return
|
|
493
|
-
}
|
|
443
|
+
const args = output?.args
|
|
444
|
+
if (!args) return
|
|
494
445
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
debugLog("inject", "Skipping - omo will handle via .claude/hooks/")
|
|
498
|
-
return
|
|
499
|
-
}
|
|
446
|
+
const subagentType = args.subagent_type
|
|
447
|
+
const originalPrompt = args.prompt || ""
|
|
500
448
|
|
|
501
|
-
|
|
502
|
-
const taskDir = ctx.getCurrentTask()
|
|
449
|
+
debugLog("inject", "Task tool called, subagent_type:", subagentType)
|
|
503
450
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if (!taskDir) {
|
|
507
|
-
debugLog("inject", "Skipping - no current task")
|
|
508
|
-
return
|
|
509
|
-
}
|
|
510
|
-
const taskDirFull = join(directory, taskDir)
|
|
511
|
-
if (!existsSync(taskDirFull)) {
|
|
512
|
-
debugLog("inject", "Skipping - task directory not found")
|
|
451
|
+
if (!AGENTS_ALL.includes(subagentType)) {
|
|
452
|
+
debugLog("inject", "Skipping - unsupported subagent_type")
|
|
513
453
|
return
|
|
514
454
|
}
|
|
515
455
|
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
}
|
|
456
|
+
// Read current task
|
|
457
|
+
const taskDir = ctx.getCurrentTask()
|
|
519
458
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
// Use check context for regular check (full specs for self-fix loop)
|
|
532
|
-
context = isFinish
|
|
533
|
-
? getFinishContext(ctx, taskDir)
|
|
534
|
-
: getCheckContext(ctx, taskDir)
|
|
535
|
-
break
|
|
536
|
-
case "debug":
|
|
537
|
-
context = getDebugContext(ctx, taskDir)
|
|
538
|
-
break
|
|
539
|
-
case "research":
|
|
540
|
-
context = getResearchContext(ctx, taskDir)
|
|
541
|
-
break
|
|
542
|
-
}
|
|
459
|
+
// Agents requiring task directory
|
|
460
|
+
if (AGENTS_REQUIRE_TASK.includes(subagentType)) {
|
|
461
|
+
if (!taskDir) {
|
|
462
|
+
debugLog("inject", "Skipping - no current task")
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
const taskDirFull = join(directory, taskDir)
|
|
466
|
+
if (!existsSync(taskDirFull)) {
|
|
467
|
+
debugLog("inject", "Skipping - task directory not found")
|
|
468
|
+
return
|
|
469
|
+
}
|
|
543
470
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
return
|
|
547
|
-
}
|
|
471
|
+
updateCurrentPhase(ctx, taskDir, subagentType)
|
|
472
|
+
}
|
|
548
473
|
|
|
549
|
-
|
|
550
|
-
|
|
474
|
+
// Check for [finish] marker
|
|
475
|
+
const isFinish = originalPrompt.toLowerCase().includes("[finish]")
|
|
476
|
+
|
|
477
|
+
// Get context based on agent type
|
|
478
|
+
let context = ""
|
|
479
|
+
switch (subagentType) {
|
|
480
|
+
case "implement":
|
|
481
|
+
context = getImplementContext(ctx, taskDir)
|
|
482
|
+
break
|
|
483
|
+
case "check":
|
|
484
|
+
context = isFinish
|
|
485
|
+
? getFinishContext(ctx, taskDir)
|
|
486
|
+
: getCheckContext(ctx, taskDir)
|
|
487
|
+
break
|
|
488
|
+
case "debug":
|
|
489
|
+
context = getDebugContext(ctx, taskDir)
|
|
490
|
+
break
|
|
491
|
+
case "research":
|
|
492
|
+
context = getResearchContext(ctx, taskDir)
|
|
493
|
+
break
|
|
494
|
+
}
|
|
551
495
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
496
|
+
if (!context) {
|
|
497
|
+
debugLog("inject", "No context to inject")
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const newPrompt = buildPrompt(subagentType, originalPrompt, context, isFinish)
|
|
557
502
|
|
|
558
|
-
|
|
503
|
+
// Mutate args in-place — whole-object replacement does NOT work for the task tool
|
|
504
|
+
// because the runtime holds a local reference to the same args object.
|
|
505
|
+
args.prompt = newPrompt
|
|
559
506
|
|
|
560
|
-
|
|
561
|
-
|
|
507
|
+
debugLog("inject", "Injected context for", subagentType, "prompt length:", newPrompt.length)
|
|
508
|
+
|
|
509
|
+
} catch (error) {
|
|
510
|
+
debugLog("inject", "Error in tool.execute.before:", error.message, error.stack)
|
|
511
|
+
}
|
|
562
512
|
}
|
|
563
513
|
}
|
|
564
514
|
}
|