@mindfoldhq/trellis 0.5.0-beta.13 → 0.5.0-beta.15

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 (119) hide show
  1. package/README.md +5 -5
  2. package/dist/cli/index.js +1 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/init.d.ts +1 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +24 -20
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +15 -12
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/configurators/claude.js +1 -1
  12. package/dist/configurators/claude.js.map +1 -1
  13. package/dist/configurators/codebuddy.js +1 -1
  14. package/dist/configurators/codebuddy.js.map +1 -1
  15. package/dist/configurators/codex.d.ts.map +1 -1
  16. package/dist/configurators/codex.js +3 -6
  17. package/dist/configurators/codex.js.map +1 -1
  18. package/dist/configurators/copilot.d.ts.map +1 -1
  19. package/dist/configurators/copilot.js +4 -11
  20. package/dist/configurators/copilot.js.map +1 -1
  21. package/dist/configurators/cursor.js +1 -1
  22. package/dist/configurators/cursor.js.map +1 -1
  23. package/dist/configurators/droid.js +1 -1
  24. package/dist/configurators/droid.js.map +1 -1
  25. package/dist/configurators/gemini.d.ts.map +1 -1
  26. package/dist/configurators/gemini.js +1 -3
  27. package/dist/configurators/gemini.js.map +1 -1
  28. package/dist/configurators/index.d.ts.map +1 -1
  29. package/dist/configurators/index.js +24 -38
  30. package/dist/configurators/index.js.map +1 -1
  31. package/dist/configurators/kiro.js +1 -1
  32. package/dist/configurators/kiro.js.map +1 -1
  33. package/dist/configurators/pi.d.ts +3 -0
  34. package/dist/configurators/pi.d.ts.map +1 -0
  35. package/dist/configurators/pi.js +39 -0
  36. package/dist/configurators/pi.js.map +1 -0
  37. package/dist/configurators/qoder.d.ts.map +1 -1
  38. package/dist/configurators/qoder.js +1 -3
  39. package/dist/configurators/qoder.js.map +1 -1
  40. package/dist/configurators/shared.d.ts +2 -4
  41. package/dist/configurators/shared.d.ts.map +1 -1
  42. package/dist/configurators/shared.js +6 -9
  43. package/dist/configurators/shared.js.map +1 -1
  44. package/dist/migrations/manifests/0.5.0-beta.14.json +9 -0
  45. package/dist/migrations/manifests/0.5.0-beta.15.json +126 -0
  46. package/dist/templates/claude/agents/trellis-research.md +1 -1
  47. package/dist/templates/claude/settings.json +0 -4
  48. package/dist/templates/codebuddy/agents/trellis-research.md +1 -1
  49. package/dist/templates/codex/agents/trellis-check.toml +0 -16
  50. package/dist/templates/codex/agents/trellis-implement.toml +0 -16
  51. package/dist/templates/codex/agents/trellis-research.toml +3 -2
  52. package/dist/templates/codex/hooks/session-start.py +82 -22
  53. package/dist/templates/codex/skills/start/SKILL.md +1 -1
  54. package/dist/templates/copilot/hooks/session-start.py +84 -26
  55. package/dist/templates/copilot/prompts/start.prompt.md +1 -1
  56. package/dist/templates/cursor/agents/trellis-check.md +1 -1
  57. package/dist/templates/cursor/agents/trellis-implement.md +1 -1
  58. package/dist/templates/cursor/agents/trellis-research.md +2 -2
  59. package/dist/templates/cursor/hooks.json +7 -1
  60. package/dist/templates/droid/droids/trellis-research.md +1 -1
  61. package/dist/templates/extract.d.ts +6 -0
  62. package/dist/templates/extract.d.ts.map +1 -1
  63. package/dist/templates/extract.js +14 -0
  64. package/dist/templates/extract.js.map +1 -1
  65. package/dist/templates/gemini/agents/trellis-research.md +1 -1
  66. package/dist/templates/kiro/agents/trellis-research.json +1 -1
  67. package/dist/templates/markdown/agents.md +11 -12
  68. package/dist/templates/markdown/gitignore.txt +3 -0
  69. package/dist/templates/opencode/agents/trellis-check.md +1 -1
  70. package/dist/templates/opencode/agents/trellis-implement.md +1 -1
  71. package/dist/templates/opencode/agents/trellis-research.md +2 -2
  72. package/dist/templates/opencode/lib/trellis-context.js +100 -13
  73. package/dist/templates/opencode/plugins/inject-subagent-context.js +54 -4
  74. package/dist/templates/opencode/plugins/inject-workflow-state.js +50 -23
  75. package/dist/templates/opencode/plugins/session-start.js +46 -21
  76. package/dist/templates/pi/agents/trellis-check.md +28 -0
  77. package/dist/templates/pi/agents/trellis-implement.md +33 -0
  78. package/dist/templates/pi/agents/trellis-research.md +25 -0
  79. package/dist/templates/pi/extensions/trellis/index.ts.txt +549 -0
  80. package/dist/templates/pi/index.d.ts +5 -0
  81. package/dist/templates/pi/index.d.ts.map +1 -0
  82. package/dist/templates/pi/index.js +12 -0
  83. package/dist/templates/pi/index.js.map +1 -0
  84. package/dist/templates/pi/settings.json +12 -0
  85. package/dist/templates/qoder/agents/trellis-research.md +1 -1
  86. package/dist/templates/shared-hooks/index.d.ts +31 -0
  87. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  88. package/dist/templates/shared-hooks/index.js +59 -0
  89. package/dist/templates/shared-hooks/index.js.map +1 -1
  90. package/dist/templates/shared-hooks/inject-shell-session-context.py +180 -0
  91. package/dist/templates/shared-hooks/inject-subagent-context.py +128 -26
  92. package/dist/templates/shared-hooks/inject-workflow-state.py +101 -61
  93. package/dist/templates/shared-hooks/session-start.py +151 -28
  94. package/dist/templates/trellis/gitignore.txt +3 -0
  95. package/dist/templates/trellis/index.d.ts +1 -0
  96. package/dist/templates/trellis/index.d.ts.map +1 -1
  97. package/dist/templates/trellis/index.js +2 -0
  98. package/dist/templates/trellis/index.js.map +1 -1
  99. package/dist/templates/trellis/scripts/common/__init__.py +8 -0
  100. package/dist/templates/trellis/scripts/common/active_task.py +593 -0
  101. package/dist/templates/trellis/scripts/common/cli_adapter.py +43 -8
  102. package/dist/templates/trellis/scripts/common/paths.py +61 -58
  103. package/dist/templates/trellis/scripts/common/session_context.py +12 -0
  104. package/dist/templates/trellis/scripts/common/task_store.py +4 -6
  105. package/dist/templates/trellis/scripts/task.py +56 -14
  106. package/dist/templates/trellis/workflow.md +31 -26
  107. package/dist/types/ai-tools.d.ts +3 -3
  108. package/dist/types/ai-tools.d.ts.map +1 -1
  109. package/dist/types/ai-tools.js +16 -0
  110. package/dist/types/ai-tools.js.map +1 -1
  111. package/dist/utils/template-fetcher.d.ts +22 -6
  112. package/dist/utils/template-fetcher.d.ts.map +1 -1
  113. package/dist/utils/template-fetcher.js +405 -27
  114. package/dist/utils/template-fetcher.js.map +1 -1
  115. package/dist/utils/template-hash.d.ts.map +1 -1
  116. package/dist/utils/template-hash.js +3 -2
  117. package/dist/utils/template-hash.js.map +1 -1
  118. package/package.json +1 -1
  119. package/dist/templates/shared-hooks/statusline.py +0 -218
@@ -0,0 +1,549 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+
6
+ type JsonObject = Record<string, unknown>;
7
+ type TextContent = { type: "text"; text: string };
8
+
9
+ interface PiToolResult {
10
+ content: TextContent[];
11
+ details?: JsonObject;
12
+ }
13
+
14
+ interface PiExtensionContext {
15
+ hasUI?: boolean;
16
+ sessionManager?: {
17
+ getSessionId?: () => string;
18
+ getSessionFile?: () => string | undefined;
19
+ };
20
+ ui?: {
21
+ notify?: (message: string, type?: "info" | "warning" | "error") => void;
22
+ };
23
+ }
24
+
25
+ interface PiBeforeAgentStartEvent {
26
+ systemPrompt?: string;
27
+ }
28
+
29
+ interface PiContextEvent {
30
+ messages?: unknown[];
31
+ }
32
+
33
+ interface PiToolCallEvent {
34
+ toolName?: string;
35
+ input?: JsonObject;
36
+ }
37
+
38
+ interface SubagentInput {
39
+ agent?: string;
40
+ prompt?: string;
41
+ mode?: "single" | "parallel" | "chain";
42
+ prompts?: string[];
43
+ }
44
+
45
+ const TRELLIS_AGENT_JSONL: Record<string, string> = {
46
+ "trellis-implement": "implement.jsonl",
47
+ implement: "implement.jsonl",
48
+ "trellis-check": "check.jsonl",
49
+ check: "check.jsonl",
50
+ };
51
+
52
+ function findProjectRoot(startDir: string): string {
53
+ let current = resolve(startDir);
54
+ while (true) {
55
+ if (
56
+ existsSync(join(current, ".trellis")) ||
57
+ existsSync(join(current, ".pi"))
58
+ ) {
59
+ return current;
60
+ }
61
+ const parent = dirname(current);
62
+ if (parent === current) return resolve(startDir);
63
+ current = parent;
64
+ }
65
+ }
66
+
67
+ function readText(path: string): string {
68
+ try {
69
+ return readFileSync(path, "utf-8");
70
+ } catch {
71
+ return "";
72
+ }
73
+ }
74
+
75
+ function stripMarkdownFrontmatter(content: string): string {
76
+ const normalized = content.replace(/^\uFEFF/, "");
77
+ const match = normalized.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
78
+ return (match ? normalized.slice(match[0].length) : normalized).trimStart();
79
+ }
80
+
81
+ function toPiPromptArgument(prompt: string): string {
82
+ return prompt.startsWith("-") ? `\n${prompt}` : prompt;
83
+ }
84
+
85
+ function isJsonObject(value: unknown): value is JsonObject {
86
+ return typeof value === "object" && value !== null && !Array.isArray(value);
87
+ }
88
+
89
+ function stringValue(value: unknown): string | null {
90
+ return typeof value === "string" && value.trim() ? value.trim() : null;
91
+ }
92
+
93
+ function sanitizeKey(raw: string): string {
94
+ return raw
95
+ .trim()
96
+ .replace(/[^A-Za-z0-9._-]+/g, "_")
97
+ .replace(/^[._-]+|[._-]+$/g, "")
98
+ .slice(0, 160);
99
+ }
100
+
101
+ function hashValue(raw: string): string {
102
+ return createHash("sha256").update(raw).digest("hex").slice(0, 24);
103
+ }
104
+
105
+ function createProcessContextKey(projectRoot: string): string {
106
+ return `pi_process_${hashValue(
107
+ [projectRoot, process.pid, Date.now(), randomBytes(8).toString("hex")].join(":"),
108
+ )}`;
109
+ }
110
+
111
+ function callString(callback: (() => string | undefined) | undefined): string | null {
112
+ if (!callback) return null;
113
+ try {
114
+ return stringValue(callback());
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ function lookupString(data: unknown, keys: string[]): string | null {
121
+ if (!isJsonObject(data)) return null;
122
+ for (const key of keys) {
123
+ const value = stringValue(data[key]);
124
+ if (value) return value;
125
+ }
126
+ for (const nestedKey of ["input", "properties", "event", "hook_input", "hookInput"]) {
127
+ const nested = data[nestedKey];
128
+ const value = lookupString(nested, keys);
129
+ if (value) return value;
130
+ }
131
+ return null;
132
+ }
133
+
134
+ function extractTextContent(content: unknown): string {
135
+ if (typeof content === "string") return content;
136
+ if (!Array.isArray(content)) return "";
137
+
138
+ return content
139
+ .map((block) => {
140
+ if (!isJsonObject(block)) return "";
141
+ return block.type === "text" && typeof block.text === "string"
142
+ ? block.text
143
+ : "";
144
+ })
145
+ .join("");
146
+ }
147
+
148
+ function extractFinalAssistantText(output: string): string | null {
149
+ let finalText = "";
150
+
151
+ for (const line of output.split(/\r?\n/)) {
152
+ const trimmed = line.trim();
153
+ if (!trimmed) continue;
154
+
155
+ try {
156
+ const event = JSON.parse(trimmed) as JsonObject;
157
+ const message = isJsonObject(event.message) ? event.message : null;
158
+ if (message?.role !== "assistant") continue;
159
+
160
+ const text = extractTextContent(message.content);
161
+ if (text) finalText = text;
162
+ } catch {
163
+ // Pi can print non-JSON diagnostics around JSON mode; keep scanning.
164
+ }
165
+ }
166
+
167
+ return finalText || null;
168
+ }
169
+
170
+ function formatPiOutput(stdout: string, stderr: string): string {
171
+ return extractFinalAssistantText(stdout) ?? (stdout || stderr);
172
+ }
173
+
174
+ function normalizeTaskRef(raw: string): string | null {
175
+ let normalized = raw.trim().replace(/\\/g, "/");
176
+ if (!normalized) return null;
177
+ while (normalized.startsWith("./")) normalized = normalized.slice(2);
178
+ if (normalized.startsWith("tasks/")) normalized = `.trellis/${normalized}`;
179
+ return normalized;
180
+ }
181
+
182
+ function taskRefToDir(projectRoot: string, taskRef: string): string {
183
+ if (taskRef.startsWith("/")) return taskRef;
184
+ if (taskRef.startsWith(".trellis/")) return join(projectRoot, taskRef);
185
+ return join(projectRoot, ".trellis", "tasks", taskRef);
186
+ }
187
+
188
+ function resolveContextKey(
189
+ input: unknown,
190
+ ctx?: PiExtensionContext,
191
+ fallback?: string | null,
192
+ ): string | null {
193
+ const override = stringValue(process.env.TRELLIS_CONTEXT_ID);
194
+ if (override) return sanitizeKey(override) || hashValue(override);
195
+
196
+ const sessionId =
197
+ callString(ctx?.sessionManager?.getSessionId) ??
198
+ stringValue(process.env.PI_SESSION_ID) ??
199
+ stringValue(process.env.PI_SESSIONID) ??
200
+ lookupString(input, ["session_id", "sessionId", "sessionID"]);
201
+ if (sessionId) return `pi_${sanitizeKey(sessionId) || hashValue(sessionId)}`;
202
+
203
+ const transcriptPath =
204
+ callString(ctx?.sessionManager?.getSessionFile) ??
205
+ lookupString(input, [
206
+ "transcript_path",
207
+ "transcriptPath",
208
+ "transcript",
209
+ ]);
210
+ if (transcriptPath) return `pi_transcript_${hashValue(transcriptPath)}`;
211
+
212
+ return fallback ?? null;
213
+ }
214
+
215
+ function readCurrentTask(
216
+ projectRoot: string,
217
+ platformInput?: unknown,
218
+ ctx?: PiExtensionContext,
219
+ contextKeyOverride?: string | null,
220
+ ): string | null {
221
+ const contextKey = resolveContextKey(platformInput, ctx, contextKeyOverride);
222
+ if (contextKey) {
223
+ try {
224
+ const rawContext = readText(
225
+ join(projectRoot, ".trellis", ".runtime", "sessions", `${contextKey}.json`),
226
+ );
227
+ const context = JSON.parse(rawContext) as JsonObject;
228
+ const taskRef = normalizeTaskRef(stringValue(context.current_task) ?? "");
229
+ if (taskRef) return taskRefToDir(projectRoot, taskRef);
230
+ } catch {
231
+ // Missing or malformed session context means no active task.
232
+ }
233
+ }
234
+
235
+ return null;
236
+ }
237
+
238
+ function readJsonlFiles(
239
+ projectRoot: string,
240
+ taskDir: string,
241
+ jsonlName: string,
242
+ ): string {
243
+ const jsonlPath = join(taskDir, jsonlName);
244
+ const lines = readText(jsonlPath).split(/\r?\n/);
245
+ const chunks: string[] = [];
246
+
247
+ for (const line of lines) {
248
+ const trimmed = line.trim();
249
+ if (!trimmed) continue;
250
+ try {
251
+ const row = JSON.parse(trimmed) as JsonObject;
252
+ const file = typeof row.file === "string" ? row.file : "";
253
+ if (!file) continue;
254
+ const content = readText(join(projectRoot, file));
255
+ if (content) {
256
+ chunks.push(`## ${file}\n\n${content}`);
257
+ }
258
+ } catch {
259
+ // Seed rows and malformed lines must not block sub-agent startup.
260
+ }
261
+ }
262
+
263
+ return chunks.join("\n\n---\n\n");
264
+ }
265
+
266
+ function buildTrellisContext(
267
+ projectRoot: string,
268
+ agent: string,
269
+ platformInput?: unknown,
270
+ ctx?: PiExtensionContext,
271
+ contextKey?: string | null,
272
+ ): string {
273
+ const taskDir = readCurrentTask(projectRoot, platformInput, ctx, contextKey);
274
+ if (!taskDir) {
275
+ return "No active Trellis task found. Read .trellis/ before proceeding.";
276
+ }
277
+
278
+ const prd = readText(join(taskDir, "prd.md"));
279
+ const info = readText(join(taskDir, "info.md"));
280
+ const jsonlName = TRELLIS_AGENT_JSONL[agent] ?? "";
281
+ const specContext = jsonlName
282
+ ? readJsonlFiles(projectRoot, taskDir, jsonlName)
283
+ : "";
284
+
285
+ return [
286
+ "## Trellis Task Context",
287
+ `Task directory: ${taskDir}`,
288
+ "",
289
+ "### prd.md",
290
+ prd || "(missing)",
291
+ info ? "\n### info.md\n" + info : "",
292
+ specContext ? "\n### Curated Spec / Research Context\n" + specContext : "",
293
+ ].join("\n");
294
+ }
295
+
296
+ function readAgentDefinition(projectRoot: string, agent: string): string {
297
+ const normalized = agent.startsWith("trellis-") ? agent : `trellis-${agent}`;
298
+ return stripMarkdownFrontmatter(
299
+ readText(join(projectRoot, ".pi", "agents", `${normalized}.md`)),
300
+ );
301
+ }
302
+
303
+ function commandStartsWithTrellisContext(command: string): boolean {
304
+ const trimmed = command.trimStart();
305
+ return (
306
+ /^export\s+TRELLIS_CONTEXT_ID=/.test(trimmed) ||
307
+ /^TRELLIS_CONTEXT_ID=/.test(trimmed) ||
308
+ /^env\s+.*\bTRELLIS_CONTEXT_ID=/.test(trimmed)
309
+ );
310
+ }
311
+
312
+ function shellQuote(value: string): string {
313
+ return `'${value.replace(/'/g, `'\\''`)}'`;
314
+ }
315
+
316
+ function injectTrellisContextIntoBash(
317
+ event: unknown,
318
+ contextKey: string,
319
+ ): boolean {
320
+ const toolCall = event as PiToolCallEvent;
321
+ if (toolCall.toolName !== "bash" || !isJsonObject(toolCall.input)) {
322
+ return false;
323
+ }
324
+
325
+ const rawCommand = toolCall.input.command;
326
+ if (typeof rawCommand !== "string" || !rawCommand.trim()) {
327
+ return false;
328
+ }
329
+ if (commandStartsWithTrellisContext(rawCommand)) {
330
+ return false;
331
+ }
332
+
333
+ toolCall.input.command = `export TRELLIS_CONTEXT_ID=${shellQuote(contextKey)}; ${rawCommand}`;
334
+ return true;
335
+ }
336
+
337
+ function runPi(
338
+ projectRoot: string,
339
+ prompt: string,
340
+ contextKey?: string | null,
341
+ ): Promise<string> {
342
+ return new Promise((resolvePromise, reject) => {
343
+ const child = spawn(
344
+ "pi",
345
+ ["--mode", "json", "-p", "--no-session", toPiPromptArgument(prompt)],
346
+ {
347
+ cwd: projectRoot,
348
+ env: contextKey
349
+ ? { ...process.env, TRELLIS_CONTEXT_ID: contextKey }
350
+ : process.env,
351
+ stdio: ["ignore", "pipe", "pipe"],
352
+ },
353
+ );
354
+
355
+ const stdout: Buffer[] = [];
356
+ const stderr: Buffer[] = [];
357
+ child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk));
358
+ child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
359
+ child.on("error", reject);
360
+ child.on("close", (code) => {
361
+ const out = Buffer.concat(stdout).toString("utf-8");
362
+ const err = Buffer.concat(stderr).toString("utf-8");
363
+ if (code === 0) {
364
+ resolvePromise(formatPiOutput(out, err));
365
+ } else {
366
+ reject(new Error(err || `pi exited with code ${code ?? "unknown"}`));
367
+ }
368
+ });
369
+ });
370
+ }
371
+
372
+ function buildSubagentPrompt(
373
+ projectRoot: string,
374
+ input: SubagentInput,
375
+ contextKey?: string | null,
376
+ ): string {
377
+ const agent = input.agent ?? "trellis-implement";
378
+ const normalized = agent.startsWith("trellis-") ? agent : `trellis-${agent}`;
379
+ const definition = readAgentDefinition(projectRoot, normalized);
380
+ const context = buildTrellisContext(
381
+ projectRoot,
382
+ normalized,
383
+ input,
384
+ undefined,
385
+ contextKey,
386
+ );
387
+ const prompt = input.prompt ?? "";
388
+
389
+ return [
390
+ "## Trellis Agent Definition",
391
+ definition || "(missing agent definition)",
392
+ "",
393
+ context,
394
+ "",
395
+ "## Delegated Task",
396
+ prompt,
397
+ ].join("\n");
398
+ }
399
+
400
+ async function runSubagent(
401
+ projectRoot: string,
402
+ input: SubagentInput,
403
+ contextKey?: string | null,
404
+ ): Promise<string> {
405
+ const mode = input.mode ?? "single";
406
+ if (mode === "parallel") {
407
+ const prompts = input.prompts ?? (input.prompt ? [input.prompt] : []);
408
+ const outputs = await Promise.all(
409
+ prompts.map((prompt) =>
410
+ runPi(
411
+ projectRoot,
412
+ buildSubagentPrompt(projectRoot, { ...input, prompt }, contextKey),
413
+ contextKey,
414
+ ),
415
+ ),
416
+ );
417
+ return outputs.join("\n\n---\n\n");
418
+ }
419
+
420
+ if (mode === "chain") {
421
+ let previous = "";
422
+ const prompts = input.prompts ?? (input.prompt ? [input.prompt] : []);
423
+ for (const prompt of prompts) {
424
+ previous = await runPi(
425
+ projectRoot,
426
+ buildSubagentPrompt(projectRoot, {
427
+ ...input,
428
+ prompt: previous
429
+ ? `${prompt}\n\nPrevious output:\n${previous}`
430
+ : prompt,
431
+ }, contextKey),
432
+ contextKey,
433
+ );
434
+ }
435
+ return previous;
436
+ }
437
+
438
+ return runPi(
439
+ projectRoot,
440
+ buildSubagentPrompt(projectRoot, input, contextKey),
441
+ contextKey,
442
+ );
443
+ }
444
+
445
+ export default function trellisExtension(pi: {
446
+ registerTool?: (tool: JsonObject) => void;
447
+ on?: (
448
+ event: string,
449
+ handler: (event: unknown, ctx?: PiExtensionContext) => unknown,
450
+ ) => void;
451
+ cwd?: string;
452
+ }): void {
453
+ const projectRoot = findProjectRoot(pi.cwd ?? process.cwd());
454
+ const processContextKey = createProcessContextKey(projectRoot);
455
+ let currentContextKey: string | null = null;
456
+
457
+ const getContextKey = (input?: unknown, ctx?: PiExtensionContext): string => {
458
+ const contextKey = resolveContextKey(
459
+ input,
460
+ ctx,
461
+ currentContextKey ?? processContextKey,
462
+ );
463
+ currentContextKey = contextKey ?? processContextKey;
464
+ return currentContextKey;
465
+ };
466
+
467
+ pi.registerTool?.({
468
+ name: "subagent",
469
+ label: "Subagent",
470
+ description: "Run a Trellis project sub-agent with active task context.",
471
+ parameters: {
472
+ type: "object",
473
+ properties: {
474
+ agent: {
475
+ type: "string",
476
+ description: "Agent name, such as trellis-implement or trellis-check.",
477
+ },
478
+ prompt: {
479
+ type: "string",
480
+ description: "Task prompt for the sub-agent.",
481
+ },
482
+ mode: {
483
+ type: "string",
484
+ enum: ["single", "parallel", "chain"],
485
+ description: "Delegation mode.",
486
+ },
487
+ prompts: {
488
+ type: "array",
489
+ items: { type: "string" },
490
+ description: "Prompts for parallel or chain mode.",
491
+ },
492
+ },
493
+ required: ["prompt"],
494
+ },
495
+ execute: async (
496
+ _toolCallId: string,
497
+ input: SubagentInput,
498
+ _signal?: AbortSignal,
499
+ _onUpdate?: (partialResult: PiToolResult) => void,
500
+ ctx?: PiExtensionContext,
501
+ ): Promise<PiToolResult> => {
502
+ const contextKey = getContextKey(input, ctx);
503
+ const output = await runSubagent(projectRoot, input, contextKey);
504
+ return {
505
+ content: [{ type: "text", text: output }],
506
+ details: {
507
+ agent: input.agent ?? "trellis-implement",
508
+ mode: input.mode ?? "single",
509
+ },
510
+ };
511
+ },
512
+ });
513
+
514
+ pi.on?.("session_start", (event, ctx) => {
515
+ getContextKey(event, ctx);
516
+ ctx?.ui?.notify?.(
517
+ "Trellis project context is available. Use /trellis-continue to resume the current task.",
518
+ "info",
519
+ );
520
+ });
521
+ pi.on?.("before_agent_start", (event, ctx) => {
522
+ const contextKey = getContextKey(event, ctx);
523
+ const current = (event as PiBeforeAgentStartEvent).systemPrompt ?? "";
524
+ const context = buildTrellisContext(
525
+ projectRoot,
526
+ "trellis-implement",
527
+ event,
528
+ ctx,
529
+ contextKey,
530
+ );
531
+ return {
532
+ systemPrompt: [current, context].filter(Boolean).join("\n\n"),
533
+ };
534
+ });
535
+ pi.on?.("context", (event, ctx) => {
536
+ getContextKey(event, ctx);
537
+ const messages = (event as PiContextEvent).messages;
538
+ return Array.isArray(messages) ? { messages } : undefined;
539
+ });
540
+ pi.on?.("input", (event, ctx) => {
541
+ getContextKey(event, ctx);
542
+ return { action: "continue" };
543
+ });
544
+ pi.on?.("tool_call", (event, ctx) => {
545
+ const contextKey = getContextKey(event, ctx);
546
+ injectTrellisContextIntoBash(event, contextKey);
547
+ return undefined;
548
+ });
549
+ }
@@ -0,0 +1,5 @@
1
+ import { type AgentTemplate, type HookTemplate } from "../template-utils.js";
2
+ export declare function getAllAgents(): AgentTemplate[];
3
+ export declare function getSettingsTemplate(): HookTemplate;
4
+ export declare function getExtensionTemplate(): string;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/pi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,sBAAsB,CAAC;AAM9B,wBAAgB,YAAY,IAAI,aAAa,EAAE,CAE9C;AAED,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
@@ -0,0 +1,12 @@
1
+ import { createTemplateReader, } from "../template-utils.js";
2
+ const { listMdAgents, getSettings, readTemplate } = createTemplateReader(import.meta.url);
3
+ export function getAllAgents() {
4
+ return listMdAgents();
5
+ }
6
+ export function getSettingsTemplate() {
7
+ return getSettings();
8
+ }
9
+ export function getExtensionTemplate() {
10
+ return readTemplate("extensions/trellis/index.ts.txt");
11
+ }
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/templates/pi/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,GAGrB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,oBAAoB,CACtE,MAAM,CAAC,IAAI,CAAC,GAAG,CAChB,CAAC;AAEF,MAAM,UAAU,YAAY;IAC1B,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,WAAW,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,YAAY,CAAC,iCAAiC,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,12 @@
1
+ {
2
+ "enableSkillCommands": true,
3
+ "extensions": [
4
+ "./extensions/trellis/index.ts"
5
+ ],
6
+ "skills": [
7
+ "./skills"
8
+ ],
9
+ "prompts": [
10
+ "./prompts"
11
+ ]
12
+ }
@@ -29,7 +29,7 @@ Conversations get compacted; files don't. Every research output MUST end up as a
29
29
 
30
30
  ### Step 1: Resolve Current Task
31
31
 
32
- Read `.trellis/.current-task` → task directory (e.g. `.trellis/tasks/04-17-foo/`). If empty or missing, ask the user where to write output; do NOT guess.
32
+ 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.
33
33
 
34
34
  Ensure `{TASK_DIR}/research/` exists:
35
35
 
@@ -11,9 +11,40 @@ export interface HookScript {
11
11
  /** Script content — no placeholders, ready to write directly */
12
12
  content: string;
13
13
  }
14
+ export type SharedHookName = "session-start.py" | "inject-shell-session-context.py" | "inject-workflow-state.py" | "inject-subagent-context.py";
15
+ export type SharedHookPlatform = "claude" | "cursor" | "codex" | "gemini" | "qoder" | "copilot" | "codebuddy" | "droid" | "kiro";
16
+ /**
17
+ * Which shared hooks each platform actually invokes. Single source of truth
18
+ * for shared-hook distribution — both `writeSharedHooks` (runtime install)
19
+ * and `collectSharedHooks` (`trellis update` diff) read from this table.
20
+ *
21
+ * Routing rules encoded here:
22
+ * - `session-start.py` — shipped by every platform with a SessionStart
23
+ * hook event *except* codex + copilot, which bundle a platform-specific
24
+ * session-start.py under their own template dirs.
25
+ * - `inject-workflow-state.py` — every platform with a UserPromptSubmit
26
+ * (or equivalent) event. Kiro + codex self-included; platforms without
27
+ * per-turn main-session hooks are excluded.
28
+ * - `inject-subagent-context.py` — class-1 (push-based) platforms only.
29
+ * Class-2 (pull-based) platforms (codex, copilot, gemini, qoder) can't
30
+ * have hooks mutate sub-agent prompts — their sub-agents load context
31
+ * via a prelude instead.
32
+ * - Kiro supports only `agentSpawn` (no SessionStart / UserPromptSubmit
33
+ * event), so it takes just `inject-subagent-context.py`.
34
+ * - Claude Code `statusLine` is intentionally not installed by default.
35
+ * Users can add their own statusLine command in `.claude/settings.json`
36
+ * without Trellis owning a generated hook file.
37
+ */
38
+ export declare const SHARED_HOOKS_BY_PLATFORM: Record<SharedHookPlatform, readonly SharedHookName[]>;
14
39
  /**
15
40
  * Get all shared hook scripts. Content is platform-independent and can be
16
41
  * written directly without placeholder resolution.
17
42
  */
18
43
  export declare function getSharedHookScripts(): HookScript[];
44
+ /**
45
+ * Get the shared hook scripts that a given platform actually registers.
46
+ * Drives both `writeSharedHooks` and `collectSharedHooks` so distribution
47
+ * never drifts from the per-platform capability declared above.
48
+ */
49
+ export declare function getSharedHookScriptsForPlatform(platform: SharedHookPlatform): HookScript[];
19
50
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/shared-hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,iCAAiC,GACjC,0BAA0B,GAC1B,4BAA4B,CAAC;AAEjC,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,OAAO,GACP,SAAS,GACT,WAAW,GACX,OAAO,GACP,MAAM,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAC3C,kBAAkB,EAClB,SAAS,cAAc,EAAE,CA4B1B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,EAAE,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,kBAAkB,GAC3B,UAAU,EAAE,CAGd"}