gsd-pi 2.36.0-dev.f887f4e → 2.37.0-dev.3186675
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/dist/resources/extensions/cmux/index.js +321 -0
- package/dist/resources/extensions/cmux/package.json +7 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
- package/dist/resources/extensions/gsd/auto-loop.js +29 -4
- package/dist/resources/extensions/gsd/auto.js +35 -5
- package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +51 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -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/index.js +5 -0
- package/dist/resources/extensions/gsd/metrics.js +4 -2
- package/dist/resources/extensions/gsd/notifications.js +10 -1
- package/dist/resources/extensions/gsd/preferences-types.js +2 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +29 -0
- package/dist/resources/extensions/gsd/preferences.js +3 -0
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
- package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
- package/dist/resources/extensions/gsd/session-lock.js +26 -6
- package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
- package/dist/resources/extensions/search-the-web/native-search.js +45 -4
- 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/dist/resources/extensions/shared/terminal.js +5 -0
- package/dist/resources/extensions/subagent/index.js +180 -60
- package/package.json +1 -1
- 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/package.json +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
- package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal-image.js +4 -0
- package/packages/pi-tui/dist/terminal-image.js.map +1 -1
- package/packages/pi-tui/src/terminal-image.ts +5 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +384 -0
- package/src/resources/extensions/cmux/package.json +7 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
- package/src/resources/extensions/gsd/auto-loop.ts +66 -6
- package/src/resources/extensions/gsd/auto.ts +45 -5
- package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +54 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -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/index.ts +8 -0
- package/src/resources/extensions/gsd/metrics.ts +4 -2
- package/src/resources/extensions/gsd/notifications.ts +10 -1
- package/src/resources/extensions/gsd/preferences-types.ts +13 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +26 -0
- package/src/resources/extensions/gsd/preferences.ts +4 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
- package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
- package/src/resources/extensions/gsd/session-lock.ts +41 -6
- package/src/resources/extensions/gsd/templates/preferences.md +6 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +39 -1
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/cmux.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
- package/src/resources/extensions/search-the-web/native-search.ts +50 -4
- 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/terminal.ts +5 -0
- package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
- package/src/resources/extensions/subagent/index.ts +236 -79
|
@@ -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";
|
|
@@ -25,6 +26,7 @@ import type {
|
|
|
25
26
|
import type { DispatchAction } from "./auto-dispatch.js";
|
|
26
27
|
import type { WorktreeResolver } from "./worktree-resolver.js";
|
|
27
28
|
import { debugLog } from "./debug-logger.js";
|
|
29
|
+
import type { CmuxLogLevel } from "../cmux/index.js";
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Maximum total loop iterations before forced stop. Prevents runaway loops
|
|
@@ -276,6 +278,12 @@ export interface LoopDeps {
|
|
|
276
278
|
unitId: string,
|
|
277
279
|
state: GSDState,
|
|
278
280
|
) => void;
|
|
281
|
+
syncCmuxSidebar: (preferences: GSDPreferences | undefined, state: GSDState) => void;
|
|
282
|
+
logCmuxEvent: (
|
|
283
|
+
preferences: GSDPreferences | undefined,
|
|
284
|
+
message: string,
|
|
285
|
+
level?: CmuxLogLevel,
|
|
286
|
+
) => void;
|
|
279
287
|
|
|
280
288
|
// State and cache functions
|
|
281
289
|
invalidateAllCaches: () => void;
|
|
@@ -300,7 +308,7 @@ export interface LoopDeps {
|
|
|
300
308
|
checkResourcesStale: (version: string | null) => string | null;
|
|
301
309
|
|
|
302
310
|
// Session lock
|
|
303
|
-
validateSessionLock: (basePath: string) =>
|
|
311
|
+
validateSessionLock: (basePath: string) => SessionLockStatus;
|
|
304
312
|
updateSessionLock: (
|
|
305
313
|
basePath: string,
|
|
306
314
|
unitType: string,
|
|
@@ -308,7 +316,10 @@ export interface LoopDeps {
|
|
|
308
316
|
completedUnits: number,
|
|
309
317
|
sessionFile?: string,
|
|
310
318
|
) => void;
|
|
311
|
-
handleLostSessionLock: (
|
|
319
|
+
handleLostSessionLock: (
|
|
320
|
+
ctx?: ExtensionContext,
|
|
321
|
+
lockStatus?: SessionLockStatus,
|
|
322
|
+
) => void;
|
|
312
323
|
|
|
313
324
|
// Milestone transition functions
|
|
314
325
|
sendDesktopNotification: (
|
|
@@ -552,10 +563,24 @@ export async function autoLoop(
|
|
|
552
563
|
try {
|
|
553
564
|
// ── Blanket try/catch: one bad iteration must not kill the session
|
|
554
565
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
+
}
|
|
559
584
|
}
|
|
560
585
|
|
|
561
586
|
// ── Phase 1: Pre-dispatch ───────────────────────────────────────────
|
|
@@ -609,6 +634,7 @@ export async function autoLoop(
|
|
|
609
634
|
|
|
610
635
|
// Derive state
|
|
611
636
|
let state = await deps.deriveState(s.basePath);
|
|
637
|
+
deps.syncCmuxSidebar(deps.loadEffectiveGSDPreferences()?.preferences, state);
|
|
612
638
|
let mid = state.activeMilestone?.id;
|
|
613
639
|
let midTitle = state.activeMilestone?.title;
|
|
614
640
|
debugLog("autoLoop", {
|
|
@@ -630,6 +656,11 @@ export async function autoLoop(
|
|
|
630
656
|
"success",
|
|
631
657
|
"milestone",
|
|
632
658
|
);
|
|
659
|
+
deps.logCmuxEvent(
|
|
660
|
+
deps.loadEffectiveGSDPreferences()?.preferences,
|
|
661
|
+
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
|
|
662
|
+
"success",
|
|
663
|
+
);
|
|
633
664
|
|
|
634
665
|
const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
635
666
|
if (vizPrefs?.auto_visualize) {
|
|
@@ -767,12 +798,18 @@ export async function autoLoop(
|
|
|
767
798
|
"success",
|
|
768
799
|
"milestone",
|
|
769
800
|
);
|
|
801
|
+
deps.logCmuxEvent(
|
|
802
|
+
deps.loadEffectiveGSDPreferences()?.preferences,
|
|
803
|
+
"All milestones complete.",
|
|
804
|
+
"success",
|
|
805
|
+
);
|
|
770
806
|
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
771
807
|
} else if (state.phase === "blocked") {
|
|
772
808
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
773
809
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
774
810
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
775
811
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
812
|
+
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
776
813
|
} else {
|
|
777
814
|
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
778
815
|
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
@@ -850,6 +887,11 @@ export async function autoLoop(
|
|
|
850
887
|
"success",
|
|
851
888
|
"milestone",
|
|
852
889
|
);
|
|
890
|
+
deps.logCmuxEvent(
|
|
891
|
+
deps.loadEffectiveGSDPreferences()?.preferences,
|
|
892
|
+
`Milestone ${mid} complete.`,
|
|
893
|
+
"success",
|
|
894
|
+
);
|
|
853
895
|
await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
|
|
854
896
|
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
855
897
|
break;
|
|
@@ -871,6 +913,7 @@ export async function autoLoop(
|
|
|
871
913
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
872
914
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
873
915
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
916
|
+
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
874
917
|
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
875
918
|
break;
|
|
876
919
|
}
|
|
@@ -914,12 +957,14 @@ export async function autoLoop(
|
|
|
914
957
|
"warning",
|
|
915
958
|
);
|
|
916
959
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
960
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
917
961
|
await deps.pauseAuto(ctx, pi);
|
|
918
962
|
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
919
963
|
break;
|
|
920
964
|
}
|
|
921
965
|
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
922
966
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
967
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
923
968
|
} else if (newBudgetAlertLevel === 90) {
|
|
924
969
|
s.lastBudgetAlertLevel =
|
|
925
970
|
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
@@ -933,6 +978,11 @@ export async function autoLoop(
|
|
|
933
978
|
"warning",
|
|
934
979
|
"budget",
|
|
935
980
|
);
|
|
981
|
+
deps.logCmuxEvent(
|
|
982
|
+
prefs,
|
|
983
|
+
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
984
|
+
"warning",
|
|
985
|
+
);
|
|
936
986
|
} else if (newBudgetAlertLevel === 80) {
|
|
937
987
|
s.lastBudgetAlertLevel =
|
|
938
988
|
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
@@ -946,6 +996,11 @@ export async function autoLoop(
|
|
|
946
996
|
"warning",
|
|
947
997
|
"budget",
|
|
948
998
|
);
|
|
999
|
+
deps.logCmuxEvent(
|
|
1000
|
+
prefs,
|
|
1001
|
+
`Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1002
|
+
"warning",
|
|
1003
|
+
);
|
|
949
1004
|
} else if (newBudgetAlertLevel === 75) {
|
|
950
1005
|
s.lastBudgetAlertLevel =
|
|
951
1006
|
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
@@ -959,6 +1014,11 @@ export async function autoLoop(
|
|
|
959
1014
|
"info",
|
|
960
1015
|
"budget",
|
|
961
1016
|
);
|
|
1017
|
+
deps.logCmuxEvent(
|
|
1018
|
+
prefs,
|
|
1019
|
+
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1020
|
+
"progress",
|
|
1021
|
+
);
|
|
962
1022
|
} else if (budgetAlertLevel === 0) {
|
|
963
1023
|
s.lastBudgetAlertLevel = 0;
|
|
964
1024
|
}
|
|
@@ -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,
|
|
@@ -184,6 +185,7 @@ import {
|
|
|
184
185
|
} from "./auto-supervisor.js";
|
|
185
186
|
import { isDbAvailable } from "./gsd-db.js";
|
|
186
187
|
import { countPendingCaptures } from "./captures.js";
|
|
188
|
+
import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
|
|
187
189
|
|
|
188
190
|
// ── Extracted modules ──────────────────────────────────────────────────────
|
|
189
191
|
import { startUnitSupervision } from "./auto-timers.js";
|
|
@@ -460,14 +462,33 @@ function buildSnapshotOpts(
|
|
|
460
462
|
};
|
|
461
463
|
}
|
|
462
464
|
|
|
463
|
-
function handleLostSessionLock(
|
|
464
|
-
|
|
465
|
+
function handleLostSessionLock(
|
|
466
|
+
ctx?: ExtensionContext,
|
|
467
|
+
lockStatus?: SessionLockStatus,
|
|
468
|
+
): void {
|
|
469
|
+
debugLog("session-lock-lost", {
|
|
470
|
+
lockBase: lockBase(),
|
|
471
|
+
reason: lockStatus?.failureReason,
|
|
472
|
+
existingPid: lockStatus?.existingPid,
|
|
473
|
+
expectedPid: lockStatus?.expectedPid,
|
|
474
|
+
});
|
|
465
475
|
s.active = false;
|
|
466
476
|
s.paused = false;
|
|
467
477
|
clearUnitTimeout();
|
|
468
478
|
deregisterSigtermHandler();
|
|
479
|
+
clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
|
|
480
|
+
const message =
|
|
481
|
+
lockStatus?.failureReason === "pid-mismatch"
|
|
482
|
+
? lockStatus.existingPid
|
|
483
|
+
? `Session lock moved to PID ${lockStatus.existingPid} — another GSD process appears to have taken over. Stopping gracefully.`
|
|
484
|
+
: "Session lock moved to a different process — another GSD process appears to have taken over. Stopping gracefully."
|
|
485
|
+
: lockStatus?.failureReason === "missing-metadata"
|
|
486
|
+
? "Session lock metadata disappeared, so ownership could not be confirmed. Stopping gracefully."
|
|
487
|
+
: lockStatus?.failureReason === "compromised"
|
|
488
|
+
? "Session lock was compromised or invalidated during heartbeat checks; takeover was not confirmed. Stopping gracefully."
|
|
489
|
+
: "Session lock lost. Stopping gracefully.";
|
|
469
490
|
ctx?.ui.notify(
|
|
470
|
-
|
|
491
|
+
message,
|
|
471
492
|
"error",
|
|
472
493
|
);
|
|
473
494
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
@@ -481,6 +502,7 @@ export async function stopAuto(
|
|
|
481
502
|
reason?: string,
|
|
482
503
|
): Promise<void> {
|
|
483
504
|
if (!s.active && !s.paused) return;
|
|
505
|
+
const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
|
|
484
506
|
const reasonSuffix = reason ? ` — ${reason}` : "";
|
|
485
507
|
clearUnitTimeout();
|
|
486
508
|
if (lockBase()) clearLock(lockBase());
|
|
@@ -543,6 +565,13 @@ export async function stopAuto(
|
|
|
543
565
|
}
|
|
544
566
|
}
|
|
545
567
|
|
|
568
|
+
clearCmuxSidebar(loadedPreferences);
|
|
569
|
+
logCmuxEvent(
|
|
570
|
+
loadedPreferences,
|
|
571
|
+
`Auto-mode stopped${reasonSuffix || ""}.`,
|
|
572
|
+
reason?.startsWith("Blocked:") ? "warning" : "info",
|
|
573
|
+
);
|
|
574
|
+
|
|
546
575
|
if (isDebugEnabled()) {
|
|
547
576
|
const logPath = writeDebugSummary();
|
|
548
577
|
if (logPath) {
|
|
@@ -708,6 +737,8 @@ function buildLoopDeps(): LoopDeps {
|
|
|
708
737
|
pauseAuto,
|
|
709
738
|
clearUnitTimeout,
|
|
710
739
|
updateProgressWidget,
|
|
740
|
+
syncCmuxSidebar,
|
|
741
|
+
logCmuxEvent,
|
|
711
742
|
|
|
712
743
|
// State and cache
|
|
713
744
|
invalidateAllCaches,
|
|
@@ -724,7 +755,7 @@ function buildLoopDeps(): LoopDeps {
|
|
|
724
755
|
checkResourcesStale,
|
|
725
756
|
|
|
726
757
|
// Session lock
|
|
727
|
-
validateSessionLock,
|
|
758
|
+
validateSessionLock: getSessionLockStatus,
|
|
728
759
|
updateSessionLock,
|
|
729
760
|
handleLostSessionLock,
|
|
730
761
|
|
|
@@ -890,6 +921,7 @@ export async function startAuto(
|
|
|
890
921
|
restoreHookState(s.basePath);
|
|
891
922
|
try {
|
|
892
923
|
await rebuildState(s.basePath);
|
|
924
|
+
syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
|
|
893
925
|
} catch (e) {
|
|
894
926
|
debugLog("resume-rebuild-state-failed", {
|
|
895
927
|
error: e instanceof Error ? e.message : String(e),
|
|
@@ -941,6 +973,7 @@ export async function startAuto(
|
|
|
941
973
|
s.currentMilestoneId ?? "unknown",
|
|
942
974
|
s.completedUnits.length,
|
|
943
975
|
);
|
|
976
|
+
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
|
|
944
977
|
|
|
945
978
|
await autoLoop(ctx, pi, s, buildLoopDeps());
|
|
946
979
|
return;
|
|
@@ -965,6 +998,13 @@ export async function startAuto(
|
|
|
965
998
|
);
|
|
966
999
|
if (!ready) return;
|
|
967
1000
|
|
|
1001
|
+
try {
|
|
1002
|
+
syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
|
|
1003
|
+
} catch {
|
|
1004
|
+
// Best-effort only — sidebar sync must never block auto-mode startup
|
|
1005
|
+
}
|
|
1006
|
+
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
|
|
1007
|
+
|
|
968
1008
|
// Dispatch the first unit
|
|
969
1009
|
await autoLoop(ctx, pi, s, buildLoopDeps());
|
|
970
1010
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { clearCmuxSidebar, CmuxClient, detectCmuxEnvironment, resolveCmuxConfig } from "../cmux/index.js";
|
|
4
|
+
import { saveFile } from "./files.js";
|
|
5
|
+
import {
|
|
6
|
+
getProjectGSDPreferencesPath,
|
|
7
|
+
loadEffectiveGSDPreferences,
|
|
8
|
+
loadProjectGSDPreferences,
|
|
9
|
+
} from "./preferences.js";
|
|
10
|
+
import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
|
|
11
|
+
|
|
12
|
+
function extractBodyAfterFrontmatter(content: string): string | null {
|
|
13
|
+
const start = content.startsWith("---\n") ? 4 : content.startsWith("---\r\n") ? 5 : -1;
|
|
14
|
+
if (start === -1) return null;
|
|
15
|
+
const closingIdx = content.indexOf("\n---", start);
|
|
16
|
+
if (closingIdx === -1) return null;
|
|
17
|
+
const after = content.slice(closingIdx + 4);
|
|
18
|
+
return after.trim() ? after : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function writeProjectCmuxPreferences(
|
|
22
|
+
ctx: ExtensionCommandContext,
|
|
23
|
+
updater: (prefs: Record<string, unknown>) => void,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const path = getProjectGSDPreferencesPath();
|
|
26
|
+
await ensurePreferencesFile(path, ctx, "project");
|
|
27
|
+
|
|
28
|
+
const existing = loadProjectGSDPreferences();
|
|
29
|
+
const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : { version: 1 };
|
|
30
|
+
updater(prefs);
|
|
31
|
+
prefs.version = prefs.version || 1;
|
|
32
|
+
|
|
33
|
+
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
34
|
+
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
35
|
+
if (existsSync(path)) {
|
|
36
|
+
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
37
|
+
if (preserved) body = preserved;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await saveFile(path, `---\n${frontmatter}---${body}`);
|
|
41
|
+
await ctx.waitForIdle();
|
|
42
|
+
await ctx.reload();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatCmuxStatus(): string {
|
|
46
|
+
const loaded = loadEffectiveGSDPreferences();
|
|
47
|
+
const detected = detectCmuxEnvironment();
|
|
48
|
+
const resolved = resolveCmuxConfig(loaded?.preferences);
|
|
49
|
+
const capabilities = new CmuxClient(resolved).getCapabilities() as Record<string, unknown> | null;
|
|
50
|
+
const accessMode = typeof capabilities?.mode === "string"
|
|
51
|
+
? capabilities.mode
|
|
52
|
+
: typeof capabilities?.access_mode === "string"
|
|
53
|
+
? capabilities.access_mode
|
|
54
|
+
: "unknown";
|
|
55
|
+
const methods = Array.isArray(capabilities?.methods) ? capabilities.methods.length : 0;
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
"cmux status",
|
|
59
|
+
"",
|
|
60
|
+
`Detected: ${detected.available ? "yes" : "no"}`,
|
|
61
|
+
`Enabled: ${resolved.enabled ? "yes" : "no"}`,
|
|
62
|
+
`CLI available: ${detected.cliAvailable ? "yes" : "no"}`,
|
|
63
|
+
`Socket: ${detected.socketPath}`,
|
|
64
|
+
`Workspace: ${detected.workspaceId ?? "(none)"}`,
|
|
65
|
+
`Surface: ${detected.surfaceId ?? "(none)"}`,
|
|
66
|
+
`Features: notifications=${resolved.notifications ? "on" : "off"}, sidebar=${resolved.sidebar ? "on" : "off"}, splits=${resolved.splits ? "on" : "off"}, browser=${resolved.browser ? "on" : "off"}`,
|
|
67
|
+
`Capabilities: access=${accessMode}, methods=${methods}`,
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ensureCmuxAvailableForEnable(ctx: ExtensionCommandContext): boolean {
|
|
72
|
+
const detected = detectCmuxEnvironment();
|
|
73
|
+
if (detected.available) return true;
|
|
74
|
+
ctx.ui.notify(
|
|
75
|
+
"cmux not detected. Install it from https://cmux.com and run gsd inside a cmux terminal.",
|
|
76
|
+
"warning",
|
|
77
|
+
);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function handleCmux(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
82
|
+
const trimmed = args.trim();
|
|
83
|
+
if (!trimmed || trimmed === "status") {
|
|
84
|
+
ctx.ui.notify(formatCmuxStatus(), "info");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (trimmed === "on") {
|
|
89
|
+
if (!ensureCmuxAvailableForEnable(ctx)) return;
|
|
90
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
91
|
+
prefs.cmux = {
|
|
92
|
+
enabled: true,
|
|
93
|
+
notifications: true,
|
|
94
|
+
sidebar: true,
|
|
95
|
+
splits: false,
|
|
96
|
+
browser: false,
|
|
97
|
+
...((prefs.cmux as Record<string, unknown> | undefined) ?? {}),
|
|
98
|
+
};
|
|
99
|
+
(prefs.cmux as Record<string, unknown>).enabled = true;
|
|
100
|
+
});
|
|
101
|
+
ctx.ui.notify("cmux integration enabled in project preferences.", "info");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (trimmed === "off") {
|
|
106
|
+
const effective = loadEffectiveGSDPreferences()?.preferences;
|
|
107
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
108
|
+
prefs.cmux = { ...((prefs.cmux as Record<string, unknown> | undefined) ?? {}), enabled: false };
|
|
109
|
+
});
|
|
110
|
+
clearCmuxSidebar(effective);
|
|
111
|
+
ctx.ui.notify("cmux integration disabled in project preferences.", "info");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const parts = trimmed.split(/\s+/);
|
|
116
|
+
if (parts.length === 2 && ["notifications", "sidebar", "splits", "browser"].includes(parts[0]) && ["on", "off"].includes(parts[1])) {
|
|
117
|
+
const feature = parts[0] as "notifications" | "sidebar" | "splits" | "browser";
|
|
118
|
+
const enabled = parts[1] === "on";
|
|
119
|
+
if (enabled && !ensureCmuxAvailableForEnable(ctx)) return;
|
|
120
|
+
|
|
121
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
122
|
+
const next = { ...((prefs.cmux as Record<string, unknown> | undefined) ?? {}) };
|
|
123
|
+
next[feature] = enabled;
|
|
124
|
+
if (enabled) next.enabled = true;
|
|
125
|
+
prefs.cmux = next;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!enabled && feature === "sidebar") {
|
|
129
|
+
clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const note = feature === "browser" && enabled
|
|
133
|
+
? " Browser surfaces are still a follow-up path."
|
|
134
|
+
: "";
|
|
135
|
+
ctx.ui.notify(`cmux ${feature} ${enabled ? "enabled" : "disabled"}.${note}`, "info");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ctx.ui.notify(
|
|
140
|
+
"Usage: /gsd cmux <status|on|off|notifications on|notifications off|sidebar on|sidebar off|splits on|splits off|browser on|browser off>",
|
|
141
|
+
"info",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -740,7 +740,7 @@ export function serializePreferencesToFrontmatter(prefs: Record<string, unknown>
|
|
|
740
740
|
"skill_rules", "custom_instructions", "models", "skill_discovery",
|
|
741
741
|
"skill_staleness_days", "auto_supervisor", "uat_dispatch", "unique_milestone_ids",
|
|
742
742
|
"budget_ceiling", "budget_enforcement", "context_pause_threshold",
|
|
743
|
-
"notifications", "remote_questions", "git",
|
|
743
|
+
"notifications", "cmux", "remote_questions", "git",
|
|
744
744
|
"post_unit_hooks", "pre_dispatch_hooks",
|
|
745
745
|
"dynamic_routing", "token_profile", "phases", "parallel",
|
|
746
746
|
"auto_visualize", "auto_report",
|
|
@@ -49,6 +49,7 @@ import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
|
49
49
|
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
|
+
import { handleCmux } from "./commands-cmux.js";
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
/** Resolve the effective project root, accounting for worktree paths. */
|
|
@@ -105,7 +106,7 @@ function notifyRemoteAutoActive(ctx: ExtensionCommandContext, basePath: string):
|
|
|
105
106
|
|
|
106
107
|
export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
107
108
|
pi.registerCommand("gsd", {
|
|
108
|
-
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|update",
|
|
109
|
+
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|update",
|
|
109
110
|
getArgumentCompletions: (prefix: string) => {
|
|
110
111
|
const subcommands = [
|
|
111
112
|
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
|
@@ -114,6 +115,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
114
115
|
{ cmd: "stop", desc: "Stop auto mode gracefully" },
|
|
115
116
|
{ cmd: "pause", desc: "Pause auto-mode (preserves state, /gsd auto to resume)" },
|
|
116
117
|
{ cmd: "status", desc: "Progress dashboard" },
|
|
118
|
+
{ cmd: "widget", desc: "Cycle widget: full → small → min → off" },
|
|
117
119
|
{ cmd: "visualize", desc: "Open 10-tab workflow visualizer (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)" },
|
|
118
120
|
{ cmd: "queue", desc: "Queue and reorder future milestones" },
|
|
119
121
|
{ cmd: "quick", desc: "Execute a quick task without full planning overhead" },
|
|
@@ -147,6 +149,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
147
149
|
{ cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
|
|
148
150
|
{ cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
|
|
149
151
|
{ cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
|
|
152
|
+
{ cmd: "cmux", desc: "Manage cmux integration (status, sidebar, notifications, splits)" },
|
|
150
153
|
{ cmd: "park", desc: "Park a milestone — skip without deleting" },
|
|
151
154
|
{ cmd: "unpark", desc: "Reactivate a parked milestone" },
|
|
152
155
|
{ cmd: "update", desc: "Update GSD to the latest version" },
|
|
@@ -203,6 +206,38 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
203
206
|
.map((s) => ({ value: `parallel ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
204
207
|
}
|
|
205
208
|
|
|
209
|
+
if (parts[0] === "cmux") {
|
|
210
|
+
if (parts.length <= 2) {
|
|
211
|
+
const subPrefix = parts[1] ?? "";
|
|
212
|
+
const subs = [
|
|
213
|
+
{ cmd: "status", desc: "Show cmux detection, prefs, and capabilities" },
|
|
214
|
+
{ cmd: "on", desc: "Enable cmux integration" },
|
|
215
|
+
{ cmd: "off", desc: "Disable cmux integration" },
|
|
216
|
+
{ cmd: "notifications", desc: "Toggle cmux desktop notifications" },
|
|
217
|
+
{ cmd: "sidebar", desc: "Toggle cmux sidebar metadata" },
|
|
218
|
+
{ cmd: "splits", desc: "Toggle cmux visual subagent splits" },
|
|
219
|
+
{ cmd: "browser", desc: "Toggle future browser integration flag" },
|
|
220
|
+
];
|
|
221
|
+
return subs
|
|
222
|
+
.filter((s) => s.cmd.startsWith(subPrefix))
|
|
223
|
+
.map((s) => ({ value: `cmux ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (parts.length <= 3 && ["notifications", "sidebar", "splits", "browser"].includes(parts[1])) {
|
|
227
|
+
const togglePrefix = parts[2] ?? "";
|
|
228
|
+
return [
|
|
229
|
+
{ cmd: "on", desc: "Enable this cmux area" },
|
|
230
|
+
{ cmd: "off", desc: "Disable this cmux area" },
|
|
231
|
+
]
|
|
232
|
+
.filter((item) => item.cmd.startsWith(togglePrefix))
|
|
233
|
+
.map((item) => ({
|
|
234
|
+
value: `cmux ${parts[1]} ${item.cmd}`,
|
|
235
|
+
label: item.cmd,
|
|
236
|
+
description: item.desc,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
206
241
|
if (parts[0] === "setup" && parts.length <= 2) {
|
|
207
242
|
const subPrefix = parts[1] ?? "";
|
|
208
243
|
const subs = [
|
|
@@ -474,6 +509,18 @@ export async function handleGSDCommand(
|
|
|
474
509
|
return;
|
|
475
510
|
}
|
|
476
511
|
|
|
512
|
+
if (trimmed === "widget" || trimmed.startsWith("widget ")) {
|
|
513
|
+
const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
|
|
514
|
+
const arg = trimmed.replace(/^widget\s*/, "").trim();
|
|
515
|
+
if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
|
|
516
|
+
setWidgetMode(arg);
|
|
517
|
+
} else {
|
|
518
|
+
cycleWidgetMode();
|
|
519
|
+
}
|
|
520
|
+
ctx.ui.notify(`Widget: ${getWidgetMode()}`, "info");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
477
524
|
if (trimmed === "visualize") {
|
|
478
525
|
await handleVisualize(ctx);
|
|
479
526
|
return;
|
|
@@ -493,6 +540,11 @@ export async function handleGSDCommand(
|
|
|
493
540
|
return;
|
|
494
541
|
}
|
|
495
542
|
|
|
543
|
+
if (trimmed === "cmux" || trimmed.startsWith("cmux ")) {
|
|
544
|
+
await handleCmux(trimmed.replace(/^cmux\s*/, "").trim(), ctx);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
496
548
|
if (trimmed === "init") {
|
|
497
549
|
const { detectProjectState } = await import("./detection.js");
|
|
498
550
|
const { showProjectInit, handleReinit } = await import("./init-wizard.js");
|
|
@@ -996,6 +1048,7 @@ function showHelp(ctx: ExtensionCommandContext): void {
|
|
|
996
1048
|
" /gsd setup Global setup status [llm|search|remote|keys|prefs]",
|
|
997
1049
|
" /gsd mode Set workflow mode (solo/team) [global|project]",
|
|
998
1050
|
" /gsd prefs Manage preferences [global|project|status|wizard|setup|import-claude]",
|
|
1051
|
+
" /gsd cmux Manage cmux integration [status|on|off|notifications|sidebar|splits|browser]",
|
|
999
1052
|
" /gsd config Set API keys for external tools",
|
|
1000
1053
|
" /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
|
|
1001
1054
|
" /gsd hooks Show post-unit hook configuration",
|
|
@@ -173,6 +173,13 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
173
173
|
- `on_milestone`: boolean — notify when a milestone finishes. Default: `true`.
|
|
174
174
|
- `on_attention`: boolean — notify when manual attention is needed. Default: `true`.
|
|
175
175
|
|
|
176
|
+
- `cmux`: configures cmux terminal integration when GSD is running inside a cmux workspace. Keys:
|
|
177
|
+
- `enabled`: boolean — master toggle for cmux integration. Default: `false`.
|
|
178
|
+
- `notifications`: boolean — route desktop notifications through cmux. Default: `true` when enabled.
|
|
179
|
+
- `sidebar`: boolean — publish status, progress, and log metadata to the cmux sidebar. Default: `true` when enabled.
|
|
180
|
+
- `splits`: boolean — run supported subagent work in visible cmux splits. Default: `false`.
|
|
181
|
+
- `browser`: boolean — reserve the future browser integration flag. Default: `false`.
|
|
182
|
+
|
|
176
183
|
- `dynamic_routing`: configures the dynamic model router that adjusts model selection based on task complexity. Keys:
|
|
177
184
|
- `enabled`: boolean — enable dynamic routing. Default: `false`.
|
|
178
185
|
- `tier_models`: object — model overrides per complexity tier. Keys: `light`, `standard`, `heavy`. Values are model ID strings.
|
|
@@ -477,6 +484,24 @@ Disables per-unit completion notifications (noisy in long runs) while keeping er
|
|
|
477
484
|
|
|
478
485
|
---
|
|
479
486
|
|
|
487
|
+
## cmux Example
|
|
488
|
+
|
|
489
|
+
```yaml
|
|
490
|
+
---
|
|
491
|
+
version: 1
|
|
492
|
+
cmux:
|
|
493
|
+
enabled: true
|
|
494
|
+
notifications: true
|
|
495
|
+
sidebar: true
|
|
496
|
+
splits: true
|
|
497
|
+
browser: false
|
|
498
|
+
---
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Enables cmux-aware notifications, sidebar metadata, and visible subagent splits when GSD is running inside a cmux terminal.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
480
505
|
## Post-Unit Hooks Example
|
|
481
506
|
|
|
482
507
|
```yaml
|
|
@@ -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,
|
|
@@ -65,6 +65,7 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
|
|
|
65
65
|
import { toPosixPath } from "../shared/mod.js";
|
|
66
66
|
import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
|
|
67
67
|
import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
|
|
68
|
+
import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js";
|
|
68
69
|
|
|
69
70
|
// ── Agent Instructions (DEPRECATED) ──────────────────────────────────────
|
|
70
71
|
// agent-instructions.md is deprecated. Use AGENTS.md or CLAUDE.md instead.
|
|
@@ -623,6 +624,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
623
624
|
const stopContextTimer = debugTime("context-inject");
|
|
624
625
|
const systemContent = loadPrompt("system");
|
|
625
626
|
const loadedPreferences = loadEffectiveGSDPreferences();
|
|
627
|
+
if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
|
|
628
|
+
markCmuxPromptShown();
|
|
629
|
+
ctx.ui.notify(
|
|
630
|
+
"cmux detected. Run /gsd cmux on to enable sidebar metadata, notifications, and visual subagent splits for this project.",
|
|
631
|
+
"info",
|
|
632
|
+
);
|
|
633
|
+
}
|
|
626
634
|
let preferenceBlock = "";
|
|
627
635
|
if (loadedPreferences) {
|
|
628
636
|
const cwd = process.cwd();
|