pi-harness-delegate 0.1.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -0
  3. package/extensions/activity.ts +254 -0
  4. package/extensions/command.ts +91 -0
  5. package/extensions/config.ts +158 -0
  6. package/extensions/harnesses/amp.ts +137 -0
  7. package/extensions/harnesses/claude.ts +129 -0
  8. package/extensions/harnesses/codex.ts +172 -0
  9. package/extensions/harnesses/opencode.ts +138 -0
  10. package/extensions/harnesses/registry.ts +51 -0
  11. package/extensions/harnesses/types.ts +93 -0
  12. package/extensions/hint.ts +57 -0
  13. package/extensions/index.ts +816 -0
  14. package/extensions/progress.ts +142 -0
  15. package/extensions/run-claude.ts +52 -0
  16. package/extensions/runner.ts +120 -0
  17. package/extensions/stream-parse.ts +29 -0
  18. package/extensions/templates.ts +166 -0
  19. package/extensions/usage.ts +40 -0
  20. package/package.json +58 -0
  21. package/templates/amp/docs.md +11 -0
  22. package/templates/amp/general.md +9 -0
  23. package/templates/amp/implement.md +13 -0
  24. package/templates/amp/plan.md +18 -0
  25. package/templates/amp/review.md +19 -0
  26. package/templates/amp/security-audit.md +18 -0
  27. package/templates/claude/docs.md +11 -0
  28. package/templates/claude/general.md +8 -0
  29. package/templates/claude/implement.md +13 -0
  30. package/templates/claude/plan.md +18 -0
  31. package/templates/claude/review.md +19 -0
  32. package/templates/claude/security-audit.md +18 -0
  33. package/templates/codex/docs.md +11 -0
  34. package/templates/codex/general.md +9 -0
  35. package/templates/codex/implement.md +13 -0
  36. package/templates/codex/plan.md +18 -0
  37. package/templates/codex/review.md +19 -0
  38. package/templates/codex/security-audit.md +18 -0
  39. package/templates/docs.md +11 -0
  40. package/templates/general.md +8 -0
  41. package/templates/implement.md +13 -0
  42. package/templates/opencode/docs.md +11 -0
  43. package/templates/opencode/general.md +9 -0
  44. package/templates/opencode/implement.md +13 -0
  45. package/templates/opencode/plan.md +18 -0
  46. package/templates/opencode/review.md +19 -0
  47. package/templates/opencode/security-audit.md +18 -0
  48. package/templates/plan.md +18 -0
  49. package/templates/review.md +19 -0
  50. package/templates/security-audit.md +18 -0
  51. package/templates/shared/docs.md +11 -0
  52. package/templates/shared/general.md +8 -0
  53. package/templates/shared/implement.md +13 -0
  54. package/templates/shared/plan.md +18 -0
  55. package/templates/shared/review.md +19 -0
  56. package/templates/shared/security-audit.md +18 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jorge Barnaby
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # pi-harness-delegate
2
+
3
+ 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
+
5
+ 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.
6
+
7
+ > Successor to `@yorch/pi-claude-delegate` (now a deprecated wrapper that re-exports the `claude` harness).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pi install npm:pi-harness-delegate
13
+ # or from git
14
+ pi install git:github.com/yorch/pi-harness-delegate
15
+ ```
16
+
17
+ Requires at least one harness binary on PATH (`claude --version`, `codex --version`, `opencode --version`, `amp --version`). Restart pi (or `/reload`) to activate.
18
+
19
+ ## Usage
20
+
21
+ The pi agent uses the `delegate` tool automatically when you ask for e.g. *"review this diff"*, *"make a plan for …"*.
22
+
23
+ Manual delegation:
24
+
25
+ ```bash
26
+ /delegate review the new auth flow # default harness (claude)
27
+ /delegate codex review the new auth flow # harness as first word
28
+ /delegate --harness=codex --mode=review --scope=diff … # explicit flags
29
+ /codex review the new auth flow # alias → delegate --harness=codex
30
+ /claude --mode=security-audit --scope=auth/ … # alias → delegate --harness=claude
31
+ /opencode plan the cache migration
32
+ /amp implement the caching layer
33
+ ```
34
+
35
+ Only the prompt is required. A **harness as first word** and/or **mode as next word** selects them; every `--flag` is optional (harness defaults to `delegate.defaultHarness`, mode to `delegate.defaultMode`, scope to whole repo).
36
+
37
+ Some modes have **default tasks** when the prompt is omitted:
38
+ `/delegate review` reviews the current git diff (`scope: diff`), `/delegate security-audit` audits the repo. Modes without a default (`plan`, `implement`, `docs`, `general`) print a hint asking for a prompt.
39
+
40
+ The `delegate` tool takes: `harness`, `task`, `mode`, `scope` (`diff` = git diff, `pr` = PR diff, path list, or whole repo), `model`, `maxBudgetUsd`, `allowDangerous`, `sessionId`, `pr`.
41
+
42
+ `claude_delegate` remains as a deprecated alias for `delegate{harness:claude}`.
43
+
44
+ ## Harnesses
45
+
46
+ | Harness | Binary | Permission mapping | Notes |
47
+ |---|---|---|---|
48
+ | `claude` | `claude` | `readonly→plan`, `edit→acceptEdits`, `danger→bypassPermissions` | Full stream-json, cost + context% |
49
+ | `codex` | `codex` | `readonly→read-only`, `edit→workspace-write`, `danger→danger-full-access` | `codex exec --json`, best-effort JSONL |
50
+ | `opencode` | `opencode` | `readonly→read-only`, `edit→allow-edit`, `danger→danger` | `opencode run --format json` |
51
+ | `amp` | `amp` (`omp` alias) | `readonly→read-only`, `edit→workspace`, `danger→danger` | `amp --output jsonl` |
52
+
53
+ Detect availability: `delegate` checks `harness --version` at startup; missing harnesses hint install instructions.
54
+
55
+ ## Modes (templates)
56
+
57
+ | Mode | Permission | Purpose |
58
+ |---|---|---|
59
+ | `review` | `readonly` | Code review, cites `file:line`, prioritized findings |
60
+ | `plan` | `readonly` | Detailed implementation plan with steps + risks |
61
+ | `implement` | `edit` | Implements a task, runs checks, reports changes |
62
+ | `security-audit` | `readonly` | Injection, auth, secrets, deserialization, supply chain |
63
+ | `docs` | `edit` | Generate/update docs matching repo style |
64
+ | `general` | `edit` | Any task |
65
+
66
+ Each mode is a markdown template with frontmatter:
67
+
68
+ ```yaml
69
+ ---
70
+ name: review
71
+ description: Code review of a scope. Read-only.
72
+ permission: readonly # normalized: readonly | edit | danger
73
+ model: sonnet # or gpt-5 for codex, etc. Aliases resolved via modelAliases
74
+ defaultTask: Review the current git diff
75
+ defaultScope: diff
76
+ ---
77
+ You are a senior code reviewer delegated by the pi coding agent.
78
+ ...
79
+ ```
80
+
81
+ **Native escape hatch:** if you need a harness-specific permission not covered by the normalized set, use the native key (`permissionMode: dontAsk`, `sandbox: ...`) — it overrides `permission` for that harness.
82
+
83
+ **Template sources (later wins):**
84
+
85
+ - `templates/shared/*.md` — portable prompt bodies
86
+ - `templates/<harness>/*.md` — harness-specific frontmatter (built-ins)
87
+ - `~/.pi/agent/delegate/templates/<harness>/<name>.md` (global)
88
+ - `.pi/delegate/templates/<harness>/<name>.md` (project — when trusted)
89
+ - Legacy `~/.pi/agent/claude-delegate/templates/` and `.pi/claude-delegate/templates/` still loaded for migration
90
+
91
+ Custom templates are just files dropped in the above dirs — any registered name becomes a valid `mode`.
92
+
93
+ ## How the main session consumes the output
94
+
95
+ - **Agent-driven (`delegate` tool)** — report is the tool result, flows into agent context.
96
+ - **Manual (`/delegate` / aliases)** — report is injected as a custom message on next `before_agent_start`, participates in LLM context; full transcript also in file.
97
+
98
+ ## Inspecting what the harness is doing
99
+
100
+ - **Live activity feed** — `▶ Bash: ... ✓/✗`, `💭 thinking…`, text tail. `/delegate` shows a framed progress window (spinner, `danger` banner for `danger` permission, `esc`×2 cancels, `m` minimizes, `watch` re-opens).
101
+ - **Full transcript every run** — `~/.pi/agent/delegate/outputs/<harness>/<ts>-<mode>.md` (also legacy `claude-delegate/outputs/` for claude). Tool result ends with path.
102
+ - **Resume:** every run records a session id.
103
+
104
+ ```bash
105
+ /delegate --resume=<session-id> follow up on the review
106
+ # or harness-specific alias:
107
+ /codex --resume=<session-id> fix the nits
108
+ ```
109
+
110
+ Reveal thinking live with `"inspectThinking": true` in config (off by default).
111
+
112
+ ## Config
113
+
114
+ In `~/.pi/agent/settings.json`:
115
+
116
+ ```json
117
+ {
118
+ "delegate": {
119
+ "defaultHarness": "claude",
120
+ "defaultMode": "general",
121
+ "model": "sonnet",
122
+ "timeoutMs": 600000,
123
+ "allowDangerous": false,
124
+ "inspectThinking": false,
125
+ "maxBudgetUsd": 3,
126
+ "autoDelegateHints": false,
127
+ "modelAliases": { "economy": "haiku", "balanced": "sonnet", "max": "opus" },
128
+ "maxConcurrent": 1,
129
+ "maxTranscripts": 100,
130
+ "harnesses": {
131
+ "claude": { "model": "sonnet" },
132
+ "codex": { "model": "gpt-5" },
133
+ "opencode": { "model": "opencode-default" }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ Legacy `claudeDelegate` is auto-migrated into `delegate.harnesses.claude` (deprecated).
140
+
141
+ - `modelAliases` — templates may use `economy|balanced|max` or any alias; resolution: call → template → harness → global.
142
+ - `maxConcurrent` — cap overlapping runs (default 1 global; may be `{global:1, perHarness:{claude:1}}`).
143
+ - `maxTranscripts` — oldest transcripts pruned beyond this count per harness (`0` disables).
144
+
145
+ `autoDelegateHints` is off by default — no system-prompt bias. When `true`, explicit markers (`@harness`, `with codex`, `delegate … to claude`) and imperative review/plan phrasing append a hint.
146
+
147
+ ## Metrics recorded
148
+
149
+ Every run records in details + transcript: harness, mode, permission (normalized + native), cost, tokens (input/output/cache), context% (prompt ÷ window), model, turns, duration, TTFT, stop reason, session id. Token + cost feed pi's `Usage`.
150
+
151
+ ## Security model
152
+
153
+ - `readonly` — no edits (e.g. Claude `plan`, Codex `read-only`).
154
+ - `edit` — workspace writes auto-accepted (e.g. `acceptEdits`, `workspace-write`).
155
+ - `danger` — unrestricted, **only via `allowDangerous:true` on the call** — never a default. Shows `⚠ danger` banner. `review`/`plan`/`security-audit` templates stay `readonly`.
156
+
157
+ Review what the harness is asked to do before granting broad permissions.
158
+
159
+ ## Migration from pi-claude-delegate
160
+
161
+ - `pi-claude-delegate` is now a deprecated wrapper. Install `pi-harness-delegate` instead.
162
+ - `claude_delegate` tool → `delegate{harness:claude}` (alias still works).
163
+ - `/claude` → `/delegate --harness=claude` (alias still works).
164
+ - Config `claudeDelegate` → `delegate` (auto-migrated; move your settings).
165
+ - Transcripts move from `~/.pi/agent/claude-delegate/outputs/` to `~/.pi/agent/delegate/outputs/<harness>/` (legacy dir still read).
166
+
167
+ ## Development
168
+
169
+ ```bash
170
+ npm install
171
+ npm run typecheck
172
+ npm test
173
+ ```
174
+
175
+ See `AGENTS.md` for architecture. Release: bump `version` in `package.json`, `npm publish --access public`, `pi update --extensions`.
176
+
177
+ ## License
178
+
179
+ MIT
@@ -0,0 +1,254 @@
1
+ import { readdirSync, rmSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { ActivityEvent } from './harnesses/types.ts';
4
+
5
+ function truncate(s: string, max: number): string {
6
+ return s.length > max ? `${s.slice(0, max - 1)}…` : s;
7
+ }
8
+
9
+ /** Make a template/mode name safe for use in a filename. */
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';
13
+ }
14
+
15
+ export interface MetricsInput {
16
+ numTurns: number;
17
+ totalCostUsd: number;
18
+ promptTokens: number;
19
+ contextPercent: number | null;
20
+ durationMs: number | null;
21
+ }
22
+
23
+ /** Compact run summary: `3 turn(s) · $0.54 · 62k tok · 6.2% ctx · 12s`. */
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(' · ');
33
+ }
34
+
35
+ /** Parse the metadata header of a transcript file (without loading the whole body). */
36
+ export function parseTranscriptMeta(head: string): {
37
+ mode: string;
38
+ cost: number;
39
+ sessionId: string | null;
40
+ harness: string | null;
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 };
59
+ }
60
+
61
+ /** Build the markdown report content injected into the session on the next turn. */
62
+ export function buildReportContent(opts: {
63
+ harness?: string;
64
+ mode: string;
65
+ metrics: string;
66
+ body: string;
67
+ file?: string;
68
+ sessionId?: string;
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');
76
+ }
77
+
78
+ /** Legacy wrapper for compat */
79
+ export function buildClaudeReportContent(opts: {
80
+ mode: string;
81
+ metrics: string;
82
+ body: string;
83
+ file?: string;
84
+ sessionId?: string;
85
+ }): string {
86
+ return buildReportContent({ harness: 'claude', ...opts });
87
+ }
88
+
89
+ /** Delete oldest transcript files beyond `maxCount` (0 = keep everything). */
90
+ 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
+ }
109
+ }
110
+
111
+ /** Human-readable one-liner for a tool call (uses Claude's `description` when present). */
112
+ 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;
122
+ }
123
+
124
+ /** Compact per-line activity log for the transcript (tool_input + results only). */
125
+ 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;
138
+ }
139
+
140
+ /** 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}`;
190
+
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');
213
+ }
214
+
215
+ /** Legacy wrapper */
216
+ 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;
233
+ }): 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
+ });
254
+ }
@@ -0,0 +1,91 @@
1
+ import type { DelegateTemplate } from './templates.ts';
2
+
3
+ /**
4
+ * Pure parser for /delegate and alias commands: --key=value flags, with optional
5
+ * harness as first word and template name as next word.
6
+ */
7
+
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;
19
+ }
20
+
21
+ export type ClaudeCommandArgs = DelegateCommandArgs;
22
+
23
+ const KNOWN_HARNESSES = new Set(['claude', 'codex', 'opencode', 'amp', 'omp']);
24
+
25
+ export function parseDelegateCommand(
26
+ raw: string,
27
+ knownModes: ReadonlySet<string>,
28
+ knownHarnesses: ReadonlySet<string> = KNOWN_HARNESSES,
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
+ });
35
+
36
+ let harness = flags.harness?.toLowerCase();
37
+ let mode = flags.mode;
38
+ let task = rest.trim();
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();
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;
66
+ }
67
+
68
+ export function parseClaudeCommand(raw: string, knownModes: ReadonlySet<string>): ClaudeCommandArgs {
69
+ return parseDelegateCommand(raw, knownModes);
70
+ }
71
+
72
+ /**
73
+ * Apply template defaults when the prompt is empty.
74
+ */
75
+ export function resolveDefaults(
76
+ args: DelegateCommandArgs,
77
+ templates: ReadonlyMap<string, DelegateTemplate>,
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;
91
+ }