@diegopetrucci/pi-extensions 0.1.57 → 0.1.59

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 (102) hide show
  1. package/.pi-fleet-tested-version +1 -1
  2. package/extensions/agent-workflow-audit/.pi-fleet-tested-version +1 -1
  3. package/extensions/agent-workflow-audit/index.ts +8 -1
  4. package/extensions/agent-workflow-audit/package.json +5 -1
  5. package/extensions/annotate-git-diff/.pi-fleet-tested-version +1 -1
  6. package/extensions/annotate-git-diff/package.json +5 -2
  7. package/extensions/annotate-last-message/.pi-fleet-tested-version +1 -1
  8. package/extensions/annotate-last-message/package.json +5 -2
  9. package/extensions/brrr/.pi-fleet-tested-version +1 -1
  10. package/extensions/brrr/README.md +2 -2
  11. package/extensions/brrr/package.json +5 -1
  12. package/extensions/claude-fast/.pi-fleet-tested-version +1 -1
  13. package/extensions/claude-fast/package.json +5 -1
  14. package/extensions/code-reviewer/.pi-fleet-tested-version +1 -1
  15. package/extensions/code-reviewer/index.ts +129 -39
  16. package/extensions/code-reviewer/package.json +5 -1
  17. package/extensions/confirm-destructive/.pi-fleet-tested-version +1 -1
  18. package/extensions/confirm-destructive/README.md +1 -1
  19. package/extensions/confirm-destructive/index.ts +20 -5
  20. package/extensions/confirm-destructive/package.json +5 -1
  21. package/extensions/context-cap/.pi-fleet-tested-version +1 -1
  22. package/extensions/context-cap/package.json +5 -1
  23. package/extensions/context-inspector/.pi-fleet-tested-version +1 -1
  24. package/extensions/context-inspector/package.json +5 -1
  25. package/extensions/contrarian/.pi-fleet-tested-version +1 -1
  26. package/extensions/contrarian/README.md +1 -1
  27. package/extensions/contrarian/index.ts +199 -40
  28. package/extensions/contrarian/package.json +5 -1
  29. package/extensions/dirty-repo-guard/.pi-fleet-tested-version +1 -1
  30. package/extensions/dirty-repo-guard/README.md +1 -1
  31. package/extensions/dirty-repo-guard/package.json +5 -1
  32. package/extensions/dynamic-context-pruning/.pi-fleet-tested-version +1 -1
  33. package/extensions/dynamic-context-pruning/package.json +4 -1
  34. package/extensions/git-footer/.pi-fleet-tested-version +1 -1
  35. package/extensions/git-footer/package.json +5 -1
  36. package/extensions/gnosis/.pi-fleet-tested-version +1 -1
  37. package/extensions/gnosis/package.json +5 -1
  38. package/extensions/illustrations-to-explain-things/.pi-fleet-tested-version +1 -1
  39. package/extensions/illustrations-to-explain-things/package.json +4 -1
  40. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/01-two-breakpoints.webp +0 -0
  41. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/02-minimum-loop.webp +0 -0
  42. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/03-sort-by-purpose.webp +0 -0
  43. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/04-one-fish-many-uses.webp +0 -0
  44. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/05-handoff-path.webp +0 -0
  45. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/06-three-sources.webp +0 -0
  46. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/07-three-content-jobs.webp +0 -0
  47. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/08-handoff-copy-toolbox.webp +0 -0
  48. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/09-common-pits-no-title.webp +0 -0
  49. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/10-information-well.webp +0 -0
  50. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/11-idea-press.webp +0 -0
  51. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/12-content-fermentation.webp +0 -0
  52. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/13-system-bearing.webp +0 -0
  53. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/14-trust-bridge.webp +0 -0
  54. package/extensions/inline-bash/.pi-fleet-tested-version +1 -1
  55. package/extensions/inline-bash/README.md +1 -1
  56. package/extensions/inline-bash/package.json +5 -1
  57. package/extensions/librarian/.pi-fleet-tested-version +1 -1
  58. package/extensions/librarian/index.ts +393 -107
  59. package/extensions/librarian/package.json +5 -1
  60. package/extensions/minimal-footer/.pi-fleet-tested-version +1 -1
  61. package/extensions/minimal-footer/index.ts +2 -5
  62. package/extensions/minimal-footer/openai-usage.ts +58 -11
  63. package/extensions/minimal-footer/package.json +5 -1
  64. package/extensions/notify/.pi-fleet-tested-version +1 -1
  65. package/extensions/notify/README.md +3 -3
  66. package/extensions/notify/package.json +5 -1
  67. package/extensions/openai-fast/.pi-fleet-tested-version +1 -1
  68. package/extensions/openai-fast/package.json +5 -1
  69. package/extensions/oracle/.pi-fleet-tested-version +1 -1
  70. package/extensions/oracle/README.md +1 -1
  71. package/extensions/oracle/index.ts +200 -41
  72. package/extensions/oracle/package.json +5 -1
  73. package/extensions/permission-gate/.pi-fleet-tested-version +1 -1
  74. package/extensions/permission-gate/README.md +1 -1
  75. package/extensions/permission-gate/index.ts +453 -2
  76. package/extensions/permission-gate/package.json +5 -1
  77. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  78. package/extensions/quiet-tools/package.json +5 -1
  79. package/extensions/review/.pi-fleet-tested-version +1 -1
  80. package/extensions/review/index.ts +53 -19
  81. package/extensions/review/package.json +5 -1
  82. package/extensions/todo/.pi-fleet-tested-version +1 -1
  83. package/extensions/todo/README.md +1 -1
  84. package/extensions/todo/package.json +5 -1
  85. package/extensions/triage-comments/.pi-fleet-tested-version +1 -1
  86. package/extensions/triage-comments/index.ts +115 -34
  87. package/extensions/triage-comments/package.json +5 -1
  88. package/package.json +10 -6
  89. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/01-two-breakpoints.png +0 -0
  90. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/02-minimum-loop.png +0 -0
  91. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/03-sort-by-purpose.png +0 -0
  92. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/04-one-fish-many-uses.png +0 -0
  93. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/05-handoff-path.png +0 -0
  94. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/06-three-sources.png +0 -0
  95. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/07-three-content-jobs.png +0 -0
  96. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/08-handoff-copy-toolbox.png +0 -0
  97. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/09-common-pits-no-title.png +0 -0
  98. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/10-information-well.png +0 -0
  99. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/11-idea-press.png +0 -0
  100. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/12-content-fermentation.png +0 -0
  101. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/13-system-bearing.png +0 -0
  102. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/14-trust-bridge.png +0 -0
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -69,6 +69,13 @@ type ParsedArgs = {
69
69
  error?: string;
70
70
  };
71
71
 
72
+ type CreateAgentSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
73
+
74
+ function getModelRuntimeOption(ctx: { modelRegistry?: unknown }): Pick<CreateAgentSessionOptions, "modelRuntime"> {
75
+ const modelRuntime = (ctx.modelRegistry as { runtime?: CreateAgentSessionOptions["modelRuntime"] } | undefined)?.runtime;
76
+ return modelRuntime ? { modelRuntime } : {};
77
+ }
78
+
72
79
  function parseArgs(args: string): ParsedArgs {
73
80
  const parts = args.trim().split(/\s+/).filter(Boolean);
74
81
  const focusParts: string[] = [];
@@ -1013,7 +1020,7 @@ async function runAudit(
1013
1020
  const tools = options.planOnly ? ["read", "grep", "find", "ls"] : ["read", "grep", "find", "ls", "bash"];
1014
1021
  const created = await createAgentSession({
1015
1022
  cwd,
1016
- modelRegistry: ctx.modelRegistry,
1023
+ ...getModelRuntimeOption(ctx),
1017
1024
  resourceLoader,
1018
1025
  settingsManager: isolatedSettingsManager,
1019
1026
  sessionManager: SessionManager.inMemory(cwd),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-agent-workflow-audit",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A pi extension that runs an isolated repo workflow audit and returns only the final report to the main session.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -32,5 +32,9 @@
32
32
  "peerDependencies": {
33
33
  "@earendil-works/pi-coding-agent": "*",
34
34
  "@earendil-works/pi-tui": "*"
35
+ },
36
+ "type": "module",
37
+ "engines": {
38
+ "node": ">=22.19.0"
35
39
  }
36
40
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-annotate-git-diff",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A standalone pi extension that adds /annotate-git-diff, a native Glimpse UI for annotating git diffs and sending feedback to the editor.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -46,5 +46,8 @@
46
46
  "glimpseui": "^0.8.1",
47
47
  "monaco-editor": "0.52.2"
48
48
  },
49
- "type": "module"
49
+ "type": "module",
50
+ "engines": {
51
+ "node": ">=22.19.0"
52
+ }
50
53
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-annotate-last-message",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "A standalone pi extension that adds /annotate-last-message, a native Glimpse UI for annotating the latest assistant reply.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -41,5 +41,8 @@
41
41
  "dependencies": {
42
42
  "glimpseui": "^0.8.1"
43
43
  },
44
- "type": "module"
44
+ "type": "module",
45
+ "engines": {
46
+ "node": ">=22.19.0"
47
+ }
45
48
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  # brrr
2
2
 
3
- A pi extension that sends [brrr](https://brrr.now) push notifications when pi finishes an agent turn and is ready for input.
3
+ A pi extension that sends [brrr](https://brrr.now) push notifications when pi has fully settled and is ready for input.
4
4
 
5
5
  ## Install
6
6
 
@@ -81,6 +81,6 @@ Example:
81
81
 
82
82
  ## Notes
83
83
 
84
- - Hooks the `agent_end` event.
84
+ - Hooks the `agent_settled` event so automatic retries, compaction retries, and queued follow-ups do not trigger intermediate notifications.
85
85
  - The extension sends directly to the brrr webhook; it does not require the `brrr` CLI at runtime.
86
86
  - By default, notifications are skipped unless the Mac has been idle for at least 20 seconds.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-brrr",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "A pi extension that sends brrr push notifications when pi is ready for input.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -31,5 +31,9 @@
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@earendil-works/pi-coding-agent": "*"
34
+ },
35
+ "type": "module",
36
+ "engines": {
37
+ "node": ">=22.19.0"
34
38
  }
35
39
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-claude-fast",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "A pi extension that enables Anthropic Claude Fast mode for supported Claude Opus models by injecting speed=fast.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -31,5 +31,9 @@
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@earendil-works/pi-coding-agent": "*"
34
+ },
35
+ "type": "module",
36
+ "engines": {
37
+ "node": ">=22.19.0"
34
38
  }
35
39
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -11,6 +11,8 @@ import {
11
11
  } from "@earendil-works/pi-coding-agent";
12
12
  import { Type } from "typebox";
13
13
 
14
+ import type { Usage } from "@earendil-works/pi-ai";
15
+
14
16
  const MAX_TOOL_CALLS_TO_KEEP = 80;
15
17
  const MAX_TURNS = 8;
16
18
  const MAX_RUN_MS = 8 * 60 * 1000;
@@ -220,7 +222,13 @@ type ModelRegistryContext = {
220
222
  modelRegistry: { getAvailable(): PiModel[] | Promise<PiModel[]> };
221
223
  };
222
224
 
223
- type CreateAgentSessionModel = NonNullable<Parameters<typeof createAgentSession>[0]>["model"];
225
+ type CreateAgentSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
226
+ type CreateAgentSessionModel = CreateAgentSessionOptions["model"];
227
+
228
+ function getModelRuntimeOption(ctx: { modelRegistry?: unknown }): Pick<CreateAgentSessionOptions, "modelRuntime"> {
229
+ const modelRuntime = (ctx.modelRegistry as { runtime?: CreateAgentSessionOptions["modelRuntime"] } | undefined)?.runtime;
230
+ return modelRuntime ? { modelRuntime } : {};
231
+ }
224
232
 
225
233
  const MODEL_AVAILABILITY_ERROR_PATTERN =
226
234
  /\b(?:404|403|not[_ ]?found(?:[_ ]?error)?|model[_ ]?not[_ ]?found(?:[_ ]?error)?|no such model|unknown model|does not exist|is not available|not available|model[_ ]?not[_ ]?available|unsupported model|invalid model|forbidden|access[ _-]?denied|permission[ _-]?denied|not[ _-]?entitled|do(?:es)? not have access)\b/i;
@@ -457,26 +465,72 @@ function applyModelAttemptDetails(details: ReviewDetails, model: PiModel, thinki
457
465
  details.thinkingLevelNote = thinking.note;
458
466
  }
459
467
 
460
- function extractLastAssistantText(messages: unknown[]): string {
468
+ function createEmptyUsage(): Usage {
469
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
470
+ }
471
+
472
+ function addUsage(total: Usage, usage: Usage): void {
473
+ total.input += usage.input || 0;
474
+ total.output += usage.output || 0;
475
+ total.cacheRead += usage.cacheRead || 0;
476
+ total.cacheWrite += usage.cacheWrite || 0;
477
+ if (usage.cacheWrite1h !== undefined) total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;
478
+ if (usage.reasoning !== undefined) total.reasoning = (total.reasoning ?? 0) + usage.reasoning;
479
+ total.totalTokens += usage.totalTokens || 0;
480
+ total.cost.input += usage.cost?.input || 0;
481
+ total.cost.output += usage.cost?.output || 0;
482
+ total.cost.cacheRead += usage.cost?.cacheRead || 0;
483
+ total.cost.cacheWrite += usage.cost?.cacheWrite || 0;
484
+ total.cost.total += usage.cost?.total || 0;
485
+ }
486
+
487
+ type AssistantLikeMessage = { role?: string; content?: unknown; stopReason?: unknown; errorMessage?: unknown; usage?: unknown };
488
+
489
+ function addAssistantMessageUsage(total: Usage, message: unknown): void {
490
+ const assistant = message as AssistantLikeMessage;
491
+ if (assistant?.role !== "assistant" || !assistant.usage || typeof assistant.usage !== "object") return;
492
+ addUsage(total, assistant.usage as Usage);
493
+ }
494
+
495
+ function aggregateAssistantUsage(messages: unknown[]): Usage {
496
+ const total = createEmptyUsage();
497
+ for (const message of messages) addAssistantMessageUsage(total, message);
498
+ return total;
499
+ }
500
+
501
+ function addSessionEventUsage(total: Usage, event: unknown): void {
502
+ const sessionEvent = event as { type?: string; message?: unknown; result?: { usage?: Usage } };
503
+ if (sessionEvent?.type === "message_end") return void addAssistantMessageUsage(total, sessionEvent.message);
504
+ if (sessionEvent?.type === "compaction_end" && sessionEvent.result?.usage) addUsage(total, sessionEvent.result.usage);
505
+ }
506
+
507
+ function inspectFinalAssistant(messages: unknown[]):
508
+ | { ok: true; answer: string; stopReason?: string }
509
+ | { ok: false; reason: string; stopReason?: string; errorMessage?: string } {
461
510
  for (let i = messages.length - 1; i >= 0; i -= 1) {
462
- const message = messages[i] as { role?: string; content?: unknown };
463
- if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
464
- const parts: string[] = [];
465
- for (const part of message.content) {
466
- if (part && typeof part === "object" && (part as { type?: string }).type === "text") {
467
- const text = (part as { text?: unknown }).text;
468
- if (typeof text === "string") parts.push(text);
469
- }
470
- }
471
- if (parts.length > 0) return parts.join("").trim();
511
+ const message = messages[i] as AssistantLikeMessage;
512
+ if (message?.role !== "assistant") continue;
513
+ const stopReason = typeof message.stopReason === "string" ? message.stopReason.trim() : "";
514
+ const errorMessage = typeof message.errorMessage === "string" ? message.errorMessage.trim() : "";
515
+ const answer = Array.isArray(message.content)
516
+ ? message.content
517
+ .map((part) => (part && typeof part === "object" && (part as { type?: string }).type === "text" && typeof (part as { text?: unknown }).text === "string"
518
+ ? (part as { text: string }).text
519
+ : ""))
520
+ .join("")
521
+ .trim()
522
+ : "";
523
+ if (stopReason === "error") return { ok: false, reason: `code_reviewer subagent error: ${errorMessage || "provider/model error"}`, stopReason, errorMessage };
524
+ if (stopReason === "aborted") return { ok: false, reason: "code_reviewer subagent aborted before producing a usable answer", stopReason };
525
+ if (answer) return { ok: true, answer, stopReason: stopReason || undefined };
526
+ return { ok: false, reason: stopReason ? `code_reviewer subagent produced no final assistant text (stopReason: ${stopReason})` : "code_reviewer subagent produced no final assistant text", stopReason: stopReason || undefined, errorMessage: errorMessage || undefined };
472
527
  }
473
- return "";
528
+ return { ok: false, reason: "code_reviewer subagent produced no assistant message" };
474
529
  }
475
530
 
476
- function isAbortLikeError(error: unknown): boolean {
477
- if (error && typeof error === "object" && (error as { name?: unknown }).name === "AbortError") return true;
478
- const message = error instanceof Error ? error.message : String(error);
479
- return /aborted|cancelled|canceled/i.test(message);
531
+ function classifyRunFailure(error: unknown, aborted: boolean): { status: "aborted" | "error"; message: string; error?: string } {
532
+ const message = aborted ? "Aborted" : error instanceof Error ? error.message : String(error);
533
+ return { status: aborted ? "aborted" : "error", message, error: aborted ? undefined : message };
480
534
  }
481
535
 
482
536
  function isInside(parent: string, child: string): boolean {
@@ -1076,12 +1130,25 @@ export const __test__ = {
1076
1130
  buildSafeGitCommand,
1077
1131
  formatToolCall,
1078
1132
  getBlockedBashReason,
1133
+ isModelAvailabilityError,
1079
1134
  normalizeThinkingLevel,
1080
1135
  resolveThinkingLevel,
1081
1136
  selectCodeReviewerModel,
1137
+ aggregateAssistantUsage,
1138
+ addAssistantMessageUsage,
1139
+ addSessionEventUsage,
1140
+ inspectFinalAssistant,
1141
+ classifyRunFailure,
1082
1142
  };
1083
1143
 
1084
1144
  export default function codeReviewerExtension(pi: ExtensionAPI) {
1145
+ pi.on("tool_result", async (event) => {
1146
+ if (event.toolName !== "code_reviewer") return undefined;
1147
+ const details = event.details as ReviewDetails | undefined;
1148
+ if (details?.status !== "error") return undefined;
1149
+ return { isError: true };
1150
+ });
1151
+
1085
1152
  pi.registerTool({
1086
1153
  name: "code_reviewer",
1087
1154
  label: "Code reviewer",
@@ -1119,24 +1186,29 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1119
1186
  let unsubscribe: (() => void) | undefined;
1120
1187
  let runTimeout: NodeJS.Timeout | undefined;
1121
1188
  let abortListenerAdded = false;
1122
- let aborted = Boolean(signal?.aborted);
1189
+ let callerAborted = Boolean(signal?.aborted);
1190
+ const usage = createEmptyUsage();
1123
1191
 
1124
1192
  const emit = () => {
1125
1193
  onUpdate?.({ content: [{ type: "text", text: lastContent }], details });
1126
1194
  };
1127
1195
 
1128
- const abort = () => {
1129
- aborted = true;
1196
+ const abortForCleanup = () => {
1197
+ void session?.abort();
1198
+ };
1199
+
1200
+ const abortFromCaller = () => {
1201
+ callerAborted = true;
1130
1202
  details.status = "aborted";
1131
1203
  details.endedAt = Date.now();
1132
1204
  lastContent = "Aborted";
1133
1205
  emit();
1134
- void session?.abort();
1206
+ abortForCleanup();
1135
1207
  };
1136
1208
 
1137
- if (signal?.aborted) abort();
1209
+ if (signal?.aborted) abortFromCaller();
1138
1210
  if (signal && !signal.aborted) {
1139
- signal.addEventListener("abort", abort);
1211
+ signal.addEventListener("abort", abortFromCaller);
1140
1212
  abortListenerAdded = true;
1141
1213
  }
1142
1214
 
@@ -1181,7 +1253,7 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1181
1253
  try {
1182
1254
  const created = await createAgentSession({
1183
1255
  cwd,
1184
- modelRegistry: ctx.modelRegistry,
1256
+ ...getModelRuntimeOption(ctx),
1185
1257
  resourceLoader,
1186
1258
  settingsManager: isolatedSettingsManager,
1187
1259
  sessionManager: SessionManager.inMemory(cwd),
@@ -1192,6 +1264,7 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1192
1264
  attemptSession = created.session;
1193
1265
  session = attemptSession;
1194
1266
  attemptUnsubscribe = attemptSession.subscribe((event) => {
1267
+ addSessionEventUsage(usage, event);
1195
1268
  switch (event.type) {
1196
1269
  case "turn_end":
1197
1270
  details.turns += 1;
@@ -1228,20 +1301,29 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1228
1301
  });
1229
1302
  unsubscribe = attemptUnsubscribe;
1230
1303
 
1231
- if (!aborted) {
1304
+ if (!callerAborted) {
1232
1305
  const promptPromise = attemptSession.prompt(buildUserPrompt(input, cwd), { expandPromptTemplates: false });
1233
1306
  const timeoutPromise = new Promise<never>((_resolve, reject) => {
1234
1307
  runTimeout = setTimeout(() => {
1235
- abort();
1308
+ abortForCleanup();
1236
1309
  reject(new Error(`code_reviewer timed out after ${Math.round(MAX_RUN_MS / 1000)} seconds.`));
1237
1310
  }, MAX_RUN_MS);
1238
1311
  });
1239
1312
  await Promise.race([promptPromise, timeoutPromise]);
1240
1313
  }
1241
1314
 
1242
- answer = extractLastAssistantText(attemptSession.state.messages);
1243
- succeeded = true;
1244
- break;
1315
+ const outcome = inspectFinalAssistant(attemptSession.state.messages);
1316
+ if (outcome.ok) {
1317
+ answer = outcome.answer;
1318
+ succeeded = true;
1319
+ break;
1320
+ }
1321
+ lastAttemptError = outcome.reason;
1322
+ const canFallBack =
1323
+ index < modelSelection.ordered.length - 1 &&
1324
+ outcome.stopReason === "error" &&
1325
+ isModelAvailabilityError(outcome.errorMessage || outcome.reason);
1326
+ if (!canFallBack) break;
1245
1327
  } catch (error) {
1246
1328
  const message = error instanceof Error ? error.message : String(error);
1247
1329
  lastAttemptError = message;
@@ -1260,26 +1342,34 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1260
1342
  }
1261
1343
  }
1262
1344
  }
1263
- if (!answer && !session && lastAttemptError) throw new Error(lastAttemptError);
1264
- lastContent = answer || (aborted ? "Aborted" : "(no output)");
1265
- details.status = aborted ? "aborted" : "done";
1345
+ if (!answer) {
1346
+ if (callerAborted) {
1347
+ lastContent = "Aborted";
1348
+ details.status = "aborted";
1349
+ } else {
1350
+ throw new Error(lastAttemptError || "the isolated code_reviewer session finished without a usable answer");
1351
+ }
1352
+ } else {
1353
+ lastContent = answer;
1354
+ details.status = "done";
1355
+ }
1266
1356
  details.endedAt = Date.now();
1267
1357
  const report = appendRunDetails(lastContent, details);
1268
1358
  lastContent = report;
1269
1359
  emit();
1270
- return { content: [{ type: "text", text: report }], details };
1360
+ return { content: [{ type: "text", text: report }], details, usage };
1271
1361
  } catch (error) {
1272
- const wasAbort = aborted || isAbortLikeError(error);
1273
- const message = wasAbort ? "Aborted" : error instanceof Error ? error.message : String(error);
1274
- details.status = wasAbort ? "aborted" : "error";
1275
- details.error = wasAbort ? undefined : message;
1362
+ const failure = classifyRunFailure(error, callerAborted);
1363
+ const { message } = failure;
1364
+ details.status = failure.status;
1365
+ details.error = failure.error;
1276
1366
  details.endedAt = Date.now();
1277
1367
  lastContent = appendRunDetails(`## Verdict\nReview failed.\n\n## Findings\n- major — Review execution failed. Evidence: ${message}. Why it matters: the requested read-only review did not complete.\n\n## Validation\n- none\n\n## Scope check\nReview could not be completed.\n\n## Run details\n- failure: ${message}`, details);
1278
1368
  emit();
1279
- return { content: [{ type: "text", text: lastContent }], details };
1369
+ return { content: [{ type: "text", text: lastContent }], details, usage };
1280
1370
  } finally {
1281
1371
  if (runTimeout) clearTimeout(runTimeout);
1282
- if (signal && abortListenerAdded) signal.removeEventListener("abort", abort);
1372
+ if (signal && abortListenerAdded) signal.removeEventListener("abort", abortFromCaller);
1283
1373
  unsubscribe?.();
1284
1374
  session?.dispose();
1285
1375
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-code-reviewer",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A standalone pi extension that adds a read-only code_reviewer subagent tool for isolated code reviews.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -31,5 +31,9 @@
31
31
  "peerDependencies": {
32
32
  "@earendil-works/pi-coding-agent": "*",
33
33
  "typebox": "*"
34
+ },
35
+ "type": "module",
36
+ "engines": {
37
+ "node": ">=22.19.0"
34
38
  }
35
39
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -2,7 +2,7 @@
2
2
 
3
3
  A small pi extension that asks for confirmation before destructive session actions.
4
4
 
5
- This is adapted from the original `confirm-destructive.ts` example in [`earendil-works/pi-mono`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/examples/extensions/confirm-destructive.ts) and kept basically the same.
5
+ This is adapted from the original `confirm-destructive.ts` example in [`earendil-works/pi`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/confirm-destructive.ts) and kept basically the same.
6
6
 
7
7
  ## What it checks
8
8
 
@@ -5,7 +5,24 @@
5
5
  * Demonstrates how to cancel session events using the before_* events.
6
6
  */
7
7
 
8
- import type { ExtensionAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@earendil-works/pi-coding-agent";
8
+ import type { ExtensionAPI, SessionBeforeSwitchEvent, SessionEntry } from "@earendil-works/pi-coding-agent";
9
+
10
+ function hasUnsavedUserWork(entries: SessionEntry[]): boolean {
11
+ let sawAssistant = false;
12
+
13
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
14
+ const entry = entries[index];
15
+ if (entry.type !== "message") continue;
16
+ if (entry.message.role === "assistant") {
17
+ sawAssistant = true;
18
+ break;
19
+ }
20
+ if (entry.message.role === "user") return true;
21
+ }
22
+
23
+ if (sawAssistant) return false;
24
+ return entries.some((entry) => entry.type === "message" && entry.message.role === "user");
25
+ }
9
26
 
10
27
  export default function (pi: ExtensionAPI) {
11
28
  pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => {
@@ -24,11 +41,9 @@ export default function (pi: ExtensionAPI) {
24
41
  return;
25
42
  }
26
43
 
27
- // reason === "resume" - check if there are unsaved changes (messages since last assistant response)
44
+ // reason === "resume" - check if there are unsaved changes (user messages newer than the last assistant response)
28
45
  const entries = ctx.sessionManager.getEntries();
29
- const hasUnsavedWork = entries.some(
30
- (e): e is SessionMessageEntry => e.type === "message" && e.message.role === "user",
31
- );
46
+ const hasUnsavedWork = hasUnsavedUserWork(entries);
32
47
 
33
48
  if (hasUnsavedWork) {
34
49
  const confirmed = await ctx.ui.confirm(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-confirm-destructive",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "A pi extension that confirms destructive session actions.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -29,5 +29,9 @@
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@earendil-works/pi-coding-agent": "*"
32
+ },
33
+ "type": "module",
34
+ "engines": {
35
+ "node": ">=22.19.0"
32
36
  }
33
37
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-context-cap",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A pi extension that caps effective model context windows at 200k tokens for earlier auto-compaction.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -30,5 +30,9 @@
30
30
  "peerDependencies": {
31
31
  "@earendil-works/pi-ai": "*",
32
32
  "@earendil-works/pi-coding-agent": "*"
33
+ },
34
+ "type": "module",
35
+ "engines": {
36
+ "node": ">=22.19.0"
33
37
  }
34
38
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-context-inspector",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "A pi extension that opens a local HTML dashboard explaining where the current session context is going.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -30,5 +30,9 @@
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@earendil-works/pi-coding-agent": "*"
33
+ },
34
+ "type": "module",
35
+ "engines": {
36
+ "node": ">=22.19.0"
33
37
  }
34
38
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  # contrarian
2
2
 
3
- A read-only contrarian subagent for [pi](https://github.com/earendil-works/pi-mono).
3
+ A read-only contrarian subagent for [pi](https://github.com/earendil-works/pi).
4
4
 
5
5
  It adds a `contrarian` tool that spins up a separate pi subprocess to stress-test plans, designs, assumptions, bug hypotheses, review conclusions, and product directions by steelmanning the strongest credible opposing case.
6
6