@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.
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +21 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/uninstall.d.ts +27 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +313 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +5 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/claude.d.ts.map +1 -1
- package/dist/configurators/claude.js +2 -2
- package/dist/configurators/claude.js.map +1 -1
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +5 -5
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/copilot.d.ts.map +1 -1
- package/dist/configurators/copilot.js +6 -4
- package/dist/configurators/copilot.js.map +1 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +14 -4
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/opencode.d.ts.map +1 -1
- package/dist/configurators/opencode.js +2 -2
- package/dist/configurators/opencode.js.map +1 -1
- package/dist/configurators/pi.d.ts.map +1 -1
- package/dist/configurators/pi.js +2 -2
- package/dist/configurators/pi.js.map +1 -1
- package/dist/configurators/shared.d.ts +12 -0
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +94 -24
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +3 -2
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/manifests/0.5.0-rc.1.json +9 -0
- package/dist/migrations/manifests/0.5.0-rc.2.json +9 -0
- package/dist/templates/extract.d.ts.map +1 -1
- package/dist/templates/extract.js +4 -1
- package/dist/templates/extract.js.map +1 -1
- package/dist/templates/opencode/agents/trellis-research.md +68 -68
- package/dist/templates/opencode/lib/session-utils.js +432 -0
- package/dist/templates/opencode/plugins/session-start.js +60 -531
- package/dist/utils/uninstall-scrubbers.d.ts +65 -0
- package/dist/utils/uninstall-scrubbers.d.ts.map +1 -0
- package/dist/utils/uninstall-scrubbers.js +320 -0
- package/dist/utils/uninstall-scrubbers.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: |
|
|
3
|
-
Code and tech search expert.
|
|
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:
|
|
8
|
-
edit:
|
|
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
|
-
##
|
|
19
|
+
## Core Principle
|
|
20
|
+
|
|
21
|
+
**You do one thing: find, explain, and PERSIST information.**
|
|
20
22
|
|
|
21
|
-
|
|
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
|
-
|
|
25
|
+
---
|
|
24
26
|
|
|
25
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
##
|
|
36
|
+
## Workflow
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
### Step 1: Resolve Current Task
|
|
40
39
|
|
|
41
|
-
|
|
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
|
-
|
|
44
|
+
```bash
|
|
45
|
+
mkdir -p <TASK_DIR>/research
|
|
46
|
+
```
|
|
46
47
|
|
|
47
|
-
###
|
|
48
|
+
### Step 2: Understand Search Request
|
|
48
49
|
|
|
49
|
-
|
|
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
|
-
###
|
|
52
|
+
### Step 3: Execute Search
|
|
56
53
|
|
|
57
|
-
|
|
54
|
+
Run independent searches in parallel (Glob + Grep + web) for efficiency.
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
### Step 4: Persist Each Topic
|
|
60
57
|
|
|
61
|
-
|
|
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
|
-
###
|
|
60
|
+
### Step 5: Report to Main Agent
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
- Describe **where it is**
|
|
67
|
-
- Describe **how it works**
|
|
68
|
-
- Describe **how components interact**
|
|
62
|
+
Reply with ONLY:
|
|
69
63
|
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
81
|
-
|
|
82
|
-
### Step 1: Understand Search Request
|
|
72
|
+
## Scope Limits (Strict)
|
|
83
73
|
|
|
84
|
-
|
|
74
|
+
### Write ALLOWED
|
|
85
75
|
|
|
86
|
-
-
|
|
87
|
-
-
|
|
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
|
-
###
|
|
79
|
+
### Write FORBIDDEN
|
|
91
80
|
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
91
|
+
## File Format
|
|
92
|
+
|
|
93
|
+
Each `{TASK_DIR}/research/<topic>.md` should follow:
|
|
101
94
|
|
|
102
95
|
```markdown
|
|
103
|
-
|
|
96
|
+
# Research: <topic>
|
|
104
97
|
|
|
105
|
-
|
|
98
|
+
- **Query**: <original query>
|
|
99
|
+
- **Scope**: <internal / external / mixed>
|
|
100
|
+
- **Date**: <YYYY-MM-DD>
|
|
106
101
|
|
|
107
|
-
|
|
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
|
|
111
|
+
### Code Patterns
|
|
112
|
+
|
|
113
|
+
<describe patterns, cite file:line>
|
|
114
|
+
|
|
115
|
+
### External References
|
|
117
116
|
|
|
118
|
-
|
|
117
|
+
- [Library X docs](url) — <why relevant, version constraints>
|
|
119
118
|
|
|
120
|
-
### Related
|
|
119
|
+
### Related Specs
|
|
121
120
|
|
|
122
|
-
- `.trellis/spec/xxx.md`
|
|
121
|
+
- `.trellis/spec/xxx.md` — <description>
|
|
123
122
|
|
|
124
|
-
|
|
123
|
+
## Caveats / Not Found
|
|
125
124
|
|
|
126
|
-
|
|
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
|
-
-
|
|
138
|
-
-
|
|
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
|
|
144
|
-
- Don't
|
|
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
|
+
}
|