gsd-pi 2.35.0-dev.30eec3f → 2.35.0-dev.640d5c7

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 (88) hide show
  1. package/dist/cli.js +2 -7
  2. package/dist/resource-loader.d.ts +1 -1
  3. package/dist/resource-loader.js +1 -13
  4. package/dist/resources/extensions/async-jobs/await-tool.js +2 -0
  5. package/dist/resources/extensions/async-jobs/job-manager.js +6 -0
  6. package/dist/resources/extensions/bg-shell/output-formatter.js +19 -1
  7. package/dist/resources/extensions/bg-shell/process-manager.js +4 -0
  8. package/dist/resources/extensions/bg-shell/types.js +2 -0
  9. package/dist/resources/extensions/context7/index.js +0 -5
  10. package/dist/resources/extensions/get-secrets-from-user.js +30 -2
  11. package/dist/resources/extensions/google-search/index.js +0 -5
  12. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -43
  13. package/dist/resources/extensions/gsd/auto-loop.js +1 -10
  14. package/dist/resources/extensions/gsd/auto-recovery.js +0 -35
  15. package/dist/resources/extensions/gsd/auto-start.js +2 -35
  16. package/dist/resources/extensions/gsd/auto.js +4 -59
  17. package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
  18. package/dist/resources/extensions/gsd/doctor-environment.js +17 -26
  19. package/dist/resources/extensions/gsd/files.js +1 -9
  20. package/dist/resources/extensions/gsd/gitignore.js +7 -54
  21. package/dist/resources/extensions/gsd/guided-flow.js +1 -1
  22. package/dist/resources/extensions/gsd/health-widget.js +46 -97
  23. package/dist/resources/extensions/gsd/index.js +1 -10
  24. package/dist/resources/extensions/gsd/migrate-external.js +2 -55
  25. package/dist/resources/extensions/gsd/paths.js +7 -74
  26. package/dist/resources/extensions/gsd/post-unit-hooks.js +1 -4
  27. package/dist/resources/extensions/gsd/preferences-validation.js +1 -16
  28. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
  29. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
  30. package/dist/resources/extensions/gsd/session-lock.js +2 -26
  31. package/dist/resources/extensions/gsd/templates/plan.md +0 -8
  32. package/dist/resources/extensions/gsd/worktree-resolver.js +0 -12
  33. package/dist/resources/extensions/remote-questions/remote-command.js +22 -2
  34. package/dist/resources/extensions/shared/mod.js +1 -1
  35. package/dist/resources/extensions/shared/sanitize.js +0 -30
  36. package/dist/resources/extensions/shared/wizard-ui.js +478 -0
  37. package/dist/resources/extensions/subagent/index.js +14 -6
  38. package/package.json +1 -2
  39. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/resource-loader.js +2 -13
  41. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  42. package/packages/pi-coding-agent/src/core/resource-loader.ts +2 -13
  43. package/src/resources/extensions/async-jobs/await-tool.ts +2 -0
  44. package/src/resources/extensions/async-jobs/job-manager.ts +7 -0
  45. package/src/resources/extensions/bg-shell/output-formatter.ts +17 -0
  46. package/src/resources/extensions/bg-shell/process-manager.ts +4 -0
  47. package/src/resources/extensions/bg-shell/types.ts +12 -0
  48. package/src/resources/extensions/context7/index.ts +0 -7
  49. package/src/resources/extensions/get-secrets-from-user.ts +35 -2
  50. package/src/resources/extensions/google-search/index.ts +0 -7
  51. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -49
  52. package/src/resources/extensions/gsd/auto-loop.ts +1 -11
  53. package/src/resources/extensions/gsd/auto-recovery.ts +0 -39
  54. package/src/resources/extensions/gsd/auto-start.ts +2 -42
  55. package/src/resources/extensions/gsd/auto.ts +3 -61
  56. package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
  57. package/src/resources/extensions/gsd/doctor-environment.ts +16 -26
  58. package/src/resources/extensions/gsd/files.ts +1 -10
  59. package/src/resources/extensions/gsd/gitignore.ts +7 -54
  60. package/src/resources/extensions/gsd/guided-flow.ts +1 -1
  61. package/src/resources/extensions/gsd/health-widget.ts +59 -103
  62. package/src/resources/extensions/gsd/index.ts +1 -10
  63. package/src/resources/extensions/gsd/migrate-external.ts +2 -47
  64. package/src/resources/extensions/gsd/paths.ts +7 -73
  65. package/src/resources/extensions/gsd/post-unit-hooks.ts +1 -5
  66. package/src/resources/extensions/gsd/preferences-validation.ts +1 -16
  67. package/src/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
  68. package/src/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
  69. package/src/resources/extensions/gsd/session-lock.ts +2 -29
  70. package/src/resources/extensions/gsd/templates/plan.md +0 -8
  71. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -12
  72. package/src/resources/extensions/gsd/tests/validate-directory.test.ts +0 -15
  73. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +0 -2
  74. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -32
  75. package/src/resources/extensions/gsd/worktree-resolver.ts +0 -11
  76. package/src/resources/extensions/remote-questions/remote-command.ts +23 -2
  77. package/src/resources/extensions/shared/mod.ts +1 -1
  78. package/src/resources/extensions/shared/sanitize.ts +0 -36
  79. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  80. package/src/resources/extensions/subagent/index.ts +12 -6
  81. package/dist/resources/extensions/gsd/health-widget-core.js +0 -96
  82. package/dist/resources/extensions/gsd/roadmap-mutations.js +0 -55
  83. package/src/resources/extensions/gsd/health-widget-core.ts +0 -129
  84. package/src/resources/extensions/gsd/roadmap-mutations.ts +0 -66
  85. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +0 -214
  86. package/src/resources/extensions/gsd/tests/health-widget.test.ts +0 -158
  87. package/src/resources/extensions/gsd/tests/paths.test.ts +0 -113
  88. package/src/resources/extensions/gsd/tests/test-utils.ts +0 -165
@@ -12,16 +12,14 @@
12
12
  import type { GSDState } from "./types.js";
13
13
  import type { GSDPreferences } from "./preferences.js";
14
14
  import type { UatType } from "./files.js";
15
- import { loadFile, extractUatType, loadActiveOverrides, parseRoadmap } from "./files.js";
15
+ import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
16
16
  import {
17
17
  resolveMilestoneFile,
18
18
  resolveMilestonePath,
19
19
  resolveSliceFile,
20
- resolveSlicePath,
21
20
  resolveTaskFile,
22
21
  relSliceFile,
23
22
  buildMilestoneFileName,
24
- buildSliceFileName,
25
23
  } from "./paths.js";
26
24
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
27
25
  import { join } from "node:path";
@@ -371,30 +369,6 @@ const DISPATCH_RULES: DispatchRule[] = [
371
369
  name: "validating-milestone → validate-milestone",
372
370
  match: async ({ state, mid, midTitle, basePath, prefs }) => {
373
371
  if (state.phase !== "validating-milestone") return null;
374
-
375
- // Safety guard (#1368): verify all roadmap slices have SUMMARY files before
376
- // allowing milestone validation. If any slice lacks a summary, the milestone
377
- // is not genuinely complete — something skipped earlier slices.
378
- const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
379
- const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
380
- if (roadmapContent) {
381
- const roadmap = parseRoadmap(roadmapContent);
382
- const missingSlices: string[] = [];
383
- for (const slice of roadmap.slices) {
384
- const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
385
- if (!summaryPath || !existsSync(summaryPath)) {
386
- missingSlices.push(slice.id);
387
- }
388
- }
389
- if (missingSlices.length > 0) {
390
- return {
391
- action: "stop",
392
- reason: `Cannot validate milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. These slices may have been skipped.`,
393
- level: "error",
394
- };
395
- }
396
- }
397
-
398
372
  // Skip preference: write a minimal pass-through VALIDATION file
399
373
  if (prefs?.phases?.skip_milestone_validation) {
400
374
  const mDir = resolveMilestonePath(basePath, mid);
@@ -430,28 +404,6 @@ const DISPATCH_RULES: DispatchRule[] = [
430
404
  name: "completing-milestone → complete-milestone",
431
405
  match: async ({ state, mid, midTitle, basePath }) => {
432
406
  if (state.phase !== "completing-milestone") return null;
433
-
434
- // Safety guard (#1368): verify all roadmap slices have SUMMARY files.
435
- const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
436
- const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
437
- if (roadmapContent) {
438
- const roadmap = parseRoadmap(roadmapContent);
439
- const missingSlices: string[] = [];
440
- for (const slice of roadmap.slices) {
441
- const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
442
- if (!summaryPath || !existsSync(summaryPath)) {
443
- missingSlices.push(slice.id);
444
- }
445
- }
446
- if (missingSlices.length > 0) {
447
- return {
448
- action: "stop",
449
- reason: `Cannot complete milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. Run /gsd doctor to diagnose.`,
450
- level: "error",
451
- };
452
- }
453
- }
454
-
455
407
  return {
456
408
  action: "dispatch",
457
409
  unitType: "complete-milestone",
@@ -221,15 +221,6 @@ export async function runUnit(
221
221
  s.pendingResolve = resolve;
222
222
  });
223
223
 
224
- // Ensure cwd matches basePath before dispatch (#1389).
225
- // async_bash and background jobs can drift cwd away from the worktree.
226
- // Realigning here prevents commits from landing on the wrong branch.
227
- try {
228
- if (process.cwd() !== s.basePath) {
229
- process.chdir(s.basePath);
230
- }
231
- } catch { /* non-fatal — chdir may fail if dir was removed */ }
232
-
233
224
  // ── Send the prompt ──
234
225
  debugLog("runUnit", { phase: "send-message", unitType, unitId });
235
226
 
@@ -353,7 +344,6 @@ export interface LoopDeps {
353
344
  getManifestStatus: (
354
345
  basePath: string,
355
346
  mid: string | undefined,
356
- projectRoot?: string,
357
347
  ) => Promise<{ pending: unknown[] } | null>;
358
348
  collectSecretsFromManifest: (
359
349
  basePath: string,
@@ -993,7 +983,7 @@ export async function autoLoop(
993
983
 
994
984
  // Secrets re-check gate
995
985
  try {
996
- const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
986
+ const manifestStatus = await deps.getManifestStatus(s.basePath, mid);
997
987
  if (manifestStatus && manifestStatus.pending.length > 0) {
998
988
  const result = await deps.collectSecretsFromManifest(
999
989
  s.basePath,
@@ -8,8 +8,6 @@
8
8
  */
9
9
 
10
10
  import type { ExtensionContext } from "@gsd/pi-coding-agent";
11
- import { parseUnitId } from "./unit-id.js";
12
- import { atomicWriteSync } from "./atomic-write.js";
13
11
  import { clearUnitRuntimeRecord } from "./unit-runtime.js";
14
12
  import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
15
13
  import { isValidationTerminal } from "./state.js";
@@ -37,7 +35,6 @@ import {
37
35
  clearPathCache,
38
36
  resolveGsdRootFile,
39
37
  } from "./paths.js";
40
- import { markSliceDoneInRoadmap } from "./roadmap-mutations.js";
41
38
  import {
42
39
  existsSync,
43
40
  mkdirSync,
@@ -502,42 +499,6 @@ export async function selfHealRuntimeRecords(
502
499
  for (const record of records) {
503
500
  const { unitType, unitId } = record;
504
501
 
505
- // Case 0: complete-slice with SUMMARY + UAT but unchecked roadmap (#1350).
506
- // If a complete-slice was interrupted after writing artifacts but before
507
- // flipping the roadmap checkbox, the verification fails and the dispatch
508
- // loop relaunches the same unit forever. Auto-fix the checkbox.
509
- if (unitType === "complete-slice") {
510
- const { milestone: mid, slice: sid } = parseUnitId(unitId);
511
- if (mid && sid) {
512
- const dir = resolveSlicePath(base, mid, sid);
513
- if (dir) {
514
- const summaryPath = join(dir, buildSliceFileName(sid, "SUMMARY"));
515
- const uatPath = join(dir, buildSliceFileName(sid, "UAT"));
516
- if (existsSync(summaryPath) && existsSync(uatPath)) {
517
- const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
518
- if (roadmapFile && existsSync(roadmapFile)) {
519
- try {
520
- const roadmapContent = readFileSync(roadmapFile, "utf-8");
521
- const roadmap = parseRoadmap(roadmapContent);
522
- const slice = (roadmap.slices ?? []).find(s => s.id === sid);
523
- if (slice && !slice.done) {
524
- // Auto-fix: flip the checkbox using shared utility
525
- if (markSliceDoneInRoadmap(base, mid, sid)) {
526
- ctx.ui.notify(
527
- `Self-heal: marked ${sid} done in roadmap (SUMMARY + UAT exist but checkbox was stale).`,
528
- "info",
529
- );
530
- }
531
- }
532
- } catch {
533
- // Roadmap parse failure — don't block self-heal
534
- }
535
- }
536
- }
537
- }
538
- }
539
- }
540
-
541
502
  // Clear stale dispatched records (dispatched > 1h ago, process crashed)
542
503
  const age = now - (record.startedAt ?? 0);
543
504
  if (record.phase === "dispatched" && age > STALE_THRESHOLD_MS) {
@@ -20,8 +20,6 @@ import {
20
20
  resolveSkillDiscoveryMode,
21
21
  getIsolationMode,
22
22
  } from "./preferences.js";
23
- import { ensureGsdSymlink } from "./repo-identity.js";
24
- import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
25
23
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
26
24
  import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
27
25
  import { invalidateAllCaches } from "./cache.js";
@@ -94,13 +92,6 @@ export interface BootstrapDeps {
94
92
  * Returns false if the bootstrap aborted (e.g., guided flow returned,
95
93
  * concurrent session detected). Returns true when ready to dispatch.
96
94
  */
97
-
98
- /** Guard: tracks consecutive bootstrap attempts that found phase === "complete".
99
- * Prevents the recursive dialog loop described in #1348 where
100
- * bootstrapAutoSession → showSmartEntry → checkAutoStartAfterDiscuss → startAuto
101
- * cycles indefinitely when the discuss workflow doesn't produce a milestone. */
102
- let _consecutiveCompleteBootstraps = 0;
103
- const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2;
104
95
  export async function bootstrapAutoSession(
105
96
  s: AutoSession,
106
97
  ctx: ExtensionCommandContext,
@@ -137,20 +128,7 @@ export async function bootstrapAutoSession(
137
128
  nativeInit(base, mainBranch);
138
129
  }
139
130
 
140
- // Migrate legacy in-project .gsd/ to external state directory.
141
- // Migration MUST run before ensureGitignore to avoid adding ".gsd" to
142
- // .gitignore when .gsd/ is git-tracked (data-loss bug #1364).
143
- recoverFailedMigration(base);
144
- const migration = migrateToExternalState(base);
145
- if (migration.error) {
146
- ctx.ui.notify(`External state migration warning: ${migration.error}`, "warning");
147
- }
148
- // Ensure symlink exists (handles fresh projects and post-migration)
149
- ensureGsdSymlink(base);
150
-
151
- // Ensure .gitignore has baseline patterns.
152
- // ensureGitignore checks for git-tracked .gsd/ files and skips the
153
- // ".gsd" pattern if the project intentionally tracks .gsd/ in git.
131
+ // Ensure .gitignore has baseline patterns
154
132
  const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
155
133
  const commitDocs = gitPrefs?.commit_docs;
156
134
  const manageGitignore = gitPrefs?.manage_gitignore;
@@ -308,20 +286,6 @@ export async function bootstrapAutoSession(
308
286
  if (!hasSurvivorBranch) {
309
287
  // No active work — start a new milestone via discuss flow
310
288
  if (!state.activeMilestone || state.phase === "complete") {
311
- // Guard against recursive dialog loop (#1348):
312
- // If we've entered this branch multiple times in quick succession,
313
- // the discuss workflow isn't producing a milestone. Break the cycle.
314
- _consecutiveCompleteBootstraps++;
315
- if (_consecutiveCompleteBootstraps > MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS) {
316
- _consecutiveCompleteBootstraps = 0;
317
- ctx.ui.notify(
318
- "All milestones are complete and the discussion didn't produce a new one. " +
319
- "Run /gsd to start a new milestone manually.",
320
- "warning",
321
- );
322
- return releaseLockAndReturn();
323
- }
324
-
325
289
  const { showSmartEntry } = await import("./guided-flow.js");
326
290
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
327
291
 
@@ -332,7 +296,6 @@ export async function bootstrapAutoSession(
332
296
  postState.phase !== "complete" &&
333
297
  postState.phase !== "pre-planning"
334
298
  ) {
335
- _consecutiveCompleteBootstraps = 0; // Successfully advanced past "complete"
336
299
  state = postState;
337
300
  } else if (
338
301
  postState.activeMilestone &&
@@ -389,9 +352,6 @@ export async function bootstrapAutoSession(
389
352
  return releaseLockAndReturn();
390
353
  }
391
354
 
392
- // Successfully resolved an active milestone — reset the re-entry guard
393
- _consecutiveCompleteBootstraps = 0;
394
-
395
355
  // ── Initialize session state ──
396
356
  s.active = true;
397
357
  s.stepMode = requestedStepMode;
@@ -524,7 +484,7 @@ export async function bootstrapAutoSession(
524
484
  // Secrets collection gate
525
485
  const mid = state.activeMilestone!.id;
526
486
  try {
527
- const manifestStatus = await getManifestStatus(base, mid, s.originalBasePath || base);
487
+ const manifestStatus = await getManifestStatus(base, mid);
528
488
  if (manifestStatus && manifestStatus.pending.length > 0) {
529
489
  const result = await collectSecretsFromManifest(base, mid, ctx);
530
490
  if (
@@ -127,7 +127,7 @@ import {
127
127
  formatTokenCount,
128
128
  } from "./metrics.js";
129
129
  import { join } from "node:path";
130
- import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
130
+ import { readFileSync, existsSync, mkdirSync } from "node:fs";
131
131
  import { atomicWriteSync } from "./atomic-write.js";
132
132
  import {
133
133
  autoCommitCurrentBranch,
@@ -554,13 +554,6 @@ export async function stopAuto(
554
554
  resetRoutingHistory();
555
555
  resetHookState();
556
556
  if (s.basePath) clearPersistedHookState(s.basePath);
557
-
558
- // Remove paused-session metadata if present (#1383)
559
- try {
560
- const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
561
- if (existsSync(pausedPath)) unlinkSync(pausedPath);
562
- } catch { /* non-fatal */ }
563
-
564
557
  s.active = false;
565
558
  s.paused = false;
566
559
  s.stepMode = false;
@@ -614,32 +607,8 @@ export async function pauseAuto(
614
607
 
615
608
  s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
616
609
 
617
- // Persist paused-session metadata so resume survives /exit (#1383).
618
- // The fresh-start bootstrap checks for this file and restores worktree context.
619
- try {
620
- const pausedMeta = {
621
- milestoneId: s.currentMilestoneId,
622
- worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
623
- originalBasePath: s.originalBasePath,
624
- stepMode: s.stepMode,
625
- pausedAt: new Date().toISOString(),
626
- sessionFile: s.pausedSessionFile,
627
- };
628
- const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime");
629
- mkdirSync(runtimeDir, { recursive: true });
630
- writeFileSync(
631
- join(runtimeDir, "paused-session.json"),
632
- JSON.stringify(pausedMeta, null, 2),
633
- "utf-8",
634
- );
635
- } catch {
636
- // Non-fatal — resume will still work via full bootstrap, just without worktree context
637
- }
638
-
639
- if (lockBase()) {
640
- releaseSessionLock(lockBase());
641
- clearLock(lockBase());
642
- }
610
+ if (lockBase()) clearLock(lockBase());
611
+ if (lockBase()) releaseSessionLock(lockBase());
643
612
 
644
613
  deregisterSigtermHandler();
645
614
 
@@ -823,30 +792,6 @@ export async function startAuto(
823
792
  base = escapeStaleWorktree(base);
824
793
 
825
794
  // If resuming from paused state, just re-activate and dispatch next unit.
826
- // Check persisted paused-session first (#1383) — survives /exit.
827
- if (!s.paused) {
828
- try {
829
- const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
830
- if (existsSync(pausedPath)) {
831
- const meta = JSON.parse(readFileSync(pausedPath, "utf-8"));
832
- if (meta.milestoneId) {
833
- s.currentMilestoneId = meta.milestoneId;
834
- s.originalBasePath = meta.originalBasePath || base;
835
- s.stepMode = meta.stepMode ?? requestedStepMode;
836
- s.paused = true;
837
- // Clean up the persisted file — we're consuming it
838
- try { unlinkSync(pausedPath); } catch { /* non-fatal */ }
839
- ctx.ui.notify(
840
- `Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`,
841
- "info",
842
- );
843
- }
844
- }
845
- } catch {
846
- // Malformed or missing — proceed with fresh bootstrap
847
- }
848
- }
849
-
850
795
  if (s.paused) {
851
796
  const resumeLock = acquireSessionLock(base);
852
797
  if (!resumeLock.acquired) {
@@ -1200,9 +1145,6 @@ export async function dispatchHookUnit(
1200
1145
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1201
1146
  ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
1202
1147
 
1203
- // Ensure cwd matches basePath before hook dispatch (#1389)
1204
- try { if (process.cwd() !== s.basePath) process.chdir(s.basePath); } catch {}
1205
-
1206
1148
  debugLog("dispatchHookUnit", {
1207
1149
  phase: "send-message",
1208
1150
  promptLength: hookPrompt.length,
@@ -24,7 +24,7 @@ import { projectRoot } from "./commands.js";
24
24
  import { loadPrompt } from "./prompt-loader.js";
25
25
 
26
26
  export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
27
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
27
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
28
28
  const workflow = readFileSync(workflowPath, "utf-8");
29
29
  const prompt = loadPrompt("doctor-heal", {
30
30
  doctorSummary: reportText,
@@ -187,7 +187,7 @@ export async function handleTriage(ctx: ExtensionCommandContext, pi: ExtensionAP
187
187
  roadmapContext: roadmapContext || "(no active roadmap)",
188
188
  });
189
189
 
190
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
190
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
191
191
  const workflow = readFileSync(workflowPath, "utf-8");
192
192
 
193
193
  pi.sendMessage(
@@ -180,36 +180,26 @@ function checkPortConflicts(basePath: string): EnvironmentCheckResult[] {
180
180
  const portsToCheck = new Set<number>();
181
181
  const pkgPath = join(basePath, "package.json");
182
182
 
183
- if (!existsSync(pkgPath)) {
184
- // No package.json — this isn't a Node.js project. Skip port checks
185
- // entirely to avoid false positives from system services (e.g., macOS
186
- // AirPlay Receiver on port 5000). (#1381)
187
- return [];
188
- }
189
-
190
- try {
191
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
192
- const scripts = pkg.scripts ?? {};
193
- const scriptText = Object.values(scripts).join(" ");
194
-
195
- // Look for --port NNNN, -p NNNN, PORT=NNNN, :NNNN patterns
196
- const portMatches = scriptText.matchAll(/(?:--port\s+|(?:^|[^a-z])PORT[=:]\s*|-p\s+|:)(\d{4,5})\b/gi);
197
- for (const m of portMatches) {
198
- const port = parseInt(m[1], 10);
199
- if (port >= 1024 && port <= 65535) portsToCheck.add(port);
183
+ if (existsSync(pkgPath)) {
184
+ try {
185
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
186
+ const scripts = pkg.scripts ?? {};
187
+ const scriptText = Object.values(scripts).join(" ");
188
+
189
+ // Look for --port NNNN, -p NNNN, PORT=NNNN, :NNNN patterns
190
+ const portMatches = scriptText.matchAll(/(?:--port\s+|(?:^|[^a-z])PORT[=:]\s*|-p\s+|:)(\d{4,5})\b/gi);
191
+ for (const m of portMatches) {
192
+ const port = parseInt(m[1], 10);
193
+ if (port >= 1024 && port <= 65535) portsToCheck.add(port);
194
+ }
195
+ } catch {
196
+ // parse failed — use defaults
200
197
  }
201
- } catch {
202
- // parse failed — skip port checks rather than using defaults
203
- return [];
204
198
  }
205
199
 
206
- // If no ports found in scripts, check common defaults.
207
- // Filter out port 5000 on macOS — AirPlay Receiver uses it by default (#1381).
200
+ // If no ports found in scripts, check common defaults
208
201
  if (portsToCheck.size === 0) {
209
- for (const p of DEFAULT_DEV_PORTS) {
210
- if (p === 5000 && process.platform === "darwin") continue;
211
- portsToCheck.add(p);
212
- }
202
+ for (const p of DEFAULT_DEV_PORTS) portsToCheck.add(p);
213
203
  }
214
204
 
215
205
  for (const port of portsToCheck) {
@@ -805,7 +805,7 @@ export async function inlinePriorMilestoneSummary(mid: string, base: string): Pr
805
805
  * file not on disk) - callers can distinguish "no manifest" from "empty manifest".
806
806
  */
807
807
  export async function getManifestStatus(
808
- base: string, milestoneId: string, projectRoot?: string,
808
+ base: string, milestoneId: string,
809
809
  ): Promise<ManifestStatus | null> {
810
810
  const resolvedPath = resolveMilestoneFile(base, milestoneId, 'SECRETS');
811
811
  if (!resolvedPath) return null;
@@ -815,18 +815,9 @@ export async function getManifestStatus(
815
815
 
816
816
  const manifest = parseSecretsManifest(content);
817
817
  const keys = manifest.entries.map(e => e.key);
818
-
819
- // Check both the base path .env AND the project root .env (#1387).
820
- // In worktree mode, base is the worktree path which may not have .env.
821
- // The project root's .env is where the user actually defined their keys.
822
818
  const existingKeys = await checkExistingEnvKeys(keys, resolve(base, '.env'));
823
819
  const existingSet = new Set(existingKeys);
824
820
 
825
- if (projectRoot && projectRoot !== base) {
826
- const rootKeys = await checkExistingEnvKeys(keys, resolve(projectRoot, '.env'));
827
- for (const k of rootKeys) existingSet.add(k);
828
- }
829
-
830
821
  const result: ManifestStatus = {
831
822
  pending: [],
832
823
  collected: [],
@@ -7,8 +7,8 @@
7
7
  */
8
8
 
9
9
  import { join } from "node:path";
10
- import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
11
- import { nativeRmCached, nativeLsFiles } from "./native-git-bridge.js";
10
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { nativeRmCached } from "./native-git-bridge.js";
12
12
  import { gsdRoot } from "./paths.js";
13
13
 
14
14
  /**
@@ -79,47 +79,12 @@ const BASELINE_PATTERNS = [
79
79
  ];
80
80
 
81
81
  /**
82
- * Check whether `.gsd/` contains files tracked by git.
83
- * If so, the project intentionally keeps `.gsd/` in version control
84
- * and we must NOT add `.gsd` to `.gitignore` or attempt migration.
85
- *
86
- * Returns true if git tracks at least one file under `.gsd/`.
87
- * Returns false (safe to ignore) if:
88
- * - Not a git repo
89
- * - `.gsd/` is a symlink (external state, should be ignored)
90
- * - `.gsd/` doesn't exist
91
- * - No tracked files found under `.gsd/`
92
- */
93
- export function hasGitTrackedGsdFiles(basePath: string): boolean {
94
- const localGsd = join(basePath, ".gsd");
95
-
96
- // If .gsd doesn't exist or is already a symlink, no tracked files concern
97
- if (!existsSync(localGsd)) return false;
98
- try {
99
- if (lstatSync(localGsd).isSymbolicLink()) return false;
100
- } catch {
101
- return false;
102
- }
103
-
104
- // Check if git tracks any files under .gsd/
105
- try {
106
- const tracked = nativeLsFiles(basePath, ".gsd");
107
- return tracked.length > 0;
108
- } catch {
109
- // Not a git repo or git not available — safe to proceed
110
- return false;
111
- }
112
- }
113
-
114
- /**
115
- * Ensure basePath/.gitignore contains baseline ignore patterns.
116
- * Creates the file if missing; appends missing patterns.
82
+ * Ensure basePath/.gitignore contains a blanket `.gsd/` ignore.
83
+ * Creates the file if missing; appends `.gsd/` if not present.
117
84
  * Returns true if the file was created or modified, false if already complete.
118
85
  *
119
- * **Safety check:** If `.gsd/` contains git-tracked files (i.e., the project
120
- * intentionally keeps `.gsd/` in version control), the `.gsd` ignore pattern
121
- * is excluded to prevent data loss. Only the `.gsd` pattern is affected —
122
- * all other baseline patterns are still applied normally.
86
+ * `.gsd/` state is managed externally (symlinked to `~/.gsd/projects/<hash>/`),
87
+ * so the entire directory is always gitignored.
123
88
  */
124
89
  export function ensureGitignore(
125
90
  basePath: string,
@@ -143,15 +108,8 @@ export function ensureGitignore(
143
108
  .filter((l) => l && !l.startsWith("#")),
144
109
  );
145
110
 
146
- // Determine which patterns to apply. If .gsd/ has tracked files,
147
- // exclude the ".gsd" pattern to prevent deleting tracked state.
148
- const gsdIsTracked = hasGitTrackedGsdFiles(basePath);
149
- const patternsToApply = gsdIsTracked
150
- ? BASELINE_PATTERNS.filter((p) => p !== ".gsd")
151
- : BASELINE_PATTERNS;
152
-
153
111
  // Find patterns not yet present
154
- const missing = patternsToApply.filter((p) => !existingLines.has(p));
112
+ const missing = BASELINE_PATTERNS.filter((p) => !existingLines.has(p));
155
113
 
156
114
  if (missing.length === 0) return false;
157
115
 
@@ -177,11 +135,6 @@ export function ensureGitignore(
177
135
  * already in the index even after .gitignore is updated.
178
136
  *
179
137
  * Only removes from the index (`--cached`), never from disk. Idempotent.
180
- *
181
- * Note: These are strictly runtime/ephemeral paths (activity logs, lock files,
182
- * metrics, STATE.md). They are always safe to untrack, even when the project
183
- * intentionally keeps other `.gsd/` files (like PROJECT.md, milestones/) in
184
- * version control.
185
138
  */
186
139
  export function untrackRuntimeFiles(basePath: string): void {
187
140
  const runtimePaths = GSD_RUNTIME_PATTERNS;
@@ -191,7 +191,7 @@ type UIContext = ExtensionContext;
191
191
  * This is the only way the wizard triggers work — everything else is the LLM's job.
192
192
  */
193
193
  function dispatchWorkflow(pi: ExtensionAPI, note: string, customType = "gsd-run"): void {
194
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
194
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
195
195
  const workflow = readFileSync(workflowPath, "utf-8");
196
196
 
197
197
  pi.sendMessage(