@mindfoldhq/trellis 0.5.0-beta.14 → 0.5.0-beta.16

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 (127) hide show
  1. package/README.md +5 -5
  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 +24 -20
  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 +44 -13
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/configurators/claude.js +1 -1
  12. package/dist/configurators/claude.js.map +1 -1
  13. package/dist/configurators/codebuddy.js +1 -1
  14. package/dist/configurators/codebuddy.js.map +1 -1
  15. package/dist/configurators/codex.d.ts.map +1 -1
  16. package/dist/configurators/codex.js +3 -6
  17. package/dist/configurators/codex.js.map +1 -1
  18. package/dist/configurators/copilot.d.ts.map +1 -1
  19. package/dist/configurators/copilot.js +4 -11
  20. package/dist/configurators/copilot.js.map +1 -1
  21. package/dist/configurators/cursor.js +1 -1
  22. package/dist/configurators/cursor.js.map +1 -1
  23. package/dist/configurators/droid.js +1 -1
  24. package/dist/configurators/droid.js.map +1 -1
  25. package/dist/configurators/gemini.d.ts.map +1 -1
  26. package/dist/configurators/gemini.js +1 -3
  27. package/dist/configurators/gemini.js.map +1 -1
  28. package/dist/configurators/index.d.ts.map +1 -1
  29. package/dist/configurators/index.js +24 -38
  30. package/dist/configurators/index.js.map +1 -1
  31. package/dist/configurators/kiro.js +1 -1
  32. package/dist/configurators/kiro.js.map +1 -1
  33. package/dist/configurators/opencode.d.ts.map +1 -1
  34. package/dist/configurators/opencode.js +4 -1
  35. package/dist/configurators/opencode.js.map +1 -1
  36. package/dist/configurators/pi.d.ts +3 -0
  37. package/dist/configurators/pi.d.ts.map +1 -0
  38. package/dist/configurators/pi.js +39 -0
  39. package/dist/configurators/pi.js.map +1 -0
  40. package/dist/configurators/qoder.d.ts.map +1 -1
  41. package/dist/configurators/qoder.js +1 -3
  42. package/dist/configurators/qoder.js.map +1 -1
  43. package/dist/configurators/shared.d.ts +2 -4
  44. package/dist/configurators/shared.d.ts.map +1 -1
  45. package/dist/configurators/shared.js +6 -9
  46. package/dist/configurators/shared.js.map +1 -1
  47. package/dist/migrations/manifests/0.5.0-beta.15.json +116 -0
  48. package/dist/migrations/manifests/0.5.0-beta.16.json +9 -0
  49. package/dist/templates/claude/agents/trellis-research.md +1 -1
  50. package/dist/templates/claude/settings.json +0 -4
  51. package/dist/templates/codebuddy/agents/trellis-research.md +1 -1
  52. package/dist/templates/codex/agents/trellis-check.toml +0 -16
  53. package/dist/templates/codex/agents/trellis-implement.toml +0 -16
  54. package/dist/templates/codex/agents/trellis-research.toml +3 -2
  55. package/dist/templates/codex/hooks/session-start.py +51 -21
  56. package/dist/templates/codex/skills/start/SKILL.md +1 -1
  57. package/dist/templates/copilot/hooks/session-start.py +51 -21
  58. package/dist/templates/copilot/prompts/start.prompt.md +1 -1
  59. package/dist/templates/cursor/agents/trellis-check.md +1 -1
  60. package/dist/templates/cursor/agents/trellis-implement.md +1 -1
  61. package/dist/templates/cursor/agents/trellis-research.md +2 -2
  62. package/dist/templates/cursor/hooks.json +7 -1
  63. package/dist/templates/droid/droids/trellis-research.md +1 -1
  64. package/dist/templates/extract.d.ts +6 -0
  65. package/dist/templates/extract.d.ts.map +1 -1
  66. package/dist/templates/extract.js +14 -0
  67. package/dist/templates/extract.js.map +1 -1
  68. package/dist/templates/gemini/agents/trellis-research.md +1 -1
  69. package/dist/templates/kiro/agents/trellis-research.json +1 -1
  70. package/dist/templates/markdown/agents.md +11 -12
  71. package/dist/templates/markdown/gitignore.txt +3 -0
  72. package/dist/templates/opencode/agents/trellis-check.md +1 -1
  73. package/dist/templates/opencode/agents/trellis-implement.md +1 -1
  74. package/dist/templates/opencode/agents/trellis-research.md +2 -2
  75. package/dist/templates/opencode/lib/trellis-context.js +100 -13
  76. package/dist/templates/opencode/plugins/inject-subagent-context.js +54 -4
  77. package/dist/templates/opencode/plugins/inject-workflow-state.js +48 -25
  78. package/dist/templates/opencode/plugins/session-start.js +29 -16
  79. package/dist/templates/pi/agents/trellis-check.md +28 -0
  80. package/dist/templates/pi/agents/trellis-implement.md +33 -0
  81. package/dist/templates/pi/agents/trellis-research.md +25 -0
  82. package/dist/templates/pi/extensions/trellis/index.ts.txt +549 -0
  83. package/dist/templates/pi/index.d.ts +5 -0
  84. package/dist/templates/pi/index.d.ts.map +1 -0
  85. package/dist/templates/pi/index.js +12 -0
  86. package/dist/templates/pi/index.js.map +1 -0
  87. package/dist/templates/pi/settings.json +12 -0
  88. package/dist/templates/qoder/agents/trellis-research.md +1 -1
  89. package/dist/templates/shared-hooks/index.d.ts +31 -0
  90. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  91. package/dist/templates/shared-hooks/index.js +59 -0
  92. package/dist/templates/shared-hooks/index.js.map +1 -1
  93. package/dist/templates/shared-hooks/inject-shell-session-context.py +180 -0
  94. package/dist/templates/shared-hooks/inject-subagent-context.py +128 -26
  95. package/dist/templates/shared-hooks/inject-workflow-state.py +99 -62
  96. package/dist/templates/shared-hooks/session-start.py +139 -24
  97. package/dist/templates/trellis/gitignore.txt +3 -0
  98. package/dist/templates/trellis/index.d.ts +1 -0
  99. package/dist/templates/trellis/index.d.ts.map +1 -1
  100. package/dist/templates/trellis/index.js +2 -0
  101. package/dist/templates/trellis/index.js.map +1 -1
  102. package/dist/templates/trellis/scripts/common/__init__.py +8 -0
  103. package/dist/templates/trellis/scripts/common/active_task.py +593 -0
  104. package/dist/templates/trellis/scripts/common/cli_adapter.py +43 -8
  105. package/dist/templates/trellis/scripts/common/paths.py +61 -58
  106. package/dist/templates/trellis/scripts/common/session_context.py +12 -0
  107. package/dist/templates/trellis/scripts/common/task_store.py +6 -8
  108. package/dist/templates/trellis/scripts/task.py +59 -17
  109. package/dist/templates/trellis/workflow.md +30 -26
  110. package/dist/types/ai-tools.d.ts +3 -3
  111. package/dist/types/ai-tools.d.ts.map +1 -1
  112. package/dist/types/ai-tools.js +16 -0
  113. package/dist/types/ai-tools.js.map +1 -1
  114. package/dist/utils/posix.d.ts +13 -0
  115. package/dist/utils/posix.d.ts.map +1 -0
  116. package/dist/utils/posix.js +15 -0
  117. package/dist/utils/posix.js.map +1 -0
  118. package/dist/utils/template-fetcher.d.ts +22 -6
  119. package/dist/utils/template-fetcher.d.ts.map +1 -1
  120. package/dist/utils/template-fetcher.js +405 -27
  121. package/dist/utils/template-fetcher.js.map +1 -1
  122. package/dist/utils/template-hash.d.ts +22 -3
  123. package/dist/utils/template-hash.d.ts.map +1 -1
  124. package/dist/utils/template-hash.js +80 -18
  125. package/dist/utils/template-hash.js.map +1 -1
  126. package/package.json +1 -1
  127. package/dist/templates/shared-hooks/statusline.py +0 -219
@@ -6,7 +6,7 @@ permission:
6
6
  read: allow
7
7
  write: deny
8
8
  edit: deny
9
- bash: deny
9
+ bash: allow
10
10
  glob: allow
11
11
  grep: allow
12
12
  mcp__exa__*: allow
@@ -22,7 +22,7 @@ You are the Research Agent in the Trellis workflow.
22
22
 
23
23
  Otherwise, if task-specific research is needed:
24
24
 
25
- 1. Read `.trellis/.current-task` → get task directory (if exists)
25
+ 1. Run `python3 ./.trellis/scripts/task.py current --source` → get active task directory and source (if set)
26
26
  2. For each entry in JSONL (if task dir exists):
27
27
  - If `path` is a file → Read it
28
28
  - If `path` is a directory → Read all `.md` files in it
@@ -9,6 +9,8 @@ import { existsSync, readFileSync, appendFileSync, readdirSync } from "fs"
9
9
  import { isAbsolute, join } from "path"
10
10
  import { platform } from "os"
11
11
  import { execSync } from "child_process"
12
+ import { createHash } from "crypto"
13
+ import process from "process"
12
14
 
13
15
  const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
14
16
  // Debug logging
@@ -24,6 +26,43 @@ function debugLog(prefix, ...args) {
24
26
  }
25
27
  }
26
28
 
29
+ function stringValue(value) {
30
+ return typeof value === "string" && value.trim() ? value.trim() : null
31
+ }
32
+
33
+ function sanitizeKey(raw) {
34
+ const safe = raw.trim().replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._-]+|[._-]+$/g, "")
35
+ return safe ? safe.slice(0, 160) : ""
36
+ }
37
+
38
+ function hashValue(raw) {
39
+ return createHash("sha256").update(raw).digest("hex").slice(0, 24)
40
+ }
41
+
42
+ function lookupString(data, keys) {
43
+ if (!data || typeof data !== "object") return null
44
+ for (const key of keys) {
45
+ const value = stringValue(data[key])
46
+ if (value) return value
47
+ }
48
+ for (const nestedKey of ["input", "properties", "event", "hook_input", "hookInput"]) {
49
+ const nested = data[nestedKey]
50
+ if (nested && typeof nested === "object") {
51
+ const value = lookupString(nested, keys)
52
+ if (value) return value
53
+ }
54
+ }
55
+ return null
56
+ }
57
+
58
+ function buildContextKey(platformName, kind, value) {
59
+ if (kind === "transcript") {
60
+ return `${platformName}_transcript_${hashValue(value)}`
61
+ }
62
+ const safeValue = sanitizeKey(value)
63
+ return safeValue ? `${platformName}_${safeValue}` : `${platformName}_${hashValue(value)}`
64
+ }
65
+
27
66
  /**
28
67
  * Trellis Context Manager
29
68
  */
@@ -41,23 +80,67 @@ export class TrellisContext {
41
80
  return existsSync(join(this.directory, ".trellis"))
42
81
  }
43
82
 
44
- /**
45
- * Get current task directory from .trellis/.current-task
46
- */
47
- getCurrentTask() {
83
+ getContextKey(platformInput = null) {
84
+ const override = stringValue(process.env.TRELLIS_CONTEXT_ID)
85
+ if (override) {
86
+ return sanitizeKey(override) || hashValue(override)
87
+ }
88
+
89
+ const runID = stringValue(process.env.OPENCODE_RUN_ID)
90
+ if (runID) return buildContextKey("opencode", "session", runID)
91
+
92
+ const input = platformInput && typeof platformInput === "object" ? platformInput : null
93
+ if (!input) return null
94
+
95
+ const sessionID = lookupString(input, ["session_id", "sessionId", "sessionID"])
96
+ if (sessionID) return buildContextKey("opencode", "session", sessionID)
97
+
98
+ const conversationID = lookupString(input, ["conversation_id", "conversationId", "conversationID"])
99
+ if (conversationID) return buildContextKey("opencode", "conversation", conversationID)
100
+
101
+ const transcriptPath = lookupString(input, ["transcript_path", "transcriptPath", "transcript"])
102
+ if (transcriptPath) return buildContextKey("opencode", "transcript", transcriptPath)
103
+
104
+ return null
105
+ }
106
+
107
+ readContext(contextKey) {
48
108
  try {
49
- const currentTaskPath = join(this.directory, ".trellis", ".current-task")
50
- if (!existsSync(currentTaskPath)) {
51
- return null
52
- }
53
- const taskRef = readFileSync(currentTaskPath, "utf-8").trim()
54
- const normalized = this.normalizeTaskRef(taskRef)
55
- return normalized || null
109
+ const contextPath = join(this.directory, ".trellis", ".runtime", "sessions", `${contextKey}.json`)
110
+ if (!existsSync(contextPath)) return null
111
+ return JSON.parse(readFileSync(contextPath, "utf-8"))
56
112
  } catch {
57
113
  return null
58
114
  }
59
115
  }
60
116
 
117
+ /**
118
+ * Get active task from session runtime context.
119
+ */
120
+ getActiveTask(platformInput = null) {
121
+ const contextKey = this.getContextKey(platformInput)
122
+ if (!contextKey) {
123
+ return { taskPath: null, source: "none", stale: false }
124
+ }
125
+
126
+ const context = this.readContext(contextKey)
127
+ const taskRef = this.normalizeTaskRef(context?.current_task || "")
128
+ if (taskRef) {
129
+ const taskDir = this.resolveTaskDir(taskRef)
130
+ return {
131
+ taskPath: taskRef,
132
+ source: `session:${contextKey}`,
133
+ stale: !taskDir || !existsSync(taskDir),
134
+ }
135
+ }
136
+
137
+ return { taskPath: null, source: "none", stale: false }
138
+ }
139
+
140
+ getCurrentTask(platformInput = null) {
141
+ return this.getActiveTask(platformInput).taskPath
142
+ }
143
+
61
144
  normalizeTaskRef(taskRef) {
62
145
  if (!taskRef) {
63
146
  return ""
@@ -115,13 +198,17 @@ export class TrellisContext {
115
198
  return this.readFile(join(this.directory, relativePath))
116
199
  }
117
200
 
118
- runScript(scriptPath, cwd = null) {
201
+ runScript(scriptPath, cwd = null, contextKey = null) {
119
202
  try {
120
203
  const result = execSync(`${PYTHON_CMD} "${scriptPath}"`, {
121
204
  cwd: cwd || this.directory,
122
205
  timeout: 10000,
123
206
  encoding: "utf-8",
124
- stdio: ["pipe", "pipe", "pipe"]
207
+ stdio: ["pipe", "pipe", "pipe"],
208
+ env: {
209
+ ...process.env,
210
+ ...(contextKey ? { TRELLIS_CONTEXT_ID: contextKey } : {}),
211
+ },
125
212
  })
126
213
  return result || ""
127
214
  } catch {
@@ -255,6 +255,46 @@ ${originalPrompt}
255
255
  return templates[agentType] || originalPrompt
256
256
  }
257
257
 
258
+ function shellQuote(value) {
259
+ return `'${String(value).replace(/'/g, "'\\''")}'`
260
+ }
261
+
262
+ function getBashCommandKey(args) {
263
+ if (!args || typeof args !== "object") return null
264
+ if (typeof args.command === "string") return "command"
265
+ if (typeof args.cmd === "string") return "cmd"
266
+ return null
267
+ }
268
+
269
+ function commandStartsWithTrellisContext(command) {
270
+ const firstCommand = command.trimStart().split(/[;&|]/, 1)[0].trimStart()
271
+ return (
272
+ /^TRELLIS_CONTEXT_ID\s*=/.test(firstCommand) ||
273
+ /^export\s+TRELLIS_CONTEXT_ID\s*=/.test(firstCommand) ||
274
+ /^env\s+(?:[^\s=]+\s+)*TRELLIS_CONTEXT_ID\s*=/.test(firstCommand)
275
+ )
276
+ }
277
+
278
+ /**
279
+ * OpenCode TUI may not expose OPENCODE_RUN_ID to Bash. The plugin hook still
280
+ * receives session identity, so inject it into Bash commands before execution.
281
+ */
282
+ function injectTrellisContextIntoBash(ctx, input, output) {
283
+ const args = output?.args
284
+ const commandKey = getBashCommandKey(args)
285
+ if (!commandKey) return false
286
+
287
+ const command = args[commandKey]
288
+ if (!command.trim()) return false
289
+ if (commandStartsWithTrellisContext(command)) return false
290
+
291
+ const contextKey = ctx.getContextKey(input)
292
+ if (!contextKey) return false
293
+
294
+ args[commandKey] = `export TRELLIS_CONTEXT_ID=${shellQuote(contextKey)}; ${command}`
295
+ return true
296
+ }
297
+
258
298
  // OpenCode plugin factory: `export default async (input) => hooks`.
259
299
  // OpenCode 1.2.x iterates every module export and invokes it as a function
260
300
  // (packages/opencode/src/plugin/index.ts — `for ([_, fn] of Object.entries(mod)) await fn(input)`);
@@ -270,6 +310,13 @@ export default async ({ directory }) => {
270
310
  debugLog("inject", "tool.execute.before called, tool:", input?.tool)
271
311
 
272
312
  const toolName = input?.tool?.toLowerCase()
313
+ if (toolName === "bash") {
314
+ if (injectTrellisContextIntoBash(ctx, input, output)) {
315
+ debugLog("inject", "Injected TRELLIS_CONTEXT_ID into Bash command")
316
+ }
317
+ return
318
+ }
319
+
273
320
  if (toolName !== "task") {
274
321
  return
275
322
  }
@@ -277,21 +324,24 @@ export default async ({ directory }) => {
277
324
  const args = output?.args
278
325
  if (!args) return
279
326
 
280
- const subagentType = args.subagent_type
327
+ const rawSubagentType = args.subagent_type
328
+ // Strip "trellis-" prefix added by v0.5.0-beta.5 agent rename migration
329
+ const subagentType = (rawSubagentType || "").replace(/^trellis-/, "")
281
330
  const originalPrompt = args.prompt || ""
282
331
 
283
- debugLog("inject", "Task tool called, subagent_type:", subagentType)
332
+ debugLog("inject", "Task tool called, subagent_type:", rawSubagentType)
284
333
 
285
334
  if (!AGENTS_ALL.includes(subagentType)) {
286
335
  debugLog("inject", "Skipping - unsupported subagent_type")
287
336
  return
288
337
  }
289
338
 
290
- // Read current task
291
- const taskDir = ctx.getCurrentTask()
339
+ // Resolve active task through session runtime context.
340
+ const taskDir = ctx.getCurrentTask(input)
292
341
 
293
342
  // Agents requiring task directory
294
343
  if (AGENTS_REQUIRE_TASK.includes(subagentType)) {
344
+ // subagentType is already stripped of "trellis-" prefix above
295
345
  if (!taskDir) {
296
346
  debugLog("inject", "Skipping - no current task")
297
347
  return
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * Silently skips when:
19
19
  * - No .trellis/ directory
20
- * - No active task (.trellis/.current-task missing or stale)
20
+ * - No active task in the session runtime context
21
21
  * - task.json malformed or missing status
22
22
  */
23
23
 
@@ -32,25 +32,32 @@ const TAG_RE = /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workf
32
32
  // Hardcoded defaults for built-in Trellis statuses. Used when workflow.md
33
33
  // is missing, malformed, or lacks the tag for this status.
34
34
  //
35
- // `no_task` is a pseudo-status emitted when .current-task is missing — keeps
35
+ // `no_task` is a pseudo-status emitted when no session active task exists — keeps
36
36
  // the Next-Action reminder flowing per-turn even without an active task.
37
37
  const FALLBACK_BREADCRUMBS = {
38
38
  no_task:
39
39
  "No active task.\n" +
40
- "Trigger words in the user message that REQUIRE creating a task " +
41
- "(non-negotiable, do NOT self-exempt): 重构 / 抽成 / 独立 / 分发 / " +
42
- "拆出来 / 搞一个 / 做成 / 接入 / 集成 / refactor / rewrite / extract / " +
43
- "productize / publish / build X / design Y.\n" +
44
- "Task is NOT required ONLY if ALL three hold: (a) zero file writes " +
45
- "this turn, (b) answer fits in one reply with no multi-round plan, " +
46
- "(c) no research beyond reading 1-2 repo files.\n" +
47
- "When in doubt: create task. Over-tasking is cheap; under-tasking " +
48
- "leaks plans and research into main context.\n" +
40
+ "Trigger words in the user message that suggest creating a task: " +
41
+ "重构 / 抽成 / 独立 / 分发 / 拆出来 / 搞一个 / 做成 / 接入 / 集成 / " +
42
+ "refactor / rewrite / extract / productize / publish / build X / design Y.\n" +
43
+ "Task is NOT required if ALL three hold: (a) zero file writes this turn, " +
44
+ "(b) answer fits in one reply with no multi-round plan, (c) no research " +
45
+ "beyond reading 1-2 repo files.\n" +
46
+ "When in doubt and no override below applies: prefer creating a task — " +
47
+ "over-tasking is cheap; under-tasking leaks plans and research into " +
48
+ "main context.\n" +
49
49
  "Flow: load `trellis-brainstorm` skill → it creates the task via " +
50
50
  "`python3 ./.trellis/scripts/task.py create` and drives requirements Q&A. " +
51
51
  "For research-heavy work (tool comparison, docs, cross-platform survey), " +
52
52
  "spawn `trellis-research` sub-agents via Task tool — NEVER do 3+ inline " +
53
- "WebFetch/WebSearch/`gh api` calls in the main conversation.",
53
+ "WebFetch/WebSearch/`gh api` calls in the main conversation.\n" +
54
+ "User override (per-turn escape hatch): if the user's CURRENT message " +
55
+ "contains an explicit opt-out phrase (\"跳过 trellis\" / \"别走流程\" / " +
56
+ "\"小修一下\" / \"直接改\" / \"先别建任务\" / \"skip trellis\" / " +
57
+ "\"no task\" / \"just do it\" / \"don't create a task\"), honor it for " +
58
+ "this turn — briefly acknowledge (\"好,本轮跳过 trellis 流程\") and " +
59
+ "proceed without creating a task. Per-turn only; does not carry forward; " +
60
+ "do NOT invent an override the user did not say.",
54
61
  planning:
55
62
  "Complete prd.md via trellis-brainstorm skill; then run task.py start.\n" +
56
63
  "Research belongs in `{task_dir}/research/*.md`, written by " +
@@ -60,9 +67,19 @@ const FALLBACK_BREADCRUMBS = {
60
67
  "Flow: trellis-implement → trellis-check → trellis-update-spec → finish\n" +
61
68
  "Next required action: inspect conversation history + git status, then " +
62
69
  "execute the next uncompleted step in that sequence.\n" +
63
- "For agent-capable platforms, do NOT edit code in the main session; " +
64
- "dispatch `trellis-implement` for implementation and dispatch " +
65
- "`trellis-check` before reporting completion.",
70
+ "For agent-capable platforms, the default is to dispatch " +
71
+ "`trellis-implement` for implementation and `trellis-check` before " +
72
+ "reporting completion do not edit code in the main session by default.\n" +
73
+ "Use the exact Trellis agent type names when spawning sub-agents: " +
74
+ "`trellis-implement`, `trellis-check`, or `trellis-research`. " +
75
+ "Generic/default/generalPurpose sub-agents do not receive " +
76
+ "`implement.jsonl` / `check.jsonl` injection.\n" +
77
+ "User override (per-turn escape hatch): if the user's CURRENT message " +
78
+ "explicitly tells the main session to handle it directly (\"你直接改\" / " +
79
+ "\"别派 sub-agent\" / \"main session 写就行\" / \"do it inline\" / " +
80
+ "\"不用 sub-agent\"), honor it for this turn and edit code directly. " +
81
+ "Per-turn only; does not carry forward; do NOT invent an override the " +
82
+ "user did not say.",
66
83
  completed:
67
84
  "User commits changes; then run task.py archive.",
68
85
  }
@@ -92,11 +109,14 @@ function loadBreadcrumbs(directory) {
92
109
  /**
93
110
  * Get (taskId, status) from active task, or null if no active task.
94
111
  */
95
- function getActiveTask(ctx) {
96
- const taskRef = ctx.getCurrentTask()
112
+ function getActiveTask(ctx, platformInput = null) {
113
+ const active = ctx.getActiveTask(platformInput)
114
+ const taskRef = active.taskPath
97
115
  if (!taskRef) return null
98
116
  const taskDir = ctx.resolveTaskDir(taskRef)
99
- if (!taskDir || !existsSync(taskDir)) return null
117
+ if (active.stale || !taskDir || !existsSync(taskDir)) {
118
+ return { id: taskRef.split("/").pop(), status: "stale", source: active.source }
119
+ }
100
120
  const taskJsonPath = join(taskDir, "task.json")
101
121
  if (!existsSync(taskJsonPath)) return null
102
122
  try {
@@ -104,7 +124,7 @@ function getActiveTask(ctx) {
104
124
  const status = typeof data.status === "string" ? data.status : ""
105
125
  if (!status) return null
106
126
  const id = data.id || taskRef.split("/").pop()
107
- return { id, status }
127
+ return { id, status, source: active.source }
108
128
  } catch {
109
129
  return null
110
130
  }
@@ -116,12 +136,15 @@ function getActiveTask(ctx) {
116
136
  * - Unknown status → generic "refer to workflow.md"
117
137
  * - no_task pseudo-status (id === null) → header omits task info
118
138
  */
119
- function buildBreadcrumb(id, status, templates) {
139
+ function buildBreadcrumb(id, status, templates, source = null) {
120
140
  let body = templates[status]
121
141
  if (body === undefined) {
122
142
  body = "Refer to workflow.md for current step."
123
143
  }
124
- const header = id === null ? `Status: ${status}` : `Task: ${id} (${status})`
144
+ let header = id === null ? `Status: ${status}` : `Task: ${id} (${status})`
145
+ if (source) {
146
+ header = `${header}\nSource: ${source}`
147
+ }
125
148
  return `<workflow-state>\n${header}\n${body}\n</workflow-state>`
126
149
  }
127
150
 
@@ -142,9 +165,9 @@ export default async ({ directory }) => {
142
165
  return
143
166
  }
144
167
  const templates = loadBreadcrumbs(directory)
145
- const task = getActiveTask(ctx)
168
+ const task = getActiveTask(ctx, input)
146
169
  const breadcrumb = task
147
- ? buildBreadcrumb(task.id, task.status, templates)
170
+ ? buildBreadcrumb(task.id, task.status, templates, task.source)
148
171
  : buildBreadcrumb(null, "no_task", templates)
149
172
 
150
173
  const parts = output?.parts || []
@@ -160,9 +183,9 @@ export default async ({ directory }) => {
160
183
  debugLog(
161
184
  "workflow-state",
162
185
  "Injected breadcrumb for task",
163
- task.id,
186
+ task ? task.id : "none",
164
187
  "status",
165
- task.status,
188
+ task ? task.status : "no_task",
166
189
  )
167
190
  } catch (error) {
168
191
  debugLog(
@@ -52,16 +52,17 @@ function hasCuratedJsonlEntry(jsonlPath) {
52
52
  * Check current task status and return structured status string.
53
53
  * JavaScript equivalent of _get_task_status in Claude's session-start.py.
54
54
  */
55
- function getTaskStatus(ctx) {
56
- const taskRef = ctx.getCurrentTask()
55
+ function getTaskStatus(ctx, platformInput = null) {
56
+ const active = ctx.getActiveTask(platformInput)
57
+ const taskRef = active.taskPath
57
58
  if (!taskRef) {
58
- return "Status: NO ACTIVE TASK\nNext: Describe what you want to work on"
59
+ return `Status: NO ACTIVE TASK\nSource: ${active.source}\nNext: Describe what you want to work on`
59
60
  }
60
61
 
61
62
  const taskDir = ctx.resolveTaskDir(taskRef)
62
63
 
63
- if (!taskDir || !existsSync(taskDir)) {
64
- return `Status: STALE POINTER\nTask: ${taskRef}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish`
64
+ if (active.stale || !taskDir || !existsSync(taskDir)) {
65
+ return `Status: STALE POINTER\nTask: ${taskRef}\nSource: ${active.source}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish`
65
66
  }
66
67
 
67
68
  let taskData = {}
@@ -79,7 +80,7 @@ function getTaskStatus(ctx) {
79
80
 
80
81
  if (taskStatus === "completed") {
81
82
  const dirName = basename(taskDir)
82
- return `Status: COMPLETED\nTask: ${taskTitle}\nNext: Archive with \`python3 ./.trellis/scripts/task.py archive ${dirName}\` or start a new task`
83
+ return `Status: COMPLETED\nTask: ${taskTitle}\nSource: ${active.source}\nNext: Archive with \`python3 ./.trellis/scripts/task.py archive ${dirName}\` or start a new task`
83
84
  }
84
85
 
85
86
  let hasContext = false
@@ -94,18 +95,23 @@ function getTaskStatus(ctx) {
94
95
  const hasPrd = existsSync(join(taskDir, "prd.md"))
95
96
 
96
97
  if (!hasPrd) {
97
- return `Status: NOT READY\nTask: ${taskTitle}\nMissing: prd.md not created\nNext: Write PRD (see workflow.md Phase 1.1) then curate implement.jsonl per Phase 1.3`
98
+ return `Status: NOT READY\nTask: ${taskTitle}\nSource: ${active.source}\nMissing: prd.md not created\nNext: Write PRD (see workflow.md Phase 1.1) then curate implement.jsonl per Phase 1.3`
98
99
  }
99
100
 
100
101
  if (!hasContext) {
101
- return `Status: NOT READY\nTask: ${taskTitle}\nMissing: implement.jsonl / check.jsonl missing or empty\nNext: Curate entries per workflow.md Phase 1.3 (spec + research files only), then \`task.py start\``
102
+ return `Status: NOT READY\nTask: ${taskTitle}\nSource: ${active.source}\nMissing: implement.jsonl / check.jsonl missing or empty\nNext: Curate entries per workflow.md Phase 1.3 (spec + research files only), then \`task.py start\``
102
103
  }
103
104
 
104
105
  return (
105
106
  `Status: READY\nTask: ${taskTitle}\n` +
107
+ `Source: ${active.source}\n` +
106
108
  "Next required action: dispatch `trellis-implement` per Phase 2.1. " +
107
- "For agent-capable platforms, do NOT edit code in the main session. " +
108
- "After implementation, dispatch `trellis-check` per Phase 2.2 before reporting completion."
109
+ "For agent-capable platforms, the default is to NOT edit code in the main session. " +
110
+ "After implementation, dispatch `trellis-check` per Phase 2.2 before reporting completion.\n" +
111
+ "User override (per-turn escape hatch): if the user's CURRENT message explicitly tells the " +
112
+ "main session to handle it directly (\"你直接改\" / \"别派 sub-agent\" / \"main session 写就行\" / " +
113
+ "\"do it inline\" / \"不用 sub-agent\"), honor it for this turn and edit code directly. " +
114
+ "Per-turn only; do NOT invent an override the user did not say."
109
115
  )
110
116
  }
111
117
 
@@ -113,7 +119,7 @@ function getTaskStatus(ctx) {
113
119
  * Load Trellis config for session-start decisions.
114
120
  * Calls get_context.py --mode packages --json for reliable config data.
115
121
  */
116
- function loadTrellisConfig(directory) {
122
+ function loadTrellisConfig(directory, contextKey = null) {
117
123
  const scriptPath = join(directory, ".trellis", "scripts", "get_context.py")
118
124
  if (!existsSync(scriptPath)) {
119
125
  return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
@@ -124,6 +130,10 @@ function loadTrellisConfig(directory) {
124
130
  timeout: 5000,
125
131
  encoding: "utf-8",
126
132
  stdio: ["pipe", "pipe", "pipe"],
133
+ env: {
134
+ ...process.env,
135
+ ...(contextKey ? { TRELLIS_CONTEXT_ID: contextKey } : {}),
136
+ },
127
137
  })
128
138
  const data = JSON.parse(output)
129
139
  if (data.mode !== "monorepo") {
@@ -226,11 +236,14 @@ function resolveSpecScope(config) {
226
236
  /**
227
237
  * Build session context for injection
228
238
  */
229
- export function buildSessionContext(ctx) {
239
+ export function buildSessionContext(ctx, platformInput = null) {
230
240
  const directory = ctx.directory
231
241
  const trellisDir = join(directory, ".trellis")
242
+ const contextKey = typeof ctx.getContextKey === "function"
243
+ ? ctx.getContextKey(platformInput)
244
+ : null
232
245
 
233
- const config = loadTrellisConfig(directory)
246
+ const config = loadTrellisConfig(directory, contextKey)
234
247
  const allowedPkgs = resolveSpecScope(config)
235
248
 
236
249
  const parts = []
@@ -251,7 +264,7 @@ Read and follow all instructions below carefully.
251
264
  // 2. Current Context (dynamic)
252
265
  const contextScript = join(trellisDir, "scripts", "get_context.py")
253
266
  if (existsSync(contextScript)) {
254
- const output = ctx.runScript(contextScript)
267
+ const output = ctx.runScript(contextScript, undefined, contextKey)
255
268
  if (output) {
256
269
  parts.push("<current-state>")
257
270
  parts.push(output)
@@ -385,7 +398,7 @@ Read and follow all instructions below carefully.
385
398
  parts.push("</guidelines>")
386
399
 
387
400
  // 6. Task status
388
- const taskStatus = getTaskStatus(ctx)
401
+ const taskStatus = getTaskStatus(ctx, platformInput)
389
402
  parts.push(`<task-status>\n${taskStatus}\n</task-status>`)
390
403
 
391
404
  // 7. Final directive
@@ -514,7 +527,7 @@ export default async ({ directory, client }) => {
514
527
  }
515
528
 
516
529
  // Build context
517
- const context = buildSessionContext(ctx)
530
+ const context = buildSessionContext(ctx, input)
518
531
  debugLog("session", "Built context, length:", context.length)
519
532
 
520
533
  // Inject context directly into output.parts so it gets persisted by updatePart
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: trellis-check
3
+ description: |
4
+ Code quality check expert. Reviews changes against Trellis specs, fixes issues directly, and verifies quality gates.
5
+ tools: Read, Write, Edit, Bash, Glob, Grep
6
+ ---
7
+ # Check Agent
8
+
9
+ You are the Check Agent in the Trellis workflow.
10
+
11
+ ## Core Responsibilities
12
+
13
+ 1. Inspect the current git diff.
14
+ 2. Read and follow the spec and research files listed in the task's `check.jsonl`.
15
+ 3. Review all changed code against the task PRD and project specs.
16
+ 4. Fix issues directly when they are within scope.
17
+ 5. Run the relevant lint, typecheck, and focused tests available for the touched code.
18
+
19
+ ## Review Priorities
20
+
21
+ - Behavioral regressions and missing requirements.
22
+ - Spec or platform contract violations.
23
+ - Missing or weak tests for logic changes.
24
+ - Cross-platform path, command, and encoding assumptions.
25
+
26
+ ## Output
27
+
28
+ Report findings fixed, files changed, and verification results. If no issues remain, say that clearly.
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: trellis-implement
3
+ description: |
4
+ Code implementation expert. Understands Trellis specs and requirements, then implements features. No git commit allowed.
5
+ tools: Read, Write, Edit, Bash, Glob, Grep
6
+ ---
7
+ # Implement Agent
8
+
9
+ You are the Implement Agent in the Trellis workflow.
10
+
11
+ ## Core Responsibilities
12
+
13
+ 1. Understand the active task requirements.
14
+ 2. Read and follow the spec and research files listed in the task's `implement.jsonl`.
15
+ 3. Implement the requested change using existing project patterns.
16
+ 4. Run the relevant lint, typecheck, and focused tests available for the touched code.
17
+ 5. Report files changed and verification results.
18
+
19
+ ## Forbidden Operations
20
+
21
+ Do not run:
22
+
23
+ - `git commit`
24
+ - `git push`
25
+ - `git merge`
26
+
27
+ ## Working Rules
28
+
29
+ - Read adjacent code and tests before editing.
30
+ - Keep changes scoped to the task.
31
+ - Do not revert unrelated user or concurrent changes.
32
+ - Fix root causes rather than masking symptoms.
33
+ - Prefer existing local helpers and platform patterns over new abstractions.
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: trellis-research
3
+ description: |
4
+ Code and technical research expert. Finds relevant files, patterns, docs, and persists findings to the current task's research/ directory.
5
+ tools: Read, Write, Bash, Glob, Grep
6
+ ---
7
+ # Research Agent
8
+
9
+ You are the Research Agent in the Trellis workflow.
10
+
11
+ ## Core Principle
12
+
13
+ Persist every finding to a file. Chat context is temporary; files under the task directory survive compaction and handoff.
14
+
15
+ ## Core Responsibilities
16
+
17
+ 1. Resolve the active task with `python3 ./.trellis/scripts/task.py current --source`.
18
+ 2. Create `<task-dir>/research/` when it does not exist.
19
+ 3. Search internal code, specs, and relevant external documentation.
20
+ 4. Write each distinct topic to `<task-dir>/research/<topic-slug>.md`.
21
+ 5. Report only file paths and concise summaries to the caller.
22
+
23
+ ## Scope Limits
24
+
25
+ Write only under the current task's `research/` directory. Do not edit code, specs, platform config, or task files outside research artifacts.