pi-harness-delegate 0.6.0 → 0.6.2
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 +2 -2
- package/extensions/config.ts +114 -20
- package/extensions/index.ts +36 -4
- package/extensions/templates.ts +53 -0
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -134,7 +134,7 @@ You are a senior engineer delegated by the pi coding agent.
|
|
|
134
134
|
|
|
135
135
|
Custom templates are just files dropped in the above dirs — any registered name becomes a valid `mode`.
|
|
136
136
|
|
|
137
|
-
**Project trust:** project-local templates (`.pi/delegate/templates/`) load only when pi itself considers the current project trusted (`ctx.isProjectTrusted()`, backed by pi's own trust store outside the project — the same trust that gates other project-scoped behavior). Trust it via pi's own trust prompt (shown the first time you open an untrusted directory) or your `defaultProjectTrust` setting; `/delegate status` reports whether the current project is trusted and, if not, that project-local templates are being skipped. There is **no way to grant trust from inside the project** — no `.pi/trusted` file, no environment variable. Earlier versions supported both (`.pi/trusted` containing `1`, or `PI_TRUSTED=1`/`PI_DELEGATE_TRUSTED=1` in the environment); both were removed as a security fix — a repo could commit `.pi/trusted` and declare itself trusted, letting a cloned hostile repo's templates silently override a builtin (e.g. widening `review` from `readonly` to `edit` and attaching a `verify:` command that runs host-side). If you relied on either, switch to pi's trust prompt or `defaultProjectTrust`.
|
|
137
|
+
**Project trust:** project-local templates (`.pi/delegate/templates/`) load only when pi itself considers the current project trusted (`ctx.isProjectTrusted()`, backed by pi's own trust store outside the project — the same trust that gates other project-scoped behavior). Trust it via pi's own trust prompt (shown the first time you open an untrusted directory) or your `defaultProjectTrust` setting; `/delegate status` reports whether the current project is trusted and, if not, that project-local templates are being skipped. There is **no way to grant trust from inside the project** — no `.pi/trusted` file, no environment variable. Earlier versions supported both (`.pi/trusted` containing `1`, or `PI_TRUSTED=1`/`PI_DELEGATE_TRUSTED=1` in the environment); both were removed as a security fix — a repo could commit `.pi/trusted` and declare itself trusted, letting a cloned hostile repo's templates silently override a builtin (e.g. widening `review` from `readonly` to `edit` and attaching a `verify:` command that runs host-side). If you relied on either, switch to pi's trust prompt or `defaultProjectTrust`. **On upgrade this is otherwise silent** — a project override shares its name with the builtin it replaces, so the run just uses the builtin and looks fine — so a delegation in an untrusted project that actually has `.pi/delegate/templates/` now prints a one-time warning saying they were skipped, and points out a leftover `.pi/trusted` file if one is present (it no longer does anything and can be deleted).
|
|
138
138
|
|
|
139
139
|
## How the main session consumes the output
|
|
140
140
|
|
|
@@ -184,7 +184,7 @@ In `~/.pi/agent/settings.json`:
|
|
|
184
184
|
|
|
185
185
|
Run `/delegate config` to see exactly what was read from `settings.json` (or why nothing was — no file, no `delegate` key, or a parse error), plus the effective config with defaults filled in, as a paste-ready JSON block for the `delegate` key. `/delegate status` shows the same provenance as one summary line.
|
|
186
186
|
|
|
187
|
-
`/delegate config init` writes that effective config into `settings.json` under the `delegate` key — the only thing this extension ever writes there, and only on this explicit command. It reads the whole file, replaces only the `delegate` key, and preserves every other key (pi's `theme`/`defaultProvider`/`packages`/…, and a leftover `claudeDelegate`, verbatim). The write is atomic (temp file + rename in the same directory — no torn file if the process dies mid-write) and refuses outright if the existing file fails to parse, rather than clobbering whatever's actually in it; `/delegate config`'s paste-ready block is the fallback in that case.
|
|
187
|
+
`/delegate config init` writes the parts of that effective config that differ from the built-in defaults into `settings.json` under the `delegate` key (an unmodified setup writes `{}`) — defaults are deliberately *not* written, because a value present in the file is treated as your choice and would stop later releases' default changes (e.g. `maxConcurrent` 1 → 4) from reaching you — the only thing this extension ever writes there, and only on this explicit command. It reads the whole file, replaces only the `delegate` key, and preserves every other key (pi's `theme`/`defaultProvider`/`packages`/…, and a leftover `claudeDelegate`, verbatim). The write is atomic (temp file + rename in the same directory — no torn file if the process dies mid-write) and refuses outright if the existing file fails to parse, rather than clobbering whatever's actually in it. It also keeps the file's mode, writes through a symlinked `settings.json` rather than replacing the link, matches the file's trailing-newline convention, and takes pi's own `settings.json.lock` — if another pi session holds it, the command refuses and you retry; `/delegate config`'s paste-ready block is the fallback in that case.
|
|
188
188
|
|
|
189
189
|
Legacy `claudeDelegate` is auto-migrated into `delegate.harnesses.claude` (deprecated) — most fields migrate, including per-harness settings like `harnesses.<name>.transport`. Two things never migrate, though, and stay silently unreachable as long as `claudeDelegate` is your *only* key (no `delegate` key at all): `defaultHarness` (stays pinned to `claude`) and a top-level default `model` (only `claudeDelegate.model` → `harnesses.claude.model` migrates — there's no global fallback). Both `/delegate status` and `/delegate config` call this out when it's happening; fix it by renaming `claudeDelegate` to `delegate`, or by running `/delegate config init`, which writes an explicit `delegate` key (with the legacy values already correctly migrated) without touching `claudeDelegate` itself.
|
|
190
190
|
|
package/extensions/config.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
2
12
|
import { homedir } from 'node:os';
|
|
3
13
|
import { dirname, join } from 'node:path';
|
|
4
14
|
import { DEFAULT_TIMEOUT_MS, type Harness, type Transport } from './harnesses/types.ts';
|
|
@@ -83,17 +93,9 @@ function sanitizeHarnessConfig(v: HarnessConfig): HarnessConfig {
|
|
|
83
93
|
return out;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
* doc comment. `loadConfig()` below is a thin wrapper for the existing call sites that only want
|
|
90
|
-
* the resolved values; this is the one place that actually reads/parses the file, so a caller
|
|
91
|
-
* needing both never pays for a second parse. Never throws — any failure (missing file, bad JSON,
|
|
92
|
-
* a non-object root) is recorded on `source` and falls back to the same defaults `loadConfig()`
|
|
93
|
-
* has always returned.
|
|
94
|
-
*/
|
|
95
|
-
export function loadConfigWithSource(): ConfigLoadResult {
|
|
96
|
-
const cfg: DelegateConfig = {
|
|
96
|
+
/** Built-in defaults — a fresh object each call, since callers mutate the result. */
|
|
97
|
+
export function defaultDelegateConfig(): DelegateConfig {
|
|
98
|
+
return {
|
|
97
99
|
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
98
100
|
defaultMode: 'general',
|
|
99
101
|
defaultHarness: 'claude',
|
|
@@ -105,6 +107,44 @@ export function loadConfigWithSource(): ConfigLoadResult {
|
|
|
105
107
|
maxTranscripts: 100,
|
|
106
108
|
harnesses: {},
|
|
107
109
|
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Only the top-level settings whose effective value differs from the built-in default.
|
|
114
|
+
*
|
|
115
|
+
* What `/delegate config init` writes. Writing the full effective config would pin every default
|
|
116
|
+
* into settings.json, and the loader treats a present value as user intent — so a later release
|
|
117
|
+
* that changes a default (maxConcurrent 1 -> 4 already did) would never reach that user.
|
|
118
|
+
*/
|
|
119
|
+
export function nonDefaultConfig(cfg: DelegateConfig): Partial<DelegateConfig> {
|
|
120
|
+
const defaults = defaultDelegateConfig() as unknown as Record<string, unknown>;
|
|
121
|
+
const out: Record<string, unknown> = {};
|
|
122
|
+
for (const [k, v] of Object.entries(cfg as unknown as Record<string, unknown>)) {
|
|
123
|
+
if (v === undefined) continue;
|
|
124
|
+
if (k === 'modelAliases') {
|
|
125
|
+
// The loader merges aliases additively onto the defaults, so only the keys that differ need
|
|
126
|
+
// writing — whole-object diffing would pin the untouched default aliases next to a custom one.
|
|
127
|
+
const d = defaults.modelAliases as Record<string, string>;
|
|
128
|
+
const diff = Object.fromEntries(Object.entries(v as Record<string, string>).filter(([a, t]) => d[a] !== t));
|
|
129
|
+
if (Object.keys(diff).length > 0) out[k] = diff;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (JSON.stringify(v) !== JSON.stringify(defaults[k])) out[k] = v;
|
|
133
|
+
}
|
|
134
|
+
return out as Partial<DelegateConfig>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Loads `delegate` config from `~/.pi/agent/settings.json` alongside `ConfigSource`, describing
|
|
139
|
+
* how that happened (file present? which key won? did it fail to parse?) — see `ConfigSource`'s
|
|
140
|
+
* doc comment. `loadConfig()` below is a thin wrapper for the existing call sites that only want
|
|
141
|
+
* the resolved values; this is the one place that actually reads/parses the file, so a caller
|
|
142
|
+
* needing both never pays for a second parse. Never throws — any failure (missing file, bad JSON,
|
|
143
|
+
* a non-object root) is recorded on `source` and falls back to the same defaults `loadConfig()`
|
|
144
|
+
* has always returned.
|
|
145
|
+
*/
|
|
146
|
+
export function loadConfigWithSource(): ConfigLoadResult {
|
|
147
|
+
const cfg = defaultDelegateConfig();
|
|
108
148
|
const file = join(agentDir(), 'settings.json');
|
|
109
149
|
const source: ConfigSource = { file, fileExists: false, usedKey: 'none', legacyKeyPresent: false };
|
|
110
150
|
try {
|
|
@@ -273,8 +313,8 @@ export function buildConfigReport(result: ConfigLoadResult): string[] {
|
|
|
273
313
|
lines.push('from file (as written, before defaults are applied):');
|
|
274
314
|
lines.push(JSON.stringify(result.source.raw ?? {}, null, 2));
|
|
275
315
|
lines.push('');
|
|
276
|
-
lines.push('effective config (file merged with defaults) —
|
|
277
|
-
lines.push('
|
|
316
|
+
lines.push('effective config (file merged with defaults) — for reference; you only need to put the values');
|
|
317
|
+
lines.push('you want to change under "delegate" (`/delegate config init` writes just those, not defaults):');
|
|
278
318
|
lines.push(JSON.stringify({ delegate: result.config }, null, 2));
|
|
279
319
|
return lines;
|
|
280
320
|
}
|
|
@@ -298,13 +338,27 @@ export interface WriteConfigResult {
|
|
|
298
338
|
* on the strength of a read like `loadConfig()`/`showStatus`.
|
|
299
339
|
*/
|
|
300
340
|
export function writeDelegateConfig(delegateSubtree: unknown): WriteConfigResult {
|
|
301
|
-
const
|
|
341
|
+
const configured = join(agentDir(), 'settings.json');
|
|
342
|
+
// Write through a symlink to its real target rather than replacing the link with a regular file.
|
|
343
|
+
let file = configured;
|
|
344
|
+
try {
|
|
345
|
+
file = realpathSync(configured);
|
|
346
|
+
} catch {
|
|
347
|
+
// absent — created below
|
|
348
|
+
}
|
|
349
|
+
// pi locks the path as configured (proper-lockfile realpath:false), not a symlink's target.
|
|
350
|
+
const lock = `${configured}.lock`;
|
|
351
|
+
const tmp = join(dirname(file), `settings.json.${process.pid}.tmp`);
|
|
352
|
+
let locked = false;
|
|
302
353
|
try {
|
|
303
354
|
let root: Record<string, unknown> = {};
|
|
355
|
+
let mode: number | undefined;
|
|
356
|
+
let trailingNewline = false;
|
|
304
357
|
if (existsSync(file)) {
|
|
358
|
+
const raw = readFileSync(file, 'utf8');
|
|
305
359
|
let parsed: unknown;
|
|
306
360
|
try {
|
|
307
|
-
parsed = JSON.parse(
|
|
361
|
+
parsed = JSON.parse(raw);
|
|
308
362
|
} catch (err) {
|
|
309
363
|
return {
|
|
310
364
|
ok: false,
|
|
@@ -316,16 +370,56 @@ export function writeDelegateConfig(delegateSubtree: unknown): WriteConfigResult
|
|
|
316
370
|
return { ok: false, file, message: `refusing to write: ${file} exists but its root isn't a JSON object` };
|
|
317
371
|
}
|
|
318
372
|
root = parsed as Record<string, unknown>;
|
|
373
|
+
mode = statSync(file).mode & 0o777;
|
|
374
|
+
trailingNewline = raw.endsWith('\n');
|
|
375
|
+
}
|
|
376
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
377
|
+
// pi saves settings.json under proper-lockfile (`<file>.lock`, a directory taken with an atomic
|
|
378
|
+
// mkdir). Take the same lock, without waiting, so we neither lose a concurrent pi write nor have
|
|
379
|
+
// ours overwritten mid-read-modify-write. The read above is repeated under the lock.
|
|
380
|
+
try {
|
|
381
|
+
mkdirSync(lock);
|
|
382
|
+
locked = true;
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
file,
|
|
388
|
+
message: `refusing to write: ${file} is locked (another pi session is saving settings, or a crashed one left a stale lock that pi clears after ~10s) — retry in a moment`,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
throw err;
|
|
392
|
+
}
|
|
393
|
+
if (existsSync(file)) {
|
|
394
|
+
const again: unknown = JSON.parse(readFileSync(file, 'utf8'));
|
|
395
|
+
if (again === null || typeof again !== 'object' || Array.isArray(again)) {
|
|
396
|
+
return { ok: false, file, message: `refusing to write: ${file} changed and its root isn't a JSON object` };
|
|
397
|
+
}
|
|
398
|
+
root = again as Record<string, unknown>;
|
|
319
399
|
}
|
|
320
400
|
root.delegate = delegateSubtree;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
writeFileSync(tmp, `${JSON.stringify(root, null, 2)}\n`, 'utf8');
|
|
401
|
+
// Match the file's own format: pi writes JSON.stringify(x, null, 2) with no trailing newline.
|
|
402
|
+
writeFileSync(tmp, `${JSON.stringify(root, null, 2)}${trailingNewline ? '\n' : ''}`, { encoding: 'utf8' });
|
|
403
|
+
if (mode !== undefined) chmodSync(tmp, mode);
|
|
325
404
|
renameSync(tmp, file);
|
|
326
405
|
return { ok: true, file, message: `wrote "delegate" key to ${file}` };
|
|
327
406
|
} catch (err) {
|
|
328
407
|
return { ok: false, file, message: `failed to write ${file}: ${err instanceof Error ? err.message : String(err)}` };
|
|
408
|
+
} finally {
|
|
409
|
+
// Cleanup is best-effort: this function must never throw.
|
|
410
|
+
try {
|
|
411
|
+
rmSync(tmp, { force: true });
|
|
412
|
+
} catch {
|
|
413
|
+
// nothing useful to do
|
|
414
|
+
}
|
|
415
|
+
// Separate from the tmp cleanup so a failure there can never leave pi's lock behind.
|
|
416
|
+
if (locked) {
|
|
417
|
+
try {
|
|
418
|
+
rmSync(lock, { recursive: true, force: true });
|
|
419
|
+
} catch {
|
|
420
|
+
// nothing useful to do
|
|
421
|
+
}
|
|
422
|
+
}
|
|
329
423
|
}
|
|
330
424
|
}
|
|
331
425
|
|
package/extensions/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
legacyOutputsDir,
|
|
68
68
|
loadConfig,
|
|
69
69
|
loadConfigWithSource,
|
|
70
|
+
nonDefaultConfig,
|
|
70
71
|
resolveModelForHarness,
|
|
71
72
|
resolveTransport,
|
|
72
73
|
writeDelegateConfig,
|
|
@@ -86,7 +87,13 @@ import { NotifyBatcher } from './notify.ts';
|
|
|
86
87
|
import { type FeedEntry, progressWindow } from './progress.ts';
|
|
87
88
|
import { formatFanoutChip, multiProgressWindow, type RunRow } from './progress-multi.ts';
|
|
88
89
|
import { runHarness } from './runner.ts';
|
|
89
|
-
import {
|
|
90
|
+
import {
|
|
91
|
+
type DelegateTemplate,
|
|
92
|
+
describeSkippedProjectTemplates,
|
|
93
|
+
loadTemplates,
|
|
94
|
+
projectTemplatePresence,
|
|
95
|
+
resolveNativePermission,
|
|
96
|
+
} from './templates.ts';
|
|
90
97
|
import { mapClaudeUsage } from './usage.ts';
|
|
91
98
|
|
|
92
99
|
/** Render a possibly-unknown cost — `null` means the harness didn't report one, not a measured $0. */
|
|
@@ -188,6 +195,28 @@ function outputsDirFor(harness: string): string {
|
|
|
188
195
|
* project-local delegate templates load — see the trust-tier comment on `loadTemplates`. Fails
|
|
189
196
|
* closed (untrusted) if the host is old enough not to expose the method, or if it throws.
|
|
190
197
|
*/
|
|
198
|
+
/**
|
|
199
|
+
* Warn once per project when trusted-only content was silently skipped.
|
|
200
|
+
*
|
|
201
|
+
* Before 0.6.0 a committed `.pi/trusted` file (or `PI_TRUSTED=1`) granted trust; both were removed as
|
|
202
|
+
* a security fix. A user who relied on either loses their project-local templates on upgrade with no
|
|
203
|
+
* visible signal — an override shares its name with the builtin it replaces, so the run just uses the
|
|
204
|
+
* builtin and looks fine. `/delegate status` reports trust state, but nobody runs it unless something
|
|
205
|
+
* already looks wrong, so this fires on an actual delegation instead — and only when the project
|
|
206
|
+
* demonstrably has the content being skipped, so it never nags anyone unaffected.
|
|
207
|
+
*/
|
|
208
|
+
const warnedUntrustedProjects = new Set<string>();
|
|
209
|
+
|
|
210
|
+
function warnIfProjectTemplatesSkipped(ctx: ExtensionContext, trusted: boolean): void {
|
|
211
|
+
if (trusted || warnedUntrustedProjects.has(ctx.cwd)) return;
|
|
212
|
+
const lines = describeSkippedProjectTemplates(projectTemplatePresence(ctx.cwd));
|
|
213
|
+
if (lines.length === 0) return;
|
|
214
|
+
warnedUntrustedProjects.add(ctx.cwd);
|
|
215
|
+
const msg = lines.join('\n');
|
|
216
|
+
if (ctx.hasUI) ctx.ui.notify?.(msg, 'warning');
|
|
217
|
+
else process.stderr.write(`${msg}\n`);
|
|
218
|
+
}
|
|
219
|
+
|
|
191
220
|
function isProjectTrusted(ctx: ExtensionContext): boolean {
|
|
192
221
|
try {
|
|
193
222
|
return typeof ctx.isProjectTrusted === 'function' && ctx.isProjectTrusted() === true;
|
|
@@ -557,7 +586,8 @@ async function showConfig(ctx: ExtensionContext): Promise<void> {
|
|
|
557
586
|
/**
|
|
558
587
|
* `/delegate config init` — the one place this extension ever writes to `settings.json`, and only
|
|
559
588
|
* because a human explicitly typed this subcommand. Writes the current effective config (defaults
|
|
560
|
-
* merged with whatever was already on disk
|
|
589
|
+
* merged with whatever was already on disk, minus anything still at its default — pinning defaults would
|
|
590
|
+
* stop later releases' default changes reaching the user) into the `delegate` key via `writeDelegateConfig()`
|
|
561
591
|
* (read-modify-write, atomic, refuses on an unparseable file rather than clobbering it). This is
|
|
562
592
|
* also the practical fix for the legacy-`claudeDelegate`-only gap `describeConfigSource` warns
|
|
563
593
|
* about: writing an explicit `delegate` key (with the correctly-resolved values already folded
|
|
@@ -566,7 +596,7 @@ async function showConfig(ctx: ExtensionContext): Promise<void> {
|
|
|
566
596
|
*/
|
|
567
597
|
async function initConfig(ctx: ExtensionContext): Promise<void> {
|
|
568
598
|
const result = loadConfigWithSource();
|
|
569
|
-
const write = writeDelegateConfig(result.config);
|
|
599
|
+
const write = writeDelegateConfig(nonDefaultConfig(result.config));
|
|
570
600
|
const msg = write.ok ? `✓ ${write.message}` : `✗ ${write.message}`;
|
|
571
601
|
if (!ctx.hasUI) process.stdout.write(`${msg}\n`);
|
|
572
602
|
else ctx.ui.notify?.(msg, write.ok ? 'info' : 'warning');
|
|
@@ -611,7 +641,9 @@ async function delegate(
|
|
|
611
641
|
throw new Error(
|
|
612
642
|
`unknown harness "${harnessName}". Available: ${HARNESS_NAMES.join(', ')} (aliases: ${Object.keys(ALIASES).join(', ')})`,
|
|
613
643
|
);
|
|
614
|
-
const
|
|
644
|
+
const projectTrusted = isProjectTrusted(ctx);
|
|
645
|
+
warnIfProjectTemplatesSkipped(ctx, projectTrusted);
|
|
646
|
+
const templates = loadTemplates(ctx.cwd, harnessName, projectTrusted);
|
|
615
647
|
const mode = opts.mode || config.defaultMode;
|
|
616
648
|
const template = templates.get(mode);
|
|
617
649
|
if (!template)
|
package/extensions/templates.ts
CHANGED
|
@@ -201,3 +201,56 @@ export function resolveNativePermission(
|
|
|
201
201
|
if (!nativePermission) return undefined;
|
|
202
202
|
return effectivePermission === templatePermission ? nativePermission : undefined;
|
|
203
203
|
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* What a project has on disk that only loads when the project is trusted.
|
|
207
|
+
*
|
|
208
|
+
* Used to warn a user whose project-local templates stopped loading after the 0.6.0 security fix
|
|
209
|
+
* (§19 of ROADMAP) — before it, a committed `.pi/trusted` file or `PI_TRUSTED=1` granted trust, and
|
|
210
|
+
* both were removed. The failure is otherwise invisible: an override shares its name with the
|
|
211
|
+
* builtin it replaces, so the run silently uses the builtin and produces plausible output.
|
|
212
|
+
*
|
|
213
|
+
* Pure filesystem inspection — no trust logic. The caller supplies the trust decision.
|
|
214
|
+
*/
|
|
215
|
+
export function projectTemplatePresence(cwd: string): {
|
|
216
|
+
/** Template dirs that exist and would load if the project were trusted. */
|
|
217
|
+
dirs: string[];
|
|
218
|
+
/** A leftover `.pi/trusted` file — strong evidence the user relied on the removed mechanism. */
|
|
219
|
+
staleTrustFile: boolean;
|
|
220
|
+
} {
|
|
221
|
+
const candidates = [projectTemplatesDir(cwd), legacyProjectTemplatesDir(cwd)];
|
|
222
|
+
const dirs: string[] = [];
|
|
223
|
+
for (const dir of candidates) {
|
|
224
|
+
try {
|
|
225
|
+
if (readdirSync(dir).some(f => f.endsWith('.md'))) dirs.push(dir);
|
|
226
|
+
} catch {
|
|
227
|
+
// absent or unreadable — nothing to warn about
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
let staleTrustFile = false;
|
|
231
|
+
try {
|
|
232
|
+
staleTrustFile = existsSync(join(cwd, '.pi', 'trusted'));
|
|
233
|
+
} catch {
|
|
234
|
+
staleTrustFile = false;
|
|
235
|
+
}
|
|
236
|
+
return { dirs, staleTrustFile };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** One-line notices for a project whose trusted-only content was skipped. Empty when nothing applies. */
|
|
240
|
+
export function describeSkippedProjectTemplates(presence: { dirs: string[]; staleTrustFile: boolean }): string[] {
|
|
241
|
+
if (presence.dirs.length === 0 && !presence.staleTrustFile) return [];
|
|
242
|
+
const out: string[] = [];
|
|
243
|
+
if (presence.dirs.length > 0) {
|
|
244
|
+
out.push(
|
|
245
|
+
`⚠ project-local templates were NOT loaded — this project is untrusted (${presence.dirs.join(', ')})`,
|
|
246
|
+
" trust it via pi's trust prompt or defaultProjectTrust; /delegate status shows trust state",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (presence.staleTrustFile) {
|
|
250
|
+
out.push(
|
|
251
|
+
' a leftover .pi/trusted file was found — it no longer grants trust (removed in 0.6.0 as a',
|
|
252
|
+
' security fix, since a repo could use it to trust itself) and can be deleted',
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-harness-delegate",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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",
|
|
@@ -65,7 +65,6 @@
|
|
|
65
65
|
"pi": {
|
|
66
66
|
"extensions": [
|
|
67
67
|
"./extensions/index.ts"
|
|
68
|
-
]
|
|
69
|
-
"image": "https://raw.githubusercontent.com/yorch/pi-harness-delegate/main/docs/assets/claude-delegate-preview.png"
|
|
68
|
+
]
|
|
70
69
|
}
|
|
71
70
|
}
|