gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.add4f78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/app-paths.js +1 -1
  2. package/dist/extension-registry.js +2 -2
  3. package/dist/remote-questions-config.js +2 -2
  4. package/dist/resources/extensions/env-utils.js +29 -0
  5. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  6. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +7 -8
  8. package/dist/resources/extensions/gsd/auto-loop.js +54 -30
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -71
  10. package/dist/resources/extensions/gsd/auto-worktree-sync.js +2 -1
  11. package/dist/resources/extensions/gsd/auto.js +10 -26
  12. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  13. package/dist/resources/extensions/gsd/commands.js +2 -1
  14. package/dist/resources/extensions/gsd/detection.js +1 -2
  15. package/dist/resources/extensions/gsd/export.js +1 -1
  16. package/dist/resources/extensions/gsd/files.js +2 -2
  17. package/dist/resources/extensions/gsd/forensics.js +1 -1
  18. package/dist/resources/extensions/gsd/index.js +2 -1
  19. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  20. package/dist/resources/extensions/gsd/preferences-validation.js +1 -1
  21. package/dist/resources/extensions/gsd/preferences.js +4 -3
  22. package/dist/resources/extensions/gsd/repo-identity.js +2 -1
  23. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  24. package/dist/resources/extensions/gsd/state.js +1 -1
  25. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  26. package/dist/resources/extensions/remote-questions/status.js +2 -1
  27. package/dist/resources/extensions/remote-questions/store.js +2 -1
  28. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  29. package/dist/resources/extensions/subagent/isolation.js +2 -1
  30. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  31. package/package.json +1 -1
  32. package/src/resources/extensions/env-utils.ts +31 -0
  33. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  34. package/src/resources/extensions/gsd/auto/session.ts +5 -1
  35. package/src/resources/extensions/gsd/auto-dispatch.ts +6 -8
  36. package/src/resources/extensions/gsd/auto-loop.ts +70 -63
  37. package/src/resources/extensions/gsd/auto-post-unit.ts +52 -42
  38. package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -1
  39. package/src/resources/extensions/gsd/auto.ts +14 -29
  40. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  41. package/src/resources/extensions/gsd/commands.ts +3 -1
  42. package/src/resources/extensions/gsd/detection.ts +2 -2
  43. package/src/resources/extensions/gsd/export.ts +1 -1
  44. package/src/resources/extensions/gsd/files.ts +2 -2
  45. package/src/resources/extensions/gsd/forensics.ts +1 -1
  46. package/src/resources/extensions/gsd/index.ts +3 -1
  47. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  48. package/src/resources/extensions/gsd/preferences-validation.ts +1 -1
  49. package/src/resources/extensions/gsd/preferences.ts +5 -3
  50. package/src/resources/extensions/gsd/repo-identity.ts +3 -1
  51. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  52. package/src/resources/extensions/gsd/state.ts +1 -1
  53. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  54. package/src/resources/extensions/remote-questions/status.ts +3 -1
  55. package/src/resources/extensions/remote-questions/store.ts +3 -1
  56. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  57. package/src/resources/extensions/subagent/isolation.ts +3 -1
  58. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
@@ -33,7 +33,6 @@ import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.j
33
33
  import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
34
34
  import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
35
35
  import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
36
- import { resetRewriteCircuitBreaker } from "./auto-dispatch.js";
37
36
  import { isDbAvailable } from "./gsd-db.js";
38
37
  import { consumeSignal } from "./session-status-io.js";
39
38
  import {
@@ -56,6 +55,13 @@ import { join } from "node:path";
56
55
  /** Throttle STATE.md rebuilds — at most once per 30 seconds */
57
56
  const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
58
57
 
58
+ export interface PreVerificationOpts {
59
+ skipSettleDelay?: boolean;
60
+ skipDoctor?: boolean;
61
+ skipStateRebuild?: boolean;
62
+ skipWorktreeSync?: boolean;
63
+ }
64
+
59
65
  export interface PostUnitContext {
60
66
  s: AutoSession;
61
67
  ctx: ExtensionContext;
@@ -73,7 +79,7 @@ export interface PostUnitContext {
73
79
  *
74
80
  * Returns "dispatched" if a signal caused stop/pause, "continue" to proceed.
75
81
  */
76
- export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"dispatched" | "continue"> {
82
+ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreVerificationOpts): Promise<"dispatched" | "continue"> {
77
83
  const { s, ctx, pi, buildSnapshotOpts, stopAuto, pauseAuto } = pctx;
78
84
 
79
85
  // ── Parallel worker signal check ──
@@ -95,8 +101,10 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
95
101
  // Invalidate all caches
96
102
  invalidateAllCaches();
97
103
 
98
- // Small delay to let files settle
99
- await new Promise(r => setTimeout(r, 500));
104
+ // Small delay to let files settle (skipped for sidecars where latency matters more)
105
+ if (!opts?.skipSettleDelay) {
106
+ await new Promise(r => setTimeout(r, 100));
107
+ }
100
108
 
101
109
  // Auto-commit
102
110
  if (s.currentUnit) {
@@ -120,8 +128,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
120
128
  keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
121
129
  };
122
130
  }
123
- } catch {
124
- // Non-fatal
131
+ } catch (e) {
132
+ debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
125
133
  }
126
134
  }
127
135
  }
@@ -131,12 +139,12 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
131
139
  if (commitMsg) {
132
140
  ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
133
141
  }
134
- } catch {
135
- // Non-fatal
142
+ } catch (e) {
143
+ debugLog("postUnit", { phase: "auto-commit", error: String(e) });
136
144
  }
137
145
 
138
- // Doctor: fix mechanical bookkeeping
139
- try {
146
+ // Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
147
+ if (!opts?.skipDoctor) try {
140
148
  const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
141
149
  const doctorScope = scopeParts.join("/");
142
150
  const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
@@ -168,24 +176,26 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
168
176
  const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
169
177
  const structuredIssues = formatDoctorIssuesForPrompt(actionable);
170
178
  dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
171
- } catch {
172
- // Non-fatal
179
+ } catch (e) {
180
+ debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
173
181
  }
174
182
  }
175
183
  }
176
- } catch {
177
- // Non-fatal
184
+ } catch (e) {
185
+ debugLog("postUnit", { phase: "doctor", error: String(e) });
178
186
  }
179
187
 
180
- // Throttled STATE.md rebuild
181
- const now = Date.now();
182
- if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
183
- try {
184
- await rebuildState(s.basePath);
185
- s.lastStateRebuildAt = now;
186
- autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
187
- } catch {
188
- // Non-fatal
188
+ // Throttled STATE.md rebuild (skipped for lightweight sidecars)
189
+ if (!opts?.skipStateRebuild) {
190
+ const now = Date.now();
191
+ if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
192
+ try {
193
+ await rebuildState(s.basePath);
194
+ s.lastStateRebuildAt = now;
195
+ autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
196
+ } catch (e) {
197
+ debugLog("postUnit", { phase: "state-rebuild", error: String(e) });
198
+ }
189
199
  }
190
200
  }
191
201
 
@@ -193,16 +203,16 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
193
203
  try {
194
204
  const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
195
205
  pruneDeadProcesses();
196
- } catch {
197
- // Non-fatal
206
+ } catch (e) {
207
+ debugLog("postUnit", { phase: "prune-bg-shell", error: String(e) });
198
208
  }
199
209
 
200
- // Sync worktree state back to project root
201
- if (s.originalBasePath && s.originalBasePath !== s.basePath) {
210
+ // Sync worktree state back to project root (skipped for lightweight sidecars)
211
+ if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
202
212
  try {
203
213
  syncStateToProjectRoot(s.basePath, s.originalBasePath, s.currentMilestoneId);
204
- } catch {
205
- // Non-fatal
214
+ } catch (e) {
215
+ debugLog("postUnit", { phase: "worktree-sync", error: String(e) });
206
216
  }
207
217
  }
208
218
 
@@ -210,10 +220,10 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
210
220
  if (s.currentUnit.type === "rewrite-docs") {
211
221
  try {
212
222
  await resolveAllOverrides(s.basePath);
213
- resetRewriteCircuitBreaker();
223
+ s.rewriteAttemptCount = 0;
214
224
  ctx.ui.notify("Override(s) resolved — rewrite-docs completed.", "info");
215
- } catch {
216
- // Non-fatal
225
+ } catch (e) {
226
+ debugLog("postUnit", { phase: "rewrite-docs-resolve", error: String(e) });
217
227
  }
218
228
  }
219
229
 
@@ -226,8 +236,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
226
236
  const { clearReactiveState } = await import("./reactive-graph.js");
227
237
  clearReactiveState(s.basePath, mid, sid);
228
238
  }
229
- } catch {
230
- // Non-fatal
239
+ } catch (e) {
240
+ debugLog("postUnit", { phase: "reactive-state-cleanup", error: String(e) });
231
241
  }
232
242
  }
233
243
 
@@ -280,8 +290,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
280
290
  if (triggerArtifactVerified) {
281
291
  invalidateAllCaches();
282
292
  }
283
- } catch {
284
- // Non-fatal
293
+ } catch (e) {
294
+ debugLog("postUnit", { phase: "artifact-verify", error: String(e) });
285
295
  }
286
296
  } else {
287
297
  // Hook unit completed — finalize its runtime record
@@ -292,8 +302,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
292
302
  lastProgressKind: "hook-completed",
293
303
  });
294
304
  clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
295
- } catch {
296
- // Non-fatal
305
+ } catch (e) {
306
+ debugLog("postUnit", { phase: "hook-finalize", error: String(e) });
297
307
  }
298
308
  }
299
309
  }
@@ -429,8 +439,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
429
439
  }
430
440
  }
431
441
  }
432
- } catch {
433
- // Triage check failure is non-fatal
442
+ } catch (e) {
443
+ debugLog("postUnit", { phase: "triage-check", error: String(e) });
434
444
  }
435
445
  }
436
446
 
@@ -475,8 +485,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
475
485
  );
476
486
 
477
487
  return "continue";
478
- } catch {
479
- // Non-fatal proceed to normal dispatch
488
+ } catch (e) {
489
+ debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
480
490
  }
481
491
  }
482
492
 
@@ -22,6 +22,8 @@ import { join, sep as pathSep } from "node:path";
22
22
  import { homedir } from "node:os";
23
23
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
24
24
 
25
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
26
+
25
27
  // ─── Project Root → Worktree Sync ─────────────────────────────────────────
26
28
 
27
29
  /**
@@ -111,7 +113,7 @@ export function syncStateToProjectRoot(
111
113
  */
112
114
  export function readResourceVersion(): string | null {
113
115
  const agentDir =
114
- process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
116
+ process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
115
117
  const manifestPath = join(agentDir, "managed-resources.json");
116
118
  try {
117
119
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
@@ -622,43 +622,28 @@ export async function stopAuto(
622
622
  if (existsSync(pausedPath)) unlinkSync(pausedPath);
623
623
  } catch { /* non-fatal */ }
624
624
 
625
- s.active = false;
626
- s.paused = false;
627
- s.stepMode = false;
628
- s.unitDispatchCount.clear();
629
- s.unitRecoveryCount.clear();
630
- clearInFlightTools();
631
- s.lastBudgetAlertLevel = 0;
632
- s.lastStateRebuildAt = 0;
633
- s.unitLifetimeDispatches.clear();
634
- s.currentUnit = null;
635
- s.autoModeStartModel = null;
636
- s.currentMilestoneId = null;
637
- s.originalBasePath = "";
638
- s.completedUnits = [];
639
- s.pendingQuickTasks = [];
640
- clearSliceProgressCache();
641
- clearActivityLogState();
642
- resetProactiveHealing();
643
- s.pendingCrashRecovery = null;
644
- s.pendingVerificationRetry = null;
645
- s.verificationRetryCount.clear();
646
- s.pausedSessionFile = null;
647
- ctx?.ui.setStatus("gsd-auto", undefined);
648
- ctx?.ui.setWidget("gsd-progress", undefined);
649
- ctx?.ui.setFooter(undefined);
650
-
625
+ // Restore original model before reset() clears the IDs
651
626
  if (pi && ctx && s.originalModelId && s.originalModelProvider) {
652
627
  const original = ctx.modelRegistry.find(
653
628
  s.originalModelProvider,
654
629
  s.originalModelId,
655
630
  );
656
631
  if (original) await pi.setModel(original);
657
- s.originalModelId = null;
658
- s.originalModelProvider = null;
659
632
  }
660
633
 
661
- s.cmdCtx = null;
634
+ // External cleanup (not covered by session reset)
635
+ clearInFlightTools();
636
+ clearSliceProgressCache();
637
+ clearActivityLogState();
638
+ resetProactiveHealing();
639
+
640
+ // UI cleanup
641
+ ctx?.ui.setStatus("gsd-auto", undefined);
642
+ ctx?.ui.setWidget("gsd-progress", undefined);
643
+ ctx?.ui.setFooter(undefined);
644
+
645
+ // Reset all session state in one call
646
+ s.reset();
662
647
  }
663
648
 
664
649
  /**
@@ -11,6 +11,8 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFile
11
11
  import { dirname, join } from "node:path";
12
12
  import { homedir } from "node:os";
13
13
 
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
+
14
16
  // ─── Types (mirrored from extension-registry.ts) ────────────────────────────
15
17
 
16
18
  interface ExtensionManifest {
@@ -48,11 +50,11 @@ interface ExtensionRegistry {
48
50
  // ─── Registry I/O ───────────────────────────────────────────────────────────
49
51
 
50
52
  function getRegistryPath(): string {
51
- return join(homedir(), ".gsd", "extensions", "registry.json");
53
+ return join(gsdHome, "extensions", "registry.json");
52
54
  }
53
55
 
54
56
  function getAgentExtensionsDir(): string {
55
- return join(homedir(), ".gsd", "agent", "extensions");
57
+ return join(gsdHome, "agent", "extensions");
56
58
  }
57
59
 
58
60
  function loadRegistry(): ExtensionRegistry {
@@ -10,6 +10,8 @@ import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { gsdRoot } from "./paths.js";
13
+
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
13
15
  import { enableDebug } from "./debug-logger.js";
14
16
  import { deriveState } from "./state.js";
15
17
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
@@ -482,7 +484,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
482
484
  if (parts.length === 3 && ["enable", "disable", "info"].includes(parts[1])) {
483
485
  const idPrefix = parts[2] ?? "";
484
486
  try {
485
- const extDir = join(homedir(), ".gsd", "agent", "extensions");
487
+ const extDir = join(gsdHome, "agent", "extensions");
486
488
  const ids: { id: string; name: string }[] = [];
487
489
  for (const entry of readdirSync(extDir, { withFileTypes: true })) {
488
490
  if (!entry.isDirectory()) continue;
@@ -11,6 +11,8 @@ import { join } from "node:path";
11
11
  import { homedir } from "node:os";
12
12
  import { gsdRoot } from "./paths.js";
13
13
 
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
+
14
16
  // ─── Types ──────────────────────────────────────────────────────────────────────
15
17
 
16
18
  export interface ProjectDetection {
@@ -400,7 +402,6 @@ function detectVerificationCommands(
400
402
  * Check if global GSD setup exists (has ~/.gsd/ with preferences).
401
403
  */
402
404
  export function hasGlobalSetup(): boolean {
403
- const gsdHome = join(homedir(), ".gsd");
404
405
  return (
405
406
  existsSync(join(gsdHome, "preferences.md")) ||
406
407
  existsSync(join(gsdHome, "PREFERENCES.md"))
@@ -412,7 +413,6 @@ export function hasGlobalSetup(): boolean {
412
413
  * Returns true if ~/.gsd/ doesn't exist or has no preferences or auth.
413
414
  */
414
415
  export function isFirstEverLaunch(): boolean {
415
- const gsdHome = join(homedir(), ".gsd");
416
416
  if (!existsSync(gsdHome)) return true;
417
417
 
418
418
  // If we have preferences, not first launch
@@ -11,7 +11,7 @@ import {
11
11
  } from "./metrics.js";
12
12
  import type { UnitMetrics } from "./metrics.js";
13
13
  import { gsdRoot } from "./paths.js";
14
- import { formatDuration, fileLink } from "../shared/mod.js";
14
+ import { formatDuration, fileLink } from "../shared/format-utils.js";
15
15
  import { getErrorMessage } from "./error-utils.js";
16
16
 
17
17
  /**
@@ -7,7 +7,7 @@ import { promises as fs } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
8
  import { atomicWriteAsync } from './atomic-write.js';
9
9
  import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
10
- import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
10
+ import { milestoneIdSort, findMilestoneIds } from './milestone-ids.js';
11
11
 
12
12
  import type {
13
13
  Roadmap, BoundaryMapEntry,
@@ -20,7 +20,7 @@ import type {
20
20
  ManifestStatus,
21
21
  } from './types.js';
22
22
 
23
- import { checkExistingEnvKeys } from '../get-secrets-from-user.js';
23
+ import { checkExistingEnvKeys } from '../env-utils.js';
24
24
  import { parseRoadmapSlices } from './roadmap-slices.js';
25
25
  import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
26
26
  import { debugTime, debugCount } from './debug-logger.js';
@@ -27,7 +27,7 @@ import { deriveState } from "./state.js";
27
27
  import { isAutoActive } from "./auto.js";
28
28
  import { loadPrompt } from "./prompt-loader.js";
29
29
  import { gsdRoot } from "./paths.js";
30
- import { formatDuration } from "../shared/mod.js";
30
+ import { formatDuration } from "../shared/format-utils.js";
31
31
  import { getAutoWorktreePath } from "./auto-worktree.js";
32
32
 
33
33
  // ─── Types ────────────────────────────────────────────────────────────────────
@@ -60,6 +60,8 @@ import { join } from "node:path";
60
60
  import { existsSync, readFileSync } from "node:fs";
61
61
  import { homedir } from "node:os";
62
62
  import { shortcutDesc } from "../shared/mod.js";
63
+
64
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
63
65
  import { Text } from "@gsd/pi-tui";
64
66
  import { pauseAutoForProviderError, classifyProviderError } from "./provider-error-pause.js";
65
67
  import { toPosixPath } from "../shared/mod.js";
@@ -73,7 +75,7 @@ import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js"
73
75
 
74
76
  function warnDeprecatedAgentInstructions(): void {
75
77
  const paths = [
76
- join(homedir(), ".gsd", "agent-instructions.md"),
78
+ join(gsdHome, "agent-instructions.md"),
77
79
  join(process.cwd(), ".gsd", "agent-instructions.md"),
78
80
  ];
79
81
  for (const p of paths) {
@@ -3,7 +3,7 @@
3
3
  // Zero Pi dependencies — uses only exported helpers from files.ts.
4
4
 
5
5
  import { splitFrontmatter, parseFrontmatterMap, extractBoldField } from '../files.js';
6
- import { normalizeStringArray } from '../../shared/mod.js';
6
+ import { normalizeStringArray } from '../../shared/format-utils.js';
7
7
 
8
8
  import type {
9
9
  PlanningRoadmap,
@@ -10,7 +10,7 @@ import type { GitPreferences } from "./git-service.js";
10
10
  import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile, PhaseSkipPreferences } from "./types.js";
11
11
  import type { DynamicRoutingConfig } from "./model-router.js";
12
12
  import { VALID_BRANCH_NAME } from "./git-service.js";
13
- import { normalizeStringArray } from "../shared/mod.js";
13
+ import { normalizeStringArray } from "../shared/format-utils.js";
14
14
 
15
15
  import {
16
16
  KNOWN_PREFERENCE_KEYS,
@@ -13,11 +13,13 @@
13
13
  import { existsSync, readFileSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import { join } from "node:path";
16
+
17
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
16
18
  import { gsdRoot } from "./paths.js";
17
19
  import { parse as parseYaml } from "yaml";
18
20
  import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile } from "./types.js";
19
21
  import type { DynamicRoutingConfig } from "./model-router.js";
20
- import { normalizeStringArray } from "../shared/mod.js";
22
+ import { normalizeStringArray } from "../shared/format-utils.js";
21
23
  import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
22
24
 
23
25
  import {
@@ -82,14 +84,14 @@ export {
82
84
 
83
85
  // ─── Path Constants & Getters ───────────────────────────────────────────────
84
86
 
85
- const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
87
+ const GLOBAL_PREFERENCES_PATH = join(gsdHome, "preferences.md");
86
88
  const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
87
89
  function projectPreferencesPath(): string {
88
90
  return join(gsdRoot(process.cwd()), "preferences.md");
89
91
  }
90
92
  // Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
91
93
  // Check uppercase as a fallback so those files aren't silently ignored.
92
- const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(homedir(), ".gsd", "PREFERENCES.md");
94
+ const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(gsdHome, "PREFERENCES.md");
93
95
  function projectPreferencesPathUppercase(): string {
94
96
  return join(gsdRoot(process.cwd()), "PREFERENCES.md");
95
97
  }
@@ -12,6 +12,8 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, s
12
12
  import { homedir } from "node:os";
13
13
  import { join, resolve } from "node:path";
14
14
 
15
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
16
+
15
17
  // ─── Repo Identity ──────────────────────────────────────────────────────────
16
18
 
17
19
  /**
@@ -113,7 +115,7 @@ export function repoIdentity(basePath: string): string {
113
115
  * otherwise `~/.gsd/projects/<hash>`.
114
116
  */
115
117
  export function externalGsdRoot(basePath: string): string {
116
- const base = process.env.GSD_STATE_DIR || join(homedir(), ".gsd");
118
+ const base = process.env.GSD_STATE_DIR || gsdHome;
117
119
  return join(base, "projects", repoIdentity(basePath));
118
120
  }
119
121
 
@@ -11,6 +11,8 @@ import { join } from "node:path";
11
11
  import { homedir } from "node:os";
12
12
  import { resolveProjectRoot } from "./worktree.js";
13
13
 
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
+
14
16
  // ─── Resource Staleness ───────────────────────────────────────────────────
15
17
 
16
18
  /**
@@ -23,7 +25,7 @@ function isManifestWithVersion(data: unknown): data is { gsdVersion: string } {
23
25
  }
24
26
 
25
27
  export function readResourceVersion(): string | null {
26
- const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
28
+ const agentDir = process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
27
29
  const manifestPath = join(agentDir, "managed-resources.json");
28
30
  const manifest = loadJsonFileOrNull(manifestPath, isManifestWithVersion);
29
31
  return manifest?.gsdVersion ?? null;
@@ -31,7 +31,7 @@ import {
31
31
  gsdRoot,
32
32
  } from './paths.js';
33
33
 
34
- import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
34
+ import { milestoneIdSort, findMilestoneIds } from './milestone-ids.js';
35
35
  import { nativeBatchParseGsdFiles, type BatchParsedFile } from './native-parser-bridge.js';
36
36
 
37
37
  import { join, resolve } from 'path';
@@ -3,7 +3,7 @@
3
3
  import { existsSync, readFileSync, statSync } from 'node:fs';
4
4
  import { deriveState } from './state.js';
5
5
  import { parseRoadmap, parsePlan, parseSummary, loadFile } from './files.js';
6
- import { findMilestoneIds } from './guided-flow.js';
6
+ import { findMilestoneIds } from './milestone-ids.js';
7
7
  import { resolveMilestoneFile, resolveSliceFile, resolveGsdRootFile } from './paths.js';
8
8
  import {
9
9
  getLedger,
@@ -7,6 +7,8 @@ import { join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import { readPromptRecord } from "./store.js";
9
9
 
10
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
+
10
12
  export interface LatestPromptSummary {
11
13
  id: string;
12
14
  status: string;
@@ -14,7 +16,7 @@ export interface LatestPromptSummary {
14
16
  }
15
17
 
16
18
  export function getLatestPromptSummary(): LatestPromptSummary | null {
17
- const runtimeDir = join(homedir(), ".gsd", "runtime", "remote-questions");
19
+ const runtimeDir = join(gsdHome, "runtime", "remote-questions");
18
20
  if (!existsSync(runtimeDir)) return null;
19
21
  const files = readdirSync(runtimeDir).filter((f) => f.endsWith(".json"));
20
22
  if (files.length === 0) return null;
@@ -7,8 +7,10 @@ import { join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import type { RemotePrompt, RemotePromptRecord, RemotePromptRef, RemoteAnswer, RemotePromptStatus } from "./types.js";
9
9
 
10
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
+
10
12
  function runtimeDir(): string {
11
- return join(homedir(), ".gsd", "runtime", "remote-questions");
13
+ return join(gsdHome, "runtime", "remote-questions");
12
14
  }
13
15
 
14
16
  function recordPath(id: string): string {
@@ -17,7 +17,8 @@ import { resolveSearchProviderFromPreferences } from '../gsd/preferences.js'
17
17
  // Compute authFilePath locally instead of importing from app-paths.ts,
18
18
  // because extensions are copied to ~/.gsd/agent/extensions/ at runtime
19
19
  // where the relative import '../../../app-paths.ts' doesn't resolve.
20
- const authFilePath = join(homedir(), '.gsd', 'agent', 'auth.json')
20
+ const gsdHome = process.env.GSD_HOME || join(homedir(), '.gsd')
21
+ const authFilePath = join(gsdHome, 'agent', 'auth.json')
21
22
 
22
23
  export type SearchProvider = 'tavily' | 'brave' | 'ollama'
23
24
  export type SearchProviderPreference = SearchProvider | 'auto'
@@ -57,8 +57,10 @@ function encodeCwd(cwd: string): string {
57
57
  return cwd.replace(/\//g, "--");
58
58
  }
59
59
 
60
+ const gsdHome = process.env.GSD_HOME || path.join(os.homedir(), ".gsd");
61
+
60
62
  function getIsolationBaseDir(cwd: string, taskId: string): string {
61
- return path.join(os.homedir(), ".gsd", "wt", encodeCwd(cwd), taskId);
63
+ return path.join(gsdHome, "wt", encodeCwd(cwd), taskId);
62
64
  }
63
65
 
64
66
  // Track active isolation dirs for cleanup on exit
@@ -8,6 +8,8 @@
8
8
  import { readdirSync, readFileSync, existsSync } from "node:fs";
9
9
  import { join, basename } from "node:path";
10
10
  import { homedir } from "node:os";
11
+
12
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
13
  import type { Rule } from "./ttsr-manager.js";
12
14
  import { splitFrontmatter, parseFrontmatterMap } from "../shared/frontmatter.js";
13
15
 
@@ -59,7 +61,7 @@ function scanDir(dir: string): Rule[] {
59
61
  * Project rules override global rules with the same name.
60
62
  */
61
63
  export function loadRules(cwd: string): Rule[] {
62
- const globalDir = join(homedir(), ".gsd", "agent", "rules");
64
+ const globalDir = join(gsdHome, "agent", "rules");
63
65
  const projectDir = join(cwd, ".gsd", "rules");
64
66
 
65
67
  const globalRules = scanDir(globalDir);