pi-harness-delegate 0.1.0 → 0.1.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.
@@ -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
  }
@@ -6,16 +6,16 @@ import type { DelegateTemplate } from './templates.ts';
6
6
  */
7
7
 
8
8
  export interface DelegateCommandArgs {
9
- task: string;
10
- harness?: string;
11
- mode?: string;
12
- model?: string;
13
- scope?: string;
14
- budget?: number;
15
- /** Resume an existing delegated session (--resume=<id>). */
16
- sessionId?: string;
17
- /** GitHub PR number/URL to review (--pr=). */
18
- pr?: string;
9
+ task: string;
10
+ harness?: string;
11
+ mode?: string;
12
+ model?: string;
13
+ scope?: string;
14
+ budget?: number;
15
+ /** Resume an existing delegated session (--resume=<id>). */
16
+ sessionId?: string;
17
+ /** GitHub PR number/URL to review (--pr=). */
18
+ pr?: string;
19
19
  }
20
20
 
21
21
  export type ClaudeCommandArgs = DelegateCommandArgs;
@@ -23,69 +23,69 @@ export type ClaudeCommandArgs = DelegateCommandArgs;
23
23
  const KNOWN_HARNESSES = new Set(['claude', 'codex', 'opencode', 'amp', 'omp']);
24
24
 
25
25
  export function parseDelegateCommand(
26
- raw: string,
27
- knownModes: ReadonlySet<string>,
28
- knownHarnesses: ReadonlySet<string> = KNOWN_HARNESSES,
26
+ raw: string,
27
+ knownModes: ReadonlySet<string>,
28
+ knownHarnesses: ReadonlySet<string> = KNOWN_HARNESSES,
29
29
  ): DelegateCommandArgs {
30
- const flags: Record<string, string> = {};
31
- const rest = raw.replace(/--([a-zA-Z-]+)=(\S+)/g, (_m, k: string, v: string) => {
32
- flags[k] = v;
33
- return '';
34
- });
30
+ const flags: Record<string, string> = {};
31
+ const rest = raw.replace(/--([a-zA-Z-]+)=(\S+)/g, (_m, k: string, v: string) => {
32
+ flags[k] = v;
33
+ return '';
34
+ });
35
35
 
36
- let harness = flags.harness?.toLowerCase();
37
- let mode = flags.mode;
38
- let task = rest.trim();
36
+ let harness = flags.harness?.toLowerCase();
37
+ let mode = flags.mode;
38
+ let task = rest.trim();
39
39
 
40
- // First word handling: harness, mode, or both
41
- const words = task.split(/\s+/).filter(Boolean);
42
- let idx = 0;
43
- if (!harness && words[idx] && knownHarnesses.has(words[idx].toLowerCase())) {
44
- harness = words[idx].toLowerCase();
45
- if (harness === 'omp') harness = 'amp';
46
- idx++;
47
- }
48
- if (!mode && words[idx] && knownModes.has(words[idx])) {
49
- mode = words[idx];
50
- idx++;
51
- }
52
- if (idx > 0) task = words.slice(idx).join(' ').trim();
40
+ // First word handling: harness, mode, or both
41
+ const words = task.split(/\s+/).filter(Boolean);
42
+ let idx = 0;
43
+ if (!harness && words[idx] && knownHarnesses.has(words[idx].toLowerCase())) {
44
+ harness = words[idx].toLowerCase();
45
+ if (harness === 'omp') harness = 'amp';
46
+ idx++;
47
+ }
48
+ if (!mode && words[idx] && knownModes.has(words[idx])) {
49
+ mode = words[idx];
50
+ idx++;
51
+ }
52
+ if (idx > 0) task = words.slice(idx).join(' ').trim();
53
53
 
54
- const out: DelegateCommandArgs = { task };
55
- if (harness) out.harness = harness;
56
- if (mode) out.mode = mode;
57
- if (flags.model) out.model = flags.model;
58
- if (flags.scope) out.scope = flags.scope;
59
- if (flags.budget !== undefined) {
60
- const budget = Number(flags.budget);
61
- if (Number.isFinite(budget) && budget > 0) out.budget = budget;
62
- }
63
- if (flags.resume) out.sessionId = flags.resume;
64
- if (flags.pr) out.pr = flags.pr;
65
- return out;
54
+ const out: DelegateCommandArgs = { task };
55
+ if (harness) out.harness = harness;
56
+ if (mode) out.mode = mode;
57
+ if (flags.model) out.model = flags.model;
58
+ if (flags.scope) out.scope = flags.scope;
59
+ if (flags.budget !== undefined) {
60
+ const budget = Number(flags.budget);
61
+ if (Number.isFinite(budget) && budget > 0) out.budget = budget;
62
+ }
63
+ if (flags.resume) out.sessionId = flags.resume;
64
+ if (flags.pr) out.pr = flags.pr;
65
+ return out;
66
66
  }
67
67
 
68
68
  export function parseClaudeCommand(raw: string, knownModes: ReadonlySet<string>): ClaudeCommandArgs {
69
- return parseDelegateCommand(raw, knownModes);
69
+ return parseDelegateCommand(raw, knownModes);
70
70
  }
71
71
 
72
72
  /**
73
73
  * Apply template defaults when the prompt is empty.
74
74
  */
75
75
  export function resolveDefaults(
76
- args: DelegateCommandArgs,
77
- templates: ReadonlyMap<string, DelegateTemplate>,
76
+ args: DelegateCommandArgs,
77
+ templates: ReadonlyMap<string, DelegateTemplate>,
78
78
  ): { task: string; scope?: string } | null {
79
- if (args.task) {
80
- return args.scope ? { task: args.task, scope: args.scope } : { task: args.task };
81
- }
82
- if (args.mode) {
83
- const t = templates.get(args.mode);
84
- if (t?.defaultTask) {
85
- const scope = args.scope ?? t.defaultScope;
86
- return scope ? { task: t.defaultTask, scope } : { task: t.defaultTask };
87
- }
88
- return null;
89
- }
90
- return null;
79
+ if (args.task) {
80
+ return args.scope ? { task: args.task, scope: args.scope } : { task: args.task };
81
+ }
82
+ if (args.mode) {
83
+ const t = templates.get(args.mode);
84
+ if (t?.defaultTask) {
85
+ const scope = args.scope ?? t.defaultScope;
86
+ return scope ? { task: t.defaultTask, scope } : { task: t.defaultTask };
87
+ }
88
+ return null;
89
+ }
90
+ return null;
91
91
  }