gsd-pi 2.36.0 → 2.37.0-dev.68605cd

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 (71) hide show
  1. package/dist/resources/extensions/cmux/index.js +321 -0
  2. package/dist/resources/extensions/cmux/package.json +7 -0
  3. package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
  4. package/dist/resources/extensions/gsd/auto-loop.js +29 -4
  5. package/dist/resources/extensions/gsd/auto.js +58 -5
  6. package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
  7. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  8. package/dist/resources/extensions/gsd/commands.js +131 -34
  9. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  10. package/dist/resources/extensions/gsd/git-service.js +9 -1
  11. package/dist/resources/extensions/gsd/history.js +2 -1
  12. package/dist/resources/extensions/gsd/index.js +5 -0
  13. package/dist/resources/extensions/gsd/metrics.js +4 -2
  14. package/dist/resources/extensions/gsd/notifications.js +10 -1
  15. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  16. package/dist/resources/extensions/gsd/preferences-validation.js +29 -0
  17. package/dist/resources/extensions/gsd/preferences.js +3 -0
  18. package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  19. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
  20. package/dist/resources/extensions/gsd/session-lock.js +26 -6
  21. package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
  22. package/dist/resources/extensions/search-the-web/native-search.js +45 -4
  23. package/dist/resources/extensions/shared/format-utils.js +5 -41
  24. package/dist/resources/extensions/shared/layout-utils.js +46 -0
  25. package/dist/resources/extensions/shared/mod.js +2 -1
  26. package/dist/resources/extensions/shared/terminal.js +5 -0
  27. package/dist/resources/extensions/subagent/index.js +180 -60
  28. package/package.json +1 -1
  29. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
  31. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  32. package/packages/pi-coding-agent/package.json +1 -1
  33. package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
  34. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  35. package/packages/pi-tui/dist/terminal-image.js +4 -0
  36. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  37. package/packages/pi-tui/src/terminal-image.ts +5 -0
  38. package/pkg/package.json +1 -1
  39. package/src/resources/extensions/cmux/index.ts +384 -0
  40. package/src/resources/extensions/cmux/package.json +7 -0
  41. package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
  42. package/src/resources/extensions/gsd/auto-loop.ts +66 -6
  43. package/src/resources/extensions/gsd/auto.ts +77 -5
  44. package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
  45. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  46. package/src/resources/extensions/gsd/commands.ts +139 -32
  47. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  48. package/src/resources/extensions/gsd/git-service.ts +12 -1
  49. package/src/resources/extensions/gsd/history.ts +2 -1
  50. package/src/resources/extensions/gsd/index.ts +8 -0
  51. package/src/resources/extensions/gsd/metrics.ts +4 -2
  52. package/src/resources/extensions/gsd/notifications.ts +10 -1
  53. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  54. package/src/resources/extensions/gsd/preferences-validation.ts +26 -0
  55. package/src/resources/extensions/gsd/preferences.ts +4 -0
  56. package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  57. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
  58. package/src/resources/extensions/gsd/session-lock.ts +41 -6
  59. package/src/resources/extensions/gsd/templates/preferences.md +6 -0
  60. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +39 -1
  61. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
  62. package/src/resources/extensions/gsd/tests/cmux.test.ts +122 -0
  63. package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
  64. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
  65. package/src/resources/extensions/search-the-web/native-search.ts +50 -4
  66. package/src/resources/extensions/shared/format-utils.ts +5 -44
  67. package/src/resources/extensions/shared/layout-utils.ts +49 -0
  68. package/src/resources/extensions/shared/mod.ts +7 -4
  69. package/src/resources/extensions/shared/terminal.ts +5 -0
  70. package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
  71. package/src/resources/extensions/subagent/index.ts +236 -79
@@ -34,6 +34,8 @@ import {
34
34
  readIsolationMode,
35
35
  } from "./isolation.js";
36
36
  import { registerWorker, updateWorker } from "./worker-registry.js";
37
+ import { loadEffectiveGSDPreferences } from "../gsd/preferences.js";
38
+ import { CmuxClient, shellEscape } from "../cmux/index.js";
37
39
 
38
40
  const MAX_PARALLEL_TASKS = 8;
39
41
  const MAX_CONCURRENCY = 4;
@@ -257,6 +259,70 @@ function writePromptToTempFile(agentName: string, prompt: string): { dir: string
257
259
  return { dir: tmpDir, filePath };
258
260
  }
259
261
 
262
+ function buildSubagentProcessArgs(
263
+ agent: AgentConfig,
264
+ task: string,
265
+ tmpPromptPath: string | null,
266
+ ): string[] {
267
+ const args: string[] = ["--mode", "json", "-p", "--no-session"];
268
+ if (agent.model) args.push("--model", agent.model);
269
+ if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
270
+ if (tmpPromptPath) args.push("--append-system-prompt", tmpPromptPath);
271
+ args.push(`Task: ${task}`);
272
+ return args;
273
+ }
274
+
275
+ function processSubagentEventLine(
276
+ line: string,
277
+ currentResult: SingleResult,
278
+ emitUpdate: () => void,
279
+ ): void {
280
+ if (!line.trim()) return;
281
+ let event: any;
282
+ try {
283
+ event = JSON.parse(line);
284
+ } catch {
285
+ return;
286
+ }
287
+
288
+ if (event.type === "message_end" && event.message) {
289
+ const msg = event.message as Message;
290
+ currentResult.messages.push(msg);
291
+
292
+ if (msg.role === "assistant") {
293
+ currentResult.usage.turns++;
294
+ const usage = msg.usage;
295
+ if (usage) {
296
+ currentResult.usage.input += usage.input || 0;
297
+ currentResult.usage.output += usage.output || 0;
298
+ currentResult.usage.cacheRead += usage.cacheRead || 0;
299
+ currentResult.usage.cacheWrite += usage.cacheWrite || 0;
300
+ currentResult.usage.cost += usage.cost?.total || 0;
301
+ currentResult.usage.contextTokens = usage.totalTokens || 0;
302
+ }
303
+ if (!currentResult.model && msg.model) currentResult.model = msg.model;
304
+ if (msg.stopReason) currentResult.stopReason = msg.stopReason;
305
+ if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
306
+ }
307
+ emitUpdate();
308
+ }
309
+
310
+ if (event.type === "tool_result_end" && event.message) {
311
+ currentResult.messages.push(event.message as Message);
312
+ emitUpdate();
313
+ }
314
+ }
315
+
316
+ async function waitForFile(filePath: string, signal: AbortSignal | undefined, timeoutMs = 30 * 60 * 1000): Promise<boolean> {
317
+ const started = Date.now();
318
+ while (Date.now() - started < timeoutMs) {
319
+ if (signal?.aborted) return false;
320
+ if (fs.existsSync(filePath)) return true;
321
+ await new Promise((resolve) => setTimeout(resolve, 150));
322
+ }
323
+ return false;
324
+ }
325
+
260
326
  type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
261
327
 
262
328
  async function runSingleAgent(
@@ -286,10 +352,6 @@ async function runSingleAgent(
286
352
  };
287
353
  }
288
354
 
289
- const args: string[] = ["--mode", "json", "-p", "--no-session"];
290
- if (agent.model) args.push("--model", agent.model);
291
- if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
292
-
293
355
  let tmpPromptDir: string | null = null;
294
356
  let tmpPromptPath: string | null = null;
295
357
 
@@ -319,10 +381,8 @@ async function runSingleAgent(
319
381
  const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
320
382
  tmpPromptDir = tmp.dir;
321
383
  tmpPromptPath = tmp.filePath;
322
- args.push("--append-system-prompt", tmpPromptPath);
323
384
  }
324
-
325
- args.push(`Task: ${task}`);
385
+ const args = buildSubagentProcessArgs(agent, task, tmpPromptPath);
326
386
  let wasAborted = false;
327
387
 
328
388
  const exitCode = await new Promise<number>((resolve) => {
@@ -336,48 +396,11 @@ async function runSingleAgent(
336
396
  liveSubagentProcesses.add(proc);
337
397
  let buffer = "";
338
398
 
339
- const processLine = (line: string) => {
340
- if (!line.trim()) return;
341
- let event: any;
342
- try {
343
- event = JSON.parse(line);
344
- } catch {
345
- return;
346
- }
347
-
348
- if (event.type === "message_end" && event.message) {
349
- const msg = event.message as Message;
350
- currentResult.messages.push(msg);
351
-
352
- if (msg.role === "assistant") {
353
- currentResult.usage.turns++;
354
- const usage = msg.usage;
355
- if (usage) {
356
- currentResult.usage.input += usage.input || 0;
357
- currentResult.usage.output += usage.output || 0;
358
- currentResult.usage.cacheRead += usage.cacheRead || 0;
359
- currentResult.usage.cacheWrite += usage.cacheWrite || 0;
360
- currentResult.usage.cost += usage.cost?.total || 0;
361
- currentResult.usage.contextTokens = usage.totalTokens || 0;
362
- }
363
- if (!currentResult.model && msg.model) currentResult.model = msg.model;
364
- if (msg.stopReason) currentResult.stopReason = msg.stopReason;
365
- if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
366
- }
367
- emitUpdate();
368
- }
369
-
370
- if (event.type === "tool_result_end" && event.message) {
371
- currentResult.messages.push(event.message as Message);
372
- emitUpdate();
373
- }
374
- };
375
-
376
399
  proc.stdout.on("data", (data) => {
377
400
  buffer += data.toString();
378
401
  const lines = buffer.split("\n");
379
402
  buffer = lines.pop() || "";
380
- for (const line of lines) processLine(line);
403
+ for (const line of lines) processSubagentEventLine(line, currentResult, emitUpdate);
381
404
  });
382
405
 
383
406
  proc.stderr.on("data", (data) => {
@@ -386,7 +409,7 @@ async function runSingleAgent(
386
409
 
387
410
  proc.on("close", (code) => {
388
411
  liveSubagentProcesses.delete(proc);
389
- if (buffer.trim()) processLine(buffer);
412
+ if (buffer.trim()) processSubagentEventLine(buffer, currentResult, emitUpdate);
390
413
  resolve(code ?? 0);
391
414
  });
392
415
 
@@ -427,6 +450,120 @@ async function runSingleAgent(
427
450
  }
428
451
  }
429
452
 
453
+ async function runSingleAgentInCmuxSplit(
454
+ cmuxClient: CmuxClient,
455
+ direction: "right" | "down",
456
+ defaultCwd: string,
457
+ agents: AgentConfig[],
458
+ agentName: string,
459
+ task: string,
460
+ cwd: string | undefined,
461
+ step: number | undefined,
462
+ signal: AbortSignal | undefined,
463
+ onUpdate: OnUpdateCallback | undefined,
464
+ makeDetails: (results: SingleResult[]) => SubagentDetails,
465
+ ): Promise<SingleResult> {
466
+ const agent = agents.find((a) => a.name === agentName);
467
+ if (!agent) {
468
+ return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
469
+ }
470
+
471
+ let tmpPromptDir: string | null = null;
472
+ let tmpPromptPath: string | null = null;
473
+ let tmpOutputDir: string | null = null;
474
+
475
+ const currentResult: SingleResult = {
476
+ agent: agentName,
477
+ agentSource: agent.source,
478
+ task,
479
+ exitCode: 0,
480
+ messages: [],
481
+ stderr: "",
482
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
483
+ model: agent.model,
484
+ step,
485
+ };
486
+
487
+ const emitUpdate = () => {
488
+ if (onUpdate) {
489
+ onUpdate({
490
+ content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
491
+ details: makeDetails([currentResult]),
492
+ });
493
+ }
494
+ };
495
+
496
+ try {
497
+ if (agent.systemPrompt.trim()) {
498
+ const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
499
+ tmpPromptDir = tmp.dir;
500
+ tmpPromptPath = tmp.filePath;
501
+ }
502
+ tmpOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-cmux-"));
503
+ const stdoutPath = path.join(tmpOutputDir, "stdout.jsonl");
504
+ const stderrPath = path.join(tmpOutputDir, "stderr.log");
505
+ const exitPath = path.join(tmpOutputDir, "exit.code");
506
+ const cmuxSurfaceId = await cmuxClient.createSplit(direction);
507
+ if (!cmuxSurfaceId) {
508
+ return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
509
+ }
510
+
511
+ const bundledPaths = (process.env.GSD_BUNDLED_EXTENSION_PATHS ?? "").split(path.delimiter).map((s) => s.trim()).filter(Boolean);
512
+ const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
513
+ const processArgs = [process.env.GSD_BIN_PATH!, ...extensionArgs, ...buildSubagentProcessArgs(agent, task, tmpPromptPath)];
514
+ const innerScript = [
515
+ `cd ${shellEscape(cwd ?? defaultCwd)}`,
516
+ "set -o pipefail",
517
+ `${shellEscape(process.execPath)} ${processArgs.map(shellEscape).join(" ")} 2> >(tee ${shellEscape(stderrPath)} >&2) | tee ${shellEscape(stdoutPath)}`,
518
+ "status=${PIPESTATUS[0]}",
519
+ `printf '%s' "$status" > ${shellEscape(exitPath)}`,
520
+ ].join("; ");
521
+
522
+ const sent = await cmuxClient.sendSurface(cmuxSurfaceId, `bash -lc ${shellEscape(innerScript)}`);
523
+ if (!sent) {
524
+ return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
525
+ }
526
+
527
+ const finished = await waitForFile(exitPath, signal);
528
+ if (!finished) {
529
+ currentResult.exitCode = 1;
530
+ currentResult.stderr = "cmux split execution timed out or was aborted";
531
+ return currentResult;
532
+ }
533
+
534
+ if (fs.existsSync(stdoutPath)) {
535
+ const stdout = fs.readFileSync(stdoutPath, "utf-8");
536
+ for (const line of stdout.split("\n")) {
537
+ processSubagentEventLine(line, currentResult, emitUpdate);
538
+ }
539
+ }
540
+ if (fs.existsSync(stderrPath)) {
541
+ currentResult.stderr = fs.readFileSync(stderrPath, "utf-8");
542
+ }
543
+ currentResult.exitCode = Number.parseInt(fs.readFileSync(exitPath, "utf-8").trim() || "1", 10) || 0;
544
+ return currentResult;
545
+ } finally {
546
+ if (tmpPromptPath)
547
+ try {
548
+ fs.unlinkSync(tmpPromptPath);
549
+ } catch {
550
+ /* ignore */
551
+ }
552
+ if (tmpPromptDir)
553
+ try {
554
+ fs.rmdirSync(tmpPromptDir);
555
+ } catch {
556
+ /* ignore */
557
+ }
558
+ if (tmpOutputDir)
559
+ try {
560
+ fs.rmSync(tmpOutputDir, { recursive: true, force: true });
561
+ } catch {
562
+ /* ignore */
563
+ }
564
+ }
565
+ }
566
+
430
567
  const TaskItem = Type.Object({
431
568
  agent: Type.String({ description: "Name of the agent to invoke" }),
432
569
  task: Type.String({ description: "Task to delegate to the agent" }),
@@ -511,6 +648,8 @@ export default function (pi: ExtensionAPI) {
511
648
  const discovery = discoverAgents(ctx.cwd, agentScope);
512
649
  const agents = discovery.agents;
513
650
  const confirmProjectAgents = params.confirmProjectAgents ?? false;
651
+ const cmuxClient = CmuxClient.fromPreferences(loadEffectiveGSDPreferences()?.preferences);
652
+ const cmuxSplitsEnabled = cmuxClient.getConfig().splits;
514
653
 
515
654
  // Resolve isolation mode
516
655
  const isolationMode = readIsolationMode();
@@ -669,28 +808,26 @@ export default function (pi: ExtensionAPI) {
669
808
  const batchSize = params.tasks.length;
670
809
  const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
671
810
  const workerId = registerWorker(t.agent, t.task, index, batchSize, batchId);
672
- let result = await runSingleAgent(
673
- ctx.cwd,
674
- agents,
675
- t.agent,
676
- t.task,
677
- t.cwd,
678
- undefined,
679
- signal,
680
- // Per-task update callback
681
- (partial) => {
682
- if (partial.details?.results[0]) {
683
- allResults[index] = partial.details.results[0];
684
- emitParallelUpdate();
685
- }
686
- },
687
- makeDetails("parallel"),
688
- );
689
-
690
- // Auto-retry failed tasks (likely API rate limit or transient error)
691
- const isFailed = result.exitCode !== 0 || (result.messages.length === 0 && !signal?.aborted);
692
- if (isFailed && MAX_RETRIES > 0 && !signal?.aborted) {
693
- result = await runSingleAgent(
811
+ const runTask = () => cmuxSplitsEnabled
812
+ ? runSingleAgentInCmuxSplit(
813
+ cmuxClient,
814
+ index % 2 === 0 ? "right" : "down",
815
+ ctx.cwd,
816
+ agents,
817
+ t.agent,
818
+ t.task,
819
+ t.cwd,
820
+ undefined,
821
+ signal,
822
+ (partial) => {
823
+ if (partial.details?.results[0]) {
824
+ allResults[index] = partial.details.results[0];
825
+ emitParallelUpdate();
826
+ }
827
+ },
828
+ makeDetails("parallel"),
829
+ )
830
+ : runSingleAgent(
694
831
  ctx.cwd,
695
832
  agents,
696
833
  t.agent,
@@ -706,6 +843,12 @@ export default function (pi: ExtensionAPI) {
706
843
  },
707
844
  makeDetails("parallel"),
708
845
  );
846
+ let result = await runTask();
847
+
848
+ // Auto-retry failed tasks (likely API rate limit or transient error)
849
+ const isFailed = result.exitCode !== 0 || (result.messages.length === 0 && !signal?.aborted);
850
+ if (isFailed && MAX_RETRIES > 0 && !signal?.aborted) {
851
+ result = await runTask();
709
852
  }
710
853
 
711
854
  updateWorker(workerId, result.exitCode === 0 ? "completed" : "failed");
@@ -744,17 +887,31 @@ export default function (pi: ExtensionAPI) {
744
887
  isolation = await createIsolation(effectiveCwd, taskId, isolationMode);
745
888
  }
746
889
 
747
- const result = await runSingleAgent(
748
- ctx.cwd,
749
- agents,
750
- params.agent,
751
- params.task,
752
- isolation ? isolation.workDir : params.cwd,
753
- undefined,
754
- signal,
755
- onUpdate,
756
- makeDetails("single"),
757
- );
890
+ const result = cmuxSplitsEnabled
891
+ ? await runSingleAgentInCmuxSplit(
892
+ cmuxClient,
893
+ "right",
894
+ ctx.cwd,
895
+ agents,
896
+ params.agent,
897
+ params.task,
898
+ isolation ? isolation.workDir : params.cwd,
899
+ undefined,
900
+ signal,
901
+ onUpdate,
902
+ makeDetails("single"),
903
+ )
904
+ : await runSingleAgent(
905
+ ctx.cwd,
906
+ agents,
907
+ params.agent,
908
+ params.task,
909
+ isolation ? isolation.workDir : params.cwd,
910
+ undefined,
911
+ signal,
912
+ onUpdate,
913
+ makeDetails("single"),
914
+ );
758
915
 
759
916
  // Capture and merge delta if isolated
760
917
  if (isolation) {