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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # pi-harness-delegate
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/pi-harness-delegate?logo=npm&color=CB3837)](https://www.npmjs.com/package/pi-harness-delegate) [![CI](https://github.com/yorch/pi-harness-delegate/actions/workflows/ci.yml/badge.svg)](https://github.com/yorch/pi-harness-delegate/actions/workflows/ci.yml) [![Release](https://github.com/yorch/pi-harness-delegate/actions/workflows/release.yml/badge.svg)](https://github.com/yorch/pi-harness-delegate/actions/workflows/release.yml) [![Node](https://img.shields.io/badge/node-26.x-brightgreen?logo=node.js)](https://nodejs.org) [![Bun](https://img.shields.io/badge/bun-1.3.14-black?logo=bun)](https://bun.sh) [![Biome](https://img.shields.io/badge/Biome-2.5.10-60a5fa)](https://biomejs.dev) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+
3
5
  Delegate work to **any harness** ([Claude Code](https://github.com/anthropics/claude-code), [Muse](https://github.com/openai/codex), [OpenCode](https://opencode.ai), [Amp](https://ampcode.com)) from the [pi coding agent](https://github.com/badlogic/pi-mono): code reviews, detailed plans, implementation, security audits, docs — or your own custom templates.
4
6
 
5
7
  Each harness runs headless in your repo with a normalized permission (`readonly` / `edit` / `danger`). Results stream back live, and token/cost usage feeds into pi's footer stats. Templates are portable — prompt bodies live in `templates/shared/`, harness-specific frontmatter selects the native permission.
@@ -44,7 +46,7 @@ The `delegate` tool takes: `harness`, `task`, `mode`, `scope` (`diff` = git diff
44
46
  ## Harnesses
45
47
 
46
48
  | Harness | Binary | Permission mapping | Notes |
47
- |---|---|---|---|
49
+ | --- | --- | --- | --- |
48
50
  | `claude` | `claude` | `readonly→plan`, `edit→acceptEdits`, `danger→bypassPermissions` | Full stream-json, cost + context% |
49
51
  | `codex` | `codex` | `readonly→read-only`, `edit→workspace-write`, `danger→danger-full-access` | `codex exec --json`, best-effort JSONL |
50
52
  | `opencode` | `opencode` | `readonly→read-only`, `edit→allow-edit`, `danger→danger` | `opencode run --format json` |
@@ -55,7 +57,7 @@ Detect availability: `delegate` checks `harness --version` at startup; missing h
55
57
  ## Modes (templates)
56
58
 
57
59
  | Mode | Permission | Purpose |
58
- |---|---|---|
60
+ | --- | --- | --- |
59
61
  | `review` | `readonly` | Code review, cites `file:line`, prioritized findings |
60
62
  | `plan` | `readonly` | Detailed implementation plan with steps + risks |
61
63
  | `implement` | `edit` | Implements a task, runs checks, reports changes |
@@ -167,12 +169,12 @@ Review what the harness is asked to do before granting broad permissions.
167
169
  ## Development
168
170
 
169
171
  ```bash
170
- npm install
171
- npm run typecheck
172
- npm test
172
+ bun install
173
+ bun run typecheck
174
+ bun test
173
175
  ```
174
176
 
175
- See `AGENTS.md` for architecture. Release: bump `version` in `package.json`, `npm publish --access public`, `pi update --extensions`.
177
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the project layout, the release dev-loop, and the npm publish gotchas. Agents working in this repo should read [AGENTS.md](AGENTS.md).
176
178
 
177
179
  ## License
178
180
 
@@ -3,252 +3,276 @@ import { join } from 'node:path';
3
3
  import type { ActivityEvent } from './harnesses/types.ts';
4
4
 
5
5
  function truncate(s: string, max: number): string {
6
- return s.length > max ? `${s.slice(0, max - 1)}…` : s;
6
+ return s.length > max ? `${s.slice(0, max - 1)}…` : s;
7
7
  }
8
8
 
9
9
  /** Make a template/mode name safe for use in a filename. */
10
10
  export function safeSegmentName(name: string): string {
11
- const safe = name.replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
12
- return safe.length > 0 ? safe : 'delegate';
11
+ const safe = name.replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
12
+ return safe.length > 0 ? safe : 'delegate';
13
13
  }
14
14
 
15
15
  export interface MetricsInput {
16
- numTurns: number;
17
- totalCostUsd: number;
18
- promptTokens: number;
19
- contextPercent: number | null;
20
- durationMs: number | null;
16
+ numTurns: number;
17
+ totalCostUsd: number;
18
+ promptTokens: number;
19
+ contextPercent: number | null;
20
+ durationMs: number | null;
21
21
  }
22
22
 
23
23
  /** Compact run summary: `3 turn(s) · $0.54 · 62k tok · 6.2% ctx · 12s`. */
24
24
  export function formatMetrics(m: MetricsInput): string {
25
- const parts: Array<string | null> = [
26
- `${m.numTurns} turn(s)`,
27
- `$${m.totalCostUsd.toFixed(3)}`,
28
- m.promptTokens > 0 ? `${Math.round(m.promptTokens / 1000)}k tok` : null,
29
- typeof m.contextPercent === 'number' ? `${m.contextPercent.toFixed(1)}% ctx` : null,
30
- typeof m.durationMs === 'number' && m.durationMs !== null ? `${(m.durationMs / 1000).toFixed(0)}s` : null,
31
- ];
32
- return parts.filter((p): p is string => Boolean(p)).join(' · ');
25
+ const parts: Array<string | null> = [
26
+ `${m.numTurns} turn(s)`,
27
+ `$${m.totalCostUsd.toFixed(3)}`,
28
+ m.promptTokens > 0 ? `${Math.round(m.promptTokens / 1000)}k tok` : null,
29
+ typeof m.contextPercent === 'number' ? `${m.contextPercent.toFixed(1)}% ctx` : null,
30
+ typeof m.durationMs === 'number' && m.durationMs !== null ? `${(m.durationMs / 1000).toFixed(0)}s` : null,
31
+ ];
32
+ return parts.filter((p): p is string => Boolean(p)).join(' · ');
33
33
  }
34
34
 
35
35
  /** Parse the metadata header of a transcript file (without loading the whole body). */
36
36
  export function parseTranscriptMeta(head: string): {
37
- mode: string;
38
- cost: number;
39
- sessionId: string | null;
40
- harness: string | null;
37
+ mode: string;
38
+ cost: number;
39
+ sessionId: string | null;
40
+ harness: string | null;
41
41
  } {
42
- let mode = 'delegate';
43
- let cost = 0;
44
- let sessionId: string | null = null;
45
- let harness: string | null = null;
46
- const mm = /^# Delegated (?:Claude|Harness) run — (.+)$/m.exec(head);
47
- if (mm) mode = mm[1];
48
- // Also match new header: # Delegated <harness> run — <mode>
49
- const hm = /^# Delegated (\w+) run —/m.exec(head);
50
- if (hm) harness = hm[1].toLowerCase();
51
- const cm = /\bcost: \$([\d.]+)/.exec(head);
52
- if (cm) cost = Number(cm[1]);
53
- const sm = /\bsession: ([0-9a-f-]+)/.exec(head);
54
- if (sm) sessionId = sm[1];
55
- // harness explicit field
56
- const hfm = /^-\s*harness:\s*(\w+)/m.exec(head);
57
- if (hfm) harness = hfm[1];
58
- return { mode, cost, sessionId, harness };
42
+ let mode = 'delegate';
43
+ let cost = 0;
44
+ let sessionId: string | null = null;
45
+ let harness: string | null = null;
46
+ const mm = /^# Delegated (?:Claude|Harness) run — (.+)$/m.exec(head);
47
+ if (mm) mode = mm[1];
48
+ // Also match new header: # Delegated <harness> run — <mode>
49
+ const hm = /^# Delegated (\w+) run —/m.exec(head);
50
+ if (hm) harness = hm[1].toLowerCase();
51
+ const cm = /\bcost: \$([\d.]+)/.exec(head);
52
+ if (cm) cost = Number(cm[1]);
53
+ const sm = /\bsession: ([0-9a-f-]+)/.exec(head);
54
+ if (sm) sessionId = sm[1];
55
+ // harness explicit field
56
+ const hfm = /^-\s*harness:\s*(\w+)/m.exec(head);
57
+ if (hfm) harness = hfm[1];
58
+ return { mode, cost, sessionId, harness };
59
59
  }
60
60
 
61
61
  /** Build the markdown report content injected into the session on the next turn. */
62
62
  export function buildReportContent(opts: {
63
- harness?: string;
64
- mode: string;
65
- metrics: string;
66
- body: string;
67
- file?: string;
68
- sessionId?: string;
63
+ harness?: string;
64
+ mode: string;
65
+ metrics: string;
66
+ body: string;
67
+ file?: string;
68
+ sessionId?: string;
69
69
  }): string {
70
- const harness = opts.harness ?? 'claude';
71
- const header = `## ${harness} ${opts.mode} (${opts.metrics})`;
72
- const foot: string[] = [];
73
- if (opts.file) foot.push(`transcript: ${opts.file}`);
74
- if (opts.sessionId) foot.push(`resume: \`/delegate --harness=${opts.harness} --resume=${opts.sessionId} <prompt>\` (or /${opts.harness} --resume=${opts.sessionId})`);
75
- return [header, '', opts.body, foot.length > 0 ? `\n_${foot.join(' · ')}_` : ''].join('\n');
70
+ const harness = opts.harness ?? 'claude';
71
+ const header = `## ${harness} ${opts.mode} (${opts.metrics})`;
72
+ const foot: string[] = [];
73
+ if (opts.file) foot.push(`transcript: ${opts.file}`);
74
+ if (opts.sessionId)
75
+ foot.push(
76
+ `resume: \`/delegate --harness=${opts.harness} --resume=${opts.sessionId} <prompt>\` (or /${opts.harness} --resume=${opts.sessionId})`,
77
+ );
78
+ return [header, '', opts.body, foot.length > 0 ? `\n_${foot.join(' · ')}_` : ''].join('\n');
76
79
  }
77
80
 
78
81
  /** Legacy wrapper for compat */
79
82
  export function buildClaudeReportContent(opts: {
80
- mode: string;
81
- metrics: string;
82
- body: string;
83
- file?: string;
84
- sessionId?: string;
83
+ mode: string;
84
+ metrics: string;
85
+ body: string;
86
+ file?: string;
87
+ sessionId?: string;
85
88
  }): string {
86
- return buildReportContent({ harness: 'claude', ...opts });
89
+ return buildReportContent({ harness: 'claude', ...opts });
87
90
  }
88
91
 
89
92
  /** Delete oldest transcript files beyond `maxCount` (0 = keep everything). */
90
93
  export function pruneOutputs(dir: string, maxCount: number): void {
91
- if (maxCount <= 0) return;
92
- let files: string[];
93
- try {
94
- files = readdirSync(dir);
95
- } catch {
96
- return;
97
- }
98
- const byMtime = files
99
- .filter((f) => f.endsWith('.md'))
100
- .map((f) => ({ f, mtime: statSync(join(dir, f), { throwIfNoEntry: false })?.mtimeMs ?? 0 }))
101
- .sort((a, b) => b.mtime - a.mtime);
102
- for (const { f } of byMtime.slice(maxCount)) {
103
- try {
104
- rmSync(join(dir, f));
105
- } catch {
106
- // best-effort
107
- }
108
- }
94
+ if (maxCount <= 0) return;
95
+ let files: string[];
96
+ try {
97
+ files = readdirSync(dir);
98
+ } catch {
99
+ return;
100
+ }
101
+ const byMtime = files
102
+ .filter(f => f.endsWith('.md'))
103
+ .map(f => ({ f, mtime: statSync(join(dir, f), { throwIfNoEntry: false })?.mtimeMs ?? 0 }))
104
+ .sort((a, b) => b.mtime - a.mtime);
105
+ for (const { f } of byMtime.slice(maxCount)) {
106
+ try {
107
+ rmSync(join(dir, f));
108
+ } catch {
109
+ // best-effort
110
+ }
111
+ }
109
112
  }
110
113
 
111
114
  /** Human-readable one-liner for a tool call (uses Claude's `description` when present). */
112
115
  export function formatToolUse(name: string, input: Record<string, unknown>): string {
113
- if (typeof input.description === 'string' && input.description) {
114
- return `${name}: ${truncate(input.description, 90)}`;
115
- }
116
- if (typeof input.command === 'string') return `${name}: ${truncate(input.command.split('\n')[0], 90)}`;
117
- if (typeof input.file_path === 'string') return `${name}: ${input.file_path}`;
118
- if (typeof input.pattern === 'string') return `${name}: ${input.pattern}`;
119
- if (typeof input.url === 'string') return `${name}: ${input.url}`;
120
- const first = Object.values(input).find((v): v is string => typeof v === 'string' && v.length > 0);
121
- return first ? `${name}: ${truncate(first, 90)}` : name;
116
+ if (typeof input.description === 'string' && input.description) {
117
+ return `${name}: ${truncate(input.description, 90)}`;
118
+ }
119
+ if (typeof input.command === 'string') return `${name}: ${truncate(input.command.split('\n')[0], 90)}`;
120
+ if (typeof input.file_path === 'string') return `${name}: ${input.file_path}`;
121
+ if (typeof input.pattern === 'string') return `${name}: ${input.pattern}`;
122
+ if (typeof input.url === 'string') return `${name}: ${input.url}`;
123
+ const first = Object.values(input).find((v): v is string => typeof v === 'string' && v.length > 0);
124
+ return first ? `${name}: ${truncate(first, 90)}` : name;
122
125
  }
123
126
 
124
127
  /** Compact per-line activity log for the transcript (tool_input + results only). */
125
128
  export function collectActivityLog(events: ActivityEvent[]): string[] {
126
- const log: string[] = [];
127
- for (const ev of events) {
128
- if (ev.kind === 'tool_input') {
129
- log.push(`▶ ${formatToolUse(ev.name, ev.input)}`);
130
- } else if (ev.kind === 'tool_result') {
131
- const last = log.length - 1;
132
- if (last >= 0 && log[last].startsWith('▶')) {
133
- log[last] += ev.isError ? ' ✗ error' : ' ✓';
134
- }
135
- }
136
- }
137
- return log;
129
+ const log: string[] = [];
130
+ for (const ev of events) {
131
+ if (ev.kind === 'tool_input') {
132
+ log.push(`▶ ${formatToolUse(ev.name, ev.input)}`);
133
+ } else if (ev.kind === 'tool_result') {
134
+ const last = log.length - 1;
135
+ if (last >= 0 && log[last].startsWith('▶')) {
136
+ log[last] += ev.isError ? ' ✗ error' : ' ✓';
137
+ }
138
+ }
139
+ }
140
+ return log;
138
141
  }
139
142
 
140
143
  /** Full transcript written to the outputs dir: metadata + activity + output. */
141
- export function buildTranscript(opts: {
142
- harness?: string;
143
- mode: string;
144
- permission?: string;
145
- permissionMode?: string;
146
- nativePermission?: string;
147
- model: string | null;
148
- cwd: string;
149
- sessionId: string | null;
150
- resumed: boolean;
151
- numTurns: number;
152
- totalCostUsd: number;
153
- isError: boolean;
154
- stopReason: string | null;
155
- durationMs: number | null;
156
- usage: { inputTokens: number; outputTokens: number; cacheCreationInputTokens: number; cacheReadInputTokens: number } | null;
157
- contextPercent: number | null;
158
- contextWindow: number | null;
159
- activityLog: string[];
160
- output: string;
161
- } & Record<string, unknown>): string {
162
- const harness = (opts.harness as string | undefined) ?? 'claude';
163
- const permissionRaw = (opts.permission as string | undefined) ?? (opts.permissionMode as string | undefined) ?? 'edit';
164
- let permission = permissionRaw;
165
- let nativePermission = opts.nativePermission as string | undefined;
166
- // map legacy permissionMode to normalized if needed
167
- if ((opts as Record<string, unknown>).permissionMode && !opts.permission) {
168
- const pm = (opts as Record<string, unknown>).permissionMode as string;
169
- if (pm === 'plan') { permission = 'readonly'; nativePermission = pm; }
170
- else if (pm === 'bypassPermissions') { permission = 'danger'; nativePermission = pm; }
171
- else { permission = 'edit'; nativePermission = pm; }
172
- }
173
- const u = opts.usage;
174
- const tokens = u
175
- ? [
176
- `input ${u.inputTokens}`,
177
- `output ${u.outputTokens}`,
178
- `cache+${u.cacheCreationInputTokens}`,
179
- `cache ${u.cacheReadInputTokens}`,
180
- ].join(' · ')
181
- : null;
182
- const context =
183
- opts.contextPercent !== null && opts.contextWindow
184
- ? `${opts.contextPercent.toFixed(1)}% of ${opts.contextWindow.toLocaleString()} window`
185
- : null;
186
- const duration = opts.durationMs !== null ? `${(opts.durationMs / 1000).toFixed(1)}s` : null;
187
- const permLine = nativePermission
188
- ? `- permission: ${permission} (${nativePermission})`
189
- : `- permission: ${permission}`;
144
+ export function buildTranscript(
145
+ opts: {
146
+ harness?: string;
147
+ mode: string;
148
+ permission?: string;
149
+ permissionMode?: string;
150
+ nativePermission?: string;
151
+ model: string | null;
152
+ cwd: string;
153
+ sessionId: string | null;
154
+ resumed: boolean;
155
+ numTurns: number;
156
+ totalCostUsd: number;
157
+ isError: boolean;
158
+ stopReason: string | null;
159
+ durationMs: number | null;
160
+ usage: {
161
+ inputTokens: number;
162
+ outputTokens: number;
163
+ cacheCreationInputTokens: number;
164
+ cacheReadInputTokens: number;
165
+ } | null;
166
+ contextPercent: number | null;
167
+ contextWindow: number | null;
168
+ activityLog: string[];
169
+ output: string;
170
+ } & Record<string, unknown>,
171
+ ): string {
172
+ const harness = (opts.harness as string | undefined) ?? 'claude';
173
+ const permissionRaw =
174
+ (opts.permission as string | undefined) ?? (opts.permissionMode as string | undefined) ?? 'edit';
175
+ let permission = permissionRaw;
176
+ let nativePermission = opts.nativePermission as string | undefined;
177
+ // map legacy permissionMode to normalized if needed
178
+ if ((opts as Record<string, unknown>).permissionMode && !opts.permission) {
179
+ const pm = (opts as Record<string, unknown>).permissionMode as string;
180
+ if (pm === 'plan') {
181
+ permission = 'readonly';
182
+ nativePermission = pm;
183
+ } else if (pm === 'bypassPermissions') {
184
+ permission = 'danger';
185
+ nativePermission = pm;
186
+ } else {
187
+ permission = 'edit';
188
+ nativePermission = pm;
189
+ }
190
+ }
191
+ const u = opts.usage;
192
+ const tokens = u
193
+ ? [
194
+ `input ${u.inputTokens}`,
195
+ `output ${u.outputTokens}`,
196
+ `cache+${u.cacheCreationInputTokens}`,
197
+ `cache ${u.cacheReadInputTokens}`,
198
+ ].join(' · ')
199
+ : null;
200
+ const context =
201
+ opts.contextPercent !== null && opts.contextWindow
202
+ ? `${opts.contextPercent.toFixed(1)}% of ${opts.contextWindow.toLocaleString()} window`
203
+ : null;
204
+ const duration = opts.durationMs !== null ? `${(opts.durationMs / 1000).toFixed(1)}s` : null;
205
+ const permLine = nativePermission
206
+ ? `- permission: ${permission} (${nativePermission})`
207
+ : `- permission: ${permission}`;
190
208
 
191
- return [
192
- `# Delegated ${harness.charAt(0).toUpperCase() + harness.slice(1)} run — ${opts.mode}`,
193
- '',
194
- `- harness: ${harness}`,
195
- `- mode: ${opts.mode}`,
196
- permLine,
197
- `- model: ${opts.model ?? 'default'}`,
198
- `- cwd: ${opts.cwd}`,
199
- `- session: ${opts.sessionId ?? 'n/a'}${opts.resumed ? ' (resumed)' : ''}`,
200
- `- turns: ${opts.numTurns} · cost: $${opts.totalCostUsd.toFixed(4)} · isError: ${opts.isError}`,
201
- `- tokens: ${tokens ?? 'n/a'}`,
202
- `- context: ${context ?? 'n/a'}`,
203
- `- duration: ${duration ?? 'n/a'}`,
204
- `- stop reason: ${opts.stopReason ?? 'n/a'}`,
205
- '',
206
- '## Activity',
207
- opts.activityLog.length > 0 ? opts.activityLog.join('\n') : '(no tool activity)',
208
- '',
209
- '## Output',
210
- opts.output || '(empty)',
211
- '',
212
- ].join('\n');
209
+ return [
210
+ `# Delegated ${harness.charAt(0).toUpperCase() + harness.slice(1)} run — ${opts.mode}`,
211
+ '',
212
+ `- harness: ${harness}`,
213
+ `- mode: ${opts.mode}`,
214
+ permLine,
215
+ `- model: ${opts.model ?? 'default'}`,
216
+ `- cwd: ${opts.cwd}`,
217
+ `- session: ${opts.sessionId ?? 'n/a'}${opts.resumed ? ' (resumed)' : ''}`,
218
+ `- turns: ${opts.numTurns} · cost: $${opts.totalCostUsd.toFixed(4)} · isError: ${opts.isError}`,
219
+ `- tokens: ${tokens ?? 'n/a'}`,
220
+ `- context: ${context ?? 'n/a'}`,
221
+ `- duration: ${duration ?? 'n/a'}`,
222
+ `- stop reason: ${opts.stopReason ?? 'n/a'}`,
223
+ '',
224
+ '## Activity',
225
+ opts.activityLog.length > 0 ? opts.activityLog.join('\n') : '(no tool activity)',
226
+ '',
227
+ '## Output',
228
+ opts.output || '(empty)',
229
+ '',
230
+ ].join('\n');
213
231
  }
214
232
 
215
233
  /** Legacy wrapper */
216
234
  export function buildClaudeTranscript(opts: {
217
- mode: string;
218
- permissionMode: string;
219
- model: string | null;
220
- cwd: string;
221
- sessionId: string | null;
222
- resumed: boolean;
223
- numTurns: number;
224
- totalCostUsd: number;
225
- isError: boolean;
226
- stopReason: string | null;
227
- durationMs: number | null;
228
- usage: { inputTokens: number; outputTokens: number; cacheCreationInputTokens: number; cacheReadInputTokens: number } | null;
229
- contextPercent: number | null;
230
- contextWindow: number | null;
231
- activityLog: string[];
232
- output: string;
235
+ mode: string;
236
+ permissionMode: string;
237
+ model: string | null;
238
+ cwd: string;
239
+ sessionId: string | null;
240
+ resumed: boolean;
241
+ numTurns: number;
242
+ totalCostUsd: number;
243
+ isError: boolean;
244
+ stopReason: string | null;
245
+ durationMs: number | null;
246
+ usage: {
247
+ inputTokens: number;
248
+ outputTokens: number;
249
+ cacheCreationInputTokens: number;
250
+ cacheReadInputTokens: number;
251
+ } | null;
252
+ contextPercent: number | null;
253
+ contextWindow: number | null;
254
+ activityLog: string[];
255
+ output: string;
233
256
  }): string {
234
- return buildTranscript({
235
- harness: 'claude',
236
- mode: opts.mode,
237
- permission: opts.permissionMode === 'plan' ? 'readonly' : opts.permissionMode === 'bypassPermissions' ? 'danger' : 'edit',
238
- nativePermission: opts.permissionMode,
239
- model: opts.model,
240
- cwd: opts.cwd,
241
- sessionId: opts.sessionId,
242
- resumed: opts.resumed,
243
- numTurns: opts.numTurns,
244
- totalCostUsd: opts.totalCostUsd,
245
- isError: opts.isError,
246
- stopReason: opts.stopReason,
247
- durationMs: opts.durationMs,
248
- usage: opts.usage,
249
- contextPercent: opts.contextPercent,
250
- contextWindow: opts.contextWindow,
251
- activityLog: opts.activityLog,
252
- output: opts.output,
253
- });
257
+ return buildTranscript({
258
+ harness: 'claude',
259
+ mode: opts.mode,
260
+ permission:
261
+ opts.permissionMode === 'plan' ? 'readonly' : opts.permissionMode === 'bypassPermissions' ? 'danger' : 'edit',
262
+ nativePermission: opts.permissionMode,
263
+ model: opts.model,
264
+ cwd: opts.cwd,
265
+ sessionId: opts.sessionId,
266
+ resumed: opts.resumed,
267
+ numTurns: opts.numTurns,
268
+ totalCostUsd: opts.totalCostUsd,
269
+ isError: opts.isError,
270
+ stopReason: opts.stopReason,
271
+ durationMs: opts.durationMs,
272
+ usage: opts.usage,
273
+ contextPercent: opts.contextPercent,
274
+ contextWindow: opts.contextWindow,
275
+ activityLog: opts.activityLog,
276
+ output: opts.output,
277
+ });
254
278
  }