pi-harness-delegate 0.5.0 → 0.6.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 +23 -7
- package/extensions/acp-runner.ts +69 -9
- package/extensions/command.ts +21 -0
- package/extensions/concurrency.ts +16 -3
- package/extensions/config.ts +216 -13
- package/extensions/harnesses/amp.ts +24 -4
- package/extensions/harnesses/claude.ts +3 -0
- package/extensions/harnesses/codex.ts +13 -1
- package/extensions/harnesses/devin.ts +2 -0
- package/extensions/harnesses/opencode.ts +160 -0
- package/extensions/harnesses/types.ts +21 -6
- package/extensions/index.ts +159 -44
- package/extensions/run-registry.ts +50 -5
- package/extensions/templates.ts +14 -15
- package/extensions/usage.ts +16 -8
- package/package.json +1 -1
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
* registry I/O failures never break a delegation — callers should combine this with their own
|
|
7
7
|
* in-process counters as a fallback.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* `acquireRun()` + `countActiveRuns()` alone are a plain check-then-act pair: read the count,
|
|
10
|
+
* decide, write — with no lock between the read and the write, so two pi processes starting at
|
|
11
|
+
* the same instant can both observe a count under the limit and both proceed, over-admitting for
|
|
12
|
+
* the full lifetime of both runs. `acquireRunWithinLimits()` below closes that specific window by
|
|
13
|
+
* re-verifying *after* writing: a write that turns out to push either count over its limit is
|
|
14
|
+
* undone immediately, so the cap can never be permanently exceeded — see its own doc comment for
|
|
15
|
+
* exactly what guarantee that is (and isn't). Callers that don't need the cap enforced — `/delegate
|
|
16
|
+
* status`'s display, or a caller happy with the plain best-effort behavior — can still use
|
|
17
|
+
* `acquireRun`/`countActiveRuns` directly.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
@@ -88,3 +92,44 @@ export function countActiveRuns(harness?: string): number {
|
|
|
88
92
|
}
|
|
89
93
|
return count;
|
|
90
94
|
}
|
|
95
|
+
|
|
96
|
+
export type AcquireWithinLimitsResult =
|
|
97
|
+
| { status: 'acquired'; handle: RunHandle }
|
|
98
|
+
/** Writing succeeded, but the run would push a limit over the top — undone, nothing held. */
|
|
99
|
+
| { status: 'full' }
|
|
100
|
+
/** Registry I/O failed — best-effort, same as `acquireRun` returning null: caller should fall
|
|
101
|
+
* back to in-process-only accounting and proceed rather than block the run. */
|
|
102
|
+
| { status: 'unavailable' };
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Register an active run, then atomically-in-effect verify it's still within `maxGlobal` and
|
|
106
|
+
* `maxPerHarness` (either `<= 0` means "no limit" for that dimension) — undoing the registration
|
|
107
|
+
* if not. This turns the classic count-then-act race into a write-then-recheck one: because the
|
|
108
|
+
* recheck happens strictly *after* the write is committed to disk, whichever of two racing
|
|
109
|
+
* processes writes last is guaranteed to see both entries and correctly back off — over-admission
|
|
110
|
+
* (more than the limit standing at once) is impossible by construction, unlike plain
|
|
111
|
+
* `countActiveRuns()` + `acquireRun()`.
|
|
112
|
+
*
|
|
113
|
+
* This is not a perfect mutex, and doesn't try to be: in a tight enough multi-way race, more than
|
|
114
|
+
* one contender can each write, then each see the other's (or others') entry when it rechecks, and
|
|
115
|
+
* each concludes it's over the limit and backs off — even though exactly one of them could have
|
|
116
|
+
* fit. That's a transient *under*-admission (self-heals on the caller's next attempt, e.g. via
|
|
117
|
+
* `acquireSlot({wait: true})`'s poll loop) — the property this function actually guarantees is
|
|
118
|
+
* that the limit is never exceeded, not that it's always saturated.
|
|
119
|
+
*/
|
|
120
|
+
export function acquireRunWithinLimits(
|
|
121
|
+
harness: string,
|
|
122
|
+
mode: string,
|
|
123
|
+
maxGlobal: number,
|
|
124
|
+
maxPerHarness: number,
|
|
125
|
+
): AcquireWithinLimitsResult {
|
|
126
|
+
const handle = acquireRun(harness, mode);
|
|
127
|
+
if (!handle) return { status: 'unavailable' };
|
|
128
|
+
const overGlobal = maxGlobal > 0 && countActiveRuns() > maxGlobal;
|
|
129
|
+
const overHarness = maxPerHarness > 0 && countActiveRuns(harness) > maxPerHarness;
|
|
130
|
+
if (overGlobal || overHarness) {
|
|
131
|
+
releaseRun(handle);
|
|
132
|
+
return { status: 'full' };
|
|
133
|
+
}
|
|
134
|
+
return { status: 'acquired', handle };
|
|
135
|
+
}
|
package/extensions/templates.ts
CHANGED
|
@@ -139,16 +139,6 @@ export function projectTemplatesDir(cwd: string, harness?: string): string {
|
|
|
139
139
|
return join(cwd, '.pi', 'delegate', 'templates');
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
/** Minimal trust gate for project-local templates — untrusted clones must not override builtins. */
|
|
143
|
-
function isTrusted(cwd: string): boolean {
|
|
144
|
-
if (process.env.PI_TRUSTED === '1' || process.env.PI_DELEGATE_TRUSTED === '1') return true;
|
|
145
|
-
try {
|
|
146
|
-
return readFileSync(join(cwd, '.pi', 'trusted'), 'utf8').trim() === '1';
|
|
147
|
-
} catch {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
142
|
/** Legacy dirs for compat */
|
|
153
143
|
function legacyUserTemplatesDir(): string {
|
|
154
144
|
const dir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
|
|
@@ -158,8 +148,17 @@ function legacyProjectTemplatesDir(cwd: string): string {
|
|
|
158
148
|
return join(cwd, '.pi', 'claude-delegate', 'templates');
|
|
159
149
|
}
|
|
160
150
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Legacy root < shared < harness builtins < legacyUser < user < user/harness < legacyProject <
|
|
153
|
+
* project < project/harness (later wins).
|
|
154
|
+
*
|
|
155
|
+
* `trusted` gates the project-local tiers only (global/user tiers always load — they're the
|
|
156
|
+
* operator's own files, not the project's). It must come from pi's own trust store
|
|
157
|
+
* (`ctx.isProjectTrusted()`), never from anything inside `cwd` itself: a trust anchor that lives
|
|
158
|
+
* in the content it's supposed to gate can simply declare itself trusted. Callers that fail to
|
|
159
|
+
* resolve trust should pass `false` — untrusted is the safe default.
|
|
160
|
+
*/
|
|
161
|
+
export function loadTemplates(cwd: string, harnessName?: string, trusted = false): Map<string, DelegateTemplate> {
|
|
163
162
|
const out = new Map<string, DelegateTemplate>();
|
|
164
163
|
const harness = harnessName ?? 'claude';
|
|
165
164
|
// legacy root builtins (templates/*.md) lowest — for migration from pi-claude-delegate
|
|
@@ -173,7 +172,7 @@ export function loadTemplates(cwd: string, harnessName?: string): Map<string, De
|
|
|
173
172
|
loadDir(userTemplatesDir(), out);
|
|
174
173
|
loadDir(userTemplatesDir(harness), out);
|
|
175
174
|
// project locals: legacy before new so new wins — only if trusted
|
|
176
|
-
if (
|
|
175
|
+
if (trusted) {
|
|
177
176
|
loadDir(legacyProjectTemplatesDir(cwd), out);
|
|
178
177
|
loadDir(projectTemplatesDir(cwd), out);
|
|
179
178
|
loadDir(projectTemplatesDir(cwd, harness), out);
|
|
@@ -181,8 +180,8 @@ export function loadTemplates(cwd: string, harnessName?: string): Map<string, De
|
|
|
181
180
|
return out;
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
export function loadAllTemplates(cwd: string): Map<string, DelegateTemplate> {
|
|
185
|
-
return loadTemplates(cwd);
|
|
183
|
+
export function loadAllTemplates(cwd: string, trusted = false): Map<string, DelegateTemplate> {
|
|
184
|
+
return loadTemplates(cwd, undefined, trusted);
|
|
186
185
|
}
|
|
187
186
|
|
|
188
187
|
/**
|
package/extensions/usage.ts
CHANGED
|
@@ -12,13 +12,21 @@ export interface HarnessUsage {
|
|
|
12
12
|
export type ClaudeUsage = HarnessUsage;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Map harness usage/cost into pi's `Usage` shape so delegated runs appear
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Map harness usage/cost into pi's `Usage` shape so delegated runs appear in the pi footer
|
|
16
|
+
* token/cost stats and /session totals.
|
|
17
|
+
*
|
|
18
|
+
* Deliberate, bounded exception to the "never fake a number" rule: `Usage.cost.total` is
|
|
19
|
+
* mandatory (unlike `StreamedResult.totalCostUsd`, which stays `number | null` everywhere else
|
|
20
|
+
* in this codebase — the transcript still renders `cost: —` and `/delegate status`'s
|
|
21
|
+
* `aggregateSpend` still tracks unknown-cost runs separately). Codex and Devin genuinely report
|
|
22
|
+
* no dollar cost, so treating "cost unknown" as "usage unknown" here would drop 2 of 5 harnesses'
|
|
23
|
+
* tokens out of pi's session totals entirely. Reporting `$0` under-reports spend by a knowable
|
|
24
|
+
* amount (bounded: it's exactly the missing harnesses' true cost, never a guess); reporting no
|
|
25
|
+
* usage at all loses real token counts outright. Between those two errors, under-reporting spend
|
|
26
|
+
* is the lesser one — but this exception applies ONLY to this pi-`Usage` mapping. Do not
|
|
27
|
+
* generalize a `null -> 0` fallback to any other cost/spend path in the codebase.
|
|
19
28
|
*/
|
|
20
|
-
export function mapHarnessUsage(u: HarnessUsage): Usage
|
|
21
|
-
if (u.totalCostUsd === null) return undefined;
|
|
29
|
+
export function mapHarnessUsage(u: HarnessUsage): Usage {
|
|
22
30
|
const input = u.inputTokens + u.cacheCreationInputTokens;
|
|
23
31
|
const cacheRead = u.cacheReadInputTokens;
|
|
24
32
|
const output = u.outputTokens;
|
|
@@ -34,11 +42,11 @@ export function mapHarnessUsage(u: HarnessUsage): Usage | undefined {
|
|
|
34
42
|
output: 0,
|
|
35
43
|
cacheRead: 0,
|
|
36
44
|
cacheWrite: 0,
|
|
37
|
-
total: u.totalCostUsd,
|
|
45
|
+
total: u.totalCostUsd ?? 0,
|
|
38
46
|
},
|
|
39
47
|
};
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
export function mapClaudeUsage(u: ClaudeUsage): Usage
|
|
50
|
+
export function mapClaudeUsage(u: ClaudeUsage): Usage {
|
|
43
51
|
return mapHarnessUsage(u);
|
|
44
52
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-harness-delegate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|