@mindfoldhq/trellis 0.5.0-beta.13 → 0.5.0-beta.15

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 (119) 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 +15 -12
  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/pi.d.ts +3 -0
  34. package/dist/configurators/pi.d.ts.map +1 -0
  35. package/dist/configurators/pi.js +39 -0
  36. package/dist/configurators/pi.js.map +1 -0
  37. package/dist/configurators/qoder.d.ts.map +1 -1
  38. package/dist/configurators/qoder.js +1 -3
  39. package/dist/configurators/qoder.js.map +1 -1
  40. package/dist/configurators/shared.d.ts +2 -4
  41. package/dist/configurators/shared.d.ts.map +1 -1
  42. package/dist/configurators/shared.js +6 -9
  43. package/dist/configurators/shared.js.map +1 -1
  44. package/dist/migrations/manifests/0.5.0-beta.14.json +9 -0
  45. package/dist/migrations/manifests/0.5.0-beta.15.json +126 -0
  46. package/dist/templates/claude/agents/trellis-research.md +1 -1
  47. package/dist/templates/claude/settings.json +0 -4
  48. package/dist/templates/codebuddy/agents/trellis-research.md +1 -1
  49. package/dist/templates/codex/agents/trellis-check.toml +0 -16
  50. package/dist/templates/codex/agents/trellis-implement.toml +0 -16
  51. package/dist/templates/codex/agents/trellis-research.toml +3 -2
  52. package/dist/templates/codex/hooks/session-start.py +82 -22
  53. package/dist/templates/codex/skills/start/SKILL.md +1 -1
  54. package/dist/templates/copilot/hooks/session-start.py +84 -26
  55. package/dist/templates/copilot/prompts/start.prompt.md +1 -1
  56. package/dist/templates/cursor/agents/trellis-check.md +1 -1
  57. package/dist/templates/cursor/agents/trellis-implement.md +1 -1
  58. package/dist/templates/cursor/agents/trellis-research.md +2 -2
  59. package/dist/templates/cursor/hooks.json +7 -1
  60. package/dist/templates/droid/droids/trellis-research.md +1 -1
  61. package/dist/templates/extract.d.ts +6 -0
  62. package/dist/templates/extract.d.ts.map +1 -1
  63. package/dist/templates/extract.js +14 -0
  64. package/dist/templates/extract.js.map +1 -1
  65. package/dist/templates/gemini/agents/trellis-research.md +1 -1
  66. package/dist/templates/kiro/agents/trellis-research.json +1 -1
  67. package/dist/templates/markdown/agents.md +11 -12
  68. package/dist/templates/markdown/gitignore.txt +3 -0
  69. package/dist/templates/opencode/agents/trellis-check.md +1 -1
  70. package/dist/templates/opencode/agents/trellis-implement.md +1 -1
  71. package/dist/templates/opencode/agents/trellis-research.md +2 -2
  72. package/dist/templates/opencode/lib/trellis-context.js +100 -13
  73. package/dist/templates/opencode/plugins/inject-subagent-context.js +54 -4
  74. package/dist/templates/opencode/plugins/inject-workflow-state.js +50 -23
  75. package/dist/templates/opencode/plugins/session-start.js +46 -21
  76. package/dist/templates/pi/agents/trellis-check.md +28 -0
  77. package/dist/templates/pi/agents/trellis-implement.md +33 -0
  78. package/dist/templates/pi/agents/trellis-research.md +25 -0
  79. package/dist/templates/pi/extensions/trellis/index.ts.txt +549 -0
  80. package/dist/templates/pi/index.d.ts +5 -0
  81. package/dist/templates/pi/index.d.ts.map +1 -0
  82. package/dist/templates/pi/index.js +12 -0
  83. package/dist/templates/pi/index.js.map +1 -0
  84. package/dist/templates/pi/settings.json +12 -0
  85. package/dist/templates/qoder/agents/trellis-research.md +1 -1
  86. package/dist/templates/shared-hooks/index.d.ts +31 -0
  87. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  88. package/dist/templates/shared-hooks/index.js +59 -0
  89. package/dist/templates/shared-hooks/index.js.map +1 -1
  90. package/dist/templates/shared-hooks/inject-shell-session-context.py +180 -0
  91. package/dist/templates/shared-hooks/inject-subagent-context.py +128 -26
  92. package/dist/templates/shared-hooks/inject-workflow-state.py +101 -61
  93. package/dist/templates/shared-hooks/session-start.py +151 -28
  94. package/dist/templates/trellis/gitignore.txt +3 -0
  95. package/dist/templates/trellis/index.d.ts +1 -0
  96. package/dist/templates/trellis/index.d.ts.map +1 -1
  97. package/dist/templates/trellis/index.js +2 -0
  98. package/dist/templates/trellis/index.js.map +1 -1
  99. package/dist/templates/trellis/scripts/common/__init__.py +8 -0
  100. package/dist/templates/trellis/scripts/common/active_task.py +593 -0
  101. package/dist/templates/trellis/scripts/common/cli_adapter.py +43 -8
  102. package/dist/templates/trellis/scripts/common/paths.py +61 -58
  103. package/dist/templates/trellis/scripts/common/session_context.py +12 -0
  104. package/dist/templates/trellis/scripts/common/task_store.py +4 -6
  105. package/dist/templates/trellis/scripts/task.py +56 -14
  106. package/dist/templates/trellis/workflow.md +31 -26
  107. package/dist/types/ai-tools.d.ts +3 -3
  108. package/dist/types/ai-tools.d.ts.map +1 -1
  109. package/dist/types/ai-tools.js +16 -0
  110. package/dist/types/ai-tools.js.map +1 -1
  111. package/dist/utils/template-fetcher.d.ts +22 -6
  112. package/dist/utils/template-fetcher.d.ts.map +1 -1
  113. package/dist/utils/template-fetcher.js +405 -27
  114. package/dist/utils/template-fetcher.js.map +1 -1
  115. package/dist/utils/template-hash.d.ts.map +1 -1
  116. package/dist/utils/template-hash.js +3 -2
  117. package/dist/utils/template-hash.js.map +1 -1
  118. package/package.json +1 -1
  119. package/dist/templates/shared-hooks/statusline.py +0 -218
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trellis-research",
3
3
  "description": "Code and tech search expert. Persists findings to {TASK_DIR}/research/. No writes outside that directory.",
4
- "instructions": "# Research Agent\n\nYou are the Research Agent in the Trellis workflow.\n\n## Core Principle\n\n**You do one thing: find, explain, and PERSIST information.**\n\nConversations get compacted; files don't. Every research output MUST end up as a file under `{TASK_DIR}/research/`. Returning findings only through the chat reply is a failure — the caller cannot read them next session.\n\n---\n\n## Core Responsibilities\n\n1. **Internal Search** — locate files/components, understand code logic, discover patterns (Glob, Grep, Read)\n2. **External Search** — library docs, API references, best practices (web search)\n3. **Persist** — write each research topic to `{TASK_DIR}/research/<topic>.md`\n4. **Report** — return file paths + one-line summaries to the main agent (not full content)\n\n---\n\n## Workflow\n\n### Step 1: Resolve Current Task\n\nRead `.trellis/.current-task` → task directory (e.g. `.trellis/tasks/04-17-foo/`). If empty or missing, ask the user where to write output; do NOT guess.\n\nEnsure `{TASK_DIR}/research/` exists:\n\n```bash\nmkdir -p <TASK_DIR>/research\n```\n\n### Step 2: Understand Search Request\n\nClassify: internal / external / mixed. Determine scope (global / specific directory) and expected shape (file list / pattern notes / tech comparison).\n\n### Step 3: Execute Search\n\nRun independent searches in parallel (Glob + Grep + web) for efficiency.\n\n### Step 4: Persist Each Topic\n\nFor each distinct research topic, Write a markdown file at `{TASK_DIR}/research/<topic-slug>.md`. Use the File Format below.\n\n### Step 5: Report to Main Agent\n\nReply with ONLY:\n\n- List of files written (paths relative to repo root)\n- One-line summary per file\n- Any critical caveats that the main agent needs to know right now\n\nDo NOT paste full research content into the reply. The files are the contract.\n\n---\n\n## Scope Limits (Strict)\n\n### Write ALLOWED\n\n- `{TASK_DIR}/research/*.md` — your own output\n- Creating `{TASK_DIR}/research/` if it doesn't exist (via `mkdir -p`)\n\n### Write FORBIDDEN\n\n- Code files (`src/`, `lib/`, …)\n- Spec files (`.trellis/spec/`) — main agent should use `update-spec` skill instead\n- `.trellis/scripts/`, `.trellis/workflow.md`, platform config (`.claude/`, `.cursor/`, etc.)\n- Other task directories\n- Any git operation (commit / push / branch / merge)\n\nIf the user asks you to edit code, decline and suggest spawning `implement` instead.\n\n---\n\n## File Format\n\nEach `{TASK_DIR}/research/<topic>.md` should follow:\n\n```markdown\n# Research: <topic>\n\n- **Query**: <original query>\n- **Scope**: <internal / external / mixed>\n- **Date**: <YYYY-MM-DD>\n\n## Findings\n\n### Files Found\n\n| File Path | Description |\n|---|---|\n| `src/services/xxx.ts` | Main implementation |\n| `src/types/xxx.ts` | Type definitions |\n\n### Code Patterns\n\n<describe patterns, cite file:line>\n\n### External References\n\n- [Library X docs](url) — <why relevant, version constraints>\n\n### Related Specs\n\n- `.trellis/spec/xxx.md` — <description>\n\n## Caveats / Not Found\n\n<anything incomplete or uncertain>\n```\n\n---\n\n## Guidelines\n\n### DO\n\n- Provide specific file paths and line numbers\n- Quote actual code snippets\n- Persist every topic to its own file\n- Return file paths in your reply, not the full content\n- Mark \"not found\" explicitly when searches come up empty\n\n### DON'T\n\n- Don't write code or modify files outside `{TASK_DIR}/research/`\n- Don't guess uncertain info\n- Don't paste full research text into the reply (files are the deliverable)\n- Don't propose improvements or critique implementation (that's not your role)\n",
4
+ "instructions": "# Research Agent\n\nYou are the Research Agent in the Trellis workflow.\n\n## Core Principle\n\n**You do one thing: find, explain, and PERSIST information.**\n\nConversations get compacted; files don't. Every research output MUST end up as a file under `{TASK_DIR}/research/`. Returning findings only through the chat reply is a failure — the caller cannot read them next session.\n\n---\n\n## Core Responsibilities\n\n1. **Internal Search** — locate files/components, understand code logic, discover patterns (Glob, Grep, Read)\n2. **External Search** — library docs, API references, best practices (web search)\n3. **Persist** — write each research topic to `{TASK_DIR}/research/<topic>.md`\n4. **Report** — return file paths + one-line summaries to the main agent (not full content)\n\n---\n\n## Workflow\n\n### Step 1: Resolve Current Task\n\nRun `python3 ./.trellis/scripts/task.py current --source` → active task path. If no active task is set, ask the user where to write output; do NOT guess.\n\nEnsure `{TASK_DIR}/research/` exists:\n\n```bash\nmkdir -p <TASK_DIR>/research\n```\n\n### Step 2: Understand Search Request\n\nClassify: internal / external / mixed. Determine scope (global / specific directory) and expected shape (file list / pattern notes / tech comparison).\n\n### Step 3: Execute Search\n\nRun independent searches in parallel (Glob + Grep + web) for efficiency.\n\n### Step 4: Persist Each Topic\n\nFor each distinct research topic, Write a markdown file at `{TASK_DIR}/research/<topic-slug>.md`. Use the File Format below.\n\n### Step 5: Report to Main Agent\n\nReply with ONLY:\n\n- List of files written (paths relative to repo root)\n- One-line summary per file\n- Any critical caveats that the main agent needs to know right now\n\nDo NOT paste full research content into the reply. The files are the contract.\n\n---\n\n## Scope Limits (Strict)\n\n### Write ALLOWED\n\n- `{TASK_DIR}/research/*.md` — your own output\n- Creating `{TASK_DIR}/research/` if it doesn't exist (via `mkdir -p`)\n\n### Write FORBIDDEN\n\n- Code files (`src/`, `lib/`, …)\n- Spec files (`.trellis/spec/`) — main agent should use `update-spec` skill instead\n- `.trellis/scripts/`, `.trellis/workflow.md`, platform config (`.claude/`, `.cursor/`, etc.)\n- Other task directories\n- Any git operation (commit / push / branch / merge)\n\nIf the user asks you to edit code, decline and suggest spawning `implement` instead.\n\n---\n\n## File Format\n\nEach `{TASK_DIR}/research/<topic>.md` should follow:\n\n```markdown\n# Research: <topic>\n\n- **Query**: <original query>\n- **Scope**: <internal / external / mixed>\n- **Date**: <YYYY-MM-DD>\n\n## Findings\n\n### Files Found\n\n| File Path | Description |\n|---|---|\n| `src/services/xxx.ts` | Main implementation |\n| `src/types/xxx.ts` | Type definitions |\n\n### Code Patterns\n\n<describe patterns, cite file:line>\n\n### External References\n\n- [Library X docs](url) — <why relevant, version constraints>\n\n### Related Specs\n\n- `.trellis/spec/xxx.md` — <description>\n\n## Caveats / Not Found\n\n<anything incomplete or uncertain>\n```\n\n---\n\n## Guidelines\n\n### DO\n\n- Provide specific file paths and line numbers\n- Quote actual code snippets\n- Persist every topic to its own file\n- Return file paths in your reply, not the full content\n- Mark \"not found\" explicitly when searches come up empty\n\n### DON'T\n\n- Don't write code or modify files outside `{TASK_DIR}/research/`\n- Don't guess uncertain info\n- Don't paste full research text into the reply (files are the deliverable)\n- Don't propose improvements or critique implementation (that's not your role)\n",
5
5
  "tools": [
6
6
  "read",
7
7
  "write",
@@ -3,20 +3,19 @@
3
3
 
4
4
  These instructions are for AI assistants working in this project.
5
5
 
6
- Use the `/trellis:start` command when starting a new session to:
7
- - Initialize your developer identity
8
- - Understand current project context
9
- - Read relevant guidelines
6
+ This project is managed by Trellis. The working knowledge you need lives under `.trellis/`:
10
7
 
11
- Use `@/.trellis/` to learn:
12
- - Development workflow (`workflow.md`)
13
- - Project structure guidelines (`spec/`)
14
- - Developer workspace (`workspace/`)
8
+ - `.trellis/workflow.md` — development phases, when to create tasks, skill routing
9
+ - `.trellis/spec/` package- and layer-scoped coding guidelines (read before writing code in a given layer)
10
+ - `.trellis/workspace/` per-developer journals and session traces
11
+ - `.trellis/tasks/` active and archived tasks (PRDs, research, jsonl context)
15
12
 
16
- If you're using Codex, project-scoped helpers may also live in:
17
- - `.agents/skills/` for reusable Trellis skills
18
- - `.codex/agents/` for optional custom subagents
13
+ If a Trellis command is available on your platform (e.g. `/trellis:finish-work`, `/trellis:continue`), prefer it over manual steps. Not every platform exposes every command.
19
14
 
20
- Keep this managed block so 'trellis update' can refresh the instructions.
15
+ If you're using Codex or another agent-capable tool, additional project-scoped helpers may live in:
16
+ - `.agents/skills/` — reusable Trellis skills
17
+ - `.codex/agents/` — optional custom subagents
18
+
19
+ Managed by Trellis. Edits outside this block are preserved; edits inside may be overwritten by a future `trellis update`.
21
20
 
22
21
  <!-- TRELLIS:END -->
@@ -7,6 +7,9 @@
7
7
  # Current task pointer (local state)
8
8
  .current-task
9
9
 
10
+ # Session/window scoped runtime state
11
+ .runtime/
12
+
10
13
  # Agent runtime files (in worktree)
11
14
  .agent-log
12
15
  .session-id
@@ -21,7 +21,7 @@ You are the Check Agent in the Trellis workflow.
21
21
 
22
22
  Otherwise, load context yourself:
23
23
 
24
- 1. Read `.trellis/.current-task` → get task directory (e.g., `.trellis/tasks/xxx`)
24
+ 1. Run `python3 ./.trellis/scripts/task.py current --source` → get active task directory and source (e.g., `Current task: .trellis/tasks/xxx`)
25
25
  2. Read `{task_dir}/check.jsonl`
26
26
  3. For each entry in JSONL:
27
27
  - If `path` is a file → Read it
@@ -21,7 +21,7 @@ You are the Implement Agent in the Trellis workflow.
21
21
 
22
22
  Otherwise, load context yourself:
23
23
 
24
- 1. Read `.trellis/.current-task` → get task directory (e.g., `.trellis/tasks/xxx`)
24
+ 1. Run `python3 ./.trellis/scripts/task.py current --source` → get active task directory and source (e.g., `Current task: .trellis/tasks/xxx`)
25
25
  2. Read `{task_dir}/implement.jsonl`
26
26
  3. For each entry in JSONL (JSON object per line):
27
27
  - Skip rows without a `"file"` field (e.g. `{"_example": "..."}` seed rows)
@@ -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 " +
@@ -58,7 +65,21 @@ const FALLBACK_BREADCRUMBS = {
58
65
  "main session — PRD only links to research files.",
59
66
  in_progress:
60
67
  "Flow: trellis-implement → trellis-check → trellis-update-spec → finish\n" +
61
- "Check conversation history + git status to determine current step; do NOT skip trellis-check.",
68
+ "Next required action: inspect conversation history + git status, then " +
69
+ "execute the next uncompleted step in that sequence.\n" +
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.",
62
83
  completed:
63
84
  "User commits changes; then run task.py archive.",
64
85
  }
@@ -88,11 +109,14 @@ function loadBreadcrumbs(directory) {
88
109
  /**
89
110
  * Get (taskId, status) from active task, or null if no active task.
90
111
  */
91
- function getActiveTask(ctx) {
92
- const taskRef = ctx.getCurrentTask()
112
+ function getActiveTask(ctx, platformInput = null) {
113
+ const active = ctx.getActiveTask(platformInput)
114
+ const taskRef = active.taskPath
93
115
  if (!taskRef) return null
94
116
  const taskDir = ctx.resolveTaskDir(taskRef)
95
- 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
+ }
96
120
  const taskJsonPath = join(taskDir, "task.json")
97
121
  if (!existsSync(taskJsonPath)) return null
98
122
  try {
@@ -100,7 +124,7 @@ function getActiveTask(ctx) {
100
124
  const status = typeof data.status === "string" ? data.status : ""
101
125
  if (!status) return null
102
126
  const id = data.id || taskRef.split("/").pop()
103
- return { id, status }
127
+ return { id, status, source: active.source }
104
128
  } catch {
105
129
  return null
106
130
  }
@@ -112,12 +136,15 @@ function getActiveTask(ctx) {
112
136
  * - Unknown status → generic "refer to workflow.md"
113
137
  * - no_task pseudo-status (id === null) → header omits task info
114
138
  */
115
- function buildBreadcrumb(id, status, templates) {
139
+ function buildBreadcrumb(id, status, templates, source = null) {
116
140
  let body = templates[status]
117
141
  if (body === undefined) {
118
142
  body = "Refer to workflow.md for current step."
119
143
  }
120
- 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
+ }
121
148
  return `<workflow-state>\n${header}\n${body}\n</workflow-state>`
122
149
  }
123
150
 
@@ -138,9 +165,9 @@ export default async ({ directory }) => {
138
165
  return
139
166
  }
140
167
  const templates = loadBreadcrumbs(directory)
141
- const task = getActiveTask(ctx)
168
+ const task = getActiveTask(ctx, input)
142
169
  const breadcrumb = task
143
- ? buildBreadcrumb(task.id, task.status, templates)
170
+ ? buildBreadcrumb(task.id, task.status, templates, task.source)
144
171
  : buildBreadcrumb(null, "no_task", templates)
145
172
 
146
173
  const parts = output?.parts || []
@@ -156,9 +183,9 @@ export default async ({ directory }) => {
156
183
  debugLog(
157
184
  "workflow-state",
158
185
  "Injected breadcrumb for task",
159
- task.id,
186
+ task ? task.id : "none",
160
187
  "status",
161
- task.status,
188
+ task ? task.status : "no_task",
162
189
  )
163
190
  } catch (error) {
164
191
  debugLog(