@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
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
46
|
+
if (process.env.OPENCODE_NON_INTERACTIVE === "1") {
|
|
47
|
+
debugLog("session", "Skipping - non-interactive mode")
|
|
48
|
+
return
|
|
49
|
+
}
|
|
528
50
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
51
|
+
if (contextCollector.isProcessed(sessionID)) {
|
|
52
|
+
debugLog("session", "Skipping - session already processed")
|
|
53
|
+
return
|
|
54
|
+
}
|
|
532
55
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
540
|
-
|
|
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
|
-
|
|
65
|
+
const parts = output?.parts || []
|
|
66
|
+
const textPartIndex = parts.findIndex(
|
|
67
|
+
p => p.type === "text" && p.text !== undefined
|
|
68
|
+
)
|
|
553
69
|
|
|
554
|
-
|
|
555
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scrubbers for structured config files during `trellis uninstall`.
|
|
3
|
+
*
|
|
4
|
+
* Each scrubber takes the file content (and any context it needs) and returns
|
|
5
|
+
* `{ content, fullyEmpty }`:
|
|
6
|
+
* - `content` is the post-scrub text to write back if the file should remain.
|
|
7
|
+
* - `fullyEmpty` is true when, after stripping every trellis-managed value,
|
|
8
|
+
* nothing meaningful is left. The caller deletes the file in that case.
|
|
9
|
+
*
|
|
10
|
+
* Manifest path matching (for hooks.json scrubbers) uses substring containment
|
|
11
|
+
* on the resolved `command` string. The leading `python3 ` / `python ` prefix
|
|
12
|
+
* does not matter — we just look for the manifest-relative file path.
|
|
13
|
+
*/
|
|
14
|
+
export interface ScrubResult {
|
|
15
|
+
content: string;
|
|
16
|
+
fullyEmpty: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Scrub a hooks-shaped settings JSON file.
|
|
20
|
+
*
|
|
21
|
+
* `mode = "nested"` → `hooks.{Event}.[ {matcher?, hooks: [ {command,...} ]} ]`
|
|
22
|
+
* `mode = "flat"` → `hooks.{Event}.[ {command,...} ]`
|
|
23
|
+
*
|
|
24
|
+
* Strips every entry whose command references a path in `deletedPaths`,
|
|
25
|
+
* then bottom-up cleans empty containers (matcher block, event array, hooks
|
|
26
|
+
* object). Any user-defined keys outside `hooks` (e.g. `env`, `model`,
|
|
27
|
+
* `permissions`, `version`) are preserved verbatim.
|
|
28
|
+
*/
|
|
29
|
+
export declare function scrubHooksJson(content: string, deletedPaths: readonly string[], mode: "nested" | "flat"): ScrubResult;
|
|
30
|
+
/**
|
|
31
|
+
* Scrub `.opencode/package.json`:
|
|
32
|
+
* - remove `dependencies["@opencode-ai/plugin"]`
|
|
33
|
+
* - if `dependencies` ends up empty → drop the field
|
|
34
|
+
* - fully empty when nothing is left in the object
|
|
35
|
+
*/
|
|
36
|
+
export declare function scrubOpencodePackageJson(content: string): ScrubResult;
|
|
37
|
+
/**
|
|
38
|
+
* Scrub `.pi/settings.json`:
|
|
39
|
+
* - drop `enableSkillCommands` (trellis-flagged)
|
|
40
|
+
* - remove trellis entries from `extensions`/`skills`/`prompts` arrays
|
|
41
|
+
* - drop arrays that become empty
|
|
42
|
+
*/
|
|
43
|
+
export declare function scrubPiSettings(content: string): ScrubResult;
|
|
44
|
+
/**
|
|
45
|
+
* Scrub `.codex/config.toml`.
|
|
46
|
+
*
|
|
47
|
+
* The current trellis-emitted file has two distinct chunks:
|
|
48
|
+
* 1. The line `project_doc_fallback_filenames = ["AGENTS.md"]`
|
|
49
|
+
* 2. A multi-line comment block that begins with the marker
|
|
50
|
+
* `# NOTE: Trellis's SessionStart + UserPromptSubmit hooks require opt-in.`
|
|
51
|
+
* and continues through `# be injected into Codex sessions.`
|
|
52
|
+
*
|
|
53
|
+
* Plus the leading "Project-scoped Codex defaults" header comments.
|
|
54
|
+
*
|
|
55
|
+
* Strategy: line-based removal. We strip:
|
|
56
|
+
* - the `project_doc_fallback_filenames = ...` line
|
|
57
|
+
* - any line that is *only* a comment introduced by trellis (the entire file
|
|
58
|
+
* as shipped is comments + that one assignment)
|
|
59
|
+
* - blank lines that surrounded those removals
|
|
60
|
+
*
|
|
61
|
+
* If the user added their own non-trellis lines, they are preserved as-is.
|
|
62
|
+
* "Fully empty" = post-scrub content has no non-whitespace characters.
|
|
63
|
+
*/
|
|
64
|
+
export declare function scrubCodexConfigToml(content: string): ScrubResult;
|
|
65
|
+
//# sourceMappingURL=uninstall-scrubbers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uninstall-scrubbers.d.ts","sourceRoot":"","sources":["../../src/utils/uninstall-scrubbers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;CACrB;AAmDD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,IAAI,EAAE,QAAQ,GAAG,MAAM,GACtB,WAAW,CAgGb;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CA+BrE;AAgBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAuC5D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAmDjE"}
|