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/command.ts
CHANGED
|
@@ -6,16 +6,16 @@ import type { DelegateTemplate } from './templates.ts';
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export interface DelegateCommandArgs {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
19
|
}
|
|
20
20
|
|
|
21
21
|
export type ClaudeCommandArgs = DelegateCommandArgs;
|
|
@@ -23,69 +23,69 @@ export type ClaudeCommandArgs = DelegateCommandArgs;
|
|
|
23
23
|
const KNOWN_HARNESSES = new Set(['claude', 'codex', 'opencode', 'amp', 'omp']);
|
|
24
24
|
|
|
25
25
|
export function parseDelegateCommand(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
raw: string,
|
|
27
|
+
knownModes: ReadonlySet<string>,
|
|
28
|
+
knownHarnesses: ReadonlySet<string> = KNOWN_HARNESSES,
|
|
29
29
|
): DelegateCommandArgs {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
let harness = flags.harness?.toLowerCase();
|
|
37
|
+
let mode = flags.mode;
|
|
38
|
+
let task = rest.trim();
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
66
|
}
|
|
67
67
|
|
|
68
68
|
export function parseClaudeCommand(raw: string, knownModes: ReadonlySet<string>): ClaudeCommandArgs {
|
|
69
|
-
|
|
69
|
+
return parseDelegateCommand(raw, knownModes);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Apply template defaults when the prompt is empty.
|
|
74
74
|
*/
|
|
75
75
|
export function resolveDefaults(
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
args: DelegateCommandArgs,
|
|
77
|
+
templates: ReadonlyMap<string, DelegateTemplate>,
|
|
78
78
|
): { task: string; scope?: string } | null {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
91
|
}
|
package/extensions/config.ts
CHANGED
|
@@ -4,155 +4,161 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { DEFAULT_TIMEOUT_MS } from './harnesses/types.ts';
|
|
5
5
|
|
|
6
6
|
export interface HarnessConfig {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
model?: string;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
allowDangerous?: boolean;
|
|
10
|
+
maxBudgetUsd?: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface DelegateConfig {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
model?: string;
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
defaultMode: string;
|
|
17
|
+
defaultHarness: string;
|
|
18
|
+
allowDangerous: boolean;
|
|
19
|
+
inspectThinking: boolean;
|
|
20
|
+
maxBudgetUsd?: number;
|
|
21
|
+
autoDelegateHints: boolean;
|
|
22
|
+
modelAliases: Record<string, string>;
|
|
23
|
+
maxConcurrent: number | { global?: number; perHarness?: Record<string, number> };
|
|
24
|
+
maxTranscripts: number;
|
|
25
|
+
harnesses: Record<string, HarnessConfig>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function agentDir(): string {
|
|
29
|
-
|
|
29
|
+
return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function outputsDir(harness?: string): string {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const base = join(agentDir(), 'delegate', 'outputs');
|
|
34
|
+
return harness ? join(base, harness) : base;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function legacyOutputsDir(): string {
|
|
38
|
-
|
|
38
|
+
return join(agentDir(), 'claude-delegate', 'outputs');
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function loadConfig(): DelegateConfig {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
42
|
+
const cfg: DelegateConfig = {
|
|
43
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
44
|
+
defaultMode: 'general',
|
|
45
|
+
defaultHarness: 'claude',
|
|
46
|
+
allowDangerous: false,
|
|
47
|
+
inspectThinking: false,
|
|
48
|
+
autoDelegateHints: false,
|
|
49
|
+
modelAliases: { economy: 'haiku', balanced: 'sonnet', max: 'opus' },
|
|
50
|
+
maxConcurrent: 1,
|
|
51
|
+
maxTranscripts: 100,
|
|
52
|
+
harnesses: {},
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
const file = join(agentDir(), 'settings.json');
|
|
56
|
+
if (!existsSync(file)) return cfg;
|
|
57
|
+
const settings = JSON.parse(readFileSync(file, 'utf8')) as {
|
|
58
|
+
delegate?: Partial<DelegateConfig & { harnesses: Record<string, HarnessConfig> }>;
|
|
59
|
+
claudeDelegate?: Partial<DelegateConfig & { model?: string }>;
|
|
60
|
+
};
|
|
61
|
+
// Legacy claudeDelegate -> delegate.harnesses.claude migration
|
|
62
|
+
if (settings.claudeDelegate && !settings.delegate) {
|
|
63
|
+
const c = settings.claudeDelegate as Partial<DelegateConfig>;
|
|
64
|
+
if (typeof c.model === 'string') cfg.harnesses['claude'] = { ...(cfg.harnesses['claude'] ?? {}), model: c.model };
|
|
65
|
+
if (typeof c.timeoutMs === 'number' && c.timeoutMs > 0) cfg.timeoutMs = c.timeoutMs;
|
|
66
|
+
if (typeof c.defaultMode === 'string') cfg.defaultMode = c.defaultMode;
|
|
67
|
+
if (typeof c.allowDangerous === 'boolean') cfg.allowDangerous = c.allowDangerous;
|
|
68
|
+
if (typeof c.inspectThinking === 'boolean') cfg.inspectThinking = c.inspectThinking;
|
|
69
|
+
if (typeof (c as DelegateConfig).maxBudgetUsd === 'number' && (c as DelegateConfig).maxBudgetUsd! > 0)
|
|
70
|
+
cfg.maxBudgetUsd = (c as DelegateConfig).maxBudgetUsd;
|
|
71
|
+
if (typeof c.autoDelegateHints === 'boolean') cfg.autoDelegateHints = c.autoDelegateHints;
|
|
72
|
+
if ((c as DelegateConfig).modelAliases && typeof (c as DelegateConfig).modelAliases === 'object') {
|
|
73
|
+
for (const [k, v] of Object.entries((c as DelegateConfig).modelAliases!)) {
|
|
74
|
+
if (typeof v === 'string' && v) cfg.modelAliases[k] = v;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (typeof c.maxConcurrent === 'number' && c.maxConcurrent >= 0) cfg.maxConcurrent = c.maxConcurrent;
|
|
78
|
+
else if (c.maxConcurrent && typeof c.maxConcurrent === 'object') {
|
|
79
|
+
const obj = c.maxConcurrent as { global?: unknown; perHarness?: unknown };
|
|
80
|
+
const out: { global?: number; perHarness?: Record<string, number> } = {};
|
|
81
|
+
if (typeof obj.global === 'number' && obj.global >= 0) out.global = obj.global;
|
|
82
|
+
if (obj.perHarness && typeof obj.perHarness === 'object') {
|
|
83
|
+
const ph: Record<string, number> = {};
|
|
84
|
+
for (const [k, v] of Object.entries(obj.perHarness as Record<string, unknown>)) {
|
|
85
|
+
if (typeof v === 'number' && v >= 0) ph[k] = v;
|
|
86
|
+
}
|
|
87
|
+
if (Object.keys(ph).length > 0) out.perHarness = ph;
|
|
88
|
+
}
|
|
89
|
+
if (out.global !== undefined || out.perHarness) cfg.maxConcurrent = out;
|
|
90
|
+
}
|
|
91
|
+
if (typeof c.maxTranscripts === 'number' && c.maxTranscripts >= 0) cfg.maxTranscripts = c.maxTranscripts;
|
|
92
|
+
if (c.harnesses && typeof c.harnesses === 'object') {
|
|
93
|
+
for (const [k, v] of Object.entries(c.harnesses)) {
|
|
94
|
+
if (v && typeof v === 'object') cfg.harnesses[k] = { ...(cfg.harnesses[k] ?? {}), ...(v as HarnessConfig) };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// also map harnesses.claude if any
|
|
98
|
+
return cfg;
|
|
99
|
+
}
|
|
100
|
+
const d = settings.delegate ?? {};
|
|
101
|
+
if (typeof d.defaultHarness === 'string' && d.defaultHarness) cfg.defaultHarness = d.defaultHarness;
|
|
102
|
+
if (typeof d.defaultMode === 'string') cfg.defaultMode = d.defaultMode;
|
|
103
|
+
if (typeof d.model === 'string') cfg.model = d.model;
|
|
104
|
+
if (typeof d.timeoutMs === 'number' && d.timeoutMs > 0) cfg.timeoutMs = d.timeoutMs;
|
|
105
|
+
if (typeof d.allowDangerous === 'boolean') cfg.allowDangerous = d.allowDangerous;
|
|
106
|
+
if (typeof d.inspectThinking === 'boolean') cfg.inspectThinking = d.inspectThinking;
|
|
107
|
+
if (typeof d.maxBudgetUsd === 'number' && d.maxBudgetUsd > 0) cfg.maxBudgetUsd = d.maxBudgetUsd;
|
|
108
|
+
if (typeof d.autoDelegateHints === 'boolean') cfg.autoDelegateHints = d.autoDelegateHints;
|
|
109
|
+
if (d.modelAliases && typeof d.modelAliases === 'object') {
|
|
110
|
+
for (const [k, v] of Object.entries(d.modelAliases)) {
|
|
111
|
+
if (typeof v === 'string' && v) cfg.modelAliases[k] = v;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (typeof d.maxConcurrent === 'number' && d.maxConcurrent >= 0) cfg.maxConcurrent = d.maxConcurrent;
|
|
115
|
+
else if (d.maxConcurrent && typeof d.maxConcurrent === 'object') {
|
|
116
|
+
const obj = d.maxConcurrent as { global?: unknown; perHarness?: unknown };
|
|
117
|
+
const out: { global?: number; perHarness?: Record<string, number> } = {};
|
|
118
|
+
if (typeof obj.global === 'number' && obj.global >= 0) out.global = obj.global;
|
|
119
|
+
if (obj.perHarness && typeof obj.perHarness === 'object') {
|
|
120
|
+
const ph: Record<string, number> = {};
|
|
121
|
+
for (const [k, v] of Object.entries(obj.perHarness as Record<string, unknown>)) {
|
|
122
|
+
if (typeof v === 'number' && v >= 0) ph[k] = v;
|
|
123
|
+
}
|
|
124
|
+
if (Object.keys(ph).length > 0) out.perHarness = ph;
|
|
125
|
+
}
|
|
126
|
+
if (out.global !== undefined || out.perHarness) cfg.maxConcurrent = out;
|
|
127
|
+
}
|
|
128
|
+
if (typeof d.maxTranscripts === 'number' && d.maxTranscripts >= 0) cfg.maxTranscripts = d.maxTranscripts;
|
|
129
|
+
if (d.harnesses && typeof d.harnesses === 'object') {
|
|
130
|
+
for (const [k, v] of Object.entries(d.harnesses)) {
|
|
131
|
+
if (v && typeof v === 'object') cfg.harnesses[k] = { ...(v as HarnessConfig) };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// also support legacy claudeDelegate merged when delegate also present (delegate wins)
|
|
135
|
+
if (settings.claudeDelegate) {
|
|
136
|
+
const c = settings.claudeDelegate as Partial<DelegateConfig>;
|
|
137
|
+
if (typeof c.model === 'string' && !cfg.harnesses['claude']?.model) {
|
|
138
|
+
cfg.harnesses['claude'] = { ...(cfg.harnesses['claude'] ?? {}), model: c.model };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// invalid settings — fall back to defaults
|
|
143
|
+
}
|
|
144
|
+
return cfg;
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
export function resolveModelForHarness(
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
export function resolveModelForHarness(
|
|
148
|
+
cfg: DelegateConfig,
|
|
149
|
+
harness: string,
|
|
150
|
+
model?: string,
|
|
151
|
+
templateModel?: string,
|
|
152
|
+
): string | undefined {
|
|
153
|
+
const resolve = (m?: string) => (m ? (cfg.modelAliases[m] ?? m) : undefined);
|
|
154
|
+
return resolve(model) ?? resolve(templateModel) ?? resolve(cfg.harnesses[harness]?.model) ?? resolve(cfg.model);
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
export function getMaxConcurrent(cfg: DelegateConfig, harness?: string): number {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
if (typeof cfg.maxConcurrent === 'number') return cfg.maxConcurrent;
|
|
159
|
+
// if object shape {global, perHarness}
|
|
160
|
+
const mc = cfg.maxConcurrent as unknown as { global?: number; perHarness?: Record<string, number> };
|
|
161
|
+
if (harness && mc.perHarness && typeof mc.perHarness[harness] === 'number') return mc.perHarness[harness]!;
|
|
162
|
+
if (typeof mc.global === 'number') return mc.global;
|
|
163
|
+
return 1;
|
|
158
164
|
}
|