pi-harness-delegate 0.1.0 → 0.2.0
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 +8 -6
- package/extensions/activity.ts +225 -201
- package/extensions/command.ts +61 -61
- package/extensions/config.ts +137 -131
- package/extensions/harnesses/amp.ts +122 -115
- package/extensions/harnesses/claude.ts +136 -111
- package/extensions/harnesses/codex.ts +191 -147
- package/extensions/harnesses/opencode.ts +133 -116
- package/extensions/harnesses/registry.ts +22 -22
- package/extensions/harnesses/types.ts +60 -60
- package/extensions/hint.ts +22 -19
- package/extensions/index.ts +1128 -743
- package/extensions/progress.ts +104 -104
- package/extensions/run-claude.ts +44 -35
- package/extensions/runner.ts +111 -99
- package/extensions/stream-parse.ts +32 -24
- package/extensions/templates.ts +134 -117
- package/extensions/usage.ts +24 -24
- package/package.json +69 -56
package/extensions/index.ts
CHANGED
|
@@ -14,803 +14,1188 @@
|
|
|
14
14
|
* Legacy: { claudeDelegate: {...} } is auto-migrated.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
import { homedir } from 'node:os';
|
|
17
|
+
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
19
18
|
import { basename, join } from 'node:path';
|
|
19
|
+
import { type ExtensionAPI, type ExtensionContext, getMarkdownTheme } from '@earendil-works/pi-coding-agent';
|
|
20
|
+
import {
|
|
21
|
+
Container,
|
|
22
|
+
Key,
|
|
23
|
+
Markdown,
|
|
24
|
+
matchesKey,
|
|
25
|
+
type OverlayHandle,
|
|
26
|
+
type SelectItem,
|
|
27
|
+
SelectList,
|
|
28
|
+
Text,
|
|
29
|
+
truncateToWidth,
|
|
30
|
+
} from '@earendil-works/pi-tui';
|
|
20
31
|
import { Type } from 'typebox';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
import {
|
|
33
|
+
buildReportContent,
|
|
34
|
+
buildTranscript,
|
|
35
|
+
collectActivityLog,
|
|
36
|
+
formatMetrics,
|
|
37
|
+
formatToolUse,
|
|
38
|
+
parseTranscriptMeta,
|
|
39
|
+
pruneOutputs,
|
|
40
|
+
safeSegmentName,
|
|
41
|
+
} from './activity.ts';
|
|
42
|
+
import { parseDelegateCommand, resolveDefaults } from './command.ts';
|
|
43
|
+
import { outputsDir as getOutputsDir, legacyOutputsDir, loadConfig, resolveModelForHarness } from './config.ts';
|
|
44
|
+
import { ALIASES, detectAll, getHarness, HARNESS_NAMES, isKnownHarness } from './harnesses/registry.ts';
|
|
45
|
+
import type { ActivityEvent, NormalizedPermission } from './harnesses/types.ts';
|
|
46
|
+
|
|
26
47
|
import { delegationHint, stripMarker } from './hint.ts';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
48
|
+
import { type FeedEntry, progressWindow } from './progress.ts';
|
|
49
|
+
import { runHarness } from './runner.ts';
|
|
50
|
+
import { type DelegateTemplate, loadTemplates } from './templates.ts';
|
|
29
51
|
import { mapClaudeUsage } from './usage.ts';
|
|
30
|
-
import { buildReportContent, buildTranscript, collectActivityLog, formatMetrics, formatToolUse, parseTranscriptMeta, pruneOutputs, safeSegmentName } from './activity.ts';
|
|
31
|
-
import { loadConfig, outputsDir as getOutputsDir, legacyOutputsDir, resolveModelForHarness, agentDir } from './config.ts';
|
|
32
|
-
import { getHarness, HARNESS_NAMES, ALIASES, isKnownHarness } from './harnesses/registry.ts';
|
|
33
|
-
import type { ActivityEvent } from './harnesses/types.ts';
|
|
34
|
-
import type { NormalizedPermission } from './harnesses/types.ts';
|
|
35
52
|
|
|
36
53
|
interface DelegateOptions {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
harness?: string;
|
|
55
|
+
task: string;
|
|
56
|
+
mode?: string;
|
|
57
|
+
scope?: string;
|
|
58
|
+
model?: string;
|
|
59
|
+
maxBudgetUsd?: number;
|
|
60
|
+
allowDangerous?: boolean;
|
|
61
|
+
sessionId?: string;
|
|
62
|
+
pr?: string;
|
|
63
|
+
onStream?: (text: string) => void;
|
|
64
|
+
onActivity?: (ev: ActivityEvent) => void;
|
|
65
|
+
signal?: AbortSignal;
|
|
49
66
|
}
|
|
50
67
|
|
|
51
68
|
const activeRuns = new Map<string, number>();
|
|
52
69
|
let globalActiveRuns = 0;
|
|
53
70
|
|
|
54
71
|
function getMaxConcurrentGlobal(): number {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
const cfg = loadConfig();
|
|
73
|
+
if (typeof cfg.maxConcurrent === 'number') return cfg.maxConcurrent;
|
|
74
|
+
// SAFETY: maxConcurrent is validated to be number or object with global/perHarness in loadConfig
|
|
75
|
+
const mc = cfg.maxConcurrent as unknown as { global?: number }; // SAFETY: maxConcurrent validated in loadConfig
|
|
76
|
+
if (typeof mc.global === 'number') return mc.global;
|
|
77
|
+
return 1;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
async function closeWhenMounted(getClose: () => (() => void) | null, capMs: number): Promise<void> {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
const close = getClose();
|
|
82
|
+
if (close) {
|
|
83
|
+
close();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await new Promise<void>(resolve => {
|
|
87
|
+
const start = Date.now();
|
|
88
|
+
const timer = setInterval(() => {
|
|
89
|
+
const fn = getClose();
|
|
90
|
+
if (fn || Date.now() - start > capMs) {
|
|
91
|
+
clearInterval(timer);
|
|
92
|
+
fn?.();
|
|
93
|
+
resolve();
|
|
94
|
+
}
|
|
95
|
+
}, 20);
|
|
96
|
+
});
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
function outputsDirFor(harness: string): string {
|
|
82
|
-
|
|
100
|
+
return getOutputsDir(harness);
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
function formatTemplateRow(t: DelegateTemplate): string {
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
const parts = [
|
|
105
|
+
t.name,
|
|
106
|
+
`[${t.permission}]`,
|
|
107
|
+
t.model ? `model=${t.model}` : '',
|
|
108
|
+
t.defaultTask ? '↳ default task' : '',
|
|
109
|
+
t.harness ? `(${t.harness})` : '',
|
|
110
|
+
];
|
|
111
|
+
return `${parts.filter(Boolean).join(' ')} — ${t.description}`;
|
|
88
112
|
}
|
|
89
113
|
|
|
90
114
|
async function showModes(ctx: ExtensionContext, harnessFilter?: string): Promise<void> {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
115
|
+
const all = new Map<string, DelegateTemplate>();
|
|
116
|
+
// collect from all harnesses if no filter
|
|
117
|
+
if (harnessFilter) {
|
|
118
|
+
for (const [k, v] of loadTemplates(ctx.cwd, harnessFilter)) all.set(k, v);
|
|
119
|
+
} else {
|
|
120
|
+
for (const h of [...HARNESS_NAMES, 'shared']) {
|
|
121
|
+
for (const [k, v] of loadTemplates(ctx.cwd, h)) if (!all.has(k)) all.set(k, v);
|
|
122
|
+
}
|
|
123
|
+
// also load without harness param
|
|
124
|
+
for (const [k, v] of loadTemplates(ctx.cwd)) if (!all.has(k)) all.set(k, v);
|
|
125
|
+
}
|
|
126
|
+
const rows = [...all.values()].map(formatTemplateRow);
|
|
127
|
+
if (!ctx.hasUI) {
|
|
128
|
+
process.stdout.write(`${rows.join('\n')}\n`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
132
|
+
let offset = 0;
|
|
133
|
+
const height = 12;
|
|
134
|
+
return {
|
|
135
|
+
render(width: number): string[] {
|
|
136
|
+
const header = theme.fg(
|
|
137
|
+
'accent',
|
|
138
|
+
`delegate — modes${harnessFilter ? ` (${harnessFilter})` : ''} (↑↓ scroll · any key to close)`,
|
|
139
|
+
);
|
|
140
|
+
const visible = rows.slice(offset, offset + height);
|
|
141
|
+
return [header, ...visible.map(l => theme.fg('muted', truncateToWidth(l, width)))];
|
|
142
|
+
},
|
|
143
|
+
handleInput(data: string): void {
|
|
144
|
+
if (matchesKey(data, Key.up) && offset > 0) {
|
|
145
|
+
offset--;
|
|
146
|
+
tui.requestRender();
|
|
147
|
+
} else if (matchesKey(data, Key.down) && offset < rows.length - 1) {
|
|
148
|
+
offset++;
|
|
149
|
+
tui.requestRender();
|
|
150
|
+
} else {
|
|
151
|
+
done(undefined);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
invalidate() {},
|
|
155
|
+
};
|
|
156
|
+
});
|
|
130
157
|
}
|
|
131
158
|
|
|
132
159
|
interface HistoryEntry {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
160
|
+
file: string;
|
|
161
|
+
mode: string;
|
|
162
|
+
harness: string;
|
|
163
|
+
cost: number;
|
|
164
|
+
sessionId: string | null;
|
|
165
|
+
mtime: number;
|
|
139
166
|
}
|
|
140
167
|
|
|
141
168
|
function readHistory(dir: string, harness: string): HistoryEntry[] {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
try {
|
|
170
|
+
return readdirSync(dir)
|
|
171
|
+
.filter(f => f.endsWith('.md') && !f.includes('-partial'))
|
|
172
|
+
.map(f => {
|
|
173
|
+
const file = join(dir, f);
|
|
174
|
+
let mode = 'delegate';
|
|
175
|
+
let cost = 0;
|
|
176
|
+
let sessionId: string | null = null;
|
|
177
|
+
try {
|
|
178
|
+
const meta = parseTranscriptMeta(readFileSync(file, 'utf8').slice(0, 2000));
|
|
179
|
+
mode = meta.mode;
|
|
180
|
+
cost = meta.cost;
|
|
181
|
+
sessionId = meta.sessionId;
|
|
182
|
+
} catch (_e) {
|
|
183
|
+
void _e;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
file,
|
|
187
|
+
mode,
|
|
188
|
+
harness,
|
|
189
|
+
cost,
|
|
190
|
+
sessionId,
|
|
191
|
+
mtime: statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? 0,
|
|
192
|
+
};
|
|
193
|
+
})
|
|
194
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
195
|
+
} catch {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
162
198
|
}
|
|
163
199
|
|
|
164
200
|
function readAllHistory(): HistoryEntry[] {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
201
|
+
const entries: HistoryEntry[] = [];
|
|
202
|
+
// new partitioned dir
|
|
203
|
+
for (const h of HARNESS_NAMES) {
|
|
204
|
+
entries.push(...readHistory(getOutputsDir(h), h));
|
|
205
|
+
}
|
|
206
|
+
// also legacy dir for migration display
|
|
207
|
+
try {
|
|
208
|
+
const legacy = readdirSync(legacyOutputsDir()).filter(f => f.endsWith('.md'));
|
|
209
|
+
for (const f of legacy) {
|
|
210
|
+
const file = join(legacyOutputsDir(), f);
|
|
211
|
+
let mode = 'delegate';
|
|
212
|
+
let cost = 0;
|
|
213
|
+
let sessionId: string | null = null;
|
|
214
|
+
try {
|
|
215
|
+
const meta = parseTranscriptMeta(readFileSync(file, 'utf8').slice(0, 2000));
|
|
216
|
+
mode = meta.mode;
|
|
217
|
+
cost = meta.cost;
|
|
218
|
+
sessionId = meta.sessionId;
|
|
219
|
+
} catch (_e) {
|
|
220
|
+
void _e;
|
|
221
|
+
}
|
|
222
|
+
entries.push({
|
|
223
|
+
file,
|
|
224
|
+
mode,
|
|
225
|
+
harness: 'claude',
|
|
226
|
+
cost,
|
|
227
|
+
sessionId,
|
|
228
|
+
mtime: statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? 0,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
} catch (_e) {
|
|
232
|
+
void _e;
|
|
233
|
+
}
|
|
234
|
+
return entries.sort((a, b) => b.mtime - a.mtime);
|
|
188
235
|
}
|
|
189
236
|
|
|
190
237
|
async function viewTranscript(ctx: ExtensionContext, entry: HistoryEntry): Promise<void> {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
238
|
+
if (!ctx.hasUI) {
|
|
239
|
+
process.stdout.write(readFileSync(entry.file, 'utf8'));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
243
|
+
const lines = readFileSync(entry.file, 'utf8').split('\n');
|
|
244
|
+
let offset = 0;
|
|
245
|
+
const height = 12;
|
|
246
|
+
return {
|
|
247
|
+
render(width: number): string[] {
|
|
248
|
+
const resume = entry.sessionId ? ` · r resume` : '';
|
|
249
|
+
const header = theme.fg('accent', `${basename(entry.file)} (↑↓ scroll${resume} · esc close)`);
|
|
250
|
+
const visible = lines.slice(offset, offset + height);
|
|
251
|
+
return [header, ...visible.map(l => theme.fg('muted', truncateToWidth(l, width)))];
|
|
252
|
+
},
|
|
253
|
+
handleInput(data: string): void {
|
|
254
|
+
if (matchesKey(data, Key.down) && offset < lines.length - 1) {
|
|
255
|
+
offset++;
|
|
256
|
+
tui.requestRender();
|
|
257
|
+
} else if (matchesKey(data, Key.up) && offset > 0) {
|
|
258
|
+
offset--;
|
|
259
|
+
tui.requestRender();
|
|
260
|
+
} else if (matchesKey(data, Key.escape)) {
|
|
261
|
+
done(undefined);
|
|
262
|
+
} else if (entry.sessionId && data === 'r') {
|
|
263
|
+
ctx.ui.notify?.(`resume with: /delegate --resume=${entry.sessionId} <prompt>`, 'info');
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
invalidate() {},
|
|
267
|
+
};
|
|
268
|
+
});
|
|
222
269
|
}
|
|
223
270
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
for (const e of entries) {
|
|
232
|
-
process.stdout.write(`${e.harness} ${e.mode} · $${e.cost.toFixed(3)} · ${e.sessionId ?? '-'}\n`);
|
|
233
|
-
}
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const entry = await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
237
|
-
const items: SelectItem[] = entries.map((e) => ({
|
|
238
|
-
value: e.file,
|
|
239
|
-
label: `${e.harness} ${e.mode} · $${e.cost.toFixed(3)} · ${new Date(e.mtime).toISOString().slice(0, 16)}`,
|
|
240
|
-
description: e.sessionId ? `session ${e.sessionId.slice(0, 8)}…` : undefined,
|
|
241
|
-
}));
|
|
242
|
-
const list = new SelectList(items, Math.min(items.length, 10), {
|
|
243
|
-
selectedPrefix: (s: string) => theme.fg('accent', s),
|
|
244
|
-
selectedText: (s: string) => theme.fg('accent', s),
|
|
245
|
-
description: (s: string) => theme.fg('dim', s),
|
|
246
|
-
scrollInfo: (s: string) => theme.fg('dim', s),
|
|
247
|
-
noMatch: (s: string) => theme.fg('warning', s),
|
|
248
|
-
});
|
|
249
|
-
list.onSelect = (item) => done(item.value);
|
|
250
|
-
list.onCancel = () => done(undefined);
|
|
251
|
-
return {
|
|
252
|
-
render: (w: number) => list.render(w),
|
|
253
|
-
invalidate: () => list.invalidate(),
|
|
254
|
-
handleInput: (data: string) => {
|
|
255
|
-
list.handleInput(data);
|
|
256
|
-
tui.requestRender();
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
});
|
|
260
|
-
if (entry) {
|
|
261
|
-
const chosen = entries.find((e) => e.file === entry);
|
|
262
|
-
if (chosen) await viewTranscript(ctx, chosen);
|
|
263
|
-
}
|
|
271
|
+
function saveOutput(harness: string, mode: string, text: string): string {
|
|
272
|
+
const dir = outputsDirFor(harness);
|
|
273
|
+
mkdirSync(dir, { recursive: true });
|
|
274
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
275
|
+
const file = join(dir, `${stamp}-${safeSegmentName(mode)}.md`);
|
|
276
|
+
writeFileSync(file, text, 'utf8');
|
|
277
|
+
return file;
|
|
264
278
|
}
|
|
265
279
|
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
280
|
+
async function showHistory(ctx: ExtensionContext, harnessFilter?: string): Promise<void> {
|
|
281
|
+
const entries = harnessFilter ? readAllHistory().filter(e => e.harness === harnessFilter) : readAllHistory();
|
|
282
|
+
if (entries.length === 0) {
|
|
283
|
+
const msg = harnessFilter
|
|
284
|
+
? `No transcripts yet for ${harnessFilter} — run /delegate ${harnessFilter} <mode> <prompt> first`
|
|
285
|
+
: 'No transcripts yet — run /delegate <harness> <mode> <prompt> first';
|
|
286
|
+
if (!ctx.hasUI) process.stdout.write(`${msg}\n`);
|
|
287
|
+
else ctx.ui.notify?.(msg, 'info');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!ctx.hasUI) {
|
|
291
|
+
for (const e of entries)
|
|
292
|
+
process.stdout.write(`${e.harness} ${e.mode} · $${e.cost.toFixed(3)} · ${e.sessionId ?? '-'}\n`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const entry = await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
296
|
+
const items: SelectItem[] = entries.map(e => ({
|
|
297
|
+
value: e.file,
|
|
298
|
+
label: `${e.harness} ${e.mode} · $${e.cost.toFixed(3)} · ${new Date(e.mtime).toISOString().slice(0, 16)}`,
|
|
299
|
+
description: e.sessionId ? `session ${e.sessionId.slice(0, 8)}…` : undefined,
|
|
300
|
+
}));
|
|
301
|
+
const list = new SelectList(items, Math.min(items.length, 10), {
|
|
302
|
+
selectedPrefix: (s: string) => theme.fg('accent', s),
|
|
303
|
+
selectedText: (s: string) => theme.fg('accent', s),
|
|
304
|
+
description: (s: string) => theme.fg('dim', s),
|
|
305
|
+
scrollInfo: (s: string) => theme.fg('dim', s),
|
|
306
|
+
noMatch: (s: string) => theme.fg('warning', s),
|
|
307
|
+
});
|
|
308
|
+
list.onSelect = item => done(item.value);
|
|
309
|
+
list.onCancel = () => done(undefined);
|
|
310
|
+
return {
|
|
311
|
+
render: (w: number) => list.render(w),
|
|
312
|
+
invalidate: () => list.invalidate(),
|
|
313
|
+
handleInput: (data: string) => {
|
|
314
|
+
list.handleInput(data);
|
|
315
|
+
tui.requestRender();
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
if (entry) {
|
|
320
|
+
const chosen = entries.find(e => e.file === entry);
|
|
321
|
+
if (chosen) await viewTranscript(ctx, chosen);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function showStatus(ctx: ExtensionContext, harnessFilter?: string): Promise<void> {
|
|
326
|
+
const cfg = loadConfig();
|
|
327
|
+
const detection = await detectAll();
|
|
328
|
+
const allHarnesses = harnessFilter ? [harnessFilter].filter(h => isKnownHarness(h)) : HARNESS_NAMES;
|
|
329
|
+
const lines: string[] = [];
|
|
330
|
+
lines.push(`delegate — status${harnessFilter ? ` (${harnessFilter})` : ''}`);
|
|
331
|
+
lines.push(`defaultHarness: ${cfg.defaultHarness} · defaultMode: ${cfg.defaultMode} · model: ${cfg.model ?? '—'}`);
|
|
332
|
+
lines.push(
|
|
333
|
+
`maxConcurrent: ${typeof cfg.maxConcurrent === 'number' ? cfg.maxConcurrent : JSON.stringify(cfg.maxConcurrent)} · maxTranscripts: ${cfg.maxTranscripts}`,
|
|
334
|
+
);
|
|
335
|
+
lines.push('');
|
|
336
|
+
lines.push('harness binary ok version outputs templates active');
|
|
337
|
+
lines.push('─'.repeat(78));
|
|
338
|
+
for (const h of harnessFilter ? allHarnesses : HARNESS_NAMES) {
|
|
339
|
+
const det = detection[h] ?? { ok: false };
|
|
340
|
+
const harness = getHarness(h);
|
|
341
|
+
const bin = harness?.binary ?? h;
|
|
342
|
+
const ver = det.version ? det.version.slice(0, 18) : det.hint ? '—' : '—';
|
|
343
|
+
const ok = det.ok ? '✓' : '✗';
|
|
344
|
+
let outputs = 0;
|
|
345
|
+
try {
|
|
346
|
+
outputs = readdirSync(getOutputsDir(h)).filter(f => f.endsWith('.md')).length;
|
|
347
|
+
} catch {}
|
|
348
|
+
let templates = 0;
|
|
349
|
+
try {
|
|
350
|
+
templates = loadTemplates(ctx.cwd, h).size;
|
|
351
|
+
} catch {}
|
|
352
|
+
const active = activeRuns.get(h) ?? 0;
|
|
353
|
+
const hint = !det.ok && det.hint ? ` ← ${det.hint}` : '';
|
|
354
|
+
lines.push(
|
|
355
|
+
`${h.padEnd(20)} ${bin.padEnd(8)} ${ok.padEnd(3)} ${ver.padEnd(20)} ${String(outputs).padEnd(8)} ${String(templates).padEnd(10)} ${active}${hint}`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (!harnessFilter) {
|
|
359
|
+
lines.push('');
|
|
360
|
+
lines.push(
|
|
361
|
+
`global active: ${globalActiveRuns} · aliases: ${
|
|
362
|
+
Object.entries(ALIASES)
|
|
363
|
+
.map(([k, v]) => `${k}→${v}`)
|
|
364
|
+
.join(', ') || '—'
|
|
365
|
+
}`,
|
|
366
|
+
);
|
|
367
|
+
lines.push(`outputs dir: ${getOutputsDir()} (plus ${legacyOutputsDir()} legacy)`);
|
|
368
|
+
}
|
|
369
|
+
if (!ctx.hasUI) {
|
|
370
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
374
|
+
let offset = 0;
|
|
375
|
+
const height = 14;
|
|
376
|
+
return {
|
|
377
|
+
render(width: number): string[] {
|
|
378
|
+
const header = theme.fg(
|
|
379
|
+
'accent',
|
|
380
|
+
`delegate status${harnessFilter ? ` — ${harnessFilter}` : ''} (↑↓ scroll · any key to close)`,
|
|
381
|
+
);
|
|
382
|
+
const visible = lines.slice(offset, offset + height);
|
|
383
|
+
return [header, ...visible.map(l => theme.fg('muted', truncateToWidth(l, width)))];
|
|
384
|
+
},
|
|
385
|
+
handleInput(data: string): void {
|
|
386
|
+
if (matchesKey(data, Key.up) && offset > 0) {
|
|
387
|
+
offset--;
|
|
388
|
+
tui.requestRender();
|
|
389
|
+
} else if (matchesKey(data, Key.down) && offset < lines.length - 1) {
|
|
390
|
+
offset++;
|
|
391
|
+
tui.requestRender();
|
|
392
|
+
} else done(undefined);
|
|
393
|
+
},
|
|
394
|
+
invalidate() {},
|
|
395
|
+
};
|
|
396
|
+
});
|
|
273
397
|
}
|
|
274
398
|
|
|
275
|
-
function buildPrompt(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
399
|
+
function buildPrompt(
|
|
400
|
+
template: DelegateTemplate,
|
|
401
|
+
task: string,
|
|
402
|
+
scopeText: string | null,
|
|
403
|
+
cwd: string,
|
|
404
|
+
harness: string,
|
|
405
|
+
): string {
|
|
406
|
+
let prompt = [
|
|
407
|
+
`You are being delegated a subtask by the pi coding agent.`,
|
|
408
|
+
`Working directory: ${cwd}`,
|
|
409
|
+
`Harness: ${harness}`,
|
|
410
|
+
`Mode: ${template.name}`,
|
|
411
|
+
``,
|
|
412
|
+
template.prompt,
|
|
413
|
+
].join('\n');
|
|
414
|
+
prompt += `\n\n# Task\n${task}`;
|
|
415
|
+
if (scopeText) prompt += `\n\n# Scope\n${scopeText}`;
|
|
416
|
+
if (template.skill) prompt += `\n\nUse the "${template.skill}" skill.`;
|
|
417
|
+
return prompt;
|
|
281
418
|
}
|
|
282
419
|
|
|
283
420
|
async function delegate(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
): Promise<{
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
421
|
+
pi: ExtensionAPI,
|
|
422
|
+
ctx: ExtensionContext,
|
|
423
|
+
opts: DelegateOptions,
|
|
424
|
+
): Promise<{
|
|
425
|
+
content: string;
|
|
426
|
+
details: Record<string, unknown>;
|
|
427
|
+
result: import('./harnesses/types.ts').StreamedResult & { streamedText: string; harness: string };
|
|
428
|
+
activityLog: string[];
|
|
429
|
+
}> {
|
|
430
|
+
const config = loadConfig();
|
|
431
|
+
const harnessName = opts.harness ?? config.defaultHarness ?? 'claude';
|
|
432
|
+
const harness = getHarness(harnessName);
|
|
433
|
+
if (!harness)
|
|
434
|
+
throw new Error(
|
|
435
|
+
`unknown harness "${harnessName}". Available: ${HARNESS_NAMES.join(', ')} (aliases: ${Object.keys(ALIASES).join(', ')})`,
|
|
436
|
+
);
|
|
437
|
+
const templates = loadTemplates(ctx.cwd, harnessName);
|
|
438
|
+
const mode = opts.mode || config.defaultMode;
|
|
439
|
+
const template = templates.get(mode);
|
|
440
|
+
if (!template)
|
|
441
|
+
throw new Error(
|
|
442
|
+
`unknown delegate mode "${mode}" for harness "${harnessName}". Available: ${[...templates.keys()].sort().join(', ')}`,
|
|
443
|
+
);
|
|
444
|
+
const task = opts.task || template.defaultTask;
|
|
445
|
+
if (!task) throw new Error(`delegate mode "${mode}" requires a task`);
|
|
446
|
+
|
|
447
|
+
// concurrency guard
|
|
448
|
+
const maxGlobal = getMaxConcurrentGlobal();
|
|
449
|
+
const perHarnessCount = activeRuns.get(harnessName) ?? 0;
|
|
450
|
+
if (maxGlobal > 0 && globalActiveRuns >= maxGlobal)
|
|
451
|
+
throw new Error('another delegate run is already in progress (global limit)');
|
|
452
|
+
// per-harness limit if configured as object
|
|
453
|
+
const perHarnessLimit = (() => {
|
|
454
|
+
// SAFETY: maxConcurrent shape checked for perHarness record before access
|
|
455
|
+
const mc = config.maxConcurrent as unknown as { perHarness?: Record<string, number> }; // SAFETY: shape checked before access
|
|
456
|
+
if (mc && typeof mc === 'object' && mc.perHarness && typeof mc.perHarness[harnessName] === 'number')
|
|
457
|
+
return mc.perHarness[harnessName]!;
|
|
458
|
+
return maxGlobal;
|
|
459
|
+
})();
|
|
460
|
+
if (perHarnessLimit > 0 && perHarnessCount >= perHarnessLimit)
|
|
461
|
+
throw new Error(`another ${harnessName} run is already in progress`);
|
|
462
|
+
activeRuns.set(harnessName, perHarnessCount + 1);
|
|
463
|
+
globalActiveRuns++;
|
|
464
|
+
const release = () => {
|
|
465
|
+
activeRuns.set(harnessName, (activeRuns.get(harnessName) ?? 1) - 1);
|
|
466
|
+
globalActiveRuns = Math.max(0, globalActiveRuns - 1);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
let scopeText: string | null = opts.scope ?? null;
|
|
470
|
+
if (opts.scope === 'diff') {
|
|
471
|
+
const diff = await pi.exec('git', ['diff', 'HEAD'], { cwd: ctx.cwd });
|
|
472
|
+
scopeText = diff.stdout
|
|
473
|
+
? `Current git diff (working tree vs HEAD):\n${diff.stdout}`
|
|
474
|
+
: 'No git diff vs HEAD (working tree clean).';
|
|
475
|
+
} else if (opts.scope === 'pr' || opts.pr) {
|
|
476
|
+
const target = opts.pr ?? '';
|
|
477
|
+
const pr = await pi.exec('gh', target ? ['pr', 'diff', target] : ['pr', 'diff'], { cwd: ctx.cwd });
|
|
478
|
+
scopeText = pr.stdout
|
|
479
|
+
? `Pull request diff (${target || 'current branch'}):\n${pr.stdout}`
|
|
480
|
+
: `Could not resolve the PR diff${pr.stderr ? ` — ${pr.stderr.trim().slice(0, 300)}` : ''}.`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// permission: normalized, danger requires explicit per-call allowDangerous:true
|
|
484
|
+
let permission: NormalizedPermission = template.permission;
|
|
485
|
+
const nativePerm = template.nativePermission;
|
|
486
|
+
const isNativeDanger = !!nativePerm && ['bypassPermissions', 'danger-full-access', 'danger'].includes(nativePerm);
|
|
487
|
+
if (template.permission === 'danger' || isNativeDanger) {
|
|
488
|
+
if (opts.allowDangerous !== true) {
|
|
489
|
+
throw new Error(
|
|
490
|
+
`template "${mode}" requires danger permission — pass allowDangerous:true to run it (never a default)`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
permission = 'danger';
|
|
494
|
+
} else if (opts.allowDangerous === true) {
|
|
495
|
+
// explicit per-call escalation for any template
|
|
496
|
+
permission = 'danger';
|
|
497
|
+
}
|
|
498
|
+
const permissionForDisplay = nativePerm ?? permission;
|
|
499
|
+
|
|
500
|
+
const model = resolveModelForHarness(config, harnessName, opts.model, template.model);
|
|
501
|
+
const prompt = buildPrompt(template, task, scopeText, ctx.cwd, harnessName);
|
|
502
|
+
|
|
503
|
+
const activityEvents: ActivityEvent[] = [];
|
|
504
|
+
let streamedFull = '';
|
|
505
|
+
let result: import('./runner.ts').HarnessResult;
|
|
506
|
+
try {
|
|
507
|
+
result = await runHarness({
|
|
508
|
+
harness,
|
|
509
|
+
prompt,
|
|
510
|
+
cwd: ctx.cwd,
|
|
511
|
+
permission,
|
|
512
|
+
model,
|
|
513
|
+
maxBudgetUsd:
|
|
514
|
+
opts.maxBudgetUsd ??
|
|
515
|
+
template.maxBudgetUsd ??
|
|
516
|
+
config.maxBudgetUsd ??
|
|
517
|
+
config.harnesses[harnessName]?.maxBudgetUsd,
|
|
518
|
+
signal: opts.signal,
|
|
519
|
+
timeoutMs: config.harnesses[harnessName]?.timeoutMs ?? config.timeoutMs,
|
|
520
|
+
resumeSessionId: opts.sessionId,
|
|
521
|
+
onStream: t => {
|
|
522
|
+
streamedFull += t;
|
|
523
|
+
opts.onStream?.(t);
|
|
524
|
+
},
|
|
525
|
+
onActivity: ev => {
|
|
526
|
+
activityEvents.push(ev);
|
|
527
|
+
opts.onActivity?.(ev);
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
} catch (err) {
|
|
531
|
+
release();
|
|
532
|
+
if (streamedFull.length > 0) {
|
|
533
|
+
try {
|
|
534
|
+
saveOutput(
|
|
535
|
+
harnessName,
|
|
536
|
+
`${mode}-partial`,
|
|
537
|
+
buildTranscript({
|
|
538
|
+
harness: harnessName,
|
|
539
|
+
mode: `${mode} (partial)`,
|
|
540
|
+
permission: permission,
|
|
541
|
+
nativePermission: nativePerm ?? undefined,
|
|
542
|
+
model: model ?? null,
|
|
543
|
+
cwd: ctx.cwd,
|
|
544
|
+
sessionId: null,
|
|
545
|
+
resumed: Boolean(opts.sessionId),
|
|
546
|
+
numTurns: 0,
|
|
547
|
+
totalCostUsd: 0,
|
|
548
|
+
isError: true,
|
|
549
|
+
stopReason: null,
|
|
550
|
+
durationMs: null,
|
|
551
|
+
usage: null,
|
|
552
|
+
contextPercent: null,
|
|
553
|
+
contextWindow: null,
|
|
554
|
+
activityLog: collectActivityLog(activityEvents),
|
|
555
|
+
output: streamedFull,
|
|
556
|
+
}),
|
|
557
|
+
);
|
|
558
|
+
} catch (_e) {
|
|
559
|
+
void _e;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
throw err;
|
|
563
|
+
}
|
|
564
|
+
release();
|
|
565
|
+
|
|
566
|
+
if (result.isError && !result.result && !result.streamedText)
|
|
567
|
+
throw new Error(`${harnessName} reported an error and produced no output`);
|
|
568
|
+
|
|
569
|
+
const actualModel = result.model ?? model ?? null;
|
|
570
|
+
const promptTokens =
|
|
571
|
+
result.usage === null
|
|
572
|
+
? null
|
|
573
|
+
: result.usage.inputTokens + result.usage.cacheCreationInputTokens + result.usage.cacheReadInputTokens;
|
|
574
|
+
const contextPercent =
|
|
575
|
+
promptTokens !== null && result.contextWindow ? (promptTokens / result.contextWindow) * 100 : null;
|
|
576
|
+
|
|
577
|
+
const file = saveOutput(
|
|
578
|
+
harnessName,
|
|
579
|
+
mode,
|
|
580
|
+
buildTranscript({
|
|
581
|
+
harness: harnessName,
|
|
582
|
+
mode: mode,
|
|
583
|
+
permission: permission,
|
|
584
|
+
nativePermission: nativePerm ?? undefined,
|
|
585
|
+
model: actualModel,
|
|
586
|
+
cwd: ctx.cwd,
|
|
587
|
+
sessionId: result.sessionId,
|
|
588
|
+
resumed: Boolean(opts.sessionId),
|
|
589
|
+
numTurns: result.numTurns,
|
|
590
|
+
totalCostUsd: result.totalCostUsd,
|
|
591
|
+
isError: result.isError,
|
|
592
|
+
stopReason: result.stopReason,
|
|
593
|
+
durationMs: result.durationMs,
|
|
594
|
+
usage: result.usage,
|
|
595
|
+
contextPercent,
|
|
596
|
+
contextWindow: result.contextWindow,
|
|
597
|
+
activityLog: collectActivityLog(activityEvents),
|
|
598
|
+
output: result.result || result.streamedText,
|
|
599
|
+
}),
|
|
600
|
+
);
|
|
601
|
+
pruneOutputs(outputsDirFor(harnessName), config.maxTranscripts);
|
|
602
|
+
// also prune legacy if claude
|
|
603
|
+
if (harnessName === 'claude') pruneOutputs(legacyOutputsDir(), config.maxTranscripts);
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
content: result.result || result.streamedText || '(empty result)',
|
|
607
|
+
details: {
|
|
608
|
+
harness: harnessName,
|
|
609
|
+
mode,
|
|
610
|
+
permission,
|
|
611
|
+
nativePermission: nativePerm ?? null,
|
|
612
|
+
permissionMode: String(permissionForDisplay),
|
|
613
|
+
model: actualModel,
|
|
614
|
+
numTurns: result.numTurns,
|
|
615
|
+
totalCostUsd: result.totalCostUsd,
|
|
616
|
+
sessionId: result.sessionId,
|
|
617
|
+
stopReason: result.stopReason,
|
|
618
|
+
permissionDenials: result.permissionDenials,
|
|
619
|
+
isError: result.isError,
|
|
620
|
+
resumed: Boolean(opts.sessionId),
|
|
621
|
+
file,
|
|
622
|
+
durationMs: result.durationMs,
|
|
623
|
+
ttftMs: result.ttftMs,
|
|
624
|
+
contextWindow: result.contextWindow,
|
|
625
|
+
contextPercent,
|
|
626
|
+
promptTokens,
|
|
627
|
+
usage: result.usage,
|
|
628
|
+
},
|
|
629
|
+
result,
|
|
630
|
+
activityLog: collectActivityLog(activityEvents),
|
|
631
|
+
};
|
|
463
632
|
}
|
|
464
633
|
|
|
465
634
|
function summarize(content: string, max = 30_000): { text: string; truncated: boolean } {
|
|
466
|
-
|
|
467
|
-
|
|
635
|
+
if (content.length <= max) return { text: content, truncated: false };
|
|
636
|
+
return { text: `${content.slice(0, max)}\n…[truncated — full output saved to file]`, truncated: true };
|
|
468
637
|
}
|
|
469
638
|
|
|
470
639
|
interface PendingReport {
|
|
471
|
-
|
|
472
|
-
|
|
640
|
+
content: string;
|
|
641
|
+
details: Record<string, unknown>;
|
|
473
642
|
}
|
|
474
643
|
let pendingReport: PendingReport | null = null;
|
|
475
|
-
function injectReport(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
644
|
+
function injectReport(
|
|
645
|
+
_ctx: ExtensionContext,
|
|
646
|
+
opts: { harness: string; mode: string; metrics: string; body: string; file?: string; sessionId?: string },
|
|
647
|
+
): void {
|
|
648
|
+
pendingReport = {
|
|
649
|
+
content: buildReportContent({
|
|
650
|
+
harness: opts.harness,
|
|
651
|
+
mode: opts.mode,
|
|
652
|
+
metrics: opts.metrics,
|
|
653
|
+
body: opts.body,
|
|
654
|
+
file: opts.file,
|
|
655
|
+
sessionId: opts.sessionId,
|
|
656
|
+
}),
|
|
657
|
+
details: {
|
|
658
|
+
harness: opts.harness,
|
|
659
|
+
mode: opts.mode,
|
|
660
|
+
file: opts.file,
|
|
661
|
+
sessionId: opts.sessionId,
|
|
662
|
+
metrics: opts.metrics,
|
|
663
|
+
},
|
|
664
|
+
};
|
|
480
665
|
}
|
|
481
666
|
|
|
482
667
|
export default function (pi: ExtensionAPI) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
668
|
+
let activeRunId = 0;
|
|
669
|
+
let activeOverlay: { show(): void; focus(): void; runId: number } | null = null;
|
|
670
|
+
|
|
671
|
+
// ── Tools ────────────────────────────────────────────────────────────────
|
|
672
|
+
const delegateToolDef = {
|
|
673
|
+
name: 'delegate',
|
|
674
|
+
label: 'Delegate',
|
|
675
|
+
description:
|
|
676
|
+
'Delegate a task to any harness (claude, codex, opencode, amp) running headless in the repo and return its streamed report (cost, token usage, context %, session id). harness selects the backend (default from config, fallback claude). mode selects a template: review, plan, implement, security-audit, docs, general, or custom. scope restricts work: diff for current git diff, pr for PR diff, path list, or whole repo. sessionId continues a prior session.',
|
|
677
|
+
promptSnippet: 'Delegate a subtask to a harness and return its report',
|
|
678
|
+
promptGuidelines: [
|
|
679
|
+
'delegate runs a harness headless in the working directory and returns a streamed report with cost, token usage, and a session id for follow-ups.',
|
|
680
|
+
'Pass harness (claude|codex|opencode|amp) + focused task string + intent and constraints. Use scope: diff for current git diff, pr for PR diff, path list, or omit for whole repo.',
|
|
681
|
+
'mode selects the template and its permission level: review/plan/security-audit are readonly; implement/docs/general are edit. Custom template names also work.',
|
|
682
|
+
'sessionId resumes a previous delegated session instead of starting fresh.',
|
|
683
|
+
'Do not set allowDangerous unless the user explicitly asks for unrestricted access (danger permission).',
|
|
684
|
+
],
|
|
685
|
+
parameters: Type.Object({
|
|
686
|
+
harness: Type.Optional(
|
|
687
|
+
Type.String({
|
|
688
|
+
description:
|
|
689
|
+
'Harness to use: claude, codex, opencode, amp (aliases: omp). Defaults to config defaultHarness.',
|
|
690
|
+
}),
|
|
691
|
+
),
|
|
692
|
+
task: Type.String({ description: 'The task/intent to delegate. Be specific.' }),
|
|
693
|
+
mode: Type.Optional(
|
|
694
|
+
Type.String({
|
|
695
|
+
description:
|
|
696
|
+
'Template/mode to run: review, plan, implement, security-audit, docs, general, or custom. Defaults to config defaultMode.',
|
|
697
|
+
}),
|
|
698
|
+
),
|
|
699
|
+
scope: Type.Optional(
|
|
700
|
+
Type.String({
|
|
701
|
+
description:
|
|
702
|
+
'Restrict the work: diff (git diff), pr (PR diff), comma/space-separated path list, or omit for whole repo.',
|
|
703
|
+
}),
|
|
704
|
+
),
|
|
705
|
+
model: Type.Optional(
|
|
706
|
+
Type.String({ description: 'Model (e.g. sonnet, opus, gpt-5). Defaults to template/config.' }),
|
|
707
|
+
),
|
|
708
|
+
maxBudgetUsd: Type.Optional(Type.Number({ description: 'Hard spend cap in USD for the run.' })),
|
|
709
|
+
sessionId: Type.Optional(
|
|
710
|
+
Type.String({
|
|
711
|
+
description: 'Resume an existing delegated session (pass its session id from a previous run details).',
|
|
712
|
+
}),
|
|
713
|
+
),
|
|
714
|
+
allowDangerous: Type.Optional(
|
|
715
|
+
Type.Boolean({
|
|
716
|
+
description: 'Escalate to danger permission (unrestricted). Only with explicit user approval.',
|
|
717
|
+
}),
|
|
718
|
+
),
|
|
719
|
+
pr: Type.Optional(Type.String({ description: 'GitHub PR number/URL (alternative to scope pr).' })),
|
|
720
|
+
}),
|
|
721
|
+
async execute(
|
|
722
|
+
_toolCallId: string,
|
|
723
|
+
params: {
|
|
724
|
+
harness?: string;
|
|
725
|
+
task: string;
|
|
726
|
+
mode?: string;
|
|
727
|
+
scope?: string;
|
|
728
|
+
model?: string;
|
|
729
|
+
maxBudgetUsd?: number;
|
|
730
|
+
allowDangerous?: boolean;
|
|
731
|
+
sessionId?: string;
|
|
732
|
+
pr?: string;
|
|
733
|
+
},
|
|
734
|
+
signal: AbortSignal | undefined,
|
|
735
|
+
onUpdate: ((u: { content: { type: string; text: string }[]; details: { progress: number } }) => void) | undefined,
|
|
736
|
+
ctx: ExtensionContext,
|
|
737
|
+
) {
|
|
738
|
+
const config = loadConfig();
|
|
739
|
+
const feed: string[] = [];
|
|
740
|
+
let liveTail = '';
|
|
741
|
+
let thinkingChars = 0;
|
|
742
|
+
let lastPushAt = 0;
|
|
743
|
+
const THROTTLE_MS = 250;
|
|
744
|
+
const pushFeed = () => {
|
|
745
|
+
const now = Date.now();
|
|
746
|
+
if (now - lastPushAt < THROTTLE_MS) return;
|
|
747
|
+
lastPushAt = now;
|
|
748
|
+
const lines: string[] = [...feed.slice(-6)];
|
|
749
|
+
if (thinkingChars > 0)
|
|
750
|
+
lines.push(config.inspectThinking ? `💭 thinking… (${thinkingChars} chars)` : '💭 thinking…');
|
|
751
|
+
if (liveTail) lines.push(`✍ ${liveTail}`);
|
|
752
|
+
if (lines.length === 0) return;
|
|
753
|
+
onUpdate?.({ content: [{ type: 'text', text: lines.join('\n') }], details: { progress: 0.5 } });
|
|
754
|
+
};
|
|
755
|
+
const { content, details, result } = await delegate(pi, ctx, {
|
|
756
|
+
harness: params.harness,
|
|
757
|
+
task: params.task,
|
|
758
|
+
mode: params.mode,
|
|
759
|
+
scope: params.scope,
|
|
760
|
+
model: params.model,
|
|
761
|
+
maxBudgetUsd: params.maxBudgetUsd,
|
|
762
|
+
allowDangerous: params.allowDangerous === true, // invariant: never inherit from config.allowDangerous — danger requires explicit per-call approval
|
|
763
|
+
sessionId: params.sessionId,
|
|
764
|
+
pr: params.pr,
|
|
765
|
+
signal,
|
|
766
|
+
onStream: text => {
|
|
767
|
+
liveTail = (liveTail + text).slice(-400);
|
|
768
|
+
pushFeed();
|
|
769
|
+
},
|
|
770
|
+
onActivity: ev => {
|
|
771
|
+
if (ev.kind === 'tool_input') {
|
|
772
|
+
feed.push(`▶ ${formatToolUse(ev.name, ev.input)}`);
|
|
773
|
+
if (feed.length > 40) feed.splice(0, feed.length - 40);
|
|
774
|
+
} else if (ev.kind === 'tool_result') {
|
|
775
|
+
const last = feed.length - 1;
|
|
776
|
+
if (last >= 0 && feed[last].startsWith('▶')) feed[last] += ev.isError ? ' ✗' : ' ✓';
|
|
777
|
+
} else if (ev.kind === 'thinking') thinkingChars += ev.chars;
|
|
778
|
+
pushFeed();
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
const summary = summarize(content);
|
|
782
|
+
const resumed = details.resumed ? ' · resumed' : '';
|
|
783
|
+
const head = result.isError
|
|
784
|
+
? `⚠ ${details.harness} reported an error`
|
|
785
|
+
: `${details.harness} ${details.mode} (${result.numTurns} turn(s), $${result.totalCostUsd.toFixed(3)})${resumed}`;
|
|
786
|
+
const body = result.isError ? `\n${summary.text}` : `\n\n${summary.text}`;
|
|
787
|
+
const footer = summary.truncated ? `\nFull output: ${details.file}` : `\nTranscript: ${details.file}`;
|
|
788
|
+
(details as Record<string, unknown>).markdown = summary.text;
|
|
789
|
+
return {
|
|
790
|
+
content: [{ type: 'text', text: `${head}${body}${footer}` }],
|
|
791
|
+
details,
|
|
792
|
+
usage: result.usage ? mapClaudeUsage({ ...result.usage, totalCostUsd: result.totalCostUsd }) : undefined,
|
|
793
|
+
};
|
|
794
|
+
},
|
|
795
|
+
renderCall(args: unknown, theme: { fg: (c: string, s: string) => string; bg: (c: string, s: string) => string }) {
|
|
796
|
+
const params = args as { harness?: string; mode?: string; task?: string };
|
|
797
|
+
const harness = params.harness ?? 'delegate';
|
|
798
|
+
const mode = params.mode ?? 'general';
|
|
799
|
+
const task = params.task ?? '';
|
|
800
|
+
const taskStr = task ? ` — ${task.length > 60 ? `${task.slice(0, 59)}…` : task}` : '';
|
|
801
|
+
return new Text(theme.fg('accent', `${harness} ${mode}`) + theme.fg('dim', taskStr), 1, 1, s =>
|
|
802
|
+
theme.bg('toolPendingBg', s),
|
|
803
|
+
);
|
|
804
|
+
},
|
|
805
|
+
renderResult(
|
|
806
|
+
result: { content?: { type: string; text: string }[]; details?: Record<string, unknown> },
|
|
807
|
+
options: { isPartial: boolean },
|
|
808
|
+
theme: { fg: (c: string, s: string) => string; bg: (c: string, s: string) => string },
|
|
809
|
+
) {
|
|
810
|
+
if (options.isPartial) {
|
|
811
|
+
const text = (result.content ?? [])
|
|
812
|
+
.filter(c => c.type === 'text')
|
|
813
|
+
.map(c => c.text)
|
|
814
|
+
.join('\n');
|
|
815
|
+
return new Text(text, 1, 1, s => theme.bg('toolPendingBg', s));
|
|
816
|
+
}
|
|
817
|
+
const details = (result.details ?? {}) as Record<string, unknown>;
|
|
818
|
+
const harness = typeof details.harness === 'string' ? details.harness : 'delegate';
|
|
819
|
+
const mode = typeof details.mode === 'string' ? details.mode : 'delegate';
|
|
820
|
+
const cost = typeof details.totalCostUsd === 'number' ? details.totalCostUsd : 0;
|
|
821
|
+
const turns = typeof details.numTurns === 'number' ? details.numTurns : 0;
|
|
822
|
+
const isError = details.isError === true;
|
|
823
|
+
const resumed = details.resumed === true;
|
|
824
|
+
const file = typeof details.file === 'string' ? details.file : null;
|
|
825
|
+
const sessionId = typeof details.sessionId === 'string' ? details.sessionId : null;
|
|
826
|
+
const container = new Container();
|
|
827
|
+
container.addChild(
|
|
828
|
+
new Text(
|
|
829
|
+
theme.fg(isError ? 'error' : 'accent', `${harness} ${mode}`) +
|
|
830
|
+
theme.fg('dim', ` · ${turns} turn(s) · `) +
|
|
831
|
+
theme.fg('warning', `$${cost.toFixed(3)}`) +
|
|
832
|
+
(resumed ? theme.fg('dim', ' · resumed') : ''),
|
|
833
|
+
1,
|
|
834
|
+
1,
|
|
835
|
+
),
|
|
836
|
+
);
|
|
837
|
+
const md = typeof details.markdown === 'string' && details.markdown ? details.markdown : null;
|
|
838
|
+
if (md) container.addChild(new Markdown(md, 1, 1, getMarkdownTheme()));
|
|
839
|
+
else {
|
|
840
|
+
const text = (result.content ?? [])
|
|
841
|
+
.filter(c => c.type === 'text')
|
|
842
|
+
.map(c => c.text)
|
|
843
|
+
.join('\n');
|
|
844
|
+
container.addChild(new Text(text, 1, 1));
|
|
845
|
+
}
|
|
846
|
+
const foot: string[] = [];
|
|
847
|
+
if (file) foot.push(`Transcript: ${file}`);
|
|
848
|
+
if (sessionId) foot.push(`Resume: /delegate --resume=${sessionId} <prompt>`);
|
|
849
|
+
if (foot.length > 0) container.addChild(new Text(theme.fg('dim', foot.join(' ')), 1, 1));
|
|
850
|
+
return container;
|
|
851
|
+
},
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// SAFETY: delegateToolDef satisfies registerTool params via TypeBox, widened for alias registration
|
|
855
|
+
pi.registerTool(delegateToolDef as unknown as Parameters<typeof pi.registerTool>[0]); // SAFETY: delegateToolDef satisfies registerTool params
|
|
856
|
+
|
|
857
|
+
// deprecated alias
|
|
858
|
+
pi.registerTool({
|
|
859
|
+
name: 'claude_delegate',
|
|
860
|
+
label: 'Claude Delegate (deprecated)',
|
|
861
|
+
description:
|
|
862
|
+
'Deprecated alias for delegate{harness:claude}. Use delegate tool with harness:claude instead. ' +
|
|
863
|
+
(delegateToolDef as { description: string }).description,
|
|
864
|
+
promptSnippet: 'Delegate a subtask to Claude Code (deprecated alias)',
|
|
865
|
+
// SAFETY: delegateToolDef promptGuidelines is string[] from literal, safe to spread
|
|
866
|
+
promptGuidelines: [...(delegateToolDef as unknown as { promptGuidelines: string[] }).promptGuidelines], // SAFETY: promptGuidelines is string[]
|
|
867
|
+
parameters: (delegateToolDef as { parameters: unknown }).parameters as never,
|
|
868
|
+
async execute(
|
|
869
|
+
toolCallId: string,
|
|
870
|
+
params: {
|
|
871
|
+
harness?: string;
|
|
872
|
+
task: string;
|
|
873
|
+
mode?: string;
|
|
874
|
+
scope?: string;
|
|
875
|
+
model?: string;
|
|
876
|
+
maxBudgetUsd?: number;
|
|
877
|
+
allowDangerous?: boolean;
|
|
878
|
+
sessionId?: string;
|
|
879
|
+
pr?: string;
|
|
880
|
+
},
|
|
881
|
+
signal: AbortSignal | undefined,
|
|
882
|
+
onUpdate: never,
|
|
883
|
+
ctx: ExtensionContext,
|
|
884
|
+
) {
|
|
885
|
+
return (
|
|
886
|
+
// SAFETY: deprecated alias delegates to primary tool, shape identical
|
|
887
|
+
(
|
|
888
|
+
delegateToolDef as unknown as {
|
|
889
|
+
// SAFETY: alias shape identical
|
|
890
|
+
execute: (a: string, b: unknown, c: unknown, d: unknown, e: unknown) => Promise<unknown>;
|
|
891
|
+
}
|
|
892
|
+
).execute(toolCallId, { ...params, harness: 'claude' }, signal, onUpdate, ctx)
|
|
893
|
+
);
|
|
894
|
+
},
|
|
895
|
+
// SAFETY: delegateToolDef renderCall matches expected signature
|
|
896
|
+
renderCall: (delegateToolDef as unknown as { renderCall: (a: unknown, b: unknown) => unknown }).renderCall, // SAFETY: matches signature
|
|
897
|
+
// SAFETY: delegateToolDef renderResult matches expected signature
|
|
898
|
+
renderResult: (delegateToolDef as unknown as { renderResult: (a: unknown, b: unknown, c: unknown) => unknown }) // SAFETY: matches signature
|
|
899
|
+
.renderResult,
|
|
900
|
+
// SAFETY: final alias tool matches registerTool overload
|
|
901
|
+
} as unknown as Parameters<typeof pi.registerTool>[0]); // SAFETY: alias tool matches overload
|
|
902
|
+
|
|
903
|
+
// ── Commands ─────────────────────────────────────────────────────────────
|
|
904
|
+
const makeHandler = (forcedHarness?: string) => async (args: string, ctx: ExtensionContext) => {
|
|
905
|
+
const sub = args.trim();
|
|
906
|
+
const subLower = sub.toLowerCase();
|
|
907
|
+
// status / health / doctor — harness health check
|
|
908
|
+
if (subLower === 'status' || subLower === 'health' || subLower === 'doctor' || subLower === 'check') {
|
|
909
|
+
await showStatus(ctx, forcedHarness);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (
|
|
913
|
+
subLower.startsWith('status ') ||
|
|
914
|
+
subLower.startsWith('health ') ||
|
|
915
|
+
subLower.startsWith('doctor ') ||
|
|
916
|
+
subLower.startsWith('check ')
|
|
917
|
+
) {
|
|
918
|
+
const maybeH = sub.split(/\s+/)[1]?.toLowerCase();
|
|
919
|
+
const flagMatch = sub.match(/--harness=([^\s]+)/);
|
|
920
|
+
const h =
|
|
921
|
+
forcedHarness ??
|
|
922
|
+
(flagMatch ? flagMatch[1].toLowerCase() : maybeH && isKnownHarness(maybeH) ? maybeH : undefined);
|
|
923
|
+
await showStatus(ctx, h);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
// extract --harness flag for list/history subcommands
|
|
927
|
+
const harnessFlag = sub.match(/--harness=([^\s]+)/)?.[1]?.toLowerCase();
|
|
928
|
+
if (sub === 'watch' || sub === 'show') {
|
|
929
|
+
if (activeOverlay) {
|
|
930
|
+
activeOverlay.show();
|
|
931
|
+
activeOverlay.focus();
|
|
932
|
+
} else {
|
|
933
|
+
ctx.ui.notify?.('No active delegate run to show — start one with /delegate <harness> <mode> <prompt>', 'info');
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (sub === 'list' || subLower.startsWith('list ')) {
|
|
938
|
+
const h =
|
|
939
|
+
forcedHarness ??
|
|
940
|
+
harnessFlag ??
|
|
941
|
+
(subLower.startsWith('list ') ? sub.slice(5).trim().split(/\s+/)[0]?.toLowerCase() : undefined);
|
|
942
|
+
if (h && isKnownHarness(h)) {
|
|
943
|
+
await showModes(ctx, h);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (sub === 'list' || subLower === `list --harness=${h}`) {
|
|
947
|
+
await showModes(ctx, forcedHarness ?? h);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
// fallback: list without filter or with unknown word — show filtered if known, otherwise all
|
|
951
|
+
await showModes(ctx, forcedHarness);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (sub === 'history' || sub === 'logs' || subLower.startsWith('history ') || subLower.startsWith('logs ')) {
|
|
955
|
+
const h =
|
|
956
|
+
forcedHarness ??
|
|
957
|
+
harnessFlag ??
|
|
958
|
+
(subLower.startsWith('history ') || subLower.startsWith('logs ')
|
|
959
|
+
? sub.split(/\s+/)[1]?.toLowerCase()
|
|
960
|
+
: undefined);
|
|
961
|
+
if (h && isKnownHarness(h)) {
|
|
962
|
+
await showHistory(ctx, h);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
await showHistory(ctx, forcedHarness);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// combine forced harness + args for parsing
|
|
970
|
+
const rawForParse = forcedHarness ? `${forcedHarness} ${args}`.trim() : args;
|
|
971
|
+
// gather known modes across all harnesses for parsing
|
|
972
|
+
const allModes = new Set<string>();
|
|
973
|
+
for (const h of HARNESS_NAMES) for (const k of loadTemplates(ctx.cwd, h).keys()) allModes.add(k);
|
|
974
|
+
for (const k of loadTemplates(ctx.cwd).keys()) allModes.add(k);
|
|
975
|
+
const knownHarnessesSet = new Set([...HARNESS_NAMES, ...Object.keys(ALIASES)]);
|
|
976
|
+
const parsed = parseDelegateCommand(rawForParse, allModes, knownHarnessesSet);
|
|
977
|
+
// if forcedHarness provided, it wins
|
|
978
|
+
if (forcedHarness) parsed.harness = forcedHarness;
|
|
979
|
+
const harnessName = parsed.harness ?? loadConfig().defaultHarness ?? 'claude';
|
|
980
|
+
const templates = loadTemplates(ctx.cwd, harnessName);
|
|
981
|
+
const resolved = resolveDefaults(parsed, templates);
|
|
982
|
+
const template = parsed.mode ? templates.get(parsed.mode) : undefined;
|
|
983
|
+
const isDanger =
|
|
984
|
+
template?.permission === 'danger' ||
|
|
985
|
+
(template?.nativePermission
|
|
986
|
+
? ['bypassPermissions', 'danger-full-access', 'danger'].includes(template.nativePermission)
|
|
987
|
+
: false);
|
|
988
|
+
|
|
989
|
+
if (!resolved) {
|
|
990
|
+
if (parsed.mode)
|
|
991
|
+
ctx.ui.notify?.(
|
|
992
|
+
`/delegate ${parsed.mode} <what to do> — give a prompt for the "${parsed.mode}" mode`,
|
|
993
|
+
'warning',
|
|
994
|
+
);
|
|
995
|
+
else
|
|
996
|
+
ctx.ui.notify?.(
|
|
997
|
+
'Usage: /delegate [--harness=claude|codex|opencode|amp] [--mode=…] [--model=…] [--scope=…] <prompt>',
|
|
998
|
+
'warning',
|
|
999
|
+
);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const modeForDisplay = parsed.mode ?? 'general';
|
|
1003
|
+
const harnessForDisplay = harnessName;
|
|
1004
|
+
|
|
1005
|
+
const feed: FeedEntry[] = [];
|
|
1006
|
+
let thinkingChars = 0;
|
|
1007
|
+
let liveTail = '';
|
|
1008
|
+
let requestRender: (() => void) | null = null;
|
|
1009
|
+
const getEntries = (): FeedEntry[] => {
|
|
1010
|
+
const entries = [...feed.slice(-12)];
|
|
1011
|
+
if (thinkingChars > 0) entries.push({ kind: 'thinking', text: '💭 thinking…' });
|
|
1012
|
+
if (liveTail) entries.push({ kind: 'text', text: liveTail.slice(-200) });
|
|
1013
|
+
return entries;
|
|
1014
|
+
};
|
|
1015
|
+
let chipActivity = '';
|
|
1016
|
+
let chipLastPush = 0;
|
|
1017
|
+
const pushChip = () => {
|
|
1018
|
+
if (!ctx.hasUI) return;
|
|
1019
|
+
const now = Date.now();
|
|
1020
|
+
if (now - chipLastPush < 500) return;
|
|
1021
|
+
chipLastPush = now;
|
|
1022
|
+
const theme = ctx.ui.theme;
|
|
1023
|
+
const activity = chipActivity ? ` ${chipActivity}` : theme.fg('dim', ' running…');
|
|
1024
|
+
ctx.ui.setStatus(
|
|
1025
|
+
'delegate',
|
|
1026
|
+
theme.fg('accent', '●') + theme.fg('dim', ` ${harnessForDisplay} ${modeForDisplay}`) + activity,
|
|
1027
|
+
);
|
|
1028
|
+
};
|
|
1029
|
+
const onActivity = (ev: ActivityEvent) => {
|
|
1030
|
+
if (ev.kind === 'tool_input') {
|
|
1031
|
+
chipActivity = `▶ ${formatToolUse(ev.name, ev.input)}`;
|
|
1032
|
+
feed.push({ kind: 'tool', text: formatToolUse(ev.name, ev.input) });
|
|
1033
|
+
if (feed.length > 40) feed.splice(0, feed.length - 40);
|
|
1034
|
+
} else if (ev.kind === 'tool_result') {
|
|
1035
|
+
if (chipActivity.startsWith('▶')) chipActivity += ev.isError ? ' ✗' : ' ✓';
|
|
1036
|
+
const last = feed.length - 1;
|
|
1037
|
+
if (last >= 0 && feed[last].kind === 'tool') feed[last] = { ...feed[last], ok: ev.isError ? false : true };
|
|
1038
|
+
} else if (ev.kind === 'thinking') {
|
|
1039
|
+
chipActivity = '💭 thinking…';
|
|
1040
|
+
thinkingChars += ev.chars;
|
|
1041
|
+
}
|
|
1042
|
+
pushChip();
|
|
1043
|
+
requestRender?.();
|
|
1044
|
+
};
|
|
1045
|
+
const ac = new AbortController();
|
|
1046
|
+
let cancelled = false;
|
|
1047
|
+
const runState: { error: Error | null } = { error: null };
|
|
1048
|
+
const runId = ++activeRunId;
|
|
1049
|
+
const clearActive = () => {
|
|
1050
|
+
if (activeOverlay?.runId === runId) activeOverlay = null;
|
|
1051
|
+
};
|
|
1052
|
+
const run = delegate(pi, ctx, {
|
|
1053
|
+
harness: harnessName,
|
|
1054
|
+
task: resolved.task,
|
|
1055
|
+
mode: parsed.mode,
|
|
1056
|
+
scope: resolved.scope,
|
|
1057
|
+
model: parsed.model,
|
|
1058
|
+
maxBudgetUsd: parsed.budget,
|
|
1059
|
+
sessionId: parsed.sessionId,
|
|
1060
|
+
pr: parsed.pr,
|
|
1061
|
+
signal: ac.signal,
|
|
1062
|
+
onStream: t => {
|
|
1063
|
+
liveTail = (liveTail + t).slice(-400);
|
|
1064
|
+
requestRender?.();
|
|
1065
|
+
},
|
|
1066
|
+
onActivity,
|
|
1067
|
+
}).catch((err: unknown) => {
|
|
1068
|
+
runState.error = err instanceof Error ? err : new Error(String(err));
|
|
1069
|
+
return null;
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
let closeWindow: (() => void) | null = null;
|
|
1073
|
+
let result: Awaited<ReturnType<typeof delegate>> | null = null;
|
|
1074
|
+
if (ctx.hasUI) {
|
|
1075
|
+
let overlayHandle: OverlayHandle | null = null;
|
|
1076
|
+
const uiPromise = ctx.ui
|
|
1077
|
+
.custom(
|
|
1078
|
+
(tui, theme, _kb, done) => {
|
|
1079
|
+
requestRender = () => tui.requestRender();
|
|
1080
|
+
closeWindow = () => done(undefined);
|
|
1081
|
+
return progressWindow(tui, theme, {
|
|
1082
|
+
mode: `${harnessForDisplay} ${modeForDisplay}`,
|
|
1083
|
+
model:
|
|
1084
|
+
parsed.model ?? template?.model ?? loadConfig().harnesses[harnessName]?.model ?? loadConfig().model,
|
|
1085
|
+
startedAt: Date.now(),
|
|
1086
|
+
getEntries,
|
|
1087
|
+
dangerous: isDanger,
|
|
1088
|
+
onCancel: () => {
|
|
1089
|
+
cancelled = true;
|
|
1090
|
+
ac.abort();
|
|
1091
|
+
},
|
|
1092
|
+
onMinimize: () => {
|
|
1093
|
+
overlayHandle?.setHidden(true);
|
|
1094
|
+
overlayHandle?.unfocus();
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
overlay: true,
|
|
1100
|
+
overlayOptions: { width: '70%', maxHeight: '60%', anchor: 'top-center' },
|
|
1101
|
+
onHandle: h => {
|
|
1102
|
+
overlayHandle = h;
|
|
1103
|
+
activeOverlay = { show: () => h.setHidden(false), focus: () => h.focus(), runId };
|
|
1104
|
+
h.focus();
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
)
|
|
1108
|
+
.catch(() => {});
|
|
1109
|
+
result = await run;
|
|
1110
|
+
await closeWhenMounted(() => closeWindow, 2000);
|
|
1111
|
+
await uiPromise;
|
|
1112
|
+
} else {
|
|
1113
|
+
result = await run;
|
|
1114
|
+
}
|
|
1115
|
+
clearActive();
|
|
1116
|
+
if (cancelled || !result) {
|
|
1117
|
+
if (ctx.hasUI) ctx.ui.setStatus('delegate', undefined);
|
|
1118
|
+
const message = runState.error ? runState.error.message : cancelled ? 'cancelled' : 'delegation failed';
|
|
1119
|
+
if (ctx.hasUI)
|
|
1120
|
+
ctx.ui.notify(`delegate ${cancelled ? 'cancelled' : 'failed'}: ${message}`, cancelled ? 'warning' : 'error');
|
|
1121
|
+
else process.stderr.write(`${message}\n`);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const { content, details } = result;
|
|
1125
|
+
const summary = summarize(content);
|
|
1126
|
+
const file = (details.file as string) ?? null;
|
|
1127
|
+
const sessionId = (details.sessionId as string) ?? null;
|
|
1128
|
+
const resumeHint = sessionId ? ` · resume: /delegate --resume=${sessionId} <prompt>` : '';
|
|
1129
|
+
const usage = details.usage as
|
|
1130
|
+
| {
|
|
1131
|
+
inputTokens?: number;
|
|
1132
|
+
outputTokens?: number;
|
|
1133
|
+
cacheCreationInputTokens?: number;
|
|
1134
|
+
cacheReadInputTokens?: number;
|
|
1135
|
+
}
|
|
1136
|
+
| undefined;
|
|
1137
|
+
const promptTokens = usage
|
|
1138
|
+
? (usage.inputTokens ?? 0) + (usage.cacheCreationInputTokens ?? 0) + (usage.cacheReadInputTokens ?? 0)
|
|
1139
|
+
: 0;
|
|
1140
|
+
const metrics = formatMetrics({
|
|
1141
|
+
numTurns: (details.numTurns as number) ?? 0,
|
|
1142
|
+
totalCostUsd: (details.totalCostUsd as number) ?? 0,
|
|
1143
|
+
promptTokens,
|
|
1144
|
+
contextPercent: typeof details.contextPercent === 'number' ? (details.contextPercent as number) : null,
|
|
1145
|
+
durationMs:
|
|
1146
|
+
typeof details.durationMs === 'number' && details.durationMs !== null ? (details.durationMs as number) : null,
|
|
1147
|
+
});
|
|
1148
|
+
injectReport(ctx, {
|
|
1149
|
+
harness: details.harness as string,
|
|
1150
|
+
mode: details.mode as string,
|
|
1151
|
+
metrics,
|
|
1152
|
+
body: summary.text,
|
|
1153
|
+
file: file ?? undefined,
|
|
1154
|
+
sessionId: sessionId ?? undefined,
|
|
1155
|
+
});
|
|
1156
|
+
if (ctx.hasUI) {
|
|
1157
|
+
ctx.ui.setStatus('delegate', undefined);
|
|
1158
|
+
ctx.ui.notify(`${details.harness} ${details.mode} done — ${metrics}${resumeHint} · transcript: ${file}`, 'info');
|
|
1159
|
+
} else process.stdout.write(`${summary.text}\n`);
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
pi.registerCommand('delegate', {
|
|
1163
|
+
description:
|
|
1164
|
+
'Delegate a task to any harness. Usage: /delegate [--harness=claude|codex|opencode|amp] [--mode=review|plan|implement|security-audit|docs|general] [--model=...] [--scope=diff|pr|paths] [--resume=<id>] <prompt> — or use harness as first word: /delegate codex review <prompt>',
|
|
1165
|
+
handler: makeHandler(),
|
|
1166
|
+
});
|
|
1167
|
+
pi.registerCommand('claude', {
|
|
1168
|
+
description: 'Alias for /delegate --harness=claude. Usage: /claude [--mode=...] <prompt>',
|
|
1169
|
+
handler: makeHandler('claude'),
|
|
1170
|
+
});
|
|
1171
|
+
pi.registerCommand('codex', {
|
|
1172
|
+
description: 'Alias for /delegate --harness=codex. Usage: /codex [--mode=...] <prompt>',
|
|
1173
|
+
handler: makeHandler('codex'),
|
|
1174
|
+
});
|
|
1175
|
+
pi.registerCommand('opencode', {
|
|
1176
|
+
description: 'Alias for /delegate --harness=opencode. Usage: /opencode [--mode=...] <prompt>',
|
|
1177
|
+
handler: makeHandler('opencode'),
|
|
1178
|
+
});
|
|
1179
|
+
pi.registerCommand('amp', {
|
|
1180
|
+
description: 'Alias for /delegate --harness=amp. Usage: /amp [--mode=...] <prompt>',
|
|
1181
|
+
handler: makeHandler('amp'),
|
|
1182
|
+
});
|
|
1183
|
+
pi.registerCommand('omp', {
|
|
1184
|
+
description: 'Alias for /delegate --harness=amp (omp compat). Usage: /omp [--mode=...] <prompt>',
|
|
1185
|
+
handler: makeHandler('amp'),
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
pi.on('input', async (event, _ctx) => {
|
|
1189
|
+
if (event.source === 'extension') return { action: 'continue' };
|
|
1190
|
+
const hint = delegationHint(event.text, { autoDelegateHints: loadConfig().autoDelegateHints });
|
|
1191
|
+
if (!hint) return { action: 'continue' };
|
|
1192
|
+
return { action: 'transform', text: `${stripMarker(event.text)}\n\n${hint}` };
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
pi.on('before_agent_start', async () => {
|
|
1196
|
+
if (!pendingReport) return;
|
|
1197
|
+
const report = pendingReport;
|
|
1198
|
+
pendingReport = null;
|
|
1199
|
+
return { message: { customType: 'delegate', content: report.content, display: true, details: report.details } };
|
|
1200
|
+
});
|
|
816
1201
|
}
|