pi-harness-delegate 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/extensions/index.ts +136 -21
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# pi-harness-delegate
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/pi-harness-delegate) [](https://github.com/yorch/pi-harness-delegate/actions/workflows/ci.yml) [](https://github.com/yorch/pi-harness-delegate/actions/workflows/release.yml) [](https://nodejs.org) [](https://bun.sh) [](https://biomejs.dev) [](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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
bun install
|
|
173
|
+
bun run typecheck
|
|
174
|
+
bun test
|
|
173
175
|
```
|
|
174
176
|
|
|
175
|
-
See
|
|
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
|
|
package/extensions/index.ts
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from './activity.ts';
|
|
42
42
|
import { parseDelegateCommand, resolveDefaults } from './command.ts';
|
|
43
43
|
import { outputsDir as getOutputsDir, legacyOutputsDir, loadConfig, resolveModelForHarness } from './config.ts';
|
|
44
|
-
import { ALIASES, getHarness, HARNESS_NAMES, isKnownHarness } from './harnesses/registry.ts';
|
|
44
|
+
import { ALIASES, detectAll, getHarness, HARNESS_NAMES, isKnownHarness } from './harnesses/registry.ts';
|
|
45
45
|
import type { ActivityEvent, NormalizedPermission } from './harnesses/types.ts';
|
|
46
46
|
|
|
47
47
|
import { delegationHint, stripMarker } from './hint.ts';
|
|
@@ -268,16 +268,28 @@ async function viewTranscript(ctx: ExtensionContext, entry: HistoryEntry): Promi
|
|
|
268
268
|
});
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
|
|
272
|
-
const
|
|
271
|
+
function saveOutput(harness: string, mode: string, text: string): string {
|
|
272
|
+
const dir = outputsDirFor(harness);
|
|
273
|
+
mkdirSync(dir, { recursive: true });
|
|
274
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
275
|
+
const file = join(dir, `${stamp}-${safeSegmentName(mode)}.md`);
|
|
276
|
+
writeFileSync(file, text, 'utf8');
|
|
277
|
+
return file;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function showHistory(ctx: ExtensionContext, harnessFilter?: string): Promise<void> {
|
|
281
|
+
const entries = harnessFilter ? readAllHistory().filter(e => e.harness === harnessFilter) : readAllHistory();
|
|
273
282
|
if (entries.length === 0) {
|
|
274
|
-
|
|
283
|
+
const msg = harnessFilter
|
|
284
|
+
? `No transcripts yet for ${harnessFilter} — run /delegate ${harnessFilter} <mode> <prompt> first`
|
|
285
|
+
: 'No transcripts yet — run /delegate <harness> <mode> <prompt> first';
|
|
286
|
+
if (!ctx.hasUI) process.stdout.write(`${msg}\n`);
|
|
287
|
+
else ctx.ui.notify?.(msg, 'info');
|
|
275
288
|
return;
|
|
276
289
|
}
|
|
277
290
|
if (!ctx.hasUI) {
|
|
278
|
-
for (const e of entries)
|
|
291
|
+
for (const e of entries)
|
|
279
292
|
process.stdout.write(`${e.harness} ${e.mode} · $${e.cost.toFixed(3)} · ${e.sessionId ?? '-'}\n`);
|
|
280
|
-
}
|
|
281
293
|
return;
|
|
282
294
|
}
|
|
283
295
|
const entry = await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
@@ -310,13 +322,78 @@ async function showHistory(ctx: ExtensionContext): Promise<void> {
|
|
|
310
322
|
}
|
|
311
323
|
}
|
|
312
324
|
|
|
313
|
-
function
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
325
|
+
async function showStatus(ctx: ExtensionContext, harnessFilter?: string): Promise<void> {
|
|
326
|
+
const cfg = loadConfig();
|
|
327
|
+
const detection = await detectAll();
|
|
328
|
+
const allHarnesses = harnessFilter ? [harnessFilter].filter(h => isKnownHarness(h)) : HARNESS_NAMES;
|
|
329
|
+
const lines: string[] = [];
|
|
330
|
+
lines.push(`delegate — status${harnessFilter ? ` (${harnessFilter})` : ''}`);
|
|
331
|
+
lines.push(`defaultHarness: ${cfg.defaultHarness} · defaultMode: ${cfg.defaultMode} · model: ${cfg.model ?? '—'}`);
|
|
332
|
+
lines.push(
|
|
333
|
+
`maxConcurrent: ${typeof cfg.maxConcurrent === 'number' ? cfg.maxConcurrent : JSON.stringify(cfg.maxConcurrent)} · maxTranscripts: ${cfg.maxTranscripts}`,
|
|
334
|
+
);
|
|
335
|
+
lines.push('');
|
|
336
|
+
lines.push('harness binary ok version outputs templates active');
|
|
337
|
+
lines.push('─'.repeat(78));
|
|
338
|
+
for (const h of harnessFilter ? allHarnesses : HARNESS_NAMES) {
|
|
339
|
+
const det = detection[h] ?? { ok: false };
|
|
340
|
+
const harness = getHarness(h);
|
|
341
|
+
const bin = harness?.binary ?? h;
|
|
342
|
+
const ver = det.version ? det.version.slice(0, 18) : det.hint ? '—' : '—';
|
|
343
|
+
const ok = det.ok ? '✓' : '✗';
|
|
344
|
+
let outputs = 0;
|
|
345
|
+
try {
|
|
346
|
+
outputs = readdirSync(getOutputsDir(h)).filter(f => f.endsWith('.md')).length;
|
|
347
|
+
} catch {}
|
|
348
|
+
let templates = 0;
|
|
349
|
+
try {
|
|
350
|
+
templates = loadTemplates(ctx.cwd, h).size;
|
|
351
|
+
} catch {}
|
|
352
|
+
const active = activeRuns.get(h) ?? 0;
|
|
353
|
+
const hint = !det.ok && det.hint ? ` ← ${det.hint}` : '';
|
|
354
|
+
lines.push(
|
|
355
|
+
`${h.padEnd(20)} ${bin.padEnd(8)} ${ok.padEnd(3)} ${ver.padEnd(20)} ${String(outputs).padEnd(8)} ${String(templates).padEnd(10)} ${active}${hint}`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (!harnessFilter) {
|
|
359
|
+
lines.push('');
|
|
360
|
+
lines.push(
|
|
361
|
+
`global active: ${globalActiveRuns} · aliases: ${
|
|
362
|
+
Object.entries(ALIASES)
|
|
363
|
+
.map(([k, v]) => `${k}→${v}`)
|
|
364
|
+
.join(', ') || '—'
|
|
365
|
+
}`,
|
|
366
|
+
);
|
|
367
|
+
lines.push(`outputs dir: ${getOutputsDir()} (plus ${legacyOutputsDir()} legacy)`);
|
|
368
|
+
}
|
|
369
|
+
if (!ctx.hasUI) {
|
|
370
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
374
|
+
let offset = 0;
|
|
375
|
+
const height = 14;
|
|
376
|
+
return {
|
|
377
|
+
render(width: number): string[] {
|
|
378
|
+
const header = theme.fg(
|
|
379
|
+
'accent',
|
|
380
|
+
`delegate status${harnessFilter ? ` — ${harnessFilter}` : ''} (↑↓ scroll · any key to close)`,
|
|
381
|
+
);
|
|
382
|
+
const visible = lines.slice(offset, offset + height);
|
|
383
|
+
return [header, ...visible.map(l => theme.fg('muted', truncateToWidth(l, width)))];
|
|
384
|
+
},
|
|
385
|
+
handleInput(data: string): void {
|
|
386
|
+
if (matchesKey(data, Key.up) && offset > 0) {
|
|
387
|
+
offset--;
|
|
388
|
+
tui.requestRender();
|
|
389
|
+
} else if (matchesKey(data, Key.down) && offset < lines.length - 1) {
|
|
390
|
+
offset++;
|
|
391
|
+
tui.requestRender();
|
|
392
|
+
} else done(undefined);
|
|
393
|
+
},
|
|
394
|
+
invalidate() {},
|
|
395
|
+
};
|
|
396
|
+
});
|
|
320
397
|
}
|
|
321
398
|
|
|
322
399
|
function buildPrompt(
|
|
@@ -826,6 +903,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
826
903
|
// ── Commands ─────────────────────────────────────────────────────────────
|
|
827
904
|
const makeHandler = (forcedHarness?: string) => async (args: string, ctx: ExtensionContext) => {
|
|
828
905
|
const sub = args.trim();
|
|
906
|
+
const subLower = sub.toLowerCase();
|
|
907
|
+
// status / health / doctor — harness health check
|
|
908
|
+
if (subLower === 'status' || subLower === 'health' || subLower === 'doctor' || subLower === 'check') {
|
|
909
|
+
await showStatus(ctx, forcedHarness);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (
|
|
913
|
+
subLower.startsWith('status ') ||
|
|
914
|
+
subLower.startsWith('health ') ||
|
|
915
|
+
subLower.startsWith('doctor ') ||
|
|
916
|
+
subLower.startsWith('check ')
|
|
917
|
+
) {
|
|
918
|
+
const maybeH = sub.split(/\s+/)[1]?.toLowerCase();
|
|
919
|
+
const flagMatch = sub.match(/--harness=([^\s]+)/);
|
|
920
|
+
const h =
|
|
921
|
+
forcedHarness ??
|
|
922
|
+
(flagMatch ? flagMatch[1].toLowerCase() : maybeH && isKnownHarness(maybeH) ? maybeH : undefined);
|
|
923
|
+
await showStatus(ctx, h);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
// extract --harness flag for list/history subcommands
|
|
927
|
+
const harnessFlag = sub.match(/--harness=([^\s]+)/)?.[1]?.toLowerCase();
|
|
829
928
|
if (sub === 'watch' || sub === 'show') {
|
|
830
929
|
if (activeOverlay) {
|
|
831
930
|
activeOverlay.show();
|
|
@@ -835,19 +934,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
835
934
|
}
|
|
836
935
|
return;
|
|
837
936
|
}
|
|
838
|
-
if (sub === 'list') {
|
|
937
|
+
if (sub === 'list' || subLower.startsWith('list ')) {
|
|
938
|
+
const h =
|
|
939
|
+
forcedHarness ??
|
|
940
|
+
harnessFlag ??
|
|
941
|
+
(subLower.startsWith('list ') ? sub.slice(5).trim().split(/\s+/)[0]?.toLowerCase() : undefined);
|
|
942
|
+
if (h && isKnownHarness(h)) {
|
|
943
|
+
await showModes(ctx, h);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (sub === 'list' || subLower === `list --harness=${h}`) {
|
|
947
|
+
await showModes(ctx, forcedHarness ?? h);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
// fallback: list without filter or with unknown word — show filtered if known, otherwise all
|
|
839
951
|
await showModes(ctx, forcedHarness);
|
|
840
952
|
return;
|
|
841
953
|
}
|
|
842
|
-
if (sub.startsWith('
|
|
843
|
-
const h =
|
|
844
|
-
|
|
845
|
-
|
|
954
|
+
if (sub === 'history' || sub === 'logs' || subLower.startsWith('history ') || subLower.startsWith('logs ')) {
|
|
955
|
+
const h =
|
|
956
|
+
forcedHarness ??
|
|
957
|
+
harnessFlag ??
|
|
958
|
+
(subLower.startsWith('history ') || subLower.startsWith('logs ')
|
|
959
|
+
? sub.split(/\s+/)[1]?.toLowerCase()
|
|
960
|
+
: undefined);
|
|
961
|
+
if (h && isKnownHarness(h)) {
|
|
962
|
+
await showHistory(ctx, h);
|
|
846
963
|
return;
|
|
847
964
|
}
|
|
848
|
-
|
|
849
|
-
if (sub === 'history' || sub === 'logs') {
|
|
850
|
-
await showHistory(ctx);
|
|
965
|
+
await showHistory(ctx, forcedHarness);
|
|
851
966
|
return;
|
|
852
967
|
}
|
|
853
968
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-harness-delegate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Delegate work to any harness (Claude Code, Muse, OpenCode, Amp) from the pi coding agent \u2014 code reviews, plans, implementation, security audits, docs, or your own custom templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.3.14",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": "26
|
|
8
|
+
"node": "22 || 24 || 26"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"extensions",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@earendil-works/pi-ai": "0.84.3",
|
|
59
59
|
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
60
60
|
"@earendil-works/pi-tui": "0.84.3",
|
|
61
|
-
"@types/node": "
|
|
61
|
+
"@types/node": "22.15.32",
|
|
62
62
|
"typebox": "1.3.18",
|
|
63
63
|
"typescript": "7.0.2"
|
|
64
64
|
},
|