@mindfoldhq/trellis 0.5.0-beta.16 → 0.5.0-beta.18

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 (104) hide show
  1. package/README.md +60 -98
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +1 -29
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/update.d.ts.map +1 -1
  6. package/dist/commands/update.js +92 -5
  7. package/dist/commands/update.js.map +1 -1
  8. package/dist/configurators/antigravity.d.ts.map +1 -1
  9. package/dist/configurators/antigravity.js +2 -8
  10. package/dist/configurators/antigravity.js.map +1 -1
  11. package/dist/configurators/claude.d.ts.map +1 -1
  12. package/dist/configurators/claude.js +3 -9
  13. package/dist/configurators/claude.js.map +1 -1
  14. package/dist/configurators/codebuddy.d.ts.map +1 -1
  15. package/dist/configurators/codebuddy.js +2 -2
  16. package/dist/configurators/codebuddy.js.map +1 -1
  17. package/dist/configurators/codex.d.ts.map +1 -1
  18. package/dist/configurators/codex.js +2 -7
  19. package/dist/configurators/codex.js.map +1 -1
  20. package/dist/configurators/copilot.d.ts.map +1 -1
  21. package/dist/configurators/copilot.js +2 -9
  22. package/dist/configurators/copilot.js.map +1 -1
  23. package/dist/configurators/cursor.d.ts.map +1 -1
  24. package/dist/configurators/cursor.js +2 -2
  25. package/dist/configurators/cursor.js.map +1 -1
  26. package/dist/configurators/droid.d.ts.map +1 -1
  27. package/dist/configurators/droid.js +2 -2
  28. package/dist/configurators/droid.js.map +1 -1
  29. package/dist/configurators/gemini.d.ts.map +1 -1
  30. package/dist/configurators/gemini.js +2 -2
  31. package/dist/configurators/gemini.js.map +1 -1
  32. package/dist/configurators/index.d.ts.map +1 -1
  33. package/dist/configurators/index.js +13 -11
  34. package/dist/configurators/index.js.map +1 -1
  35. package/dist/configurators/kilo.d.ts.map +1 -1
  36. package/dist/configurators/kilo.js +2 -8
  37. package/dist/configurators/kilo.js.map +1 -1
  38. package/dist/configurators/kiro.d.ts.map +1 -1
  39. package/dist/configurators/kiro.js +2 -2
  40. package/dist/configurators/kiro.js.map +1 -1
  41. package/dist/configurators/opencode.d.ts.map +1 -1
  42. package/dist/configurators/opencode.js +3 -3
  43. package/dist/configurators/opencode.js.map +1 -1
  44. package/dist/configurators/pi.d.ts.map +1 -1
  45. package/dist/configurators/pi.js +9 -4
  46. package/dist/configurators/pi.js.map +1 -1
  47. package/dist/configurators/qoder.d.ts.map +1 -1
  48. package/dist/configurators/qoder.js +2 -2
  49. package/dist/configurators/qoder.js.map +1 -1
  50. package/dist/configurators/shared.d.ts +21 -2
  51. package/dist/configurators/shared.d.ts.map +1 -1
  52. package/dist/configurators/shared.js +32 -3
  53. package/dist/configurators/shared.js.map +1 -1
  54. package/dist/configurators/windsurf.d.ts.map +1 -1
  55. package/dist/configurators/windsurf.js +2 -8
  56. package/dist/configurators/windsurf.js.map +1 -1
  57. package/dist/constants/paths.d.ts +2 -0
  58. package/dist/constants/paths.d.ts.map +1 -1
  59. package/dist/constants/paths.js +2 -0
  60. package/dist/constants/paths.js.map +1 -1
  61. package/dist/migrations/manifests/0.5.0-beta.17.json +9 -0
  62. package/dist/migrations/manifests/0.5.0-beta.18.json +9 -0
  63. package/dist/templates/codex/skills/finish-work/SKILL.md +41 -109
  64. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +73 -0
  65. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +83 -0
  66. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +54 -0
  67. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +81 -0
  68. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-hooks.md +57 -0
  69. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +78 -0
  70. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +83 -0
  71. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +79 -0
  72. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +48 -0
  73. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +55 -0
  74. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +68 -0
  75. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +80 -0
  76. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/overview.md +51 -0
  77. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +102 -0
  78. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +101 -0
  79. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +75 -0
  80. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workspace-memory.md +71 -0
  81. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +79 -0
  82. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +69 -0
  83. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +59 -0
  84. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +74 -0
  85. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +83 -0
  86. package/dist/templates/common/commands/finish-work.md +34 -10
  87. package/dist/templates/common/index.d.ts +22 -2
  88. package/dist/templates/common/index.d.ts.map +1 -1
  89. package/dist/templates/common/index.js +53 -4
  90. package/dist/templates/common/index.js.map +1 -1
  91. package/dist/templates/common/skills/brainstorm.md +3 -0
  92. package/dist/templates/copilot/prompts/finish-work.prompt.md +44 -112
  93. package/dist/templates/markdown/agents.md +8 -0
  94. package/dist/templates/opencode/plugins/inject-subagent-context.js +20 -5
  95. package/dist/templates/opencode/plugins/inject-workflow-state.js +6 -1
  96. package/dist/templates/pi/extensions/trellis/index.ts.txt +499 -51
  97. package/dist/templates/shared-hooks/inject-workflow-state.py +6 -1
  98. package/dist/templates/trellis/scripts/common/task_store.py +4 -16
  99. package/dist/templates/trellis/scripts/common/tasks.py +4 -1
  100. package/dist/templates/trellis/workflow.md +59 -3
  101. package/dist/utils/template-hash.d.ts.map +1 -1
  102. package/dist/utils/template-hash.js +19 -1
  103. package/dist/utils/template-hash.js.map +1 -1
  104. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
- import { dirname, join, resolve } from "node:path";
3
+ import { delimiter, dirname, join, resolve } from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
 
6
6
  type JsonObject = Record<string, unknown>;
@@ -40,6 +40,27 @@ interface SubagentInput {
40
40
  prompt?: string;
41
41
  mode?: "single" | "parallel" | "chain";
42
42
  prompts?: string[];
43
+ model?: string;
44
+ thinking?: ThinkingLevel;
45
+ }
46
+
47
+ type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
48
+
49
+ interface AgentConfig {
50
+ model?: string;
51
+ thinking?: ThinkingLevel;
52
+ // Parsed for pi-subagents-compatible agent files; Pi CLI has no documented fallback-model flag to pass through here.
53
+ fallbackModels: string[];
54
+ }
55
+
56
+ interface AgentDefinition {
57
+ content: string;
58
+ config: AgentConfig;
59
+ }
60
+
61
+ interface PiRunConfig {
62
+ model?: string;
63
+ thinking?: ThinkingLevel;
43
64
  }
44
65
 
45
66
  const TRELLIS_AGENT_JSONL: Record<string, string> = {
@@ -72,14 +93,19 @@ function readText(path: string): string {
72
93
  }
73
94
  }
74
95
 
75
- function stripMarkdownFrontmatter(content: string): string {
96
+ function splitMarkdownFrontmatter(content: string): {
97
+ frontmatter: string;
98
+ body: string;
99
+ } {
76
100
  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();
101
+ const match = normalized.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
102
+ return match
103
+ ? { frontmatter: match[1] ?? "", body: normalized.slice(match[0].length) }
104
+ : { frontmatter: "", body: normalized };
79
105
  }
80
106
 
81
- function toPiPromptArgument(prompt: string): string {
82
- return prompt.startsWith("-") ? `\n${prompt}` : prompt;
107
+ function stripMarkdownFrontmatter(content: string): string {
108
+ return splitMarkdownFrontmatter(content).body.trimStart();
83
109
  }
84
110
 
85
111
  function isJsonObject(value: unknown): value is JsonObject {
@@ -90,6 +116,136 @@ function stringValue(value: unknown): string | null {
90
116
  return typeof value === "string" && value.trim() ? value.trim() : null;
91
117
  }
92
118
 
119
+ const THINKING_LEVELS = [
120
+ "off",
121
+ "minimal",
122
+ "low",
123
+ "medium",
124
+ "high",
125
+ "xhigh",
126
+ ] as const satisfies readonly ThinkingLevel[];
127
+ const THINKING_SUFFIX_RE = /:(?:off|minimal|low|medium|high|xhigh)$/i;
128
+
129
+ function normalizeThinking(value: unknown): ThinkingLevel | undefined {
130
+ const raw = stringValue(value)?.toLowerCase();
131
+ if (!raw) return undefined;
132
+ return THINKING_LEVELS.includes(raw as ThinkingLevel)
133
+ ? (raw as ThinkingLevel)
134
+ : undefined;
135
+ }
136
+
137
+ function parseFrontmatterScalar(value: string): string | null {
138
+ const trimmed = value.trim();
139
+ if (
140
+ !trimmed ||
141
+ trimmed === "|" ||
142
+ trimmed === ">" ||
143
+ trimmed === "[]" ||
144
+ trimmed === "null" ||
145
+ trimmed === "~"
146
+ ) {
147
+ return null;
148
+ }
149
+ if (
150
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
151
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
152
+ ) {
153
+ return trimmed.slice(1, -1).trim() || null;
154
+ }
155
+ return trimmed;
156
+ }
157
+
158
+ function parseInlineList(value: string): string[] {
159
+ const trimmed = value.trim();
160
+ if (!trimmed || trimmed === "[]") return [];
161
+ const body =
162
+ trimmed.startsWith("[") && trimmed.endsWith("]")
163
+ ? trimmed.slice(1, -1)
164
+ : trimmed;
165
+ return body
166
+ .split(",")
167
+ .map((item) => parseFrontmatterScalar(item))
168
+ .filter((item): item is string => !!item);
169
+ }
170
+
171
+ function readIndentedList(
172
+ lines: string[],
173
+ startIndex: number,
174
+ ): { values: string[]; nextIndex: number } {
175
+ const values: string[] = [];
176
+ let index = startIndex + 1;
177
+ while (index < lines.length) {
178
+ const line = lines[index] ?? "";
179
+ if (/^[A-Za-z][A-Za-z0-9_-]*\s*:/.test(line)) break;
180
+ const item = line.match(/^\s*-\s*(.*)$/);
181
+ if (item) {
182
+ const scalar = parseFrontmatterScalar(item[1] ?? "");
183
+ if (scalar) values.push(scalar);
184
+ }
185
+ index += 1;
186
+ }
187
+ return { values, nextIndex: index - 1 };
188
+ }
189
+
190
+ function parseAgentConfig(content: string): AgentConfig {
191
+ const config: AgentConfig = { fallbackModels: [] };
192
+ const { frontmatter } = splitMarkdownFrontmatter(content);
193
+ const lines = frontmatter.split(/\r?\n/);
194
+
195
+ for (let index = 0; index < lines.length; index += 1) {
196
+ const match = (lines[index] ?? "").match(
197
+ /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/,
198
+ );
199
+ if (!match) continue;
200
+
201
+ const key = match[1] ?? "";
202
+ const value = match[2] ?? "";
203
+ if (key === "model") {
204
+ config.model = parseFrontmatterScalar(value) ?? undefined;
205
+ } else if (key === "thinking") {
206
+ config.thinking = normalizeThinking(parseFrontmatterScalar(value));
207
+ } else if (key === "fallbackModels" || key === "fallback_models") {
208
+ if (value.trim()) {
209
+ config.fallbackModels = parseInlineList(value);
210
+ } else {
211
+ const result = readIndentedList(lines, index);
212
+ config.fallbackModels = result.values;
213
+ index = result.nextIndex;
214
+ }
215
+ }
216
+ }
217
+
218
+ return config;
219
+ }
220
+
221
+ function modelHasThinkingSuffix(model: string): boolean {
222
+ return THINKING_SUFFIX_RE.test(model.trim());
223
+ }
224
+
225
+ function buildPiModelArgs(config: PiRunConfig): string[] {
226
+ const model = stringValue(config.model);
227
+ const thinking = normalizeThinking(config.thinking);
228
+ if (model) {
229
+ return [
230
+ "--model",
231
+ thinking && !modelHasThinkingSuffix(model)
232
+ ? `${model}:${thinking}`
233
+ : model,
234
+ ];
235
+ }
236
+ return thinking ? ["--thinking", thinking] : [];
237
+ }
238
+
239
+ function resolveSubagentRunConfig(
240
+ input: SubagentInput,
241
+ agentConfig: AgentConfig,
242
+ ): PiRunConfig {
243
+ return {
244
+ model: stringValue(input.model) ?? agentConfig.model,
245
+ thinking: normalizeThinking(input.thinking) ?? agentConfig.thinking,
246
+ };
247
+ }
248
+
93
249
  function sanitizeKey(raw: string): string {
94
250
  return raw
95
251
  .trim()
@@ -102,13 +258,149 @@ function hashValue(raw: string): string {
102
258
  return createHash("sha256").update(raw).digest("hex").slice(0, 24);
103
259
  }
104
260
 
261
+ interface PiInvocation {
262
+ command: string;
263
+ argsPrefix: string[];
264
+ }
265
+
266
+ const PI_CLI_JS_SEGMENTS = [
267
+ "node_modules",
268
+ "@mariozechner",
269
+ "pi-coding-agent",
270
+ "dist",
271
+ "cli.js",
272
+ ];
273
+ const MAX_SUBAGENT_STDOUT_BYTES = 8 * 1024 * 1024;
274
+ const MAX_SUBAGENT_STDERR_BYTES = 1024 * 1024;
275
+
276
+ // Nested agents can emit unbounded output; keep the tail so diagnostics survive without growing memory indefinitely.
277
+ class BoundedBufferCollector {
278
+ private chunks: Buffer[] = [];
279
+ private length = 0;
280
+ private truncatedBytes = 0;
281
+
282
+ constructor(private readonly maxBytes: number) {}
283
+
284
+ append(chunk: Buffer): void {
285
+ const data = chunk;
286
+ if (data.length >= this.maxBytes) {
287
+ this.truncatedBytes += this.length + data.length - this.maxBytes;
288
+ this.chunks = [data.subarray(data.length - this.maxBytes)];
289
+ this.length = this.maxBytes;
290
+ return;
291
+ }
292
+
293
+ this.chunks.push(data);
294
+ this.length += data.length;
295
+
296
+ while (this.length > this.maxBytes) {
297
+ const first = this.chunks[0];
298
+ if (!first) break;
299
+ const overflow = this.length - this.maxBytes;
300
+ if (first.length <= overflow) {
301
+ this.chunks.shift();
302
+ this.length -= first.length;
303
+ this.truncatedBytes += first.length;
304
+ } else {
305
+ this.chunks[0] = first.subarray(overflow);
306
+ this.length -= overflow;
307
+ this.truncatedBytes += overflow;
308
+ break;
309
+ }
310
+ }
311
+ }
312
+
313
+ toString(): string {
314
+ const body = Buffer.concat(this.chunks, this.length).toString("utf-8");
315
+ return this.truncatedBytes
316
+ ? `[${this.truncatedBytes} bytes truncated]\n${body}`
317
+ : body;
318
+ }
319
+ }
320
+
321
+ function isExistingFile(path: string): boolean {
322
+ try {
323
+ return statSync(path).isFile();
324
+ } catch {
325
+ return false;
326
+ }
327
+ }
328
+
329
+ function uniqueStrings(values: string[]): string[] {
330
+ const seen = new Set<string>();
331
+ const unique: string[] = [];
332
+ for (const value of values) {
333
+ if (!value || seen.has(value)) continue;
334
+ seen.add(value);
335
+ unique.push(value);
336
+ }
337
+ return unique;
338
+ }
339
+
340
+ function candidatePiCliJsPaths(): string[] {
341
+ const candidates: string[] = [];
342
+
343
+ for (const arg of process.argv) {
344
+ if (/pi-coding-agent[\\/]dist[\\/]cli\.js$/i.test(arg)) {
345
+ candidates.push(resolve(arg));
346
+ }
347
+ }
348
+
349
+ const npmPrefix =
350
+ stringValue(process.env.npm_config_prefix) ??
351
+ stringValue(process.env.NPM_CONFIG_PREFIX);
352
+ if (npmPrefix) {
353
+ candidates.push(join(npmPrefix, ...PI_CLI_JS_SEGMENTS));
354
+ candidates.push(join(npmPrefix, "lib", ...PI_CLI_JS_SEGMENTS));
355
+ }
356
+
357
+ const appData = stringValue(process.env.APPDATA);
358
+ if (appData) {
359
+ candidates.push(join(appData, "npm", ...PI_CLI_JS_SEGMENTS));
360
+ }
361
+
362
+ const pathValue = process.env.PATH ?? process.env.Path ?? "";
363
+ for (const pathEntry of pathValue.split(delimiter)) {
364
+ const entry = pathEntry.trim();
365
+ if (!entry) continue;
366
+ candidates.push(join(entry, ...PI_CLI_JS_SEGMENTS));
367
+ candidates.push(join(dirname(entry), ...PI_CLI_JS_SEGMENTS));
368
+ candidates.push(join(dirname(entry), "lib", ...PI_CLI_JS_SEGMENTS));
369
+ }
370
+
371
+ return uniqueStrings(candidates);
372
+ }
373
+
374
+ function resolvePiInvocation(): PiInvocation {
375
+ const envCli = stringValue(process.env.TRELLIS_PI_CLI_JS);
376
+ if (envCli) {
377
+ const cliJs = resolve(envCli);
378
+ if (!isExistingFile(cliJs)) {
379
+ throw new Error(`TRELLIS_PI_CLI_JS points to a missing file: ${cliJs}`);
380
+ }
381
+ return { command: process.execPath, argsPrefix: [cliJs] };
382
+ }
383
+
384
+ for (const cliJs of candidatePiCliJsPaths()) {
385
+ if (isExistingFile(cliJs)) {
386
+ return { command: process.execPath, argsPrefix: [cliJs] };
387
+ }
388
+ }
389
+
390
+ return { command: "pi", argsPrefix: [] };
391
+ }
392
+
105
393
  function createProcessContextKey(projectRoot: string): string {
106
394
  return `pi_process_${hashValue(
107
- [projectRoot, process.pid, Date.now(), randomBytes(8).toString("hex")].join(":"),
395
+ [projectRoot, process.pid, Date.now(), randomBytes(8).toString("hex")].join(
396
+ ":",
397
+ ),
108
398
  )}`;
109
399
  }
110
400
 
111
- function callString(callback: (() => string | undefined) | undefined): string | null {
401
+ function callString(
402
+ callback: (() => string | undefined) | undefined,
403
+ ): string | null {
112
404
  if (!callback) return null;
113
405
  try {
114
406
  return stringValue(callback());
@@ -123,7 +415,13 @@ function lookupString(data: unknown, keys: string[]): string | null {
123
415
  const value = stringValue(data[key]);
124
416
  if (value) return value;
125
417
  }
126
- for (const nestedKey of ["input", "properties", "event", "hook_input", "hookInput"]) {
418
+ for (const nestedKey of [
419
+ "input",
420
+ "properties",
421
+ "event",
422
+ "hook_input",
423
+ "hookInput",
424
+ ]) {
127
425
  const nested = data[nestedKey];
128
426
  const value = lookupString(nested, keys);
129
427
  if (value) return value;
@@ -160,7 +458,7 @@ function extractFinalAssistantText(output: string): string | null {
160
458
  const text = extractTextContent(message.content);
161
459
  if (text) finalText = text;
162
460
  } catch {
163
- // Pi can print non-JSON diagnostics around JSON mode; keep scanning.
461
+ // Pi can print non-JSON diagnostics around structured output; keep scanning.
164
462
  }
165
463
  }
166
464
 
@@ -185,6 +483,44 @@ function taskRefToDir(projectRoot: string, taskRef: string): string {
185
483
  return join(projectRoot, ".trellis", "tasks", taskRef);
186
484
  }
187
485
 
486
+ function sessionFileHasCurrentTask(path: string): boolean {
487
+ try {
488
+ const context = JSON.parse(readText(path)) as JsonObject;
489
+ return !!normalizeTaskRef(stringValue(context.current_task) ?? "");
490
+ } catch {
491
+ return false;
492
+ }
493
+ }
494
+
495
+ function activeRuntimeContextKeys(projectRoot: string): string[] {
496
+ const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions");
497
+ try {
498
+ return readdirSync(sessionsDir, { withFileTypes: true })
499
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
500
+ .map((entry) => entry.name.slice(0, -".json".length))
501
+ .filter((key) =>
502
+ sessionFileHasCurrentTask(join(sessionsDir, `${key}.json`)),
503
+ );
504
+ } catch {
505
+ return [];
506
+ }
507
+ }
508
+
509
+ function adoptExistingContextKey(
510
+ projectRoot: string,
511
+ contextKey: string,
512
+ ): string {
513
+ const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions");
514
+ if (sessionFileHasCurrentTask(join(sessionsDir, `${contextKey}.json`))) {
515
+ return contextKey;
516
+ }
517
+
518
+ const keys = activeRuntimeContextKeys(projectRoot);
519
+ const processKeys = keys.filter((key) => key.startsWith("pi_process_"));
520
+ const candidates = processKeys.length ? processKeys : keys;
521
+ return candidates.length === 1 ? candidates[0] : contextKey;
522
+ }
523
+
188
524
  function resolveContextKey(
189
525
  input: unknown,
190
526
  ctx?: PiExtensionContext,
@@ -202,11 +538,7 @@ function resolveContextKey(
202
538
 
203
539
  const transcriptPath =
204
540
  callString(ctx?.sessionManager?.getSessionFile) ??
205
- lookupString(input, [
206
- "transcript_path",
207
- "transcriptPath",
208
- "transcript",
209
- ]);
541
+ lookupString(input, ["transcript_path", "transcriptPath", "transcript"]);
210
542
  if (transcriptPath) return `pi_transcript_${hashValue(transcriptPath)}`;
211
543
 
212
544
  return fallback ?? null;
@@ -218,11 +550,18 @@ function readCurrentTask(
218
550
  ctx?: PiExtensionContext,
219
551
  contextKeyOverride?: string | null,
220
552
  ): string | null {
221
- const contextKey = resolveContextKey(platformInput, ctx, contextKeyOverride);
553
+ const contextKey =
554
+ contextKeyOverride ?? resolveContextKey(platformInput, ctx);
222
555
  if (contextKey) {
223
556
  try {
224
557
  const rawContext = readText(
225
- join(projectRoot, ".trellis", ".runtime", "sessions", `${contextKey}.json`),
558
+ join(
559
+ projectRoot,
560
+ ".trellis",
561
+ ".runtime",
562
+ "sessions",
563
+ `${contextKey}.json`,
564
+ ),
226
565
  );
227
566
  const context = JSON.parse(rawContext) as JsonObject;
228
567
  const taskRef = normalizeTaskRef(stringValue(context.current_task) ?? "");
@@ -293,11 +632,20 @@ function buildTrellisContext(
293
632
  ].join("\n");
294
633
  }
295
634
 
296
- function readAgentDefinition(projectRoot: string, agent: string): string {
635
+ function normalizeAgentName(agent: string): string {
636
+ return agent.startsWith("trellis-") ? agent : `trellis-${agent}`;
637
+ }
638
+
639
+ function readAgentDefinition(
640
+ projectRoot: string,
641
+ agent: string,
642
+ ): AgentDefinition {
297
643
  const normalized = agent.startsWith("trellis-") ? agent : `trellis-${agent}`;
298
- return stripMarkdownFrontmatter(
299
- readText(join(projectRoot, ".pi", "agents", `${normalized}.md`)),
300
- );
644
+ const raw = readText(join(projectRoot, ".pi", "agents", `${normalized}.md`));
645
+ return {
646
+ content: stripMarkdownFrontmatter(raw),
647
+ config: parseAgentConfig(raw),
648
+ };
301
649
  }
302
650
 
303
651
  function commandStartsWithTrellisContext(command: string): boolean {
@@ -337,35 +685,89 @@ function injectTrellisContextIntoBash(
337
685
  function runPi(
338
686
  projectRoot: string,
339
687
  prompt: string,
688
+ runConfig: PiRunConfig,
340
689
  contextKey?: string | null,
690
+ signal?: AbortSignal,
341
691
  ): Promise<string> {
342
692
  return new Promise((resolvePromise, reject) => {
693
+ if (signal?.aborted) {
694
+ reject(new Error("pi subagent cancelled"));
695
+ return;
696
+ }
697
+
698
+ const invocation = resolvePiInvocation();
699
+ const modelArgs = buildPiModelArgs(runConfig);
343
700
  const child = spawn(
344
- "pi",
345
- ["--mode", "json", "-p", "--no-session", toPiPromptArgument(prompt)],
701
+ invocation.command,
702
+ [
703
+ ...invocation.argsPrefix,
704
+ "--mode",
705
+ "text",
706
+ ...modelArgs,
707
+ "-p",
708
+ "--no-session",
709
+ ],
346
710
  {
347
711
  cwd: projectRoot,
348
712
  env: contextKey
349
713
  ? { ...process.env, TRELLIS_CONTEXT_ID: contextKey }
350
714
  : process.env,
351
- stdio: ["ignore", "pipe", "pipe"],
715
+ stdio: ["pipe", "pipe", "pipe"],
716
+ windowsHide: true,
352
717
  },
353
718
  );
354
719
 
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);
720
+ const stdout = new BoundedBufferCollector(MAX_SUBAGENT_STDOUT_BYTES);
721
+ const stderr = new BoundedBufferCollector(MAX_SUBAGENT_STDERR_BYTES);
722
+ let settled = false;
723
+ let aborted = false;
724
+
725
+ const abortChild = (): void => {
726
+ aborted = true;
727
+ child.kill();
728
+ };
729
+
730
+ const cleanup = (): void => {
731
+ signal?.removeEventListener("abort", abortChild);
732
+ };
733
+
734
+ const fail = (error: Error): void => {
735
+ if (settled) return;
736
+ settled = true;
737
+ cleanup();
738
+ reject(error);
739
+ };
740
+
741
+ const succeed = (value: string): void => {
742
+ if (settled) return;
743
+ settled = true;
744
+ cleanup();
745
+ resolvePromise(value);
746
+ };
747
+
748
+ signal?.addEventListener("abort", abortChild, { once: true });
749
+
750
+ child.stdout?.on("data", (chunk: Buffer) => stdout.append(chunk));
751
+ child.stderr?.on("data", (chunk: Buffer) => stderr.append(chunk));
752
+ child.stdin?.on("error", (error: Error & { code?: string }) => {
753
+ if (!aborted && error.code !== "EPIPE") fail(error);
754
+ });
755
+ child.on("error", fail);
360
756
  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));
757
+ const out = stdout.toString();
758
+ const err = stderr.toString();
759
+ if (aborted) {
760
+ fail(new Error("pi subagent cancelled"));
761
+ } else if (code === 0) {
762
+ succeed(formatPiOutput(out, err));
365
763
  } else {
366
- reject(new Error(err || `pi exited with code ${code ?? "unknown"}`));
764
+ fail(
765
+ new Error(err || out || `pi exited with code ${code ?? "unknown"}`),
766
+ );
367
767
  }
368
768
  });
769
+
770
+ child.stdin?.end(prompt);
369
771
  });
370
772
  }
371
773
 
@@ -373,10 +775,13 @@ function buildSubagentPrompt(
373
775
  projectRoot: string,
374
776
  input: SubagentInput,
375
777
  contextKey?: string | null,
778
+ agentName?: string,
779
+ agentDefinition?: AgentDefinition,
376
780
  ): string {
377
- const agent = input.agent ?? "trellis-implement";
378
- const normalized = agent.startsWith("trellis-") ? agent : `trellis-${agent}`;
379
- const definition = readAgentDefinition(projectRoot, normalized);
781
+ const normalized =
782
+ agentName ?? normalizeAgentName(input.agent ?? "trellis-implement");
783
+ const definition =
784
+ agentDefinition ?? readAgentDefinition(projectRoot, normalized);
380
785
  const context = buildTrellisContext(
381
786
  projectRoot,
382
787
  normalized,
@@ -388,7 +793,7 @@ function buildSubagentPrompt(
388
793
 
389
794
  return [
390
795
  "## Trellis Agent Definition",
391
- definition || "(missing agent definition)",
796
+ definition.content || "(missing agent definition)",
392
797
  "",
393
798
  context,
394
799
  "",
@@ -401,7 +806,11 @@ async function runSubagent(
401
806
  projectRoot: string,
402
807
  input: SubagentInput,
403
808
  contextKey?: string | null,
809
+ signal?: AbortSignal,
404
810
  ): Promise<string> {
811
+ const agentName = normalizeAgentName(input.agent ?? "trellis-implement");
812
+ const agentDefinition = readAgentDefinition(projectRoot, agentName);
813
+ const runConfig = resolveSubagentRunConfig(input, agentDefinition.config);
405
814
  const mode = input.mode ?? "single";
406
815
  if (mode === "parallel") {
407
816
  const prompts = input.prompts ?? (input.prompt ? [input.prompt] : []);
@@ -409,8 +818,16 @@ async function runSubagent(
409
818
  prompts.map((prompt) =>
410
819
  runPi(
411
820
  projectRoot,
412
- buildSubagentPrompt(projectRoot, { ...input, prompt }, contextKey),
821
+ buildSubagentPrompt(
822
+ projectRoot,
823
+ { ...input, prompt },
824
+ contextKey,
825
+ agentName,
826
+ agentDefinition,
827
+ ),
828
+ runConfig,
413
829
  contextKey,
830
+ signal,
414
831
  ),
415
832
  ),
416
833
  );
@@ -423,13 +840,21 @@ async function runSubagent(
423
840
  for (const prompt of prompts) {
424
841
  previous = await runPi(
425
842
  projectRoot,
426
- buildSubagentPrompt(projectRoot, {
427
- ...input,
428
- prompt: previous
429
- ? `${prompt}\n\nPrevious output:\n${previous}`
430
- : prompt,
431
- }, contextKey),
843
+ buildSubagentPrompt(
844
+ projectRoot,
845
+ {
846
+ ...input,
847
+ prompt: previous
848
+ ? `${prompt}\n\nPrevious output:\n${previous}`
849
+ : prompt,
850
+ },
851
+ contextKey,
852
+ agentName,
853
+ agentDefinition,
854
+ ),
855
+ runConfig,
432
856
  contextKey,
857
+ signal,
433
858
  );
434
859
  }
435
860
  return previous;
@@ -437,8 +862,16 @@ async function runSubagent(
437
862
 
438
863
  return runPi(
439
864
  projectRoot,
440
- buildSubagentPrompt(projectRoot, input, contextKey),
865
+ buildSubagentPrompt(
866
+ projectRoot,
867
+ input,
868
+ contextKey,
869
+ agentName,
870
+ agentDefinition,
871
+ ),
872
+ runConfig,
441
873
  contextKey,
874
+ signal,
442
875
  );
443
876
  }
444
877
 
@@ -455,12 +888,15 @@ export default function trellisExtension(pi: {
455
888
  let currentContextKey: string | null = null;
456
889
 
457
890
  const getContextKey = (input?: unknown, ctx?: PiExtensionContext): string => {
458
- const contextKey = resolveContextKey(
891
+ const resolvedContextKey = resolveContextKey(
459
892
  input,
460
893
  ctx,
461
894
  currentContextKey ?? processContextKey,
462
895
  );
463
- currentContextKey = contextKey ?? processContextKey;
896
+ currentContextKey = adoptExistingContextKey(
897
+ projectRoot,
898
+ resolvedContextKey ?? processContextKey,
899
+ );
464
900
  return currentContextKey;
465
901
  };
466
902
 
@@ -473,7 +909,8 @@ export default function trellisExtension(pi: {
473
909
  properties: {
474
910
  agent: {
475
911
  type: "string",
476
- description: "Agent name, such as trellis-implement or trellis-check.",
912
+ description:
913
+ "Agent name, such as trellis-implement or trellis-check.",
477
914
  },
478
915
  prompt: {
479
916
  type: "string",
@@ -489,6 +926,17 @@ export default function trellisExtension(pi: {
489
926
  items: { type: "string" },
490
927
  description: "Prompts for parallel or chain mode.",
491
928
  },
929
+ model: {
930
+ type: "string",
931
+ description:
932
+ "Optional Pi model override for the child sub-agent process.",
933
+ },
934
+ thinking: {
935
+ type: "string",
936
+ enum: ["off", "minimal", "low", "medium", "high", "xhigh"],
937
+ description:
938
+ "Optional Pi thinking level override for the child sub-agent process.",
939
+ },
492
940
  },
493
941
  required: ["prompt"],
494
942
  },
@@ -500,7 +948,7 @@ export default function trellisExtension(pi: {
500
948
  ctx?: PiExtensionContext,
501
949
  ): Promise<PiToolResult> => {
502
950
  const contextKey = getContextKey(input, ctx);
503
- const output = await runSubagent(projectRoot, input, contextKey);
951
+ const output = await runSubagent(projectRoot, input, contextKey, _signal);
504
952
  return {
505
953
  content: [{ type: "text", text: output }],
506
954
  details: {