pi-harness-delegate 0.1.0 → 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.
@@ -4,163 +4,180 @@ import { join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import type { NormalizedPermission } from './harnesses/types.ts';
6
6
 
7
- export type PermissionMode =
8
- | 'plan'
9
- | 'acceptEdits'
10
- | 'bypassPermissions'
11
- | 'dontAsk'
12
- | 'auto'
13
- | 'manual';
7
+ export type PermissionMode = 'plan' | 'acceptEdits' | 'bypassPermissions' | 'dontAsk' | 'auto' | 'manual';
14
8
 
15
9
  const PERMISSION_MODES = new Set<PermissionMode>([
16
- 'plan',
17
- 'acceptEdits',
18
- 'bypassPermissions',
19
- 'dontAsk',
20
- 'auto',
21
- 'manual',
10
+ 'plan',
11
+ 'acceptEdits',
12
+ 'bypassPermissions',
13
+ 'dontAsk',
14
+ 'auto',
15
+ 'manual',
22
16
  ]);
23
17
 
24
18
  export interface DelegateTemplate {
25
- name: string;
26
- description: string;
27
- permission: NormalizedPermission;
28
- /** Native harness permission string if user used escape hatch. */
29
- nativePermission?: string;
30
- /** Legacy raw permissionMode for transcript compat. */
31
- permissionMode: PermissionMode;
32
- model?: string;
33
- maxBudgetUsd?: number;
34
- skill?: string;
35
- defaultTask?: string;
36
- defaultScope?: string;
37
- prompt: string;
38
- harness?: string;
19
+ name: string;
20
+ description: string;
21
+ permission: NormalizedPermission;
22
+ /** Native harness permission string if user used escape hatch. */
23
+ nativePermission?: string;
24
+ /** Legacy raw permissionMode for transcript compat. */
25
+ permissionMode: PermissionMode;
26
+ model?: string;
27
+ maxBudgetUsd?: number;
28
+ skill?: string;
29
+ defaultTask?: string;
30
+ defaultScope?: string;
31
+ prompt: string;
32
+ harness?: string;
39
33
  }
40
34
 
41
- export function normalizePermission(raw: string | undefined, fallbackMode: string | undefined): { permission: NormalizedPermission; nativePermission?: string; permissionMode: PermissionMode } {
42
- // Prefer normalized permission
43
- if (raw) {
44
- const lower = raw.trim().toLowerCase();
45
- if (lower === 'readonly' || lower === 'read-only' || lower === 'read_only') return { permission: 'readonly', permissionMode: 'plan' };
46
- if (lower === 'edit' || lower === 'acceptEdits' || lower === 'accept-edits') return { permission: 'edit', permissionMode: 'acceptEdits' };
47
- if (lower === 'danger' || lower === 'bypassPermissions' || lower === 'danger-full-access' || lower === 'danger_full_access') return { permission: 'danger', permissionMode: 'bypassPermissions' };
48
- // Unknown native — treat as native escape hatch
49
- return { permission: 'edit', nativePermission: raw.trim(), permissionMode: 'acceptEdits' };
50
- }
51
- // Legacy permissionMode mapping
52
- if (fallbackMode && PERMISSION_MODES.has(fallbackMode as PermissionMode)) {
53
- const m = fallbackMode as PermissionMode;
54
- if (m === 'plan') return { permission: 'readonly', permissionMode: m };
55
- if (m === 'bypassPermissions') return { permission: 'danger', permissionMode: m };
56
- return { permission: 'edit', permissionMode: m };
57
- }
58
- return { permission: 'edit', permissionMode: 'acceptEdits' };
35
+ export function normalizePermission(
36
+ raw: string | undefined,
37
+ fallbackMode: string | undefined,
38
+ ): { permission: NormalizedPermission; nativePermission?: string; permissionMode: PermissionMode } {
39
+ // Prefer normalized permission
40
+ if (raw) {
41
+ const lower = raw.trim().toLowerCase();
42
+ if (lower === 'readonly' || lower === 'read-only' || lower === 'read_only')
43
+ return { permission: 'readonly', permissionMode: 'plan' };
44
+ if (lower === 'edit' || lower === 'acceptEdits' || lower === 'accept-edits')
45
+ return { permission: 'edit', permissionMode: 'acceptEdits' };
46
+ if (
47
+ lower === 'danger' ||
48
+ lower === 'bypassPermissions' ||
49
+ lower === 'danger-full-access' ||
50
+ lower === 'danger_full_access'
51
+ )
52
+ return { permission: 'danger', permissionMode: 'bypassPermissions' };
53
+ // Unknown native — treat as native escape hatch
54
+ return { permission: 'edit', nativePermission: raw.trim(), permissionMode: 'acceptEdits' };
55
+ }
56
+ // Legacy permissionMode mapping
57
+ if (fallbackMode && PERMISSION_MODES.has(fallbackMode as PermissionMode)) {
58
+ const m = fallbackMode as PermissionMode;
59
+ if (m === 'plan') return { permission: 'readonly', permissionMode: m };
60
+ if (m === 'bypassPermissions') return { permission: 'danger', permissionMode: m };
61
+ return { permission: 'edit', permissionMode: m };
62
+ }
63
+ return { permission: 'edit', permissionMode: 'acceptEdits' };
59
64
  }
60
65
 
61
66
  /** Parse a template file: frontmatter (---\nkey: value\n---) + markdown body. */
62
67
  export function parseTemplate(text: string): DelegateTemplate | null {
63
- const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(text.trimStart());
64
- if (!m) return null;
65
-
66
- const meta: Record<string, string> = {};
67
- for (const line of m[1].split('\n')) {
68
- const i = line.indexOf(':');
69
- if (i <= 0) continue;
70
- meta[line.slice(0, i).trim()] = line.slice(i + 1).trim();
71
- }
72
-
73
- const name = meta.name?.trim();
74
- if (!name) return null;
75
-
76
- const permRaw = meta.permission?.trim();
77
- const permModeRaw = meta.permissionMode?.trim() ?? meta.sandbox?.trim();
78
- const norm = normalizePermission(permRaw, permModeRaw);
79
-
80
- const budget = meta.maxBudgetUsd ? Number(meta.maxBudgetUsd) : NaN;
81
-
82
- return {
83
- name,
84
- description: meta.description ?? '',
85
- permission: norm.permission,
86
- nativePermission: norm.nativePermission,
87
- permissionMode: norm.permissionMode,
88
- model: meta.model || undefined,
89
- maxBudgetUsd: Number.isFinite(budget) && budget > 0 ? budget : undefined,
90
- skill: meta.skill || undefined,
91
- defaultTask: meta.defaultTask || undefined,
92
- defaultScope: meta.defaultScope || undefined,
93
- prompt: m[2].trim(),
94
- harness: meta.harness || undefined,
95
- };
68
+ const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(text.trimStart());
69
+ if (!m) return null;
70
+
71
+ const meta: Record<string, string> = {};
72
+ for (const line of m[1].split('\n')) {
73
+ const i = line.indexOf(':');
74
+ if (i <= 0) continue;
75
+ meta[line.slice(0, i).trim()] = line.slice(i + 1).trim();
76
+ }
77
+
78
+ const name = meta.name?.trim();
79
+ if (!name) return null;
80
+
81
+ const permRaw = meta.permission?.trim();
82
+ const permModeRaw = meta.permissionMode?.trim() ?? meta.sandbox?.trim();
83
+ const norm = normalizePermission(permRaw, permModeRaw);
84
+
85
+ const budget = meta.maxBudgetUsd ? Number(meta.maxBudgetUsd) : NaN;
86
+
87
+ return {
88
+ name,
89
+ description: meta.description ?? '',
90
+ permission: norm.permission,
91
+ nativePermission: norm.nativePermission,
92
+ permissionMode: norm.permissionMode,
93
+ model: meta.model || undefined,
94
+ maxBudgetUsd: Number.isFinite(budget) && budget > 0 ? budget : undefined,
95
+ skill: meta.skill || undefined,
96
+ defaultTask: meta.defaultTask || undefined,
97
+ defaultScope: meta.defaultScope || undefined,
98
+ prompt: m[2].trim(),
99
+ harness: meta.harness || undefined,
100
+ };
96
101
  }
97
102
 
98
103
  function loadDir(dir: string, out: Map<string, DelegateTemplate>): void {
99
- if (!existsSync(dir)) return;
100
- for (const f of readdirSync(dir)) {
101
- if (!f.endsWith('.md')) continue;
102
- try {
103
- const t = parseTemplate(readFileSync(join(dir, f), 'utf8'));
104
- if (t) out.set(t.name, t);
105
- } catch {
106
- // skip unreadable files
107
- }
108
- }
104
+ if (!existsSync(dir)) return;
105
+ for (const f of readdirSync(dir)) {
106
+ if (!f.endsWith('.md')) continue;
107
+ try {
108
+ const t = parseTemplate(readFileSync(join(dir, f), 'utf8'));
109
+ if (t) out.set(t.name, t);
110
+ } catch {
111
+ // skip unreadable files
112
+ }
113
+ }
109
114
  }
110
115
 
111
116
  export function builtinTemplatesDir(): string {
112
- return fileURLToPath(new URL('../templates/', import.meta.url));
117
+ return fileURLToPath(new URL('../templates/', import.meta.url));
113
118
  }
114
119
 
115
120
  export function builtinHarnessTemplatesDir(harness: string): string {
116
- return fileURLToPath(new URL(`../templates/${harness}/`, import.meta.url));
121
+ return fileURLToPath(new URL(`../templates/${harness}/`, import.meta.url));
117
122
  }
118
123
 
119
124
  export function sharedTemplatesDir(): string {
120
- return fileURLToPath(new URL('../templates/shared/', import.meta.url));
125
+ return fileURLToPath(new URL('../templates/shared/', import.meta.url));
121
126
  }
122
127
 
123
128
  export function userTemplatesDir(harness?: string): string {
124
- const dir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
125
- if (harness) return join(dir, 'delegate', 'templates', harness);
126
- return join(dir, 'delegate', 'templates');
129
+ const dir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
130
+ if (harness) return join(dir, 'delegate', 'templates', harness);
131
+ return join(dir, 'delegate', 'templates');
127
132
  }
128
133
 
129
134
  export function projectTemplatesDir(cwd: string, harness?: string): string {
130
- if (harness) return join(cwd, '.pi', 'delegate', 'templates', harness);
131
- return join(cwd, '.pi', 'delegate', 'templates');
135
+ if (harness) return join(cwd, '.pi', 'delegate', 'templates', harness);
136
+ return join(cwd, '.pi', 'delegate', 'templates');
137
+ }
138
+
139
+ /** Minimal trust gate for project-local templates — untrusted clones must not override builtins. */
140
+ function isTrusted(cwd: string): boolean {
141
+ if (process.env.PI_TRUSTED === '1' || process.env.PI_DELEGATE_TRUSTED === '1') return true;
142
+ try {
143
+ return readFileSync(join(cwd, '.pi', 'trusted'), 'utf8').trim() === '1';
144
+ } catch {
145
+ return false;
146
+ }
132
147
  }
133
148
 
134
149
  /** Legacy dirs for compat */
135
150
  function legacyUserTemplatesDir(): string {
136
- const dir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
137
- return join(dir, 'claude-delegate', 'templates');
151
+ const dir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
152
+ return join(dir, 'claude-delegate', 'templates');
138
153
  }
139
154
  function legacyProjectTemplatesDir(cwd: string): string {
140
- return join(cwd, '.pi', 'claude-delegate', 'templates');
155
+ return join(cwd, '.pi', 'claude-delegate', 'templates');
141
156
  }
142
157
 
143
158
  /** Legacy root < shared < harness builtins < legacyUser < user < user/harness < legacyProject < project < project/harness (later wins). */
144
159
  export function loadTemplates(cwd: string, harnessName?: string): Map<string, DelegateTemplate> {
145
- const out = new Map<string, DelegateTemplate>();
146
- const harness = harnessName ?? 'claude';
147
- // legacy root builtins (templates/*.md) lowest — for migration from pi-claude-delegate
148
- loadDir(builtinTemplatesDir(), out);
149
- // shared canonical bodies
150
- loadDir(sharedTemplatesDir(), out);
151
- // harness-specific builtins override shared
152
- loadDir(builtinHarnessTemplatesDir(harness), out);
153
- // user globals: legacy before new so new wins
154
- loadDir(legacyUserTemplatesDir(), out);
155
- loadDir(userTemplatesDir(), out);
156
- loadDir(userTemplatesDir(harness), out);
157
- // project locals: legacy before new so new wins
158
- loadDir(legacyProjectTemplatesDir(cwd), out);
159
- loadDir(projectTemplatesDir(cwd), out);
160
- loadDir(projectTemplatesDir(cwd, harness), out);
161
- return out;
160
+ const out = new Map<string, DelegateTemplate>();
161
+ const harness = harnessName ?? 'claude';
162
+ // legacy root builtins (templates/*.md) lowest — for migration from pi-claude-delegate
163
+ loadDir(builtinTemplatesDir(), out);
164
+ // shared canonical bodies
165
+ loadDir(sharedTemplatesDir(), out);
166
+ // harness-specific builtins override shared
167
+ loadDir(builtinHarnessTemplatesDir(harness), out);
168
+ // user globals: legacy before new so new wins
169
+ loadDir(legacyUserTemplatesDir(), out);
170
+ loadDir(userTemplatesDir(), out);
171
+ loadDir(userTemplatesDir(harness), out);
172
+ // project locals: legacy before new so new wins — only if trusted
173
+ if (isTrusted(cwd)) {
174
+ loadDir(legacyProjectTemplatesDir(cwd), out);
175
+ loadDir(projectTemplatesDir(cwd), out);
176
+ loadDir(projectTemplatesDir(cwd, harness), out);
177
+ }
178
+ return out;
162
179
  }
163
180
 
164
181
  export function loadAllTemplates(cwd: string): Map<string, DelegateTemplate> {
165
- return loadTemplates(cwd);
182
+ return loadTemplates(cwd);
166
183
  }
@@ -1,11 +1,11 @@
1
1
  import type { Usage } from '@earendil-works/pi-ai';
2
2
 
3
3
  export interface HarnessUsage {
4
- inputTokens: number;
5
- outputTokens: number;
6
- cacheCreationInputTokens: number;
7
- cacheReadInputTokens: number;
8
- totalCostUsd: number;
4
+ inputTokens: number;
5
+ outputTokens: number;
6
+ cacheCreationInputTokens: number;
7
+ cacheReadInputTokens: number;
8
+ totalCostUsd: number;
9
9
  }
10
10
 
11
11
  export type ClaudeUsage = HarnessUsage;
@@ -15,26 +15,26 @@ export type ClaudeUsage = HarnessUsage;
15
15
  * in the pi footer token/cost stats and /session totals.
16
16
  */
17
17
  export function mapHarnessUsage(u: HarnessUsage): Usage {
18
- const input = u.inputTokens + u.cacheCreationInputTokens;
19
- const cacheRead = u.cacheReadInputTokens;
20
- const output = u.outputTokens;
21
- const totalTokens = input + output + cacheRead;
22
- return {
23
- input,
24
- output,
25
- cacheRead,
26
- cacheWrite: 0,
27
- totalTokens,
28
- cost: {
29
- input: 0,
30
- output: 0,
31
- cacheRead: 0,
32
- cacheWrite: 0,
33
- total: u.totalCostUsd,
34
- },
35
- };
18
+ const input = u.inputTokens + u.cacheCreationInputTokens;
19
+ const cacheRead = u.cacheReadInputTokens;
20
+ const output = u.outputTokens;
21
+ const totalTokens = input + output + cacheRead;
22
+ return {
23
+ input,
24
+ output,
25
+ cacheRead,
26
+ cacheWrite: 0,
27
+ totalTokens,
28
+ cost: {
29
+ input: 0,
30
+ output: 0,
31
+ cacheRead: 0,
32
+ cacheWrite: 0,
33
+ total: u.totalCostUsd,
34
+ },
35
+ };
36
36
  }
37
37
 
38
38
  export function mapClaudeUsage(u: ClaudeUsage): Usage {
39
- return mapHarnessUsage(u);
39
+ return mapHarnessUsage(u);
40
40
  }
package/package.json CHANGED
@@ -1,58 +1,71 @@
1
1
  {
2
- "name": "pi-harness-delegate",
3
- "version": "0.1.0",
4
- "description": "Delegate work to any harness (Claude Code, Muse, OpenCode, Amp) from the pi coding agent — code reviews, plans, implementation, security audits, docs, or your own custom templates.",
5
- "type": "module",
6
- "files": [
7
- "extensions",
8
- "templates",
9
- "README.md",
10
- "LICENSE"
11
- ],
12
- "keywords": [
13
- "pi-package",
14
- "pi",
15
- "coding-agent",
16
- "claude",
17
- "codex",
18
- "opencode",
19
- "amp",
20
- "delegate",
21
- "subagent",
22
- "code-review",
23
- "harness"
24
- ],
25
- "license": "MIT",
26
- "author": "Jorge Barnaby",
27
- "repository": {
28
- "type": "git",
29
- "url": "git+https://github.com/yorch/pi-harness-delegate.git"
30
- },
31
- "bugs": {
32
- "url": "https://github.com/yorch/pi-harness-delegate/issues"
33
- },
34
- "scripts": {
35
- "typecheck": "tsc --noEmit",
36
- "test": "node --experimental-strip-types --test tests/**/*.test.ts"
37
- },
38
- "peerDependencies": {
39
- "@earendil-works/pi-ai": "*",
40
- "@earendil-works/pi-coding-agent": "*",
41
- "@earendil-works/pi-tui": "*",
42
- "typebox": "*"
43
- },
44
- "devDependencies": {
45
- "@earendil-works/pi-ai": "^0.84.1",
46
- "@earendil-works/pi-coding-agent": "^0.84.1",
47
- "@earendil-works/pi-tui": "^0.84.1",
48
- "@types/node": "24.x",
49
- "typebox": "^1.3.7",
50
- "typescript": "^5.9.0"
51
- },
52
- "pi": {
53
- "extensions": [
54
- "./extensions/index.ts"
55
- ],
56
- "image": "https://raw.githubusercontent.com/yorch/pi-harness-delegate/main/docs/assets/claude-delegate-preview.png"
57
- }
2
+ "name": "pi-harness-delegate",
3
+ "version": "0.2.0",
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
+ "type": "module",
6
+ "packageManager": "bun@1.3.14",
7
+ "engines": {
8
+ "node": "26.x"
9
+ },
10
+ "files": [
11
+ "extensions",
12
+ "templates",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "keywords": [
17
+ "pi-package",
18
+ "pi",
19
+ "coding-agent",
20
+ "claude",
21
+ "codex",
22
+ "opencode",
23
+ "amp",
24
+ "delegate",
25
+ "subagent",
26
+ "code-review",
27
+ "harness"
28
+ ],
29
+ "license": "MIT",
30
+ "author": "Jorge Barnaby",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/yorch/pi-harness-delegate.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/yorch/pi-harness-delegate/issues"
37
+ },
38
+ "scripts": {
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "bun test",
41
+ "lint": "biome check .",
42
+ "lint:fix": "biome check --write .",
43
+ "verify": "bun run lint && bun run typecheck && bun run test",
44
+ "changeset": "changeset",
45
+ "version-packages": "changeset version && bun install",
46
+ "release": "bun run verify && node scripts/check-packables.mjs && changeset publish"
47
+ },
48
+ "peerDependencies": {
49
+ "@earendil-works/pi-ai": "*",
50
+ "@earendil-works/pi-coding-agent": "*",
51
+ "@earendil-works/pi-tui": "*",
52
+ "typebox": "*"
53
+ },
54
+ "devDependencies": {
55
+ "@biomejs/biome": "2.5.10",
56
+ "@changesets/changelog-github": "1.0.0",
57
+ "@changesets/cli": "3.0.1",
58
+ "@earendil-works/pi-ai": "0.84.3",
59
+ "@earendil-works/pi-coding-agent": "0.84.3",
60
+ "@earendil-works/pi-tui": "0.84.3",
61
+ "@types/node": "26.3.0",
62
+ "typebox": "1.3.18",
63
+ "typescript": "7.0.2"
64
+ },
65
+ "pi": {
66
+ "extensions": [
67
+ "./extensions/index.ts"
68
+ ],
69
+ "image": "https://raw.githubusercontent.com/yorch/pi-harness-delegate/main/docs/assets/claude-delegate-preview.png"
70
+ }
58
71
  }