@diegopetrucci/pi-extensions 0.1.78 → 0.1.79

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.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/extensions/agent-workflow-audit/index.ts +3 -20
  3. package/extensions/agent-workflow-audit/package.json +1 -1
  4. package/extensions/annotate-git-diff/glimpseui.d.ts +5 -65
  5. package/extensions/annotate-git-diff/package.json +1 -1
  6. package/extensions/annotate-last-message/glimpseui.d.ts +5 -65
  7. package/extensions/annotate-last-message/package.json +1 -1
  8. package/extensions/annotate-last-message/web/index.html +0 -27
  9. package/extensions/code-reviewer/index.ts +0 -34
  10. package/extensions/code-reviewer/package.json +1 -1
  11. package/extensions/context-cap/README.md +9 -9
  12. package/extensions/context-cap/index.ts +3 -3
  13. package/extensions/context-cap/package.json +2 -2
  14. package/extensions/context-inspector/index.ts +2 -46
  15. package/extensions/context-inspector/package.json +1 -1
  16. package/extensions/contrarian/index.ts +1 -27
  17. package/extensions/contrarian/package.json +1 -1
  18. package/extensions/dynamic-context-pruning/README.md +16 -7
  19. package/extensions/dynamic-context-pruning/index.ts +9 -114
  20. package/extensions/dynamic-context-pruning/package.json +1 -1
  21. package/extensions/fast/README.md +16 -9
  22. package/extensions/fast/index.ts +9 -5
  23. package/extensions/fast/package.json +1 -1
  24. package/extensions/git-footer/index.ts +2 -16
  25. package/extensions/git-footer/package.json +1 -1
  26. package/extensions/librarian/index.ts +7 -39
  27. package/extensions/librarian/package.json +1 -1
  28. package/extensions/minimal-footer/README.md +5 -5
  29. package/extensions/minimal-footer/index.ts +3 -17
  30. package/extensions/minimal-footer/minimal-footer.example.json +1 -1
  31. package/extensions/minimal-footer/openai-usage.ts +2 -11
  32. package/extensions/minimal-footer/package.json +1 -1
  33. package/extensions/oracle/index.ts +1 -27
  34. package/extensions/oracle/package.json +1 -1
  35. package/extensions/permission-gate/index.ts +31 -63
  36. package/extensions/permission-gate/package.json +1 -1
  37. package/extensions/review/README.md +2 -0
  38. package/extensions/review/package.json +1 -1
  39. package/extensions/triage-comments/index.ts +0 -15
  40. package/extensions/triage-comments/package.json +1 -1
  41. package/package.json +1 -1
@@ -13,10 +13,6 @@ type GitStatusSnapshot = {
13
13
 
14
14
  type PullRequestSnapshot = {
15
15
  number?: number | string;
16
- state?: string;
17
- isDraft?: boolean;
18
- url?: string;
19
- title?: string;
20
16
  };
21
17
 
22
18
  type CommandResult = {
@@ -74,7 +70,7 @@ const GIT_STATUS_ARGS = [
74
70
  "--porcelain=v2",
75
71
  "--branch",
76
72
  ] as const;
77
- const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number,state,isDraft,url,title"] as const;
73
+ const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number"] as const;
78
74
 
79
75
  function createEmptyGitStatus(): GitStatusSnapshot {
80
76
  return {
@@ -202,10 +198,6 @@ function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
202
198
  if (typeof record.number === "number" || typeof record.number === "string") {
203
199
  snapshot.number = record.number;
204
200
  }
205
- if (typeof record.state === "string") snapshot.state = record.state;
206
- if (typeof record.isDraft === "boolean") snapshot.isDraft = record.isDraft;
207
- if (typeof record.url === "string") snapshot.url = record.url;
208
- if (typeof record.title === "string") snapshot.title = record.title;
209
201
  return snapshot;
210
202
  }
211
203
 
@@ -228,13 +220,7 @@ function pullRequestSnapshotsEqual(
228
220
  left: PullRequestSnapshot | undefined,
229
221
  right: PullRequestSnapshot | undefined,
230
222
  ): boolean {
231
- return (
232
- left?.number === right?.number &&
233
- left?.state === right?.state &&
234
- left?.isDraft === right?.isDraft &&
235
- left?.url === right?.url &&
236
- left?.title === right?.title
237
- );
223
+ return left?.number === right?.number;
238
224
  }
239
225
 
240
226
  function defaultRunner(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-git-footer",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "A TLH-style git status add-on for pi's built-in footer.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -766,11 +766,6 @@ async function writeLibrarianPreferences(preferences: LibrarianPreferences): Pro
766
766
  await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
767
767
  }
768
768
 
769
- async function writeCachePreference(preference: CacheMode): Promise<void> {
770
- const preferences = await readLibrarianPreferences();
771
- await writeLibrarianPreferences({ ...preferences, cacheMode: preference });
772
- }
773
-
774
769
  function resolveCacheDecision(preference: CacheMode): { enabled: boolean; reason: string } {
775
770
  if (preference === "enabled") {
776
771
  return { enabled: true, reason: "cache preference enabled; using cached local checkouts" };
@@ -1045,7 +1040,7 @@ function tryCurlyQuotePathVariant(filePath: string): string {
1045
1040
  }
1046
1041
  /**
1047
1042
  * Returns all read path candidates including Pi's resolveReadPathAsync fallback
1048
- * variants. Used only for the read tool guard; grep/find/ls use resolveToCwd only.
1043
+ * variants.
1049
1044
  */
1050
1045
  function getReadPathCandidates(resolved: string): string[] {
1051
1046
  const nfd = tryNFDPathVariant(resolved);
@@ -1058,11 +1053,6 @@ function getReadPathCandidates(resolved: string): string[] {
1058
1053
  ];
1059
1054
  }
1060
1055
 
1061
- function resolveToolPath(cwd: string, rawPath: string): string {
1062
- const normalized = rawPath.startsWith("@") ? rawPath.slice(1) : rawPath;
1063
- return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
1064
- }
1065
-
1066
1056
  function getBlockedBashReason(
1067
1057
  command: string,
1068
1058
  options: { workspace: string; cacheRoot: string; cacheEnabled: boolean },
@@ -1165,17 +1155,9 @@ function createLibrarianRuntimeGuardExtension(options: {
1165
1155
  (options.cacheEnabled && (isInside(options.cacheRoot, p) || isInside(realCacheRoot, p)))
1166
1156
  );
1167
1157
  }
1168
- const resolved = resolveToolPath(options.workspace, input.path);
1169
- const realPath = await fs.realpath(resolved).catch(() => resolved);
1170
- if (!isAllowedPath(realPath))
1171
- return {
1172
- block: true,
1173
- reason: `Librarian read is limited to its workspace/cache: ${realPath}`,
1174
- };
1175
- // Pi-effective path check (monotonic: block if either resolution is
1176
- // outside). For the read tool, also check Pi's resolveReadPathAsync
1177
- // fallback variants so a missing file whose variant resolves outside is
1178
- // not silently allowed.
1158
+ // For the read tool, also check Pi's resolveReadPathAsync fallback
1159
+ // variants so a missing file whose variant resolves outside is not
1160
+ // silently allowed.
1179
1161
  try {
1180
1162
  const piResolved = resolveToCwd(input.path, options.workspace);
1181
1163
  const candidates = getReadPathCandidates(piResolved);
@@ -1482,7 +1464,6 @@ export const __test__ = {
1482
1464
  parseModelPreference,
1483
1465
  resolveThinkingLevel,
1484
1466
  aggregateAssistantUsage,
1485
- addAssistantMessageUsage,
1486
1467
  addSessionEventUsage,
1487
1468
  inspectFinalAssistant,
1488
1469
  classifyRunFailure,
@@ -1490,14 +1471,12 @@ export const __test__ = {
1490
1471
  createLibrarianAbortController,
1491
1472
  cleanupExpiredCache,
1492
1473
  cleanupExpiredRepos,
1493
- cleanupLockOwnershipError,
1494
1474
  tryAcquireCleanupLock,
1495
1475
  heartbeatCleanupLock,
1496
1476
  releaseCleanupLock,
1497
1477
  CLEANUP_LOCK_FILE,
1498
1478
  CLEANUP_LOCK_STALE_MS,
1499
1479
  CLEANUP_LOCK_MAX_LEASE_MS,
1500
- isCleanupLockLeaseExpired,
1501
1480
  };
1502
1481
 
1503
1482
  export default function librarianExtension(pi: ExtensionAPI) {
@@ -1668,17 +1647,6 @@ export default function librarianExtension(pi: ExtensionAPI) {
1668
1647
  const cacheRoot = getUserCacheReposRoot();
1669
1648
  const configPath = getCacheConfigPath();
1670
1649
 
1671
- const setPreference = async (mode: CacheMode): Promise<string | undefined> => {
1672
- cachePreference = mode;
1673
- try {
1674
- await writeCachePreference(mode);
1675
- return undefined;
1676
- } catch (error) {
1677
- const message = error instanceof Error ? error.message : String(error);
1678
- return `Preference changed for this process, but could not save ${configPath}: ${message}`;
1679
- }
1680
- };
1681
-
1682
1650
  const formatSetMessage = (mode: CacheMode, warning?: string): string => {
1683
1651
  const main =
1684
1652
  mode === "enabled"
@@ -1688,20 +1656,20 @@ export default function librarianExtension(pi: ExtensionAPI) {
1688
1656
  };
1689
1657
 
1690
1658
  if (action === "on" || action === "enable") {
1691
- const warning = await setPreference("enabled");
1659
+ const warning = await savePreferences({ ...currentPreferences(), cacheMode: "enabled" });
1692
1660
  notifyCommand(ctx, formatSetMessage("enabled", warning), warning ? "warning" : "info");
1693
1661
  return;
1694
1662
  }
1695
1663
 
1696
1664
  if (action === "off" || action === "disable") {
1697
- const warning = await setPreference("disabled");
1665
+ const warning = await savePreferences({ ...currentPreferences(), cacheMode: "disabled" });
1698
1666
  notifyCommand(ctx, formatSetMessage("disabled", warning), warning ? "warning" : "info");
1699
1667
  return;
1700
1668
  }
1701
1669
 
1702
1670
  if (action === "toggle") {
1703
1671
  const next = cachePreference === "enabled" ? "disabled" : "enabled";
1704
- const warning = await setPreference(next);
1672
+ const warning = await savePreferences({ ...currentPreferences(), cacheMode: next });
1705
1673
  notifyCommand(ctx, formatSetMessage(next, warning), warning ? "warning" : "info");
1706
1674
  return;
1707
1675
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-librarian",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "A pi GitHub research scout with a toggleable local repo checkout cache under the user's OS cache directory.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -10,7 +10,7 @@ It replaces pi's built-in footer with a cleaner two-line layout that focuses on
10
10
  - TLH-style git dirty counts, ahead/behind counts, and optional PR number
11
11
  - current repo name
12
12
  - current context percentage
13
- - red `DUMB ZONE` indicator when context usage is above 300k tokens
13
+ - red `DUMB ZONE` indicator when context usage is above 350k tokens
14
14
  - current model and thinking level
15
15
  - OpenAI Codex usage windows, labeled from the durations reported by OpenAI
16
16
  - `xp` marker when Pi experimental features are enabled
@@ -39,7 +39,7 @@ main · +1 ~2 ?3 ↑1 • PR #42
39
39
 
40
40
  The markers are `!` conflicts, `+` staged, `~` unstaged, `?` untracked, `↑` ahead, and `↓` behind. PR numbers are fetched best-effort with `gh pr view`. Git polling is skipped until Pi reports that the project is trusted, and `git status` is run with fsmonitor disabled.
41
41
 
42
- When context usage is above 300k tokens, the bottom-left line includes a red warning:
42
+ When context usage is above 350k tokens, the bottom-left line includes a red warning:
43
43
 
44
44
  ```text
45
45
  44.1% · DUMB ZONE
@@ -115,7 +115,7 @@ Example:
115
115
  "showPercent": true,
116
116
  "dumbZone": {
117
117
  "enabled": true,
118
- "thresholdTokens": 300000,
118
+ "thresholdTokens": 350000,
119
119
  "label": "DUMB ZONE",
120
120
  "color": "error"
121
121
  }
@@ -231,7 +231,7 @@ Disable git dirty/ahead/PR status:
231
231
 
232
232
  - **Top left:** current git branch plus dirty/ahead/PR status when available
233
233
  - **Top right:** current repo directory name
234
- - **Bottom left:** current context usage percentage, plus red `DUMB ZONE` above 300k context tokens
234
+ - **Bottom left:** current context usage percentage, plus red `DUMB ZONE` above 350k context tokens
235
235
  - **Bottom left on `openai-codex`:** current context usage percentage plus the Codex usage windows reported for the account
236
236
  - **Bottom left on `openai` ChatGPT OAuth:** current context usage percentage plus `usage unsupported`; no legacy WHAM request is attempted
237
237
  - **Bottom left with `PI_EXPERIMENTAL=1`:** current context usage percentage plus `xp`
@@ -246,7 +246,7 @@ This extension also lives inside the broader [`pi-extensions`](../../README.md)
246
246
  - Replaces pi's built-in footer entirely.
247
247
  - Uses pi footer data for git branch updates, plus background cached git/gh checks for dirty counts and PR number after project trust is granted.
248
248
  - Shows only context percentage, not context window size.
249
- - Shows `DUMB ZONE` only while context usage is above 300k tokens.
249
+ - Shows `DUMB ZONE` only while context usage is above 350k tokens.
250
250
  - Shows the model id rather than a provider-specific display label.
251
251
  - Shows `xp` when `PI_EXPERIMENTAL=1`.
252
252
  - For `openai-codex`, reads pi's stored OAuth login and fetches usage from ChatGPT's backend usage endpoint. Window labels are inferred from each window's reported duration; missing or unrecognized durations use neutral `usage` labels rather than guessing from window position.
@@ -24,7 +24,7 @@ const DEFAULT_CONFIG: MinimalFooterConfig = {
24
24
  showPercent: true,
25
25
  dumbZone: {
26
26
  enabled: true,
27
- thresholdTokens: 300_000,
27
+ thresholdTokens: 350_000,
28
28
  label: "DUMB ZONE",
29
29
  color: "error",
30
30
  },
@@ -148,10 +148,6 @@ type GitStatusSnapshot = {
148
148
 
149
149
  type PullRequestSnapshot = {
150
150
  number?: number | string;
151
- state?: string;
152
- isDraft?: boolean;
153
- url?: string;
154
- title?: string;
155
151
  };
156
152
 
157
153
  type CommandResult = {
@@ -200,7 +196,7 @@ const GIT_STATUS_ARGS = [
200
196
  "--porcelain=v2",
201
197
  "--branch",
202
198
  ] as const;
203
- const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number,state,isDraft,url,title"] as const;
199
+ const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number"] as const;
204
200
 
205
201
  function readConfigFile(path: string): RecursivePartial<MinimalFooterConfig> {
206
202
  if (!existsSync(path)) return {};
@@ -468,10 +464,6 @@ function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
468
464
  if (typeof record.number === "number" || typeof record.number === "string") {
469
465
  snapshot.number = record.number;
470
466
  }
471
- if (typeof record.state === "string") snapshot.state = record.state;
472
- if (typeof record.isDraft === "boolean") snapshot.isDraft = record.isDraft;
473
- if (typeof record.url === "string") snapshot.url = record.url;
474
- if (typeof record.title === "string") snapshot.title = record.title;
475
467
  return snapshot;
476
468
  }
477
469
 
@@ -494,13 +486,7 @@ function pullRequestSnapshotsEqual(
494
486
  left: PullRequestSnapshot | undefined,
495
487
  right: PullRequestSnapshot | undefined,
496
488
  ): boolean {
497
- return (
498
- left?.number === right?.number &&
499
- left?.state === right?.state &&
500
- left?.isDraft === right?.isDraft &&
501
- left?.url === right?.url &&
502
- left?.title === right?.title
503
- );
489
+ return left?.number === right?.number;
504
490
  }
505
491
 
506
492
  function defaultRunner(
@@ -3,7 +3,7 @@
3
3
  "showPercent": true,
4
4
  "dumbZone": {
5
5
  "enabled": true,
6
- "thresholdTokens": 300000,
6
+ "thresholdTokens": 350000,
7
7
  "label": "DUMB ZONE",
8
8
  "color": "error"
9
9
  }
@@ -15,7 +15,6 @@ export type UsageAvailability = "unsupported" | "unavailable";
15
15
 
16
16
  interface WhamUsageWindow {
17
17
  limit_window_seconds?: number;
18
- reset_at?: number;
19
18
  used_percent?: number;
20
19
  }
21
20
 
@@ -29,7 +28,6 @@ interface WhamUsageResponse {
29
28
  export interface UsageWindow {
30
29
  usedPercent?: number;
31
30
  windowSeconds?: number;
32
- resetAt?: number;
33
31
  }
34
32
 
35
33
  export interface UsageSnapshot {
@@ -59,19 +57,12 @@ function normalizeWindowSeconds(value?: number): number | undefined {
59
57
  return value;
60
58
  }
61
59
 
62
- function normalizeResetAt(value?: number): number | undefined {
63
- if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
64
- return value * 1000;
65
- }
66
-
67
60
  function parseUsageWindow(window?: WhamUsageWindow): UsageWindow | undefined {
68
61
  if (!window) return undefined;
69
62
  const usedPercent = normalizeUsedPercent(window.used_percent);
70
63
  const windowSeconds = normalizeWindowSeconds(window.limit_window_seconds);
71
- const resetAt = normalizeResetAt(window.reset_at);
72
- if (usedPercent === undefined && windowSeconds === undefined && resetAt === undefined)
73
- return undefined;
74
- return { usedPercent, windowSeconds, resetAt };
64
+ if (usedPercent === undefined && windowSeconds === undefined) return undefined;
65
+ return { usedPercent, windowSeconds };
75
66
  }
76
67
 
77
68
  function parseUsageSnapshot(data: WhamUsageResponse): Omit<UsageSnapshot, "fetchedAt"> {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-minimal-footer",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "A minimal custom footer for pi.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1810,8 +1810,7 @@ function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {})
1810
1810
  onUpdate?.(partial);
1811
1811
  };
1812
1812
 
1813
- let lastErrorResult: { ok: false; error: string; details: OracleDetails } | undefined;
1814
- for (let index = 0; index < attempts.length; index++) {
1813
+ for (let index = 0; ; index++) {
1815
1814
  const previous = index > 0 ? attempts[index - 1] : undefined;
1816
1815
  const attempt = previous
1817
1816
  ? withFallbackReason(attempts[index], previous)
@@ -1835,7 +1834,6 @@ function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {})
1835
1834
  });
1836
1835
  }
1837
1836
 
1838
- lastErrorResult = result;
1839
1837
  const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
1840
1838
  if (!canFallBack) {
1841
1839
  return finalizeResult({
@@ -1844,30 +1842,6 @@ function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {})
1844
1842
  });
1845
1843
  }
1846
1844
  }
1847
-
1848
- return finalizeResult({
1849
- content: [
1850
- {
1851
- type: "text",
1852
- text: lastErrorResult?.error ?? "Oracle could not select an available model.",
1853
- },
1854
- ],
1855
- details: lastErrorResult?.details ?? {
1856
- modelRef: "",
1857
- provider: ctx.model?.provider ?? "unknown",
1858
- modelId: "",
1859
- modelName: undefined,
1860
- thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
1861
- autoSelected: true,
1862
- selectionReason: "No authenticated models were available to run the oracle.",
1863
- includeBash: params.includeBash ?? false,
1864
- usage: createEmptyUsage(),
1865
- stderr: "",
1866
- exitCode: 1,
1867
- durationMs: 0,
1868
- cwd: params.cwd ?? ctx.cwd,
1869
- },
1870
- });
1871
1845
  } finally {
1872
1846
  activeRuns.delete(toolCallId);
1873
1847
  updateOracleUi(ctx, activeRuns);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-oracle",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "description": "An Amp-style oracle extension for pi that consults the strongest reasoning model on your current provider.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -77,17 +77,9 @@ function normalizeWindowsShellPathLocal(filePath: string): string {
77
77
  }
78
78
 
79
79
  /**
80
- * Mirrors Pi's normalizePath (dist/utils/paths.js).
81
- *
82
- * Options (all default false/true as noted):
83
- * normalizeUnicodeSpaces — replace Unicode space characters with ASCII space.
84
- * stripAtPrefix — strip a leading "@" path sigil.
85
- * expandTilde — expand "~"/"~/" to homedir (default true).
86
- *
87
- * On win32, Git Bash / WSL drive-path normalisation always runs (not gated
88
- * by expandTilde); only the tilde expansion itself is gated by expandTilde.
89
- * Always resolves "file://" URLs via fileURLToPath; throws for non-local hosts
90
- * (e.g. file://hostname/…) or invalid URLs.
80
+ * Mirrors Pi's normalizePath (dist/utils/paths.js). On win32, Git Bash / WSL
81
+ * drive-path normalisation always runs; only tilde expansion is gated by
82
+ * expandTilde.
91
83
  */
92
84
  function normalizePath(
93
85
  input: string,
@@ -797,6 +789,28 @@ async function confirmProtectedPathAction(
797
789
  );
798
790
  }
799
791
 
792
+ async function confirmLexicalOrResolvedPath(
793
+ toolName: "write" | "edit",
794
+ normalizedPath: NormalizedPath,
795
+ ctx: ConfirmationContext & { cwd?: string },
796
+ ): Promise<GuardDecision> {
797
+ // Monotonic hardening: also check via Pi-style path resolution against ctx.cwd.
798
+ // OR logic: protected if EITHER the lexical check OR the Pi-resolved check matches.
799
+ // When only the Pi-resolved check catches a path, show the resolved absolute path in
800
+ // the reason so the user understands why the plain-looking path is protected.
801
+ const lexicalProtected = isProtectedPath(normalizedPath);
802
+ const piResult = checkPiResolvedPath(normalizedPath.original, ctx.cwd ?? process.cwd());
803
+ const piAlsoProtected = piResult.status === "protected" || piResult.status === "error";
804
+ const displaySuffix = lexicalProtected
805
+ ? undefined
806
+ : piResult.status === "protected"
807
+ ? ` (resolves to ${piResult.resolvedPath})`
808
+ : piResult.status === "error"
809
+ ? " (path could not be resolved)"
810
+ : undefined;
811
+ return confirmProtectedPathAction(toolName, normalizedPath, ctx, piAlsoProtected, displaySuffix);
812
+ }
813
+
800
814
  export default function (pi: ExtensionAPI) {
801
815
  pi.on("tool_call", async (event, ctx) => {
802
816
  if (event.toolName === "bash" || event.toolName === "powershell") {
@@ -824,60 +838,14 @@ export default function (pi: ExtensionAPI) {
824
838
  return undefined;
825
839
  }
826
840
 
827
- if (event.toolName === "write") {
828
- const normalizedPath = validateWriteInput(event.input);
829
- if (!normalizedPath) {
830
- return { block: true, reason: "Malformed write input blocked" };
831
- }
832
- // Monotonic hardening: also check via Pi-style path resolution against ctx.cwd.
833
- // OR logic: protected if EITHER the lexical check OR the Pi-resolved check matches.
834
- // When only the Pi-resolved check catches a path, show the resolved absolute path in
835
- // the reason so the user understands why the plain-looking path is protected.
836
- const lexicalProtected = isProtectedPath(normalizedPath);
837
- const piResult = checkPiResolvedPath(normalizedPath.original, ctx.cwd ?? process.cwd());
838
- const piAlsoProtected = piResult.status === "protected" || piResult.status === "error";
839
- const displaySuffix = lexicalProtected
840
- ? undefined
841
- : piResult.status === "protected"
842
- ? ` (resolves to ${piResult.resolvedPath})`
843
- : piResult.status === "error"
844
- ? " (path could not be resolved)"
845
- : undefined;
846
- return confirmProtectedPathAction(
847
- "write",
848
- normalizedPath,
849
- ctx,
850
- piAlsoProtected,
851
- displaySuffix,
852
- );
853
- }
854
-
855
- if (event.toolName === "edit") {
856
- const normalizedPath = validateEditInput(event.input);
841
+ if (event.toolName === "write" || event.toolName === "edit") {
842
+ const toolName = event.toolName;
843
+ const normalizedPath =
844
+ toolName === "write" ? validateWriteInput(event.input) : validateEditInput(event.input);
857
845
  if (!normalizedPath) {
858
- return { block: true, reason: "Malformed edit input blocked" };
846
+ return { block: true, reason: `Malformed ${toolName} input blocked` };
859
847
  }
860
- // Monotonic hardening: also check via Pi-style path resolution against ctx.cwd.
861
- // OR logic: protected if EITHER the lexical check OR the Pi-resolved check matches.
862
- // When only the Pi-resolved check catches a path, show the resolved absolute path in
863
- // the reason so the user understands why the plain-looking path is protected.
864
- const lexicalProtected = isProtectedPath(normalizedPath);
865
- const piResult = checkPiResolvedPath(normalizedPath.original, ctx.cwd ?? process.cwd());
866
- const piAlsoProtected = piResult.status === "protected" || piResult.status === "error";
867
- const displaySuffix = lexicalProtected
868
- ? undefined
869
- : piResult.status === "protected"
870
- ? ` (resolves to ${piResult.resolvedPath})`
871
- : piResult.status === "error"
872
- ? " (path could not be resolved)"
873
- : undefined;
874
- return confirmProtectedPathAction(
875
- "edit",
876
- normalizedPath,
877
- ctx,
878
- piAlsoProtected,
879
- displaySuffix,
880
- );
848
+ return confirmLexicalOrResolvedPath(toolName, normalizedPath, ctx);
881
849
  }
882
850
 
883
851
  return undefined;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-permission-gate",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "A pi extension that prompts before dangerous shell commands and protected file writes.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -29,7 +29,9 @@ Then reload pi:
29
29
 
30
30
  ## Notes
31
31
 
32
+ - `/review` requires interactive mode (TUI); `/end-review` requires a UI.
32
33
  - This fork adds loop fixing, custom review instructions persisted in the session, a blocking-findings parser, and `/end-review` handoff modes (return only, return and summarize, or return and fix findings). Loop fixing cycles until the parser reports no blocking findings: tagged P0–P2 items, or a "needs attention" verdict when no priority tags are present. Commit review is excluded from loop fixing.
34
+ - Loop fixing always uses Empty branch mode and skips the Empty branch / Current session choice.
33
35
  - PR review requires `gh` access and a clean working tree for tracked files.
34
36
  - If the project is trusted and a `REVIEW_GUIDELINES.md` file exists next to the repo's `<pi-config-dir>` directory, its contents are appended to the review prompt. `<pi-config-dir>` is Pi's runtime config directory name (`CONFIG_DIR_NAME`; `.pi` by default).
35
37
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-review",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "A standalone pi extension that adds /review and /end-review commands adapted from mitsuhiko/agent-stuff.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -272,8 +272,6 @@ type ToolCall = {
272
272
  id: string;
273
273
  name: string;
274
274
  args: unknown;
275
- startedAt: number;
276
- endedAt?: number;
277
275
  isError?: boolean;
278
276
  };
279
277
 
@@ -736,12 +734,6 @@ function getReadPathCandidates(resolved: string): string[] {
736
734
  ];
737
735
  }
738
736
 
739
- function resolveToolPath(cwd: string, rawPath: string | undefined): string {
740
- const input = rawPath?.trim() || ".";
741
- const normalized = input.startsWith("@") ? input.slice(1) : input;
742
- return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
743
- }
744
-
745
737
  async function assertToolPathInsideCwd(
746
738
  cwd: string,
747
739
  rawPath: unknown,
@@ -754,11 +746,6 @@ async function assertToolPathInsideCwd(
754
746
  // where realpath fails and the lexical fallback starts with the alias prefix.
755
747
  const root = await fs.realpath(cwd).catch(() => path.resolve(cwd));
756
748
  const lexicalCwd = path.resolve(cwd);
757
- const resolved = resolveToolPath(cwd, rawPath);
758
- const realPath = await fs.realpath(resolved).catch(() => resolved);
759
- if (!isInside(root, realPath) && !isInside(lexicalCwd, realPath))
760
- return `${toolName} is limited to the local checkout: ${realPath}`;
761
- // Pi-effective path check (monotonic: block if either resolution is outside).
762
749
  // For the read tool, also check Pi's resolveReadPathAsync fallback variants so
763
750
  // a missing file whose variant resolves outside is not silently allowed.
764
751
  try {
@@ -2841,7 +2828,6 @@ export default function triageCommentsExtension(pi: ExtensionAPI) {
2841
2828
  id: event.toolCallId,
2842
2829
  name: event.toolName,
2843
2830
  args: event.args,
2844
- startedAt: Date.now(),
2845
2831
  });
2846
2832
  if (details.toolCalls.length > MAX_TOOL_CALLS_TO_KEEP) {
2847
2833
  details.toolCalls.splice(0, details.toolCalls.length - MAX_TOOL_CALLS_TO_KEEP);
@@ -2851,7 +2837,6 @@ export default function triageCommentsExtension(pi: ExtensionAPI) {
2851
2837
  case "tool_execution_end": {
2852
2838
  const call = details.toolCalls.find((item) => item.id === event.toolCallId);
2853
2839
  if (call) {
2854
- call.endedAt = Date.now();
2855
2840
  call.isError = event.isError;
2856
2841
  }
2857
2842
  emit();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-triage-comments",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "A pi extension that adds /triage-comments and a read-only triage_comments subagent tool for review-comment triage.",
5
5
  "keywords": [
6
6
  "pi-package",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-extensions",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "description": "A collection of pi extensions and skills for annotation UIs, context management, workflow audits, contrarian review, review-comment triage, notifications, brrr push alerts, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, model/provider helpers, and Xiaohei-style article illustrations.",
5
5
  "keywords": [
6
6
  "pi-package",