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 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 graft = join(root, "graft");
75
- if (existsSync(join(graft, ".graph", "wiring.json"))) {
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: graft });
78
- return graft;
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): number { return [...output.matchAll(/\[(?:trail|graft)\] tokens saved ≈ ([\d,]+)/g)].reduce((total, match) => total + (Number(match[1].replaceAll(",", "")) || 0), 0); }
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
- const deadline = started + (options.interactiveTimeoutMs ?? INTERACTIVE_TIMEOUT_MS);
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}. Raw navigation is available.` } }; }
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. Raw navigation is available." } };
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"}. Raw navigation is available.`); }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-orchestrator-config",
3
- "version": "4.22.0",
3
+ "version": "4.22.2",
4
4
  "description": "Orchestrator pattern for pi: specialist subagents, enforcement, code review loop, workflow commands",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -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[]`; each entry in `packages[]`
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