gsd-pi 2.37.0 → 2.37.1-dev.3bbb0a9
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 +21 -20
- package/dist/onboarding.js +1 -0
- package/dist/resources/extensions/cmux/package.json +7 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +67 -1
- package/dist/resources/extensions/gsd/auto-loop.js +18 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
- package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
- package/dist/resources/extensions/gsd/auto.js +42 -5
- package/dist/resources/extensions/gsd/commands.js +80 -33
- package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
- package/dist/resources/extensions/gsd/files.js +41 -0
- package/dist/resources/extensions/gsd/git-service.js +9 -1
- package/dist/resources/extensions/gsd/history.js +2 -1
- package/dist/resources/extensions/gsd/metrics.js +4 -2
- package/dist/resources/extensions/gsd/observability-validator.js +24 -0
- package/dist/resources/extensions/gsd/preferences-types.js +2 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +42 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
- package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
- package/dist/resources/extensions/gsd/session-lock.js +26 -6
- package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/dist/resources/extensions/shared/format-utils.js +5 -41
- package/dist/resources/extensions/shared/layout-utils.js +46 -0
- package/dist/resources/extensions/shared/mod.js +2 -1
- package/package.json +2 -1
- package/packages/pi-ai/dist/env-api-keys.js +13 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +172 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +172 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +47 -764
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
- package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +2 -2
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/package.json +1 -0
- package/packages/pi-ai/src/env-api-keys.ts +14 -0
- package/packages/pi-ai/src/models.generated.ts +172 -0
- package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
- package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
- package/packages/pi-ai/src/providers/anthropic.ts +76 -868
- package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
- package/packages/pi-ai/src/types.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/package.json +7 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +93 -0
- package/src/resources/extensions/gsd/auto-loop.ts +24 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
- package/src/resources/extensions/gsd/auto.ts +56 -5
- package/src/resources/extensions/gsd/commands.ts +85 -31
- package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
- package/src/resources/extensions/gsd/files.ts +45 -0
- package/src/resources/extensions/gsd/git-service.ts +12 -1
- package/src/resources/extensions/gsd/history.ts +2 -1
- package/src/resources/extensions/gsd/metrics.ts +4 -2
- package/src/resources/extensions/gsd/observability-validator.ts +27 -0
- package/src/resources/extensions/gsd/preferences-types.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +41 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
- package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
- package/src/resources/extensions/gsd/session-lock.ts +41 -6
- package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +37 -1
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/cmux.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
- package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
- package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
- package/src/resources/extensions/gsd/types.ts +43 -0
- package/src/resources/extensions/shared/format-utils.ts +5 -44
- package/src/resources/extensions/shared/layout-utils.ts +49 -0
- package/src/resources/extensions/shared/mod.ts +7 -4
- 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) =>
|
|
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: (
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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 {
|
|
@@ -485,6 +485,41 @@ export async function getPriorTaskSummaryPaths(
|
|
|
485
485
|
.map(f => `${sRel}/tasks/${f}`);
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Get carry-forward summary paths scoped to a task's derived dependencies.
|
|
490
|
+
*
|
|
491
|
+
* Instead of all prior tasks (order-based), returns only summaries for task
|
|
492
|
+
* IDs in `dependsOn`. Used by reactive-execute to give each subagent only
|
|
493
|
+
* the context it actually needs — not sibling tasks from a parallel batch.
|
|
494
|
+
*
|
|
495
|
+
* Falls back to order-based when dependsOn is empty (root tasks still get
|
|
496
|
+
* any available prior summaries for continuity).
|
|
497
|
+
*/
|
|
498
|
+
export async function getDependencyTaskSummaryPaths(
|
|
499
|
+
mid: string, sid: string, currentTid: string,
|
|
500
|
+
dependsOn: string[], base: string,
|
|
501
|
+
): Promise<string[]> {
|
|
502
|
+
// If no dependencies, fall back to order-based for root tasks
|
|
503
|
+
if (dependsOn.length === 0) {
|
|
504
|
+
return getPriorTaskSummaryPaths(mid, sid, currentTid, base);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
508
|
+
if (!tDir) return [];
|
|
509
|
+
|
|
510
|
+
const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
|
|
511
|
+
const sRel = relSlicePath(base, mid, sid);
|
|
512
|
+
const depSet = new Set(dependsOn.map((d) => d.toUpperCase()));
|
|
513
|
+
|
|
514
|
+
return summaryFiles
|
|
515
|
+
.filter((f) => {
|
|
516
|
+
// Extract task ID from filename: "T02-SUMMARY.md" → "T02"
|
|
517
|
+
const tid = f.replace(/-SUMMARY\.md$/i, "").toUpperCase();
|
|
518
|
+
return depSet.has(tid);
|
|
519
|
+
})
|
|
520
|
+
.map((f) => `${sRel}/tasks/${f}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
488
523
|
// ─── Adaptive Replanning Checks ────────────────────────────────────────────
|
|
489
524
|
|
|
490
525
|
/**
|
|
@@ -772,13 +807,24 @@ export async function buildPlanSlicePrompt(
|
|
|
772
807
|
});
|
|
773
808
|
}
|
|
774
809
|
|
|
810
|
+
/** Options for customizing execute-task prompt construction. */
|
|
811
|
+
export interface ExecuteTaskPromptOptions {
|
|
812
|
+
level?: InlineLevel;
|
|
813
|
+
/** Override carry-forward paths (dependency-based instead of order-based). */
|
|
814
|
+
carryForwardPaths?: string[];
|
|
815
|
+
}
|
|
816
|
+
|
|
775
817
|
export async function buildExecuteTaskPrompt(
|
|
776
818
|
mid: string, sid: string, sTitle: string,
|
|
777
|
-
tid: string, tTitle: string, base: string,
|
|
819
|
+
tid: string, tTitle: string, base: string,
|
|
820
|
+
level?: InlineLevel | ExecuteTaskPromptOptions,
|
|
778
821
|
): Promise<string> {
|
|
779
|
-
const
|
|
822
|
+
const opts: ExecuteTaskPromptOptions = typeof level === "object" && level !== null && !Array.isArray(level)
|
|
823
|
+
? level
|
|
824
|
+
: { level: level as InlineLevel | undefined };
|
|
825
|
+
const inlineLevel = opts.level ?? resolveInlineLevel();
|
|
780
826
|
|
|
781
|
-
const priorSummaries = await getPriorTaskSummaryPaths(mid, sid, tid, base);
|
|
827
|
+
const priorSummaries = opts.carryForwardPaths ?? await getPriorTaskSummaryPaths(mid, sid, tid, base);
|
|
782
828
|
const priorLines = priorSummaries.length > 0
|
|
783
829
|
? priorSummaries.map(p => `- \`${p}\``).join("\n")
|
|
784
830
|
: "- (no prior tasks)";
|
|
@@ -1234,6 +1280,82 @@ export async function buildReassessRoadmapPrompt(
|
|
|
1234
1280
|
});
|
|
1235
1281
|
}
|
|
1236
1282
|
|
|
1283
|
+
// ─── Reactive Execute Prompt ──────────────────────────────────────────────
|
|
1284
|
+
|
|
1285
|
+
export async function buildReactiveExecutePrompt(
|
|
1286
|
+
mid: string, midTitle: string, sid: string, sTitle: string,
|
|
1287
|
+
readyTaskIds: string[], base: string,
|
|
1288
|
+
): Promise<string> {
|
|
1289
|
+
const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
|
|
1290
|
+
|
|
1291
|
+
// Build graph for context
|
|
1292
|
+
const taskIO = await loadSliceTaskIO(base, mid, sid);
|
|
1293
|
+
const graph = deriveTaskGraph(taskIO);
|
|
1294
|
+
const metrics = graphMetrics(graph);
|
|
1295
|
+
|
|
1296
|
+
// Build graph context section
|
|
1297
|
+
const graphLines: string[] = [];
|
|
1298
|
+
for (const node of graph) {
|
|
1299
|
+
const status = node.done ? "✅ done" : readyTaskIds.includes(node.id) ? "🟢 ready" : "⏳ waiting";
|
|
1300
|
+
const deps = node.dependsOn.length > 0 ? ` (depends on: ${node.dependsOn.join(", ")})` : "";
|
|
1301
|
+
graphLines.push(`- **${node.id}: ${node.title}** — ${status}${deps}`);
|
|
1302
|
+
if (node.outputFiles.length > 0) {
|
|
1303
|
+
graphLines.push(` - Outputs: ${node.outputFiles.map(f => `\`${f}\``).join(", ")}`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
const graphContext = [
|
|
1307
|
+
`Tasks: ${metrics.taskCount}, Edges: ${metrics.edgeCount}, Ready: ${metrics.readySetSize}`,
|
|
1308
|
+
"",
|
|
1309
|
+
...graphLines,
|
|
1310
|
+
].join("\n");
|
|
1311
|
+
|
|
1312
|
+
// Build individual subagent prompts for each ready task
|
|
1313
|
+
const subagentSections: string[] = [];
|
|
1314
|
+
const readyTaskListLines: string[] = [];
|
|
1315
|
+
|
|
1316
|
+
for (const tid of readyTaskIds) {
|
|
1317
|
+
const node = graph.find((n) => n.id === tid);
|
|
1318
|
+
const tTitle = node?.title ?? tid;
|
|
1319
|
+
readyTaskListLines.push(`- **${tid}: ${tTitle}**`);
|
|
1320
|
+
|
|
1321
|
+
// Build dependency-scoped carry-forward paths for this task
|
|
1322
|
+
const depPaths = await getDependencyTaskSummaryPaths(
|
|
1323
|
+
mid, sid, tid, node?.dependsOn ?? [], base,
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1326
|
+
// Build a full execute-task prompt with dependency-based carry-forward
|
|
1327
|
+
const taskPrompt = await buildExecuteTaskPrompt(
|
|
1328
|
+
mid, sid, sTitle, tid, tTitle, base,
|
|
1329
|
+
{ carryForwardPaths: depPaths },
|
|
1330
|
+
);
|
|
1331
|
+
|
|
1332
|
+
subagentSections.push([
|
|
1333
|
+
`### ${tid}: ${tTitle}`,
|
|
1334
|
+
"",
|
|
1335
|
+
"Use this as the prompt for a `subagent` call:",
|
|
1336
|
+
"",
|
|
1337
|
+
"```",
|
|
1338
|
+
taskPrompt,
|
|
1339
|
+
"```",
|
|
1340
|
+
].join("\n"));
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const inlinedTemplates = inlineTemplate("task-summary", "Task Summary");
|
|
1344
|
+
|
|
1345
|
+
return loadPrompt("reactive-execute", {
|
|
1346
|
+
workingDirectory: base,
|
|
1347
|
+
milestoneId: mid,
|
|
1348
|
+
milestoneTitle: midTitle,
|
|
1349
|
+
sliceId: sid,
|
|
1350
|
+
sliceTitle: sTitle,
|
|
1351
|
+
graphContext,
|
|
1352
|
+
readyTaskCount: String(readyTaskIds.length),
|
|
1353
|
+
readyTaskList: readyTaskListLines.join("\n"),
|
|
1354
|
+
subagentPrompts: subagentSections.join("\n\n---\n\n"),
|
|
1355
|
+
inlinedTemplates,
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1237
1359
|
export async function buildRewriteDocsPrompt(
|
|
1238
1360
|
mid: string, midTitle: string,
|
|
1239
1361
|
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,44 @@ export function verifyExpectedArtifact(
|
|
|
148
152
|
return !content.includes("**Scope:** active");
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
// Reactive-execute: verify that each dispatched task's summary exists.
|
|
156
|
+
// The unitId encodes the batch: "{mid}/{sid}/reactive+T02,T03"
|
|
157
|
+
if (unitType === "reactive-execute") {
|
|
158
|
+
const parts = unitId.split("/");
|
|
159
|
+
const mid = parts[0];
|
|
160
|
+
const sidAndBatch = parts[1];
|
|
161
|
+
const batchPart = parts[2]; // "reactive+T02,T03"
|
|
162
|
+
if (!mid || !sidAndBatch || !batchPart) return false;
|
|
163
|
+
|
|
164
|
+
const sid = sidAndBatch;
|
|
165
|
+
const plusIdx = batchPart.indexOf("+");
|
|
166
|
+
if (plusIdx === -1) {
|
|
167
|
+
// Legacy format "reactive" without batch IDs — fall back to "any summary"
|
|
168
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
169
|
+
if (!tDir) return false;
|
|
170
|
+
const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
|
|
171
|
+
return summaryFiles.length > 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const batchIds = batchPart.slice(plusIdx + 1).split(",").filter(Boolean);
|
|
175
|
+
if (batchIds.length === 0) return false;
|
|
176
|
+
|
|
177
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
178
|
+
if (!tDir) return false;
|
|
179
|
+
|
|
180
|
+
const existingSummaries = new Set(
|
|
181
|
+
resolveTaskFiles(tDir, "SUMMARY").map((f) =>
|
|
182
|
+
f.replace(/-SUMMARY\.md$/i, "").toUpperCase(),
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Every dispatched task must have a summary file
|
|
187
|
+
for (const tid of batchIds) {
|
|
188
|
+
if (!existingSummaries.has(tid.toUpperCase())) return false;
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
151
193
|
const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
|
|
152
194
|
// For unit types with no verifiable artifact (null path), the parent directory
|
|
153
195
|
// 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
|
-
|
|
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(
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
76
|
-
* Returns
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 (
|
|
1050
|
+
if (!(await guardRemoteSession(ctx, pi))) return;
|
|
997
1051
|
await startAuto(ctx, pi, projectRoot(), false, { step: true });
|
|
998
1052
|
return;
|
|
999
1053
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
17
|
+
import { getEnvApiKey } from "@gsd/pi-ai";
|
|
17
18
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
18
19
|
import { getAuthPath, PROVIDER_REGISTRY, type ProviderCategory } from "./key-manager.js";
|
|
19
20
|
|
|
@@ -56,6 +57,7 @@ function modelToProviderId(model: string): string | null {
|
|
|
56
57
|
google: "google",
|
|
57
58
|
anthropic: "anthropic",
|
|
58
59
|
openai: "openai",
|
|
60
|
+
"github-copilot": "github-copilot",
|
|
59
61
|
};
|
|
60
62
|
if (prefixMap[prefix]) return prefixMap[prefix];
|
|
61
63
|
}
|
|
@@ -139,7 +141,15 @@ function resolveKey(providerId: string): KeyLookup {
|
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
// Check environment variable
|
|
144
|
+
// Check environment variable using the authoritative env var resolution
|
|
145
|
+
// (handles multi-var lookups like ANTHROPIC_OAUTH_TOKEN || ANTHROPIC_API_KEY,
|
|
146
|
+
// COPILOT_GITHUB_TOKEN || GH_TOKEN || GITHUB_TOKEN, Vertex ADC, Bedrock, etc.)
|
|
147
|
+
if (getEnvApiKey(providerId)) {
|
|
148
|
+
return { found: true, source: "env", backedOff: false };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fall back to PROVIDER_REGISTRY env var for providers not covered by getEnvApiKey
|
|
152
|
+
// (e.g., search providers like Brave, Tavily; tool providers like Jina, Context7)
|
|
143
153
|
if (info?.envVar && process.env[info.envVar]) {
|
|
144
154
|
return { found: true, source: "env", backedOff: false };
|
|
145
155
|
}
|
|
@@ -149,6 +159,16 @@ function resolveKey(providerId: string): KeyLookup {
|
|
|
149
159
|
|
|
150
160
|
// ── Individual check groups ────────────────────────────────────────────────────
|
|
151
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Providers that can serve models normally associated with another provider.
|
|
164
|
+
* Key = the provider whose models can be served, Value = alternative providers to check.
|
|
165
|
+
* e.g. GitHub Copilot subscriptions can access Claude and GPT models.
|
|
166
|
+
*/
|
|
167
|
+
const PROVIDER_ROUTES: Record<string, string[]> = {
|
|
168
|
+
anthropic: ["github-copilot"],
|
|
169
|
+
openai: ["github-copilot"],
|
|
170
|
+
};
|
|
171
|
+
|
|
152
172
|
function checkLlmProviders(): ProviderCheckResult[] {
|
|
153
173
|
const required = collectConfiguredModelProviders();
|
|
154
174
|
const results: ProviderCheckResult[] = [];
|
|
@@ -159,6 +179,23 @@ function checkLlmProviders(): ProviderCheckResult[] {
|
|
|
159
179
|
const lookup = resolveKey(providerId);
|
|
160
180
|
|
|
161
181
|
if (!lookup.found) {
|
|
182
|
+
// Check if a cross-provider can serve this provider's models
|
|
183
|
+
const routes = PROVIDER_ROUTES[providerId];
|
|
184
|
+
const routeProvider = routes?.find(routeId => resolveKey(routeId).found);
|
|
185
|
+
if (routeProvider) {
|
|
186
|
+
const routeInfo = PROVIDER_REGISTRY.find(p => p.id === routeProvider);
|
|
187
|
+
const routeLabel = routeInfo?.label ?? routeProvider;
|
|
188
|
+
results.push({
|
|
189
|
+
name: providerId,
|
|
190
|
+
label,
|
|
191
|
+
category: "llm",
|
|
192
|
+
status: "ok",
|
|
193
|
+
message: `${label} — available via ${routeLabel}`,
|
|
194
|
+
required: true,
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
162
199
|
const envVar = info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
|
|
163
200
|
results.push({
|
|
164
201
|
name: providerId,
|