gsd-pi 2.29.0-dev.2ccf3fb → 2.29.0-dev.4c155ee

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 (116) hide show
  1. package/dist/headless.js +4 -0
  2. package/dist/resources/extensions/gsd/auto-dashboard.ts +31 -0
  3. package/dist/resources/extensions/gsd/auto-dispatch.ts +32 -3
  4. package/dist/resources/extensions/gsd/auto-post-unit.ts +39 -10
  5. package/dist/resources/extensions/gsd/auto-prompts.ts +40 -17
  6. package/dist/resources/extensions/gsd/auto-recovery.ts +2 -1
  7. package/dist/resources/extensions/gsd/auto-start.ts +18 -32
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +21 -182
  9. package/dist/resources/extensions/gsd/auto.ts +2 -9
  10. package/dist/resources/extensions/gsd/captures.ts +4 -10
  11. package/dist/resources/extensions/gsd/commands-handlers.ts +2 -1
  12. package/dist/resources/extensions/gsd/commands.ts +2 -1
  13. package/dist/resources/extensions/gsd/detection.ts +2 -1
  14. package/dist/resources/extensions/gsd/doctor-checks.ts +49 -1
  15. package/dist/resources/extensions/gsd/doctor-types.ts +3 -1
  16. package/dist/resources/extensions/gsd/forensics.ts +2 -2
  17. package/dist/resources/extensions/gsd/git-service.ts +3 -2
  18. package/dist/resources/extensions/gsd/gitignore.ts +9 -63
  19. package/dist/resources/extensions/gsd/gsd-db.ts +1 -165
  20. package/dist/resources/extensions/gsd/guided-flow.ts +8 -5
  21. package/dist/resources/extensions/gsd/index.ts +3 -3
  22. package/dist/resources/extensions/gsd/md-importer.ts +3 -2
  23. package/dist/resources/extensions/gsd/mechanical-completion.ts +430 -0
  24. package/dist/resources/extensions/gsd/migrate/command.ts +3 -2
  25. package/dist/resources/extensions/gsd/migrate/writer.ts +2 -1
  26. package/dist/resources/extensions/gsd/migrate-external.ts +123 -0
  27. package/dist/resources/extensions/gsd/paths.ts +24 -2
  28. package/dist/resources/extensions/gsd/post-unit-hooks.ts +6 -5
  29. package/dist/resources/extensions/gsd/preferences-models.ts +7 -1
  30. package/dist/resources/extensions/gsd/preferences-validation.ts +2 -1
  31. package/dist/resources/extensions/gsd/preferences.ts +10 -5
  32. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +4 -2
  33. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +26 -2
  34. package/dist/resources/extensions/gsd/prompts/plan-slice.md +15 -1
  35. package/dist/resources/extensions/gsd/repo-identity.ts +148 -0
  36. package/dist/resources/extensions/gsd/resource-version.ts +99 -0
  37. package/dist/resources/extensions/gsd/session-forensics.ts +4 -3
  38. package/dist/resources/extensions/gsd/tests/activity-log.test.ts +2 -2
  39. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +3 -3
  40. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +0 -58
  41. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +3 -4
  42. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +5 -18
  43. package/dist/resources/extensions/gsd/tests/git-service.test.ts +10 -37
  44. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +4 -4
  45. package/dist/resources/extensions/gsd/tests/mechanical-completion.test.ts +356 -0
  46. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +1 -0
  47. package/dist/resources/extensions/gsd/tests/token-profile.test.ts +14 -16
  48. package/dist/resources/extensions/gsd/triage-resolution.ts +2 -1
  49. package/dist/resources/extensions/gsd/types.ts +2 -0
  50. package/dist/resources/extensions/gsd/worktree-command.ts +1 -11
  51. package/dist/resources/extensions/gsd/worktree-manager.ts +3 -2
  52. package/dist/resources/extensions/gsd/worktree.ts +42 -5
  53. package/dist/resources/skills/react-best-practices/SKILL.md +1 -1
  54. package/package.json +1 -1
  55. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  56. package/packages/pi-coding-agent/dist/core/lsp/client.js +3 -0
  57. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  58. package/packages/pi-coding-agent/src/core/lsp/client.ts +3 -0
  59. package/src/resources/extensions/gsd/auto-dashboard.ts +31 -0
  60. package/src/resources/extensions/gsd/auto-dispatch.ts +32 -3
  61. package/src/resources/extensions/gsd/auto-post-unit.ts +39 -10
  62. package/src/resources/extensions/gsd/auto-prompts.ts +40 -17
  63. package/src/resources/extensions/gsd/auto-recovery.ts +2 -1
  64. package/src/resources/extensions/gsd/auto-start.ts +18 -32
  65. package/src/resources/extensions/gsd/auto-worktree.ts +21 -182
  66. package/src/resources/extensions/gsd/auto.ts +2 -9
  67. package/src/resources/extensions/gsd/captures.ts +4 -10
  68. package/src/resources/extensions/gsd/commands-handlers.ts +2 -1
  69. package/src/resources/extensions/gsd/commands.ts +2 -1
  70. package/src/resources/extensions/gsd/detection.ts +2 -1
  71. package/src/resources/extensions/gsd/doctor-checks.ts +49 -1
  72. package/src/resources/extensions/gsd/doctor-types.ts +3 -1
  73. package/src/resources/extensions/gsd/forensics.ts +2 -2
  74. package/src/resources/extensions/gsd/git-service.ts +3 -2
  75. package/src/resources/extensions/gsd/gitignore.ts +9 -63
  76. package/src/resources/extensions/gsd/gsd-db.ts +1 -165
  77. package/src/resources/extensions/gsd/guided-flow.ts +8 -5
  78. package/src/resources/extensions/gsd/index.ts +3 -3
  79. package/src/resources/extensions/gsd/md-importer.ts +3 -2
  80. package/src/resources/extensions/gsd/mechanical-completion.ts +430 -0
  81. package/src/resources/extensions/gsd/migrate/command.ts +3 -2
  82. package/src/resources/extensions/gsd/migrate/writer.ts +2 -1
  83. package/src/resources/extensions/gsd/migrate-external.ts +123 -0
  84. package/src/resources/extensions/gsd/paths.ts +24 -2
  85. package/src/resources/extensions/gsd/post-unit-hooks.ts +6 -5
  86. package/src/resources/extensions/gsd/preferences-models.ts +7 -1
  87. package/src/resources/extensions/gsd/preferences-validation.ts +2 -1
  88. package/src/resources/extensions/gsd/preferences.ts +10 -5
  89. package/src/resources/extensions/gsd/prompts/discuss-headless.md +4 -2
  90. package/src/resources/extensions/gsd/prompts/plan-milestone.md +26 -2
  91. package/src/resources/extensions/gsd/prompts/plan-slice.md +15 -1
  92. package/src/resources/extensions/gsd/repo-identity.ts +148 -0
  93. package/src/resources/extensions/gsd/resource-version.ts +99 -0
  94. package/src/resources/extensions/gsd/session-forensics.ts +4 -3
  95. package/src/resources/extensions/gsd/tests/activity-log.test.ts +2 -2
  96. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +3 -3
  97. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +0 -58
  98. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +3 -4
  99. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +5 -18
  100. package/src/resources/extensions/gsd/tests/git-service.test.ts +10 -37
  101. package/src/resources/extensions/gsd/tests/knowledge.test.ts +4 -4
  102. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +356 -0
  103. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +1 -0
  104. package/src/resources/extensions/gsd/tests/token-profile.test.ts +14 -16
  105. package/src/resources/extensions/gsd/triage-resolution.ts +2 -1
  106. package/src/resources/extensions/gsd/types.ts +2 -0
  107. package/src/resources/extensions/gsd/worktree-command.ts +1 -11
  108. package/src/resources/extensions/gsd/worktree-manager.ts +3 -2
  109. package/src/resources/extensions/gsd/worktree.ts +42 -5
  110. package/src/resources/skills/react-best-practices/SKILL.md +1 -1
  111. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +0 -199
  112. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +0 -205
  113. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +0 -442
  114. package/src/resources/extensions/gsd/auto-worktree-sync.ts +0 -199
  115. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +0 -205
  116. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +0 -442
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import { join, sep } from "node:path";
3
3
 
4
4
  import type { DoctorIssue, DoctorIssueCode } from "./doctor-types.js";
@@ -13,6 +13,7 @@ import { nativeIsRepo, nativeWorktreeRemove, nativeBranchList, nativeBranchDelet
13
13
  import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.js";
14
14
  import { ensureGitignore } from "./gitignore.js";
15
15
  import { readAllSessionStatuses, isSessionStale, removeSessionStatus } from "./session-status-io.js";
16
+ import { recoverFailedMigration } from "./migrate-external.js";
16
17
 
17
18
  export async function checkGitHealth(
18
19
  basePath: string,
@@ -508,6 +509,53 @@ export async function checkRuntimeHealth(
508
509
  } catch {
509
510
  // Non-fatal — gitignore check failed
510
511
  }
512
+
513
+ // ── External state symlink health ──────────────────────────────────────
514
+ try {
515
+ const localGsd = join(basePath, ".gsd");
516
+ if (existsSync(localGsd)) {
517
+ const stat = lstatSync(localGsd);
518
+
519
+ // Check for .gsd.migrating (failed migration)
520
+ const migratingPath = join(basePath, ".gsd.migrating");
521
+ if (existsSync(migratingPath)) {
522
+ issues.push({
523
+ severity: "error",
524
+ code: "failed_migration",
525
+ scope: "project",
526
+ unitId: "project",
527
+ message: "Found .gsd.migrating — a previous external state migration failed. State may be incomplete.",
528
+ file: ".gsd.migrating",
529
+ fixable: true,
530
+ });
531
+
532
+ if (shouldFix("failed_migration")) {
533
+ if (recoverFailedMigration(basePath)) {
534
+ fixesApplied.push("recovered failed migration (.gsd.migrating → .gsd)");
535
+ }
536
+ }
537
+ }
538
+
539
+ // Check symlink target exists
540
+ if (stat.isSymbolicLink()) {
541
+ try {
542
+ realpathSync(localGsd);
543
+ } catch {
544
+ issues.push({
545
+ severity: "error",
546
+ code: "broken_symlink",
547
+ scope: "project",
548
+ unitId: "project",
549
+ message: ".gsd symlink target does not exist. External state directory may have been deleted.",
550
+ file: ".gsd",
551
+ fixable: false,
552
+ });
553
+ }
554
+ }
555
+ }
556
+ } catch {
557
+ // Non-fatal — external state check failed
558
+ }
511
559
  }
512
560
 
513
561
  /**
@@ -30,7 +30,9 @@ export type DoctorIssueCode =
30
30
  | "state_file_stale"
31
31
  | "state_file_missing"
32
32
  | "gitignore_missing_patterns"
33
- | "unresolvable_dependency";
33
+ | "unresolvable_dependency"
34
+ | "failed_migration"
35
+ | "broken_symlink";
34
36
 
35
37
  /**
36
38
  * Issue codes that represent expected completion-transition states.
@@ -268,7 +268,7 @@ function resolveActivityDirs(basePath: string, activeMilestone?: string | null):
268
268
  if (activeMilestone) {
269
269
  const wtPath = getAutoWorktreePath(basePath, activeMilestone);
270
270
  if (wtPath) {
271
- const wtActivityDir = join(wtPath, ".gsd", "activity");
271
+ const wtActivityDir = join(gsdRoot(wtPath), "activity");
272
272
  if (existsSync(wtActivityDir)) {
273
273
  dirs.push(wtActivityDir);
274
274
  }
@@ -285,7 +285,7 @@ function resolveActivityDirs(basePath: string, activeMilestone?: string | null):
285
285
  // ─── Completed Keys Loader ────────────────────────────────────────────────────
286
286
 
287
287
  function loadCompletedKeys(basePath: string): string[] {
288
- const file = join(basePath, ".gsd", "completed-units.json");
288
+ const file = join(gsdRoot(basePath), "completed-units.json");
289
289
  try {
290
290
  if (existsSync(file)) {
291
291
  return JSON.parse(readFileSync(file, "utf-8"));
@@ -11,6 +11,7 @@
11
11
  import { execFileSync, execSync } from "node:child_process";
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
13
  import { join } from "node:path";
14
+ import { gsdRoot } from "./paths.js";
14
15
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
15
16
 
16
17
  import {
@@ -193,7 +194,7 @@ export const RUNTIME_EXCLUSION_PATHS: readonly string[] = [
193
194
  * Format: .gsd/milestones/<MID>/<MID>-META.json
194
195
  */
195
196
  function milestoneMetaPath(basePath: string, milestoneId: string): string {
196
- return join(basePath, ".gsd", "milestones", milestoneId, `${milestoneId}-META.json`);
197
+ return join(gsdRoot(basePath), "milestones", milestoneId, `${milestoneId}-META.json`);
197
198
  }
198
199
 
199
200
  /**
@@ -237,7 +238,7 @@ export function writeIntegrationBranch(basePath: string, milestoneId: string, br
237
238
  if (existingBranch === branch) return;
238
239
 
239
240
  const metaFile = milestoneMetaPath(basePath, milestoneId);
240
- mkdirSync(join(basePath, ".gsd", "milestones", milestoneId), { recursive: true });
241
+ mkdirSync(join(gsdRoot(basePath), "milestones", milestoneId), { recursive: true });
241
242
 
242
243
  // Merge with existing metadata if present
243
244
  let existing: Record<string, unknown> = {};
@@ -9,10 +9,12 @@
9
9
  import { join } from "node:path";
10
10
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import { nativeRmCached } from "./native-git-bridge.js";
12
+ import { gsdRoot } from "./paths.js";
12
13
 
13
14
  /**
14
- * Patterns that are always correct regardless of project type.
15
- * No one ever wants these tracked.
15
+ * GSD runtime patterns for git index cleanup.
16
+ * With external state (symlink), these are a no-op in most cases,
17
+ * but retained for backwards compatibility during migration.
16
18
  */
17
19
  const GSD_RUNTIME_PATTERNS = [
18
20
  ".gsd/activity/",
@@ -31,8 +33,8 @@ const GSD_RUNTIME_PATTERNS = [
31
33
  ] as const;
32
34
 
33
35
  const BASELINE_PATTERNS = [
34
- // ── GSD runtime (not source artifacts — planning files are tracked) ──
35
- ...GSD_RUNTIME_PATTERNS,
36
+ // ── GSD state directory (symlink to external storage) ──
37
+ ".gsd",
36
38
 
37
39
  // ── OS junk ──
38
40
  ".DS_Store",
@@ -90,41 +92,12 @@ export function ensureGitignore(basePath: string, options?: { commitDocs?: boole
90
92
  if (options?.manageGitignore === false) return false;
91
93
 
92
94
  const gitignorePath = join(basePath, ".gitignore");
93
- const commitDocs = options?.commitDocs !== false; // default true
94
95
 
95
96
  let existing = "";
96
97
  if (existsSync(gitignorePath)) {
97
98
  existing = readFileSync(gitignorePath, "utf-8");
98
99
  }
99
100
 
100
- // When commit_docs is false, ensure blanket ".gsd/" is in .gitignore
101
- // and skip the self-heal that would remove it.
102
- if (!commitDocs) {
103
- return ensureBlanketGsdIgnore(gitignorePath, existing);
104
- }
105
-
106
- // Self-heal: remove blanket ".gsd/" lines from pre-v2.14.0 projects.
107
- // The blanket ignore prevented planning artifacts (.gsd/milestones/) from
108
- // being tracked in git, causing artifacts to vanish in worktrees and
109
- // triggering loop detection failures. Replace with explicit runtime-only
110
- // ignores so planning files are tracked naturally.
111
- let modified = false;
112
- const lines = existing.split("\n");
113
- const filteredLines = lines.filter(line => {
114
- const trimmed = line.trim();
115
- // Remove standalone ".gsd/" lines (blanket ignore) but keep specific
116
- // .gsd/ subpath patterns like ".gsd/activity/" or ".gsd/auto.lock"
117
- if (trimmed === ".gsd/" || trimmed === ".gsd") {
118
- modified = true;
119
- return false;
120
- }
121
- return true;
122
- });
123
- if (modified) {
124
- existing = filteredLines.join("\n");
125
- writeFileSync(gitignorePath, existing, "utf-8");
126
- }
127
-
128
101
  // Parse existing lines (trimmed, ignoring comments and blanks)
129
102
  const existingLines = new Set(
130
103
  existing
@@ -136,7 +109,7 @@ export function ensureGitignore(basePath: string, options?: { commitDocs?: boole
136
109
  // Find patterns not yet present
137
110
  const missing = BASELINE_PATTERNS.filter((p) => !existingLines.has(p));
138
111
 
139
- if (missing.length === 0) return modified;
112
+ if (missing.length === 0) return false;
140
113
 
141
114
  // Build the block to append
142
115
  const block = [
@@ -184,8 +157,8 @@ export function untrackRuntimeFiles(basePath: string): void {
184
157
  * creating a duplicate when an uppercase file already exists.
185
158
  */
186
159
  export function ensurePreferences(basePath: string): boolean {
187
- const preferencesPath = join(basePath, ".gsd", "preferences.md");
188
- const legacyPath = join(basePath, ".gsd", "PREFERENCES.md");
160
+ const preferencesPath = join(gsdRoot(basePath), "preferences.md");
161
+ const legacyPath = join(gsdRoot(basePath), "PREFERENCES.md");
189
162
 
190
163
  if (existsSync(preferencesPath) || existsSync(legacyPath)) {
191
164
  return false;
@@ -240,31 +213,4 @@ custom_instructions:
240
213
  return true;
241
214
  }
242
215
 
243
- /**
244
- * When commit_docs is false, ensure `.gsd/` is in .gitignore as a blanket
245
- * pattern. This keeps all GSD artifacts local-only.
246
- * Returns true if the file was modified, false if already complete.
247
- */
248
- function ensureBlanketGsdIgnore(gitignorePath: string, existing: string): boolean {
249
- const existingLines = new Set(
250
- existing
251
- .split("\n")
252
- .map((l) => l.trim())
253
- .filter((l) => l && !l.startsWith("#")),
254
- );
255
-
256
- // Already has blanket .gsd/ ignore
257
- if (existingLines.has(".gsd/") || existingLines.has(".gsd")) return false;
258
-
259
- const block = [
260
- "",
261
- "# ── GSD (local-only, commit_docs: false) ──",
262
- ".gsd/",
263
- "",
264
- ].join("\n");
265
-
266
- const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
267
- writeFileSync(gitignorePath, existing + prefix + block, "utf-8");
268
- return true;
269
- }
270
216
 
@@ -6,8 +6,7 @@
6
6
  // Schema is initialized on first open with WAL mode for file-backed DBs.
7
7
 
8
8
  import { createRequire } from 'node:module';
9
- import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
10
- import { dirname } from 'node:path';
9
+ import { existsSync } from 'node:fs';
11
10
  import type { Decision, Requirement } from './types.js';
12
11
  import { GSDError, GSD_STALE_STATE } from './errors.js';
13
12
 
@@ -565,169 +564,6 @@ export function getActiveRequirements(): Requirement[] {
565
564
  }));
566
565
  }
567
566
 
568
- // ─── Worktree DB Operations ────────────────────────────────────────────────
569
-
570
- /**
571
- * Copy a gsd.db file to a new worktree location.
572
- * Copies only the .db file — skips -wal and -shm files so the copy starts clean.
573
- * Returns true on success, false on failure (never throws).
574
- */
575
- export function copyWorktreeDb(srcDbPath: string, destDbPath: string): boolean {
576
- try {
577
- if (!existsSync(srcDbPath)) {
578
- return false; // source doesn't exist — expected when no DB yet
579
- }
580
- const destDir = dirname(destDbPath);
581
- mkdirSync(destDir, { recursive: true });
582
- copyFileSync(srcDbPath, destDbPath);
583
- return true;
584
- } catch (err) {
585
- process.stderr.write(`gsd-db: failed to copy DB to worktree: ${(err as Error).message}\n`);
586
- return false;
587
- }
588
- }
589
-
590
- /**
591
- * Reconcile rows from a worktree DB back into the main DB using ATTACH DATABASE.
592
- * Merges all three tables (decisions, requirements, artifacts) via INSERT OR REPLACE.
593
- * Detects conflicts where both DBs modified the same row.
594
- *
595
- * ATTACH must happen outside any transaction. INSERT OR REPLACE runs inside a transaction.
596
- * DETACH happens after commit (or rollback on error).
597
- */
598
- export function reconcileWorktreeDb(
599
- mainDbPath: string,
600
- worktreeDbPath: string,
601
- ): { decisions: number; requirements: number; artifacts: number; conflicts: string[] } {
602
- const zero = { decisions: 0, requirements: 0, artifacts: 0, conflicts: [] as string[] };
603
-
604
- // Validate worktree DB exists
605
- if (!existsSync(worktreeDbPath)) {
606
- return zero;
607
- }
608
-
609
- // Safety: reject single quotes which could break the ATTACH DATABASE '...' SQL literal.
610
- // SQLite ATTACH doesn't support parameterized binding. We block the one dangerous char
611
- // rather than allowlisting, since OS temp paths vary widely (tildes, parens, unicode).
612
- if (worktreeDbPath.includes("'")) {
613
- process.stderr.write(`gsd-db: worktree DB reconciliation failed: path contains unsafe characters\n`);
614
- return zero;
615
- }
616
-
617
- // Ensure main DB is open
618
- if (!currentDb) {
619
- const opened = openDatabase(mainDbPath);
620
- if (!opened) {
621
- process.stderr.write(`gsd-db: worktree DB reconciliation failed: cannot open main DB\n`);
622
- return zero;
623
- }
624
- }
625
-
626
- const adapter = currentDb!;
627
- const conflicts: string[] = [];
628
-
629
- try {
630
- // ATTACH must be outside transaction
631
- adapter.exec(`ATTACH DATABASE '${worktreeDbPath}' AS wt`);
632
-
633
- try {
634
- // ── Conflict detection phase ──
635
- // Decisions: same id, different content
636
- const decisionConflicts = adapter.prepare(
637
- `SELECT m.id FROM decisions m
638
- INNER JOIN wt.decisions w ON m.id = w.id
639
- WHERE m.decision != w.decision
640
- OR m.choice != w.choice
641
- OR m.rationale != w.rationale
642
- OR m.superseded_by IS NOT w.superseded_by`,
643
- ).all();
644
- for (const row of decisionConflicts) {
645
- conflicts.push(`decision ${row['id']}: modified in both main and worktree`);
646
- }
647
-
648
- // Requirements: same id, different content
649
- const reqConflicts = adapter.prepare(
650
- `SELECT m.id FROM requirements m
651
- INNER JOIN wt.requirements w ON m.id = w.id
652
- WHERE m.description != w.description
653
- OR m.status != w.status
654
- OR m.notes != w.notes
655
- OR m.superseded_by IS NOT w.superseded_by`,
656
- ).all();
657
- for (const row of reqConflicts) {
658
- conflicts.push(`requirement ${row['id']}: modified in both main and worktree`);
659
- }
660
-
661
- // Artifacts: same path, different content
662
- const artifactConflicts = adapter.prepare(
663
- `SELECT m.path FROM artifacts m
664
- INNER JOIN wt.artifacts w ON m.path = w.path
665
- WHERE m.full_content != w.full_content
666
- OR m.artifact_type != w.artifact_type`,
667
- ).all();
668
- for (const row of artifactConflicts) {
669
- conflicts.push(`artifact ${row['path']}: modified in both main and worktree`);
670
- }
671
-
672
- // ── Merge phase (inside manual transaction) ──
673
- adapter.exec('BEGIN');
674
- try {
675
- // Decisions: exclude seq to let main auto-assign
676
- adapter.exec(
677
- `INSERT OR REPLACE INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
678
- SELECT id, when_context, scope, decision, choice, rationale, revisable, superseded_by FROM wt.decisions`,
679
- );
680
- const dCount = adapter.prepare('SELECT changes() as cnt').get();
681
-
682
- // Requirements: full row copy
683
- adapter.exec(
684
- `INSERT OR REPLACE INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
685
- SELECT id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by FROM wt.requirements`,
686
- );
687
- const rCount = adapter.prepare('SELECT changes() as cnt').get();
688
-
689
- // Artifacts: copy with fresh imported_at timestamp
690
- adapter.exec(
691
- `INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
692
- SELECT path, artifact_type, milestone_id, slice_id, task_id, full_content, datetime('now') FROM wt.artifacts`,
693
- );
694
- const aCount = adapter.prepare('SELECT changes() as cnt').get();
695
-
696
- adapter.exec('COMMIT');
697
-
698
- const result = {
699
- decisions: (dCount?.['cnt'] as number) || 0,
700
- requirements: (rCount?.['cnt'] as number) || 0,
701
- artifacts: (aCount?.['cnt'] as number) || 0,
702
- conflicts,
703
- };
704
-
705
- if (conflicts.length > 0) {
706
- process.stderr.write(`gsd-db: reconciliation conflicts:\n${conflicts.map(c => ` - ${c}`).join('\n')}\n`);
707
- }
708
- process.stderr.write(
709
- `gsd-db: reconciled ${result.decisions} decisions, ${result.requirements} requirements, ${result.artifacts} artifacts (${conflicts.length} conflicts)\n`,
710
- );
711
-
712
- return result;
713
- } catch (err) {
714
- adapter.exec('ROLLBACK');
715
- throw err;
716
- }
717
- } finally {
718
- // DETACH always, even on error
719
- try {
720
- adapter.exec('DETACH DATABASE wt');
721
- } catch {
722
- // swallow — may already be detached
723
- }
724
- }
725
- } catch (err) {
726
- process.stderr.write(`gsd-db: worktree DB reconciliation failed: ${(err as Error).message}\n`);
727
- return zero;
728
- }
729
- }
730
-
731
567
  /**
732
568
  * Returns the PID of the process that opened the current DB connection.
733
569
  * Returns 0 if no connection is open.
@@ -104,7 +104,7 @@ export function checkAutoStartAfterDiscuss(): boolean {
104
104
  const missing = milestoneIds.filter(id => {
105
105
  const hasContext = !!resolveMilestoneFile(basePath, id, "CONTEXT");
106
106
  const hasDraft = !!resolveMilestoneFile(basePath, id, "CONTEXT-DRAFT");
107
- const hasDir = existsSync(join(basePath, ".gsd", "milestones", id));
107
+ const hasDir = existsSync(join(gsdRoot(basePath), "milestones", id));
108
108
  return !hasContext && !hasDraft && !hasDir;
109
109
  });
110
110
  if (missing.length > 0) {
@@ -122,7 +122,7 @@ export function checkAutoStartAfterDiscuss(): boolean {
122
122
  // The LLM writes DISCUSSION-MANIFEST.json after each Phase 3 gate decision.
123
123
  // If the manifest exists but gates_completed < total, the LLM hasn't finished
124
124
  // presenting all readiness gates to the user — block auto-start.
125
- const manifestPath = join(basePath, ".gsd", "DISCUSSION-MANIFEST.json");
125
+ const manifestPath = join(gsdRoot(basePath), "DISCUSSION-MANIFEST.json");
126
126
  if (existsSync(manifestPath)) {
127
127
  try {
128
128
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
@@ -295,7 +295,7 @@ export async function showHeadlessMilestoneCreation(
295
295
  const nextId = nextMilestoneId(existingIds, prefs?.preferences?.unique_milestone_ids ?? false);
296
296
 
297
297
  // Create milestone directory
298
- const milestoneDir = join(basePath, ".gsd", "milestones", nextId, "slices");
298
+ const milestoneDir = join(gsdRoot(basePath), "milestones", nextId, "slices");
299
299
  mkdirSync(milestoneDir, { recursive: true });
300
300
 
301
301
  // Build and dispatch the headless discuss prompt
@@ -410,11 +410,14 @@ export async function showDiscuss(
410
410
  basePath: string,
411
411
  ): Promise<void> {
412
412
  // Guard: no .gsd/ project
413
- if (!existsSync(join(basePath, ".gsd"))) {
413
+ if (!existsSync(gsdRoot(basePath))) {
414
414
  ctx.ui.notify("No GSD project found. Run /gsd to start one first.", "warning");
415
415
  return;
416
416
  }
417
417
 
418
+ // Invalidate caches to pick up artifacts written by a just-completed discuss/plan
419
+ invalidateAllCaches();
420
+
418
421
  const state = await deriveState(basePath);
419
422
 
420
423
  // Guard: no active milestone
@@ -750,7 +753,7 @@ export async function showSmartEntry(
750
753
  }
751
754
 
752
755
  // ── Detection preamble — run before any bootstrap ────────────────────
753
- if (!existsSync(join(basePath, ".gsd"))) {
756
+ if (!existsSync(gsdRoot(basePath))) {
754
757
  const detection = detectProjectState(basePath);
755
758
 
756
759
  // v1 .planning/ detected — offer migration before anything else
@@ -75,7 +75,7 @@ async function ensureDbAvailable(): Promise<boolean> {
75
75
  if (db.isDbAvailable()) return true;
76
76
 
77
77
  // Auto-initialize: open (and create if needed) the DB at the standard path
78
- const gsdDir = join(process.cwd(), ".gsd");
78
+ const gsdDir = gsdRoot(process.cwd());
79
79
  if (!existsSync(gsdDir)) return false; // No GSD project — can't create DB
80
80
  const dbPath = join(gsdDir, "gsd.db");
81
81
  return db.openDatabase(dbPath);
@@ -629,7 +629,7 @@ export default function (pi: ExtensionAPI) {
629
629
  description: shortcutDesc("Open GSD dashboard", "/gsd status"),
630
630
  handler: async (ctx) => {
631
631
  // Only show if .gsd/ exists
632
- if (!existsSync(join(process.cwd(), ".gsd"))) {
632
+ if (!existsSync(gsdRoot(process.cwd()))) {
633
633
  ctx.ui.notify("No .gsd/ directory found. Run /gsd to start.", "info");
634
634
  return;
635
635
  }
@@ -659,7 +659,7 @@ export default function (pi: ExtensionAPI) {
659
659
 
660
660
  // ── before_agent_start: inject GSD contract into true system prompt ─────
661
661
  pi.on("before_agent_start", async (event, ctx: ExtensionContext) => {
662
- if (!existsSync(join(process.cwd(), ".gsd"))) return;
662
+ if (!existsSync(gsdRoot(process.cwd()))) return;
663
663
 
664
664
  const stopContextTimer = debugTime("context-inject");
665
665
  const systemContent = loadPrompt("system");
@@ -18,6 +18,7 @@ import {
18
18
  import {
19
19
  resolveGsdRootFile,
20
20
  milestonesDir,
21
+ gsdRoot,
21
22
  resolveTaskFiles,
22
23
  } from './paths.js';
23
24
  import { findMilestoneIds } from './guided-flow.js';
@@ -298,7 +299,7 @@ const TASK_SUFFIXES = ['PLAN', 'SUMMARY', 'CONTINUE', 'CONTEXT', 'RESEARCH'];
298
299
  */
299
300
  function importHierarchyArtifacts(gsdDir: string): number {
300
301
  let count = 0;
301
- const gsdPath = join(gsdDir, '.gsd');
302
+ const gsdPath = gsdRoot(gsdDir);
302
303
 
303
304
  // Root-level artifacts: PROJECT.md, QUEUE.md
304
305
  const rootFiles = ['PROJECT.md', 'QUEUE.md', 'SECRETS-MANIFEST.md'];
@@ -487,7 +488,7 @@ export function migrateFromMarkdown(gsdDir: string): {
487
488
  requirements: number;
488
489
  artifacts: number;
489
490
  } {
490
- const dbPath = join(gsdDir, '.gsd', 'gsd.db');
491
+ const dbPath = join(gsdRoot(gsdDir), 'gsd.db');
491
492
 
492
493
  // Open DB if not already open
493
494
  if (!_getAdapter()) {