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

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 (48) hide show
  1. package/dist/cli/index.d.ts.map +1 -1
  2. package/dist/cli/index.js +21 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/uninstall.d.ts +27 -0
  5. package/dist/commands/uninstall.d.ts.map +1 -0
  6. package/dist/commands/uninstall.js +313 -0
  7. package/dist/commands/uninstall.js.map +1 -0
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +5 -0
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/configurators/claude.d.ts.map +1 -1
  12. package/dist/configurators/claude.js +2 -2
  13. package/dist/configurators/claude.js.map +1 -1
  14. package/dist/configurators/codex.d.ts.map +1 -1
  15. package/dist/configurators/codex.js +5 -5
  16. package/dist/configurators/codex.js.map +1 -1
  17. package/dist/configurators/copilot.d.ts.map +1 -1
  18. package/dist/configurators/copilot.js +6 -4
  19. package/dist/configurators/copilot.js.map +1 -1
  20. package/dist/configurators/index.d.ts.map +1 -1
  21. package/dist/configurators/index.js +14 -4
  22. package/dist/configurators/index.js.map +1 -1
  23. package/dist/configurators/opencode.d.ts.map +1 -1
  24. package/dist/configurators/opencode.js +2 -2
  25. package/dist/configurators/opencode.js.map +1 -1
  26. package/dist/configurators/pi.d.ts.map +1 -1
  27. package/dist/configurators/pi.js +2 -2
  28. package/dist/configurators/pi.js.map +1 -1
  29. package/dist/configurators/shared.d.ts +12 -0
  30. package/dist/configurators/shared.d.ts.map +1 -1
  31. package/dist/configurators/shared.js +94 -24
  32. package/dist/configurators/shared.js.map +1 -1
  33. package/dist/configurators/workflow.d.ts.map +1 -1
  34. package/dist/configurators/workflow.js +3 -2
  35. package/dist/configurators/workflow.js.map +1 -1
  36. package/dist/migrations/manifests/0.5.0-rc.1.json +9 -0
  37. package/dist/migrations/manifests/0.5.0-rc.2.json +9 -0
  38. package/dist/templates/extract.d.ts.map +1 -1
  39. package/dist/templates/extract.js +4 -1
  40. package/dist/templates/extract.js.map +1 -1
  41. package/dist/templates/opencode/agents/trellis-research.md +68 -68
  42. package/dist/templates/opencode/lib/session-utils.js +432 -0
  43. package/dist/templates/opencode/plugins/session-start.js +60 -531
  44. package/dist/utils/uninstall-scrubbers.d.ts +65 -0
  45. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -0
  46. package/dist/utils/uninstall-scrubbers.js +320 -0
  47. package/dist/utils/uninstall-scrubbers.js.map +1 -0
  48. package/package.json +1 -1
@@ -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
+ }