@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
@@ -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
  }
@@ -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"}