gsd-pi 2.37.0-dev.b5e7ebc → 2.37.0-dev.c5c85d8

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.
package/README.md CHANGED
@@ -16,7 +16,7 @@ This version is different. GSD is now a standalone CLI built on the [Pi SDK](htt
16
16
 
17
17
  One command. Walk away. Come back to a built project with clean git history.
18
18
 
19
- <pre><code>npm install -g gsd-pi</code></pre>
19
+ <pre><code>npm install -g gsd-pi@latest</code></pre>
20
20
 
21
21
  > **📋 NOTICE: New to Node on Mac?** If you installed Node.js via Homebrew, you may be running a development release instead of LTS. **[Read this guide](./docs/node-lts-macos.md)** to pin Node 24 LTS and avoid compatibility issues.
22
22
 
@@ -209,6 +209,29 @@ export function stopAutoRemote(projectRoot) {
209
209
  return { found: false, error: err.message };
210
210
  }
211
211
  }
212
+ /**
213
+ * Check if a remote auto-mode session is running (from a different process).
214
+ * Reads the crash lock, checks PID liveness, and returns session details.
215
+ * Used by the guard in commands.ts to prevent bare /gsd, /gsd next, and
216
+ * /gsd auto from stealing the session lock.
217
+ */
218
+ export function checkRemoteAutoSession(projectRoot) {
219
+ const lock = readCrashLock(projectRoot);
220
+ if (!lock)
221
+ return { running: false };
222
+ if (!isLockProcessAlive(lock)) {
223
+ // Stale lock from a dead process — not a live remote session
224
+ return { running: false };
225
+ }
226
+ return {
227
+ running: true,
228
+ pid: lock.pid,
229
+ unitType: lock.unitType,
230
+ unitId: lock.unitId,
231
+ startedAt: lock.startedAt,
232
+ completedUnits: lock.completedUnits,
233
+ };
234
+ }
212
235
  export function isStepMode() {
213
236
  return s.stepMode;
214
237
  }
@@ -12,7 +12,7 @@ import { deriveState } from "./state.js";
12
12
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
13
13
  import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
14
14
  import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
15
- import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, stopAutoRemote } from "./auto.js";
15
+ import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, stopAutoRemote, checkRemoteAutoSession } from "./auto.js";
16
16
  import { dispatchDirectPhase } from "./auto-direct-dispatch.js";
17
17
  import { resolveProjectRoot } from "./worktree.js";
18
18
  import { assertSafeDirectory } from "./validate-directory.js";
@@ -36,8 +36,8 @@ import { computeProgressScore, formatProgressLine } from "./progress-score.js";
36
36
  import { runEnvironmentChecks } from "./doctor-environment.js";
37
37
  import { handleLogs } from "./commands-logs.js";
38
38
  import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
39
- import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
40
39
  import { handleCmux } from "./commands-cmux.js";
40
+ import { showNextAction } from "../shared/mod.js";
41
41
  /** Resolve the effective project root, accounting for worktree paths. */
42
42
  export function projectRoot() {
43
43
  const cwd = process.cwd();
@@ -57,36 +57,81 @@ export function projectRoot() {
57
57
  return root;
58
58
  }
59
59
  /**
60
- * Check if another process holds the auto-mode session lock.
61
- * Returns the lock data if a remote session is alive, null otherwise.
60
+ * Guard against starting auto-mode when a remote session is already running.
61
+ * Returns true if the caller should proceed with startAuto, false if handled.
62
62
  */
63
- function getRemoteAutoSession(basePath) {
64
- const lockData = readSessionLockData(basePath);
65
- if (!lockData)
66
- return null;
67
- if (lockData.pid === process.pid)
68
- return null;
69
- if (!isSessionLockProcessAlive(lockData))
70
- return null;
71
- return { pid: lockData.pid };
72
- }
73
- /**
74
- * Show a steering menu when auto-mode is running in another process.
75
- * Returns true if a remote session was detected (caller should return early).
76
- */
77
- function notifyRemoteAutoActive(ctx, basePath) {
78
- const remote = getRemoteAutoSession(basePath);
79
- if (!remote)
63
+ async function guardRemoteSession(ctx, pi) {
64
+ // Local session already active — proceed (startAuto handles re-entrant calls)
65
+ if (isAutoActive() || isAutoPaused())
66
+ return true;
67
+ const remote = checkRemoteAutoSession(projectRoot());
68
+ if (!remote.running || !remote.pid)
69
+ return true;
70
+ const unitLabel = remote.unitType && remote.unitId
71
+ ? `${remote.unitType} (${remote.unitId})`
72
+ : "unknown unit";
73
+ const unitsMsg = remote.completedUnits != null
74
+ ? `${remote.completedUnits} units completed`
75
+ : "";
76
+ const choice = await showNextAction(ctx, {
77
+ title: `Auto-mode is running in another terminal (PID ${remote.pid})`,
78
+ summary: [
79
+ `Currently executing: ${unitLabel}`,
80
+ ...(unitsMsg ? [unitsMsg] : []),
81
+ ...(remote.startedAt ? [`Started: ${remote.startedAt}`] : []),
82
+ ],
83
+ actions: [
84
+ {
85
+ id: "status",
86
+ label: "View status",
87
+ description: "Show the current GSD progress dashboard.",
88
+ recommended: true,
89
+ },
90
+ {
91
+ id: "steer",
92
+ label: "Steer the session",
93
+ description: "Use /gsd steer <instruction> to redirect the running session.",
94
+ },
95
+ {
96
+ id: "stop",
97
+ label: "Stop remote session",
98
+ description: `Send SIGTERM to PID ${remote.pid} to stop it gracefully.`,
99
+ },
100
+ {
101
+ id: "force",
102
+ label: "Force start (steal lock)",
103
+ description: "Start a new session, terminating the existing one.",
104
+ },
105
+ ],
106
+ notYetMessage: "Run /gsd when ready.",
107
+ });
108
+ if (choice === "status") {
109
+ await handleStatus(ctx);
110
+ return false;
111
+ }
112
+ if (choice === "steer") {
113
+ ctx.ui.notify("Use /gsd steer <instruction> to redirect the running auto-mode session.\n" +
114
+ "Example: /gsd steer Use Postgres instead of SQLite", "info");
115
+ return false;
116
+ }
117
+ if (choice === "stop") {
118
+ const result = stopAutoRemote(projectRoot());
119
+ if (result.found) {
120
+ ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
121
+ }
122
+ else if (result.error) {
123
+ ctx.ui.notify(`Failed to stop remote auto-mode: ${result.error}`, "error");
124
+ }
125
+ else {
126
+ ctx.ui.notify("Remote session is no longer running.", "info");
127
+ }
80
128
  return false;
81
- ctx.ui.notify(`Auto-mode is running in another process (PID ${remote.pid}).\n` +
82
- `Use these commands to interact with it:\n` +
83
- ` /gsd status check progress\n` +
84
- ` /gsd discuss — discuss architecture decisions\n` +
85
- ` /gsd queue queue the next milestone\n` +
86
- ` /gsd steer apply an override to active work\n` +
87
- ` /gsd capture — fire-and-forget thought\n` +
88
- ` /gsd stop — stop auto-mode`, "warning");
89
- return true;
129
+ }
130
+ if (choice === "force") {
131
+ return true; // Proceed startAuto will steal the lock
132
+ }
133
+ // "not_yet" or escape
134
+ return false;
90
135
  }
91
136
  export function registerGSDCommand(pi) {
92
137
  pi.registerCommand("gsd", {
@@ -542,12 +587,12 @@ export async function handleGSDCommand(args, ctx, pi) {
542
587
  await handleDryRun(ctx, projectRoot());
543
588
  return;
544
589
  }
545
- if (notifyRemoteAutoActive(ctx, projectRoot()))
546
- return;
547
590
  const verboseMode = trimmed.includes("--verbose");
548
591
  const debugMode = trimmed.includes("--debug");
549
592
  if (debugMode)
550
593
  enableDebug(projectRoot());
594
+ if (!(await guardRemoteSession(ctx, pi)))
595
+ return;
551
596
  await startAuto(ctx, pi, projectRoot(), verboseMode, { step: true });
552
597
  return;
553
598
  }
@@ -556,6 +601,8 @@ export async function handleGSDCommand(args, ctx, pi) {
556
601
  const debugMode = trimmed.includes("--debug");
557
602
  if (debugMode)
558
603
  enableDebug(projectRoot());
604
+ if (!(await guardRemoteSession(ctx, pi)))
605
+ return;
559
606
  await startAuto(ctx, pi, projectRoot(), verboseMode);
560
607
  return;
561
608
  }
@@ -899,7 +946,7 @@ Examples:
899
946
  return;
900
947
  }
901
948
  if (trimmed === "") {
902
- if (notifyRemoteAutoActive(ctx, projectRoot()))
949
+ if (!(await guardRemoteSession(ctx, pi)))
903
950
  return;
904
951
  await startAuto(ctx, pi, projectRoot(), false, { step: true });
905
952
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.37.0-dev.b5e7ebc",
3
+ "version": "2.37.0-dev.c5c85d8",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -418,6 +418,38 @@ export function stopAutoRemote(projectRoot: string): {
418
418
  }
419
419
  }
420
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
+
421
453
  export function isStepMode(): boolean {
422
454
  return s.stepMode;
423
455
  }
@@ -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
  }