gsd-pi 2.32.0 → 2.33.0-dev.69bff0f

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 (109) hide show
  1. package/README.md +22 -20
  2. package/dist/resource-loader.js +13 -3
  3. package/dist/resources/extensions/gsd/auto-dashboard.ts +3 -1
  4. package/dist/resources/extensions/gsd/auto-dispatch.ts +40 -12
  5. package/dist/resources/extensions/gsd/auto-idempotency.ts +3 -2
  6. package/dist/resources/extensions/gsd/auto-observability.ts +2 -4
  7. package/dist/resources/extensions/gsd/auto-post-unit.ts +5 -5
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +46 -44
  9. package/dist/resources/extensions/gsd/auto-recovery.ts +8 -22
  10. package/dist/resources/extensions/gsd/auto-start.ts +8 -6
  11. package/dist/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  12. package/dist/resources/extensions/gsd/auto-supervisor.ts +10 -5
  13. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  14. package/dist/resources/extensions/gsd/auto-timers.ts +3 -2
  15. package/dist/resources/extensions/gsd/auto-verification.ts +6 -6
  16. package/dist/resources/extensions/gsd/auto-worktree.ts +140 -5
  17. package/dist/resources/extensions/gsd/auto.ts +108 -182
  18. package/dist/resources/extensions/gsd/commands-inspect.ts +2 -1
  19. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +2 -1
  20. package/dist/resources/extensions/gsd/commands.ts +14 -2
  21. package/dist/resources/extensions/gsd/complexity-classifier.ts +5 -7
  22. package/dist/resources/extensions/gsd/crash-recovery.ts +15 -2
  23. package/dist/resources/extensions/gsd/dispatch-guard.ts +2 -1
  24. package/dist/resources/extensions/gsd/error-utils.ts +6 -0
  25. package/dist/resources/extensions/gsd/export.ts +2 -1
  26. package/dist/resources/extensions/gsd/git-service.ts +3 -2
  27. package/dist/resources/extensions/gsd/guided-flow.ts +3 -2
  28. package/dist/resources/extensions/gsd/index.ts +12 -5
  29. package/dist/resources/extensions/gsd/key-manager.ts +2 -1
  30. package/dist/resources/extensions/gsd/marketplace-discovery.ts +4 -3
  31. package/dist/resources/extensions/gsd/metrics.ts +3 -3
  32. package/dist/resources/extensions/gsd/migrate-external.ts +21 -4
  33. package/dist/resources/extensions/gsd/milestone-ids.ts +2 -1
  34. package/dist/resources/extensions/gsd/native-git-bridge.ts +2 -1
  35. package/dist/resources/extensions/gsd/parallel-merge.ts +2 -1
  36. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
  37. package/dist/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  38. package/dist/resources/extensions/gsd/quick.ts +58 -3
  39. package/dist/resources/extensions/gsd/repo-identity.ts +22 -1
  40. package/dist/resources/extensions/gsd/session-lock.ts +86 -11
  41. package/dist/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +14 -11
  42. package/dist/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +691 -0
  43. package/dist/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +317 -0
  44. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  45. package/dist/resources/extensions/gsd/tests/loop-regression.test.ts +877 -0
  46. package/dist/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +358 -0
  47. package/dist/resources/extensions/gsd/tests/session-lock-regression.test.ts +216 -0
  48. package/dist/resources/extensions/gsd/tests/session-lock.test.ts +119 -0
  49. package/dist/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +206 -0
  50. package/dist/resources/extensions/gsd/undo.ts +5 -7
  51. package/dist/resources/extensions/gsd/unit-id.ts +14 -0
  52. package/dist/resources/extensions/gsd/unit-runtime.ts +2 -1
  53. package/dist/resources/extensions/gsd/worktree-command.ts +8 -7
  54. package/package.json +3 -2
  55. package/packages/pi-coding-agent/package.json +1 -1
  56. package/pkg/package.json +1 -1
  57. package/src/resources/extensions/gsd/auto-dashboard.ts +3 -1
  58. package/src/resources/extensions/gsd/auto-dispatch.ts +40 -12
  59. package/src/resources/extensions/gsd/auto-idempotency.ts +3 -2
  60. package/src/resources/extensions/gsd/auto-observability.ts +2 -4
  61. package/src/resources/extensions/gsd/auto-post-unit.ts +5 -5
  62. package/src/resources/extensions/gsd/auto-prompts.ts +46 -44
  63. package/src/resources/extensions/gsd/auto-recovery.ts +8 -22
  64. package/src/resources/extensions/gsd/auto-start.ts +8 -6
  65. package/src/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  66. package/src/resources/extensions/gsd/auto-supervisor.ts +10 -5
  67. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  68. package/src/resources/extensions/gsd/auto-timers.ts +3 -2
  69. package/src/resources/extensions/gsd/auto-verification.ts +6 -6
  70. package/src/resources/extensions/gsd/auto-worktree.ts +140 -5
  71. package/src/resources/extensions/gsd/auto.ts +108 -182
  72. package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
  73. package/src/resources/extensions/gsd/commands-workflow-templates.ts +2 -1
  74. package/src/resources/extensions/gsd/commands.ts +14 -2
  75. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -7
  76. package/src/resources/extensions/gsd/crash-recovery.ts +15 -2
  77. package/src/resources/extensions/gsd/dispatch-guard.ts +2 -1
  78. package/src/resources/extensions/gsd/error-utils.ts +6 -0
  79. package/src/resources/extensions/gsd/export.ts +2 -1
  80. package/src/resources/extensions/gsd/git-service.ts +3 -2
  81. package/src/resources/extensions/gsd/guided-flow.ts +3 -2
  82. package/src/resources/extensions/gsd/index.ts +12 -5
  83. package/src/resources/extensions/gsd/key-manager.ts +2 -1
  84. package/src/resources/extensions/gsd/marketplace-discovery.ts +4 -3
  85. package/src/resources/extensions/gsd/metrics.ts +3 -3
  86. package/src/resources/extensions/gsd/migrate-external.ts +21 -4
  87. package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
  88. package/src/resources/extensions/gsd/native-git-bridge.ts +2 -1
  89. package/src/resources/extensions/gsd/parallel-merge.ts +2 -1
  90. package/src/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
  91. package/src/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  92. package/src/resources/extensions/gsd/quick.ts +58 -3
  93. package/src/resources/extensions/gsd/repo-identity.ts +22 -1
  94. package/src/resources/extensions/gsd/session-lock.ts +86 -11
  95. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +14 -11
  96. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +691 -0
  97. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +317 -0
  98. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  99. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +877 -0
  100. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +358 -0
  101. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +216 -0
  102. package/src/resources/extensions/gsd/tests/session-lock.test.ts +119 -0
  103. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +206 -0
  104. package/src/resources/extensions/gsd/undo.ts +5 -7
  105. package/src/resources/extensions/gsd/unit-id.ts +14 -0
  106. package/src/resources/extensions/gsd/unit-runtime.ts +2 -1
  107. package/src/resources/extensions/gsd/worktree-command.ts +8 -7
  108. package/dist/resources/extensions/mcporter/extension-manifest.json +0 -12
  109. package/src/resources/extensions/mcporter/extension-manifest.json +0 -12
@@ -10,7 +10,7 @@ import { createHash } from "node:crypto";
10
10
  import { execFileSync } from "node:child_process";
11
11
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, symlinkSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
- import { join, resolve } from "node:path";
13
+ import { join, resolve, sep } from "node:path";
14
14
 
15
15
  // ─── Repo Identity ──────────────────────────────────────────────────────────
16
16
 
@@ -37,6 +37,27 @@ function getRemoteUrl(basePath: string): string {
37
37
  */
38
38
  function resolveGitRoot(basePath: string): string {
39
39
  try {
40
+ // In a worktree, --show-toplevel returns the worktree path, not the main
41
+ // repo root. Use --git-common-dir to find the shared .git directory,
42
+ // then derive the main repo root from it (#1288).
43
+ const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
44
+ cwd: basePath,
45
+ encoding: "utf-8",
46
+ stdio: ["ignore", "pipe", "ignore"],
47
+ timeout: 5_000,
48
+ }).trim();
49
+
50
+ // If commonDir ends with .git/worktrees/<name>, the main repo is two
51
+ // levels up from the worktrees dir. If it's just .git, resolve normally.
52
+ if (commonDir.includes(`${sep}worktrees${sep}`) || commonDir.includes("/worktrees/")) {
53
+ // e.g., /path/to/project/.gsd/worktrees/M001/.git → /path/to/project
54
+ // or /path/to/project/.git/worktrees/M001 → /path/to/project
55
+ const gitDir = commonDir.replace(/[/\\]worktrees[/\\][^/\\]+$/, "");
56
+ const mainRoot = resolve(gitDir, "..");
57
+ return mainRoot;
58
+ }
59
+
60
+ // Not in a worktree — use --show-toplevel as usual
40
61
  return execFileSync("git", ["rev-parse", "--show-toplevel"], {
41
62
  cwd: basePath,
42
63
  encoding: "utf-8",
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { createRequire } from "node:module";
20
- import { existsSync, readFileSync, mkdirSync, unlinkSync, rmSync, statSync } from "node:fs";
20
+ import { existsSync, readFileSync, readdirSync, mkdirSync, unlinkSync, rmSync, statSync } from "node:fs";
21
21
  import { join, dirname } from "node:path";
22
22
  import { gsdRoot } from "./paths.js";
23
23
  import { atomicWriteSync } from "./atomic-write.js";
@@ -54,12 +54,81 @@ let _lockPid: number = 0;
54
54
  /** Set to true when proper-lockfile fires onCompromised (mtime drift, sleep, etc.). */
55
55
  let _lockCompromised: boolean = false;
56
56
 
57
+ /** Whether we've already registered a process.on('exit') handler. */
58
+ let _exitHandlerRegistered: boolean = false;
59
+
57
60
  const LOCK_FILE = "auto.lock";
58
61
 
59
62
  function lockPath(basePath: string): string {
60
63
  return join(gsdRoot(basePath), LOCK_FILE);
61
64
  }
62
65
 
66
+ // ─── Stray Lock Cleanup ─────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * Remove numbered lock file variants (e.g. "auto 2.lock", "auto 3.lock")
70
+ * that accumulate from macOS file conflict resolution (iCloud/Dropbox/OneDrive)
71
+ * or other filesystem-level copy-on-conflict behavior (#1315).
72
+ *
73
+ * Also removes stray proper-lockfile directories beyond the canonical `.gsd.lock/`.
74
+ */
75
+ export function cleanupStrayLockFiles(basePath: string): void {
76
+ const gsdDir = gsdRoot(basePath);
77
+
78
+ // Clean numbered auto lock files inside .gsd/
79
+ try {
80
+ if (existsSync(gsdDir)) {
81
+ for (const entry of readdirSync(gsdDir)) {
82
+ // Match "auto <N>.lock" or "auto (<N>).lock" variants but NOT the canonical "auto.lock"
83
+ if (entry !== LOCK_FILE && /^auto\s.+\.lock$/i.test(entry)) {
84
+ try { unlinkSync(join(gsdDir, entry)); } catch { /* best-effort */ }
85
+ }
86
+ }
87
+ }
88
+ } catch { /* non-fatal: directory read failure */ }
89
+
90
+ // Clean stray proper-lockfile directories (e.g. ".gsd 2.lock/")
91
+ // The canonical one is ".gsd.lock/" — anything else is stray.
92
+ try {
93
+ const parentDir = dirname(gsdDir);
94
+ const gsdDirName = gsdDir.split("/").pop() || ".gsd";
95
+ if (existsSync(parentDir)) {
96
+ for (const entry of readdirSync(parentDir)) {
97
+ // Match ".gsd <N>.lock" or ".gsd (<N>).lock" directories but NOT ".gsd.lock"
98
+ if (entry !== `${gsdDirName}.lock` && entry.startsWith(gsdDirName) && entry.endsWith(".lock")) {
99
+ const fullPath = join(parentDir, entry);
100
+ try {
101
+ const stat = statSync(fullPath);
102
+ if (stat.isDirectory()) {
103
+ rmSync(fullPath, { recursive: true, force: true });
104
+ }
105
+ } catch { /* best-effort */ }
106
+ }
107
+ }
108
+ }
109
+ } catch { /* non-fatal */ }
110
+ }
111
+
112
+ /**
113
+ * Register a single process exit handler that cleans up lock state.
114
+ * Uses module-level references so it always operates on current state.
115
+ * Only registers once — subsequent calls are no-ops.
116
+ */
117
+ function ensureExitHandler(gsdDir: string): void {
118
+ if (_exitHandlerRegistered) return;
119
+ _exitHandlerRegistered = true;
120
+
121
+ process.once("exit", () => {
122
+ try {
123
+ if (_releaseFunction) { _releaseFunction(); _releaseFunction = null; }
124
+ } catch { /* best-effort */ }
125
+ try {
126
+ const lockDir = join(gsdDir + ".lock");
127
+ if (existsSync(lockDir)) rmSync(lockDir, { recursive: true, force: true });
128
+ } catch { /* best-effort */ }
129
+ });
130
+ }
131
+
63
132
  // ─── Public API ─────────────────────────────────────────────────────────────
64
133
 
65
134
  /**
@@ -77,6 +146,9 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
77
146
  // Ensure the directory exists
78
147
  mkdirSync(dirname(lp), { recursive: true });
79
148
 
149
+ // Clean up numbered lock file variants from cloud sync conflicts (#1315)
150
+ cleanupStrayLockFiles(basePath);
151
+
80
152
  // Write our lock data first (the content is informational; the OS lock is the real guard)
81
153
  const lockData: SessionLockData = {
82
154
  pid: process.pid,
@@ -124,15 +196,7 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
124
196
 
125
197
  // Safety net: clean up lock dir on process exit if _releaseFunction
126
198
  // wasn't called (e.g., normal exit after clean completion) (#1245).
127
- const lockDirForCleanup = join(gsdDir + ".lock");
128
- process.once("exit", () => {
129
- try {
130
- if (_releaseFunction) { _releaseFunction(); _releaseFunction = null; }
131
- } catch { /* best-effort */ }
132
- try {
133
- if (existsSync(lockDirForCleanup)) rmSync(lockDirForCleanup, { recursive: true, force: true });
134
- } catch { /* best-effort */ }
135
- });
199
+ ensureExitHandler(gsdDir);
136
200
 
137
201
  // Write the informational lock data
138
202
  atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
@@ -154,12 +218,20 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
154
218
  // Retry acquisition after cleanup
155
219
  const release = lockfile.lockSync(gsdDir, {
156
220
  realpath: false,
157
- stale: 300_000,
221
+ stale: 1_800_000, // 30 minutes — match primary lock settings
158
222
  update: 10_000,
223
+ onCompromised: () => {
224
+ _lockCompromised = true;
225
+ _releaseFunction = null;
226
+ },
159
227
  });
160
228
  _releaseFunction = release;
161
229
  _lockedPath = basePath;
162
230
  _lockPid = process.pid;
231
+
232
+ // Safety net — uses centralized handler to avoid double-registration
233
+ ensureExitHandler(gsdDir);
234
+
163
235
  atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
164
236
  return { acquired: true };
165
237
  } catch {
@@ -299,6 +371,9 @@ export function releaseSessionLock(basePath: string): void {
299
371
  // Non-fatal
300
372
  }
301
373
 
374
+ // Clean up numbered lock file variants from cloud sync conflicts (#1315)
375
+ cleanupStrayLockFiles(basePath);
376
+
302
377
  _lockedPath = null;
303
378
  _lockPid = 0;
304
379
  _lockCompromised = false;
@@ -70,31 +70,34 @@ test("auto.ts 'all milestones complete' path merges before stopping (#962)", ()
70
70
  const incompleteIdx = autoSrc.indexOf("incomplete.length === 0");
71
71
  assert.ok(incompleteIdx > -1, "auto.ts should have 'incomplete.length === 0' check");
72
72
 
73
- // The merge call must appear BETWEEN the incomplete check and the stopAuto call
74
- // in that same block
73
+ // The merge call must appear BETWEEN the incomplete check and the stopAuto call.
74
+ // After the #1308 refactor, the merge is delegated to tryMergeMilestone.
75
75
  const blockAfterIncomplete = autoSrc.slice(incompleteIdx, incompleteIdx + 3000);
76
76
 
77
77
  assert.ok(
78
- blockAfterIncomplete.includes("mergeMilestoneToMain"),
79
- "auto.ts should call mergeMilestoneToMain in the 'all milestones complete' path",
78
+ blockAfterIncomplete.includes("tryMergeMilestone"),
79
+ "auto.ts should call tryMergeMilestone in the 'all milestones complete' path",
80
80
  );
81
81
 
82
82
  // The merge should come before stopAuto in this block
83
- const mergePos = blockAfterIncomplete.indexOf("mergeMilestoneToMain");
83
+ const mergePos = blockAfterIncomplete.indexOf("tryMergeMilestone");
84
84
  const stopPos = blockAfterIncomplete.indexOf("stopAuto");
85
85
  assert.ok(
86
86
  mergePos < stopPos,
87
- "mergeMilestoneToMain should be called before stopAuto in the 'all complete' path",
87
+ "tryMergeMilestone should be called before stopAuto in the 'all complete' path",
88
88
  );
89
89
 
90
- // Should handle both worktree and branch isolation modes
90
+ // Verify tryMergeMilestone handles both worktree and branch isolation
91
+ const helperIdx = autoSrc.indexOf("function tryMergeMilestone");
92
+ assert.ok(helperIdx > -1, "tryMergeMilestone helper should exist");
93
+ const helperBlock = autoSrc.slice(helperIdx, helperIdx + 2000);
91
94
  assert.ok(
92
- blockAfterIncomplete.includes("isInAutoWorktree"),
93
- "should check isInAutoWorktree for worktree mode",
95
+ helperBlock.includes("isInAutoWorktree"),
96
+ "tryMergeMilestone should check isInAutoWorktree for worktree mode",
94
97
  );
95
98
  assert.ok(
96
- blockAfterIncomplete.includes("getIsolationMode"),
97
- "should check getIsolationMode for branch isolation mode",
99
+ helperBlock.includes("getIsolationMode") || helperBlock.includes("isolationMode"),
100
+ "tryMergeMilestone should check isolation mode for branch mode",
98
101
  );
99
102
  });
100
103