@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.
- package/README.md +60 -98
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -29
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +92 -5
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/antigravity.d.ts.map +1 -1
- package/dist/configurators/antigravity.js +2 -8
- package/dist/configurators/antigravity.js.map +1 -1
- package/dist/configurators/claude.d.ts.map +1 -1
- package/dist/configurators/claude.js +3 -9
- package/dist/configurators/claude.js.map +1 -1
- package/dist/configurators/codebuddy.d.ts.map +1 -1
- package/dist/configurators/codebuddy.js +2 -2
- package/dist/configurators/codebuddy.js.map +1 -1
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +2 -7
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/copilot.d.ts.map +1 -1
- package/dist/configurators/copilot.js +2 -9
- package/dist/configurators/copilot.js.map +1 -1
- package/dist/configurators/cursor.d.ts.map +1 -1
- package/dist/configurators/cursor.js +2 -2
- package/dist/configurators/cursor.js.map +1 -1
- package/dist/configurators/droid.d.ts.map +1 -1
- package/dist/configurators/droid.js +2 -2
- package/dist/configurators/droid.js.map +1 -1
- package/dist/configurators/gemini.d.ts.map +1 -1
- package/dist/configurators/gemini.js +2 -2
- package/dist/configurators/gemini.js.map +1 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +13 -11
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/kilo.d.ts.map +1 -1
- package/dist/configurators/kilo.js +2 -8
- package/dist/configurators/kilo.js.map +1 -1
- package/dist/configurators/kiro.d.ts.map +1 -1
- package/dist/configurators/kiro.js +2 -2
- package/dist/configurators/kiro.js.map +1 -1
- package/dist/configurators/opencode.d.ts.map +1 -1
- package/dist/configurators/opencode.js +3 -3
- package/dist/configurators/opencode.js.map +1 -1
- package/dist/configurators/pi.d.ts.map +1 -1
- package/dist/configurators/pi.js +9 -4
- package/dist/configurators/pi.js.map +1 -1
- package/dist/configurators/qoder.d.ts.map +1 -1
- package/dist/configurators/qoder.js +2 -2
- package/dist/configurators/qoder.js.map +1 -1
- package/dist/configurators/shared.d.ts +21 -2
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +32 -3
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/windsurf.d.ts.map +1 -1
- package/dist/configurators/windsurf.js +2 -8
- package/dist/configurators/windsurf.js.map +1 -1
- package/dist/constants/paths.d.ts +2 -0
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +2 -0
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.5.0-beta.17.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.18.json +9 -0
- package/dist/templates/codex/skills/finish-work/SKILL.md +41 -109
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +73 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +83 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +54 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +81 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-hooks.md +57 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +78 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +83 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +79 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +48 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +55 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +68 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +80 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/overview.md +51 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +102 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +101 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +75 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workspace-memory.md +71 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +79 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +69 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +59 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +74 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +83 -0
- package/dist/templates/common/commands/finish-work.md +34 -10
- package/dist/templates/common/index.d.ts +22 -2
- package/dist/templates/common/index.d.ts.map +1 -1
- package/dist/templates/common/index.js +53 -4
- package/dist/templates/common/index.js.map +1 -1
- package/dist/templates/common/skills/brainstorm.md +3 -0
- package/dist/templates/copilot/prompts/finish-work.prompt.md +44 -112
- package/dist/templates/markdown/agents.md +8 -0
- package/dist/templates/opencode/plugins/inject-subagent-context.js +20 -5
- package/dist/templates/opencode/plugins/inject-workflow-state.js +6 -1
- package/dist/templates/pi/extensions/trellis/index.ts.txt +499 -51
- package/dist/templates/shared-hooks/inject-workflow-state.py +6 -1
- package/dist/templates/trellis/scripts/common/task_store.py +4 -16
- package/dist/templates/trellis/scripts/common/tasks.py +4 -1
- package/dist/templates/trellis/workflow.md +59 -3
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +19 -1
- package/dist/utils/template-hash.js.map +1 -1
- 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
|
|
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]
|
|
78
|
-
return
|
|
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
|
|
82
|
-
return
|
|
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(
|
|
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 [
|
|
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
|
|
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 =
|
|
553
|
+
const contextKey =
|
|
554
|
+
contextKeyOverride ?? resolveContextKey(platformInput, ctx);
|
|
222
555
|
if (contextKey) {
|
|
223
556
|
try {
|
|
224
557
|
const rawContext = readText(
|
|
225
|
-
join(
|
|
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
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
345
|
-
[
|
|
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: ["
|
|
715
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
716
|
+
windowsHide: true,
|
|
352
717
|
},
|
|
353
718
|
);
|
|
354
719
|
|
|
355
|
-
const stdout
|
|
356
|
-
const stderr
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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 =
|
|
362
|
-
const err =
|
|
363
|
-
if (
|
|
364
|
-
|
|
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
|
-
|
|
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
|
|
378
|
-
|
|
379
|
-
const definition =
|
|
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(
|
|
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(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
:
|
|
431
|
-
|
|
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(
|
|
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
|
|
891
|
+
const resolvedContextKey = resolveContextKey(
|
|
459
892
|
input,
|
|
460
893
|
ctx,
|
|
461
894
|
currentContextKey ?? processContextKey,
|
|
462
895
|
);
|
|
463
|
-
currentContextKey =
|
|
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:
|
|
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: {
|