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.
- package/README.md +8 -6
- package/extensions/activity.ts +225 -201
- package/extensions/command.ts +61 -61
- package/extensions/config.ts +137 -131
- package/extensions/harnesses/amp.ts +122 -115
- package/extensions/harnesses/claude.ts +136 -111
- package/extensions/harnesses/codex.ts +191 -147
- package/extensions/harnesses/opencode.ts +133 -116
- package/extensions/harnesses/registry.ts +22 -22
- package/extensions/harnesses/types.ts +60 -60
- package/extensions/hint.ts +22 -19
- package/extensions/index.ts +1128 -743
- package/extensions/progress.ts +104 -104
- package/extensions/run-claude.ts +44 -35
- package/extensions/runner.ts +111 -99
- package/extensions/stream-parse.ts +32 -24
- package/extensions/templates.ts +134 -117
- package/extensions/usage.ts +24 -24
- package/package.json +69 -56
package/extensions/templates.ts
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
'plan',
|
|
11
|
+
'acceptEdits',
|
|
12
|
+
'bypassPermissions',
|
|
13
|
+
'dontAsk',
|
|
14
|
+
'auto',
|
|
15
|
+
'manual',
|
|
22
16
|
]);
|
|
23
17
|
|
|
24
18
|
export interface DelegateTemplate {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
117
|
+
return fileURLToPath(new URL('../templates/', import.meta.url));
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
export function builtinHarnessTemplatesDir(harness: string): string {
|
|
116
|
-
|
|
121
|
+
return fileURLToPath(new URL(`../templates/${harness}/`, import.meta.url));
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
export function sharedTemplatesDir(): string {
|
|
120
|
-
|
|
125
|
+
return fileURLToPath(new URL('../templates/shared/', import.meta.url));
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
export function userTemplatesDir(harness?: string): string {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
182
|
+
return loadTemplates(cwd);
|
|
166
183
|
}
|
package/extensions/usage.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Usage } from '@earendil-works/pi-ai';
|
|
2
2
|
|
|
3
3
|
export interface HarnessUsage {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
39
|
+
return mapHarnessUsage(u);
|
|
40
40
|
}
|
package/package.json
CHANGED
|
@@ -1,58 +1,71 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
}
|