takomi 2.1.29 → 2.1.31

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 (38) hide show
  1. package/.pi/agents/coder.md +6 -0
  2. package/.pi/agents/orchestrator.md +16 -0
  3. package/.pi/extensions/notify-sound/index.ts +262 -262
  4. package/.pi/extensions/oauth-router/README.md +4 -2
  5. package/.pi/extensions/oauth-router/commands.ts +37 -9
  6. package/.pi/extensions/oauth-router/config.ts +7 -1
  7. package/.pi/extensions/oauth-router/index.ts +1 -1
  8. package/.pi/extensions/oauth-router/oauth-flow.ts +71 -22
  9. package/.pi/extensions/oauth-router/provider.ts +112 -12
  10. package/.pi/extensions/oauth-router/state.ts +12 -1
  11. package/.pi/extensions/oauth-router/types.ts +15 -2
  12. package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +1 -1
  13. package/.pi/extensions/takomi-context-manager/index.ts +1 -1
  14. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +1 -1
  15. package/.pi/extensions/takomi-context-manager/policy-tools.ts +1 -1
  16. package/.pi/extensions/takomi-context-manager/prerequisite-gates.ts +1 -1
  17. package/.pi/extensions/takomi-context-manager/skill-tools.ts +1 -1
  18. package/.pi/extensions/takomi-runtime/commands.ts +1 -1
  19. package/.pi/extensions/takomi-runtime/context-panel.ts +708 -605
  20. package/.pi/extensions/takomi-runtime/index.ts +4 -2
  21. package/.pi/extensions/takomi-runtime/shared.ts +1 -1
  22. package/.pi/extensions/takomi-runtime/subagent-controller.ts +1 -1
  23. package/.pi/extensions/takomi-runtime/subagent-render.ts +1 -1
  24. package/.pi/extensions/takomi-runtime/subagent-types.ts +11 -11
  25. package/.pi/extensions/takomi-runtime/takomi-stats.js +679 -679
  26. package/.pi/extensions/takomi-runtime/ui.ts +2 -2
  27. package/.pi/extensions/takomi-subagents/agents.ts +1 -1
  28. package/.pi/extensions/takomi-subagents/index.ts +1 -1
  29. package/.pi/extensions/takomi-subagents/native-render.ts +3 -3
  30. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +2 -2
  31. package/.pi/extensions/takomi-subagents/tool-runner.ts +1 -1
  32. package/.pi/prompts/build-prompt.md +15 -0
  33. package/.pi/prompts/genesis-prompt.md +8 -0
  34. package/.pi/prompts/takomi-prompt.md +16 -0
  35. package/package.json +5 -9
  36. package/src/doctor.js +1 -1
  37. package/src/pi-harness.js +0 -3
  38. package/src/takomi-stats.js +749 -679
@@ -42,6 +42,12 @@ If scope is unclear, ask a focused question instead of guessing.
42
42
  - handle errors and edge cases deliberately
43
43
  - avoid broad refactors unless requested or required
44
44
 
45
+ ## Tool-Use Safety
46
+ - Keep `bash` commands short; use them for filesystem operations, running scripts, inspections, and verification.
47
+ - Do not embed large generated files or long scripts directly in `bash` heredocs.
48
+ - Use `write` for large files, or `write` a small generator script to disk and then run it.
49
+ - If a command fails with `ENAMETOOLONG`, stop using the oversized inline command and switch to file-based writes or a written script.
50
+
45
51
  ## Phase 4: Verification
46
52
  After edits, run the strongest practical checks for the repo.
47
53
  For TypeScript/TSX work, prefer `npx tsc --noEmit` or the project equivalent.
@@ -90,6 +90,22 @@ Task packets should be self-contained enough for a subagent to execute without g
90
90
 
91
91
  Keep human-readable markdown meaningful; keep JSON as tracking/continuity metadata.
92
92
 
93
+ ### Tool-Use Safety for Authored Plans
94
+ - Do not pack large markdown plans, task packets, or generator scripts into one oversized `bash` command.
95
+ - Prefer direct markdown authorship through the appropriate file-writing path, then register the same content with `takomi_board`.
96
+ - If repeated files are needed, write a small generator script to disk first and run it with a short shell command.
97
+ - If `ENAMETOOLONG` occurs, switch immediately to file-based writes or a written generator script instead of retrying the inline command.
98
+
99
+ ### Stage Expansion Quality Gate
100
+ Before calling `takomi_board expand_stage`:
101
+ - Write or provide full `taskMarkdown` for every expanded task.
102
+ - Ensure each task has real Scope, Definition of Done, Expected Artifacts, Dependencies, and Verification/Review checkpoint.
103
+ - Include exact prime-agent context paths and relevant FR/mockup/design docs.
104
+ - For Build, decompose by implementation slice and FR coverage, not just broad labels like “school workflows.”
105
+ - If a detailed subagent prompt is needed, the task packet should contain the same level of detail before launch.
106
+ - Do not leave generated task files with `Scope: None specified`, `Definition Of Done: None specified`, or `Expected Artifacts: None specified`.
107
+ - If `takomi_board` produced generic task packets, immediately repair them with detailed markdown before dispatching more work.
108
+
93
109
  ## Phase 4: Delegation
94
110
  When delegating:
95
111
  - send self-contained task instructions
@@ -1,262 +1,262 @@
1
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
4
- import { homedir, platform } from "node:os";
5
- import { spawn } from "node:child_process";
6
-
7
- type NotifySoundConfig = {
8
- enabled: boolean;
9
- };
10
-
11
- type NotifyMethod = "auto" | "wav";
12
-
13
- const GLOBAL_NOTIFY_DIR = join(homedir(), ".pi", "agent", "notify-sound");
14
- const CONFIG_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.json");
15
- const WAV_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.wav");
16
- const PS1_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.ps1");
17
- const VBS_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.vbs");
18
- const STALE_AGENT_START_MS = 24 * 60 * 60 * 1000;
19
-
20
- let config: NotifySoundConfig = { enabled: true };
21
- let lastAgentStartedAt = 0;
22
-
23
- export default async function notifySoundExtension(pi: ExtensionAPI) {
24
- await loadConfig();
25
- await ensureTuneWav();
26
-
27
- pi.on("session_start", async (_event, ctx) => {
28
- updateStatus(ctx);
29
- });
30
-
31
- pi.on("agent_start", async () => {
32
- lastAgentStartedAt = Date.now();
33
- });
34
-
35
- pi.on("agent_end", async (_event, ctx) => {
36
- updateStatus(ctx);
37
-
38
- if (!config.enabled) return;
39
- if (!lastAgentStartedAt) return;
40
- if (Date.now() - lastAgentStartedAt > STALE_AGENT_START_MS) return;
41
-
42
- playCompletionTune();
43
- });
44
-
45
- const command = {
46
- description: "Toggle or test the agent completion tune notification",
47
- getArgumentCompletions: (argumentPrefix: string) => {
48
- const token = argumentPrefix.trim().toLowerCase();
49
- return [
50
- { value: "status", label: "status", description: "Show whether the completion tune is on" },
51
- { value: "test", label: "test", description: "Play the completion tune" },
52
- { value: "test wav", label: "test wav", description: "Play the generated WAV completion tune" },
53
- { value: "on", label: "on", description: "Enable the completion tune" },
54
- { value: "off", label: "off", description: "Disable the completion tune" },
55
- { value: "toggle", label: "toggle", description: "Toggle the completion tune" },
56
- ].filter((completion) => !token || completion.value.startsWith(token));
57
- },
58
- handler: async (args: string, ctx: ExtensionCommandContext) => {
59
- const action = args.trim().toLowerCase() || "toggle";
60
-
61
- if (action === "test") {
62
- playCompletionTune("wav");
63
- ctx.ui.notify("Played completion tune using WAV mode. This is the default reliable Windows path.", "info");
64
- return;
65
- }
66
-
67
- if (action === "test wav") {
68
- playCompletionTune("wav");
69
- ctx.ui.notify("Played completion tune using WAV mode.", "info");
70
- return;
71
- }
72
-
73
- if (action.startsWith("test ")) {
74
- ctx.ui.notify("Usage: /notify test", "warning");
75
- return;
76
- }
77
-
78
- if (action === "status") {
79
- updateStatus(ctx);
80
- ctx.ui.notify(`Completion tune is ${config.enabled ? "ON" : "OFF"}.`, "info");
81
- return;
82
- }
83
-
84
- if (action === "on") {
85
- await setEnabled(true, ctx);
86
- return;
87
- }
88
-
89
- if (action === "off") {
90
- await setEnabled(false, ctx);
91
- return;
92
- }
93
-
94
- if (action === "toggle") {
95
- await setEnabled(!config.enabled, ctx);
96
- return;
97
- }
98
-
99
- ctx.ui.notify("Usage: /notify [test|status|on|off|toggle]", "warning");
100
- },
101
- };
102
-
103
- pi.registerCommand("notify-sound", command);
104
- pi.registerCommand("notify", {
105
- ...command,
106
- description: "Alias for /notify-sound",
107
- });
108
- }
109
-
110
- async function loadConfig(): Promise<void> {
111
- try {
112
- const raw = await readFile(CONFIG_PATH, "utf8");
113
- const parsed = JSON.parse(raw) as Partial<NotifySoundConfig>;
114
- config = { enabled: parsed.enabled !== false };
115
- } catch {
116
- config = { enabled: true };
117
- await saveConfig();
118
- }
119
- }
120
-
121
- async function saveConfig(): Promise<void> {
122
- await mkdir(dirname(CONFIG_PATH), { recursive: true });
123
- await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8");
124
- }
125
-
126
- async function setEnabled(enabled: boolean, ctx: ExtensionContext): Promise<void> {
127
- config.enabled = enabled;
128
- await saveConfig();
129
- updateStatus(ctx);
130
- ctx.ui.notify(`Completion tune ${enabled ? "enabled" : "disabled"}.`, "info");
131
- }
132
-
133
- async function ensureTuneWav(): Promise<void> {
134
- await mkdir(dirname(WAV_PATH), { recursive: true });
135
- await writeFile(WAV_PATH, createTuneWav(), "binary");
136
- const wav = WAV_PATH.replace(/'/g, "''");
137
- await writeFile(PS1_PATH, [
138
- "$ErrorActionPreference = 'SilentlyContinue'",
139
- `if (Test-Path '${wav}') {`,
140
- ` $p = New-Object System.Media.SoundPlayer '${wav}'`,
141
- " $p.Load()",
142
- " $p.PlaySync()",
143
- "}",
144
- "[System.Media.SystemSounds]::Asterisk.Play()",
145
- "Start-Sleep -Milliseconds 250",
146
- "",
147
- ].join("\n"), "utf8");
148
- const ps1 = PS1_PATH.replace(/"/g, "\"\"");
149
- await writeFile(VBS_PATH, [
150
- "Set shell = CreateObject(\"WScript.Shell\")",
151
- `shell.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -File ""${ps1}""", 0, False`,
152
- "",
153
- ].join("\r\n"), "utf8");
154
- }
155
-
156
- function createTuneWav(): Buffer {
157
- const sampleRate = 44100;
158
- const notes = [
159
- [659, 0.11],
160
- [784, 0.11],
161
- [988, 0.15],
162
- [1319, 0.21],
163
- [1175, 0.12],
164
- [1319, 0.26],
165
- ];
166
- const gapSeconds = 0.035;
167
- const samples: number[] = [];
168
-
169
- for (const [freq, seconds] of notes) {
170
- const count = Math.floor(sampleRate * seconds);
171
- for (let i = 0; i < count; i++) {
172
- const t = i / sampleRate;
173
- const fadeIn = Math.min(1, i / 120);
174
- const fadeOut = Math.min(1, (count - i) / 300);
175
- const env = Math.min(fadeIn, fadeOut) * 0.28;
176
- samples.push(Math.sin(2 * Math.PI * freq * t) * env);
177
- }
178
- for (let i = 0; i < Math.floor(sampleRate * gapSeconds); i++) samples.push(0);
179
- }
180
-
181
- const dataSize = samples.length * 2;
182
- const buffer = Buffer.alloc(44 + dataSize);
183
- buffer.write("RIFF", 0);
184
- buffer.writeUInt32LE(36 + dataSize, 4);
185
- buffer.write("WAVE", 8);
186
- buffer.write("fmt ", 12);
187
- buffer.writeUInt32LE(16, 16);
188
- buffer.writeUInt16LE(1, 20);
189
- buffer.writeUInt16LE(1, 22);
190
- buffer.writeUInt32LE(sampleRate, 24);
191
- buffer.writeUInt32LE(sampleRate * 2, 28);
192
- buffer.writeUInt16LE(2, 32);
193
- buffer.writeUInt16LE(16, 34);
194
- buffer.write("data", 36);
195
- buffer.writeUInt32LE(dataSize, 40);
196
-
197
- let offset = 44;
198
- for (const sample of samples) {
199
- const value = Math.max(-1, Math.min(1, sample));
200
- buffer.writeInt16LE(Math.round(value * 32767), offset);
201
- offset += 2;
202
- }
203
- return buffer;
204
- }
205
-
206
- function updateStatus(ctx: ExtensionContext): void {
207
- if (!ctx.hasUI) return;
208
- ctx.ui.setStatus("notify-sound", ctx.ui.theme.fg("dim", `tune:${config.enabled ? "on" : "off"}`));
209
- }
210
-
211
- function playCompletionTune(method: NotifyMethod = "auto"): void {
212
- const os = platform();
213
-
214
- if (os === "win32") {
215
- playWindowsTune(method);
216
- return;
217
- }
218
-
219
- if (os === "darwin") {
220
- runDetached("osascript", ["-e", "beep 1", "-e", "delay 0.12", "-e", "beep 1", "-e", "delay 0.12", "-e", "beep 2"]);
221
- return;
222
- }
223
-
224
- playLinuxTune();
225
- }
226
-
227
- function playWindowsTune(method: NotifyMethod): void {
228
- // Windows uses the generated WAV path because Console.Beep, system sounds,
229
- // and terminal bells are silent on many modern Windows machines.
230
- playWindowsWav();
231
- }
232
-
233
- function playWindowsWav(): void {
234
- // WScript launches the PowerShell player hidden without stealing focus. This
235
- // is more reliable than spawning hidden PowerShell directly from Pi's TUI.
236
- runDetached("wscript.exe", [VBS_PATH]);
237
- }
238
-
239
- function playLinuxTune(): void {
240
- // Prefer shell printf bells for broad compatibility; terminal may silence them.
241
- // If paplay/aplay exists, play the freedesktop complete sound as an additional fallback.
242
- runDetached("sh", [
243
- "-c",
244
- "(command -v paplay >/dev/null 2>&1 && paplay /usr/share/sounds/freedesktop/stereo/complete.oga >/dev/null 2>&1) || " +
245
- "(command -v aplay >/dev/null 2>&1 && aplay /usr/share/sounds/alsa/Front_Center.wav >/dev/null 2>&1) || " +
246
- "printf '\\a'; sleep 0.12; printf '\\a'; sleep 0.12; printf '\\a'",
247
- ]);
248
- }
249
-
250
- function runDetached(command: string, args: string[]): void {
251
- try {
252
- const child = spawn(command, args, {
253
- detached: true,
254
- stdio: "ignore",
255
- windowsHide: true,
256
- shell: false,
257
- });
258
- child.unref();
259
- } catch {
260
- // Notification failures should never interrupt pi.
261
- }
262
- }
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir, platform } from "node:os";
5
+ import { spawn } from "node:child_process";
6
+
7
+ type NotifySoundConfig = {
8
+ enabled: boolean;
9
+ };
10
+
11
+ type NotifyMethod = "auto" | "wav";
12
+
13
+ const GLOBAL_NOTIFY_DIR = join(homedir(), ".pi", "agent", "notify-sound");
14
+ const CONFIG_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.json");
15
+ const WAV_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.wav");
16
+ const PS1_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.ps1");
17
+ const VBS_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.vbs");
18
+ const STALE_AGENT_START_MS = 24 * 60 * 60 * 1000;
19
+
20
+ let config: NotifySoundConfig = { enabled: true };
21
+ let lastAgentStartedAt = 0;
22
+
23
+ export default async function notifySoundExtension(pi: ExtensionAPI) {
24
+ await loadConfig();
25
+ await ensureTuneWav();
26
+
27
+ pi.on("session_start", async (_event, ctx) => {
28
+ updateStatus(ctx);
29
+ });
30
+
31
+ pi.on("agent_start", async () => {
32
+ lastAgentStartedAt = Date.now();
33
+ });
34
+
35
+ pi.on("agent_end", async (_event, ctx) => {
36
+ updateStatus(ctx);
37
+
38
+ if (!config.enabled) return;
39
+ if (!lastAgentStartedAt) return;
40
+ if (Date.now() - lastAgentStartedAt > STALE_AGENT_START_MS) return;
41
+
42
+ playCompletionTune();
43
+ });
44
+
45
+ const command = {
46
+ description: "Toggle or test the agent completion tune notification",
47
+ getArgumentCompletions: (argumentPrefix: string) => {
48
+ const token = argumentPrefix.trim().toLowerCase();
49
+ return [
50
+ { value: "status", label: "status", description: "Show whether the completion tune is on" },
51
+ { value: "test", label: "test", description: "Play the completion tune" },
52
+ { value: "test wav", label: "test wav", description: "Play the generated WAV completion tune" },
53
+ { value: "on", label: "on", description: "Enable the completion tune" },
54
+ { value: "off", label: "off", description: "Disable the completion tune" },
55
+ { value: "toggle", label: "toggle", description: "Toggle the completion tune" },
56
+ ].filter((completion) => !token || completion.value.startsWith(token));
57
+ },
58
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
59
+ const action = args.trim().toLowerCase() || "toggle";
60
+
61
+ if (action === "test") {
62
+ playCompletionTune("wav");
63
+ ctx.ui.notify("Played completion tune using WAV mode. This is the default reliable Windows path.", "info");
64
+ return;
65
+ }
66
+
67
+ if (action === "test wav") {
68
+ playCompletionTune("wav");
69
+ ctx.ui.notify("Played completion tune using WAV mode.", "info");
70
+ return;
71
+ }
72
+
73
+ if (action.startsWith("test ")) {
74
+ ctx.ui.notify("Usage: /notify test", "warning");
75
+ return;
76
+ }
77
+
78
+ if (action === "status") {
79
+ updateStatus(ctx);
80
+ ctx.ui.notify(`Completion tune is ${config.enabled ? "ON" : "OFF"}.`, "info");
81
+ return;
82
+ }
83
+
84
+ if (action === "on") {
85
+ await setEnabled(true, ctx);
86
+ return;
87
+ }
88
+
89
+ if (action === "off") {
90
+ await setEnabled(false, ctx);
91
+ return;
92
+ }
93
+
94
+ if (action === "toggle") {
95
+ await setEnabled(!config.enabled, ctx);
96
+ return;
97
+ }
98
+
99
+ ctx.ui.notify("Usage: /notify [test|status|on|off|toggle]", "warning");
100
+ },
101
+ };
102
+
103
+ pi.registerCommand("notify-sound", command);
104
+ pi.registerCommand("notify", {
105
+ ...command,
106
+ description: "Alias for /notify-sound",
107
+ });
108
+ }
109
+
110
+ async function loadConfig(): Promise<void> {
111
+ try {
112
+ const raw = await readFile(CONFIG_PATH, "utf8");
113
+ const parsed = JSON.parse(raw) as Partial<NotifySoundConfig>;
114
+ config = { enabled: parsed.enabled !== false };
115
+ } catch {
116
+ config = { enabled: true };
117
+ await saveConfig();
118
+ }
119
+ }
120
+
121
+ async function saveConfig(): Promise<void> {
122
+ await mkdir(dirname(CONFIG_PATH), { recursive: true });
123
+ await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8");
124
+ }
125
+
126
+ async function setEnabled(enabled: boolean, ctx: ExtensionContext): Promise<void> {
127
+ config.enabled = enabled;
128
+ await saveConfig();
129
+ updateStatus(ctx);
130
+ ctx.ui.notify(`Completion tune ${enabled ? "enabled" : "disabled"}.`, "info");
131
+ }
132
+
133
+ async function ensureTuneWav(): Promise<void> {
134
+ await mkdir(dirname(WAV_PATH), { recursive: true });
135
+ await writeFile(WAV_PATH, createTuneWav(), "binary");
136
+ const wav = WAV_PATH.replace(/'/g, "''");
137
+ await writeFile(PS1_PATH, [
138
+ "$ErrorActionPreference = 'SilentlyContinue'",
139
+ `if (Test-Path '${wav}') {`,
140
+ ` $p = New-Object System.Media.SoundPlayer '${wav}'`,
141
+ " $p.Load()",
142
+ " $p.PlaySync()",
143
+ "}",
144
+ "[System.Media.SystemSounds]::Asterisk.Play()",
145
+ "Start-Sleep -Milliseconds 250",
146
+ "",
147
+ ].join("\n"), "utf8");
148
+ const ps1 = PS1_PATH.replace(/"/g, "\"\"");
149
+ await writeFile(VBS_PATH, [
150
+ "Set shell = CreateObject(\"WScript.Shell\")",
151
+ `shell.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -File ""${ps1}""", 0, False`,
152
+ "",
153
+ ].join("\r\n"), "utf8");
154
+ }
155
+
156
+ function createTuneWav(): Buffer {
157
+ const sampleRate = 44100;
158
+ const notes = [
159
+ [659, 0.11],
160
+ [784, 0.11],
161
+ [988, 0.15],
162
+ [1319, 0.21],
163
+ [1175, 0.12],
164
+ [1319, 0.26],
165
+ ];
166
+ const gapSeconds = 0.035;
167
+ const samples: number[] = [];
168
+
169
+ for (const [freq, seconds] of notes) {
170
+ const count = Math.floor(sampleRate * seconds);
171
+ for (let i = 0; i < count; i++) {
172
+ const t = i / sampleRate;
173
+ const fadeIn = Math.min(1, i / 120);
174
+ const fadeOut = Math.min(1, (count - i) / 300);
175
+ const env = Math.min(fadeIn, fadeOut) * 0.28;
176
+ samples.push(Math.sin(2 * Math.PI * freq * t) * env);
177
+ }
178
+ for (let i = 0; i < Math.floor(sampleRate * gapSeconds); i++) samples.push(0);
179
+ }
180
+
181
+ const dataSize = samples.length * 2;
182
+ const buffer = Buffer.alloc(44 + dataSize);
183
+ buffer.write("RIFF", 0);
184
+ buffer.writeUInt32LE(36 + dataSize, 4);
185
+ buffer.write("WAVE", 8);
186
+ buffer.write("fmt ", 12);
187
+ buffer.writeUInt32LE(16, 16);
188
+ buffer.writeUInt16LE(1, 20);
189
+ buffer.writeUInt16LE(1, 22);
190
+ buffer.writeUInt32LE(sampleRate, 24);
191
+ buffer.writeUInt32LE(sampleRate * 2, 28);
192
+ buffer.writeUInt16LE(2, 32);
193
+ buffer.writeUInt16LE(16, 34);
194
+ buffer.write("data", 36);
195
+ buffer.writeUInt32LE(dataSize, 40);
196
+
197
+ let offset = 44;
198
+ for (const sample of samples) {
199
+ const value = Math.max(-1, Math.min(1, sample));
200
+ buffer.writeInt16LE(Math.round(value * 32767), offset);
201
+ offset += 2;
202
+ }
203
+ return buffer;
204
+ }
205
+
206
+ function updateStatus(ctx: ExtensionContext): void {
207
+ if (!ctx.hasUI) return;
208
+ ctx.ui.setStatus("notify-sound", ctx.ui.theme.fg("dim", `tune:${config.enabled ? "on" : "off"}`));
209
+ }
210
+
211
+ function playCompletionTune(method: NotifyMethod = "auto"): void {
212
+ const os = platform();
213
+
214
+ if (os === "win32") {
215
+ playWindowsTune(method);
216
+ return;
217
+ }
218
+
219
+ if (os === "darwin") {
220
+ runDetached("osascript", ["-e", "beep 1", "-e", "delay 0.12", "-e", "beep 1", "-e", "delay 0.12", "-e", "beep 2"]);
221
+ return;
222
+ }
223
+
224
+ playLinuxTune();
225
+ }
226
+
227
+ function playWindowsTune(method: NotifyMethod): void {
228
+ // Windows uses the generated WAV path because Console.Beep, system sounds,
229
+ // and terminal bells are silent on many modern Windows machines.
230
+ playWindowsWav();
231
+ }
232
+
233
+ function playWindowsWav(): void {
234
+ // WScript launches the PowerShell player hidden without stealing focus. This
235
+ // is more reliable than spawning hidden PowerShell directly from Pi's TUI.
236
+ runDetached("wscript.exe", [VBS_PATH]);
237
+ }
238
+
239
+ function playLinuxTune(): void {
240
+ // Prefer shell printf bells for broad compatibility; terminal may silence them.
241
+ // If paplay/aplay exists, play the freedesktop complete sound as an additional fallback.
242
+ runDetached("sh", [
243
+ "-c",
244
+ "(command -v paplay >/dev/null 2>&1 && paplay /usr/share/sounds/freedesktop/stereo/complete.oga >/dev/null 2>&1) || " +
245
+ "(command -v aplay >/dev/null 2>&1 && aplay /usr/share/sounds/alsa/Front_Center.wav >/dev/null 2>&1) || " +
246
+ "printf '\\a'; sleep 0.12; printf '\\a'; sleep 0.12; printf '\\a'",
247
+ ]);
248
+ }
249
+
250
+ function runDetached(command: string, args: string[]): void {
251
+ try {
252
+ const child = spawn(command, args, {
253
+ detached: true,
254
+ stdio: "ignore",
255
+ windowsHide: true,
256
+ shell: false,
257
+ });
258
+ child.unref();
259
+ } catch {
260
+ // Notification failures should never interrupt pi.
261
+ }
262
+ }
@@ -27,6 +27,8 @@ Pi extension that auto-loads and registers an `oauth-router` provider with multi
27
27
  - weighted round robin
28
28
  - 429 cooldowns
29
29
  - transient failure penalties
30
+ - client/network transport failure tracking without default account cooldown
31
+ - 5 router-level client/network retries by default before pre-output failover, starting at 5s and doubling
30
32
  - auth failure quarantine
31
33
  - safe pre-output failover
32
34
 
@@ -45,7 +47,7 @@ The extension ships with two default upstream profiles:
45
47
  - api: `openai-responses`
46
48
  - default models: `gpt-4o`, `gpt-4.1`, `o4-mini`
47
49
 
48
- Edit `~/.pi/agent/oauth-router/config.json` to add more upstreams, swap endpoints, or change model catalogs.
50
+ Edit `~/.pi/agent/oauth-router/config.json` to add more upstreams, swap endpoints, change model catalogs, or tune retry behavior. By default client-side transport failures such as `Codex SSE response headers timed out after 10000ms` retry the same account 5 times with exponential backoff (`5s`, `10s`, `20s`, `40s`, then capped at `60s`) before router failover. These failures are recorded but do not cool down an account unless `clientNetworkPenaltyMs` is set above `0`.
49
51
 
50
52
  ## Setup
51
53
 
@@ -98,7 +100,7 @@ Health and local usage state live separately in:
98
100
 
99
101
  - `~/.pi/agent/oauth-router/state.json`
100
102
 
101
- The router records successful request usage per account for rolling local 5-hour and weekly windows. These are router-observed counters only. `/router-refresh-usage [id|all]` now also probes configured authenticated provider endpoints for ChatGPT/Codex quota windows, then falls back to safe token claims such as account id, token expiry, issuer, subject, and available claim keys. `/router-usage` shows a compact visual quota view; `/router-usage-raw [id]` shows detailed/raw provider data. Provider-side quota counters depend on undocumented upstream endpoints and may change; they are not normally present in the OAuth token itself.
103
+ The router records successful request usage per account for rolling local 5-hour and weekly windows. These are router-observed counters only. `/router-refresh-usage [id|all]` probes ChatGPT/Codex provider quota from `/backend-api/wham/usage` first and treats `rate_limit.primary_window.used_percent` as the 5-hour window and `rate_limit.secondary_window.used_percent` as the weekly window. It then falls back to safe token claims such as account id, token expiry, issuer, subject, and available claim keys. `/router-usage` shows a compact visual quota view; `/router-usage-raw [id]` shows detailed/raw provider data. Provider-side quota counters depend on undocumented upstream endpoints and may change; they are not normally present in the OAuth token itself.
102
104
 
103
105
  Most account commands accept an optional account ID. When run in the Pi UI without an ID, the extension opens an account picker instead of dumping the same account list repeatedly.
104
106