pi-harness-delegate 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # pi-harness-delegate
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/pi-harness-delegate?logo=npm&color=CB3837)](https://www.npmjs.com/package/pi-harness-delegate) [![CI](https://github.com/yorch/pi-harness-delegate/actions/workflows/ci.yml/badge.svg)](https://github.com/yorch/pi-harness-delegate/actions/workflows/ci.yml) [![Release](https://github.com/yorch/pi-harness-delegate/actions/workflows/release.yml/badge.svg)](https://github.com/yorch/pi-harness-delegate/actions/workflows/release.yml) [![Node](https://img.shields.io/badge/node-26.x-brightgreen?logo=node.js)](https://nodejs.org) [![Bun](https://img.shields.io/badge/bun-1.3.14-black?logo=bun)](https://bun.sh) [![Biome](https://img.shields.io/badge/Biome-2.5.10-60a5fa)](https://biomejs.dev) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+
3
5
  Delegate work to **any harness** ([Claude Code](https://github.com/anthropics/claude-code), [Muse](https://github.com/openai/codex), [OpenCode](https://opencode.ai), [Amp](https://ampcode.com)) from the [pi coding agent](https://github.com/badlogic/pi-mono): code reviews, detailed plans, implementation, security audits, docs — or your own custom templates.
4
6
 
5
7
  Each harness runs headless in your repo with a normalized permission (`readonly` / `edit` / `danger`). Results stream back live, and token/cost usage feeds into pi's footer stats. Templates are portable — prompt bodies live in `templates/shared/`, harness-specific frontmatter selects the native permission.
@@ -44,7 +46,7 @@ The `delegate` tool takes: `harness`, `task`, `mode`, `scope` (`diff` = git diff
44
46
  ## Harnesses
45
47
 
46
48
  | Harness | Binary | Permission mapping | Notes |
47
- |---|---|---|---|
49
+ | --- | --- | --- | --- |
48
50
  | `claude` | `claude` | `readonly→plan`, `edit→acceptEdits`, `danger→bypassPermissions` | Full stream-json, cost + context% |
49
51
  | `codex` | `codex` | `readonly→read-only`, `edit→workspace-write`, `danger→danger-full-access` | `codex exec --json`, best-effort JSONL |
50
52
  | `opencode` | `opencode` | `readonly→read-only`, `edit→allow-edit`, `danger→danger` | `opencode run --format json` |
@@ -55,7 +57,7 @@ Detect availability: `delegate` checks `harness --version` at startup; missing h
55
57
  ## Modes (templates)
56
58
 
57
59
  | Mode | Permission | Purpose |
58
- |---|---|---|
60
+ | --- | --- | --- |
59
61
  | `review` | `readonly` | Code review, cites `file:line`, prioritized findings |
60
62
  | `plan` | `readonly` | Detailed implementation plan with steps + risks |
61
63
  | `implement` | `edit` | Implements a task, runs checks, reports changes |
@@ -167,12 +169,12 @@ Review what the harness is asked to do before granting broad permissions.
167
169
  ## Development
168
170
 
169
171
  ```bash
170
- npm install
171
- npm run typecheck
172
- npm test
172
+ bun install
173
+ bun run typecheck
174
+ bun test
173
175
  ```
174
176
 
175
- See `AGENTS.md` for architecture. Release: bump `version` in `package.json`, `npm publish --access public`, `pi update --extensions`.
177
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the project layout, the release dev-loop, and the npm publish gotchas. Agents working in this repo should read [AGENTS.md](AGENTS.md).
176
178
 
177
179
  ## License
178
180
 
@@ -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
- async function showHistory(ctx: ExtensionContext): Promise<void> {
272
- const entries = readAllHistory();
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
- ctx.ui.notify?.('No transcripts yet — run /delegate <harness> <mode> <prompt> first', 'info');
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 saveOutput(harness: string, mode: string, text: string): string {
314
- const dir = outputsDirFor(harness);
315
- mkdirSync(dir, { recursive: true });
316
- const stamp = new Date().toISOString().replace(/[:.]/g, '-');
317
- const file = join(dir, `${stamp}-${safeSegmentName(mode)}.md`);
318
- writeFileSync(file, text, 'utf8');
319
- return file;
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('list ')) {
843
- const h = sub.slice(5).trim();
844
- if (isKnownHarness(h)) {
845
- await showModes(ctx, h);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-delegate",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
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",