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