@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
@@ -3,15 +3,11 @@
3
3
  * Trellis Session Start Plugin
4
4
  *
5
5
  * Injects context when user sends the first message in a session.
6
- * Uses OpenCode's chat.message + experimental.chat.messages.transform hooks.
7
- *
8
- * Compatibility:
9
- * - If oh-my-opencode handles via .claude/hooks/, this plugin skips
10
- * - Otherwise, this plugin handles injection
6
+ * Uses OpenCode's chat.message hook directly so the context persists in history.
11
7
  */
12
8
 
13
9
  import { existsSync, readFileSync, readdirSync, statSync } from "fs"
14
- import { join } from "path"
10
+ import { basename, join } from "path"
15
11
  import { execFileSync } from "child_process"
16
12
  import { platform } from "os"
17
13
  import { TrellisContext, contextCollector, debugLog } from "../lib/trellis-context.js"
@@ -23,40 +19,18 @@ const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
23
19
  * Check current task status and return structured status string.
24
20
  * JavaScript equivalent of _get_task_status in Claude's session-start.py.
25
21
  */
26
- function getTaskStatus(directory) {
27
- const trellisDir = join(directory, ".trellis")
28
- const currentTaskFile = join(trellisDir, ".current-task")
29
-
30
- if (!existsSync(currentTaskFile)) {
31
- return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
32
- }
33
-
34
- let taskRef
35
- try {
36
- taskRef = readFileSync(currentTaskFile, "utf-8").trim()
37
- } catch {
38
- return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
39
- }
40
-
22
+ function getTaskStatus(ctx) {
23
+ const taskRef = ctx.getCurrentTask()
41
24
  if (!taskRef) {
42
25
  return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
43
26
  }
44
27
 
45
- // Resolve task directory
46
- let taskDir
47
- if (taskRef.startsWith("/")) {
48
- taskDir = taskRef
49
- } else if (taskRef.startsWith(".trellis/")) {
50
- taskDir = join(directory, taskRef)
51
- } else {
52
- taskDir = join(trellisDir, "tasks", taskRef)
53
- }
28
+ const taskDir = ctx.resolveTaskDir(taskRef)
54
29
 
55
- if (!existsSync(taskDir)) {
30
+ if (!taskDir || !existsSync(taskDir)) {
56
31
  return `Status: STALE POINTER\nTask: ${taskRef}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish`
57
32
  }
58
33
 
59
- // Read task.json
60
34
  let taskData = {}
61
35
  const taskJsonPath = join(taskDir, "task.json")
62
36
  if (existsSync(taskJsonPath)) {
@@ -71,11 +45,10 @@ function getTaskStatus(directory) {
71
45
  const taskStatus = taskData.status || "unknown"
72
46
 
73
47
  if (taskStatus === "completed") {
74
- const dirName = taskDir.split("/").pop()
48
+ const dirName = basename(taskDir)
75
49
  return `Status: COMPLETED\nTask: ${taskTitle}\nNext: Archive with \`python3 ./.trellis/scripts/task.py archive ${dirName}\` or start a new task`
76
50
  }
77
51
 
78
- // Check if context is configured (jsonl files exist and non-empty)
79
52
  let hasContext = false
80
53
  for (const jsonlName of ["implement.jsonl", "check.jsonl", "spec.jsonl"]) {
81
54
  const jsonlPath = join(taskDir, jsonlName)
@@ -125,7 +98,6 @@ function loadTrellisConfig(directory) {
125
98
  if (data.mode !== "monorepo") {
126
99
  return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
127
100
  }
128
- // Convert packages array to dict keyed by name
129
101
  const pkgDict = {}
130
102
  for (const pkg of (data.packages || [])) {
131
103
  pkgDict[pkg.name] = pkg
@@ -155,7 +127,6 @@ function checkLegacySpec(directory, config) {
155
127
  const specDir = join(directory, ".trellis", "spec")
156
128
  if (!existsSync(specDir)) return null
157
129
 
158
- // Check for legacy flat spec dirs
159
130
  let hasLegacy = false
160
131
  for (const name of ["backend", "frontend"]) {
161
132
  if (existsSync(join(specDir, name, "index.md"))) {
@@ -165,7 +136,6 @@ function checkLegacySpec(directory, config) {
165
136
  }
166
137
  if (!hasLegacy) return null
167
138
 
168
- // Check which packages are missing spec/<pkg>/ directory
169
139
  const pkgNames = Object.keys(config.packages).sort()
170
140
  const missing = pkgNames.filter(name => !existsSync(join(specDir, name)))
171
141
 
@@ -213,7 +183,6 @@ function resolveSpecScope(config) {
213
183
  }
214
184
  }
215
185
  if (valid.size > 0) return valid
216
- // All invalid: fallback
217
186
  if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage])
218
187
  if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage])
219
188
  return null
@@ -229,10 +198,7 @@ function resolveSpecScope(config) {
229
198
  function buildSessionContext(ctx) {
230
199
  const directory = ctx.directory
231
200
  const trellisDir = join(directory, ".trellis")
232
- const claudeDir = join(directory, ".claude")
233
- const opencodeDir = join(directory, ".opencode")
234
201
 
235
- // Load config for scope filtering and legacy detection
236
202
  const config = loadTrellisConfig(directory)
237
203
  const allowedPkgs = resolveSpecScope(config)
238
204
 
@@ -261,11 +227,20 @@ Read and follow all instructions below carefully.
261
227
  }
262
228
  }
263
229
 
264
- // 3. Workflow Guide
265
- const workflow = ctx.readProjectFile(".trellis/workflow.md")
266
- if (workflow) {
230
+ // 3. Workflow Guide (ToC only — lazy-load the full file on demand)
231
+ const workflowContent = ctx.readProjectFile(".trellis/workflow.md")
232
+ if (workflowContent) {
233
+ const tocLines = [
234
+ "# Development Workflow — Section Index",
235
+ "Full guide: .trellis/workflow.md (read on demand)",
236
+ "",
237
+ ]
238
+ for (const line of workflowContent.split("\n")) {
239
+ if (line.startsWith("## ")) tocLines.push(line)
240
+ }
241
+ tocLines.push("", "To read a section: use the Read tool on .trellis/workflow.md")
267
242
  parts.push("<workflow>")
268
- parts.push(workflow)
243
+ parts.push(tocLines.join("\n"))
269
244
  parts.push("</workflow>")
270
245
  }
271
246
 
@@ -287,7 +262,6 @@ Read and follow all instructions below carefully.
287
262
  }).sort()
288
263
 
289
264
  for (const sub of subs) {
290
- // Always include guides/ regardless of scope
291
265
  if (sub === "guides") {
292
266
  const indexFile = join(specDir, sub, "index.md")
293
267
  if (existsSync(indexFile)) {
@@ -301,14 +275,11 @@ Read and follow all instructions below carefully.
301
275
 
302
276
  const indexFile = join(specDir, sub, "index.md")
303
277
  if (existsSync(indexFile)) {
304
- // Flat spec dir: spec/<layer>/index.md (single-repo)
305
278
  const content = ctx.readFile(indexFile)
306
279
  if (content) {
307
280
  parts.push(`## ${sub}\n${content}\n`)
308
281
  }
309
282
  } else {
310
- // Nested package dirs (monorepo): spec/<pkg>/<layer>/index.md
311
- // Apply scope filter
312
283
  if (allowedPkgs !== null && !allowedPkgs.has(sub)) {
313
284
  continue
314
285
  }
@@ -342,131 +313,164 @@ Read and follow all instructions below carefully.
342
313
 
343
314
  parts.push("</guidelines>")
344
315
 
345
- // 5. Session Instructions - try both .claude and .opencode
346
- let startMd = ctx.readFile(join(claudeDir, "commands", "trellis", "start.md"))
347
- if (!startMd) {
348
- startMd = ctx.readFile(join(opencodeDir, "commands", "trellis", "start.md"))
349
- }
350
- if (startMd) {
351
- parts.push("<instructions>")
352
- parts.push(startMd)
353
- parts.push("</instructions>")
354
- }
355
-
356
- // 6. Task status (R2: check task state for session resume)
357
- const taskStatus = getTaskStatus(directory)
316
+ // 6. Task status
317
+ const taskStatus = getTaskStatus(ctx)
358
318
  parts.push(`<task-status>\n${taskStatus}\n</task-status>`)
359
319
 
360
- // 7. Final directive (R3: active, not passive)
320
+ // 7. Final directive
361
321
  parts.push(`<ready>
362
- Context loaded. Steps 1-3 (workflow, context, guidelines) are already injected above — do NOT re-read them.
363
- Start from Step 4. Wait for user's first message, then follow <instructions> to handle their request.
322
+ Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them.
323
+ Wait for the user's first message, then handle it following the workflow guide.
364
324
  If there is an active task, ask whether to continue it.
365
325
  </ready>`)
366
326
 
367
327
  return parts.join("\n\n")
368
328
  }
369
329
 
370
- export default async ({ directory }) => {
371
- const ctx = new TrellisContext(directory)
372
- debugLog("session", "Plugin loaded, directory:", directory)
330
+ function getTrellisMetadata(metadata) {
331
+ if (!metadata || typeof metadata !== "object") {
332
+ return {}
333
+ }
373
334
 
374
- return {
375
- // chat.message - triggered when user sends a message
376
- "chat.message": async (input) => {
377
- try {
378
- const sessionID = input.sessionID
379
- const agent = input.agent || "unknown"
380
- debugLog("session", "chat.message called, sessionID:", sessionID, "agent:", agent)
381
-
382
- // Skip in non-interactive mode
383
- if (process.env.OPENCODE_NON_INTERACTIVE === "1") {
384
- debugLog("session", "Skipping - non-interactive mode")
385
- return
386
- }
335
+ const trellis = metadata.trellis
336
+ if (!trellis || typeof trellis !== "object") {
337
+ return {}
338
+ }
387
339
 
388
- // Check if we should skip (omo will handle)
389
- if (ctx.shouldSkipHook("session-start")) {
390
- debugLog("session", "Skipping - omo will handle via .claude/hooks/")
391
- return
392
- }
340
+ return trellis
341
+ }
393
342
 
394
- // Only inject on first message
395
- if (contextCollector.isProcessed(sessionID)) {
396
- debugLog("session", "Skipping - session already processed")
397
- return
398
- }
343
+ function markPartAsSessionStart(part) {
344
+ const metadata = part.metadata && typeof part.metadata === "object"
345
+ ? part.metadata
346
+ : {}
347
+ part.metadata = {
348
+ ...metadata,
349
+ trellis: {
350
+ ...getTrellisMetadata(metadata),
351
+ sessionStart: true,
352
+ },
353
+ }
354
+ }
399
355
 
400
- // Mark session as processed
401
- contextCollector.markProcessed(sessionID)
356
+ function hasSessionStartMarker(part) {
357
+ if (!part || part.type !== "text" || typeof part.text !== "string") {
358
+ return false
359
+ }
402
360
 
403
- // Build and store context
404
- const context = buildSessionContext(ctx)
405
- debugLog("session", "Built context, length:", context.length)
361
+ return getTrellisMetadata(part.metadata).sessionStart === true
362
+ }
406
363
 
407
- contextCollector.store(sessionID, context)
408
- debugLog("session", "Context stored for session:", sessionID)
364
+ export function hasInjectedTrellisContext(messages) {
365
+ if (!Array.isArray(messages)) {
366
+ return false
367
+ }
409
368
 
410
- } catch (error) {
411
- debugLog("session", "Error in chat.message:", error.message, error.stack)
412
- }
413
- },
369
+ return messages.some(message => {
370
+ if (!message?.info || message.info.role !== "user" || !Array.isArray(message.parts)) {
371
+ return false
372
+ }
414
373
 
415
- // experimental.chat.messages.transform - modify messages before sending to AI
416
- "experimental.chat.messages.transform": async (input, output) => {
417
- try {
418
- const { messages } = output
419
- debugLog("session", "messages.transform called, messageCount:", messages?.length)
374
+ return message.parts.some(hasSessionStartMarker)
375
+ })
376
+ }
420
377
 
421
- if (!messages || messages.length === 0) {
422
- return
423
- }
378
+ async function hasPersistedInjectedContext(client, directory, sessionID) {
379
+ try {
380
+ const response = await client.session.messages({
381
+ path: { id: sessionID },
382
+ query: { directory },
383
+ throwOnError: true,
384
+ })
385
+ return hasInjectedTrellisContext(response.data || [])
386
+ } catch (error) {
387
+ debugLog(
388
+ "session",
389
+ "Failed to read session history for dedupe:",
390
+ error instanceof Error ? error.message : String(error),
391
+ )
392
+ return false
393
+ }
394
+ }
424
395
 
425
- // Find last user message
426
- let lastUserMessageIndex = -1
427
- for (let i = messages.length - 1; i >= 0; i--) {
428
- if (messages[i].info?.role === "user") {
429
- lastUserMessageIndex = i
430
- break
431
- }
432
- }
396
+ export default {
397
+ id: "trellis.session-start",
398
+ server: async ({ directory, client }) => {
399
+ const ctx = new TrellisContext(directory)
400
+ debugLog("session", "Plugin loaded, directory:", directory)
433
401
 
434
- if (lastUserMessageIndex === -1) {
435
- debugLog("session", "No user message found")
436
- return
402
+ return {
403
+ // Clear in-memory dedupe after compaction so context can be re-injected.
404
+ event: ({ event }) => {
405
+ try {
406
+ if (event?.type === "session.compacted" && event?.properties?.sessionID) {
407
+ const sessionID = event.properties.sessionID
408
+ contextCollector.clear(sessionID)
409
+ debugLog("session", "Cleared processed flag after compaction for session:", sessionID)
410
+ }
411
+ } catch (error) {
412
+ debugLog(
413
+ "session",
414
+ "Error in event hook:",
415
+ error instanceof Error ? error.message : String(error),
416
+ )
437
417
  }
418
+ },
438
419
 
439
- const lastUserMessage = messages[lastUserMessageIndex]
440
- const sessionID = lastUserMessage.info?.sessionID
420
+ // chat.message - triggered when user sends a message.
421
+ // Modify the message in-place so the context is persisted with updateMessage/updatePart.
422
+ "chat.message": async (input, output) => {
423
+ try {
424
+ const sessionID = input.sessionID
425
+ const agent = input.agent || "unknown"
426
+ debugLog("session", "chat.message called, sessionID:", sessionID, "agent:", agent)
427
+
428
+ // Skip in non-interactive mode
429
+ if (process.env.OPENCODE_NON_INTERACTIVE === "1") {
430
+ debugLog("session", "Skipping - non-interactive mode")
431
+ return
432
+ }
441
433
 
442
- debugLog("session", "Found user message, sessionID:", sessionID)
434
+ // Only inject on first message
435
+ if (contextCollector.isProcessed(sessionID)) {
436
+ debugLog("session", "Skipping - session already processed")
437
+ return
438
+ }
443
439
 
444
- if (!sessionID || !contextCollector.hasPending(sessionID)) {
445
- debugLog("session", "No pending context for session")
446
- return
447
- }
440
+ if (await hasPersistedInjectedContext(client, ctx.directory, sessionID)) {
441
+ contextCollector.markProcessed(sessionID)
442
+ debugLog("session", "Skipping - session already contains persisted Trellis context")
443
+ return
444
+ }
448
445
 
449
- // Get and consume pending context
450
- const pending = contextCollector.consume(sessionID)
446
+ // Build context
447
+ const context = buildSessionContext(ctx)
448
+ debugLog("session", "Built context, length:", context.length)
449
+
450
+ // Inject context directly into output.parts so it gets persisted by updatePart
451
+ const parts = output?.parts || []
452
+ const textPartIndex = parts.findIndex(
453
+ p => p.type === "text" && p.text !== undefined
454
+ )
455
+
456
+ if (textPartIndex !== -1) {
457
+ const originalText = parts[textPartIndex].text || ""
458
+ parts[textPartIndex].text = `${context}\n\n---\n\n${originalText}`
459
+ markPartAsSessionStart(parts[textPartIndex])
460
+ debugLog("session", "Injected context into chat.message text part, length:", context.length)
461
+ } else {
462
+ // No existing text part: prepend a new one
463
+ const injectedPart = { type: "text", text: context }
464
+ markPartAsSessionStart(injectedPart)
465
+ parts.unshift(injectedPart)
466
+ debugLog("session", "Prepended new text part with context, length:", context.length)
467
+ }
451
468
 
452
- // Find first text part
453
- const textPartIndex = lastUserMessage.parts?.findIndex(
454
- p => p.type === "text" && p.text !== undefined
455
- )
469
+ contextCollector.markProcessed(sessionID)
456
470
 
457
- if (textPartIndex === -1) {
458
- debugLog("session", "No text part found in user message")
459
- return
471
+ } catch (error) {
472
+ debugLog("session", "Error in chat.message:", error.message, error.stack)
460
473
  }
461
-
462
- // Prepend context to the text part (same approach as omo)
463
- const originalText = lastUserMessage.parts[textPartIndex].text || ""
464
- lastUserMessage.parts[textPartIndex].text = `${pending.content}\n\n---\n\n${originalText}`
465
-
466
- debugLog("session", "Injected context by prepending to text, length:", pending.content.length)
467
-
468
- } catch (error) {
469
- debugLog("session", "Error in messages.transform:", error.message, error.stack)
470
474
  }
471
475
  }
472
476
  }
@@ -316,11 +316,16 @@ def update_index(
316
316
  def _auto_commit_workspace(repo_root: Path) -> None:
317
317
  """Stage .trellis/workspace and .trellis/tasks, then commit with a configured message."""
318
318
  commit_msg = get_session_commit_message(repo_root)
319
- subprocess.run(
319
+ add_result = subprocess.run(
320
320
  ["git", "add", "-A", ".trellis/workspace", ".trellis/tasks"],
321
321
  cwd=repo_root,
322
322
  capture_output=True,
323
+ text=True,
323
324
  )
325
+ if add_result.returncode != 0:
326
+ print(f"[WARN] git add failed (exit {add_result.returncode}): {add_result.stderr.strip()}", file=sys.stderr)
327
+ print("[WARN] Please commit .trellis/ changes manually: git add .trellis && git commit", file=sys.stderr)
328
+ return
324
329
  # Check if there are staged changes
325
330
  result = subprocess.run(
326
331
  ["git", "diff", "--cached", "--quiet", "--", ".trellis/workspace", ".trellis/tasks"],
@@ -75,6 +75,8 @@ from .paths import (
75
75
  count_lines,
76
76
  get_current_task,
77
77
  get_current_task_abs,
78
+ normalize_task_ref,
79
+ resolve_task_ref,
78
80
  set_current_task,
79
81
  clear_current_task,
80
82
  has_current_task,