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.
- 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 +54 -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 +55 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +19 -1
- package/dist/resources/extensions/gsd/auto.js +42 -5
- package/dist/resources/extensions/gsd/commands.js +80 -33
- 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/preferences-types.js +2 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +42 -0
- 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/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 +78 -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 +68 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +18 -0
- package/src/resources/extensions/gsd/auto.ts +56 -5
- package/src/resources/extensions/gsd/commands.ts +85 -31
- 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/preferences-types.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +41 -0
- 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/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/reactive-executor.test.ts +367 -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 +41 -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 {
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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,
|
|
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 —
|
|
24
|
-
|
|
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)) {
|