@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.
Files changed (56) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/index.js +1 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/init.d.ts +1 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +132 -4
  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/droid.d.ts +5 -0
  12. package/dist/configurators/droid.d.ts.map +1 -0
  13. package/dist/configurators/droid.js +48 -0
  14. package/dist/configurators/droid.js.map +1 -0
  15. package/dist/configurators/index.d.ts.map +1 -1
  16. package/dist/configurators/index.js +13 -0
  17. package/dist/configurators/index.js.map +1 -1
  18. package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
  19. package/dist/migrations/manifests/0.4.0-rc.0.json +9 -0
  20. package/dist/migrations/manifests/0.4.0-rc.1.json +9 -0
  21. package/dist/templates/claude/hooks/ralph-loop.py +10 -9
  22. package/dist/templates/claude/hooks/session-start.py +29 -12
  23. package/dist/templates/claude/hooks/statusline.py +7 -0
  24. package/dist/templates/codex/hooks/session-start.py +29 -14
  25. package/dist/templates/copilot/hooks/session-start.py +29 -4
  26. package/dist/templates/droid/commands/trellis/before-dev.md +33 -0
  27. package/dist/templates/droid/commands/trellis/brainstorm.md +491 -0
  28. package/dist/templates/droid/commands/trellis/break-loop.md +111 -0
  29. package/dist/templates/droid/commands/trellis/check-cross-layer.md +157 -0
  30. package/dist/templates/droid/commands/trellis/check.md +29 -0
  31. package/dist/templates/droid/commands/trellis/create-command.md +158 -0
  32. package/dist/templates/droid/commands/trellis/finish-work.md +147 -0
  33. package/dist/templates/droid/commands/trellis/integrate-skill.md +223 -0
  34. package/dist/templates/droid/commands/trellis/onboard.md +362 -0
  35. package/dist/templates/droid/commands/trellis/record-session.md +66 -0
  36. package/dist/templates/droid/commands/trellis/start.md +377 -0
  37. package/dist/templates/droid/commands/trellis/update-spec.md +358 -0
  38. package/dist/templates/droid/index.d.ts +27 -0
  39. package/dist/templates/droid/index.d.ts.map +1 -0
  40. package/dist/templates/droid/index.js +47 -0
  41. package/dist/templates/droid/index.js.map +1 -0
  42. package/dist/templates/extract.d.ts +11 -0
  43. package/dist/templates/extract.d.ts.map +1 -1
  44. package/dist/templates/extract.js +19 -0
  45. package/dist/templates/extract.js.map +1 -1
  46. package/dist/templates/iflow/hooks/session-start.py +29 -12
  47. package/dist/templates/opencode/lib/trellis-context.js +4 -248
  48. package/dist/templates/opencode/plugins/inject-subagent-context.js +71 -121
  49. package/dist/templates/opencode/plugins/session-start.js +143 -119
  50. package/dist/templates/trellis/scripts/common/cli_adapter.py +27 -2
  51. package/dist/templates/trellis/workflow.md +17 -4
  52. package/dist/types/ai-tools.d.ts +3 -3
  53. package/dist/types/ai-tools.d.ts.map +1 -1
  54. package/dist/types/ai-tools.js +9 -1
  55. package/dist/types/ai-tools.js.map +1 -1
  56. package/package.json +1 -1
@@ -1,23 +1,16 @@
1
1
  /**
2
2
  * Trellis Context Manager
3
3
  *
4
- * Unified context management for OpenCode plugins.
5
- * Handles detection of oh-my-opencode, .claude/hooks/, and other edge cases.
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 { homedir, platform } from "os"
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 synthetic message injection)
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" // finish uses check agent
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 async ({ directory }) => {
452
- const ctx = new TrellisContext(directory)
453
- debugLog("inject", "Plugin loaded, directory:", directory)
454
-
455
- return {
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
- const args = output?.args || {}
484
- const subagentType = args.subagent_type
485
- const originalPrompt = args.prompt || ""
433
+ return {
434
+ "tool.execute.before": async (input, output) => {
435
+ try {
436
+ debugLog("inject", "tool.execute.before called, tool:", input?.tool)
486
437
 
487
- debugLog("inject", "Task tool called, subagent_type:", subagentType)
438
+ const toolName = input?.tool?.toLowerCase()
439
+ if (toolName !== "task") {
440
+ return
441
+ }
488
442
 
489
- // Only handle supported agent types
490
- if (!AGENTS_ALL.includes(subagentType)) {
491
- debugLog("inject", "Skipping - unsupported subagent_type")
492
- return
493
- }
443
+ const args = output?.args
444
+ if (!args) return
494
445
 
495
- // Check if we should skip (omo will handle)
496
- if (ctx.shouldSkipHook("inject-subagent-context")) {
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
- // Read current task
502
- const taskDir = ctx.getCurrentTask()
449
+ debugLog("inject", "Task tool called, subagent_type:", subagentType)
503
450
 
504
- // Agents requiring task directory
505
- if (AGENTS_REQUIRE_TASK.includes(subagentType)) {
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
- // Update current_phase in task.json
517
- updateCurrentPhase(ctx, taskDir, subagentType)
518
- }
456
+ // Read current task
457
+ const taskDir = ctx.getCurrentTask()
519
458
 
520
- // Check for [finish] marker
521
- const isFinish = originalPrompt.toLowerCase().includes("[finish]")
522
-
523
- // Get context based on agent type
524
- let context = ""
525
- switch (subagentType) {
526
- case "implement":
527
- context = getImplementContext(ctx, taskDir)
528
- break
529
- case "check":
530
- // Use finish context for [finish] phase (lighter, focused on final verification)
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
- if (!context) {
545
- debugLog("inject", "No context to inject")
546
- return
547
- }
471
+ updateCurrentPhase(ctx, taskDir, subagentType)
472
+ }
548
473
 
549
- // Build enhanced prompt
550
- const newPrompt = buildPrompt(subagentType, originalPrompt, context, isFinish)
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
- // Update the tool input
553
- output.args = {
554
- ...args,
555
- prompt: newPrompt
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
- debugLog("inject", "Injected context for", subagentType, "prompt length:", newPrompt.length)
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
- } catch (error) {
561
- debugLog("inject", "Error in tool.execute.before:", error.message, error.stack)
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
  }