pi-orchestrator-config 4.1.5

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 (175) hide show
  1. package/README.md +808 -0
  2. package/agents/api-documenter.md +32 -0
  3. package/agents/bash-expert.md +53 -0
  4. package/agents/code-reviewer-docs.md +145 -0
  5. package/agents/code-reviewer-guidelines.md +97 -0
  6. package/agents/code-reviewer-quality.md +142 -0
  7. package/agents/code-reviewer-security.md +107 -0
  8. package/agents/code-reviewer-spec.md +164 -0
  9. package/agents/debugger.md +63 -0
  10. package/agents/docker-expert.md +40 -0
  11. package/agents/docs-fetcher.md +48 -0
  12. package/agents/git-expert.md +63 -0
  13. package/agents/github-expert.md +83 -0
  14. package/agents/go-expert.md +62 -0
  15. package/agents/issue-reviewer-feasibility.md +60 -0
  16. package/agents/issue-reviewer-scope.md +63 -0
  17. package/agents/issue-reviewer-spec.md +77 -0
  18. package/agents/java-expert.md +31 -0
  19. package/agents/jenkins-expert.md +38 -0
  20. package/agents/kubernetes-expert.md +40 -0
  21. package/agents/planner.md +42 -0
  22. package/agents/python-expert.md +61 -0
  23. package/agents/reviewer.md +26 -0
  24. package/agents/scout.md +40 -0
  25. package/agents/security-auditor.md +254 -0
  26. package/agents/technical-documentation-writer.md +37 -0
  27. package/agents/test-automator.md +55 -0
  28. package/agents/test-runner.md +47 -0
  29. package/agents/ts-expert.md +23 -0
  30. package/agents/worker.md +30 -0
  31. package/extensions/acpx-provider/index.ts +104 -0
  32. package/extensions/acpx-provider/load-runtime.ts +99 -0
  33. package/extensions/acpx-provider/runtime-models.ts +40 -0
  34. package/extensions/cli-provider/agents/claude.ts +128 -0
  35. package/extensions/cli-provider/agents/cursor.ts +84 -0
  36. package/extensions/cli-provider/agents/gemini.ts +100 -0
  37. package/extensions/cli-provider/discover.ts +77 -0
  38. package/extensions/cli-provider/index.ts +57 -0
  39. package/extensions/cli-provider/parsers.ts +304 -0
  40. package/extensions/cli-provider/providers.ts +58 -0
  41. package/extensions/cli-provider/runner.ts +225 -0
  42. package/extensions/cli-provider/runtime-models.ts +32 -0
  43. package/extensions/cli-provider/session-reaper.ts +122 -0
  44. package/extensions/cli-provider/sessions.ts +533 -0
  45. package/extensions/cli-provider/shared/discover-cache.ts +235 -0
  46. package/extensions/cli-provider/types.ts +48 -0
  47. package/extensions/coms/coms-net-server.ts +1642 -0
  48. package/extensions/coms/coms-net-wrapper.ts +521 -0
  49. package/extensions/coms/coms-net.ts +1589 -0
  50. package/extensions/coms/coms-p2p.ts +1879 -0
  51. package/extensions/coms/coms-shared.ts +510 -0
  52. package/extensions/coms/coms-wrapper.ts +138 -0
  53. package/extensions/coms/index.ts +20 -0
  54. package/extensions/coms/themeMap.ts +147 -0
  55. package/extensions/image-gen/image-gen.ts +332 -0
  56. package/extensions/image-gen/index.ts +14 -0
  57. package/extensions/orchestrator/agents.ts +134 -0
  58. package/extensions/orchestrator/ask-user.ts +293 -0
  59. package/extensions/orchestrator/async-agents.ts +1037 -0
  60. package/extensions/orchestrator/async-capability.ts +129 -0
  61. package/extensions/orchestrator/async-runner.ts +324 -0
  62. package/extensions/orchestrator/async-status-parse.ts +31 -0
  63. package/extensions/orchestrator/async-status-ui.ts +201 -0
  64. package/extensions/orchestrator/async-wait.ts +36 -0
  65. package/extensions/orchestrator/btw.ts +210 -0
  66. package/extensions/orchestrator/cron-status-format.ts +126 -0
  67. package/extensions/orchestrator/cron-status-ui.ts +132 -0
  68. package/extensions/orchestrator/cron.ts +730 -0
  69. package/extensions/orchestrator/dreaming.ts +355 -0
  70. package/extensions/orchestrator/enforcement-helpers.ts +530 -0
  71. package/extensions/orchestrator/enforcement-rules.ts +329 -0
  72. package/extensions/orchestrator/enforcement.ts +828 -0
  73. package/extensions/orchestrator/extended-autocomplete.ts +593 -0
  74. package/extensions/orchestrator/git-helpers.ts +420 -0
  75. package/extensions/orchestrator/github-autocomplete.ts +250 -0
  76. package/extensions/orchestrator/icons.ts +17 -0
  77. package/extensions/orchestrator/index.ts +100 -0
  78. package/extensions/orchestrator/memory-embeddings.ts +281 -0
  79. package/extensions/orchestrator/memory-promotion.ts +449 -0
  80. package/extensions/orchestrator/memory-provenance.ts +80 -0
  81. package/extensions/orchestrator/memory-query-class.ts +90 -0
  82. package/extensions/orchestrator/memory-scoring.ts +434 -0
  83. package/extensions/orchestrator/memory-tools.ts +883 -0
  84. package/extensions/orchestrator/memory-tree.ts +212 -0
  85. package/extensions/orchestrator/nvim.ts +169 -0
  86. package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
  87. package/extensions/orchestrator/overlay-dashboard.ts +512 -0
  88. package/extensions/orchestrator/pi-config-review-state.ts +303 -0
  89. package/extensions/orchestrator/preference-extractor.ts +106 -0
  90. package/extensions/orchestrator/project-settings.ts +468 -0
  91. package/extensions/orchestrator/promotion-queue.ts +311 -0
  92. package/extensions/orchestrator/resolve-agent-model.ts +32 -0
  93. package/extensions/orchestrator/review-ui.ts +204 -0
  94. package/extensions/orchestrator/rule-placeholders.ts +12 -0
  95. package/extensions/orchestrator/rules.ts +481 -0
  96. package/extensions/orchestrator/session-search.ts +243 -0
  97. package/extensions/orchestrator/session-validation.ts +374 -0
  98. package/extensions/orchestrator/situation-report.ts +370 -0
  99. package/extensions/orchestrator/status-bar.ts +31 -0
  100. package/extensions/orchestrator/status-line.ts +192 -0
  101. package/extensions/orchestrator/status.ts +110 -0
  102. package/extensions/orchestrator/subagent-tool.ts +1748 -0
  103. package/extensions/orchestrator/task-lifecycle.ts +88 -0
  104. package/extensions/orchestrator/utils.ts +227 -0
  105. package/extensions/pidash/index.ts +15 -0
  106. package/extensions/pidash/pidash.ts +941 -0
  107. package/extensions/pidiff/index.ts +13 -0
  108. package/extensions/pidiff/pidiff.ts +369 -0
  109. package/extensions/providers/acpx-driver.ts +479 -0
  110. package/extensions/providers/built-in-drivers.ts +52 -0
  111. package/extensions/providers/claude-driver.ts +319 -0
  112. package/extensions/providers/cursor-acpx-driver.ts +398 -0
  113. package/extensions/providers/cursor-cli-driver.ts +484 -0
  114. package/extensions/providers/gemini-driver.ts +302 -0
  115. package/extensions/providers/index.ts +513 -0
  116. package/extensions/shared/build-system-prompt.ts +57 -0
  117. package/extensions/shared/create-runtime-provider.ts +190 -0
  118. package/extensions/shared/daemon-manager.ts +248 -0
  119. package/extensions/shared/file-logger.ts +115 -0
  120. package/extensions/shared/managed-refresh.ts +211 -0
  121. package/extensions/shared/provider-driver.ts +295 -0
  122. package/extensions/shared/provider-errors.ts +305 -0
  123. package/extensions/shared/provider-registry.ts +388 -0
  124. package/extensions/shared/stream-builder.ts +275 -0
  125. package/extensions/shared/types.ts +20 -0
  126. package/extensions/shared/ui/README.md +10 -0
  127. package/extensions/shared/ui/badge.tsx +52 -0
  128. package/extensions/shared/ui/button.tsx +58 -0
  129. package/extensions/shared/ui/collapsible.tsx +21 -0
  130. package/extensions/shared/ui/dropdown-menu.tsx +266 -0
  131. package/extensions/shared/ui/input.tsx +20 -0
  132. package/extensions/shared/ui/popover.tsx +114 -0
  133. package/extensions/shared/ui/resizable.tsx +48 -0
  134. package/extensions/shared/ui/scroll-area.tsx +53 -0
  135. package/extensions/shared/ui/select.tsx +201 -0
  136. package/extensions/shared/ui/separator.tsx +25 -0
  137. package/extensions/shared/ui/tooltip.tsx +64 -0
  138. package/extensions/shared/ws-client.ts +74 -0
  139. package/package.json +59 -0
  140. package/prompts/coderabbit-rate-limit.md +95 -0
  141. package/prompts/create-coms-feature-manager.md +186 -0
  142. package/prompts/create-skill.md +128 -0
  143. package/prompts/domain-model.md +109 -0
  144. package/prompts/external-ai.md +665 -0
  145. package/prompts/implement-and-review.md +25 -0
  146. package/prompts/implement.md +26 -0
  147. package/prompts/issue-review.md +256 -0
  148. package/prompts/pr-review.md +753 -0
  149. package/prompts/query-db.md +154 -0
  150. package/prompts/refine-review.md +208 -0
  151. package/prompts/release.md +235 -0
  152. package/prompts/remember.md +33 -0
  153. package/prompts/review-handler-status.md +27 -0
  154. package/prompts/review-handler.md +677 -0
  155. package/prompts/review-local.md +159 -0
  156. package/prompts/scout-and-plan.md +21 -0
  157. package/rules/00-orchestrator-core.md +42 -0
  158. package/rules/05-issue-first-workflow.md +119 -0
  159. package/rules/10-agent-routing.md +58 -0
  160. package/rules/15-mcp-launchpad.md +46 -0
  161. package/rules/20-code-review-loop.md +234 -0
  162. package/rules/25-documentation-updates.md +15 -0
  163. package/rules/30-prompt-templates.md +19 -0
  164. package/rules/35-memory.md +240 -0
  165. package/rules/40-critical-rules.md +124 -0
  166. package/rules/45-file-preview.md +29 -0
  167. package/rules/50-agent-bug-reporting.md +90 -0
  168. package/rules/55-coms-protocol.md +63 -0
  169. package/rules/60-task-tracking.md +66 -0
  170. package/skills/conflict-resolver/SKILL.md +213 -0
  171. package/skills/skill-creator/SKILL.md +85 -0
  172. package/skills/systematic-debugging/SKILL.md +353 -0
  173. package/skills/test-driven-development/SKILL.md +335 -0
  174. package/skills/update-agents-md/SKILL.md +282 -0
  175. package/templates/coms-feature-manager-prompt.md +313 -0
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Pidiff Extension for pi
3
+ *
4
+ * Standalone diff viewer extension — connects to the pidiff daemon.
5
+ * Publishes review comments from the browser back into the pi session.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
+ import { registerPidiff } from "./pidiff.js";
10
+
11
+ export default function (pi: ExtensionAPI) {
12
+ registerPidiff(pi);
13
+ }
@@ -0,0 +1,369 @@
1
+ /**
2
+ * pidiff — diff viewer extension (per-project server mode).
3
+ *
4
+ * Uses daemon-manager for spawn/connect/reconnect.
5
+ * Each project gets its own server on a dynamically allocated port,
6
+ * tracked via lockfiles in <cwd>/.pi/tmp/.
7
+ * Receives published review comments and injects into pi session.
8
+ */
9
+
10
+ import { execFileSync } from "node:child_process";
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { createRequire } from "node:module";
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { hyperlink } from "@earendil-works/pi-tui";
16
+ import {
17
+ createLogger,
18
+ checkHealth,
19
+ ensureUiBuilt,
20
+ spawnDaemon,
21
+ killDaemonByPid,
22
+ waitForDaemon,
23
+ findFreePort,
24
+ writeLockfile,
25
+ readLockfile,
26
+ removeLockfile,
27
+ } from "../shared/daemon-manager.js";
28
+ import { setupHeartbeat, setupReconnectPoller } from "../shared/ws-client.js";
29
+ import { getSetting } from "../orchestrator/project-settings.js";
30
+
31
+ const RECONNECT_INTERVAL_MS = 5000;
32
+ const ICON_DIFF = "";
33
+
34
+ const log = createLogger(
35
+ path.join(process.env.HOME || "/tmp", ".pi", "pidiff-debug.log"),
36
+ "ext",
37
+ );
38
+
39
+ function isGitRepo(cwd: string): boolean {
40
+ try { execFileSync("git", ["rev-parse", "--git-dir"], { cwd, stdio: "ignore" }); return true; } catch { return false; }
41
+ }
42
+
43
+ function getBranch(cwd: string): string {
44
+ try {
45
+ return execFileSync("git", ["branch", "--show-current"], {
46
+ cwd, encoding: "utf-8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
47
+ }).trim();
48
+ } catch { return ""; }
49
+ }
50
+
51
+ export function registerPidiff(pi: ExtensionAPI): void {
52
+ if (process.env.PI_SUBAGENT_CHILD === "1") return;
53
+
54
+ const projectCwd = process.cwd();
55
+ const pidiffDisabled = !getSetting(projectCwd, "pidiff_enable");
56
+ if (pidiffDisabled) {
57
+ pi.registerCommand("pidiff", {
58
+ description: "Manage pidiff server — /pidiff start|stop|restart|status",
59
+ getArgumentCompletions: (prefix: string) => {
60
+ const items = [
61
+ { value: "start", label: "start", description: "Start pidiff server" },
62
+ { value: "stop", label: "stop", description: "Stop pidiff server" },
63
+ { value: "restart", label: "restart", description: "Restart pidiff server" },
64
+ { value: "status", label: "status", description: "Show pidiff status" },
65
+ ];
66
+ return items.filter(i => i.value.startsWith(prefix.toLowerCase()));
67
+ },
68
+ handler: async (_args, ctx) => {
69
+ if (ctx.hasUI) ctx.ui.notify("pidiff is disabled (pidiff_enable=false in pi-config-settings.json or PI_PIDIFF_ENABLE=false).", "info");
70
+ },
71
+ });
72
+ return;
73
+ }
74
+
75
+ let ws: any = null;
76
+ let connected = false;
77
+ let connecting = false;
78
+ let shuttingDown = false;
79
+ let spawning = false;
80
+ let lastCtx: any = null;
81
+ let cleanupHeartbeat: (() => void) | null = null;
82
+ let lockDir = ""; // Set on session_start from ctx.cwd
83
+ let activePort = 0;
84
+
85
+ function setStatus(ctx: any): void {
86
+ try {
87
+ if (ctx?.hasUI && activePort) {
88
+ const link = hyperlink("pi-diff", `http://localhost:${activePort}`);
89
+ const label = ICON_DIFF ? `${ICON_DIFF} ${link}` : link;
90
+ ctx.ui.setStatus("8-diff", ctx.ui.theme.fg("accent", label));
91
+ }
92
+ } catch {
93
+ // ctx may be stale if session was replaced during WebSocket connect
94
+ }
95
+ if (activePort) pi.events?.emit("diff-viewer:port", activePort);
96
+ }
97
+
98
+ function doSpawn(port: number, cwd: string): void {
99
+ ensureUiBuilt(import.meta.url, "pidiff-ui", log);
100
+ spawnDaemon({
101
+ serverScript: "pidiff-server.ts",
102
+ logFile: path.join(process.env.HOME || "/tmp", ".pi", "pidiff-server.log"),
103
+ env: { PI_PIDIFF_PORT: String(port), PI_PIDIFF_CWD: cwd },
104
+ log,
105
+ });
106
+ }
107
+
108
+ async function connect(ctx: any) {
109
+ log(`connect() called, connected=${connected}, connecting=${connecting}`);
110
+ if (connected || connecting || shuttingDown) return;
111
+ connecting = true;
112
+ lastCtx = ctx;
113
+ if (!lockDir) { log("connect: lockDir not set (session_start not fired yet)"); connecting = false; return; }
114
+
115
+ // Try to discover port from lockfile
116
+ let port = activePort;
117
+ if (!port) {
118
+ const lock = readLockfile(lockDir);
119
+ if (lock) port = lock.port;
120
+ }
121
+
122
+ // Check if existing port is healthy
123
+ let running = false;
124
+ if (port) {
125
+ running = await checkHealth(port);
126
+ log(`lockfile port=${port}, healthy=${running}`);
127
+ }
128
+
129
+ if (!running) {
130
+ if (spawning) {
131
+ log("daemon already spawning, waiting...");
132
+ connecting = false;
133
+ return;
134
+ }
135
+
136
+ // Atomic spawn guard — prevent two sessions from racing to spawn
137
+ const spawnLock = path.join(lockDir, "pidiff.spawning");
138
+ try {
139
+ fs.writeFileSync(spawnLock, String(process.pid), { flag: "wx" });
140
+ } catch {
141
+ // Another session is already spawning — wait for lockfile to appear
142
+ log("another session is spawning pidiff, waiting for lockfile...");
143
+ for (let i = 0; i < 30; i++) {
144
+ await new Promise(r => setTimeout(r, 1000));
145
+ const lock = readLockfile(lockDir);
146
+ if (lock && await checkHealth(lock.port)) {
147
+ port = lock.port;
148
+ activePort = port;
149
+ break;
150
+ }
151
+ }
152
+ connecting = false;
153
+ if (port) setTimeout(() => connect(ctx), 1000);
154
+ return;
155
+ }
156
+
157
+ spawning = true;
158
+ try {
159
+ log("spawning daemon...");
160
+ port = await findFreePort();
161
+ log(`allocated free port: ${port}`);
162
+ doSpawn(port, ctx.cwd);
163
+ writeLockfile(lockDir, port, null, log);
164
+ const ready = await waitForDaemon(port, 60, log);
165
+ if (!ready) { connecting = false; return; }
166
+ } finally {
167
+ spawning = false;
168
+ try { fs.unlinkSync(spawnLock); } catch {}
169
+ }
170
+ }
171
+
172
+ activePort = port;
173
+
174
+ try {
175
+ const _require = createRequire(import.meta.url);
176
+ const WebSocket = _require("ws");
177
+ log(`connecting WebSocket to port ${activePort}...`);
178
+ const wsClient = new WebSocket(`ws://127.0.0.1:${activePort}/ws/pi`);
179
+
180
+ wsClient.on("open", () => {
181
+ log("WebSocket connected");
182
+ ws = wsClient;
183
+ connected = true;
184
+ connecting = false;
185
+
186
+ try {
187
+ setStatus(ctx);
188
+
189
+ // Register this session
190
+ const branch = getBranch(ctx.cwd);
191
+ wsClient.send(JSON.stringify({
192
+ type: "register",
193
+ pid: process.pid,
194
+ sessionId: `${process.pid}:${ctx.cwd}`,
195
+ cwd: ctx.cwd,
196
+ branch,
197
+ }));
198
+ } catch {
199
+ // ctx may be stale if session was replaced during WebSocket connect
200
+ log("skipped registration — ctx is stale");
201
+ }
202
+
203
+ // Keepalive + dead connection detection
204
+ if (cleanupHeartbeat) cleanupHeartbeat();
205
+ cleanupHeartbeat = setupHeartbeat({
206
+ ws: wsClient,
207
+ log,
208
+ onDead: () => {
209
+ connected = false; ws = null;
210
+ if (!shuttingDown && lastCtx) setTimeout(() => connect(lastCtx), 1000);
211
+ },
212
+ });
213
+ });
214
+
215
+ wsClient.on("message", (data: Buffer) => {
216
+ try {
217
+ const parsed = JSON.parse(data.toString());
218
+ if (parsed.type === "publish-review" && parsed.comments?.length > 0) {
219
+ log(`review received: ${parsed.comments.length} comments`);
220
+ const review = {
221
+ source: "pidiff",
222
+ type: "code-review",
223
+ comments: parsed.comments.map((c: any) => ({
224
+ file: c.file,
225
+ line: c.line,
226
+ side: c.side,
227
+ body: c.body,
228
+ ...(c.branch ? { branch: c.branch } : {}),
229
+ ...(c.worktreePath ? { worktreePath: c.worktreePath } : {}),
230
+ ...(c.replies?.length ? { replies: c.replies } : {}),
231
+ ...(c.resolved ? { resolved: true } : {}),
232
+ })),
233
+ ...(parsed.summary ? { summary: parsed.summary } : {}),
234
+ };
235
+ const message = `## pidiff: Code Review Comments\n\n\`\`\`json\n${JSON.stringify(review, null, 2)}\n\`\`\`\n\nPlease address these review comments.`;
236
+ pi.sendUserMessage(message, { deliverAs: "followUp" });
237
+ }
238
+ } catch (e: any) { log(`message handler error: ${e.message}`); }
239
+ });
240
+
241
+ wsClient.on("close", () => {
242
+ log("WebSocket closed");
243
+ connected = false; ws = null; connecting = false;
244
+ if (!shuttingDown) {
245
+ setTimeout(() => { if (lastCtx && !shuttingDown) connect(lastCtx); }, RECONNECT_INTERVAL_MS);
246
+ }
247
+ });
248
+
249
+ wsClient.on("error", (e: Error) => { log(`WebSocket error: ${e.message}`); });
250
+ } catch (e: any) {
251
+ log(`connect error: ${e.message}`);
252
+ connecting = false;
253
+ }
254
+ }
255
+
256
+ // /pidiff command
257
+ pi.registerCommand("pidiff", {
258
+ description: "Manage pidiff server — /pidiff start|stop|restart|status",
259
+ getArgumentCompletions: (prefix: string) => {
260
+ const items = [
261
+ { value: "start", label: "start", description: "Start pidiff server" },
262
+ { value: "stop", label: "stop", description: "Stop pidiff server" },
263
+ { value: "restart", label: "restart", description: "Restart pidiff server" },
264
+ { value: "status", label: "status", description: "Show pidiff status" },
265
+ ];
266
+ return items.filter(i => i.value.startsWith(prefix.toLowerCase()));
267
+ },
268
+ handler: async (args, ctx) => {
269
+ lastCtx = ctx;
270
+ const cmd = (args || "").trim().toLowerCase();
271
+
272
+ if (cmd === "stop") {
273
+ if (ws) { try { ws.close(); } catch {} ws = null; }
274
+ connected = false;
275
+ const pidFile = path.join(lockDir, "pidiff.pid");
276
+ killDaemonByPid(pidFile, log);
277
+ removeLockfile(lockDir, log);
278
+ activePort = 0;
279
+ if (ctx.hasUI) { ctx.ui.setStatus("8-diff", undefined); ctx.ui.notify("pidiff server stopped", "info"); }
280
+ return;
281
+ }
282
+
283
+ if (cmd === "start") {
284
+ const lock = readLockfile(lockDir);
285
+ if (lock && await checkHealth(lock.port)) {
286
+ activePort = lock.port;
287
+ if (ctx.hasUI) ctx.ui.notify(`pidiff already running at http://localhost:${activePort}`, "info");
288
+ if (!connected) connect(ctx);
289
+ return;
290
+ }
291
+ const port = await findFreePort();
292
+ doSpawn(port, ctx.cwd);
293
+ writeLockfile(lockDir, port, null, log);
294
+ if (ctx.hasUI) ctx.ui.notify("Starting pidiff server...", "info");
295
+ if (await waitForDaemon(port, 60, log)) {
296
+ activePort = port;
297
+ connect(ctx);
298
+ if (ctx.hasUI) ctx.ui.notify(`pidiff started at http://localhost:${activePort}`, "info");
299
+ } else {
300
+ if (ctx.hasUI) ctx.ui.notify("pidiff failed to start", "warning");
301
+ }
302
+ return;
303
+ }
304
+
305
+ if (cmd === "restart") {
306
+ if (ws) { try { ws.close(); } catch {} ws = null; }
307
+ connected = false;
308
+ const pidFile = path.join(lockDir, "pidiff.pid");
309
+ killDaemonByPid(pidFile, log);
310
+ removeLockfile(lockDir, log);
311
+ await new Promise(r => setTimeout(r, 1000));
312
+ const port = await findFreePort();
313
+ doSpawn(port, ctx.cwd);
314
+ writeLockfile(lockDir, port, null, log);
315
+ if (ctx.hasUI) ctx.ui.notify("Restarting pidiff server...", "info");
316
+ if (await waitForDaemon(port, 60, log)) {
317
+ activePort = port;
318
+ connect(ctx);
319
+ if (ctx.hasUI) ctx.ui.notify(`pidiff restarted at http://localhost:${activePort}`, "info");
320
+ } else {
321
+ if (ctx.hasUI) ctx.ui.notify("pidiff failed to restart", "warning");
322
+ }
323
+ return;
324
+ }
325
+
326
+ const lock = readLockfile(lockDir);
327
+ const statusPort = activePort || lock?.port || 0;
328
+ const running = statusPort ? await checkHealth(statusPort) : false;
329
+ let msg = `Server: ${running ? "running" : "stopped"}\nPort: ${statusPort || "(none)"}\n`;
330
+ msg += `Extension: ${connected ? "connected" : "disconnected"}`;
331
+ if (statusPort) msg += `\nURL: http://localhost:${statusPort}`;
332
+ if (ctx.hasUI) ctx.ui.notify(msg, "info");
333
+ },
334
+ });
335
+
336
+ // Session lifecycle
337
+ pi.on("session_start", async (_event, ctx) => {
338
+ lastCtx = ctx;
339
+ lockDir = path.join(ctx.cwd, ".pi", "tmp");
340
+ if (!isGitRepo(ctx.cwd)) return;
341
+ if (!connected && ctx.mode === "tui") connect(ctx);
342
+ });
343
+
344
+ pi.on("agent_end", (_event, ctx) => {
345
+ lastCtx = ctx;
346
+ if (connected) setStatus(ctx);
347
+ });
348
+
349
+ pi.on("tool_result", (_event, ctx) => {
350
+ if (!connected && !shuttingDown && ctx.mode === "tui") connect(ctx);
351
+ });
352
+
353
+ const cleanupReconnect = setupReconnectPoller({
354
+ isConnected: () => connected,
355
+ isConnecting: () => connecting,
356
+ isShuttingDown: () => shuttingDown,
357
+ connect: () => { if (lastCtx?.mode === "tui") connect(lastCtx); },
358
+ });
359
+
360
+ pi.on("session_shutdown", () => {
361
+ // Server auto-exits when last pi session disconnects (onPiClose in pidiff-server.ts).
362
+ // No need to kill here — just disconnect and let the server decide.
363
+ shuttingDown = true;
364
+ cleanupReconnect();
365
+ if (cleanupHeartbeat) { cleanupHeartbeat(); cleanupHeartbeat = null; }
366
+ if (ws) { try { ws.close(); } catch {} ws = null; }
367
+ connected = false;
368
+ });
369
+ }