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.
@@ -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
- * Concurrency cap is best-effort, not a hard mutex: `countActiveRuns()` (read) and
10
- * `acquireRun()` (write) are two separate steps with no lock between them, so two pi
11
- * processes starting at the same instant can both observe a count under the limit and both
12
- * proceed — `maxConcurrent` can be exceeded by a small margin under a tight race. This is a
13
- * deliberate simplicity tradeoff (see AGENTS.md); do not rely on it for a hard cap.
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
+ }
@@ -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
- /** Legacy root < shared < harness builtins < legacyUser < user < user/harness < legacyProject < project < project/harness (later wins). */
162
- export function loadTemplates(cwd: string, harnessName?: string): Map<string, DelegateTemplate> {
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 (isTrusted(cwd)) {
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
  /**
@@ -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
- * in the pi footer token/cost stats and /session totals. Returns undefined when cost
17
- * is unknown — `Usage.cost.total` is mandatory, so there's no honest number to put there,
18
- * and reporting a fake $0 would silently under-report spend in pi's session totals.
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 | undefined {
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 | undefined {
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.5.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",