@mindfoldhq/trellis 0.4.0-beta.8 → 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.
Files changed (99) hide show
  1. package/README.md +10 -5
  2. package/dist/cli/index.js +2 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/init.d.ts +2 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +165 -13
  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/codex.d.ts.map +1 -1
  12. package/dist/configurators/codex.js +2 -1
  13. package/dist/configurators/codex.js.map +1 -1
  14. package/dist/configurators/copilot.d.ts +9 -0
  15. package/dist/configurators/copilot.d.ts.map +1 -0
  16. package/dist/configurators/copilot.js +34 -0
  17. package/dist/configurators/copilot.js.map +1 -0
  18. package/dist/configurators/index.d.ts.map +1 -1
  19. package/dist/configurators/index.js +32 -1
  20. package/dist/configurators/index.js.map +1 -1
  21. package/dist/configurators/windsurf.d.ts +8 -0
  22. package/dist/configurators/windsurf.d.ts.map +1 -0
  23. package/dist/configurators/windsurf.js +18 -0
  24. package/dist/configurators/windsurf.js.map +1 -0
  25. package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
  26. package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
  27. package/dist/migrations/manifests/0.4.0-rc.0.json +9 -0
  28. package/dist/templates/claude/hooks/inject-subagent-context.py +8 -1
  29. package/dist/templates/claude/hooks/ralph-loop.py +18 -10
  30. package/dist/templates/claude/hooks/session-start.py +60 -19
  31. package/dist/templates/claude/hooks/statusline.py +218 -0
  32. package/dist/templates/claude/settings.json +4 -0
  33. package/dist/templates/codex/hooks/session-start.py +60 -21
  34. package/dist/templates/codex/hooks.json +1 -1
  35. package/dist/templates/copilot/hooks/session-start.py +243 -0
  36. package/dist/templates/copilot/hooks.json +11 -0
  37. package/dist/templates/copilot/index.d.ts +23 -0
  38. package/dist/templates/copilot/index.d.ts.map +1 -0
  39. package/dist/templates/copilot/index.js +54 -0
  40. package/dist/templates/copilot/index.js.map +1 -0
  41. package/dist/templates/copilot/prompts/before-dev.prompt.md +33 -0
  42. package/dist/templates/copilot/prompts/brainstorm.prompt.md +491 -0
  43. package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
  44. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +157 -0
  45. package/dist/templates/copilot/prompts/check.prompt.md +29 -0
  46. package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
  47. package/dist/templates/copilot/prompts/finish-work.prompt.md +157 -0
  48. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
  49. package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
  50. package/dist/templates/copilot/prompts/parallel.prompt.md +196 -0
  51. package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
  52. package/dist/templates/copilot/prompts/start.prompt.md +397 -0
  53. package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
  54. package/dist/templates/extract.d.ts +18 -0
  55. package/dist/templates/extract.d.ts.map +1 -1
  56. package/dist/templates/extract.js +32 -0
  57. package/dist/templates/extract.js.map +1 -1
  58. package/dist/templates/iflow/hooks/inject-subagent-context.py +8 -1
  59. package/dist/templates/iflow/hooks/ralph-loop.py +8 -1
  60. package/dist/templates/iflow/hooks/session-start.py +60 -19
  61. package/dist/templates/markdown/spec/backend/directory-structure.md +1 -1
  62. package/dist/templates/opencode/agents/dispatch.md +20 -19
  63. package/dist/templates/opencode/lib/trellis-context.js +35 -239
  64. package/dist/templates/opencode/plugins/inject-subagent-context.js +71 -121
  65. package/dist/templates/opencode/plugins/session-start.js +150 -146
  66. package/dist/templates/trellis/scripts/add_session.py +6 -1
  67. package/dist/templates/trellis/scripts/common/__init__.py +2 -0
  68. package/dist/templates/trellis/scripts/common/cli_adapter.py +87 -9
  69. package/dist/templates/trellis/scripts/common/paths.py +57 -6
  70. package/dist/templates/trellis/scripts/common/task_store.py +6 -4
  71. package/dist/templates/trellis/scripts/common/task_utils.py +14 -8
  72. package/dist/templates/trellis/scripts/multi_agent/start.py +9 -5
  73. package/dist/templates/trellis/scripts/task.py +1 -1
  74. package/dist/templates/trellis/workflow.md +17 -4
  75. package/dist/templates/windsurf/index.d.ts +21 -0
  76. package/dist/templates/windsurf/index.d.ts.map +1 -0
  77. package/dist/templates/windsurf/index.js +44 -0
  78. package/dist/templates/windsurf/index.js.map +1 -0
  79. package/dist/templates/windsurf/workflows/trellis-before-dev.md +31 -0
  80. package/dist/templates/windsurf/workflows/trellis-brainstorm.md +491 -0
  81. package/dist/templates/windsurf/workflows/trellis-break-loop.md +111 -0
  82. package/dist/templates/windsurf/workflows/trellis-check-cross-layer.md +157 -0
  83. package/dist/templates/windsurf/workflows/trellis-check.md +27 -0
  84. package/dist/templates/windsurf/workflows/trellis-create-command.md +154 -0
  85. package/dist/templates/windsurf/workflows/trellis-finish-work.md +147 -0
  86. package/dist/templates/windsurf/workflows/trellis-integrate-skill.md +220 -0
  87. package/dist/templates/windsurf/workflows/trellis-onboard.md +362 -0
  88. package/dist/templates/windsurf/workflows/trellis-record-session.md +66 -0
  89. package/dist/templates/windsurf/workflows/trellis-start.md +373 -0
  90. package/dist/templates/windsurf/workflows/trellis-update-spec.md +358 -0
  91. package/dist/types/ai-tools.d.ts +5 -3
  92. package/dist/types/ai-tools.d.ts.map +1 -1
  93. package/dist/types/ai-tools.js +21 -1
  94. package/dist/types/ai-tools.js.map +1 -1
  95. package/dist/utils/template-fetcher.d.ts +17 -4
  96. package/dist/utils/template-fetcher.d.ts.map +1 -1
  97. package/dist/utils/template-fetcher.js +94 -12
  98. package/dist/utils/template-fetcher.js.map +1 -1
  99. package/package.json +1 -1
@@ -71,6 +71,10 @@ Execute each step in `phase` order.
71
71
 
72
72
  > Hook will auto-inject all specs, requirements, and technical design to subagent context.
73
73
  > Dispatch only needs to issue simple call commands.
74
+ >
75
+ > **OpenCode dispatch rule**: Call subagents synchronously (`run_in_background: false`).
76
+ > Do NOT use `TaskOutput` or background polling as the completion signal for child phases.
77
+ > The background wrapper can finish before the real subagent session is actually done.
74
78
 
75
79
  ### action: "implement"
76
80
 
@@ -79,7 +83,7 @@ Task(
79
83
  subagent_type: "implement",
80
84
  prompt: "Implement the feature described in prd.md in the task directory",
81
85
  model: "opus",
82
- run_in_background: true
86
+ run_in_background: false
83
87
  )
84
88
  ```
85
89
 
@@ -98,7 +102,7 @@ Task(
98
102
  subagent_type: "check",
99
103
  prompt: "Check code changes, fix issues yourself",
100
104
  model: "opus",
101
- run_in_background: true
105
+ run_in_background: false
102
106
  )
103
107
  ```
104
108
 
@@ -116,7 +120,7 @@ Task(
116
120
  subagent_type: "debug",
117
121
  prompt: "Fix the issues described in the task context",
118
122
  model: "opus",
119
- run_in_background: true
123
+ run_in_background: false
120
124
  )
121
125
  ```
122
126
 
@@ -132,7 +136,7 @@ Task(
132
136
  subagent_type: "check",
133
137
  prompt: "[finish] Execute final completion check before PR",
134
138
  model: "opus",
135
- run_in_background: true
139
+ run_in_background: false
136
140
  )
137
141
  ```
138
142
 
@@ -168,27 +172,23 @@ This will:
168
172
  ### Basic Pattern
169
173
 
170
174
  ```
171
- task_id = Task(
175
+ result = Task(
172
176
  subagent_type: "implement", // or "check", "debug"
173
177
  prompt: "Simple task description",
174
178
  model: "opus",
175
- run_in_background: true
179
+ run_in_background: false
176
180
  )
177
181
 
178
- // Poll for completion
179
- for i in 1..N:
180
- result = TaskOutput(task_id, block=true, timeout=300000)
181
- if result.status == "completed":
182
- break
182
+ // Wait for the Task call to return before starting the next phase.
183
+ // Do NOT call TaskOutput or use background polling inside OpenCode dispatch.
183
184
  ```
184
185
 
185
- ### Timeout Settings
186
+ ### Execution Rule
186
187
 
187
- | Phase | Max Time | Poll Count |
188
- |-------|----------|------------|
189
- | implement | 30 min | 6 times |
190
- | check | 15 min | 3 times |
191
- | debug | 20 min | 4 times |
188
+ - Run one phase at a time
189
+ - Start the next phase only after the current `Task(...)` call returns
190
+ - If a phase returns a clear timeout or failure, handle that result explicitly
191
+ - Do **not** simulate completion by polling a background task wrapper
192
192
 
193
193
  ---
194
194
 
@@ -196,7 +196,7 @@ for i in 1..N:
196
196
 
197
197
  ### Timeout
198
198
 
199
- If a subagent times out, notify the user and ask for guidance:
199
+ If a synchronous subagent call times out, notify the user and ask for guidance:
200
200
 
201
201
  ```
202
202
  "Subagent {phase} timed out after {time}. Options:
@@ -207,10 +207,11 @@ If a subagent times out, notify the user and ask for guidance:
207
207
 
208
208
  ### Subagent Failure
209
209
 
210
- If a subagent reports failure, read the output and decide:
210
+ If a synchronous subagent call reports failure, read the output and decide:
211
211
 
212
212
  - If recoverable: call debug agent to fix
213
213
  - If not recoverable: notify user and ask for guidance
214
+ - Do not switch back to `TaskOutput` polling for the same phase
214
215
 
215
216
  ---
216
217
 
@@ -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
- import { join } from "path"
15
- import { homedir, platform } from "os"
9
+ import { isAbsolute, join } from "path"
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
  }
@@ -191,60 +50,56 @@ export class TrellisContext {
191
50
  if (!existsSync(currentTaskPath)) {
192
51
  return null
193
52
  }
194
- return readFileSync(currentTaskPath, "utf-8").trim()
53
+ const taskRef = readFileSync(currentTaskPath, "utf-8").trim()
54
+ const normalized = this.normalizeTaskRef(taskRef)
55
+ return normalized || null
195
56
  } catch {
196
57
  return null
197
58
  }
198
59
  }
199
60
 
200
- // ============================================================
201
- // Hook Decision Logic
202
- // ============================================================
61
+ normalizeTaskRef(taskRef) {
62
+ if (!taskRef) {
63
+ return ""
64
+ }
203
65
 
204
- /**
205
- * Determine if our plugin should skip this hook
206
- * (because omo will handle it via .claude/hooks/)
207
- *
208
- * @param {string} hookName - Hook name without extension (e.g., "session-start")
209
- * @returns {boolean} - true if we should skip, false if we should handle
210
- */
211
- shouldSkipHook(hookName) {
212
- // Not a Trellis project? Skip.
213
- if (!this.isTrellisProject()) {
214
- debugLog("context", `shouldSkipHook(${hookName}): skip - not Trellis project`)
215
- return true
66
+ if (isAbsolute(taskRef)) {
67
+ return taskRef.trim()
216
68
  }
217
69
 
218
- // omo not installed? We handle it.
219
- if (!this.isOmoInstalled()) {
220
- debugLog("context", `shouldSkipHook(${hookName}): handle - omo not installed`)
221
- return false
70
+ let normalized = taskRef.trim().replace(/\\/g, "/")
71
+ while (normalized.startsWith("./")) {
72
+ normalized = normalized.slice(2)
222
73
  }
223
74
 
224
- // omo installed but hooks disabled? We handle it.
225
- if (!this.isOmoHooksEnabled()) {
226
- debugLog("context", `shouldSkipHook(${hookName}): handle - omo hooks disabled`)
227
- return false
75
+ if (normalized.startsWith("tasks/")) {
76
+ return `.trellis/${normalized}`
228
77
  }
229
78
 
230
- // omo installed + hooks enabled + .claude/hooks/ exists? Skip (omo handles).
231
- if (this.hasClaudeHook(hookName)) {
232
- debugLog("context", `shouldSkipHook(${hookName}): skip - omo will handle via .claude/hooks/`)
233
- return true
79
+ return normalized
80
+ }
81
+
82
+ resolveTaskDir(taskRef) {
83
+ const normalized = this.normalizeTaskRef(taskRef)
84
+ if (!normalized) {
85
+ return null
234
86
  }
235
87
 
236
- // omo installed but no .claude/hooks/ file? We handle it.
237
- debugLog("context", `shouldSkipHook(${hookName}): handle - no .claude/hooks/ file`)
238
- return false
88
+ if (isAbsolute(normalized)) {
89
+ return normalized
90
+ }
91
+
92
+ if (normalized.startsWith(".trellis/")) {
93
+ return join(this.directory, normalized)
94
+ }
95
+
96
+ return join(this.directory, ".trellis", "tasks", normalized)
239
97
  }
240
98
 
241
99
  // ============================================================
242
100
  // File Reading Utilities
243
101
  // ============================================================
244
102
 
245
- /**
246
- * Read a file, return null on error
247
- */
248
103
  readFile(filePath) {
249
104
  try {
250
105
  if (existsSync(filePath)) {
@@ -256,16 +111,10 @@ export class TrellisContext {
256
111
  return null
257
112
  }
258
113
 
259
- /**
260
- * Read a file relative to project directory
261
- */
262
114
  readProjectFile(relativePath) {
263
115
  return this.readFile(join(this.directory, relativePath))
264
116
  }
265
117
 
266
- /**
267
- * Run a Python script and return output
268
- */
269
118
  runScript(scriptPath, cwd = null) {
270
119
  try {
271
120
  const result = execSync(`${PYTHON_CMD} "${scriptPath}"`, {
@@ -284,12 +133,6 @@ export class TrellisContext {
284
133
  // JSONL Reading
285
134
  // ============================================================
286
135
 
287
- /**
288
- * Read all .md files in a directory
289
- * @param {string} dirPath - Directory path relative to project root
290
- * @param {number} maxFiles - Max files to read (prevent huge directories)
291
- * @returns {Array<{path: string, content: string}>}
292
- */
293
136
  readDirectoryMdFiles(dirPath, maxFiles = 20) {
294
137
  const results = []
295
138
  const fullPath = join(this.directory, dirPath)
@@ -339,11 +182,9 @@ export class TrellisContext {
339
182
  if (!file) continue
340
183
 
341
184
  if (entryType === "directory") {
342
- // Read all .md files in directory
343
185
  const dirEntries = this.readDirectoryMdFiles(file)
344
186
  results.push(...dirEntries)
345
187
  } else {
346
- // Read single file
347
188
  const fullPath = join(this.directory, file)
348
189
  const fileContent = this.readFile(fullPath)
349
190
  if (fileContent) {
@@ -357,74 +198,29 @@ export class TrellisContext {
357
198
  return results
358
199
  }
359
200
 
360
- /**
361
- * Build context string from file entries
362
- */
363
201
  buildContextFromEntries(entries) {
364
202
  return entries.map(e => `=== ${e.path} ===\n${e.content}`).join("\n\n")
365
203
  }
366
204
  }
367
205
 
368
206
  // ============================================================
369
- // Context Collector (for synthetic message injection)
207
+ // Context Collector (for session deduplication)
370
208
  // ============================================================
371
209
 
372
- /**
373
- * Simple context collector for cross-hook communication
374
- * Similar to oh-my-opencode's contextCollector
375
- */
376
210
  class ContextCollector {
377
211
  constructor() {
378
- this.pending = new Map()
379
212
  this.processed = new Set()
380
213
  }
381
214
 
382
- /**
383
- * Store context for a session
384
- */
385
- store(sessionID, content) {
386
- this.pending.set(sessionID, {
387
- content,
388
- timestamp: Date.now()
389
- })
390
- debugLog("collector", "stored context for session:", sessionID, "length:", content.length)
391
- }
392
-
393
- /**
394
- * Check if session has pending context
395
- */
396
- hasPending(sessionID) {
397
- return this.pending.has(sessionID)
398
- }
399
-
400
- /**
401
- * Get and consume pending context
402
- */
403
- consume(sessionID) {
404
- const pending = this.pending.get(sessionID)
405
- this.pending.delete(sessionID)
406
- return pending
407
- }
408
-
409
- /**
410
- * Mark session as processed (for first-message-only injection)
411
- */
412
215
  markProcessed(sessionID) {
413
216
  this.processed.add(sessionID)
414
217
  }
415
218
 
416
- /**
417
- * Check if session was already processed
418
- */
419
219
  isProcessed(sessionID) {
420
220
  return this.processed.has(sessionID)
421
221
  }
422
222
 
423
- /**
424
- * Clear session state
425
- */
426
223
  clear(sessionID) {
427
- this.pending.delete(sessionID)
428
224
  this.processed.delete(sessionID)
429
225
  }
430
226
  }