@mindfoldhq/trellis 0.6.0-beta.17 → 0.6.0-beta.19
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 +1 -1
- package/dist/commands/channel/adapters/claude.d.ts +7 -16
- package/dist/commands/channel/adapters/claude.d.ts.map +1 -1
- package/dist/commands/channel/adapters/claude.js +19 -25
- package/dist/commands/channel/adapters/claude.js.map +1 -1
- package/dist/commands/channel/adapters/codex.d.ts +5 -1
- package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
- package/dist/commands/channel/adapters/codex.js +8 -15
- package/dist/commands/channel/adapters/codex.js.map +1 -1
- package/dist/commands/channel/adapters/index.d.ts +6 -1
- package/dist/commands/channel/adapters/index.d.ts.map +1 -1
- package/dist/commands/channel/adapters/index.js +12 -6
- package/dist/commands/channel/adapters/index.js.map +1 -1
- package/dist/commands/channel/guard.d.ts +150 -0
- package/dist/commands/channel/guard.d.ts.map +1 -0
- package/dist/commands/channel/guard.js +474 -0
- package/dist/commands/channel/guard.js.map +1 -0
- package/dist/commands/channel/index.d.ts +1 -1
- package/dist/commands/channel/index.d.ts.map +1 -1
- package/dist/commands/channel/index.js +38 -10
- package/dist/commands/channel/index.js.map +1 -1
- package/dist/commands/channel/interrupt.d.ts +10 -0
- package/dist/commands/channel/interrupt.d.ts.map +1 -0
- package/dist/commands/channel/interrupt.js +22 -0
- package/dist/commands/channel/interrupt.js.map +1 -0
- package/dist/commands/channel/messages.d.ts +0 -1
- package/dist/commands/channel/messages.d.ts.map +1 -1
- package/dist/commands/channel/messages.js +2 -6
- package/dist/commands/channel/messages.js.map +1 -1
- package/dist/commands/channel/run.d.ts +0 -1
- package/dist/commands/channel/run.d.ts.map +1 -1
- package/dist/commands/channel/run.js +5 -12
- package/dist/commands/channel/run.js.map +1 -1
- package/dist/commands/channel/send.d.ts +0 -2
- package/dist/commands/channel/send.d.ts.map +1 -1
- package/dist/commands/channel/send.js +0 -2
- package/dist/commands/channel/send.js.map +1 -1
- package/dist/commands/channel/spawn.d.ts +10 -0
- package/dist/commands/channel/spawn.d.ts.map +1 -1
- package/dist/commands/channel/spawn.js +57 -7
- package/dist/commands/channel/spawn.js.map +1 -1
- package/dist/commands/channel/supervisor/idle.d.ts +46 -0
- package/dist/commands/channel/supervisor/idle.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/idle.js +72 -0
- package/dist/commands/channel/supervisor/idle.js.map +1 -0
- package/dist/commands/channel/supervisor/inbox.d.ts +4 -4
- package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -1
- package/dist/commands/channel/supervisor/inbox.js +22 -22
- package/dist/commands/channel/supervisor/inbox.js.map +1 -1
- package/dist/commands/channel/supervisor/shutdown.d.ts +3 -1
- package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -1
- package/dist/commands/channel/supervisor/shutdown.js +4 -1
- package/dist/commands/channel/supervisor/shutdown.js.map +1 -1
- package/dist/commands/channel/supervisor/turns.d.ts +11 -0
- package/dist/commands/channel/supervisor/turns.d.ts.map +1 -1
- package/dist/commands/channel/supervisor/turns.js +19 -2
- package/dist/commands/channel/supervisor/turns.js.map +1 -1
- package/dist/commands/channel/supervisor.d.ts +6 -0
- package/dist/commands/channel/supervisor.d.ts.map +1 -1
- package/dist/commands/channel/supervisor.js +43 -3
- package/dist/commands/channel/supervisor.js.map +1 -1
- package/dist/commands/channel/wait.d.ts +0 -1
- package/dist/commands/channel/wait.d.ts.map +1 -1
- package/dist/commands/channel/wait.js +0 -1
- package/dist/commands/channel/wait.js.map +1 -1
- package/dist/migrations/manifests/0.5.16.json +9 -0
- package/dist/migrations/manifests/0.5.17.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.18.json +16 -0
- package/dist/migrations/manifests/0.6.0-beta.19.json +9 -0
- package/dist/templates/codex/config.toml +5 -3
- package/dist/templates/pi/extensions/trellis/index.ts.txt +1339 -913
- package/dist/templates/pi/settings.json +0 -9
- package/dist/templates/trellis/config.yaml +20 -0
- package/dist/templates/trellis/scripts/common/task_store.py +55 -7
- package/dist/templates/trellis/workflow.md +1 -0
- package/package.json +2 -2
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { delimiter, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
|
|
6
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
6
7
|
type JsonObject = Record<string, unknown>;
|
|
7
8
|
type TextContent = { type: "text"; text: string };
|
|
8
|
-
|
|
9
9
|
interface PiToolResult {
|
|
10
10
|
content: TextContent[];
|
|
11
|
-
details?:
|
|
11
|
+
details?: unknown;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
13
|
interface PiExtensionContext {
|
|
15
14
|
hasUI?: boolean;
|
|
16
15
|
sessionManager?: {
|
|
@@ -18,1065 +17,1389 @@ interface PiExtensionContext {
|
|
|
18
17
|
getSessionFile?: () => string | undefined;
|
|
19
18
|
};
|
|
20
19
|
ui?: {
|
|
21
|
-
notify?: (
|
|
20
|
+
notify?: (msg: string, type?: "info" | "warning" | "error") => void;
|
|
22
21
|
};
|
|
23
22
|
}
|
|
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
23
|
interface SubagentInput {
|
|
39
24
|
agent?: string;
|
|
40
25
|
prompt?: string;
|
|
41
26
|
mode?: "single" | "parallel" | "chain";
|
|
42
27
|
prompts?: string[];
|
|
43
28
|
model?: string;
|
|
44
|
-
thinking?:
|
|
29
|
+
thinking?: string;
|
|
45
30
|
}
|
|
46
|
-
|
|
47
|
-
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
48
|
-
|
|
49
31
|
interface AgentConfig {
|
|
50
32
|
model?: string;
|
|
51
|
-
thinking?:
|
|
52
|
-
// Parsed for pi-subagents-compatible agent files; Pi CLI has no documented fallback-model flag to pass through here.
|
|
33
|
+
thinking?: string;
|
|
53
34
|
fallbackModels: string[];
|
|
54
35
|
}
|
|
55
|
-
|
|
56
|
-
interface AgentDefinition {
|
|
57
|
-
content: string;
|
|
58
|
-
config: AgentConfig;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
36
|
interface PiRunConfig {
|
|
62
37
|
model?: string;
|
|
63
|
-
thinking?:
|
|
38
|
+
thinking?: string;
|
|
64
39
|
}
|
|
65
40
|
|
|
41
|
+
// ── Lazy-load pi-tui (avoid failing top-level imports) ─────────────────
|
|
42
|
+
let _piTui: {
|
|
43
|
+
visibleWidth?: (s: string) => number;
|
|
44
|
+
truncateToWidth?: (s: string, w: number, ellipsis?: string) => string;
|
|
45
|
+
} | null = null;
|
|
46
|
+
function piTui() {
|
|
47
|
+
if (!_piTui) {
|
|
48
|
+
try {
|
|
49
|
+
_piTui = require("@earendil-works/pi-tui");
|
|
50
|
+
} catch {
|
|
51
|
+
_piTui = {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return _piTui;
|
|
55
|
+
}
|
|
56
|
+
function trunc(s: string, w: number) {
|
|
57
|
+
const t = piTui();
|
|
58
|
+
return t.truncateToWidth
|
|
59
|
+
? t.truncateToWidth(s, w, "…")
|
|
60
|
+
: s.length <= w
|
|
61
|
+
? s
|
|
62
|
+
: w > 1
|
|
63
|
+
? s.slice(0, w - 1) + "…"
|
|
64
|
+
: s.slice(0, w);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Constants ─────────────────────────────────────────────────────────
|
|
66
68
|
const TRELLIS_AGENT_JSONL: Record<string, string> = {
|
|
67
69
|
"trellis-implement": "implement.jsonl",
|
|
68
70
|
implement: "implement.jsonl",
|
|
69
71
|
"trellis-check": "check.jsonl",
|
|
70
72
|
check: "check.jsonl",
|
|
71
73
|
};
|
|
74
|
+
const MAX_STDOUT = 8 * 1024 * 1024;
|
|
75
|
+
const MAX_STDERR = 1024 * 1024;
|
|
76
|
+
const MAX_TAIL = 256 * 1024;
|
|
77
|
+
const MAX_LINE_BUFFER = 1024 * 1024;
|
|
78
|
+
const MAX_TOOL_ARG_CHARS = 2048;
|
|
79
|
+
const MAX_TOOLS = 256;
|
|
80
|
+
const MAX_PARALLEL_PROMPTS = 6;
|
|
81
|
+
const ABORT_KILL_GRACE_MS = 1500;
|
|
82
|
+
const SESSION_OVERVIEW_TIMEOUT_MS = 1500;
|
|
83
|
+
const THROTTLE_MS = 500;
|
|
84
|
+
|
|
85
|
+
// ── State types ───────────────────────────────────────────────────────
|
|
86
|
+
type RunStatus = "pending" | "running" | "succeeded" | "failed" | "cancelled";
|
|
87
|
+
type ToolStatus = "running" | "succeeded" | "failed";
|
|
88
|
+
|
|
89
|
+
interface Usage {
|
|
90
|
+
input: number;
|
|
91
|
+
output: number;
|
|
92
|
+
cacheRead: number;
|
|
93
|
+
cacheWrite: number;
|
|
94
|
+
cost: number;
|
|
95
|
+
ctxTokens: number;
|
|
96
|
+
turns: number;
|
|
97
|
+
}
|
|
98
|
+
interface ToolTrace {
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
args: string;
|
|
102
|
+
status: ToolStatus;
|
|
103
|
+
startedAt: number;
|
|
104
|
+
finishedAt?: number;
|
|
105
|
+
}
|
|
106
|
+
interface RunState {
|
|
107
|
+
id: string;
|
|
108
|
+
agent: string;
|
|
109
|
+
prompt: string;
|
|
110
|
+
step?: number;
|
|
111
|
+
status: RunStatus;
|
|
112
|
+
startedAt?: number;
|
|
113
|
+
finishedAt?: number;
|
|
114
|
+
finalText: string;
|
|
115
|
+
textTail: string;
|
|
116
|
+
thinkingTail: string;
|
|
117
|
+
stderrTail: string;
|
|
118
|
+
tools: ToolTrace[];
|
|
119
|
+
usage: Usage;
|
|
120
|
+
model?: string;
|
|
121
|
+
thinking?: string;
|
|
122
|
+
errorMessage?: string;
|
|
123
|
+
}
|
|
124
|
+
interface ProgressDetails {
|
|
125
|
+
kind: "trellis-subagent-progress";
|
|
126
|
+
agent: string;
|
|
127
|
+
mode: "single" | "parallel" | "chain";
|
|
128
|
+
startedAt: number;
|
|
129
|
+
updatedAt: number;
|
|
130
|
+
final: boolean;
|
|
131
|
+
runs: RunState[];
|
|
132
|
+
}
|
|
72
133
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
134
|
+
// ── Native partial-update card state ──────────────────────────────────
|
|
135
|
+
interface NativeCardHandle {
|
|
136
|
+
state: JsonObject;
|
|
137
|
+
invalidate: () => void;
|
|
138
|
+
updatedAt: number;
|
|
139
|
+
}
|
|
140
|
+
const MAX_NATIVE_CARDS = 20;
|
|
141
|
+
const nativeCards = new Map<string, NativeCardHandle>();
|
|
142
|
+
let activeSubagentToolCallId: string | null = null;
|
|
143
|
+
function rememberNativeCard(id: string, card: NativeCardHandle) {
|
|
144
|
+
nativeCards.set(id, card);
|
|
145
|
+
const active = activeSubagentToolCallId
|
|
146
|
+
? nativeCards.get(activeSubagentToolCallId)
|
|
147
|
+
: undefined;
|
|
148
|
+
if (!active || card.updatedAt >= active.updatedAt)
|
|
149
|
+
activeSubagentToolCallId = id;
|
|
150
|
+
for (const key of nativeCards.keys()) {
|
|
151
|
+
if (nativeCards.size <= MAX_NATIVE_CARDS) break;
|
|
152
|
+
if (key !== activeSubagentToolCallId) nativeCards.delete(key);
|
|
85
153
|
}
|
|
86
154
|
}
|
|
87
|
-
|
|
88
|
-
|
|
155
|
+
function totalUsage(d: ProgressDetails): Usage {
|
|
156
|
+
const u: Usage = {
|
|
157
|
+
input: 0,
|
|
158
|
+
output: 0,
|
|
159
|
+
cacheRead: 0,
|
|
160
|
+
cacheWrite: 0,
|
|
161
|
+
cost: 0,
|
|
162
|
+
ctxTokens: 0,
|
|
163
|
+
turns: 0,
|
|
164
|
+
};
|
|
165
|
+
for (const r of d.runs) {
|
|
166
|
+
u.input += r.usage.input;
|
|
167
|
+
u.output += r.usage.output;
|
|
168
|
+
u.cacheRead += r.usage.cacheRead;
|
|
169
|
+
u.cacheWrite += r.usage.cacheWrite;
|
|
170
|
+
u.cost += r.usage.cost;
|
|
171
|
+
u.ctxTokens = Math.max(u.ctxTokens, r.usage.ctxTokens);
|
|
172
|
+
u.turns += r.usage.turns;
|
|
173
|
+
}
|
|
174
|
+
return u;
|
|
175
|
+
}
|
|
176
|
+
function activeRun(d: ProgressDetails) {
|
|
177
|
+
return d.runs.find((r) => r.status === "running") ?? d.runs.at(-1);
|
|
178
|
+
}
|
|
179
|
+
function toolArgs(t: ToolTrace) {
|
|
89
180
|
try {
|
|
90
|
-
return
|
|
181
|
+
return JSON.parse(t.args) as Record<string, unknown>;
|
|
91
182
|
} catch {
|
|
92
|
-
return
|
|
183
|
+
return {};
|
|
93
184
|
}
|
|
94
185
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
body: string;
|
|
99
|
-
} {
|
|
100
|
-
const normalized = content.replace(/^\uFEFF/, "");
|
|
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 };
|
|
186
|
+
function bashCommand(t: ToolTrace) {
|
|
187
|
+
const a = toolArgs(t);
|
|
188
|
+
return String(a.command || "").toLowerCase();
|
|
105
189
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return splitMarkdownFrontmatter(content).body.trimStart();
|
|
190
|
+
function isSearchTool(t: ToolTrace) {
|
|
191
|
+
return t.name === "read" || t.name === "grep" || t.name === "find";
|
|
109
192
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
193
|
+
function isMutationTool(t: ToolTrace) {
|
|
194
|
+
return t.name === "edit" || t.name === "write";
|
|
113
195
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return
|
|
196
|
+
function isValidationCommand(t: ToolTrace) {
|
|
197
|
+
const c = bashCommand(t);
|
|
198
|
+
return /\b(test|typecheck|lint|build|gofmt|go test|npm run|pnpm|vitest|tsc)\b/.test(
|
|
199
|
+
c,
|
|
200
|
+
);
|
|
117
201
|
}
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
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;
|
|
202
|
+
function isInspectionCommand(t: ToolTrace) {
|
|
203
|
+
const c = bashCommand(t);
|
|
204
|
+
return /\b(rg|grep|find|git diff|git status|ls|tree)\b/.test(c);
|
|
135
205
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
return
|
|
206
|
+
function thinkingIntent(text: string) {
|
|
207
|
+
const s = text.toLowerCase();
|
|
208
|
+
if (/error|failed|failure|panic|exception|报错|失败|错误|异常/.test(s))
|
|
209
|
+
return "Analyzing failure cause";
|
|
210
|
+
if (/test|verify|check|typecheck|lint|验证|测试|检查/.test(s))
|
|
211
|
+
return "Planning verification steps";
|
|
212
|
+
if (/plan|approach|design|strategy|方案|计划|思路|设计/.test(s))
|
|
213
|
+
return "Structuring the implementation approach";
|
|
214
|
+
if (/implement|change|edit|modify|refactor|实现|修改|重构/.test(s))
|
|
215
|
+
return "Reasoning through code changes";
|
|
216
|
+
if (/inspect|search|locate|read|context|定位|搜索|阅读|上下文/.test(s))
|
|
217
|
+
return "Locating relevant context";
|
|
218
|
+
return "";
|
|
219
|
+
}
|
|
220
|
+
function behaviorSummary(r: RunState) {
|
|
221
|
+
if (r.status === "succeeded") return "Task completed and result returned";
|
|
222
|
+
if (r.status === "failed")
|
|
223
|
+
return "Task failed and error details were retained";
|
|
224
|
+
|
|
225
|
+
const runningTool = r.tools.findLast((t) => t.status === "running");
|
|
226
|
+
if (runningTool) {
|
|
227
|
+
if (isMutationTool(runningTool)) return "Applying the plan to code";
|
|
228
|
+
if (runningTool.name === "bash" && isValidationCommand(runningTool))
|
|
229
|
+
return "Verifying whether the implementation passes";
|
|
230
|
+
if (runningTool.name === "bash" && isInspectionCommand(runningTool))
|
|
231
|
+
return "Inspecting current code state";
|
|
232
|
+
if (isSearchTool(runningTool)) return "Locating relevant code and context";
|
|
233
|
+
if (runningTool.name === "bash")
|
|
234
|
+
return "Validating assumptions with commands";
|
|
235
|
+
return "Using tools to advance the task";
|
|
148
236
|
}
|
|
237
|
+
|
|
238
|
+
const recent = r.tools.slice(-5);
|
|
239
|
+
if (recent.some((t) => t.status === "failed"))
|
|
240
|
+
return "Investigating tool or command failure";
|
|
241
|
+
if (recent.some(isMutationTool)) return "Reviewing recent changes";
|
|
242
|
+
if (recent.some((t) => t.name === "bash" && isValidationCommand(t)))
|
|
243
|
+
return "Analyzing verification results";
|
|
149
244
|
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
245
|
+
recent.length >= 2 &&
|
|
246
|
+
recent.every(
|
|
247
|
+
(t) => isSearchTool(t) || (t.name === "bash" && isInspectionCommand(t)),
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
return "Mapping code structure and impact";
|
|
251
|
+
|
|
252
|
+
const intent = thinkingIntent(`${r.thinkingTail}\n${r.textTail}`);
|
|
253
|
+
if (intent) return intent;
|
|
254
|
+
if (!r.tools.length) return "Understanding the task and planning execution";
|
|
255
|
+
return "Advancing the task and preparing next steps";
|
|
156
256
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
?
|
|
164
|
-
:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
.map((item) => parseFrontmatterScalar(item))
|
|
168
|
-
.filter((item): item is string => !!item);
|
|
257
|
+
function progressState(d: ProgressDetails) {
|
|
258
|
+
const running = d.runs.filter((r) => r.status === "running").length;
|
|
259
|
+
const failed = d.runs.some((r) => r.status === "failed");
|
|
260
|
+
return failed
|
|
261
|
+
? "failed"
|
|
262
|
+
: d.final
|
|
263
|
+
? "completed"
|
|
264
|
+
: running
|
|
265
|
+
? `${running} running`
|
|
266
|
+
: "pending";
|
|
169
267
|
}
|
|
170
|
-
|
|
171
|
-
|
|
268
|
+
function progressDone(d: ProgressDetails) {
|
|
269
|
+
return d.runs.filter((r) => r.status !== "pending" && r.status !== "running")
|
|
270
|
+
.length;
|
|
271
|
+
}
|
|
272
|
+
function summaryText(text: string) {
|
|
273
|
+
return `${text.trim().replace(/[。.!?…]+$/u, "")}...`;
|
|
274
|
+
}
|
|
275
|
+
function splitModelThinking(model?: string, fallbackThinking?: string) {
|
|
276
|
+
const m = model?.match(/^(.*):(off|minimal|low|medium|high|xhigh)$/i);
|
|
277
|
+
return {
|
|
278
|
+
model: m ? m[1] : model,
|
|
279
|
+
thinking: (m?.[2] ?? fallbackThinking)?.toLowerCase(),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function modelLabel(r: RunState) {
|
|
283
|
+
const { model, thinking } = splitModelThinking(r.model, r.thinking);
|
|
284
|
+
if (!model) return undefined;
|
|
285
|
+
return thinking && thinking !== "off" ? `${model}(${thinking})` : model;
|
|
286
|
+
}
|
|
287
|
+
function applyRunConfig(r: RunState, cfg: PiRunConfig) {
|
|
288
|
+
const parsed = splitModelThinking(cfg.model, cfg.thinking);
|
|
289
|
+
r.model = parsed.model;
|
|
290
|
+
r.thinking = parsed.thinking;
|
|
291
|
+
}
|
|
292
|
+
function runElapsed(d: ProgressDetails, r: RunState) {
|
|
293
|
+
const start = r.startedAt ?? d.startedAt;
|
|
294
|
+
const end =
|
|
295
|
+
r.finishedAt ?? (r.status === "running" ? Date.now() : d.updatedAt);
|
|
296
|
+
return fmtDur(Math.max(0, end - start));
|
|
297
|
+
}
|
|
298
|
+
function runHeader(d: ProgressDetails, r: RunState) {
|
|
299
|
+
const usage = fmtUsage(r.usage, modelLabel(r)) || fmtUsage(totalUsage(d));
|
|
300
|
+
return `${r.agent} · ${progressDone(d)}/${d.runs.length} done · ${progressState(d)} · ${runElapsed(d, r)}${usage ? ` · ${usage}` : ""}`;
|
|
301
|
+
}
|
|
302
|
+
function renderRunBlock(
|
|
172
303
|
lines: string[],
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
304
|
+
d: ProgressDetails,
|
|
305
|
+
run: RunState,
|
|
306
|
+
expanded: boolean,
|
|
307
|
+
) {
|
|
308
|
+
const step = run.step ? `step ${run.step} · ` : "";
|
|
309
|
+
lines.push(` - ${step}${runHeader(d, run)}`);
|
|
310
|
+
const summary = behaviorSummary(run);
|
|
311
|
+
if (summary) lines.push(` › ${summaryText(summary)}`);
|
|
312
|
+
const visibleTools = expanded ? run.tools.slice(-8) : run.tools.slice(-1);
|
|
313
|
+
for (const t of visibleTools)
|
|
314
|
+
lines.push(` ${toolIcon(t.status)} ${toolBrief(t)}`);
|
|
315
|
+
if (expanded && run.errorMessage) {
|
|
316
|
+
lines.push(` ✗ ${oneLine(run.errorMessage, 120)}`);
|
|
186
317
|
}
|
|
187
|
-
return { values, nextIndex: index - 1 };
|
|
188
318
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
index = result.nextIndex;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
319
|
+
function renderProgressCard(
|
|
320
|
+
d: ProgressDetails,
|
|
321
|
+
expanded: boolean,
|
|
322
|
+
w: number,
|
|
323
|
+
): string[] {
|
|
324
|
+
const r = activeRun(d);
|
|
325
|
+
if (!r) return [];
|
|
326
|
+
const spinner = ["◐", "◓", "◑", "◒"][Math.floor(Date.now() / 250) % 4]!;
|
|
327
|
+
const icon = d.final
|
|
328
|
+
? d.runs.some((x) => x.status === "failed")
|
|
329
|
+
? "✗"
|
|
330
|
+
: "✓"
|
|
331
|
+
: spinner;
|
|
332
|
+
const totalElapsed = fmtDur(
|
|
333
|
+
(d.final ? d.updatedAt : Date.now()) - d.startedAt,
|
|
334
|
+
);
|
|
335
|
+
const lines: string[] = [
|
|
336
|
+
`${icon} subagent ${d.mode} · total ${totalElapsed}`,
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
if (!expanded) {
|
|
340
|
+
renderRunBlock(lines, d, r, false);
|
|
341
|
+
lines.push(" Alt+O expand latest subagent card");
|
|
342
|
+
return lines.map((l) => trunc(l, w));
|
|
216
343
|
}
|
|
217
344
|
|
|
218
|
-
|
|
345
|
+
for (const run of d.runs) renderRunBlock(lines, d, run, true);
|
|
346
|
+
lines.push(" Alt+O collapse latest subagent card");
|
|
347
|
+
const max = 48;
|
|
348
|
+
const shown =
|
|
349
|
+
lines.length > max
|
|
350
|
+
? [
|
|
351
|
+
...lines.slice(0, max - 1),
|
|
352
|
+
` … ${lines.length - max + 1} lines hidden`,
|
|
353
|
+
]
|
|
354
|
+
: lines;
|
|
355
|
+
return shown.map((l) => trunc(l, w));
|
|
219
356
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
357
|
+
function progressKey(d: ProgressDetails) {
|
|
358
|
+
return d.runs
|
|
359
|
+
.map((r) => {
|
|
360
|
+
const t = r.tools.at(-1);
|
|
361
|
+
return [
|
|
362
|
+
r.id,
|
|
363
|
+
r.status,
|
|
364
|
+
r.tools.length,
|
|
365
|
+
t?.id ?? "",
|
|
366
|
+
t?.status ?? "",
|
|
367
|
+
r.usage.turns,
|
|
368
|
+
r.usage.input,
|
|
369
|
+
r.usage.output,
|
|
370
|
+
r.usage.cacheRead,
|
|
371
|
+
r.usage.cacheWrite,
|
|
372
|
+
r.usage.ctxTokens,
|
|
373
|
+
r.model ?? "",
|
|
374
|
+
r.thinking ?? "",
|
|
375
|
+
r.errorMessage ?? "",
|
|
376
|
+
].join("~");
|
|
377
|
+
})
|
|
378
|
+
.join("|");
|
|
237
379
|
}
|
|
238
380
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
): PiRunConfig {
|
|
243
|
-
return {
|
|
244
|
-
model: stringValue(input.model) ?? agentConfig.model,
|
|
245
|
-
thinking: normalizeThinking(input.thinking) ?? agentConfig.thinking,
|
|
246
|
-
};
|
|
381
|
+
// ── Utilities ─────────────────────────────────────────────────────────
|
|
382
|
+
function isObj(v: unknown): v is JsonObject {
|
|
383
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
247
384
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return raw
|
|
251
|
-
.trim()
|
|
252
|
-
.replace(/[^A-Za-z0-9._-]+/g, "_")
|
|
253
|
-
.replace(/^[._-]+|[._-]+$/g, "")
|
|
254
|
-
.slice(0, 160);
|
|
385
|
+
function str(v: unknown): string | null {
|
|
386
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
255
387
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return createHash("sha256").update(raw).digest("hex").slice(0, 24);
|
|
388
|
+
function num(v: unknown): number {
|
|
389
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
259
390
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
command: string;
|
|
263
|
-
argsPrefix: string[];
|
|
391
|
+
function hash(s: string) {
|
|
392
|
+
return createHash("sha256").update(s).digest("hex").slice(0, 24);
|
|
264
393
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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;
|
|
394
|
+
function readText(p: string) {
|
|
395
|
+
try {
|
|
396
|
+
return readFileSync(p, "utf-8");
|
|
397
|
+
} catch {
|
|
398
|
+
return "";
|
|
318
399
|
}
|
|
319
400
|
}
|
|
320
|
-
|
|
321
|
-
function isExistingFile(path: string): boolean {
|
|
401
|
+
function exists(p: string) {
|
|
322
402
|
try {
|
|
323
|
-
return statSync(
|
|
403
|
+
return statSync(p).isFile();
|
|
324
404
|
} catch {
|
|
325
405
|
return false;
|
|
326
406
|
}
|
|
327
407
|
}
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
393
|
-
function createProcessContextKey(projectRoot: string): string {
|
|
394
|
-
return `pi_process_${hashValue(
|
|
395
|
-
[projectRoot, process.pid, Date.now(), randomBytes(8).toString("hex")].join(
|
|
396
|
-
":",
|
|
397
|
-
),
|
|
398
|
-
)}`;
|
|
408
|
+
function shellQuote(v: string) {
|
|
409
|
+
return `'${v.replace(/'/g, `'\\''`)}'`;
|
|
399
410
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
callback: (() => string | undefined) | undefined,
|
|
403
|
-
): string | null {
|
|
404
|
-
if (!callback) return null;
|
|
411
|
+
function callStr(cb: (() => string | undefined) | undefined): string | null {
|
|
412
|
+
if (!cb) return null;
|
|
405
413
|
try {
|
|
406
|
-
return
|
|
414
|
+
return str(cb());
|
|
407
415
|
} catch {
|
|
408
416
|
return null;
|
|
409
417
|
}
|
|
410
418
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (value) return value;
|
|
419
|
+
function lookupStr(data: unknown, keys: string[]): string | null {
|
|
420
|
+
if (!isObj(data)) return null;
|
|
421
|
+
for (const k of keys) {
|
|
422
|
+
const v = str(data[k]);
|
|
423
|
+
if (v) return v;
|
|
417
424
|
}
|
|
418
|
-
for (const
|
|
425
|
+
for (const nk of [
|
|
419
426
|
"input",
|
|
420
427
|
"properties",
|
|
421
428
|
"event",
|
|
422
429
|
"hook_input",
|
|
423
430
|
"hookInput",
|
|
424
431
|
]) {
|
|
425
|
-
const nested = data[
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
432
|
+
const nested = data[nk];
|
|
433
|
+
const v = lookupStr(nested, keys);
|
|
434
|
+
if (v) return v;
|
|
428
435
|
}
|
|
429
436
|
return null;
|
|
430
437
|
}
|
|
431
|
-
|
|
432
|
-
|
|
438
|
+
function cmdHasTrellisCtx(cmd: string) {
|
|
439
|
+
const t = cmd.trimStart();
|
|
440
|
+
return (
|
|
441
|
+
/^export\s+TRELLIS_CONTEXT_ID=/.test(t) ||
|
|
442
|
+
/^TRELLIS_CONTEXT_ID=/.test(t) ||
|
|
443
|
+
/^env\s+.*TRELLIS_CONTEXT_ID=/.test(t)
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
function fmtDur(ms: number) {
|
|
447
|
+
if (ms < 1000) return `${ms}ms`;
|
|
448
|
+
const s = Math.floor(ms / 1000);
|
|
449
|
+
if (s < 60) return `${s}s`;
|
|
450
|
+
return `${Math.floor(s / 60)}m${s % 60}s`;
|
|
451
|
+
}
|
|
452
|
+
function fmtNum(n: number) {
|
|
453
|
+
if (!n) return "0";
|
|
454
|
+
if (Math.abs(n) < 1000) return `${n}`;
|
|
455
|
+
if (Math.abs(n) < 1000000) return `${(n / 1000).toFixed(1)}k`;
|
|
456
|
+
return `${(n / 1000000).toFixed(1)}m`;
|
|
457
|
+
}
|
|
458
|
+
function fmtUsage(u: Usage, m?: string) {
|
|
459
|
+
const p: string[] = [];
|
|
460
|
+
if (u.turns) p.push(`${u.turns}t`);
|
|
461
|
+
if (u.input) p.push(`↑${fmtNum(u.input)}`);
|
|
462
|
+
if (u.output) p.push(`↓${fmtNum(u.output)}`);
|
|
463
|
+
if (u.cost) p.push(`$${u.cost.toFixed(3)}`);
|
|
464
|
+
if (u.ctxTokens) p.push(`ctx:${fmtNum(u.ctxTokens)}`);
|
|
465
|
+
if (m) p.push(m);
|
|
466
|
+
return p.join(" ");
|
|
467
|
+
}
|
|
468
|
+
function statusIcon(s: RunStatus) {
|
|
469
|
+
return s === "pending"
|
|
470
|
+
? "○"
|
|
471
|
+
: s === "running"
|
|
472
|
+
? "●"
|
|
473
|
+
: s === "succeeded"
|
|
474
|
+
? "✓"
|
|
475
|
+
: s === "failed"
|
|
476
|
+
? "✗"
|
|
477
|
+
: "⊘";
|
|
478
|
+
}
|
|
479
|
+
function toolIcon(s: ToolStatus) {
|
|
480
|
+
return s === "running" ? "•" : s === "succeeded" ? "✓" : "✗";
|
|
481
|
+
}
|
|
482
|
+
function latest(text: string, n: number) {
|
|
483
|
+
return text
|
|
484
|
+
.split(/\r?\n/)
|
|
485
|
+
.map((l) => l.trimEnd())
|
|
486
|
+
.filter((l) => l.trim())
|
|
487
|
+
.slice(-n);
|
|
488
|
+
}
|
|
489
|
+
function appendTail(cur: string, next: string, max: number) {
|
|
490
|
+
if (!next) return cur;
|
|
491
|
+
const c = cur + next;
|
|
492
|
+
return c.length <= max ? c : c.slice(-max);
|
|
493
|
+
}
|
|
494
|
+
function extractText(content: unknown): string {
|
|
433
495
|
if (typeof content === "string") return content;
|
|
434
496
|
if (!Array.isArray(content)) return "";
|
|
435
|
-
|
|
436
497
|
return content
|
|
437
|
-
.map((
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
? block.text
|
|
441
|
-
: "";
|
|
442
|
-
})
|
|
498
|
+
.map((b) =>
|
|
499
|
+
isObj(b) && b.type === "text" && typeof b.text === "string" ? b.text : "",
|
|
500
|
+
)
|
|
443
501
|
.join("");
|
|
444
502
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const event = JSON.parse(trimmed) as JsonObject;
|
|
455
|
-
const message = isJsonObject(event.message) ? event.message : null;
|
|
456
|
-
if (message?.role !== "assistant") continue;
|
|
457
|
-
|
|
458
|
-
const text = extractTextContent(message.content);
|
|
459
|
-
if (text) finalText = text;
|
|
460
|
-
} catch {
|
|
461
|
-
// Pi can print non-JSON diagnostics around structured output; keep scanning.
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
return finalText || null;
|
|
503
|
+
function extractThinking(content: unknown): string {
|
|
504
|
+
if (!Array.isArray(content)) return "";
|
|
505
|
+
return content
|
|
506
|
+
.map((b) =>
|
|
507
|
+
isObj(b) && b.type === "thinking" && typeof b.thinking === "string"
|
|
508
|
+
? b.thinking
|
|
509
|
+
: "",
|
|
510
|
+
)
|
|
511
|
+
.join("\n");
|
|
466
512
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
513
|
+
function newUsage(): Usage {
|
|
514
|
+
return {
|
|
515
|
+
input: 0,
|
|
516
|
+
output: 0,
|
|
517
|
+
cacheRead: 0,
|
|
518
|
+
cacheWrite: 0,
|
|
519
|
+
cost: 0,
|
|
520
|
+
ctxTokens: 0,
|
|
521
|
+
turns: 0,
|
|
522
|
+
};
|
|
470
523
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return
|
|
524
|
+
function newRun(
|
|
525
|
+
id: string,
|
|
526
|
+
agent: string,
|
|
527
|
+
prompt: string,
|
|
528
|
+
step?: number,
|
|
529
|
+
): RunState {
|
|
530
|
+
return {
|
|
531
|
+
id,
|
|
532
|
+
agent,
|
|
533
|
+
prompt: trunc(prompt.replace(/\s+/g, " ").trim(), 120) || "(empty)",
|
|
534
|
+
step,
|
|
535
|
+
status: "pending",
|
|
536
|
+
finalText: "",
|
|
537
|
+
textTail: "",
|
|
538
|
+
thinkingTail: "",
|
|
539
|
+
stderrTail: "",
|
|
540
|
+
tools: [],
|
|
541
|
+
usage: newUsage(),
|
|
542
|
+
};
|
|
478
543
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
544
|
+
function cloneProgress(d: ProgressDetails): ProgressDetails {
|
|
545
|
+
return {
|
|
546
|
+
...d,
|
|
547
|
+
runs: d.runs.map((r) => ({
|
|
548
|
+
...r,
|
|
549
|
+
tools: r.tools.map((t) => ({ ...t })),
|
|
550
|
+
usage: { ...r.usage },
|
|
551
|
+
})),
|
|
552
|
+
};
|
|
484
553
|
}
|
|
485
554
|
|
|
486
|
-
function
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return false;
|
|
492
|
-
}
|
|
555
|
+
function oneLine(v: unknown, max = 80) {
|
|
556
|
+
return String(v || "...")
|
|
557
|
+
.replace(/\s+/g, " ")
|
|
558
|
+
.trim()
|
|
559
|
+
.slice(0, max);
|
|
493
560
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
561
|
+
function summarizeToolArgs(name: string, args: unknown): string {
|
|
562
|
+
const a = isObj(args) ? args : {};
|
|
563
|
+
const summary: JsonObject = {};
|
|
564
|
+
if ("path" in a) summary.path = oneLine(a.path, 240);
|
|
565
|
+
if ("file_path" in a) summary.file_path = oneLine(a.file_path, 240);
|
|
566
|
+
if ("command" in a) summary.command = oneLine(a.command, 240);
|
|
567
|
+
if ("pattern" in a) summary.pattern = oneLine(a.pattern, 120);
|
|
568
|
+
if ("limit" in a) summary.limit = a.limit;
|
|
569
|
+
if ("offset" in a) summary.offset = a.offset;
|
|
570
|
+
if (name === "edit" && Array.isArray(a.edits))
|
|
571
|
+
summary.edits = `${a.edits.length} edit(s)`;
|
|
572
|
+
if (name === "write" && "content" in a)
|
|
573
|
+
summary.content = `<${String(a.content ?? "").length} chars>`;
|
|
574
|
+
const json = JSON.stringify(
|
|
575
|
+
Object.keys(summary).length ? summary : { tool: name },
|
|
576
|
+
);
|
|
577
|
+
return json.length <= MAX_TOOL_ARG_CHARS
|
|
578
|
+
? json
|
|
579
|
+
: json.slice(0, MAX_TOOL_ARG_CHARS);
|
|
507
580
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
):
|
|
513
|
-
|
|
514
|
-
if (
|
|
515
|
-
|
|
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;
|
|
581
|
+
function toolBrief(t: ToolTrace): string {
|
|
582
|
+
const a = toolArgs(t);
|
|
583
|
+
if (t.name === "read") return `read: ${oneLine(a.path || a.file_path, 80)}`;
|
|
584
|
+
if (t.name === "bash") return `bash: ${oneLine(a.command, 60)}`;
|
|
585
|
+
if (t.name === "write") return `write: ${oneLine(a.path || a.file_path, 80)}`;
|
|
586
|
+
if (t.name === "edit") return `edit: ${oneLine(a.path || a.file_path, 80)}`;
|
|
587
|
+
if (t.name === "grep") return `grep: ${oneLine(a.pattern, 50)}`;
|
|
588
|
+
if (t.name === "find") return `find: ${oneLine(a.pattern || "*", 50)}`;
|
|
589
|
+
return oneLine(t.name, 50);
|
|
522
590
|
}
|
|
523
591
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const override = stringValue(process.env.TRELLIS_CONTEXT_ID);
|
|
530
|
-
if (override) return sanitizeKey(override) || hashValue(override);
|
|
592
|
+
// ── Pi CLI path resolution ────────────────────────────────────────────
|
|
593
|
+
const PI_CLI_SEGMENTS = [
|
|
594
|
+
["node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js"],
|
|
595
|
+
["node_modules", "@mariozechner", "pi-coding-agent", "dist", "cli.js"],
|
|
596
|
+
];
|
|
531
597
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
598
|
+
function resolvePiCli(): { command: string; args: string[] } {
|
|
599
|
+
const envCli = str(process.env.TRELLIS_PI_CLI_JS);
|
|
600
|
+
if (envCli) {
|
|
601
|
+
const p = resolve(envCli);
|
|
602
|
+
if (!exists(p)) throw new Error(`TRELLIS_PI_CLI_JS missing: ${p}`);
|
|
603
|
+
return { command: process.execPath, args: [p] };
|
|
604
|
+
}
|
|
605
|
+
const candidates: string[] = [];
|
|
606
|
+
for (const arg of process.argv)
|
|
607
|
+
if (/pi-coding-agent[\\/]dist[\\/]cli\.js$/i.test(arg))
|
|
608
|
+
candidates.push(resolve(arg));
|
|
609
|
+
const prefix =
|
|
610
|
+
str(process.env.npm_config_prefix) ?? str(process.env.NPM_CONFIG_PREFIX);
|
|
611
|
+
const appData = str(process.env.APPDATA);
|
|
612
|
+
const pathVal = process.env.PATH ?? process.env.Path ?? "";
|
|
613
|
+
const addBase = (base: string) => {
|
|
614
|
+
for (const seg of PI_CLI_SEGMENTS) candidates.push(join(base, ...seg));
|
|
615
|
+
};
|
|
616
|
+
if (prefix) {
|
|
617
|
+
addBase(prefix);
|
|
618
|
+
addBase(join(prefix, "lib"));
|
|
619
|
+
}
|
|
620
|
+
if (appData) addBase(join(appData, "npm"));
|
|
621
|
+
for (const entry of pathVal.split(delimiter)) {
|
|
622
|
+
const e = entry.trim();
|
|
623
|
+
if (!e) continue;
|
|
624
|
+
addBase(e);
|
|
625
|
+
addBase(dirname(e));
|
|
626
|
+
addBase(join(dirname(e), "lib"));
|
|
627
|
+
}
|
|
628
|
+
for (const c of [...new Set(candidates)])
|
|
629
|
+
if (exists(c)) return { command: process.execPath, args: [c] };
|
|
630
|
+
return { command: "pi", args: [] };
|
|
631
|
+
}
|
|
538
632
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
633
|
+
function resolveRunCfg(
|
|
634
|
+
input: SubagentInput,
|
|
635
|
+
agentCfg: AgentConfig,
|
|
636
|
+
inheritedThinking?: string,
|
|
637
|
+
): PiRunConfig {
|
|
638
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
639
|
+
const normalize = (v: unknown): string | undefined => {
|
|
640
|
+
const s = typeof v === "string" && v.trim() ? v.trim().toLowerCase() : "";
|
|
641
|
+
return THINKING_LEVELS.includes(s) ? s : undefined;
|
|
642
|
+
};
|
|
643
|
+
const suffixRe = /:(off|minimal|low|medium|high|xhigh)$/i;
|
|
644
|
+
const inputModel = str(input.model);
|
|
645
|
+
const agentModel = agentCfg.model;
|
|
646
|
+
const rawModel = inputModel ?? agentModel;
|
|
647
|
+
const inputSuffixThinking = normalize(inputModel?.match(suffixRe)?.[1]);
|
|
648
|
+
const agentSuffixThinking = normalize(agentModel?.match(suffixRe)?.[1]);
|
|
649
|
+
const baseModel = rawModel?.replace(suffixRe, "");
|
|
650
|
+
const thinking =
|
|
651
|
+
normalize(input.thinking) ??
|
|
652
|
+
inputSuffixThinking ??
|
|
653
|
+
normalize(agentCfg.thinking) ??
|
|
654
|
+
agentSuffixThinking ??
|
|
655
|
+
normalize(inheritedThinking);
|
|
656
|
+
if (baseModel && thinking && thinking !== "off")
|
|
657
|
+
return { model: `${baseModel}:${thinking}`, thinking };
|
|
658
|
+
return { model: baseModel || rawModel, thinking };
|
|
659
|
+
}
|
|
543
660
|
|
|
544
|
-
|
|
661
|
+
function buildPiArgs(cfg: PiRunConfig): string[] {
|
|
662
|
+
const args = ["--mode", "json", "-p", "--no-session"];
|
|
663
|
+
if (cfg.model)
|
|
664
|
+
args.push(
|
|
665
|
+
"--model",
|
|
666
|
+
cfg.thinking && cfg.thinking !== "off" && !cfg.model.includes(":")
|
|
667
|
+
? `${cfg.model}:${cfg.thinking}`
|
|
668
|
+
: cfg.model,
|
|
669
|
+
);
|
|
670
|
+
else if (cfg.thinking && cfg.thinking !== "off")
|
|
671
|
+
args.push("--thinking", cfg.thinking);
|
|
672
|
+
return args;
|
|
545
673
|
}
|
|
546
674
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
675
|
+
// ── BoundedBufferCollector ─────────────────────────────────────────────
|
|
676
|
+
class BBC {
|
|
677
|
+
private c: Buffer[] = [];
|
|
678
|
+
private len = 0;
|
|
679
|
+
private trunc = 0;
|
|
680
|
+
constructor(private max: number) {}
|
|
681
|
+
append(b: Buffer) {
|
|
682
|
+
if (b.length >= this.max) {
|
|
683
|
+
this.trunc += this.len + b.length - this.max;
|
|
684
|
+
this.c = [b.subarray(b.length - this.max)];
|
|
685
|
+
this.len = this.max;
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
this.c.push(b);
|
|
689
|
+
this.len += b.length;
|
|
690
|
+
while (this.len > this.max) {
|
|
691
|
+
const f = this.c[0]!;
|
|
692
|
+
if (f.length <= this.len - this.max) {
|
|
693
|
+
this.c.shift();
|
|
694
|
+
this.len -= f.length;
|
|
695
|
+
this.trunc += f.length;
|
|
696
|
+
} else {
|
|
697
|
+
const ov = this.len - this.max;
|
|
698
|
+
this.c[0] = f.subarray(ov);
|
|
699
|
+
this.len -= ov;
|
|
700
|
+
this.trunc += ov;
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
571
703
|
}
|
|
572
704
|
}
|
|
573
|
-
|
|
574
|
-
|
|
705
|
+
toString() {
|
|
706
|
+
const body = Buffer.concat(this.c, this.len).toString("utf-8");
|
|
707
|
+
return this.trunc ? `[${this.trunc} bytes truncated]\n${body}` : body;
|
|
708
|
+
}
|
|
575
709
|
}
|
|
576
710
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
)
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
711
|
+
// ── Trellis Context ────────────────────────────────────────────────────
|
|
712
|
+
function findRoot(start: string): string {
|
|
713
|
+
let c = resolve(start);
|
|
714
|
+
while (true) {
|
|
715
|
+
if (existsSync(join(c, ".trellis")) || existsSync(join(c, ".pi"))) return c;
|
|
716
|
+
const p = dirname(c);
|
|
717
|
+
if (p === c) return resolve(start);
|
|
718
|
+
c = p;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function splitFM(c: string) {
|
|
722
|
+
const m = c.replace(/^\uFEFF/, "").match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
723
|
+
return m
|
|
724
|
+
? { fm: m[1] ?? "", body: c.slice(m[0].length) }
|
|
725
|
+
: { fm: "", body: c };
|
|
726
|
+
}
|
|
727
|
+
function stripFM(c: string) {
|
|
728
|
+
return splitFM(c).body.trimStart();
|
|
729
|
+
}
|
|
730
|
+
function parseAgentFM(c: string): AgentConfig {
|
|
731
|
+
const cfg: AgentConfig = { fallbackModels: [] };
|
|
732
|
+
const { fm } = splitFM(c);
|
|
733
|
+
const lines = fm.split(/\r?\n/);
|
|
734
|
+
for (let i = 0; i < lines.length; i++) {
|
|
735
|
+
const m = (lines[i] ?? "").match(/^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
|
736
|
+
if (!m) continue;
|
|
737
|
+
const k = m[1] ?? "",
|
|
738
|
+
v = m[2] ?? "";
|
|
739
|
+
if (k === "model")
|
|
740
|
+
cfg.model = v.trim().replace(/^["']|["']$/g, "") || undefined;
|
|
741
|
+
else if (k === "thinking")
|
|
742
|
+
cfg.thinking = (v.trim().replace(/^["']|["']$/g, "") || undefined) as
|
|
743
|
+
| string
|
|
744
|
+
| undefined;
|
|
745
|
+
else if (k === "fallbackModels" || k === "fallback_models") {
|
|
746
|
+
if (v.trim()) {
|
|
747
|
+
cfg.fallbackModels = v
|
|
748
|
+
.trim()
|
|
749
|
+
.replace(/^\[|\]$/g, "")
|
|
750
|
+
.split(",")
|
|
751
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
|
752
|
+
.filter(Boolean);
|
|
753
|
+
} else {
|
|
754
|
+
i++;
|
|
755
|
+
while (i < lines.length && /^\s+-\s/.test(lines[i] ?? "")) {
|
|
756
|
+
const item = (lines[i] ?? "")
|
|
757
|
+
.trim()
|
|
758
|
+
.replace(/^-\s+/, "")
|
|
759
|
+
.replace(/^["']|["']$/g, "");
|
|
760
|
+
if (item) cfg.fallbackModels.push(item);
|
|
761
|
+
i++;
|
|
762
|
+
}
|
|
763
|
+
i--;
|
|
596
764
|
}
|
|
597
|
-
} catch {
|
|
598
|
-
// Seed rows and malformed lines must not block sub-agent startup.
|
|
599
765
|
}
|
|
600
766
|
}
|
|
601
|
-
|
|
602
|
-
return chunks.join("\n\n---\n\n");
|
|
767
|
+
return cfg;
|
|
603
768
|
}
|
|
604
769
|
|
|
605
|
-
function
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
if (
|
|
614
|
-
return
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const jsonlName = TRELLIS_AGENT_JSONL[agent] ?? "";
|
|
621
|
-
const specContext = jsonlName
|
|
622
|
-
? readJsonlFiles(projectRoot, taskDir, jsonlName)
|
|
623
|
-
: "";
|
|
624
|
-
|
|
625
|
-
return [
|
|
626
|
-
"## Trellis Task Context",
|
|
627
|
-
`Task directory: ${taskDir}`,
|
|
628
|
-
"",
|
|
629
|
-
"### prd.md",
|
|
630
|
-
prd || "(missing)",
|
|
631
|
-
design ? "\n### design.md\n" + design : "",
|
|
632
|
-
implementPlan ? "\n### implement.md\n" + implementPlan : "",
|
|
633
|
-
specContext ? "\n### Curated Spec / Research Context\n" + specContext : "",
|
|
634
|
-
].join("\n");
|
|
770
|
+
function contextKey(input?: unknown, ctx?: PiExtensionContext): string | null {
|
|
771
|
+
const ov = str(process.env.TRELLIS_CONTEXT_ID);
|
|
772
|
+
if (ov) return ov.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 160) || hash(ov);
|
|
773
|
+
const sessionId =
|
|
774
|
+
callStr(ctx?.sessionManager?.getSessionId) ??
|
|
775
|
+
str(process.env.PI_SESSION_ID) ??
|
|
776
|
+
str(process.env.PI_SESSIONID) ??
|
|
777
|
+
lookupStr(input, ["session_id", "sessionId", "sessionID"]);
|
|
778
|
+
if (sessionId)
|
|
779
|
+
return `pi_${sessionId.replace(/[^A-Za-z0-9._-]+/g, "_") || hash(sessionId)}`;
|
|
780
|
+
const transcriptPath =
|
|
781
|
+
callStr(ctx?.sessionManager?.getSessionFile) ??
|
|
782
|
+
lookupStr(input, ["transcript_path", "transcriptPath", "transcript"]);
|
|
783
|
+
if (transcriptPath) return `pi_transcript_${hash(transcriptPath)}`;
|
|
784
|
+
return null;
|
|
635
785
|
}
|
|
636
786
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const result: Record<string, string> = {};
|
|
656
|
-
for (const match of workflow.matchAll(WORKFLOW_STATE_TAG_RE)) {
|
|
657
|
-
const status = match[1] ?? "";
|
|
658
|
-
const body = (match[2] ?? "").trim();
|
|
659
|
-
if (status && body) result[status] = body;
|
|
787
|
+
function readTaskDir(root: string, key: string | null): string | null {
|
|
788
|
+
if (!key) return null;
|
|
789
|
+
try {
|
|
790
|
+
const ctx = JSON.parse(
|
|
791
|
+
readText(join(root, ".trellis", ".runtime", "sessions", `${key}.json`)),
|
|
792
|
+
) as JsonObject;
|
|
793
|
+
let ref = str(ctx.current_task);
|
|
794
|
+
if (!ref) return null;
|
|
795
|
+
ref = ref;
|
|
796
|
+
ref = ref.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
797
|
+
if (ref.startsWith("tasks/")) ref = `.trellis/${ref}`;
|
|
798
|
+
return ref.startsWith(".trellis/")
|
|
799
|
+
? join(root, ref)
|
|
800
|
+
: isAbsolute(ref)
|
|
801
|
+
? ref
|
|
802
|
+
: join(root, ".trellis", "tasks", ref);
|
|
803
|
+
} catch {
|
|
804
|
+
return null;
|
|
660
805
|
}
|
|
661
|
-
return result;
|
|
662
806
|
}
|
|
663
|
-
|
|
664
|
-
function readActiveTaskStatus(
|
|
665
|
-
projectRoot: string,
|
|
666
|
-
taskDir: string,
|
|
667
|
-
): { taskId: string; status: string } | null {
|
|
807
|
+
function sessionHasTask(root: string, key: string): boolean {
|
|
668
808
|
try {
|
|
669
|
-
const
|
|
670
|
-
readText(join(
|
|
809
|
+
const ctx = JSON.parse(
|
|
810
|
+
readText(join(root, ".trellis", ".runtime", "sessions", `${key}.json`)),
|
|
671
811
|
) as JsonObject;
|
|
672
|
-
|
|
673
|
-
if (!status) return null;
|
|
674
|
-
const id = stringValue(data.id) ?? taskDir.split(/[\\/]/).pop() ?? "";
|
|
675
|
-
return { taskId: id, status };
|
|
812
|
+
return !!str(ctx.current_task);
|
|
676
813
|
} catch {
|
|
677
|
-
return
|
|
814
|
+
return false;
|
|
678
815
|
}
|
|
679
816
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
if (!taskDir) {
|
|
695
|
-
header = "Status: no_task";
|
|
696
|
-
lookupKey = "no_task";
|
|
697
|
-
} else {
|
|
698
|
-
const info = readActiveTaskStatus(projectRoot, taskDir);
|
|
699
|
-
if (!info) {
|
|
700
|
-
header = "Status: no_task";
|
|
701
|
-
lookupKey = "no_task";
|
|
702
|
-
} else {
|
|
703
|
-
header = `Task: ${info.taskId} (${info.status})`;
|
|
704
|
-
lookupKey = info.status;
|
|
705
|
-
}
|
|
817
|
+
function adoptKey(root: string, key: string): string {
|
|
818
|
+
if (sessionHasTask(root, key)) return key;
|
|
819
|
+
try {
|
|
820
|
+
const dir = join(root, ".trellis", ".runtime", "sessions");
|
|
821
|
+
const keys = readdirSync(dir)
|
|
822
|
+
.filter(
|
|
823
|
+
(f) => f.endsWith(".json") && sessionHasTask(root, f.slice(0, -5)),
|
|
824
|
+
)
|
|
825
|
+
.map((f) => f.slice(0, -5));
|
|
826
|
+
const proc = keys.filter((k) => k.startsWith("pi_process_"));
|
|
827
|
+
const cands = proc.length ? proc : keys;
|
|
828
|
+
return cands.length === 1 ? cands[0]! : key;
|
|
829
|
+
} catch {
|
|
830
|
+
return key;
|
|
706
831
|
}
|
|
707
|
-
const body = templates[lookupKey] ?? "Refer to workflow.md for current step.";
|
|
708
|
-
return `<workflow-state>\n${header}\n${body}\n</workflow-state>`;
|
|
709
832
|
}
|
|
710
833
|
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
834
|
+
// ── Workflow State Breadcrumb ─────────────────────────────────────────
|
|
835
|
+
const WF_RE =
|
|
836
|
+
/\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
|
|
837
|
+
function workflowBreadcrumb(root: string, key: string | null): string {
|
|
838
|
+
const wf = readText(join(root, ".trellis", "workflow.md"));
|
|
839
|
+
if (!wf) return "";
|
|
840
|
+
const templates: Record<string, string> = {};
|
|
841
|
+
for (const m of wf.matchAll(WF_RE)) {
|
|
842
|
+
const s = m[1] ?? "",
|
|
843
|
+
b = (m[2] ?? "").trim();
|
|
844
|
+
if (s && b) templates[s] = b;
|
|
845
|
+
}
|
|
846
|
+
const dir = readTaskDir(root, key);
|
|
847
|
+
let header = "Status: no_task",
|
|
848
|
+
lookup = "no_task";
|
|
849
|
+
if (dir) {
|
|
850
|
+
try {
|
|
851
|
+
const d = JSON.parse(readText(join(dir, "task.json"))) as JsonObject;
|
|
852
|
+
const status = str(d.status) ?? "";
|
|
853
|
+
const id = str(d.id) ?? dir.split(/[\\/]/).pop() ?? "";
|
|
854
|
+
if (status) {
|
|
855
|
+
header = `Task: ${id} (${status})`;
|
|
856
|
+
lookup = status;
|
|
857
|
+
}
|
|
858
|
+
} catch {}
|
|
859
|
+
}
|
|
860
|
+
const body = templates[lookup] ?? "Refer to workflow.md for current step.";
|
|
861
|
+
return `<workflow-state>\n${header}\n${body}\n</workflow-state>`;
|
|
726
862
|
}
|
|
727
863
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
)
|
|
732
|
-
const script = join(projectRoot, ".trellis", "scripts", "get_context.py");
|
|
733
|
-
if (!isExistingFile(script)) return "";
|
|
864
|
+
// ── Session Overview ───────────────────────────────────────────────────
|
|
865
|
+
function sessionOverview(root: string, key: string | null): string {
|
|
866
|
+
const script = join(root, ".trellis", "scripts", "get_context.py");
|
|
867
|
+
if (!exists(script)) return "";
|
|
734
868
|
try {
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
: process.env,
|
|
869
|
+
const py = process.platform === "win32" ? "python" : "python3";
|
|
870
|
+
const result = spawnSync(py, [script], {
|
|
871
|
+
cwd: root,
|
|
872
|
+
env: key ? { ...process.env, TRELLIS_CONTEXT_ID: key } : process.env,
|
|
740
873
|
encoding: "utf-8",
|
|
741
874
|
timeout: SESSION_OVERVIEW_TIMEOUT_MS,
|
|
742
875
|
windowsHide: true,
|
|
743
876
|
});
|
|
744
877
|
if (result.status !== 0) return "";
|
|
745
878
|
const stdout = (result.stdout ?? "").trim();
|
|
746
|
-
|
|
747
|
-
return `<session-overview>\n${stdout}\n</session-overview>`;
|
|
879
|
+
return stdout ? `<session-overview>\n${stdout}\n</session-overview>` : "";
|
|
748
880
|
} catch {
|
|
749
881
|
return "";
|
|
750
882
|
}
|
|
751
883
|
}
|
|
752
884
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
885
|
+
function buildContext(root: string, agent: string, key: string | null): string {
|
|
886
|
+
const dir = readTaskDir(root, key);
|
|
887
|
+
if (!dir)
|
|
888
|
+
return "No active Trellis task found. Read .trellis/ before proceeding.";
|
|
889
|
+
const prd = readText(join(dir, "prd.md"));
|
|
890
|
+
const design = readText(join(dir, "design.md"));
|
|
891
|
+
const impl = readText(join(dir, "implement.md"));
|
|
892
|
+
const jsonlName = TRELLIS_AGENT_JSONL[agent] ?? "";
|
|
893
|
+
let spec = "";
|
|
894
|
+
if (jsonlName) {
|
|
895
|
+
const chunks: string[] = [];
|
|
896
|
+
for (const line of readText(join(dir, jsonlName)).split(/\r?\n/)) {
|
|
897
|
+
const t = line.trim();
|
|
898
|
+
if (!t) continue;
|
|
899
|
+
try {
|
|
900
|
+
const r = JSON.parse(t) as JsonObject;
|
|
901
|
+
const f = typeof r.file === "string" ? r.file : "";
|
|
902
|
+
if (f) {
|
|
903
|
+
const c = readText(join(root, f));
|
|
904
|
+
if (c) chunks.push(`## ${f}\n\n${c}`);
|
|
905
|
+
}
|
|
906
|
+
} catch {}
|
|
773
907
|
}
|
|
774
|
-
|
|
775
|
-
this.sessionOverview = buildSessionOverview(projectRoot, contextKey);
|
|
776
|
-
this.key = contextKey;
|
|
777
|
-
this.timestamp = now;
|
|
778
|
-
return {
|
|
779
|
-
workflowState: this.workflowState,
|
|
780
|
-
sessionOverview: this.sessionOverview,
|
|
781
|
-
};
|
|
908
|
+
spec = chunks.join("\n\n---\n\n");
|
|
782
909
|
}
|
|
910
|
+
return [
|
|
911
|
+
`## Trellis Task Context`,
|
|
912
|
+
`Task directory: ${dir}`,
|
|
913
|
+
"",
|
|
914
|
+
"### prd.md",
|
|
915
|
+
prd || "(missing)",
|
|
916
|
+
design ? "\n### design.md\n" + design : "",
|
|
917
|
+
impl ? "\n### implement.md\n" + impl : "",
|
|
918
|
+
spec ? "\n### Curated Spec / Research Context\n" + spec : "",
|
|
919
|
+
].join("\n");
|
|
783
920
|
}
|
|
784
921
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
// trellis/workflow.md so the AI sees the same `Active task: <path>` rule
|
|
789
|
-
// whether it reads workflow.md, the per-turn breadcrumb, or the tool prompt.
|
|
790
|
-
// ---------------------------------------------------------------------------
|
|
791
|
-
|
|
792
|
-
const SUBAGENT_DISPATCH_PROTOCOL = `Sub-agent dispatch protocol (Trellis): your dispatch prompt MUST start with one line "Active task: <task path from \`task.py current\`>" before any other instructions. No exceptions. On class-2 platforms (codex / copilot / gemini / qoder) the sub-agent depends on this line because there is no hook to inject task context. On class-1 platforms (claude / cursor / opencode / kiro / codebuddy / droid) and on Pi, the line is the canonical fallback when hook/extension injection misses. trellis-research uses the line to know which {task_dir}/research/ to write into.
|
|
793
|
-
|
|
794
|
-
Wrong: prompt: "implement the new feature"
|
|
795
|
-
Correct: prompt: "Active task: .trellis/tasks/05-09-pi-workflow-state-injection\\n\\nImplement the new feature ..."`;
|
|
796
|
-
|
|
797
|
-
function normalizeAgentName(agent: string): string {
|
|
798
|
-
return agent.startsWith("trellis-") ? agent : `trellis-${agent}`;
|
|
922
|
+
function normalizeAgent(agent: string | undefined): string {
|
|
923
|
+
const name = agent ?? "trellis-implement";
|
|
924
|
+
return name.startsWith("trellis-") ? name : `trellis-${name}`;
|
|
799
925
|
}
|
|
800
926
|
|
|
801
|
-
function
|
|
802
|
-
|
|
803
|
-
agent: string,
|
|
804
|
-
): AgentDefinition {
|
|
805
|
-
const normalized = agent.startsWith("trellis-") ? agent : `trellis-${agent}`;
|
|
806
|
-
const raw = readText(join(projectRoot, ".pi", "agents", `${normalized}.md`));
|
|
807
|
-
return {
|
|
808
|
-
content: stripMarkdownFrontmatter(raw),
|
|
809
|
-
config: parseAgentConfig(raw),
|
|
810
|
-
};
|
|
927
|
+
function isTrellisAgent(root: string, agent: string): boolean {
|
|
928
|
+
return existsSync(join(root, ".pi", "agents", `${agent}.md`));
|
|
811
929
|
}
|
|
812
930
|
|
|
813
|
-
function
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
);
|
|
931
|
+
function buildPrompt(
|
|
932
|
+
root: string,
|
|
933
|
+
input: SubagentInput,
|
|
934
|
+
key: string | null,
|
|
935
|
+
): string {
|
|
936
|
+
const agent = normalizeAgent(input.agent);
|
|
937
|
+
const raw = readText(join(root, ".pi", "agents", `${agent}.md`));
|
|
938
|
+
const def = stripFM(raw);
|
|
939
|
+
const ctx = buildContext(root, agent, key);
|
|
940
|
+
return [
|
|
941
|
+
"## Trellis Agent Definition",
|
|
942
|
+
def || "(missing)",
|
|
943
|
+
"",
|
|
944
|
+
ctx,
|
|
945
|
+
"",
|
|
946
|
+
"## Delegated Task",
|
|
947
|
+
input.prompt ?? "",
|
|
948
|
+
].join("\n");
|
|
820
949
|
}
|
|
821
950
|
|
|
822
|
-
|
|
823
|
-
|
|
951
|
+
// ── Event parsing ─────────────────────────────────────────────────────
|
|
952
|
+
function parseJsonEvent(line: string): JsonObject | null {
|
|
953
|
+
const t = line.trim();
|
|
954
|
+
if (!t) return null;
|
|
955
|
+
const i = t.indexOf("{");
|
|
956
|
+
if (i < 0) return null;
|
|
957
|
+
try {
|
|
958
|
+
const p = JSON.parse(t.slice(i));
|
|
959
|
+
return isObj(p) ? p : null;
|
|
960
|
+
} catch {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
824
963
|
}
|
|
825
964
|
|
|
826
|
-
function
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
)
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return
|
|
965
|
+
function applyEvent(r: RunState, evt: JsonObject): boolean {
|
|
966
|
+
const type = typeof evt.type === "string" ? evt.type : "";
|
|
967
|
+
if (!type) return false;
|
|
968
|
+
if (type === "agent_start" || type === "turn_start") {
|
|
969
|
+
r.status = "running";
|
|
970
|
+
r.startedAt ??= Date.now();
|
|
971
|
+
return true;
|
|
833
972
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
973
|
+
if (type === "message_update") {
|
|
974
|
+
const ae = isObj(evt.assistantMessageEvent)
|
|
975
|
+
? evt.assistantMessageEvent
|
|
976
|
+
: null;
|
|
977
|
+
if (!ae || typeof ae.delta !== "string") return false;
|
|
978
|
+
if (ae.type === "thinking_delta") {
|
|
979
|
+
r.thinkingTail = appendTail(r.thinkingTail, ae.delta, MAX_TAIL);
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
if (ae.type === "text_delta") {
|
|
983
|
+
r.textTail = appendTail(r.textTail, ae.delta, MAX_TAIL);
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
837
986
|
return false;
|
|
838
987
|
}
|
|
839
|
-
if (
|
|
840
|
-
|
|
988
|
+
if (type === "message_end" && isObj(evt.message)) {
|
|
989
|
+
const msg = evt.message;
|
|
990
|
+
if (msg.role !== "assistant") return false;
|
|
991
|
+
r.usage.turns += 1;
|
|
992
|
+
const u = isObj(msg.usage) ? msg.usage : null;
|
|
993
|
+
const cost = isObj(u?.cost) ? u.cost : null;
|
|
994
|
+
r.usage.input += num(u?.input);
|
|
995
|
+
r.usage.output += num(u?.output);
|
|
996
|
+
r.usage.cacheRead += num(u?.cacheRead);
|
|
997
|
+
r.usage.cacheWrite += num(u?.cacheWrite);
|
|
998
|
+
r.usage.cost += num(cost?.total);
|
|
999
|
+
r.usage.ctxTokens = num(u?.totalTokens);
|
|
1000
|
+
const thinking = extractThinking(msg.content);
|
|
1001
|
+
if (thinking) r.thinkingTail = appendTail("", thinking, MAX_TAIL);
|
|
1002
|
+
const text = extractText(msg.content);
|
|
1003
|
+
if (text) {
|
|
1004
|
+
r.finalText = text;
|
|
1005
|
+
r.textTail = appendTail("", text, MAX_TAIL);
|
|
1006
|
+
}
|
|
1007
|
+
if (typeof msg.model === "string") {
|
|
1008
|
+
const parsed = splitModelThinking(msg.model, r.thinking);
|
|
1009
|
+
r.model = parsed.model;
|
|
1010
|
+
r.thinking = parsed.thinking;
|
|
1011
|
+
}
|
|
1012
|
+
if (typeof msg.errorMessage === "string") r.errorMessage = msg.errorMessage;
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
if (type === "tool_execution_start") {
|
|
1016
|
+
const id =
|
|
1017
|
+
typeof evt.toolCallId === "string"
|
|
1018
|
+
? evt.toolCallId
|
|
1019
|
+
: hash(`${Date.now()}`);
|
|
1020
|
+
const name = typeof evt.toolName === "string" ? evt.toolName : "tool";
|
|
1021
|
+
const args = summarizeToolArgs(name, evt.args);
|
|
1022
|
+
const existing = r.tools.findIndex((t) => t.id === id);
|
|
1023
|
+
if (existing >= 0)
|
|
1024
|
+
r.tools[existing] = { ...r.tools[existing]!, args, status: "running" };
|
|
1025
|
+
else
|
|
1026
|
+
r.tools.push({
|
|
1027
|
+
id,
|
|
1028
|
+
name,
|
|
1029
|
+
args,
|
|
1030
|
+
status: "running",
|
|
1031
|
+
startedAt: Date.now(),
|
|
1032
|
+
});
|
|
1033
|
+
if (r.tools.length > MAX_TOOLS)
|
|
1034
|
+
r.tools.splice(0, r.tools.length - MAX_TOOLS);
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
if (type === "tool_execution_end") {
|
|
1038
|
+
const id = typeof evt.toolCallId === "string" ? evt.toolCallId : "";
|
|
1039
|
+
const idx = r.tools.findIndex((t) => t.id === id);
|
|
1040
|
+
if (idx >= 0)
|
|
1041
|
+
r.tools[idx] = {
|
|
1042
|
+
...r.tools[idx]!,
|
|
1043
|
+
status: evt.isError ? "failed" : "succeeded",
|
|
1044
|
+
finishedAt: Date.now(),
|
|
1045
|
+
};
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
if (type === "agent_end") {
|
|
1049
|
+
r.finishedAt = Date.now();
|
|
1050
|
+
if (r.status === "running" || r.status === "pending")
|
|
1051
|
+
r.status = "succeeded";
|
|
1052
|
+
return true;
|
|
841
1053
|
}
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
842
1056
|
|
|
843
|
-
|
|
844
|
-
return
|
|
1057
|
+
function finalize(r: RunState, fallback: string): string {
|
|
1058
|
+
return r.finalText || fallback.trim() || r.stderrTail.trim();
|
|
1059
|
+
}
|
|
1060
|
+
function formatPiOutput(stdout: string, stderr: string): string {
|
|
1061
|
+
let ft = "";
|
|
1062
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
1063
|
+
const t = line.trim();
|
|
1064
|
+
if (!t) continue;
|
|
1065
|
+
try {
|
|
1066
|
+
const evt = JSON.parse(t) as JsonObject;
|
|
1067
|
+
const msg = isObj(evt.message) ? evt.message : null;
|
|
1068
|
+
if (msg?.role === "assistant") {
|
|
1069
|
+
const txt = extractText(msg.content);
|
|
1070
|
+
if (txt) ft = txt;
|
|
1071
|
+
}
|
|
1072
|
+
} catch {}
|
|
1073
|
+
}
|
|
1074
|
+
return ft || stdout || stderr;
|
|
845
1075
|
}
|
|
846
1076
|
|
|
1077
|
+
// ── runPi: subprocess execution + event processing ───────────────────
|
|
847
1078
|
function runPi(
|
|
848
|
-
|
|
1079
|
+
root: string,
|
|
849
1080
|
prompt: string,
|
|
850
|
-
|
|
851
|
-
|
|
1081
|
+
cfg: PiRunConfig,
|
|
1082
|
+
state: RunState,
|
|
1083
|
+
emit: () => void,
|
|
1084
|
+
key?: string | null,
|
|
852
1085
|
signal?: AbortSignal,
|
|
853
|
-
): Promise<string> {
|
|
854
|
-
return new Promise((
|
|
1086
|
+
): Promise<{ output: string; failed: boolean }> {
|
|
1087
|
+
return new Promise((resolve) => {
|
|
855
1088
|
if (signal?.aborted) {
|
|
856
|
-
|
|
1089
|
+
state.status = "cancelled";
|
|
1090
|
+
state.errorMessage = "cancelled";
|
|
1091
|
+
state.finishedAt = Date.now();
|
|
1092
|
+
emit();
|
|
1093
|
+
resolve({ output: "cancelled", failed: true });
|
|
857
1094
|
return;
|
|
858
1095
|
}
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
env: contextKey
|
|
875
|
-
? { ...process.env, TRELLIS_CONTEXT_ID: contextKey }
|
|
876
|
-
: process.env,
|
|
877
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
878
|
-
windowsHide: true,
|
|
879
|
-
},
|
|
880
|
-
);
|
|
881
|
-
|
|
882
|
-
const stdout = new BoundedBufferCollector(MAX_SUBAGENT_STDOUT_BYTES);
|
|
883
|
-
const stderr = new BoundedBufferCollector(MAX_SUBAGENT_STDERR_BYTES);
|
|
1096
|
+
const inv = resolvePiCli();
|
|
1097
|
+
const childEnv = {
|
|
1098
|
+
...process.env,
|
|
1099
|
+
TRELLIS_SUBAGENT_CHILD: "1",
|
|
1100
|
+
...(key ? { TRELLIS_CONTEXT_ID: key } : {}),
|
|
1101
|
+
};
|
|
1102
|
+
const cli = spawn(inv.command, [...inv.args, ...buildPiArgs(cfg)], {
|
|
1103
|
+
cwd: root,
|
|
1104
|
+
env: childEnv,
|
|
1105
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1106
|
+
windowsHide: true,
|
|
1107
|
+
});
|
|
1108
|
+
const stdout = new BBC(MAX_STDOUT);
|
|
1109
|
+
const stderr = new BBC(MAX_STDERR);
|
|
1110
|
+
let buf = "";
|
|
884
1111
|
let settled = false;
|
|
885
1112
|
let aborted = false;
|
|
886
|
-
|
|
887
|
-
const
|
|
1113
|
+
let killTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1114
|
+
const abort = () => {
|
|
888
1115
|
aborted = true;
|
|
889
|
-
|
|
1116
|
+
cli.kill();
|
|
1117
|
+
killTimer = setTimeout(() => {
|
|
1118
|
+
if (!settled && cli.exitCode === null) cli.kill("SIGKILL");
|
|
1119
|
+
}, ABORT_KILL_GRACE_MS);
|
|
1120
|
+
killTimer?.unref?.();
|
|
890
1121
|
};
|
|
891
|
-
|
|
892
|
-
const cleanup = (): void => {
|
|
893
|
-
signal?.removeEventListener("abort", abortChild);
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
const fail = (error: Error): void => {
|
|
1122
|
+
const done = (v: { output: string; failed: boolean }) => {
|
|
897
1123
|
if (settled) return;
|
|
898
1124
|
settled = true;
|
|
899
|
-
|
|
900
|
-
|
|
1125
|
+
if (killTimer) clearTimeout(killTimer);
|
|
1126
|
+
signal?.removeEventListener("abort", abort);
|
|
1127
|
+
emit();
|
|
1128
|
+
resolve(v);
|
|
901
1129
|
};
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1130
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1131
|
+
state.status = "running";
|
|
1132
|
+
state.startedAt = Date.now();
|
|
1133
|
+
emit();
|
|
1134
|
+
const processLine = (line: string) => {
|
|
1135
|
+
const evt = parseJsonEvent(line);
|
|
1136
|
+
if (evt && applyEvent(state, evt)) emit();
|
|
908
1137
|
};
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1138
|
+
cli.stdout?.on("data", (d: Buffer) => {
|
|
1139
|
+
stdout.append(d);
|
|
1140
|
+
buf += d.toString("utf-8");
|
|
1141
|
+
if (buf.length > MAX_LINE_BUFFER) buf = buf.slice(-MAX_LINE_BUFFER);
|
|
1142
|
+
const lines = buf.split(/\r?\n/);
|
|
1143
|
+
buf = lines.pop() ?? "";
|
|
1144
|
+
for (const l of lines) processLine(l);
|
|
1145
|
+
});
|
|
1146
|
+
cli.stderr?.on("data", (d: Buffer) => {
|
|
1147
|
+
stderr.append(d);
|
|
1148
|
+
state.stderrTail = appendTail(
|
|
1149
|
+
state.stderrTail,
|
|
1150
|
+
d.toString("utf-8"),
|
|
1151
|
+
MAX_TAIL,
|
|
1152
|
+
);
|
|
916
1153
|
});
|
|
917
|
-
|
|
918
|
-
|
|
1154
|
+
cli.stdin?.on("error", (e: Error & { code?: string }) => {
|
|
1155
|
+
if (!aborted && e.code !== "EPIPE")
|
|
1156
|
+
done({ output: e.message, failed: true });
|
|
1157
|
+
});
|
|
1158
|
+
cli.on("error", (e) => {
|
|
1159
|
+
state.status = aborted ? "cancelled" : "failed";
|
|
1160
|
+
state.errorMessage = e instanceof Error ? e.message : String(e);
|
|
1161
|
+
state.finishedAt = Date.now();
|
|
1162
|
+
done({ output: finalize(state, state.errorMessage), failed: true });
|
|
1163
|
+
});
|
|
1164
|
+
cli.on("close", (code) => {
|
|
1165
|
+
if (buf.trim()) processLine(buf);
|
|
919
1166
|
const out = stdout.toString();
|
|
920
1167
|
const err = stderr.toString();
|
|
1168
|
+
state.stderrTail = appendTail("", err, MAX_TAIL);
|
|
1169
|
+
state.finishedAt = Date.now();
|
|
921
1170
|
if (aborted) {
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
)
|
|
1171
|
+
state.status = "cancelled";
|
|
1172
|
+
state.errorMessage = "cancelled";
|
|
1173
|
+
done({ output: finalize(state, "cancelled"), failed: true });
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (code === 0) {
|
|
1177
|
+
if (state.status === "pending" || state.status === "running")
|
|
1178
|
+
state.status = "succeeded";
|
|
1179
|
+
done({
|
|
1180
|
+
output: finalize(state, formatPiOutput(out, err)),
|
|
1181
|
+
failed: false,
|
|
1182
|
+
});
|
|
1183
|
+
return;
|
|
929
1184
|
}
|
|
1185
|
+
state.status = "failed";
|
|
1186
|
+
state.errorMessage = err || out || `exit ${code ?? "?"}`;
|
|
1187
|
+
done({ output: finalize(state, state.errorMessage), failed: true });
|
|
930
1188
|
});
|
|
931
|
-
|
|
932
|
-
child.stdin?.end(prompt);
|
|
1189
|
+
cli.stdin?.end(prompt);
|
|
933
1190
|
});
|
|
934
1191
|
}
|
|
935
1192
|
|
|
936
|
-
|
|
937
|
-
projectRoot: string,
|
|
938
|
-
input: SubagentInput,
|
|
939
|
-
contextKey?: string | null,
|
|
940
|
-
agentName?: string,
|
|
941
|
-
agentDefinition?: AgentDefinition,
|
|
942
|
-
): string {
|
|
943
|
-
const normalized =
|
|
944
|
-
agentName ?? normalizeAgentName(input.agent ?? "trellis-implement");
|
|
945
|
-
const definition =
|
|
946
|
-
agentDefinition ?? readAgentDefinition(projectRoot, normalized);
|
|
947
|
-
const context = buildTrellisContext(
|
|
948
|
-
projectRoot,
|
|
949
|
-
normalized,
|
|
950
|
-
input,
|
|
951
|
-
undefined,
|
|
952
|
-
contextKey,
|
|
953
|
-
);
|
|
954
|
-
const prompt = input.prompt ?? "";
|
|
955
|
-
|
|
956
|
-
return [
|
|
957
|
-
"## Trellis Agent Definition",
|
|
958
|
-
definition.content || "(missing agent definition)",
|
|
959
|
-
"",
|
|
960
|
-
context,
|
|
961
|
-
"",
|
|
962
|
-
"## Delegated Task",
|
|
963
|
-
prompt,
|
|
964
|
-
].join("\n");
|
|
965
|
-
}
|
|
966
|
-
|
|
1193
|
+
// ── runSubagent: orchestrate single/parallel/chain via native partial updates ──
|
|
967
1194
|
async function runSubagent(
|
|
968
|
-
|
|
1195
|
+
root: string,
|
|
969
1196
|
input: SubagentInput,
|
|
970
|
-
|
|
1197
|
+
key: string | null,
|
|
971
1198
|
signal?: AbortSignal,
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const
|
|
1199
|
+
onUpdate?: (r: PiToolResult) => void,
|
|
1200
|
+
inheritedThinking?: string,
|
|
1201
|
+
): Promise<{ output: string; details: ProgressDetails; failed: boolean }> {
|
|
1202
|
+
const agentName = normalizeAgent(input.agent);
|
|
1203
|
+
const agentRaw = readText(join(root, ".pi", "agents", `${agentName}.md`));
|
|
1204
|
+
const agentCfg = parseAgentFM(agentRaw);
|
|
1205
|
+
const runCfg = resolveRunCfg(input, agentCfg, inheritedThinking);
|
|
976
1206
|
const mode = input.mode ?? "single";
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1207
|
+
const startedAt = Date.now();
|
|
1208
|
+
const details: ProgressDetails = {
|
|
1209
|
+
kind: "trellis-subagent-progress",
|
|
1210
|
+
agent: agentName,
|
|
1211
|
+
mode,
|
|
1212
|
+
startedAt,
|
|
1213
|
+
updatedAt: startedAt,
|
|
1214
|
+
final: false,
|
|
1215
|
+
runs: [],
|
|
1216
|
+
};
|
|
1217
|
+
let lastEmit = 0;
|
|
1218
|
+
let lastPartialKey = "";
|
|
1219
|
+
let closed = false;
|
|
1220
|
+
const pushPartial = (force = false) => {
|
|
1221
|
+
if (closed || !onUpdate) return;
|
|
1222
|
+
const key = progressKey(details);
|
|
1223
|
+
if (!force && key === lastPartialKey) return;
|
|
1224
|
+
lastPartialKey = key;
|
|
1225
|
+
onUpdate({
|
|
1226
|
+
// Keep native partial content stable; renderResult owns the visible progress UI.
|
|
1227
|
+
content: [{ type: "text", text: "subagent running" }],
|
|
1228
|
+
details: cloneProgress(details),
|
|
1229
|
+
});
|
|
1230
|
+
};
|
|
1231
|
+
const emit = (force = false) => {
|
|
1232
|
+
const now = Date.now();
|
|
1233
|
+
if (!force && now - lastEmit < THROTTLE_MS) return;
|
|
1234
|
+
lastEmit = now;
|
|
1235
|
+
details.updatedAt = now;
|
|
1236
|
+
pushPartial(force);
|
|
1237
|
+
};
|
|
1238
|
+
const finish = (output: string, failed: boolean) => {
|
|
1239
|
+
closed = true;
|
|
1240
|
+
details.final = true;
|
|
1241
|
+
details.updatedAt = Date.now();
|
|
1242
|
+
return { output, details: cloneProgress(details), failed };
|
|
1243
|
+
};
|
|
998
1244
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1245
|
+
try {
|
|
1246
|
+
if (mode === "parallel") {
|
|
1247
|
+
const prompts = input.prompts ?? (input.prompt ? [input.prompt] : []);
|
|
1248
|
+
details.runs = prompts.map((p, i) => {
|
|
1249
|
+
const r = newRun(`${agentName}-${i + 1}`, agentName, p);
|
|
1250
|
+
applyRunConfig(r, runCfg);
|
|
1251
|
+
return r;
|
|
1252
|
+
});
|
|
1253
|
+
emit(true);
|
|
1254
|
+
const results = await Promise.all(
|
|
1255
|
+
prompts.map((p, i) =>
|
|
1256
|
+
runPi(
|
|
1257
|
+
root,
|
|
1258
|
+
buildPrompt(root, { ...input, prompt: p }, key),
|
|
1259
|
+
runCfg,
|
|
1260
|
+
details.runs[i]!,
|
|
1261
|
+
emit,
|
|
1262
|
+
key,
|
|
1263
|
+
signal,
|
|
1264
|
+
),
|
|
1016
1265
|
),
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1266
|
+
);
|
|
1267
|
+
return finish(
|
|
1268
|
+
results.map((r) => r.output).join("\n\n---\n\n"),
|
|
1269
|
+
results.some((r) => r.failed),
|
|
1020
1270
|
);
|
|
1021
1271
|
}
|
|
1022
|
-
|
|
1272
|
+
if (mode === "chain") {
|
|
1273
|
+
let prev = "";
|
|
1274
|
+
let failed = false;
|
|
1275
|
+
for (let i = 0; i < (input.prompts?.length ?? 1); i++) {
|
|
1276
|
+
const p = input.prompts?.[i] ?? input.prompt ?? "";
|
|
1277
|
+
const rs = newRun(`${agentName}-${i + 1}`, agentName, p, i + 1);
|
|
1278
|
+
applyRunConfig(rs, runCfg);
|
|
1279
|
+
details.runs.push(rs);
|
|
1280
|
+
emit(true);
|
|
1281
|
+
const result = await runPi(
|
|
1282
|
+
root,
|
|
1283
|
+
buildPrompt(
|
|
1284
|
+
root,
|
|
1285
|
+
{
|
|
1286
|
+
...input,
|
|
1287
|
+
prompt: prev ? `${p}\n\nPrevious output:\n${prev}` : p,
|
|
1288
|
+
},
|
|
1289
|
+
key,
|
|
1290
|
+
),
|
|
1291
|
+
runCfg,
|
|
1292
|
+
rs,
|
|
1293
|
+
emit,
|
|
1294
|
+
key,
|
|
1295
|
+
signal,
|
|
1296
|
+
);
|
|
1297
|
+
prev = result.output;
|
|
1298
|
+
failed = failed || result.failed;
|
|
1299
|
+
if (result.failed) break;
|
|
1300
|
+
}
|
|
1301
|
+
return finish(prev, failed);
|
|
1302
|
+
}
|
|
1303
|
+
const rs = newRun(`${agentName}-1`, agentName, input.prompt ?? "");
|
|
1304
|
+
applyRunConfig(rs, runCfg);
|
|
1305
|
+
details.runs = [rs];
|
|
1306
|
+
emit(true);
|
|
1307
|
+
const result = await runPi(
|
|
1308
|
+
root,
|
|
1309
|
+
buildPrompt(root, input, key),
|
|
1310
|
+
runCfg,
|
|
1311
|
+
rs,
|
|
1312
|
+
emit,
|
|
1313
|
+
key,
|
|
1314
|
+
signal,
|
|
1315
|
+
);
|
|
1316
|
+
return finish(result.output, result.failed);
|
|
1317
|
+
} catch (e) {
|
|
1318
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1319
|
+
const r = activeRun(details);
|
|
1320
|
+
if (r) {
|
|
1321
|
+
r.status = "failed";
|
|
1322
|
+
r.errorMessage = message;
|
|
1323
|
+
r.finishedAt = Date.now();
|
|
1324
|
+
}
|
|
1325
|
+
return finish(message, true);
|
|
1023
1326
|
}
|
|
1024
|
-
|
|
1025
|
-
return runPi(
|
|
1026
|
-
projectRoot,
|
|
1027
|
-
buildSubagentPrompt(
|
|
1028
|
-
projectRoot,
|
|
1029
|
-
input,
|
|
1030
|
-
contextKey,
|
|
1031
|
-
agentName,
|
|
1032
|
-
agentDefinition,
|
|
1033
|
-
),
|
|
1034
|
-
runConfig,
|
|
1035
|
-
contextKey,
|
|
1036
|
-
signal,
|
|
1037
|
-
);
|
|
1038
1327
|
}
|
|
1039
1328
|
|
|
1329
|
+
// ── Extension ──────────────────────────────────────────────────────────
|
|
1040
1330
|
export default function trellisExtension(pi: {
|
|
1041
1331
|
registerTool?: (tool: JsonObject) => void;
|
|
1332
|
+
registerShortcut?: (
|
|
1333
|
+
key: string,
|
|
1334
|
+
opts: {
|
|
1335
|
+
description?: string;
|
|
1336
|
+
handler: (ctx: PiExtensionContext) => unknown;
|
|
1337
|
+
},
|
|
1338
|
+
) => void;
|
|
1042
1339
|
on?: (
|
|
1043
1340
|
event: string,
|
|
1044
1341
|
handler: (event: unknown, ctx?: PiExtensionContext) => unknown,
|
|
1045
1342
|
) => void;
|
|
1046
|
-
|
|
1343
|
+
getThinkingLevel?: () => string;
|
|
1047
1344
|
}): void {
|
|
1048
|
-
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1345
|
+
if (process.env.TRELLIS_SUBAGENT_CHILD === "1") return;
|
|
1346
|
+
const root = findRoot(process.cwd());
|
|
1347
|
+
const procKey = `pi_process_${hash([root, process.pid, Date.now(), randomBytes(8).toString("hex")].join(":"))}`;
|
|
1348
|
+
let curKey: string | null = null;
|
|
1349
|
+
|
|
1350
|
+
const getKey = (input?: unknown, ctx?: PiExtensionContext) => {
|
|
1351
|
+
const k = adoptKey(root, contextKey(input, ctx) ?? curKey ?? procKey);
|
|
1352
|
+
curKey = k;
|
|
1353
|
+
return k;
|
|
1354
|
+
};
|
|
1052
1355
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1356
|
+
// Per-turn cache to avoid double-spawning python
|
|
1357
|
+
let turnCache: {
|
|
1358
|
+
key: string | null;
|
|
1359
|
+
ts: number;
|
|
1360
|
+
wf: string;
|
|
1361
|
+
ov: string;
|
|
1362
|
+
} | null = null;
|
|
1363
|
+
const getTurnCtx = (k: string | null) => {
|
|
1364
|
+
const now = Date.now();
|
|
1365
|
+
if (turnCache && turnCache.key === k && now - turnCache.ts < 1500)
|
|
1366
|
+
return turnCache;
|
|
1367
|
+
turnCache = {
|
|
1368
|
+
key: k,
|
|
1369
|
+
ts: now,
|
|
1370
|
+
wf: workflowBreadcrumb(root, k),
|
|
1371
|
+
ov: sessionOverview(root, k),
|
|
1372
|
+
};
|
|
1373
|
+
return turnCache;
|
|
1059
1374
|
};
|
|
1060
1375
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
);
|
|
1071
|
-
return currentContextKey;
|
|
1376
|
+
// Toggle only the latest subagent native card; do not use Pi global tool expansion.
|
|
1377
|
+
const toggleDetail = (ctx: PiExtensionContext) => {
|
|
1378
|
+
const id = activeSubagentToolCallId;
|
|
1379
|
+
const card = id ? nativeCards.get(id) : undefined;
|
|
1380
|
+
if (!card) {
|
|
1381
|
+
ctx.ui?.notify?.("No subagent card to toggle yet.", "warning");
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
card.state.localExpanded = card.state.localExpanded !== true;
|
|
1385
|
+
card.invalidate();
|
|
1072
1386
|
};
|
|
1073
1387
|
|
|
1388
|
+
pi.registerShortcut?.("alt+o", {
|
|
1389
|
+
description: "Toggle latest subagent card details",
|
|
1390
|
+
handler: async (ctx: PiExtensionContext) => toggleDetail(ctx),
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
// Tool registration
|
|
1074
1394
|
pi.registerTool?.({
|
|
1075
|
-
name: "
|
|
1076
|
-
label: "Subagent",
|
|
1395
|
+
name: "trellis_subagent",
|
|
1396
|
+
label: "Trellis Subagent",
|
|
1077
1397
|
description: "Run a Trellis project sub-agent with active task context.",
|
|
1078
|
-
promptSnippet:
|
|
1079
|
-
|
|
1398
|
+
promptSnippet:
|
|
1399
|
+
'Sub-agent dispatch protocol (Trellis): your dispatch prompt MUST start with one line "Active task: <task path from `task.py current`>" before any other instructions.',
|
|
1400
|
+
promptGuidelines: [
|
|
1401
|
+
'Use subagent for task delegation. Your dispatch prompt MUST start with "Active task: <task path from `task.py current`>".',
|
|
1402
|
+
],
|
|
1080
1403
|
parameters: {
|
|
1081
1404
|
type: "object",
|
|
1082
1405
|
properties: {
|
|
@@ -1089,15 +1412,11 @@ export default function trellisExtension(pi: {
|
|
|
1089
1412
|
type: "string",
|
|
1090
1413
|
description: "Task prompt for the sub-agent.",
|
|
1091
1414
|
},
|
|
1092
|
-
mode: {
|
|
1093
|
-
type: "string",
|
|
1094
|
-
enum: ["single", "parallel", "chain"],
|
|
1095
|
-
description: "Delegation mode.",
|
|
1096
|
-
},
|
|
1415
|
+
mode: { type: "string", enum: ["single", "parallel", "chain"] },
|
|
1097
1416
|
prompts: {
|
|
1098
1417
|
type: "array",
|
|
1099
1418
|
items: { type: "string" },
|
|
1100
|
-
|
|
1419
|
+
maxItems: MAX_PARALLEL_PROMPTS,
|
|
1101
1420
|
},
|
|
1102
1421
|
model: {
|
|
1103
1422
|
type: "string",
|
|
@@ -1106,69 +1425,176 @@ export default function trellisExtension(pi: {
|
|
|
1106
1425
|
},
|
|
1107
1426
|
thinking: {
|
|
1108
1427
|
type: "string",
|
|
1109
|
-
enum: ["off", "minimal", "low", "medium", "high", "xhigh"],
|
|
1110
1428
|
description:
|
|
1111
1429
|
"Optional Pi thinking level override for the child sub-agent process.",
|
|
1430
|
+
enum: ["off", "minimal", "low", "medium", "high", "xhigh"],
|
|
1112
1431
|
},
|
|
1113
1432
|
},
|
|
1114
|
-
required: ["prompt"],
|
|
1115
1433
|
},
|
|
1116
1434
|
execute: async (
|
|
1117
|
-
|
|
1435
|
+
id: string,
|
|
1118
1436
|
input: SubagentInput,
|
|
1119
|
-
|
|
1120
|
-
|
|
1437
|
+
signal?: AbortSignal,
|
|
1438
|
+
onUpdate?: (r: PiToolResult) => void,
|
|
1121
1439
|
ctx?: PiExtensionContext,
|
|
1122
|
-
)
|
|
1123
|
-
|
|
1124
|
-
const
|
|
1440
|
+
) => {
|
|
1441
|
+
activeSubagentToolCallId = id;
|
|
1442
|
+
const agentName = normalizeAgent(input.agent);
|
|
1443
|
+
if (!isTrellisAgent(root, agentName)) {
|
|
1444
|
+
return {
|
|
1445
|
+
content: [
|
|
1446
|
+
{
|
|
1447
|
+
type: "text",
|
|
1448
|
+
text:
|
|
1449
|
+
"`trellis_subagent` is only for Trellis workflow agents with a definition file in .pi/agents/.\n\n" +
|
|
1450
|
+
`No definition found for: ${agentName}\n\n` +
|
|
1451
|
+
"For general-purpose sub-agents, use one of these community tools:\n" +
|
|
1452
|
+
"- `subagent` tool from npm:pi-subagents (nicobailon/pi-subagents)\n" +
|
|
1453
|
+
"- `Agent` tool from npm:@tintinweb/pi-subagents\n\n" +
|
|
1454
|
+
"If neither is installed, ask the user to either:\n" +
|
|
1455
|
+
`- Create .pi/agents/${agentName}.md for your custom Trellis agent\n` +
|
|
1456
|
+
"- Install a community subagent package: pi install -l npm:@tintinweb/pi-subagents",
|
|
1457
|
+
},
|
|
1458
|
+
],
|
|
1459
|
+
details: { agent: agentName, error: "not a trellis workflow agent" },
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
const mode = input.mode ?? "single";
|
|
1463
|
+
const prompt = input.prompt?.trim();
|
|
1464
|
+
const prompts = input.prompts?.map((p) => p.trim()).filter(Boolean);
|
|
1465
|
+
if (mode === "single" && !prompt)
|
|
1466
|
+
throw new Error("subagent prompt is required for single mode");
|
|
1467
|
+
if (
|
|
1468
|
+
(mode === "parallel" || mode === "chain") &&
|
|
1469
|
+
!prompt &&
|
|
1470
|
+
!prompts?.length
|
|
1471
|
+
)
|
|
1472
|
+
throw new Error(
|
|
1473
|
+
"subagent prompt or prompts are required for parallel/chain mode",
|
|
1474
|
+
);
|
|
1475
|
+
if (
|
|
1476
|
+
mode === "parallel" &&
|
|
1477
|
+
prompts &&
|
|
1478
|
+
prompts.length > MAX_PARALLEL_PROMPTS
|
|
1479
|
+
)
|
|
1480
|
+
throw new Error(
|
|
1481
|
+
`subagent parallel mode supports at most ${MAX_PARALLEL_PROMPTS} prompts`,
|
|
1482
|
+
);
|
|
1483
|
+
const cleanInput: SubagentInput = {
|
|
1484
|
+
...input,
|
|
1485
|
+
prompt,
|
|
1486
|
+
prompts: prompts?.length ? prompts : undefined,
|
|
1487
|
+
};
|
|
1488
|
+
const key = getKey(cleanInput, ctx);
|
|
1489
|
+
const inheritedThinking = pi.getThinkingLevel?.();
|
|
1490
|
+
const result = await runSubagent(
|
|
1491
|
+
root,
|
|
1492
|
+
cleanInput,
|
|
1493
|
+
key,
|
|
1494
|
+
signal,
|
|
1495
|
+
onUpdate,
|
|
1496
|
+
inheritedThinking,
|
|
1497
|
+
);
|
|
1498
|
+
return {
|
|
1499
|
+
content: [{ type: "text", text: result.output }],
|
|
1500
|
+
details: result.details,
|
|
1501
|
+
};
|
|
1502
|
+
},
|
|
1503
|
+
// Hide the call renderer so the native card only shows result/progress once.
|
|
1504
|
+
renderCall: () => ({
|
|
1505
|
+
render() {
|
|
1506
|
+
return [];
|
|
1507
|
+
},
|
|
1508
|
+
invalidate() {},
|
|
1509
|
+
}),
|
|
1510
|
+
renderResult: (
|
|
1511
|
+
result: PiToolResult,
|
|
1512
|
+
_opts?: { expanded?: boolean; isPartial?: boolean },
|
|
1513
|
+
_theme?: unknown,
|
|
1514
|
+
context?: unknown,
|
|
1515
|
+
) => {
|
|
1516
|
+
const ctxObj = isObj(context) ? context : null;
|
|
1517
|
+
const toolCallId = str(ctxObj?.toolCallId);
|
|
1518
|
+
const state = isObj(ctxObj?.state) ? (ctxObj.state as JsonObject) : null;
|
|
1519
|
+
const invalidate =
|
|
1520
|
+
typeof ctxObj?.invalidate === "function"
|
|
1521
|
+
? (ctxObj.invalidate as () => void)
|
|
1522
|
+
: null;
|
|
1523
|
+
const isProgress =
|
|
1524
|
+
isObj(result.details) &&
|
|
1525
|
+
result.details.kind === "trellis-subagent-progress";
|
|
1526
|
+
if (toolCallId && state && invalidate) {
|
|
1527
|
+
const updatedAt = isProgress
|
|
1528
|
+
? (result.details as ProgressDetails).updatedAt
|
|
1529
|
+
: Date.now();
|
|
1530
|
+
rememberNativeCard(toolCallId, { state, invalidate, updatedAt });
|
|
1531
|
+
}
|
|
1125
1532
|
return {
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1533
|
+
render(w: number) {
|
|
1534
|
+
if (isProgress) {
|
|
1535
|
+
const expanded = state?.localExpanded === true;
|
|
1536
|
+
return renderProgressCard(
|
|
1537
|
+
result.details as ProgressDetails,
|
|
1538
|
+
expanded,
|
|
1539
|
+
w,
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
return [trunc(result.content?.[0]?.text ?? "(no output)", w)];
|
|
1130
1543
|
},
|
|
1544
|
+
invalidate() {},
|
|
1131
1545
|
};
|
|
1132
1546
|
},
|
|
1133
1547
|
});
|
|
1134
1548
|
|
|
1549
|
+
// Events
|
|
1135
1550
|
pi.on?.("session_start", (event, ctx) => {
|
|
1136
|
-
|
|
1551
|
+
getKey(event, ctx);
|
|
1137
1552
|
ctx?.ui?.notify?.(
|
|
1138
1553
|
"Trellis project context is available. Use /trellis-continue to resume the current task.",
|
|
1139
1554
|
"info",
|
|
1140
1555
|
);
|
|
1141
1556
|
});
|
|
1557
|
+
pi.on?.("session_shutdown", () => {
|
|
1558
|
+
nativeCards.clear();
|
|
1559
|
+
activeSubagentToolCallId = null;
|
|
1560
|
+
});
|
|
1561
|
+
pi.on?.("tool_call", (event, ctx) => {
|
|
1562
|
+
const k = getKey(event, ctx);
|
|
1563
|
+
const ev = event as { toolName?: string; input?: JsonObject };
|
|
1564
|
+
if (
|
|
1565
|
+
ev.toolName === "bash" &&
|
|
1566
|
+
isObj(ev.input) &&
|
|
1567
|
+
typeof ev.input.command === "string" &&
|
|
1568
|
+
!cmdHasTrellisCtx(ev.input.command)
|
|
1569
|
+
)
|
|
1570
|
+
ev.input.command = `export TRELLIS_CONTEXT_ID=${shellQuote(k)}; ${ev.input.command}`;
|
|
1571
|
+
});
|
|
1572
|
+
// Preserve progress details from execute(); mark failed subagent results through
|
|
1573
|
+
// the official tool_result patch hook instead of throwing away renderer details.
|
|
1574
|
+
pi.on?.("tool_result", (event) => {
|
|
1575
|
+
const ev = event as { toolName?: string; details?: unknown };
|
|
1576
|
+
if (
|
|
1577
|
+
ev.toolName === "trellis_subagent" &&
|
|
1578
|
+
isObj(ev.details) &&
|
|
1579
|
+
ev.details.kind === "trellis-subagent-progress" &&
|
|
1580
|
+
Array.isArray(ev.details.runs) &&
|
|
1581
|
+
ev.details.runs.some(
|
|
1582
|
+
(r) => isObj(r) && (r.status === "failed" || r.status === "cancelled"),
|
|
1583
|
+
)
|
|
1584
|
+
)
|
|
1585
|
+
return { isError: true };
|
|
1586
|
+
return undefined;
|
|
1587
|
+
});
|
|
1142
1588
|
pi.on?.("before_agent_start", (event, ctx) => {
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
"trellis-implement",
|
|
1148
|
-
event,
|
|
1149
|
-
ctx,
|
|
1150
|
-
contextKey,
|
|
1151
|
-
);
|
|
1152
|
-
const perTurn = buildPerTurnInjection(contextKey);
|
|
1589
|
+
const k = getKey(event, ctx);
|
|
1590
|
+
const cur = (event as { systemPrompt?: string }).systemPrompt ?? "";
|
|
1591
|
+
const ctxText = buildContext(root, "trellis-implement", k);
|
|
1592
|
+
const { wf, ov } = getTurnCtx(k);
|
|
1153
1593
|
return {
|
|
1154
|
-
systemPrompt: [
|
|
1594
|
+
systemPrompt: [cur, ctxText, wf, ov].filter(Boolean).join("\n\n"),
|
|
1155
1595
|
};
|
|
1156
1596
|
});
|
|
1157
1597
|
pi.on?.("context", (event, ctx) => {
|
|
1158
|
-
|
|
1159
|
-
const messages = (event as PiContextEvent).messages;
|
|
1160
|
-
return Array.isArray(messages) ? { messages } : undefined;
|
|
1161
|
-
});
|
|
1162
|
-
pi.on?.("input", (event, ctx) => {
|
|
1163
|
-
const contextKey = getContextKey(event, ctx);
|
|
1164
|
-
const additionalContext = buildPerTurnInjection(contextKey);
|
|
1165
|
-
return additionalContext
|
|
1166
|
-
? { action: "continue", additionalContext, systemPrompt: additionalContext }
|
|
1167
|
-
: { action: "continue" };
|
|
1168
|
-
});
|
|
1169
|
-
pi.on?.("tool_call", (event, ctx) => {
|
|
1170
|
-
const contextKey = getContextKey(event, ctx);
|
|
1171
|
-
injectTrellisContextIntoBash(event, contextKey);
|
|
1172
|
-
return undefined;
|
|
1598
|
+
getKey(event, ctx);
|
|
1173
1599
|
});
|
|
1174
1600
|
}
|