pi-orchestrator-config 4.2.1 → 4.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +52 -61
  2. package/agents/git-expert.md +5 -1
  3. package/agents/github-expert.md +4 -1
  4. package/extensions/acpx-provider/runtime-models.ts +18 -6
  5. package/extensions/cli-provider/agents/claude.ts +3 -2
  6. package/extensions/cli-provider/agents/cursor.ts +3 -5
  7. package/extensions/cli-provider/agents/gemini.ts +3 -2
  8. package/extensions/cli-provider/discover.ts +7 -7
  9. package/extensions/cli-provider/index.ts +5 -0
  10. package/extensions/cli-provider/runtime-models.ts +18 -6
  11. package/extensions/cli-provider/session-reaper.ts +5 -7
  12. package/extensions/cli-provider/sessions.ts +4 -0
  13. package/extensions/cli-provider/shared/discover-cache.ts +17 -176
  14. package/extensions/coms/coms-p2p.ts +1719 -423
  15. package/extensions/coms/coms-shared.ts +261 -39
  16. package/extensions/coms/coms-wrapper.ts +100 -62
  17. package/extensions/coms/index.ts +8 -4
  18. package/extensions/coms/mixed-turn.ts +26 -0
  19. package/extensions/coms/probe-socket.ts +42 -0
  20. package/extensions/coms/themeMap.ts +0 -1
  21. package/extensions/image-gen/image-gen.ts +19 -1
  22. package/extensions/orchestrator/async-agents.ts +509 -85
  23. package/extensions/orchestrator/cron.ts +14 -8
  24. package/extensions/orchestrator/dreaming.ts +31 -24
  25. package/extensions/orchestrator/enforcement-helpers.ts +459 -17
  26. package/extensions/orchestrator/enforcement-rules.ts +5 -4
  27. package/extensions/orchestrator/enforcement.ts +79 -25
  28. package/extensions/orchestrator/extended-autocomplete.ts +7 -0
  29. package/extensions/orchestrator/git-helpers.ts +2 -2
  30. package/extensions/orchestrator/index.ts +23 -10
  31. package/extensions/orchestrator/markdown-transformer.ts +313 -0
  32. package/extensions/orchestrator/memory-embeddings.ts +36 -34
  33. package/extensions/orchestrator/memory-provenance.ts +29 -25
  34. package/extensions/orchestrator/memory-scoring.ts +23 -18
  35. package/extensions/orchestrator/memory-tools.ts +10 -3
  36. package/extensions/orchestrator/overlay-dashboard.ts +17 -5
  37. package/extensions/orchestrator/pi-config-review-state.ts +44 -33
  38. package/extensions/orchestrator/project-settings.ts +220 -21
  39. package/extensions/orchestrator/review-porcelain.ts +4 -0
  40. package/extensions/orchestrator/review-ui.ts +11 -5
  41. package/extensions/orchestrator/rule-placeholders.ts +384 -1
  42. package/extensions/orchestrator/rules.ts +80 -132
  43. package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
  44. package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
  45. package/extensions/orchestrator/settings-tui.ts +636 -0
  46. package/extensions/orchestrator/state-jsonl.ts +431 -0
  47. package/extensions/orchestrator/subagent-tool.ts +105 -50
  48. package/extensions/orchestrator/sync-limit.ts +23 -0
  49. package/extensions/orchestrator/task-lifecycle.ts +77 -67
  50. package/extensions/orchestrator/utils.ts +8 -5
  51. package/extensions/pidash/pidash.ts +193 -156
  52. package/extensions/pidiff/pidiff.ts +64 -38
  53. package/extensions/pidiff/spawn-lock.ts +62 -0
  54. package/extensions/pitasks/index.ts +430 -0
  55. package/extensions/pitasks/reminders.ts +37 -0
  56. package/extensions/pitasks/task-store.ts +467 -0
  57. package/extensions/pitasks/task-tools.ts +375 -0
  58. package/extensions/pitasks/task-widget.ts +181 -0
  59. package/extensions/pitasks/types.ts +38 -0
  60. package/extensions/providers/acpx-driver.ts +12 -2
  61. package/extensions/providers/built-in-drivers.ts +6 -6
  62. package/extensions/providers/claude-driver.ts +1 -1
  63. package/extensions/providers/cursor-cli-driver.ts +1 -1
  64. package/extensions/providers/gemini-driver.ts +1 -1
  65. package/extensions/providers/index.ts +144 -26
  66. package/extensions/providers/initialized-guard.ts +27 -0
  67. package/extensions/providers/restore-default-model.ts +596 -0
  68. package/extensions/providers/session-shutdown.ts +39 -0
  69. package/extensions/shared/coms-active.ts +23 -0
  70. package/extensions/shared/create-runtime-provider.ts +2 -0
  71. package/extensions/shared/daemon-manager.ts +0 -8
  72. package/extensions/shared/file-logger.ts +132 -25
  73. package/extensions/shared/logger.ts +50 -0
  74. package/extensions/shared/models-dev.ts +421 -0
  75. package/extensions/shared/oneshot.ts +116 -0
  76. package/extensions/shared/resolve-binary.ts +191 -0
  77. package/extensions/shared/types.ts +3 -0
  78. package/package.json +7 -2
  79. package/prompts/coderabbit-rate-limit.md +2 -0
  80. package/rules/00-orchestrator-core.md +3 -0
  81. package/rules/10-agent-routing.md +1 -1
  82. package/rules/20-code-review-loop.md +38 -16
  83. package/rules/30-prompt-templates.md +4 -0
  84. package/rules/35-memory.md +3 -3
  85. package/rules/40-critical-rules.md +3 -0
  86. package/rules/55-coms-protocol.md +84 -18
  87. package/rules/60-task-tracking.md +2 -0
  88. package/settings-keys.json +481 -20
  89. package/templates/coms-feature-manager-prompt.md +4 -6
  90. package/extensions/coms/coms-net-server.ts +0 -1642
  91. package/extensions/coms/coms-net-wrapper.ts +0 -521
  92. package/extensions/coms/coms-net.ts +0 -1589
@@ -15,7 +15,6 @@ import { createRequire } from "node:module";
15
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
16
  import { hyperlink } from "@earendil-works/pi-tui";
17
17
  import {
18
- createLogger,
19
18
  checkHealth,
20
19
  ensureUiBuilt,
21
20
  spawnDaemon,
@@ -26,16 +25,21 @@ import {
26
25
  readLockfile,
27
26
  removeLockfile,
28
27
  } from "../shared/daemon-manager.js";
28
+ import { createLogger } from "../shared/logger.js";
29
29
  import { setupHeartbeat, setupReconnectPoller } from "../shared/ws-client.js";
30
30
  import { getSetting } from "../orchestrator/project-settings.js";
31
+ import { shouldSkipOneshotRegister } from "../shared/oneshot.js";
32
+ import { evaluateSpawnLock } from "./spawn-lock.js";
33
+
34
+ /** Re-export for callers/tests that import from pidiff. */
35
+ export { evaluateSpawnLock } from "./spawn-lock.js";
31
36
 
32
37
  const RECONNECT_INTERVAL_MS = 5000;
33
38
  const ICON_DIFF = "";
34
39
 
35
- const log = createLogger(
36
- path.join(process.env.HOME || "/tmp", ".pi", "pidiff-debug.log"),
37
- "ext",
38
- );
40
+ const log = createLogger("pidiff");
41
+ /** String logger for daemon-manager / ws-client APIs that take `(msg: string) => void`. */
42
+ const debugLog = (msg: string) => log.debug(msg);
39
43
 
40
44
  function isGitRepo(cwd: string): boolean {
41
45
  try { execFileSync("git", ["rev-parse", "--git-dir"], { cwd, stdio: "ignore" }); return true; } catch { return false; }
@@ -62,21 +66,22 @@ function isPortFree(port: number): Promise<boolean> {
62
66
  async function resolveSpawnPort(preferred: number | null | undefined): Promise<number> {
63
67
  if (!preferred) {
64
68
  const port = await findFreePort();
65
- log(`allocated new port: ${port}`);
69
+ log.debug(`allocated new port: ${port}`);
66
70
  return port;
67
71
  }
68
72
  if (await isPortFree(preferred)) {
69
- log(`reusing lockfile port: ${preferred}`);
73
+ log.debug(`reusing lockfile port: ${preferred}`);
70
74
  return preferred;
71
75
  }
72
- log(`lockfile port ${preferred} in use, allocating new port`);
76
+ log.debug(`lockfile port ${preferred} in use, allocating new port`);
73
77
  const port = await findFreePort();
74
- log(`allocated new port: ${port}`);
78
+ log.debug(`allocated new port: ${port}`);
75
79
  return port;
76
80
  }
77
81
 
78
82
  export function registerPidiff(pi: ExtensionAPI): void {
79
83
  if (process.env.PI_SUBAGENT_CHILD === "1") return;
84
+ if (shouldSkipOneshotRegister(log)) return;
80
85
 
81
86
  const projectCwd = process.cwd();
82
87
  const pidiffDisabled = !getSetting(projectCwd, "pidiff_enable");
@@ -123,21 +128,21 @@ export function registerPidiff(pi: ExtensionAPI): void {
123
128
  }
124
129
 
125
130
  function doSpawn(port: number, cwd: string): void {
126
- ensureUiBuilt(import.meta.url, "pidiff-ui", log);
131
+ ensureUiBuilt(import.meta.url, "pidiff-ui", debugLog);
127
132
  spawnDaemon({
128
133
  serverScript: "pidiff-server.ts",
129
134
  logFile: path.join(process.env.HOME || "/tmp", ".pi", "pidiff-server.log"),
130
135
  env: { PI_PIDIFF_PORT: String(port), PI_PIDIFF_CWD: cwd },
131
- log,
136
+ log: debugLog,
132
137
  });
133
138
  }
134
139
 
135
140
  async function connect(ctx: any) {
136
- log(`connect() called, connected=${connected}, connecting=${connecting}`);
141
+ log.debug(`connect() called, connected=${connected}, connecting=${connecting}`);
137
142
  if (connected || connecting || shuttingDown) return;
138
143
  connecting = true;
139
144
  lastCtx = ctx;
140
- if (!lockDir) { log("connect: lockDir not set (session_start not fired yet)"); connecting = false; return; }
145
+ if (!lockDir) { log.debug("connect: lockDir not set (session_start not fired yet)"); connecting = false; return; }
141
146
 
142
147
  // Try to discover port from lockfile
143
148
  let port = activePort;
@@ -150,12 +155,12 @@ export function registerPidiff(pi: ExtensionAPI): void {
150
155
  let running = false;
151
156
  if (port) {
152
157
  running = await checkHealth(port);
153
- log(`lockfile port=${port}, healthy=${running}`);
158
+ log.debug(`lockfile port=${port}, healthy=${running}`);
154
159
  }
155
160
 
156
161
  if (!running) {
157
162
  if (spawning) {
158
- log("daemon already spawning, waiting...");
163
+ log.debug("daemon already spawning, waiting...");
159
164
  connecting = false;
160
165
  return;
161
166
  }
@@ -165,8 +170,17 @@ export function registerPidiff(pi: ExtensionAPI): void {
165
170
  try {
166
171
  fs.writeFileSync(spawnLock, String(process.pid), { flag: "wx" });
167
172
  } catch {
168
- // Another session is already spawning — wait for lockfile to appear
169
- log("another session is spawning pidiff, waiting for lockfile...");
173
+ // Another session is already spawning — check if spawner is still alive
174
+ const lockDecision = evaluateSpawnLock(spawnLock, getSetting(ctx.cwd, "pidiff_stale_lock_timeout_ms"));
175
+ if (lockDecision.action === "recover" || lockDecision.action === "recover_pid_reuse") {
176
+ log.info("stale_spawn_lock", lockDecision.reason);
177
+ try { fs.unlinkSync(spawnLock); } catch {}
178
+ connecting = false;
179
+ setTimeout(() => connect(ctx), 500);
180
+ return;
181
+ }
182
+ // Spawner is alive — wait for it
183
+ log.debug("another session is spawning pidiff, waiting for lockfile...");
170
184
  for (let i = 0; i < 30; i++) {
171
185
  await new Promise(r => setTimeout(r, 1000));
172
186
  const lock = readLockfile(lockDir);
@@ -183,12 +197,12 @@ export function registerPidiff(pi: ExtensionAPI): void {
183
197
 
184
198
  spawning = true;
185
199
  try {
186
- log("spawning daemon...");
200
+ log.debug("spawning daemon...");
187
201
  // Reuse lockfile port if available — keeps URL stable across reload/restart
188
202
  port = await resolveSpawnPort(port);
189
203
  doSpawn(port, ctx.cwd);
190
- writeLockfile(lockDir, port, null, log);
191
- const ready = await waitForDaemon(port, 60, log);
204
+ writeLockfile(lockDir, port, null, debugLog);
205
+ const ready = await waitForDaemon(port, getSetting(process.cwd(), "pidiff_daemon_startup_timeout_s"), debugLog);
192
206
  if (!ready) { connecting = false; return; }
193
207
  } finally {
194
208
  spawning = false;
@@ -201,11 +215,11 @@ export function registerPidiff(pi: ExtensionAPI): void {
201
215
  try {
202
216
  const _require = createRequire(import.meta.url);
203
217
  const WebSocket = _require("ws");
204
- log(`connecting WebSocket to port ${activePort}...`);
218
+ log.debug(`connecting WebSocket to port ${activePort}...`);
205
219
  const wsClient = new WebSocket(`ws://127.0.0.1:${activePort}/ws/pi`);
206
220
 
207
221
  wsClient.on("open", () => {
208
- log("WebSocket connected");
222
+ log.debug("WebSocket connected");
209
223
  ws = wsClient;
210
224
  connected = true;
211
225
  connecting = false;
@@ -224,14 +238,14 @@ export function registerPidiff(pi: ExtensionAPI): void {
224
238
  }));
225
239
  } catch {
226
240
  // ctx may be stale if session was replaced during WebSocket connect
227
- log("skipped registration — ctx is stale");
241
+ log.debug("skipped registration — ctx is stale");
228
242
  }
229
243
 
230
244
  // Keepalive + dead connection detection
231
245
  if (cleanupHeartbeat) cleanupHeartbeat();
232
246
  cleanupHeartbeat = setupHeartbeat({
233
247
  ws: wsClient,
234
- log,
248
+ log: debugLog,
235
249
  onDead: () => {
236
250
  connected = false; ws = null;
237
251
  if (!shuttingDown && lastCtx) setTimeout(() => connect(lastCtx), 1000);
@@ -243,7 +257,7 @@ export function registerPidiff(pi: ExtensionAPI): void {
243
257
  try {
244
258
  const parsed = JSON.parse(data.toString());
245
259
  if (parsed.type === "publish-review" && parsed.comments?.length > 0) {
246
- log(`review received: ${parsed.comments.length} comments`);
260
+ log.debug(`review received: ${parsed.comments.length} comments`);
247
261
  const review = {
248
262
  source: "pidiff",
249
263
  type: "code-review",
@@ -262,25 +276,26 @@ export function registerPidiff(pi: ExtensionAPI): void {
262
276
  const message = `## pidiff: Code Review Comments\n\n\`\`\`json\n${JSON.stringify(review, null, 2)}\n\`\`\`\n\nPlease address these review comments.`;
263
277
  pi.sendUserMessage(message, { deliverAs: "followUp" });
264
278
  }
265
- } catch (e: any) { log(`message handler error: ${e.message}`); }
279
+ } catch (e: any) { log.debug(`message handler error: ${e.message}`); }
266
280
  });
267
281
 
268
282
  wsClient.on("close", () => {
269
- log("WebSocket closed");
283
+ log.debug("WebSocket closed");
270
284
  connected = false; ws = null; connecting = false;
271
285
  if (!shuttingDown) {
272
286
  setTimeout(() => { if (lastCtx && !shuttingDown) connect(lastCtx); }, RECONNECT_INTERVAL_MS);
273
287
  }
274
288
  });
275
289
 
276
- wsClient.on("error", (e: Error) => { log(`WebSocket error: ${e.message}`); });
290
+ wsClient.on("error", (e: Error) => { log.debug(`WebSocket error: ${e.message}`); });
277
291
  } catch (e: any) {
278
- log(`connect error: ${e.message}`);
292
+ log.debug(`connect error: ${e.message}`);
279
293
  connecting = false;
280
294
  }
281
295
  }
282
296
 
283
297
  // /pidiff command
298
+ let pidiffCmdHandler: any;
284
299
  pi.registerCommand("pidiff", {
285
300
  description: "Manage pidiff server — /pidiff start|stop|restart|status",
286
301
  getArgumentCompletions: (prefix: string) => {
@@ -292,7 +307,7 @@ export function registerPidiff(pi: ExtensionAPI): void {
292
307
  ];
293
308
  return items.filter(i => i.value.startsWith(prefix.toLowerCase()));
294
309
  },
295
- handler: async (args, ctx) => {
310
+ handler: pidiffCmdHandler = async (args: any, ctx: any) => {
296
311
  lastCtx = ctx;
297
312
  const cmd = (args || "").trim().toLowerCase();
298
313
 
@@ -300,8 +315,8 @@ export function registerPidiff(pi: ExtensionAPI): void {
300
315
  if (ws) { try { ws.close(); } catch {} ws = null; }
301
316
  connected = false;
302
317
  const pidFile = path.join(lockDir, "pidiff.pid");
303
- killDaemonByPid(pidFile, log);
304
- removeLockfile(lockDir, log);
318
+ killDaemonByPid(pidFile, debugLog);
319
+ removeLockfile(lockDir, debugLog);
305
320
  activePort = 0;
306
321
  if (ctx.hasUI) { ctx.ui.setStatus("8-diff", undefined); ctx.ui.notify("pidiff server stopped", "info"); }
307
322
  return;
@@ -318,9 +333,9 @@ export function registerPidiff(pi: ExtensionAPI): void {
318
333
  // Reuse lockfile port to keep URL stable — fall back if taken
319
334
  const port = await resolveSpawnPort(lock?.port);
320
335
  doSpawn(port, ctx.cwd);
321
- writeLockfile(lockDir, port, null, log);
336
+ writeLockfile(lockDir, port, null, debugLog);
322
337
  if (ctx.hasUI) ctx.ui.notify("Starting pidiff server...", "info");
323
- if (await waitForDaemon(port, 60, log)) {
338
+ if (await waitForDaemon(port, getSetting(process.cwd(), "pidiff_daemon_startup_timeout_s"), debugLog)) {
324
339
  activePort = port;
325
340
  connect(ctx);
326
341
  if (ctx.hasUI) ctx.ui.notify(`pidiff started at http://localhost:${activePort}`, "info");
@@ -337,15 +352,15 @@ export function registerPidiff(pi: ExtensionAPI): void {
337
352
  // Reuse existing port to keep URL stable (read before removeLockfile)
338
353
  const lock = readLockfile(lockDir);
339
354
  const preferredPort = activePort || lock?.port || 0;
340
- killDaemonByPid(pidFile, log);
341
- removeLockfile(lockDir, log);
355
+ killDaemonByPid(pidFile, debugLog);
356
+ removeLockfile(lockDir, debugLog);
342
357
  await new Promise(r => setTimeout(r, 1000));
343
358
  // Verify preferred port is free after kill — fall back if taken by another process
344
359
  const port = await resolveSpawnPort(preferredPort || null);
345
360
  doSpawn(port, ctx.cwd);
346
- writeLockfile(lockDir, port, null, log);
361
+ writeLockfile(lockDir, port, null, debugLog);
347
362
  if (ctx.hasUI) ctx.ui.notify("Restarting pidiff server...", "info");
348
- if (await waitForDaemon(port, 60, log)) {
363
+ if (await waitForDaemon(port, getSetting(process.cwd(), "pidiff_daemon_startup_timeout_s"), debugLog)) {
349
364
  activePort = port;
350
365
  connect(ctx);
351
366
  if (ctx.hasUI) ctx.ui.notify(`pidiff restarted at http://localhost:${activePort}`, "info");
@@ -365,6 +380,17 @@ export function registerPidiff(pi: ExtensionAPI): void {
365
380
  },
366
381
  });
367
382
 
383
+ // Expose handler to pidash for browser command dispatch
384
+ try {
385
+ if ((globalThis as any).__pidiff_pidash_listener) {
386
+ try { pi.events.removeListener("pidash:request-commands", (globalThis as any).__pidiff_pidash_listener); } catch {}
387
+ }
388
+ const registerWithPidash = () => pi.events.emit("pidash:register-command", { name: "pidiff", handler: pidiffCmdHandler });
389
+ (globalThis as any).__pidiff_pidash_listener = registerWithPidash;
390
+ registerWithPidash();
391
+ pi.events.on("pidash:request-commands", registerWithPidash);
392
+ } catch {}
393
+
368
394
  // Session lifecycle
369
395
  pi.on("session_start", async (_event, ctx) => {
370
396
  lastCtx = ctx;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Spawn-lock evaluation for pidiff daemon startup races.
3
+ * No pi-tui / ExtensionAPI deps — unit tests can import directly.
4
+ */
5
+ import * as fs from "node:fs";
6
+ import { createLogger } from "../shared/logger.js";
7
+
8
+ const log = createLogger("pidiff");
9
+
10
+ /**
11
+ * Evaluate a stale spawn lockfile and decide the recovery action.
12
+ * Exported for testing.
13
+ */
14
+ export function evaluateSpawnLock(
15
+ lockPath: string,
16
+ staleTimeoutMs: number,
17
+ ): { action: "wait" | "recover" | "recover_pid_reuse"; reason: string } {
18
+ log.debug("evaluate_spawn_lock", "lockPath", lockPath, "staleTimeoutMs", staleTimeoutMs);
19
+ const effectiveTimeout = Math.max(staleTimeoutMs, 1000); // Floor at 1s to prevent thrashing
20
+ let result: { action: "wait" | "recover" | "recover_pid_reuse"; reason: string };
21
+
22
+ try {
23
+ const content = fs.readFileSync(lockPath, "utf-8").trim();
24
+ const spawnerPid = /^\d+$/.test(content) ? parseInt(content, 10) : NaN;
25
+
26
+ if (!spawnerPid || isNaN(spawnerPid)) {
27
+ // No valid PID — check age
28
+ const lockAge = Date.now() - fs.statSync(lockPath).mtimeMs;
29
+ if (lockAge > effectiveTimeout) {
30
+ result = { action: "recover", reason: `no valid PID, age ${Math.round(lockAge / 1000)}s > timeout` };
31
+ } else {
32
+ result = { action: "wait", reason: "no valid PID but lock is young" };
33
+ }
34
+ } else {
35
+ let isAlive = false;
36
+ try {
37
+ process.kill(spawnerPid, 0);
38
+ isAlive = true;
39
+ } catch (e: any) {
40
+ // EPERM = process exists but we can't signal it — treat as alive
41
+ isAlive = e?.code === "EPERM";
42
+ }
43
+
44
+ if (!isAlive) {
45
+ result = { action: "recover", reason: `PID ${spawnerPid} is dead` };
46
+ } else {
47
+ // PID alive — check for PID reuse (lock age > timeout)
48
+ const lockAge = Date.now() - fs.statSync(lockPath).mtimeMs;
49
+ if (lockAge > effectiveTimeout) {
50
+ result = { action: "recover_pid_reuse", reason: `PID ${spawnerPid} alive but lock age ${Math.round(lockAge / 1000)}s > timeout (likely PID reuse)` };
51
+ } else {
52
+ result = { action: "wait", reason: `PID ${spawnerPid} alive, lock age ${Math.round(lockAge / 1000)}s within threshold` };
53
+ }
54
+ }
55
+ }
56
+ } catch {
57
+ result = { action: "recover", reason: "cannot read/stat lockfile" };
58
+ }
59
+
60
+ log.debug("evaluate_spawn_lock", result.action, result.reason);
61
+ return result;
62
+ }