pi-harness-delegate 0.6.2 → 0.7.1
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 +65 -9
- package/extensions/acp-runner.ts +158 -35
- package/extensions/activity.ts +91 -1
- package/extensions/command.ts +170 -13
- package/extensions/concurrency.ts +41 -5
- package/extensions/engine.ts +604 -0
- package/extensions/fanout.ts +512 -0
- package/extensions/harnesses/amp.ts +7 -6
- package/extensions/harnesses/claude.ts +2 -0
- package/extensions/harnesses/codex.ts +15 -7
- package/extensions/harnesses/opencode.ts +14 -3
- package/extensions/harnesses/registry.ts +16 -0
- package/extensions/harnesses/types.ts +8 -8
- package/extensions/history.ts +170 -0
- package/extensions/index.ts +120 -1419
- package/extensions/progress.ts +9 -3
- package/extensions/run-registry.ts +36 -3
- package/extensions/runner.ts +75 -30
- package/extensions/stream-caps.ts +46 -0
- package/extensions/subcommands.ts +227 -0
- package/extensions/templates.ts +33 -2
- package/extensions/validate.ts +214 -0
- package/package.json +4 -2
- package/extensions/run-claude.ts +0 -61
- package/extensions/stream-parse.ts +0 -37
|
@@ -46,12 +46,10 @@ export interface ParseOutcome {
|
|
|
46
46
|
streamedText?: string;
|
|
47
47
|
activities?: ActivityEvent[];
|
|
48
48
|
result?: StreamedResult | null;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
result: StreamedResult | null;
|
|
54
|
-
activities: ActivityEvent[];
|
|
49
|
+
/** The harness's own running cost total as of this line, when the line reports one mid-turn
|
|
50
|
+
* (e.g. an ACP `usage_update`) without being a final result — lets the ACP runner enforce
|
|
51
|
+
* `maxBudgetUsd` before the turn ends. Same semantics as `StreamedResult.totalCostUsd`. */
|
|
52
|
+
runningCostUsd?: number;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
export interface BuildArgsOpts {
|
|
@@ -66,8 +64,6 @@ export interface BuildArgsOpts {
|
|
|
66
64
|
resumeId?: string;
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
export type HarnessBuildOpts = BuildArgsOpts;
|
|
70
|
-
|
|
71
67
|
export interface DetectResult {
|
|
72
68
|
ok: boolean;
|
|
73
69
|
version?: string;
|
|
@@ -95,6 +91,10 @@ export interface Harness {
|
|
|
95
91
|
/** Normalized -> native CLI arg fragments, used by the 'stdout' transport's buildArgs/nativePermission. */
|
|
96
92
|
permissionMap?: Record<NormalizedPermission, string[]>;
|
|
97
93
|
permissionHint?: (permission: NormalizedPermission) => string[];
|
|
94
|
+
/** True when `buildArgs` passes `maxBudgetUsd` to a native CLI budget flag (claude's
|
|
95
|
+
* `--max-budget-usd`). Otherwise the runners enforce it host-side from the streamed
|
|
96
|
+
* `totalCostUsd` — possible only when the harness actually reports cost. */
|
|
97
|
+
nativeBudget?: boolean;
|
|
98
98
|
/** Which transports this harness's binary actually supports — the ceiling `config.harnesses.<name>.transport`
|
|
99
99
|
* is validated against (see config.ts's `resolveTransport`), independent of what a user configures.
|
|
100
100
|
* Omitted -> `[transport ?? 'stdout']` (today's single-transport harnesses). A harness legally offering
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** `/delegate history` — transcript discovery, listing, and the scrollable viewer. Split out of index.ts. */
|
|
2
|
+
|
|
3
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { Key, matchesKey, type SelectItem, SelectList, truncateToWidth } from '@earendil-works/pi-tui';
|
|
7
|
+
import { parseTranscriptMeta } from './activity.ts';
|
|
8
|
+
import { outputsDir as getOutputsDir, legacyOutputsDir } from './config.ts';
|
|
9
|
+
import { formatCost } from './engine.ts';
|
|
10
|
+
import { HARNESS_NAMES } from './harnesses/registry.ts';
|
|
11
|
+
export interface HistoryEntry {
|
|
12
|
+
file: string;
|
|
13
|
+
mode: string;
|
|
14
|
+
harness: string;
|
|
15
|
+
cost: number | null;
|
|
16
|
+
sessionId: string | null;
|
|
17
|
+
mtime: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function readHistory(dir: string, harness: string): HistoryEntry[] {
|
|
21
|
+
try {
|
|
22
|
+
return readdirSync(dir)
|
|
23
|
+
.filter(f => f.endsWith('.md') && !f.includes('-partial'))
|
|
24
|
+
.map(f => {
|
|
25
|
+
const file = join(dir, f);
|
|
26
|
+
let mode = 'delegate';
|
|
27
|
+
let cost: number | null = null;
|
|
28
|
+
let sessionId: string | null = null;
|
|
29
|
+
try {
|
|
30
|
+
const meta = parseTranscriptMeta(readFileSync(file, 'utf8').slice(0, 2000));
|
|
31
|
+
mode = meta.mode;
|
|
32
|
+
cost = meta.cost;
|
|
33
|
+
sessionId = meta.sessionId;
|
|
34
|
+
} catch (_e) {
|
|
35
|
+
void _e;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
file,
|
|
39
|
+
mode,
|
|
40
|
+
harness,
|
|
41
|
+
cost,
|
|
42
|
+
sessionId,
|
|
43
|
+
mtime: statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? 0,
|
|
44
|
+
};
|
|
45
|
+
})
|
|
46
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readAllHistory(): HistoryEntry[] {
|
|
53
|
+
const entries: HistoryEntry[] = [];
|
|
54
|
+
// new partitioned dir
|
|
55
|
+
for (const h of HARNESS_NAMES) {
|
|
56
|
+
entries.push(...readHistory(getOutputsDir(h), h));
|
|
57
|
+
}
|
|
58
|
+
// also legacy dir for migration display
|
|
59
|
+
try {
|
|
60
|
+
const legacy = readdirSync(legacyOutputsDir()).filter(f => f.endsWith('.md') && !f.includes('-partial'));
|
|
61
|
+
for (const f of legacy) {
|
|
62
|
+
const file = join(legacyOutputsDir(), f);
|
|
63
|
+
let mode = 'delegate';
|
|
64
|
+
let cost: number | null = null;
|
|
65
|
+
let sessionId: string | null = null;
|
|
66
|
+
try {
|
|
67
|
+
const meta = parseTranscriptMeta(readFileSync(file, 'utf8').slice(0, 2000));
|
|
68
|
+
mode = meta.mode;
|
|
69
|
+
cost = meta.cost;
|
|
70
|
+
sessionId = meta.sessionId;
|
|
71
|
+
} catch (_e) {
|
|
72
|
+
void _e;
|
|
73
|
+
}
|
|
74
|
+
entries.push({
|
|
75
|
+
file,
|
|
76
|
+
mode,
|
|
77
|
+
harness: 'claude',
|
|
78
|
+
cost,
|
|
79
|
+
sessionId,
|
|
80
|
+
mtime: statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? 0,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
} catch (_e) {
|
|
84
|
+
void _e;
|
|
85
|
+
}
|
|
86
|
+
return entries.sort((a, b) => b.mtime - a.mtime);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function viewTranscript(ctx: ExtensionContext, entry: HistoryEntry): Promise<void> {
|
|
90
|
+
if (!ctx.hasUI) {
|
|
91
|
+
process.stdout.write(readFileSync(entry.file, 'utf8'));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
95
|
+
const lines = readFileSync(entry.file, 'utf8').split('\n');
|
|
96
|
+
let offset = 0;
|
|
97
|
+
const height = 12;
|
|
98
|
+
return {
|
|
99
|
+
render(width: number): string[] {
|
|
100
|
+
const resume = entry.sessionId ? ` · r resume` : '';
|
|
101
|
+
const header = theme.fg('accent', `${basename(entry.file)} (↑↓ scroll${resume} · esc close)`);
|
|
102
|
+
const visible = lines.slice(offset, offset + height);
|
|
103
|
+
return [header, ...visible.map(l => theme.fg('muted', truncateToWidth(l, width)))];
|
|
104
|
+
},
|
|
105
|
+
handleInput(data: string): void {
|
|
106
|
+
if (matchesKey(data, Key.down) && offset < lines.length - 1) {
|
|
107
|
+
offset++;
|
|
108
|
+
tui.requestRender();
|
|
109
|
+
} else if (matchesKey(data, Key.up) && offset > 0) {
|
|
110
|
+
offset--;
|
|
111
|
+
tui.requestRender();
|
|
112
|
+
} else if (matchesKey(data, Key.escape)) {
|
|
113
|
+
done(undefined);
|
|
114
|
+
} else if (entry.sessionId && data === 'r') {
|
|
115
|
+
ctx.ui.notify?.(`resume with: /delegate --resume=${entry.sessionId} <prompt>`, 'info');
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
invalidate() {},
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function showHistory(ctx: ExtensionContext, harnessFilter?: string): Promise<void> {
|
|
124
|
+
const entries = harnessFilter ? readAllHistory().filter(e => e.harness === harnessFilter) : readAllHistory();
|
|
125
|
+
if (entries.length === 0) {
|
|
126
|
+
const msg = harnessFilter
|
|
127
|
+
? `No transcripts yet for ${harnessFilter} — run /delegate ${harnessFilter} <mode> <prompt> first`
|
|
128
|
+
: 'No transcripts yet — run /delegate <harness> <mode> <prompt> first';
|
|
129
|
+
if (!ctx.hasUI) process.stdout.write(`${msg}\n`);
|
|
130
|
+
else ctx.ui.notify?.(msg, 'info');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!ctx.hasUI) {
|
|
134
|
+
if (harnessFilter) process.stdout.write(`delegate — history (${harnessFilter})\n`);
|
|
135
|
+
for (const e of entries)
|
|
136
|
+
process.stdout.write(`${e.harness} ${e.mode} · ${formatCost(e.cost)} · ${e.sessionId ?? '-'}\n`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const entry = await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
140
|
+
const items: SelectItem[] = entries.map(e => ({
|
|
141
|
+
value: e.file,
|
|
142
|
+
label: `${e.harness} ${e.mode} · ${formatCost(e.cost)} · ${new Date(e.mtime).toISOString().slice(0, 16)}`,
|
|
143
|
+
description: e.sessionId ? `session ${e.sessionId.slice(0, 8)}…` : undefined,
|
|
144
|
+
}));
|
|
145
|
+
const list = new SelectList(items, Math.min(items.length, 10), {
|
|
146
|
+
selectedPrefix: (s: string) => theme.fg('accent', s),
|
|
147
|
+
selectedText: (s: string) => theme.fg('accent', s),
|
|
148
|
+
description: (s: string) => theme.fg('dim', s),
|
|
149
|
+
scrollInfo: (s: string) => theme.fg('dim', s),
|
|
150
|
+
noMatch: (s: string) => theme.fg('warning', s),
|
|
151
|
+
});
|
|
152
|
+
list.onSelect = item => done(item.value);
|
|
153
|
+
list.onCancel = () => done(undefined);
|
|
154
|
+
return {
|
|
155
|
+
render: (w: number) => {
|
|
156
|
+
const rows = list.render(w);
|
|
157
|
+
return harnessFilter ? [theme.fg('accent', `delegate — history (${harnessFilter})`), ...rows] : rows;
|
|
158
|
+
},
|
|
159
|
+
invalidate: () => list.invalidate(),
|
|
160
|
+
handleInput: (data: string) => {
|
|
161
|
+
list.handleInput(data);
|
|
162
|
+
tui.requestRender();
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
if (entry) {
|
|
167
|
+
const chosen = entries.find(e => e.file === entry);
|
|
168
|
+
if (chosen) await viewTranscript(ctx, chosen);
|
|
169
|
+
}
|
|
170
|
+
}
|