@mindfoldhq/trellis 0.5.0-rc.0 → 0.5.0-rc.1

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": "0.5.0-rc.1",
3
+ "description": "Patches rc.0 with two OpenCode fixes: trellis-research subagent gains write permission and persists findings to {TASK_DIR}/research/ (#211); session-start.js extracts named exports to lib/session-utils.js so the OpenCode 1.2.x loader reaches export default (#212). No new migrations.",
4
+ "breaking": false,
5
+ "recommendMigrate": false,
6
+ "changelog": "**Bug Fixes:**\n- fix(opencode): `trellis-research` subagent template grants `write: allow` / `edit: allow` and the prompt body now matches the cursor/claude shape (Core Principle, Workflow Step 1–5 with `mkdir -p {TASK_DIR}/research/`, Scope Limits, File Format, DO/DON'T). The redundant 'Context Self-Loading' section is removed; `inject-subagent-context.js` already pre-loads spec dir context (#211).\n- fix(opencode): extract `buildSessionContext` and `hasInjectedTrellisContext` from `.opencode/plugins/session-start.js` into `lib/session-utils.js` so each plugin file has only `export default`. The OpenCode 1.2.x loader iterates `Object.entries(mod)` and invokes every export as a factory; the named exports caused the loader to abort before reaching the default export (#212).\n\n**Testing:**\n- test(opencode): walk `templates/opencode/plugins/*.js` and assert each file has exactly one top-level export matching `^export\\s+default\\s/` to prevent #212-class regressions.\n- test(opencode): extend the `research agent persists findings to task dir` regression group with an OpenCode-specific case asserting YAML `permission:` frontmatter and body contents, closing the gap that masked #211.",
7
+ "migrations": [],
8
+ "notes": "RC install: `npm install -g @mindfoldhq/trellis@rc`. Projects on 0.5 (beta or rc.0) run `trellis update`. Projects upgrading from 0.4.x run `trellis update --migrate`; the 0.5 migration chain begins at 0.5.0-beta.0. rc.1 adds no new migration entries."
9
+ }
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  description: |
3
- Code and tech search expert. Pure research, no code modifications. Finds files, patterns, and tech solutions.
3
+ Code and tech search expert. Finds files, patterns, and tech solutions, and PERSISTS every finding to the current task's research/ directory. No code modifications outside that directory.
4
4
  mode: subagent
5
5
  permission:
6
6
  read: allow
7
- write: deny
8
- edit: deny
7
+ write: allow
8
+ edit: allow
9
9
  bash: allow
10
10
  glob: allow
11
11
  grep: allow
@@ -16,114 +16,113 @@ permission:
16
16
 
17
17
  You are the Research Agent in the Trellis workflow.
18
18
 
19
- ## Context Self-Loading
19
+ ## Core Principle
20
+
21
+ **You do one thing: find, explain, and PERSIST information.**
20
22
 
21
- **If you see "# Research Agent Task" header with pre-loaded context above, skip this section.**
23
+ Conversations 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.
22
24
 
23
- Otherwise, if task-specific research is needed:
25
+ ---
24
26
 
25
- 1. Run `python3 ./.trellis/scripts/task.py current --source` → get active task directory and source (if set)
26
- 2. For each entry in JSONL (if task dir exists):
27
- - If `path` is a file → Read it
28
- - If `path` is a directory → Read all `.md` files in it
27
+ ## Core Responsibilities
29
28
 
30
- Project spec locations for reference:
31
- - `.trellis/spec/<package>/<layer>/` - Package-specific standards
32
- - `.trellis/spec/guides/` - Thinking guides
33
- - `.trellis/big-question/` - Known issues and pitfalls
29
+ 1. **Internal Search** locate files/components, understand code logic, discover patterns (Glob, Grep, Read)
30
+ 2. **External Search** library docs, API references, best practices (web search)
31
+ 3. **Persist** write each research topic to `{TASK_DIR}/research/<topic>.md`
32
+ 4. **Report** — return file paths + one-line summaries to the main agent (not full content)
34
33
 
35
34
  ---
36
35
 
37
- ## Core Principle
36
+ ## Workflow
38
37
 
39
- **You do one thing: find and explain information.**
38
+ ### Step 1: Resolve Current Task
40
39
 
41
- You are a documenter, not a reviewer. Your job is to help get the information needed.
40
+ Run `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.
42
41
 
43
- ---
42
+ Ensure `{TASK_DIR}/research/` exists:
44
43
 
45
- ## Core Responsibilities
44
+ ```bash
45
+ mkdir -p <TASK_DIR>/research
46
+ ```
46
47
 
47
- ### 1. Internal Search (Project Code)
48
+ ### Step 2: Understand Search Request
48
49
 
49
- | Search Type | Goal | Tools |
50
- |-------------|------|-------|
51
- | **WHERE** | Locate files/components | Glob, Grep |
52
- | **HOW** | Understand code logic | Read, Grep |
53
- | **PATTERN** | Discover existing patterns | Grep, Read |
50
+ Classify: internal / external / mixed. Determine scope (global / specific directory) and expected shape (file list / pattern notes / tech comparison).
54
51
 
55
- ### 2. External Search (Tech Solutions)
52
+ ### Step 3: Execute Search
56
53
 
57
- Use web search for best practices and code examples.
54
+ Run independent searches in parallel (Glob + Grep + web) for efficiency.
58
55
 
59
- ---
56
+ ### Step 4: Persist Each Topic
60
57
 
61
- ## Strict Boundaries
58
+ For each distinct research topic, Write a markdown file at `{TASK_DIR}/research/<topic-slug>.md`. Use the File Format below.
62
59
 
63
- ### Only Allowed
60
+ ### Step 5: Report to Main Agent
64
61
 
65
- - Describe **what exists**
66
- - Describe **where it is**
67
- - Describe **how it works**
68
- - Describe **how components interact**
62
+ Reply with ONLY:
69
63
 
70
- ### Forbidden (unless explicitly asked)
64
+ - List of files written (paths relative to repo root)
65
+ - One-line summary per file
66
+ - Any critical caveats that the main agent needs to know right now
71
67
 
72
- - Suggest improvements
73
- - Criticize implementation
74
- - Recommend refactoring
75
- - Modify any files
76
- - Execute git commands
68
+ Do NOT paste full research content into the reply. The files are the contract.
77
69
 
78
70
  ---
79
71
 
80
- ## Workflow
81
-
82
- ### Step 1: Understand Search Request
72
+ ## Scope Limits (Strict)
83
73
 
84
- Analyze the query, determine:
74
+ ### Write ALLOWED
85
75
 
86
- - Search type (internal/external/mixed)
87
- - Search scope (global/specific directory)
88
- - Expected output (file list/code patterns/tech solutions)
76
+ - `{TASK_DIR}/research/*.md` your own output
77
+ - Creating `{TASK_DIR}/research/` if it doesn't exist (via `mkdir -p`)
89
78
 
90
- ### Step 2: Execute Search
79
+ ### Write FORBIDDEN
91
80
 
92
- Execute multiple independent searches in parallel for efficiency.
81
+ - Code files (`src/`, `lib/`, …)
82
+ - Spec files (`.trellis/spec/`) — main agent should use `update-spec` skill instead
83
+ - `.trellis/scripts/`, `.trellis/workflow.md`, platform config (`.claude/`, `.cursor/`, `.opencode/`, etc.)
84
+ - Other task directories
85
+ - Any git operation (commit / push / branch / merge)
93
86
 
94
- ### Step 3: Organize Results
95
-
96
- Output structured results in report format.
87
+ If the user asks you to edit code, decline and suggest spawning `implement` instead.
97
88
 
98
89
  ---
99
90
 
100
- ## Report Format
91
+ ## File Format
92
+
93
+ Each `{TASK_DIR}/research/<topic>.md` should follow:
101
94
 
102
95
  ```markdown
103
- ## Search Results
96
+ # Research: <topic>
104
97
 
105
- ### Query
98
+ - **Query**: <original query>
99
+ - **Scope**: <internal / external / mixed>
100
+ - **Date**: <YYYY-MM-DD>
106
101
 
107
- {original query}
102
+ ## Findings
108
103
 
109
104
  ### Files Found
110
105
 
111
106
  | File Path | Description |
112
- |-----------|-------------|
107
+ |---|---|
113
108
  | `src/services/xxx.ts` | Main implementation |
114
109
  | `src/types/xxx.ts` | Type definitions |
115
110
 
116
- ### Code Pattern Analysis
111
+ ### Code Patterns
112
+
113
+ <describe patterns, cite file:line>
114
+
115
+ ### External References
117
116
 
118
- {Describe discovered patterns, cite specific files and line numbers}
117
+ - [Library X docs](url) <why relevant, version constraints>
119
118
 
120
- ### Related Spec Documents
119
+ ### Related Specs
121
120
 
122
- - `.trellis/spec/xxx.md` - {description}
121
+ - `.trellis/spec/xxx.md` <description>
123
122
 
124
- ### Not Found
123
+ ## Caveats / Not Found
125
124
 
126
- {If some content was not found, explain}
125
+ <anything incomplete or uncertain>
127
126
  ```
128
127
 
129
128
  ---
@@ -134,12 +133,13 @@ Output structured results in report format.
134
133
 
135
134
  - Provide specific file paths and line numbers
136
135
  - Quote actual code snippets
137
- - Distinguish "definitely found" and "possibly related"
138
- - Explain search scope and limitations
136
+ - Persist every topic to its own file
137
+ - Return file paths in your reply, not the full content
138
+ - Mark "not found" explicitly when searches come up empty
139
139
 
140
140
  ### DON'T
141
141
 
142
+ - Don't write code or modify files outside `{TASK_DIR}/research/`
142
143
  - Don't guess uncertain info
143
- - Don't omit important search results
144
- - Don't add improvement suggestions in report (unless explicitly asked)
145
- - Don't modify any files
144
+ - Don't paste full research text into the reply (files are the deliverable)
145
+ - Don't propose improvements or critique implementation (that's not your role)
@@ -0,0 +1,432 @@
1
+ /* global process */
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs"
3
+ import { basename, join } from "path"
4
+ import { execFileSync } from "child_process"
5
+ import { platform } from "os"
6
+ import { debugLog } from "./trellis-context.js"
7
+
8
+ const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
9
+
10
+ const FIRST_REPLY_NOTICE = `<first-reply-notice>
11
+ On the first visible assistant reply in this session, begin with exactly one short Chinese sentence:
12
+ Trellis SessionStart 已注入:workflow、当前任务状态、开发者身份、git 状态、active tasks、spec 索引已加载。
13
+ Then continue directly with the user's request. This notice is one-shot: do not repeat it after the first assistant reply in the same session.
14
+ </first-reply-notice>`
15
+
16
+ function hasCuratedJsonlEntry(jsonlPath) {
17
+ try {
18
+ const content = readFileSync(jsonlPath, "utf-8")
19
+ for (const rawLine of content.split(/\r?\n/)) {
20
+ const line = rawLine.trim()
21
+ if (!line) continue
22
+ try {
23
+ const row = JSON.parse(line)
24
+ if (row && typeof row === "object" && typeof row.file === "string" && row.file) {
25
+ return true
26
+ }
27
+ } catch {
28
+ // Ignore malformed line
29
+ }
30
+ }
31
+ } catch {
32
+ return false
33
+ }
34
+ return false
35
+ }
36
+
37
+ function getTaskStatus(ctx, platformInput = null) {
38
+ const active = ctx.getActiveTask(platformInput)
39
+ const taskRef = active.taskPath
40
+ if (!taskRef) {
41
+ return `Status: NO ACTIVE TASK\nSource: ${active.source}\nNext: Describe what you want to work on`
42
+ }
43
+
44
+ const taskDir = ctx.resolveTaskDir(taskRef)
45
+
46
+ if (active.stale || !taskDir || !existsSync(taskDir)) {
47
+ return `Status: STALE POINTER\nTask: ${taskRef}\nSource: ${active.source}\nNext: Task directory not found. Run: python3 ./.trellis/scripts/task.py finish`
48
+ }
49
+
50
+ let taskData = {}
51
+ const taskJsonPath = join(taskDir, "task.json")
52
+ if (existsSync(taskJsonPath)) {
53
+ try {
54
+ taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8"))
55
+ } catch {
56
+ // Ignore parse errors
57
+ }
58
+ }
59
+
60
+ const taskTitle = taskData.title || taskRef
61
+ const taskStatus = taskData.status || "unknown"
62
+
63
+ if (taskStatus === "completed") {
64
+ const dirName = basename(taskDir)
65
+ return `Status: COMPLETED\nTask: ${taskTitle}\nSource: ${active.source}\nNext: Archive with \`python3 ./.trellis/scripts/task.py archive ${dirName}\` or start a new task`
66
+ }
67
+
68
+ let hasContext = false
69
+ for (const jsonlName of ["implement.jsonl", "check.jsonl"]) {
70
+ const jsonlPath = join(taskDir, jsonlName)
71
+ if (existsSync(jsonlPath) && hasCuratedJsonlEntry(jsonlPath)) {
72
+ hasContext = true
73
+ break
74
+ }
75
+ }
76
+
77
+ const hasPrd = existsSync(join(taskDir, "prd.md"))
78
+
79
+ if (!hasPrd) {
80
+ 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`
81
+ }
82
+
83
+ if (!hasContext) {
84
+ 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\``
85
+ }
86
+
87
+ return (
88
+ `Status: READY\nTask: ${taskTitle}\n` +
89
+ `Source: ${active.source}\n` +
90
+ "Next required action: dispatch `trellis-implement` per Phase 2.1. " +
91
+ "For agent-capable platforms, the default is to NOT edit code in the main session. " +
92
+ "After implementation, dispatch `trellis-check` per Phase 2.2 before reporting completion.\n" +
93
+ "User override (per-turn escape hatch): if the user's CURRENT message explicitly tells the " +
94
+ "main session to handle it directly (\"你直接改\" / \"别派 sub-agent\" / \"main session 写就行\" / " +
95
+ "\"do it inline\" / \"不用 sub-agent\"), honor it for this turn and edit code directly. " +
96
+ "Per-turn only; do NOT invent an override the user did not say."
97
+ )
98
+ }
99
+
100
+ function loadTrellisConfig(directory, contextKey = null) {
101
+ const scriptPath = join(directory, ".trellis", "scripts", "get_context.py")
102
+ if (!existsSync(scriptPath)) {
103
+ return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
104
+ }
105
+ try {
106
+ const output = execFileSync(PYTHON_CMD, [scriptPath, "--mode", "packages", "--json"], {
107
+ cwd: directory,
108
+ timeout: 5000,
109
+ encoding: "utf-8",
110
+ stdio: ["pipe", "pipe", "pipe"],
111
+ env: {
112
+ ...process.env,
113
+ ...(contextKey ? { TRELLIS_CONTEXT_ID: contextKey } : {}),
114
+ },
115
+ })
116
+ const data = JSON.parse(output)
117
+ if (data.mode !== "monorepo") {
118
+ return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
119
+ }
120
+ const pkgDict = {}
121
+ for (const pkg of (data.packages || [])) {
122
+ pkgDict[pkg.name] = pkg
123
+ }
124
+ return {
125
+ isMonorepo: true,
126
+ packages: pkgDict,
127
+ specScope: data.specScope || null,
128
+ activeTaskPackage: data.activeTaskPackage || null,
129
+ defaultPackage: data.defaultPackage || null,
130
+ }
131
+ } catch (e) {
132
+ debugLog("session", "loadTrellisConfig error:", e.message)
133
+ return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
134
+ }
135
+ }
136
+
137
+ function checkLegacySpec(directory, config) {
138
+ if (!config.isMonorepo || Object.keys(config.packages).length === 0) {
139
+ return null
140
+ }
141
+
142
+ const specDir = join(directory, ".trellis", "spec")
143
+ if (!existsSync(specDir)) return null
144
+
145
+ let hasLegacy = false
146
+ for (const name of ["backend", "frontend"]) {
147
+ if (existsSync(join(specDir, name, "index.md"))) {
148
+ hasLegacy = true
149
+ break
150
+ }
151
+ }
152
+ if (!hasLegacy) return null
153
+
154
+ const pkgNames = Object.keys(config.packages).sort()
155
+ const missing = pkgNames.filter(name => !existsSync(join(specDir, name)))
156
+
157
+ if (missing.length === 0) return null
158
+
159
+ if (missing.length === pkgNames.length) {
160
+ return (
161
+ `[!] Legacy spec structure detected: found \`spec/backend/\` or \`spec/frontend/\` ` +
162
+ `but no package-scoped \`spec/<package>/\` directories.\n` +
163
+ `Monorepo packages: ${pkgNames.join(", ")}\n` +
164
+ `Please reorganize: \`spec/backend/\` -> \`spec/<package>/backend/\``
165
+ )
166
+ }
167
+ return (
168
+ `[!] Partial spec migration detected: packages ${missing.join(", ")} ` +
169
+ `still missing \`spec/<pkg>/\` directory.\n` +
170
+ `Please complete migration for all packages.`
171
+ )
172
+ }
173
+
174
+ function resolveSpecScope(config) {
175
+ if (!config.isMonorepo || Object.keys(config.packages).length === 0) {
176
+ return null
177
+ }
178
+
179
+ const { specScope, activeTaskPackage, defaultPackage, packages } = config
180
+ if (specScope == null) return null
181
+
182
+ if (specScope === "active_task") {
183
+ if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage])
184
+ if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage])
185
+ return null
186
+ }
187
+
188
+ if (Array.isArray(specScope)) {
189
+ const valid = new Set()
190
+ for (const entry of specScope) {
191
+ if (entry in packages) {
192
+ valid.add(entry)
193
+ }
194
+ }
195
+ if (valid.size > 0) return valid
196
+ if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage])
197
+ if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage])
198
+ return null
199
+ }
200
+
201
+ return null
202
+ }
203
+
204
+ export function buildSessionContext(ctx, platformInput = null) {
205
+ const directory = ctx.directory
206
+ const trellisDir = join(directory, ".trellis")
207
+ const contextKey = typeof ctx.getContextKey === "function"
208
+ ? ctx.getContextKey(platformInput)
209
+ : null
210
+
211
+ const config = loadTrellisConfig(directory, contextKey)
212
+ const allowedPkgs = resolveSpecScope(config)
213
+
214
+ const parts = []
215
+
216
+ parts.push(`<trellis-context>
217
+ You are starting a new session in a Trellis-managed project.
218
+ Read and follow all instructions below carefully.
219
+ </trellis-context>`)
220
+ parts.push(FIRST_REPLY_NOTICE)
221
+
222
+ const legacyWarning = checkLegacySpec(directory, config)
223
+ if (legacyWarning) {
224
+ parts.push(`<migration-warning>\n${legacyWarning}\n</migration-warning>`)
225
+ }
226
+
227
+ const contextScript = join(trellisDir, "scripts", "get_context.py")
228
+ if (existsSync(contextScript)) {
229
+ const output = ctx.runScript(contextScript, undefined, contextKey)
230
+ if (output) {
231
+ parts.push("<current-state>")
232
+ parts.push(output)
233
+ parts.push("</current-state>")
234
+ }
235
+ }
236
+
237
+ const workflowContent = ctx.readProjectFile(".trellis/workflow.md")
238
+ if (workflowContent) {
239
+ const allLines = workflowContent.split("\n")
240
+ const overviewLines = [
241
+ "# Development Workflow — Section Index",
242
+ "Full guide: .trellis/workflow.md (read on demand)",
243
+ "",
244
+ "## Table of Contents",
245
+ ]
246
+ for (const line of allLines) {
247
+ if (line.startsWith("## ")) overviewLines.push(line)
248
+ }
249
+ overviewLines.push("", "---", "")
250
+
251
+ let rangeStart = -1
252
+ let rangeEnd = allLines.length
253
+ for (let i = 0; i < allLines.length; i++) {
254
+ const stripped = allLines[i].trim()
255
+ if (rangeStart === -1 && stripped === "## Phase Index") {
256
+ rangeStart = i
257
+ } else if (rangeStart !== -1 && stripped === "## Workflow State Breadcrumbs") {
258
+ rangeEnd = i
259
+ break
260
+ }
261
+ }
262
+ if (rangeStart !== -1) {
263
+ overviewLines.push(...allLines.slice(rangeStart, rangeEnd))
264
+ }
265
+
266
+ parts.push("<workflow>")
267
+ parts.push(overviewLines.join("\n").trimEnd())
268
+ parts.push("</workflow>")
269
+ }
270
+
271
+ parts.push("<guidelines>")
272
+ parts.push(
273
+ "Project spec indexes are listed by path below. Each index contains a " +
274
+ "**Pre-Development Checklist** listing the specific guideline files to " +
275
+ "read before coding.\n\n" +
276
+ "- If you're spawning an implement/check sub-agent, context is injected " +
277
+ "automatically via `{task}/implement.jsonl` / `check.jsonl`. You do NOT " +
278
+ "need to read these indexes yourself.\n" +
279
+ "- For agent-capable platforms, do NOT edit code directly in the main " +
280
+ "session; dispatch `trellis-implement` and `trellis-check` so JSONL " +
281
+ "context is loaded by the sub-agents.\n"
282
+ )
283
+
284
+ const specDir = join(directory, ".trellis", "spec")
285
+
286
+ const guidesIndex = join(specDir, "guides", "index.md")
287
+ if (existsSync(guidesIndex)) {
288
+ const content = ctx.readFile(guidesIndex)
289
+ if (content) {
290
+ parts.push(`## guides (inlined — cross-package thinking guides)\n${content}\n`)
291
+ }
292
+ }
293
+
294
+ const paths = []
295
+ if (existsSync(specDir)) {
296
+ try {
297
+ const subs = readdirSync(specDir).filter(name => {
298
+ if (name.startsWith(".")) return false
299
+ try {
300
+ return statSync(join(specDir, name)).isDirectory()
301
+ } catch {
302
+ return false
303
+ }
304
+ }).sort()
305
+
306
+ for (const sub of subs) {
307
+ if (sub === "guides") continue
308
+
309
+ const indexFile = join(specDir, sub, "index.md")
310
+ if (existsSync(indexFile)) {
311
+ paths.push(`.trellis/spec/${sub}/index.md`)
312
+ } else {
313
+ if (allowedPkgs !== null && !allowedPkgs.has(sub)) continue
314
+ try {
315
+ const nested = readdirSync(join(specDir, sub)).filter(name => {
316
+ try {
317
+ return statSync(join(specDir, sub, name)).isDirectory()
318
+ } catch {
319
+ return false
320
+ }
321
+ }).sort()
322
+ for (const layer of nested) {
323
+ const nestedIndex = join(specDir, sub, layer, "index.md")
324
+ if (existsSync(nestedIndex)) {
325
+ paths.push(`.trellis/spec/${sub}/${layer}/index.md`)
326
+ }
327
+ }
328
+ } catch {
329
+ // Ignore directory read errors
330
+ }
331
+ }
332
+ }
333
+ } catch {
334
+ // Ignore spec directory read errors
335
+ }
336
+ }
337
+
338
+ if (paths.length > 0) {
339
+ parts.push("## Available spec indexes (read on demand)")
340
+ for (const p of paths) {
341
+ parts.push(`- ${p}`)
342
+ }
343
+ parts.push("")
344
+ }
345
+
346
+ parts.push(
347
+ "Discover more via: " +
348
+ "`python3 ./.trellis/scripts/get_context.py --mode packages`"
349
+ )
350
+ parts.push("</guidelines>")
351
+
352
+ const taskStatus = getTaskStatus(ctx, platformInput)
353
+ parts.push(`<task-status>\n${taskStatus}\n</task-status>`)
354
+
355
+ parts.push(`<ready>
356
+ Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them.
357
+ When the user sends the first message, follow <task-status> and the workflow guide.
358
+ If a task is READY, execute its Next required action without asking whether to continue.
359
+ </ready>`)
360
+
361
+ return parts.join("\n\n")
362
+ }
363
+
364
+ function getTrellisMetadata(metadata) {
365
+ if (!metadata || typeof metadata !== "object") {
366
+ return {}
367
+ }
368
+
369
+ const trellis = metadata.trellis
370
+ if (!trellis || typeof trellis !== "object") {
371
+ return {}
372
+ }
373
+
374
+ return trellis
375
+ }
376
+
377
+ function markPartAsSessionStart(part) {
378
+ const metadata = part.metadata && typeof part.metadata === "object"
379
+ ? part.metadata
380
+ : {}
381
+ part.metadata = {
382
+ ...metadata,
383
+ trellis: {
384
+ ...getTrellisMetadata(metadata),
385
+ sessionStart: true,
386
+ },
387
+ }
388
+ }
389
+
390
+ function hasSessionStartMarker(part) {
391
+ if (!part || part.type !== "text" || typeof part.text !== "string") {
392
+ return false
393
+ }
394
+
395
+ return getTrellisMetadata(part.metadata).sessionStart === true
396
+ }
397
+
398
+ export function hasInjectedTrellisContext(messages) {
399
+ if (!Array.isArray(messages)) {
400
+ return false
401
+ }
402
+
403
+ return messages.some(message => {
404
+ if (!message?.info || message.info.role !== "user" || !Array.isArray(message.parts)) {
405
+ return false
406
+ }
407
+
408
+ return message.parts.some(hasSessionStartMarker)
409
+ })
410
+ }
411
+
412
+ export async function hasPersistedInjectedContext(client, directory, sessionID) {
413
+ try {
414
+ const response = await client.session.messages({
415
+ path: { id: sessionID },
416
+ query: { directory },
417
+ throwOnError: true,
418
+ })
419
+ return hasInjectedTrellisContext(response.data || [])
420
+ } catch (error) {
421
+ debugLog(
422
+ "session",
423
+ "Failed to read session history for dedupe:",
424
+ error instanceof Error ? error.message : String(error),
425
+ )
426
+ return false
427
+ }
428
+ }
429
+
430
+ export function markContextInjected(part) {
431
+ markPartAsSessionStart(part)
432
+ }
@@ -6,476 +6,12 @@
6
6
  * Uses OpenCode's chat.message hook directly so the context persists in history.
7
7
  */
8
8
 
9
- import { existsSync, readFileSync, readdirSync, statSync } from "fs"
10
- import { basename, join } from "path"
11
- import { execFileSync } from "child_process"
12
- import { platform } from "os"
13
9
  import { TrellisContext, contextCollector, debugLog } from "../lib/trellis-context.js"
14
-
15
- const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
16
-
17
- const FIRST_REPLY_NOTICE = `<first-reply-notice>
18
- On the first visible assistant reply in this session, begin with exactly one short Chinese sentence:
19
- Trellis SessionStart 已注入:workflow、当前任务状态、开发者身份、git 状态、active tasks、spec 索引已加载。
20
- Then continue directly with the user's request. This notice is one-shot: do not repeat it after the first assistant reply in the same session.
21
- </first-reply-notice>`
22
-
23
-
24
- /**
25
- * Return true iff jsonl has at least one row with a `file` field.
26
- * A freshly seeded jsonl only contains a `{"_example": ...}` row (no `file`
27
- * key) — that is NOT "ready". Readiness requires at least one curated entry.
28
- * Matches the contract used by `shared-hooks/inject-subagent-context.py`.
29
- */
30
- function hasCuratedJsonlEntry(jsonlPath) {
31
- try {
32
- const content = readFileSync(jsonlPath, "utf-8")
33
- for (const rawLine of content.split(/\r?\n/)) {
34
- const line = rawLine.trim()
35
- if (!line) continue
36
- try {
37
- const row = JSON.parse(line)
38
- if (row && typeof row === "object" && typeof row.file === "string" && row.file) {
39
- return true
40
- }
41
- } catch {
42
- // Ignore malformed line — move on.
43
- }
44
- }
45
- } catch {
46
- return false
47
- }
48
- return false
49
- }
50
-
51
- /**
52
- * Check current task status and return structured status string.
53
- * JavaScript equivalent of _get_task_status in Claude's session-start.py.
54
- */
55
- function getTaskStatus(ctx, platformInput = null) {
56
- const active = ctx.getActiveTask(platformInput)
57
- const taskRef = active.taskPath
58
- if (!taskRef) {
59
- return `Status: NO ACTIVE TASK\nSource: ${active.source}\nNext: Describe what you want to work on`
60
- }
61
-
62
- const taskDir = ctx.resolveTaskDir(taskRef)
63
-
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`
66
- }
67
-
68
- let taskData = {}
69
- const taskJsonPath = join(taskDir, "task.json")
70
- if (existsSync(taskJsonPath)) {
71
- try {
72
- taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8"))
73
- } catch {
74
- // Ignore parse errors
75
- }
76
- }
77
-
78
- const taskTitle = taskData.title || taskRef
79
- const taskStatus = taskData.status || "unknown"
80
-
81
- if (taskStatus === "completed") {
82
- const dirName = basename(taskDir)
83
- return `Status: COMPLETED\nTask: ${taskTitle}\nSource: ${active.source}\nNext: Archive with \`python3 ./.trellis/scripts/task.py archive ${dirName}\` or start a new task`
84
- }
85
-
86
- let hasContext = false
87
- for (const jsonlName of ["implement.jsonl", "check.jsonl"]) {
88
- const jsonlPath = join(taskDir, jsonlName)
89
- if (existsSync(jsonlPath) && hasCuratedJsonlEntry(jsonlPath)) {
90
- hasContext = true
91
- break
92
- }
93
- }
94
-
95
- const hasPrd = existsSync(join(taskDir, "prd.md"))
96
-
97
- if (!hasPrd) {
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`
99
- }
100
-
101
- if (!hasContext) {
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\``
103
- }
104
-
105
- return (
106
- `Status: READY\nTask: ${taskTitle}\n` +
107
- `Source: ${active.source}\n` +
108
- "Next required action: dispatch `trellis-implement` per Phase 2.1. " +
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."
115
- )
116
- }
117
-
118
- /**
119
- * Load Trellis config for session-start decisions.
120
- * Calls get_context.py --mode packages --json for reliable config data.
121
- */
122
- function loadTrellisConfig(directory, contextKey = null) {
123
- const scriptPath = join(directory, ".trellis", "scripts", "get_context.py")
124
- if (!existsSync(scriptPath)) {
125
- return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
126
- }
127
- try {
128
- const output = execFileSync(PYTHON_CMD, [scriptPath, "--mode", "packages", "--json"], {
129
- cwd: directory,
130
- timeout: 5000,
131
- encoding: "utf-8",
132
- stdio: ["pipe", "pipe", "pipe"],
133
- env: {
134
- ...process.env,
135
- ...(contextKey ? { TRELLIS_CONTEXT_ID: contextKey } : {}),
136
- },
137
- })
138
- const data = JSON.parse(output)
139
- if (data.mode !== "monorepo") {
140
- return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
141
- }
142
- const pkgDict = {}
143
- for (const pkg of (data.packages || [])) {
144
- pkgDict[pkg.name] = pkg
145
- }
146
- return {
147
- isMonorepo: true,
148
- packages: pkgDict,
149
- specScope: data.specScope || null,
150
- activeTaskPackage: data.activeTaskPackage || null,
151
- defaultPackage: data.defaultPackage || null,
152
- }
153
- } catch (e) {
154
- debugLog("session", "loadTrellisConfig error:", e.message)
155
- return { isMonorepo: false, packages: {}, specScope: null, activeTaskPackage: null, defaultPackage: null }
156
- }
157
- }
158
-
159
-
160
- /**
161
- * Check for legacy spec directory structure in monorepo.
162
- */
163
- function checkLegacySpec(directory, config) {
164
- if (!config.isMonorepo || Object.keys(config.packages).length === 0) {
165
- return null
166
- }
167
-
168
- const specDir = join(directory, ".trellis", "spec")
169
- if (!existsSync(specDir)) return null
170
-
171
- let hasLegacy = false
172
- for (const name of ["backend", "frontend"]) {
173
- if (existsSync(join(specDir, name, "index.md"))) {
174
- hasLegacy = true
175
- break
176
- }
177
- }
178
- if (!hasLegacy) return null
179
-
180
- const pkgNames = Object.keys(config.packages).sort()
181
- const missing = pkgNames.filter(name => !existsSync(join(specDir, name)))
182
-
183
- if (missing.length === 0) return null
184
-
185
- if (missing.length === pkgNames.length) {
186
- return (
187
- `[!] Legacy spec structure detected: found \`spec/backend/\` or \`spec/frontend/\` ` +
188
- `but no package-scoped \`spec/<package>/\` directories.\n` +
189
- `Monorepo packages: ${pkgNames.join(", ")}\n` +
190
- `Please reorganize: \`spec/backend/\` -> \`spec/<package>/backend/\``
191
- )
192
- }
193
- return (
194
- `[!] Partial spec migration detected: packages ${missing.join(", ")} ` +
195
- `still missing \`spec/<pkg>/\` directory.\n` +
196
- `Please complete migration for all packages.`
197
- )
198
- }
199
-
200
-
201
- /**
202
- * Resolve which packages should have their specs injected.
203
- * Returns a Set of allowed package names, or null for full scan.
204
- */
205
- function resolveSpecScope(config) {
206
- if (!config.isMonorepo || Object.keys(config.packages).length === 0) {
207
- return null
208
- }
209
-
210
- const { specScope, activeTaskPackage, defaultPackage, packages } = config
211
- if (specScope == null) return null
212
-
213
- if (specScope === "active_task") {
214
- if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage])
215
- if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage])
216
- return null
217
- }
218
-
219
- if (Array.isArray(specScope)) {
220
- const valid = new Set()
221
- for (const entry of specScope) {
222
- if (entry in packages) {
223
- valid.add(entry)
224
- }
225
- }
226
- if (valid.size > 0) return valid
227
- if (activeTaskPackage && activeTaskPackage in packages) return new Set([activeTaskPackage])
228
- if (defaultPackage && defaultPackage in packages) return new Set([defaultPackage])
229
- return null
230
- }
231
-
232
- return null
233
- }
234
-
235
-
236
- /**
237
- * Build session context for injection
238
- */
239
- export function buildSessionContext(ctx, platformInput = null) {
240
- const directory = ctx.directory
241
- const trellisDir = join(directory, ".trellis")
242
- const contextKey = typeof ctx.getContextKey === "function"
243
- ? ctx.getContextKey(platformInput)
244
- : null
245
-
246
- const config = loadTrellisConfig(directory, contextKey)
247
- const allowedPkgs = resolveSpecScope(config)
248
-
249
- const parts = []
250
-
251
- // 1. Header
252
- parts.push(`<trellis-context>
253
- You are starting a new session in a Trellis-managed project.
254
- Read and follow all instructions below carefully.
255
- </trellis-context>`)
256
- parts.push(FIRST_REPLY_NOTICE)
257
-
258
- // Legacy migration warning
259
- const legacyWarning = checkLegacySpec(directory, config)
260
- if (legacyWarning) {
261
- parts.push(`<migration-warning>\n${legacyWarning}\n</migration-warning>`)
262
- }
263
-
264
- // 2. Current Context (dynamic)
265
- const contextScript = join(trellisDir, "scripts", "get_context.py")
266
- if (existsSync(contextScript)) {
267
- const output = ctx.runScript(contextScript, undefined, contextKey)
268
- if (output) {
269
- parts.push("<current-state>")
270
- parts.push(output)
271
- parts.push("</current-state>")
272
- }
273
- }
274
-
275
- // 3. Workflow Guide — TOC + Phase Index + Phase 1/2/3 step details.
276
- // Meta sections (Core Principles / Trellis System / Breadcrumbs) are NOT
277
- // injected: Core Principles is short prose; Trellis System duplicates
278
- // commands in step bodies; Breadcrumbs are consumed by UserPromptSubmit hook.
279
- const workflowContent = ctx.readProjectFile(".trellis/workflow.md")
280
- if (workflowContent) {
281
- const allLines = workflowContent.split("\n")
282
- const overviewLines = [
283
- "# Development Workflow — Section Index",
284
- "Full guide: .trellis/workflow.md (read on demand)",
285
- "",
286
- "## Table of Contents",
287
- ]
288
- for (const line of allLines) {
289
- if (line.startsWith("## ")) overviewLines.push(line)
290
- }
291
- overviewLines.push("", "---", "")
292
-
293
- // Extract range from "## Phase Index" up to (but excluding)
294
- // "## Workflow State Breadcrumbs". Captures Phase Index + Phase 1/2/3.
295
- let rangeStart = -1
296
- let rangeEnd = allLines.length
297
- for (let i = 0; i < allLines.length; i++) {
298
- const stripped = allLines[i].trim()
299
- if (rangeStart === -1 && stripped === "## Phase Index") {
300
- rangeStart = i
301
- } else if (rangeStart !== -1 && stripped === "## Workflow State Breadcrumbs") {
302
- rangeEnd = i
303
- break
304
- }
305
- }
306
- if (rangeStart !== -1) {
307
- overviewLines.push(...allLines.slice(rangeStart, rangeEnd))
308
- }
309
-
310
- parts.push("<workflow>")
311
- parts.push(overviewLines.join("\n").trimEnd())
312
- parts.push("</workflow>")
313
- }
314
-
315
- // 4. Guidelines — paths-only for most indexes; guides/ inlined (cross-package,
316
- // broadly useful). Sub-agents get their specific specs via jsonl injection.
317
- parts.push("<guidelines>")
318
- parts.push(
319
- "Project spec indexes are listed by path below. Each index contains a " +
320
- "**Pre-Development Checklist** listing the specific guideline files to " +
321
- "read before coding.\n\n" +
322
- "- If you're spawning an implement/check sub-agent, context is injected " +
323
- "automatically via `{task}/implement.jsonl` / `check.jsonl`. You do NOT " +
324
- "need to read these indexes yourself.\n" +
325
- "- For agent-capable platforms, do NOT edit code directly in the main " +
326
- "session; dispatch `trellis-implement` and `trellis-check` so JSONL " +
327
- "context is loaded by the sub-agents.\n"
328
- )
329
-
330
- const specDir = join(directory, ".trellis", "spec")
331
-
332
- // guides/ inlined
333
- const guidesIndex = join(specDir, "guides", "index.md")
334
- if (existsSync(guidesIndex)) {
335
- const content = ctx.readFile(guidesIndex)
336
- if (content) {
337
- parts.push(`## guides (inlined — cross-package thinking guides)\n${content}\n`)
338
- }
339
- }
340
-
341
- // Other indexes — paths only
342
- const paths = []
343
- if (existsSync(specDir)) {
344
- try {
345
- const subs = readdirSync(specDir).filter(name => {
346
- if (name.startsWith(".")) return false
347
- try {
348
- return statSync(join(specDir, name)).isDirectory()
349
- } catch {
350
- return false
351
- }
352
- }).sort()
353
-
354
- for (const sub of subs) {
355
- if (sub === "guides") continue // already inlined above
356
-
357
- const indexFile = join(specDir, sub, "index.md")
358
- if (existsSync(indexFile)) {
359
- paths.push(`.trellis/spec/${sub}/index.md`)
360
- } else {
361
- if (allowedPkgs !== null && !allowedPkgs.has(sub)) continue
362
- try {
363
- const nested = readdirSync(join(specDir, sub)).filter(name => {
364
- try {
365
- return statSync(join(specDir, sub, name)).isDirectory()
366
- } catch {
367
- return false
368
- }
369
- }).sort()
370
- for (const layer of nested) {
371
- const nestedIndex = join(specDir, sub, layer, "index.md")
372
- if (existsSync(nestedIndex)) {
373
- paths.push(`.trellis/spec/${sub}/${layer}/index.md`)
374
- }
375
- }
376
- } catch {
377
- // Ignore directory read errors
378
- }
379
- }
380
- }
381
- } catch {
382
- // Ignore spec directory read errors
383
- }
384
- }
385
-
386
- if (paths.length > 0) {
387
- parts.push("## Available spec indexes (read on demand)")
388
- for (const p of paths) {
389
- parts.push(`- ${p}`)
390
- }
391
- parts.push("")
392
- }
393
-
394
- parts.push(
395
- "Discover more via: " +
396
- "`python3 ./.trellis/scripts/get_context.py --mode packages`"
397
- )
398
- parts.push("</guidelines>")
399
-
400
- // 6. Task status
401
- const taskStatus = getTaskStatus(ctx, platformInput)
402
- parts.push(`<task-status>\n${taskStatus}\n</task-status>`)
403
-
404
- // 7. Final directive
405
- parts.push(`<ready>
406
- Context loaded. Workflow index, project state, and guidelines are already injected above — do NOT re-read them.
407
- When the user sends the first message, follow <task-status> and the workflow guide.
408
- If a task is READY, execute its Next required action without asking whether to continue.
409
- </ready>`)
410
-
411
- return parts.join("\n\n")
412
- }
413
-
414
- function getTrellisMetadata(metadata) {
415
- if (!metadata || typeof metadata !== "object") {
416
- return {}
417
- }
418
-
419
- const trellis = metadata.trellis
420
- if (!trellis || typeof trellis !== "object") {
421
- return {}
422
- }
423
-
424
- return trellis
425
- }
426
-
427
- function markPartAsSessionStart(part) {
428
- const metadata = part.metadata && typeof part.metadata === "object"
429
- ? part.metadata
430
- : {}
431
- part.metadata = {
432
- ...metadata,
433
- trellis: {
434
- ...getTrellisMetadata(metadata),
435
- sessionStart: true,
436
- },
437
- }
438
- }
439
-
440
- function hasSessionStartMarker(part) {
441
- if (!part || part.type !== "text" || typeof part.text !== "string") {
442
- return false
443
- }
444
-
445
- return getTrellisMetadata(part.metadata).sessionStart === true
446
- }
447
-
448
- export function hasInjectedTrellisContext(messages) {
449
- if (!Array.isArray(messages)) {
450
- return false
451
- }
452
-
453
- return messages.some(message => {
454
- if (!message?.info || message.info.role !== "user" || !Array.isArray(message.parts)) {
455
- return false
456
- }
457
-
458
- return message.parts.some(hasSessionStartMarker)
459
- })
460
- }
461
-
462
- async function hasPersistedInjectedContext(client, directory, sessionID) {
463
- try {
464
- const response = await client.session.messages({
465
- path: { id: sessionID },
466
- query: { directory },
467
- throwOnError: true,
468
- })
469
- return hasInjectedTrellisContext(response.data || [])
470
- } catch (error) {
471
- debugLog(
472
- "session",
473
- "Failed to read session history for dedupe:",
474
- error instanceof Error ? error.message : String(error),
475
- )
476
- return false
477
- }
478
- }
10
+ import {
11
+ buildSessionContext,
12
+ hasPersistedInjectedContext,
13
+ markContextInjected,
14
+ } from "../lib/session-utils.js"
479
15
 
480
16
  // OpenCode 1.2.x expects plugins to be factory functions (see inject-subagent-context.js comment).
481
17
  export default async ({ directory, client }) => {
@@ -483,77 +19,70 @@ export default async ({ directory, client }) => {
483
19
  debugLog("session", "Plugin loaded, directory:", directory)
484
20
 
485
21
  return {
486
- // Clear in-memory dedupe after compaction so context can be re-injected.
487
- event: ({ event }) => {
488
- try {
489
- if (event?.type === "session.compacted" && event?.properties?.sessionID) {
490
- const sessionID = event.properties.sessionID
491
- contextCollector.clear(sessionID)
492
- debugLog("session", "Cleared processed flag after compaction for session:", sessionID)
493
- }
494
- } catch (error) {
495
- debugLog(
496
- "session",
497
- "Error in event hook:",
498
- error instanceof Error ? error.message : String(error),
499
- )
22
+ event: ({ event }) => {
23
+ try {
24
+ if (event?.type === "session.compacted" && event?.properties?.sessionID) {
25
+ const sessionID = event.properties.sessionID
26
+ contextCollector.clear(sessionID)
27
+ debugLog("session", "Cleared processed flag after compaction for session:", sessionID)
500
28
  }
501
- },
502
-
503
- // chat.message - triggered when user sends a message.
504
- // Modify the message in-place so the context is persisted with updateMessage/updatePart.
505
- "chat.message": async (input, output) => {
506
- try {
507
- const sessionID = input.sessionID
508
- const agent = input.agent || "unknown"
509
- debugLog("session", "chat.message called, sessionID:", sessionID, "agent:", agent)
510
-
511
- // Skip in non-interactive mode
512
- if (process.env.OPENCODE_NON_INTERACTIVE === "1") {
513
- debugLog("session", "Skipping - non-interactive mode")
514
- return
515
- }
29
+ } catch (error) {
30
+ debugLog(
31
+ "session",
32
+ "Error in event hook:",
33
+ error instanceof Error ? error.message : String(error),
34
+ )
35
+ }
36
+ },
516
37
 
517
- // Only inject on first message
518
- if (contextCollector.isProcessed(sessionID)) {
519
- debugLog("session", "Skipping - session already processed")
520
- return
521
- }
38
+ // chat.message - triggered when user sends a message.
39
+ // Modify the message in-place so the context is persisted with updateMessage/updatePart.
40
+ "chat.message": async (input, output) => {
41
+ try {
42
+ const sessionID = input.sessionID
43
+ const agent = input.agent || "unknown"
44
+ debugLog("session", "chat.message called, sessionID:", sessionID, "agent:", agent)
522
45
 
523
- if (await hasPersistedInjectedContext(client, ctx.directory, sessionID)) {
524
- contextCollector.markProcessed(sessionID)
525
- debugLog("session", "Skipping - session already contains persisted Trellis context")
526
- return
527
- }
46
+ if (process.env.OPENCODE_NON_INTERACTIVE === "1") {
47
+ debugLog("session", "Skipping - non-interactive mode")
48
+ return
49
+ }
528
50
 
529
- // Build context
530
- const context = buildSessionContext(ctx, input)
531
- debugLog("session", "Built context, length:", context.length)
51
+ if (contextCollector.isProcessed(sessionID)) {
52
+ debugLog("session", "Skipping - session already processed")
53
+ return
54
+ }
532
55
 
533
- // Inject context directly into output.parts so it gets persisted by updatePart
534
- const parts = output?.parts || []
535
- const textPartIndex = parts.findIndex(
536
- p => p.type === "text" && p.text !== undefined
537
- )
56
+ if (await hasPersistedInjectedContext(client, ctx.directory, sessionID)) {
57
+ contextCollector.markProcessed(sessionID)
58
+ debugLog("session", "Skipping - session already contains persisted Trellis context")
59
+ return
60
+ }
538
61
 
539
- if (textPartIndex !== -1) {
540
- const originalText = parts[textPartIndex].text || ""
541
- parts[textPartIndex].text = `${context}\n\n---\n\n${originalText}`
542
- markPartAsSessionStart(parts[textPartIndex])
543
- debugLog("session", "Injected context into chat.message text part, length:", context.length)
544
- } else {
545
- // No existing text part: prepend a new one
546
- const injectedPart = { type: "text", text: context }
547
- markPartAsSessionStart(injectedPart)
548
- parts.unshift(injectedPart)
549
- debugLog("session", "Prepended new text part with context, length:", context.length)
550
- }
62
+ const context = buildSessionContext(ctx, input)
63
+ debugLog("session", "Built context, length:", context.length)
551
64
 
552
- contextCollector.markProcessed(sessionID)
65
+ const parts = output?.parts || []
66
+ const textPartIndex = parts.findIndex(
67
+ p => p.type === "text" && p.text !== undefined
68
+ )
553
69
 
554
- } catch (error) {
555
- debugLog("session", "Error in chat.message:", error.message, error.stack)
70
+ if (textPartIndex !== -1) {
71
+ const originalText = parts[textPartIndex].text || ""
72
+ parts[textPartIndex].text = `${context}\n\n---\n\n${originalText}`
73
+ markContextInjected(parts[textPartIndex])
74
+ debugLog("session", "Injected context into chat.message text part, length:", context.length)
75
+ } else {
76
+ const injectedPart = { type: "text", text: context }
77
+ markContextInjected(injectedPart)
78
+ parts.unshift(injectedPart)
79
+ debugLog("session", "Prepended new text part with context, length:", context.length)
556
80
  }
81
+
82
+ contextCollector.markProcessed(sessionID)
83
+ } catch (error) {
84
+ debugLog("session", "Error in chat.message:", error.message, error.stack)
557
85
  }
86
+ },
558
87
  }
559
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindfoldhq/trellis",
3
- "version": "0.5.0-rc.0",
3
+ "version": "0.5.0-rc.1",
4
4
  "description": "AI capabilities grow like ivy — Trellis provides the structure to guide them along a disciplined path",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",