@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.
@@ -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
  }