pi-orchestrator-config 4.22.0 → 4.22.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 +1 -18
- package/extensions/orchestrator/trail.ts +31 -15
- package/package.json +1 -1
- package/prompts/release.md +3 -1
package/README.md
CHANGED
|
@@ -477,23 +477,6 @@ entries) is what pi carries between sessions. Trail's own layer —
|
|
|
477
477
|
`.trail/learnings/`, `trail note`, the pre-ship `trail check` — is upstream's and
|
|
478
478
|
**we do not use it**. We only read the code map.
|
|
479
479
|
|
|
480
|
-
### Migrating from Graft
|
|
481
|
-
|
|
482
|
-
The integration was renamed from Graft to Trail-code. The break is intentional:
|
|
483
|
-
|
|
484
|
-
- Only `@trailhq/trail-code` is installed and only `trail` is invoked. The `graft`
|
|
485
|
-
binary is no longer installed and there is no fallback — if you only have
|
|
486
|
-
`@nanonets/graft`, install trail to get the map tools back.
|
|
487
|
-
- `graft_enable` / `PI_GRAFT_ENABLE` are gone, with no alias and no migration. An
|
|
488
|
-
existing `graft_enable: true` is a dead key, which is harmless: the feature is
|
|
489
|
-
default-on when the binary exists.
|
|
490
|
-
- Tools are `trail_find_code`, `trail_find_all`, `trail_file_api`,
|
|
491
|
-
`trail_trace_calls`, `trail_repo_map`, `trail_refresh`, `trail_blast_radius`.
|
|
492
|
-
- `log_graft` becomes `log_trail` (env `PI_LOG_TRAIL`); the old `~/.pi/logs/graft`
|
|
493
|
-
directory is neither read nor migrated.
|
|
494
|
-
- `graft/` survives as a _path_ until upstream renames it: it is the map
|
|
495
|
-
directory, not the product.
|
|
496
|
-
|
|
497
480
|
### Cache Miss Notices
|
|
498
481
|
|
|
499
482
|
Enable `showCacheMissNotices` in pi settings to see transcript notices on significant prompt-cache misses — useful for investigating unexpected token costs:
|
|
@@ -860,7 +843,7 @@ PI_PIDIFF_ENABLE=false pi
|
|
|
860
843
|
| `myk-pi-tools pr claude-md` | Fetch `CLAUDE.md` and `AGENTS.md` content for a PR's repository |
|
|
861
844
|
| `myk-pi-tools release create` | Create a GitHub release with changelog |
|
|
862
845
|
| `myk-pi-tools release plan` | Scoped-release plan: which packages changed since their last publish (per-package registry-anchored diff windows). Detection only — no proposed versions. Read-only; requires `release-map.toml` at the repo root — without it, reports lockstep mode and changes nothing (issue #965) |
|
|
863
|
-
| `myk-pi-tools release bump-scoped` | Bump ONLY the affected packages' version files. Versions are ALWAYS explicit: `--set NAME=VERSION` per affected package (the versions the LLM/user decided in the `/release` flow) or a global `--version X.Y.Z` — the plan never proposes versions. Each package versions from its own registry history (no global repo-tag guard — no forced jumps for long-skipped packages); tags are per-package `<name>@<version>`. Versions are validated (strict x.y.z, no leading zeros, strictly greater than the registry-published version). Errored packages, undetected version files, or partially-updatable manifests abort before or at the first bad write. Requires `release-map.toml` — without it, exit 1 (use `bump-version` for lockstep) (issue #965) |
|
|
846
|
+
| `myk-pi-tools release bump-scoped` | Bump ONLY the affected packages' version files. Versions are ALWAYS explicit: `--set NAME=VERSION` per affected package (the versions the LLM/user decided in the `/release` flow) or a global `--version X.Y.Z` — the plan never proposes versions. Each package versions from its own registry history (no global repo-tag guard — no forced jumps for long-skipped packages); tags are per-package `<name>@<version>`. Versions are validated (strict x.y.z, no leading zeros, strictly greater than the registry-published version). Errored packages, undetected version files, or partially-updatable manifests abort before or at the first bad write. Requires `release-map.toml` — without it, exit 1 (use `bump-version` for lockstep) (issue #965). When the repo has a `uv.lock` the bump refreshes it with `uv lock` and reports it under its own `lockfile` key — `updated[]` keeps only version files; a missing or failing `uv` reports `refreshed: false` instead of failing the release. |
|
|
864
847
|
| `myk-pi-tools db` | Review database query commands |
|
|
865
848
|
| `myk-pi-tools reviews` | Review handling commands (fetch, poll, address, post, store, store-session) |
|
|
866
849
|
| `myk-pi-tools reviews poll` | Poll a PR for review comments until approved or actionable — `--source qodo` reads findings through the `qodo` CLI (structured, with spans and authoritative open/closed status) when the binary is installed and authenticated, and falls back to the GitHub sticky-comment scrape otherwise. Read-only: replies are always posted as GitHub comments via `reviews address` |
|
|
@@ -22,6 +22,8 @@ const MIN_PROMPT_CHARS = 12;
|
|
|
22
22
|
const QUERY_MAX_CHARS = 2_000;
|
|
23
23
|
const QUERY_FALLBACK_CHARS = 200;
|
|
24
24
|
const INTERACTIVE_TIMEOUT_MS = 2_000;
|
|
25
|
+
// The first retrieval of a session pays process spawn plus map load (median 1.4s, #992), so cold gets a larger ceiling than warm.
|
|
26
|
+
const COLD_RETRIEVAL_TIMEOUT_MS = 10_000;
|
|
25
27
|
export const TRAIL_QUERY_TOOLS = ["trail_find_code", "trail_find_all", "trail_file_api", "trail_trace_calls", "trail_repo_map"] as const;
|
|
26
28
|
/**
|
|
27
29
|
* The five read-only Trail query tools, added to a child's tool list when it declares one.
|
|
@@ -48,7 +50,7 @@ export function trailToolsArgs(tools: readonly string[] | undefined, cwd: string
|
|
|
48
50
|
export type TrailState = "syncing" | "ready" | "stale" | "absent" | "failed";
|
|
49
51
|
export type TrailRun = (args: readonly string[], options: { cwd: string; timeoutMs?: number }) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
50
52
|
type GraphStatus = "fresh" | "stale" | "absent";
|
|
51
|
-
type Options = { enabled: boolean; setting?: (cwd: string, logger?: Logger) => boolean; executable?: () => Promise<boolean>; graph?: (cwd: string) => Promise<GraphStatus> | GraphStatus; retrieve?: (query: string, cwd: string) => Promise<{ pointers: string[] }>; worktree?: (cwd: string) => string | null; run?: TrailRun; interactiveTimeoutMs?: number; logger?: Logger };
|
|
53
|
+
type Options = { enabled: boolean; setting?: (cwd: string, logger?: Logger) => boolean; executable?: () => Promise<boolean>; graph?: (cwd: string) => Promise<GraphStatus> | GraphStatus; retrieve?: (query: string, cwd: string) => Promise<{ pointers: string[] }>; worktree?: (cwd: string) => string | null; run?: TrailRun; interactiveTimeoutMs?: number; coldTimeoutMs?: number; logger?: Logger };
|
|
52
54
|
type StatusTheme = { fg: (color: string, value: string) => string };
|
|
53
55
|
type Hit = { title?: string; pointer?: string; snippet?: string; code?: string };
|
|
54
56
|
type Ask = { coverage?: number; coverageStrong?: number; hits?: Hit[] };
|
|
@@ -71,11 +73,11 @@ export function trailMapDir(root: string, logger: Logger = log): string | null {
|
|
|
71
73
|
logger.debug("map_dir_resolved", { root, layout: "trail", dir: trail });
|
|
72
74
|
return trail;
|
|
73
75
|
}
|
|
74
|
-
const
|
|
75
|
-
if (existsSync(join(
|
|
76
|
+
const legacy = join(root, "graft");
|
|
77
|
+
if (existsSync(join(legacy, ".graph", "wiring.json"))) {
|
|
76
78
|
// Upstream owns this path name; we only read it until trail renames it.
|
|
77
|
-
logger.debug("map_dir_resolved", { root, layout: "legacy", dir:
|
|
78
|
-
return
|
|
79
|
+
logger.debug("map_dir_resolved", { root, layout: "legacy", dir: legacy });
|
|
80
|
+
return legacy;
|
|
79
81
|
}
|
|
80
82
|
// No map yet: `.trail/cache` or `.trail/learnings` marks a repo on trail before
|
|
81
83
|
// its first build lands, so that is where a map will appear.
|
|
@@ -145,7 +147,12 @@ function repoRelative(root: string, value: unknown): string | null {
|
|
|
145
147
|
return rel.startsWith("..") || isAbsolute(rel) ? null : rel.split(sep).join("/");
|
|
146
148
|
}
|
|
147
149
|
function samePath(left: string, right: string): boolean { return left === right || left.endsWith(`/${right}`) || right.endsWith(`/${left}`); }
|
|
148
|
-
function savedTokens(output: string
|
|
150
|
+
function savedTokens(output: string, logger: Logger = log): number {
|
|
151
|
+
const matches = [...output.matchAll(/\[trail\] tokens saved ≈ ([\d,]+)/g)];
|
|
152
|
+
const total = matches.reduce((sum, match) => sum + (Number(match[1].replaceAll(",", "")) || 0), 0);
|
|
153
|
+
logger.debug("saved_tokens", { matches: matches.length, total });
|
|
154
|
+
return total;
|
|
155
|
+
}
|
|
149
156
|
function savingsStore(ctx: any, root: string, logger: Logger = log): string {
|
|
150
157
|
let id: unknown = process.env.PI_SUBAGENT_CHILD === "1" ? process.env.__PI_PARENT_SESSION_ID : undefined;
|
|
151
158
|
const source = id ? "parent" : "session";
|
|
@@ -401,7 +408,7 @@ export function createTrailIntegration(options: Options) {
|
|
|
401
408
|
// telemetry into ~/.pi/logs/trail, which is the G1-G3 measurement source for #978.
|
|
402
409
|
const log = options.logger ?? createLogger("trail");
|
|
403
410
|
const child = process.env.PI_SUBAGENT_CHILD === "1";
|
|
404
|
-
const state = { generation: 0, root: "", store: "", dirtySignal: "", enabled: false, toolsRegistered: false, events: undefined as ExtensionAPI["events"] | undefined, value: "stale" as TrailState, nodeCount: 0, tokenSavings: 0, trailReads: 0, sourceReads: 0, repoMapShown: false, dirty: false, buildAttempted: false, refresh: undefined as Promise<boolean> | undefined, mapPaths: new Set<string>(), mapDirs: new Set<string>(), ctx: undefined as any, gate: { active: false, retrieved: false, pointers: new Set<string>() } };
|
|
411
|
+
const state = { generation: 0, root: "", store: "", dirtySignal: "", enabled: false, toolsRegistered: false, events: undefined as ExtensionAPI["events"] | undefined, value: "stale" as TrailState, nodeCount: 0, tokenSavings: 0, trailReads: 0, sourceReads: 0, repoMapShown: false, dirty: false, buildAttempted: false, warmed: false, coldAttempts: 0, refresh: undefined as Promise<boolean> | undefined, mapPaths: new Set<string>(), mapDirs: new Set<string>(), ctx: undefined as any, gate: { active: false, retrieved: false, pointers: new Set<string>() } };
|
|
405
412
|
const publishSavings = () => {
|
|
406
413
|
if (child || !state.events) return;
|
|
407
414
|
state.events.emit("pidash:trail-savings", { tokenSavings: state.tokenSavings });
|
|
@@ -498,7 +505,7 @@ export function createTrailIntegration(options: Options) {
|
|
|
498
505
|
} catch {}
|
|
499
506
|
};
|
|
500
507
|
const invalidate = (ctx: any, reason: string) => {
|
|
501
|
-
state.generation++; state.ctx = ctx; state.enabled = false; state.root = ""; state.store = ""; state.dirtySignal = ""; state.refresh = undefined; state.dirty = false; state.buildAttempted = false; state.nodeCount = 0; state.tokenSavings = 0; state.mapPaths = new Set(); state.repoMapShown = false; state.gate = { active: false, retrieved: false, pointers: new Set<string>() };
|
|
508
|
+
state.generation++; state.ctx = ctx; state.enabled = false; state.root = ""; state.store = ""; state.dirtySignal = ""; state.refresh = undefined; state.dirty = false; state.buildAttempted = false; state.warmed = false; state.coldAttempts = 0; state.nodeCount = 0; state.tokenSavings = 0; state.mapPaths = new Set(); state.repoMapShown = false; state.gate = { active: false, retrieved: false, pointers: new Set<string>() };
|
|
502
509
|
clearSlot("trail", ctx); publishSavings(); log.info("session_invalidated", { reason });
|
|
503
510
|
};
|
|
504
511
|
const invoke = async (args: string[]) => {
|
|
@@ -568,7 +575,7 @@ export function createTrailIntegration(options: Options) {
|
|
|
568
575
|
// they only skip the binary probe, which a one-shot run cannot report on.
|
|
569
576
|
// Returning here instead used to leave those sessions without any map tool.
|
|
570
577
|
const probe = !child && (isEligibleTrailStartup(event, ctx) || ["new", "reload", "resume", "fork"].includes(event?.reason));
|
|
571
|
-
state.generation++; state.refresh = undefined; state.dirty = false; state.buildAttempted = false; state.root = resolveWorktreeRoot(ctx.cwd); const root = state.root; state.repoMapShown = false; state.gate = { active: false, retrieved: false, pointers: new Set<string>() }; state.trailReads = 0; state.sourceReads = 0;
|
|
578
|
+
state.generation++; state.refresh = undefined; state.dirty = false; state.buildAttempted = false; state.warmed = false; state.coldAttempts = 0; state.root = resolveWorktreeRoot(ctx.cwd); const root = state.root; state.repoMapShown = false; state.gate = { active: false, retrieved: false, pointers: new Set<string>() }; state.trailReads = 0; state.sourceReads = 0;
|
|
572
579
|
state.enabled = true; loadMap(root); state.value = "syncing";
|
|
573
580
|
try { state.store = savingsStore(ctx, root, log); state.dirtySignal = state.store ? `${state.store}.dirty` : ""; if (state.dirtySignal) { mkdirSync(state.dirtySignal, { recursive: true, mode: 0o700 }); chmodSync(state.dirtySignal, 0o700); } state.tokenSavings = readSavings(state.store); } catch (error: any) { state.store = ""; state.dirtySignal = ""; state.tokenSavings = 0; log.warn("savings_restore_failed", { code: error?.code }); }
|
|
574
581
|
// Provisional state while we probe. This used to be "failed", which made
|
|
@@ -624,7 +631,7 @@ export function createTrailIntegration(options: Options) {
|
|
|
624
631
|
log.info("gate_satisfied", { by: tool.startsWith("trail_") ? "tool" : "auto_pointer_read", tool });
|
|
625
632
|
}
|
|
626
633
|
const kind = !event.isError ? classifyToolUse(event.toolName, event.input) : null;
|
|
627
|
-
const savings = !event.isError ? savedTokens(toolText(event.content)) : 0;
|
|
634
|
+
const savings = !event.isError ? savedTokens(toolText(event.content), log) : 0;
|
|
628
635
|
if (kind === "trail") state.trailReads++; else if (kind === "source") state.sourceReads++;
|
|
629
636
|
addSavings(savings);
|
|
630
637
|
if (kind || savings) { setState(state.dirty ? "stale" : state.value === "absent" || state.value === "failed" ? state.value : "ready"); log.info("session_metric", { tool: event.toolName, kind, savings, trailReads: state.trailReads, sourceReads: state.sourceReads, tokenSavings: state.tokenSavings }); }
|
|
@@ -682,7 +689,14 @@ export function createTrailIntegration(options: Options) {
|
|
|
682
689
|
if (!query || !substantive(query)) { finish("guidance_only_control_prompt"); return guidance; }
|
|
683
690
|
state.gate.active = true;
|
|
684
691
|
// One shared budget: a fallback retry must not double the turn's deadline.
|
|
685
|
-
|
|
692
|
+
// `warmed` only flips on a successful retrieval, so a cold attempt that
|
|
693
|
+
// times out or fails keeps the cold ceiling; the attempt cap stops a
|
|
694
|
+
// genuinely broken CLI from paying it forever.
|
|
695
|
+
const cold = !state.warmed && state.coldAttempts < 2;
|
|
696
|
+
if (cold) state.coldAttempts++;
|
|
697
|
+
const budget = options.interactiveTimeoutMs ?? (cold ? (options.coldTimeoutMs ?? COLD_RETRIEVAL_TIMEOUT_MS) : INTERACTIVE_TIMEOUT_MS);
|
|
698
|
+
log.debug("retrieval_budget", { cold, budgetMs: budget, attempt: state.coldAttempts });
|
|
699
|
+
const deadline = started + budget;
|
|
686
700
|
const remaining = () => Math.max(1, deadline - Date.now());
|
|
687
701
|
type Attempt = { hits: Hit[]; failure?: { decision: string; note: string; diagnostic?: string } };
|
|
688
702
|
/** One bounded retrieval attempt; null means the session changed under us. */
|
|
@@ -690,15 +704,17 @@ export function createTrailIntegration(options: Options) {
|
|
|
690
704
|
if (options.retrieve) {
|
|
691
705
|
const result = await withTimeout(options.retrieve(value, root), remaining(), log);
|
|
692
706
|
if (!current()) return null;
|
|
707
|
+
state.warmed = true;
|
|
693
708
|
return { hits: result.pointers.map(pointer => ({ title: pointer, pointer })) };
|
|
694
709
|
}
|
|
695
710
|
// `--source` inlines the code, so a hit can be used without re-opening the file.
|
|
696
711
|
const result = await withTimeout(run(["ask", value, ".", "--json", "-n", "3", "--source"], { cwd: root, timeoutMs: remaining() }), remaining(), log);
|
|
697
712
|
if (!current()) return null;
|
|
698
|
-
if (result.code) { const reason = diagnostic(result.stderr); return { hits: [], failure: { decision: "ask_failed", diagnostic: reason, note: `[trail] Retrieval failed: ${reason}.
|
|
699
|
-
const savings = savedTokens(result.stdout); addSavings(savings); setState(state.value); loadMap(root);
|
|
713
|
+
if (result.code) { const reason = diagnostic(result.stderr); return { hits: [], failure: { decision: "ask_failed", diagnostic: reason, note: `[trail] Retrieval failed: ${reason}. Call trail_find_code (describe the concept, not an identifier) for the map answer; raw navigation is available if it returns nothing relevant.` } }; }
|
|
714
|
+
const savings = savedTokens(result.stdout, log); addSavings(savings); setState(state.value); loadMap(root);
|
|
700
715
|
const ask = parseAsk(result.stdout);
|
|
701
|
-
if (!ask) return { hits: [], failure: { decision: "invalid_ask_json", note: "[trail] Retrieval returned malformed output.
|
|
716
|
+
if (!ask) return { hits: [], failure: { decision: "invalid_ask_json", note: "[trail] Retrieval returned malformed output. Call trail_find_code (describe the concept, not an identifier) for the map answer; raw navigation is available if it returns nothing relevant." } };
|
|
717
|
+
state.warmed = true;
|
|
702
718
|
return { hits: relevant(ask) };
|
|
703
719
|
};
|
|
704
720
|
let hits: Hit[] = [];
|
|
@@ -716,7 +732,7 @@ export function createTrailIntegration(options: Options) {
|
|
|
716
732
|
if (!result) return;
|
|
717
733
|
if (result.failure) { state.gate.active = false; finish(result.failure.decision, result.failure.diagnostic ? { diagnostic: result.failure.diagnostic } : {}); return inject(event, [], result.failure.note); }
|
|
718
734
|
hits = result.hits;
|
|
719
|
-
} catch (error: any) { if (!current()) return; state.gate.active = false; finish("ask_error"); return inject(event, [], `[trail] Retrieval failed: ${text(error?.message, 240) ?? "unknown error"}.
|
|
735
|
+
} catch (error: any) { if (!current()) return; state.gate.active = false; finish("ask_error"); return inject(event, [], `[trail] Retrieval failed: ${text(error?.message, 240) ?? "unknown error"}. Call trail_find_code (describe the concept, not an identifier) for the map answer; raw navigation is available if it returns nothing relevant.`); }
|
|
720
736
|
// Still no map on disk (the seed found nothing, or this was never a
|
|
721
737
|
// worktree): fail open rather than blocking navigation on a map we lack.
|
|
722
738
|
if (!state.mapPaths.size) { state.gate.active = false; log.info("gate_skipped", { reason: "no_map" }); }
|
package/package.json
CHANGED
package/prompts/release.md
CHANGED
|
@@ -125,7 +125,9 @@ root only).
|
|
|
125
125
|
one `--set <name>=<version>` per affected package carrying YOUR final
|
|
126
126
|
proposed versions (after any user overrides). Use `--version X.Y.Z`
|
|
127
127
|
instead when the user passed an explicit version. Parse the JSON:
|
|
128
|
-
stage ONLY the files in `updated[]
|
|
128
|
+
stage ONLY the files in `updated[]` plus the refreshed lockfile
|
|
129
|
+
(`lockfile.path` when `lockfile.refreshed` is true — `refreshed: false`
|
|
130
|
+
is a warning: run `uv lock` yourself before committing); each entry in `packages[]`
|
|
129
131
|
carries its per-package `tag` (`<name>@<version>`). Bump PR, merge,
|
|
130
132
|
and branch sync follow the standard Phase 5 delegation (git-expert /
|
|
131
133
|
github-expert), with the bump PR message `chore: bump version to
|