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.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/extensions/activity.ts +254 -0
- package/extensions/command.ts +91 -0
- package/extensions/config.ts +158 -0
- package/extensions/harnesses/amp.ts +137 -0
- package/extensions/harnesses/claude.ts +129 -0
- package/extensions/harnesses/codex.ts +172 -0
- package/extensions/harnesses/opencode.ts +138 -0
- package/extensions/harnesses/registry.ts +51 -0
- package/extensions/harnesses/types.ts +93 -0
- package/extensions/hint.ts +57 -0
- package/extensions/index.ts +816 -0
- package/extensions/progress.ts +142 -0
- package/extensions/run-claude.ts +52 -0
- package/extensions/runner.ts +120 -0
- package/extensions/stream-parse.ts +29 -0
- package/extensions/templates.ts +166 -0
- package/extensions/usage.ts +40 -0
- package/package.json +58 -0
- package/templates/amp/docs.md +11 -0
- package/templates/amp/general.md +9 -0
- package/templates/amp/implement.md +13 -0
- package/templates/amp/plan.md +18 -0
- package/templates/amp/review.md +19 -0
- package/templates/amp/security-audit.md +18 -0
- package/templates/claude/docs.md +11 -0
- package/templates/claude/general.md +8 -0
- package/templates/claude/implement.md +13 -0
- package/templates/claude/plan.md +18 -0
- package/templates/claude/review.md +19 -0
- package/templates/claude/security-audit.md +18 -0
- package/templates/codex/docs.md +11 -0
- package/templates/codex/general.md +9 -0
- package/templates/codex/implement.md +13 -0
- package/templates/codex/plan.md +18 -0
- package/templates/codex/review.md +19 -0
- package/templates/codex/security-audit.md +18 -0
- package/templates/docs.md +11 -0
- package/templates/general.md +8 -0
- package/templates/implement.md +13 -0
- package/templates/opencode/docs.md +11 -0
- package/templates/opencode/general.md +9 -0
- package/templates/opencode/implement.md +13 -0
- package/templates/opencode/plan.md +18 -0
- package/templates/opencode/review.md +19 -0
- package/templates/opencode/security-audit.md +18 -0
- package/templates/plan.md +18 -0
- package/templates/review.md +19 -0
- package/templates/security-audit.md +18 -0
- package/templates/shared/docs.md +11 -0
- package/templates/shared/general.md +8 -0
- package/templates/shared/implement.md +13 -0
- package/templates/shared/plan.md +18 -0
- package/templates/shared/review.md +19 -0
- 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
|
+
}
|