gsd-pi 2.37.0 → 2.37.1-dev.49503be

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 (93) hide show
  1. package/README.md +21 -20
  2. package/dist/onboarding.js +1 -0
  3. package/dist/resources/extensions/cmux/package.json +7 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +54 -1
  5. package/dist/resources/extensions/gsd/auto-loop.js +18 -4
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
  7. package/dist/resources/extensions/gsd/auto-prompts.js +55 -0
  8. package/dist/resources/extensions/gsd/auto-recovery.js +19 -1
  9. package/dist/resources/extensions/gsd/auto.js +42 -5
  10. package/dist/resources/extensions/gsd/commands.js +80 -33
  11. package/dist/resources/extensions/gsd/files.js +41 -0
  12. package/dist/resources/extensions/gsd/git-service.js +9 -1
  13. package/dist/resources/extensions/gsd/history.js +2 -1
  14. package/dist/resources/extensions/gsd/metrics.js +4 -2
  15. package/dist/resources/extensions/gsd/preferences-types.js +2 -1
  16. package/dist/resources/extensions/gsd/preferences-validation.js +42 -0
  17. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  18. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  19. package/dist/resources/extensions/gsd/session-lock.js +26 -6
  20. package/dist/resources/extensions/shared/format-utils.js +5 -41
  21. package/dist/resources/extensions/shared/layout-utils.js +46 -0
  22. package/dist/resources/extensions/shared/mod.js +2 -1
  23. package/package.json +2 -1
  24. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  25. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  26. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  27. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  28. package/packages/pi-ai/dist/models.generated.js +172 -0
  29. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  30. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  31. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  32. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  33. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  34. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  35. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  36. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  37. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  38. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  39. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  40. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  41. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  42. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  43. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  44. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  45. package/packages/pi-ai/dist/types.d.ts +2 -2
  46. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  47. package/packages/pi-ai/dist/types.js.map +1 -1
  48. package/packages/pi-ai/package.json +1 -0
  49. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  50. package/packages/pi-ai/src/models.generated.ts +172 -0
  51. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  52. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  53. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  54. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  55. package/packages/pi-ai/src/types.ts +2 -0
  56. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  57. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
  58. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  59. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  61. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  62. package/packages/pi-coding-agent/package.json +1 -1
  63. package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
  64. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  65. package/pkg/package.json +1 -1
  66. package/src/resources/extensions/cmux/package.json +7 -0
  67. package/src/resources/extensions/gsd/auto-dispatch.ts +78 -0
  68. package/src/resources/extensions/gsd/auto-loop.ts +24 -6
  69. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
  70. package/src/resources/extensions/gsd/auto-prompts.ts +68 -0
  71. package/src/resources/extensions/gsd/auto-recovery.ts +18 -0
  72. package/src/resources/extensions/gsd/auto.ts +56 -5
  73. package/src/resources/extensions/gsd/commands.ts +85 -31
  74. package/src/resources/extensions/gsd/files.ts +45 -0
  75. package/src/resources/extensions/gsd/git-service.ts +12 -1
  76. package/src/resources/extensions/gsd/history.ts +2 -1
  77. package/src/resources/extensions/gsd/metrics.ts +4 -2
  78. package/src/resources/extensions/gsd/preferences-types.ts +5 -1
  79. package/src/resources/extensions/gsd/preferences-validation.ts +41 -0
  80. package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  81. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  82. package/src/resources/extensions/gsd/session-lock.ts +41 -6
  83. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +37 -1
  84. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
  85. package/src/resources/extensions/gsd/tests/cmux.test.ts +25 -1
  86. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +367 -0
  87. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  88. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
  89. package/src/resources/extensions/gsd/types.ts +41 -0
  90. package/src/resources/extensions/shared/format-utils.ts +5 -44
  91. package/src/resources/extensions/shared/layout-utils.ts +49 -0
  92. package/src/resources/extensions/shared/mod.ts +7 -4
  93. package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
@@ -15,6 +15,7 @@ import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
15
15
  import type { AutoSession } from "./auto/session.js";
16
16
  import { NEW_SESSION_TIMEOUT_MS } from "./auto/session.js";
17
17
  import type { GSDPreferences } from "./preferences.js";
18
+ import type { SessionLockStatus } from "./session-lock.js";
18
19
  import type { GSDState } from "./types.js";
19
20
  import type { CloseoutOptions } from "./auto-unit-closeout.js";
20
21
  import type { PostUnitContext } from "./auto-post-unit.js";
@@ -307,7 +308,7 @@ export interface LoopDeps {
307
308
  checkResourcesStale: (version: string | null) => string | null;
308
309
 
309
310
  // Session lock
310
- validateSessionLock: (basePath: string) => boolean;
311
+ validateSessionLock: (basePath: string) => SessionLockStatus;
311
312
  updateSessionLock: (
312
313
  basePath: string,
313
314
  unitType: string,
@@ -315,7 +316,10 @@ export interface LoopDeps {
315
316
  completedUnits: number,
316
317
  sessionFile?: string,
317
318
  ) => void;
318
- handleLostSessionLock: (ctx?: ExtensionContext) => void;
319
+ handleLostSessionLock: (
320
+ ctx?: ExtensionContext,
321
+ lockStatus?: SessionLockStatus,
322
+ ) => void;
319
323
 
320
324
  // Milestone transition functions
321
325
  sendDesktopNotification: (
@@ -559,10 +563,24 @@ export async function autoLoop(
559
563
  try {
560
564
  // ── Blanket try/catch: one bad iteration must not kill the session
561
565
 
562
- if (deps.lockBase() && !deps.validateSessionLock(deps.lockBase())) {
563
- deps.handleLostSessionLock(ctx);
564
- debugLog("autoLoop", { phase: "exit", reason: "session-lock-lost" });
565
- break;
566
+ const sessionLockBase = deps.lockBase();
567
+ if (sessionLockBase) {
568
+ const lockStatus = deps.validateSessionLock(sessionLockBase);
569
+ if (!lockStatus.valid) {
570
+ debugLog("autoLoop", {
571
+ phase: "session-lock-invalid",
572
+ reason: lockStatus.failureReason ?? "unknown",
573
+ existingPid: lockStatus.existingPid,
574
+ expectedPid: lockStatus.expectedPid,
575
+ });
576
+ deps.handleLostSessionLock(ctx, lockStatus);
577
+ debugLog("autoLoop", {
578
+ phase: "exit",
579
+ reason: "session-lock-lost",
580
+ detail: lockStatus.failureReason ?? "unknown",
581
+ });
582
+ break;
583
+ }
566
584
  }
567
585
 
568
586
  // ── Phase 1: Pre-dispatch ───────────────────────────────────────────
@@ -217,6 +217,20 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
217
217
  }
218
218
  }
219
219
 
220
+ // Reactive state cleanup on slice completion
221
+ if (s.currentUnit.type === "complete-slice") {
222
+ try {
223
+ const parts = s.currentUnit.id.split("/");
224
+ const [mid, sid] = parts;
225
+ if (mid && sid) {
226
+ const { clearReactiveState } = await import("./reactive-graph.js");
227
+ clearReactiveState(s.basePath, mid, sid);
228
+ }
229
+ } catch {
230
+ // Non-fatal
231
+ }
232
+ }
233
+
220
234
  // Post-triage: execute actionable resolutions
221
235
  if (s.currentUnit.type === "triage-captures") {
222
236
  try {
@@ -1234,6 +1234,74 @@ export async function buildReassessRoadmapPrompt(
1234
1234
  });
1235
1235
  }
1236
1236
 
1237
+ // ─── Reactive Execute Prompt ──────────────────────────────────────────────
1238
+
1239
+ export async function buildReactiveExecutePrompt(
1240
+ mid: string, midTitle: string, sid: string, sTitle: string,
1241
+ readyTaskIds: string[], base: string,
1242
+ ): Promise<string> {
1243
+ const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
1244
+
1245
+ // Build graph for context
1246
+ const taskIO = await loadSliceTaskIO(base, mid, sid);
1247
+ const graph = deriveTaskGraph(taskIO);
1248
+ const metrics = graphMetrics(graph);
1249
+
1250
+ // Build graph context section
1251
+ const graphLines: string[] = [];
1252
+ for (const node of graph) {
1253
+ const status = node.done ? "✅ done" : readyTaskIds.includes(node.id) ? "🟢 ready" : "⏳ waiting";
1254
+ const deps = node.dependsOn.length > 0 ? ` (depends on: ${node.dependsOn.join(", ")})` : "";
1255
+ graphLines.push(`- **${node.id}: ${node.title}** — ${status}${deps}`);
1256
+ if (node.outputFiles.length > 0) {
1257
+ graphLines.push(` - Outputs: ${node.outputFiles.map(f => `\`${f}\``).join(", ")}`);
1258
+ }
1259
+ }
1260
+ const graphContext = [
1261
+ `Tasks: ${metrics.taskCount}, Edges: ${metrics.edgeCount}, Ready: ${metrics.readySetSize}`,
1262
+ "",
1263
+ ...graphLines,
1264
+ ].join("\n");
1265
+
1266
+ // Build individual subagent prompts for each ready task
1267
+ const subagentSections: string[] = [];
1268
+ const readyTaskListLines: string[] = [];
1269
+
1270
+ for (const tid of readyTaskIds) {
1271
+ const node = graph.find((n) => n.id === tid);
1272
+ const tTitle = node?.title ?? tid;
1273
+ readyTaskListLines.push(`- **${tid}: ${tTitle}**`);
1274
+
1275
+ // Build a full execute-task prompt for this task (reuse existing builder)
1276
+ const taskPrompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base);
1277
+
1278
+ subagentSections.push([
1279
+ `### ${tid}: ${tTitle}`,
1280
+ "",
1281
+ "Use this as the prompt for a `subagent` call:",
1282
+ "",
1283
+ "```",
1284
+ taskPrompt,
1285
+ "```",
1286
+ ].join("\n"));
1287
+ }
1288
+
1289
+ const inlinedTemplates = inlineTemplate("task-summary", "Task Summary");
1290
+
1291
+ return loadPrompt("reactive-execute", {
1292
+ workingDirectory: base,
1293
+ milestoneId: mid,
1294
+ milestoneTitle: midTitle,
1295
+ sliceId: sid,
1296
+ sliceTitle: sTitle,
1297
+ graphContext,
1298
+ readyTaskCount: String(readyTaskIds.length),
1299
+ readyTaskList: readyTaskListLines.join("\n"),
1300
+ subagentPrompts: subagentSections.join("\n\n---\n\n"),
1301
+ inlinedTemplates,
1302
+ });
1303
+ }
1304
+
1237
1305
  export async function buildRewriteDocsPrompt(
1238
1306
  mid: string, midTitle: string,
1239
1307
  activeSlice: { id: string; title: string } | null,
@@ -26,6 +26,7 @@ import {
26
26
  resolveSlicePath,
27
27
  resolveSliceFile,
28
28
  resolveTasksDir,
29
+ resolveTaskFiles,
29
30
  relMilestoneFile,
30
31
  relSliceFile,
31
32
  relSlicePath,
@@ -110,6 +111,9 @@ export function resolveExpectedArtifactPath(
110
111
  }
111
112
  case "rewrite-docs":
112
113
  return null;
114
+ case "reactive-execute":
115
+ // Reactive execute produces multiple task summaries — verified separately
116
+ return null;
113
117
  default:
114
118
  return null;
115
119
  }
@@ -148,6 +152,20 @@ export function verifyExpectedArtifact(
148
152
  return !content.includes("**Scope:** active");
149
153
  }
150
154
 
155
+ // Reactive-execute: verify that at least one new task summary was written.
156
+ // The unitId is "{mid}/{sid}/reactive" — extract mid and sid to check.
157
+ if (unitType === "reactive-execute") {
158
+ const parts = unitId.split("/");
159
+ const mid = parts[0];
160
+ const sid = parts[1];
161
+ if (!mid || !sid) return false;
162
+ const tDir = resolveTasksDir(base, mid, sid);
163
+ if (!tDir) return false;
164
+ const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
165
+ // At least one summary file should exist
166
+ return summaryFiles.length > 0;
167
+ }
168
+
151
169
  const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
152
170
  // For unit types with no verifiable artifact (null path), the parent directory
153
171
  // is missing on disk — treat as stale completion state so the key gets evicted (#313).
@@ -47,10 +47,11 @@ import {
47
47
  } from "./crash-recovery.js";
48
48
  import {
49
49
  acquireSessionLock,
50
- validateSessionLock,
50
+ getSessionLockStatus,
51
51
  releaseSessionLock,
52
52
  updateSessionLock,
53
53
  } from "./session-lock.js";
54
+ import type { SessionLockStatus } from "./session-lock.js";
54
55
  import {
55
56
  clearUnitRuntimeRecord,
56
57
  inspectExecuteTaskDurability,
@@ -417,6 +418,38 @@ export function stopAutoRemote(projectRoot: string): {
417
418
  }
418
419
  }
419
420
 
421
+ /**
422
+ * Check if a remote auto-mode session is running (from a different process).
423
+ * Reads the crash lock, checks PID liveness, and returns session details.
424
+ * Used by the guard in commands.ts to prevent bare /gsd, /gsd next, and
425
+ * /gsd auto from stealing the session lock.
426
+ */
427
+ export function checkRemoteAutoSession(projectRoot: string): {
428
+ running: boolean;
429
+ pid?: number;
430
+ unitType?: string;
431
+ unitId?: string;
432
+ startedAt?: string;
433
+ completedUnits?: number;
434
+ } {
435
+ const lock = readCrashLock(projectRoot);
436
+ if (!lock) return { running: false };
437
+
438
+ if (!isLockProcessAlive(lock)) {
439
+ // Stale lock from a dead process — not a live remote session
440
+ return { running: false };
441
+ }
442
+
443
+ return {
444
+ running: true,
445
+ pid: lock.pid,
446
+ unitType: lock.unitType,
447
+ unitId: lock.unitId,
448
+ startedAt: lock.startedAt,
449
+ completedUnits: lock.completedUnits,
450
+ };
451
+ }
452
+
420
453
  export function isStepMode(): boolean {
421
454
  return s.stepMode;
422
455
  }
@@ -461,15 +494,33 @@ function buildSnapshotOpts(
461
494
  };
462
495
  }
463
496
 
464
- function handleLostSessionLock(ctx?: ExtensionContext): void {
465
- debugLog("session-lock-lost", { lockBase: lockBase() });
497
+ function handleLostSessionLock(
498
+ ctx?: ExtensionContext,
499
+ lockStatus?: SessionLockStatus,
500
+ ): void {
501
+ debugLog("session-lock-lost", {
502
+ lockBase: lockBase(),
503
+ reason: lockStatus?.failureReason,
504
+ existingPid: lockStatus?.existingPid,
505
+ expectedPid: lockStatus?.expectedPid,
506
+ });
466
507
  s.active = false;
467
508
  s.paused = false;
468
509
  clearUnitTimeout();
469
510
  deregisterSigtermHandler();
470
511
  clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
512
+ const message =
513
+ lockStatus?.failureReason === "pid-mismatch"
514
+ ? lockStatus.existingPid
515
+ ? `Session lock moved to PID ${lockStatus.existingPid} — another GSD process appears to have taken over. Stopping gracefully.`
516
+ : "Session lock moved to a different process — another GSD process appears to have taken over. Stopping gracefully."
517
+ : lockStatus?.failureReason === "missing-metadata"
518
+ ? "Session lock metadata disappeared, so ownership could not be confirmed. Stopping gracefully."
519
+ : lockStatus?.failureReason === "compromised"
520
+ ? "Session lock was compromised or invalidated during heartbeat checks; takeover was not confirmed. Stopping gracefully."
521
+ : "Session lock lost. Stopping gracefully.";
471
522
  ctx?.ui.notify(
472
- "Session lock lost — another GSD process appears to have taken over. Stopping gracefully.",
523
+ message,
473
524
  "error",
474
525
  );
475
526
  ctx?.ui.setStatus("gsd-auto", undefined);
@@ -736,7 +787,7 @@ function buildLoopDeps(): LoopDeps {
736
787
  checkResourcesStale,
737
788
 
738
789
  // Session lock
739
- validateSessionLock,
790
+ validateSessionLock: getSessionLockStatus,
740
791
  updateSessionLock,
741
792
  handleLostSessionLock,
742
793
 
@@ -15,7 +15,7 @@ import { deriveState } from "./state.js";
15
15
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
16
16
  import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
17
17
  import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
18
- import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
18
+ import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote, checkRemoteAutoSession } from "./auto.js";
19
19
  import { dispatchDirectPhase } from "./auto-direct-dispatch.js";
20
20
  import { resolveProjectRoot } from "./worktree.js";
21
21
  import { assertSafeDirectory } from "./validate-directory.js";
@@ -50,6 +50,7 @@ import { handleLogs } from "./commands-logs.js";
50
50
  import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
51
51
  import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
52
52
  import { handleCmux } from "./commands-cmux.js";
53
+ import { showNextAction } from "../shared/mod.js";
53
54
 
54
55
 
55
56
  /** Resolve the effective project root, accounting for worktree paths. */
@@ -72,36 +73,88 @@ export function projectRoot(): string {
72
73
  }
73
74
 
74
75
  /**
75
- * Check if another process holds the auto-mode session lock.
76
- * Returns the lock data if a remote session is alive, null otherwise.
76
+ * Guard against starting auto-mode when a remote session is already running.
77
+ * Returns true if the caller should proceed with startAuto, false if handled.
77
78
  */
78
- function getRemoteAutoSession(basePath: string): { pid: number } | null {
79
- const lockData = readSessionLockData(basePath);
80
- if (!lockData) return null;
81
- if (lockData.pid === process.pid) return null;
82
- if (!isSessionLockProcessAlive(lockData)) return null;
83
- return { pid: lockData.pid };
84
- }
79
+ async function guardRemoteSession(
80
+ ctx: ExtensionCommandContext,
81
+ pi: ExtensionAPI,
82
+ ): Promise<boolean> {
83
+ // Local session already active — proceed (startAuto handles re-entrant calls)
84
+ if (isAutoActive() || isAutoPaused()) return true;
85
+
86
+ const remote = checkRemoteAutoSession(projectRoot());
87
+ if (!remote.running || !remote.pid) return true;
88
+
89
+ const unitLabel = remote.unitType && remote.unitId
90
+ ? `${remote.unitType} (${remote.unitId})`
91
+ : "unknown unit";
92
+ const unitsMsg = remote.completedUnits != null
93
+ ? `${remote.completedUnits} units completed`
94
+ : "";
95
+
96
+ const choice = await showNextAction(ctx, {
97
+ title: `Auto-mode is running in another terminal (PID ${remote.pid})`,
98
+ summary: [
99
+ `Currently executing: ${unitLabel}`,
100
+ ...(unitsMsg ? [unitsMsg] : []),
101
+ ...(remote.startedAt ? [`Started: ${remote.startedAt}`] : []),
102
+ ],
103
+ actions: [
104
+ {
105
+ id: "status",
106
+ label: "View status",
107
+ description: "Show the current GSD progress dashboard.",
108
+ recommended: true,
109
+ },
110
+ {
111
+ id: "steer",
112
+ label: "Steer the session",
113
+ description: "Use /gsd steer <instruction> to redirect the running session.",
114
+ },
115
+ {
116
+ id: "stop",
117
+ label: "Stop remote session",
118
+ description: `Send SIGTERM to PID ${remote.pid} to stop it gracefully.`,
119
+ },
120
+ {
121
+ id: "force",
122
+ label: "Force start (steal lock)",
123
+ description: "Start a new session, terminating the existing one.",
124
+ },
125
+ ],
126
+ notYetMessage: "Run /gsd when ready.",
127
+ });
85
128
 
86
- /**
87
- * Show a steering menu when auto-mode is running in another process.
88
- * Returns true if a remote session was detected (caller should return early).
89
- */
90
- function notifyRemoteAutoActive(ctx: ExtensionCommandContext, basePath: string): boolean {
91
- const remote = getRemoteAutoSession(basePath);
92
- if (!remote) return false;
93
- ctx.ui.notify(
94
- `Auto-mode is running in another process (PID ${remote.pid}).\n` +
95
- `Use these commands to interact with it:\n` +
96
- ` /gsd status — check progress\n` +
97
- ` /gsd discuss — discuss architecture decisions\n` +
98
- ` /gsd queue — queue the next milestone\n` +
99
- ` /gsd steer — apply an override to active work\n` +
100
- ` /gsd capture — fire-and-forget thought\n` +
101
- ` /gsd stop stop auto-mode`,
102
- "warning",
103
- );
104
- return true;
129
+ if (choice === "status") {
130
+ await handleStatus(ctx);
131
+ return false;
132
+ }
133
+ if (choice === "steer") {
134
+ ctx.ui.notify(
135
+ "Use /gsd steer <instruction> to redirect the running auto-mode session.\n" +
136
+ "Example: /gsd steer Use Postgres instead of SQLite",
137
+ "info",
138
+ );
139
+ return false;
140
+ }
141
+ if (choice === "stop") {
142
+ const result = stopAutoRemote(projectRoot());
143
+ if (result.found) {
144
+ ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
145
+ } else if (result.error) {
146
+ ctx.ui.notify(`Failed to stop remote auto-mode: ${result.error}`, "error");
147
+ } else {
148
+ ctx.ui.notify("Remote session is no longer running.", "info");
149
+ }
150
+ return false;
151
+ }
152
+ if (choice === "force") {
153
+ return true; // Proceed — startAuto will steal the lock
154
+ }
155
+
156
+ // "not_yet" or escape
157
+ return false;
105
158
  }
106
159
 
107
160
  export function registerGSDCommand(pi: ExtensionAPI): void {
@@ -598,10 +651,10 @@ export async function handleGSDCommand(
598
651
  await handleDryRun(ctx, projectRoot());
599
652
  return;
600
653
  }
601
- if (notifyRemoteAutoActive(ctx, projectRoot())) return;
602
654
  const verboseMode = trimmed.includes("--verbose");
603
655
  const debugMode = trimmed.includes("--debug");
604
656
  if (debugMode) enableDebug(projectRoot());
657
+ if (!(await guardRemoteSession(ctx, pi))) return;
605
658
  await startAuto(ctx, pi, projectRoot(), verboseMode, { step: true });
606
659
  return;
607
660
  }
@@ -610,6 +663,7 @@ export async function handleGSDCommand(
610
663
  const verboseMode = trimmed.includes("--verbose");
611
664
  const debugMode = trimmed.includes("--debug");
612
665
  if (debugMode) enableDebug(projectRoot());
666
+ if (!(await guardRemoteSession(ctx, pi))) return;
613
667
  await startAuto(ctx, pi, projectRoot(), verboseMode);
614
668
  return;
615
669
  }
@@ -993,7 +1047,7 @@ Examples:
993
1047
  }
994
1048
 
995
1049
  if (trimmed === "") {
996
- if (notifyRemoteAutoActive(ctx, projectRoot())) return;
1050
+ if (!(await guardRemoteSession(ctx, pi))) return;
997
1051
  await startAuto(ctx, pi, projectRoot(), false, { step: true });
998
1052
  return;
999
1053
  }
@@ -15,6 +15,7 @@ import type {
15
15
  Summary, SummaryFrontmatter, SummaryRequires, FileModified,
16
16
  Continue, ContinueFrontmatter, ContinueStatus,
17
17
  RequirementCounts,
18
+ TaskIO,
18
19
  SecretsManifest, SecretsManifestEntry, SecretsManifestEntryStatus,
19
20
  ManifestStatus,
20
21
  } from './types.js';
@@ -724,6 +725,50 @@ export function countMustHavesMentionedInSummary(
724
725
  return count;
725
726
  }
726
727
 
728
+ // ─── Task Plan IO Extractor ────────────────────────────────────────────────
729
+
730
+ /**
731
+ * Extract input and output file paths from a task plan's `## Inputs` and
732
+ * `## Expected Output` sections. Looks for backtick-wrapped file paths on
733
+ * each line (e.g. `` `src/foo.ts` ``).
734
+ *
735
+ * Returns empty arrays for missing/empty sections — callers should treat
736
+ * tasks with no IO as ambiguous (sequential fallback trigger).
737
+ */
738
+ export function parseTaskPlanIO(content: string): { inputFiles: string[]; outputFiles: string[] } {
739
+ const backtickPathRegex = /`([^`]+)`/g;
740
+
741
+ function extractPaths(sectionText: string | null): string[] {
742
+ if (!sectionText) return [];
743
+ const paths: string[] = [];
744
+ for (const line of sectionText.split("\n")) {
745
+ const trimmed = line.trim();
746
+ if (!trimmed || trimmed.startsWith("#")) continue;
747
+ let match: RegExpExecArray | null;
748
+ backtickPathRegex.lastIndex = 0;
749
+ while ((match = backtickPathRegex.exec(trimmed)) !== null) {
750
+ const candidate = match[1];
751
+ // Filter out things that look like code tokens rather than file paths
752
+ // (e.g. `true`, `false`, `npm run test`). A file path has at least one
753
+ // dot or slash.
754
+ if (candidate.includes("/") || candidate.includes(".")) {
755
+ paths.push(candidate);
756
+ }
757
+ }
758
+ }
759
+ return paths;
760
+ }
761
+
762
+ const [, body] = splitFrontmatter(content);
763
+ const inputSection = extractSection(body, "Inputs");
764
+ const outputSection = extractSection(body, "Expected Output");
765
+
766
+ return {
767
+ inputFiles: extractPaths(inputSection),
768
+ outputFiles: extractPaths(outputSection),
769
+ };
770
+ }
771
+
727
772
  // ─── UAT Type Extractor ────────────────────────────────────────────────────
728
773
 
729
774
  /**
@@ -479,9 +479,20 @@ export class GitServiceImpl {
479
479
 
480
480
  const wtName = detectWorktreeName(this.basePath);
481
481
  if (wtName) {
482
+ // Auto-mode worktrees use milestone/<MID> branches (wtName = milestone ID)
483
+ const milestoneBranch = `milestone/${wtName}`;
484
+ const currentBranch = nativeGetCurrentBranch(this.basePath);
485
+
486
+ // If we're on a milestone/<MID> branch, use it (auto-mode case)
487
+ if (currentBranch.startsWith("milestone/")) {
488
+ return currentBranch;
489
+ }
490
+
491
+ // Otherwise check for manual worktree branch (worktree/<name>)
482
492
  const wtBranch = `worktree/${wtName}`;
483
493
  if (nativeBranchExists(this.basePath, wtBranch)) return wtBranch;
484
- return nativeGetCurrentBranch(this.basePath);
494
+
495
+ return currentBranch;
485
496
  }
486
497
 
487
498
  // Repo-level default detection: origin/HEAD → main → master → current branch.
@@ -2,7 +2,8 @@
2
2
  // Human-readable display of past auto-mode unit executions.
3
3
 
4
4
  import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
5
- import { formatDuration, padRight, truncateWithEllipsis } from "../shared/format-utils.js";
5
+ import { formatDuration, truncateWithEllipsis } from "../shared/format-utils.js";
6
+ import { padRight } from "../shared/layout-utils.js";
6
7
  import {
7
8
  getLedger, getProjectTotals, formatCost, formatTokenCount,
8
9
  aggregateBySlice, aggregateByPhase, aggregateByModel, loadLedgerFromDisk,
@@ -20,8 +20,10 @@ import { getAndClearSkills } from "./skill-telemetry.js";
20
20
  import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
21
21
  import { parseUnitId } from "./unit-id.js";
22
22
 
23
- // Re-export from shared — canonical implementation lives in format-utils.
24
- export { formatTokenCount } from "../shared/mod.js";
23
+ // Re-export from shared — import directly from format-utils to avoid pulling
24
+ // in the full barrel (mod.js → ui.js → @gsd/pi-tui) which breaks when loaded
25
+ // outside jiti's alias resolution (e.g. dynamic import in auto-loop reports).
26
+ export { formatTokenCount } from "../shared/format-utils.js";
25
27
 
26
28
  // ─── Types ────────────────────────────────────────────────────────────────────
27
29
 
@@ -18,6 +18,7 @@ import type {
18
18
  ParallelConfig,
19
19
  CompressionStrategy,
20
20
  ContextSelectionMode,
21
+ ReactiveExecutionConfig,
21
22
  } from "./types.js";
22
23
  import type { DynamicRoutingConfig } from "./model-router.js";
23
24
 
@@ -86,12 +87,13 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
86
87
  "compression_strategy",
87
88
  "context_selection",
88
89
  "widget_mode",
90
+ "reactive_execution",
89
91
  ]);
90
92
 
91
93
  /** Canonical list of all dispatch unit types. */
92
94
  export const KNOWN_UNIT_TYPES = [
93
95
  "research-milestone", "plan-milestone", "research-slice", "plan-slice",
94
- "execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
96
+ "execute-task", "reactive-execute", "complete-slice", "replan-slice", "reassess-roadmap",
95
97
  "run-uat", "complete-milestone",
96
98
  ] as const;
97
99
  export type UnitType = (typeof KNOWN_UNIT_TYPES)[number];
@@ -215,6 +217,8 @@ export interface GSDPreferences {
215
217
  context_selection?: ContextSelectionMode;
216
218
  /** Default widget display mode for auto-mode dashboard. "full" | "small" | "min" | "off". Default: "full". */
217
219
  widget_mode?: "full" | "small" | "min" | "off";
220
+ /** Reactive (graph-derived parallel) task execution within slices. Disabled by default. */
221
+ reactive_execution?: ReactiveExecutionConfig;
218
222
  }
219
223
 
220
224
  export interface LoadedGSDPreferences {
@@ -496,6 +496,47 @@ export function validatePreferences(preferences: GSDPreferences): {
496
496
  }
497
497
  }
498
498
 
499
+ // ─── Reactive Execution ─────────────────────────────────────────────────
500
+ if (preferences.reactive_execution !== undefined) {
501
+ if (typeof preferences.reactive_execution === "object" && preferences.reactive_execution !== null) {
502
+ const re = preferences.reactive_execution as unknown as Record<string, unknown>;
503
+ const validRe: Record<string, unknown> = {};
504
+
505
+ if (re.enabled !== undefined) {
506
+ if (typeof re.enabled === "boolean") validRe.enabled = re.enabled;
507
+ else errors.push("reactive_execution.enabled must be a boolean");
508
+ }
509
+ if (re.max_parallel !== undefined) {
510
+ const mp = typeof re.max_parallel === "number" ? re.max_parallel : Number(re.max_parallel);
511
+ if (Number.isFinite(mp) && mp >= 1 && mp <= 8) {
512
+ validRe.max_parallel = Math.floor(mp);
513
+ } else {
514
+ errors.push("reactive_execution.max_parallel must be a number between 1 and 8");
515
+ }
516
+ }
517
+ if (re.isolation_mode !== undefined) {
518
+ if (re.isolation_mode === "same-tree") {
519
+ validRe.isolation_mode = "same-tree";
520
+ } else {
521
+ errors.push('reactive_execution.isolation_mode must be "same-tree"');
522
+ }
523
+ }
524
+
525
+ const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode"]);
526
+ for (const key of Object.keys(re)) {
527
+ if (!knownReKeys.has(key)) {
528
+ warnings.push(`unknown reactive_execution key "${key}" — ignored`);
529
+ }
530
+ }
531
+
532
+ if (Object.keys(validRe).length > 0) {
533
+ validated.reactive_execution = validRe as unknown as import("./types.js").ReactiveExecutionConfig;
534
+ }
535
+ } else {
536
+ errors.push("reactive_execution must be an object");
537
+ }
538
+ }
539
+
499
540
  // ─── Verification Preferences ───────────────────────────────────────────
500
541
  if (preferences.verification_commands !== undefined) {
501
542
  if (Array.isArray(preferences.verification_commands)) {