gsd-pi 2.35.0 → 2.36.0-dev.d612764

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 (194) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +7 -2
  3. package/dist/resource-loader.d.ts +1 -1
  4. package/dist/resource-loader.js +13 -1
  5. package/dist/resources/extensions/async-jobs/await-tool.js +0 -2
  6. package/dist/resources/extensions/async-jobs/job-manager.js +0 -6
  7. package/dist/resources/extensions/bg-shell/output-formatter.js +1 -19
  8. package/dist/resources/extensions/bg-shell/process-manager.js +0 -4
  9. package/dist/resources/extensions/bg-shell/types.js +0 -2
  10. package/dist/resources/extensions/cmux/index.js +321 -0
  11. package/dist/resources/extensions/context7/index.js +5 -0
  12. package/dist/resources/extensions/get-secrets-from-user.js +2 -30
  13. package/dist/resources/extensions/google-search/index.js +5 -0
  14. package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
  15. package/dist/resources/extensions/gsd/auto-dispatch.js +43 -1
  16. package/dist/resources/extensions/gsd/auto-loop.js +28 -3
  17. package/dist/resources/extensions/gsd/auto-model-selection.js +15 -3
  18. package/dist/resources/extensions/gsd/auto-recovery.js +35 -0
  19. package/dist/resources/extensions/gsd/auto-start.js +35 -2
  20. package/dist/resources/extensions/gsd/auto.js +75 -4
  21. package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
  22. package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
  23. package/dist/resources/extensions/gsd/commands-inspect.js +10 -3
  24. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  25. package/dist/resources/extensions/gsd/commands-rate.js +31 -0
  26. package/dist/resources/extensions/gsd/commands.js +94 -2
  27. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  28. package/dist/resources/extensions/gsd/doctor-environment.js +26 -17
  29. package/dist/resources/extensions/gsd/files.js +11 -2
  30. package/dist/resources/extensions/gsd/gitignore.js +54 -7
  31. package/dist/resources/extensions/gsd/guided-flow.js +8 -2
  32. package/dist/resources/extensions/gsd/health-widget-core.js +96 -0
  33. package/dist/resources/extensions/gsd/health-widget.js +97 -46
  34. package/dist/resources/extensions/gsd/index.js +31 -33
  35. package/dist/resources/extensions/gsd/migrate-external.js +55 -2
  36. package/dist/resources/extensions/gsd/milestone-ids.js +3 -2
  37. package/dist/resources/extensions/gsd/notifications.js +10 -1
  38. package/dist/resources/extensions/gsd/paths.js +74 -7
  39. package/dist/resources/extensions/gsd/post-unit-hooks.js +4 -1
  40. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  41. package/dist/resources/extensions/gsd/preferences-validation.js +45 -1
  42. package/dist/resources/extensions/gsd/preferences.js +15 -0
  43. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  44. package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  45. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
  46. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  47. package/dist/resources/extensions/gsd/roadmap-mutations.js +55 -0
  48. package/dist/resources/extensions/gsd/session-lock.js +53 -2
  49. package/dist/resources/extensions/gsd/state.js +2 -1
  50. package/dist/resources/extensions/gsd/templates/plan.md +8 -0
  51. package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
  52. package/dist/resources/extensions/gsd/worktree-resolver.js +12 -0
  53. package/dist/resources/extensions/remote-questions/remote-command.js +2 -22
  54. package/dist/resources/extensions/search-the-web/native-search.js +45 -4
  55. package/dist/resources/extensions/shared/mod.js +1 -1
  56. package/dist/resources/extensions/shared/sanitize.js +30 -0
  57. package/dist/resources/extensions/shared/terminal.js +5 -0
  58. package/dist/resources/extensions/subagent/index.js +186 -74
  59. package/dist/resources/skills/core-web-vitals/SKILL.md +1 -1
  60. package/dist/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
  61. package/dist/resources/skills/github-workflows/SKILL.md +0 -2
  62. package/dist/resources/skills/web-quality-audit/SKILL.md +0 -2
  63. package/package.json +2 -1
  64. package/packages/pi-agent-core/dist/agent.d.ts +10 -2
  65. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  66. package/packages/pi-agent-core/dist/agent.js +19 -8
  67. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  68. package/packages/pi-agent-core/src/agent.ts +31 -10
  69. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  70. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  71. package/packages/pi-ai/src/providers/openai-responses.ts +1 -1
  72. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/agent-session.js +20 -4
  74. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/resource-loader.js +13 -2
  77. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  78. package/packages/pi-coding-agent/package.json +1 -1
  79. package/packages/pi-coding-agent/src/core/agent-session.ts +36 -12
  80. package/packages/pi-coding-agent/src/core/resource-loader.ts +13 -2
  81. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  82. package/packages/pi-tui/dist/terminal-image.js +4 -0
  83. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  84. package/packages/pi-tui/src/terminal-image.ts +5 -0
  85. package/pkg/package.json +1 -1
  86. package/src/resources/extensions/async-jobs/await-tool.ts +0 -2
  87. package/src/resources/extensions/async-jobs/job-manager.ts +0 -7
  88. package/src/resources/extensions/bg-shell/output-formatter.ts +0 -17
  89. package/src/resources/extensions/bg-shell/process-manager.ts +0 -4
  90. package/src/resources/extensions/bg-shell/types.ts +0 -12
  91. package/src/resources/extensions/cmux/index.ts +384 -0
  92. package/src/resources/extensions/context7/index.ts +7 -0
  93. package/src/resources/extensions/get-secrets-from-user.ts +2 -35
  94. package/src/resources/extensions/google-search/index.ts +7 -0
  95. package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
  96. package/src/resources/extensions/gsd/auto-dispatch.ts +49 -1
  97. package/src/resources/extensions/gsd/auto-loop.ts +64 -2
  98. package/src/resources/extensions/gsd/auto-model-selection.ts +23 -2
  99. package/src/resources/extensions/gsd/auto-recovery.ts +39 -0
  100. package/src/resources/extensions/gsd/auto-start.ts +42 -2
  101. package/src/resources/extensions/gsd/auto.ts +82 -3
  102. package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
  103. package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
  104. package/src/resources/extensions/gsd/commands-inspect.ts +10 -3
  105. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  106. package/src/resources/extensions/gsd/commands-rate.ts +55 -0
  107. package/src/resources/extensions/gsd/commands.ts +97 -2
  108. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  109. package/src/resources/extensions/gsd/doctor-environment.ts +26 -16
  110. package/src/resources/extensions/gsd/files.ts +12 -2
  111. package/src/resources/extensions/gsd/gitignore.ts +54 -7
  112. package/src/resources/extensions/gsd/guided-flow.ts +8 -2
  113. package/src/resources/extensions/gsd/health-widget-core.ts +129 -0
  114. package/src/resources/extensions/gsd/health-widget.ts +103 -59
  115. package/src/resources/extensions/gsd/index.ts +37 -32
  116. package/src/resources/extensions/gsd/migrate-external.ts +47 -2
  117. package/src/resources/extensions/gsd/milestone-ids.ts +3 -2
  118. package/src/resources/extensions/gsd/notifications.ts +10 -1
  119. package/src/resources/extensions/gsd/paths.ts +73 -7
  120. package/src/resources/extensions/gsd/post-unit-hooks.ts +5 -1
  121. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  122. package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
  123. package/src/resources/extensions/gsd/preferences.ts +18 -1
  124. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  125. package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  126. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
  127. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  128. package/src/resources/extensions/gsd/roadmap-mutations.ts +66 -0
  129. package/src/resources/extensions/gsd/session-lock.ts +59 -2
  130. package/src/resources/extensions/gsd/state.ts +2 -1
  131. package/src/resources/extensions/gsd/templates/plan.md +8 -0
  132. package/src/resources/extensions/gsd/templates/preferences.md +6 -0
  133. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  134. package/src/resources/extensions/gsd/tests/cmux.test.ts +98 -0
  135. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +46 -0
  136. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +20 -0
  137. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +214 -0
  138. package/src/resources/extensions/gsd/tests/health-widget.test.ts +158 -0
  139. package/src/resources/extensions/gsd/tests/paths.test.ts +113 -0
  140. package/src/resources/extensions/gsd/tests/preferences.test.ts +35 -2
  141. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +26 -0
  142. package/src/resources/extensions/gsd/tests/test-utils.ts +165 -0
  143. package/src/resources/extensions/gsd/tests/validate-directory.test.ts +15 -0
  144. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +7 -0
  145. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +32 -0
  146. package/src/resources/extensions/gsd/worktree-resolver.ts +11 -0
  147. package/src/resources/extensions/remote-questions/remote-command.ts +2 -23
  148. package/src/resources/extensions/search-the-web/native-search.ts +50 -4
  149. package/src/resources/extensions/shared/mod.ts +1 -1
  150. package/src/resources/extensions/shared/sanitize.ts +36 -0
  151. package/src/resources/extensions/shared/terminal.ts +5 -0
  152. package/src/resources/extensions/subagent/index.ts +242 -91
  153. package/src/resources/skills/core-web-vitals/SKILL.md +1 -1
  154. package/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
  155. package/src/resources/skills/github-workflows/SKILL.md +0 -2
  156. package/src/resources/skills/web-quality-audit/SKILL.md +0 -2
  157. package/dist/resources/extensions/shared/wizard-ui.js +0 -478
  158. package/dist/resources/skills/swiftui/SKILL.md +0 -208
  159. package/dist/resources/skills/swiftui/references/animations.md +0 -921
  160. package/dist/resources/skills/swiftui/references/architecture.md +0 -1561
  161. package/dist/resources/skills/swiftui/references/layout-system.md +0 -1186
  162. package/dist/resources/skills/swiftui/references/navigation.md +0 -1492
  163. package/dist/resources/skills/swiftui/references/networking-async.md +0 -214
  164. package/dist/resources/skills/swiftui/references/performance.md +0 -1706
  165. package/dist/resources/skills/swiftui/references/platform-integration.md +0 -204
  166. package/dist/resources/skills/swiftui/references/state-management.md +0 -1443
  167. package/dist/resources/skills/swiftui/references/swiftdata.md +0 -297
  168. package/dist/resources/skills/swiftui/references/testing-debugging.md +0 -247
  169. package/dist/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
  170. package/dist/resources/skills/swiftui/workflows/add-feature.md +0 -191
  171. package/dist/resources/skills/swiftui/workflows/build-new-app.md +0 -311
  172. package/dist/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
  173. package/dist/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
  174. package/dist/resources/skills/swiftui/workflows/ship-app.md +0 -203
  175. package/dist/resources/skills/swiftui/workflows/write-tests.md +0 -235
  176. package/src/resources/extensions/shared/wizard-ui.ts +0 -551
  177. package/src/resources/skills/swiftui/SKILL.md +0 -208
  178. package/src/resources/skills/swiftui/references/animations.md +0 -921
  179. package/src/resources/skills/swiftui/references/architecture.md +0 -1561
  180. package/src/resources/skills/swiftui/references/layout-system.md +0 -1186
  181. package/src/resources/skills/swiftui/references/navigation.md +0 -1492
  182. package/src/resources/skills/swiftui/references/networking-async.md +0 -214
  183. package/src/resources/skills/swiftui/references/performance.md +0 -1706
  184. package/src/resources/skills/swiftui/references/platform-integration.md +0 -204
  185. package/src/resources/skills/swiftui/references/state-management.md +0 -1443
  186. package/src/resources/skills/swiftui/references/swiftdata.md +0 -297
  187. package/src/resources/skills/swiftui/references/testing-debugging.md +0 -247
  188. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
  189. package/src/resources/skills/swiftui/workflows/add-feature.md +0 -191
  190. package/src/resources/skills/swiftui/workflows/build-new-app.md +0 -311
  191. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
  192. package/src/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
  193. package/src/resources/skills/swiftui/workflows/ship-app.md +0 -203
  194. package/src/resources/skills/swiftui/workflows/write-tests.md +0 -235
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  import { readdirSync, existsSync, realpathSync, Dirent } from "node:fs";
13
- import { join } from "node:path";
13
+ import { join, dirname, normalize } from "node:path";
14
+ import { spawnSync } from "node:child_process";
14
15
  import { nativeScanGsdTree, type GsdTreeEntry } from "./native-parser-bridge.js";
15
16
  import { DIR_CACHE_MAX } from "./constants.js";
16
17
 
@@ -277,15 +278,80 @@ const LEGACY_GSD_ROOT_FILES: Record<GSDRootFileKey, string> = {
277
278
  KNOWLEDGE: "knowledge.md",
278
279
  };
279
280
 
281
+ // ─── GSD Root Discovery ───────────────────────────────────────────────────────
282
+
283
+ const gsdRootCache = new Map<string, string>();
284
+
285
+ /** Exported for tests only — do not call in production code. */
286
+ export function _clearGsdRootCache(): void {
287
+ gsdRootCache.clear();
288
+ }
289
+
290
+ /**
291
+ * Resolve the `.gsd` directory for a given project base path.
292
+ *
293
+ * Probe order:
294
+ * 1. basePath/.gsd — fast path (common case)
295
+ * 2. git rev-parse root — handles cwd-is-a-subdirectory
296
+ * 3. Walk up from basePath — handles moved .gsd in an ancestor (bounded by git root)
297
+ * 4. basePath/.gsd — creation fallback (init scenario)
298
+ *
299
+ * Result is cached per basePath for the process lifetime.
300
+ */
280
301
  export function gsdRoot(basePath: string): string {
281
- const local = join(basePath, ".gsd");
282
- try {
283
- const resolved = realpathSync(local);
284
- if (resolved !== local) return resolved; // symlink resolved
285
- } catch { /* doesn't exist yet — fall through */ }
286
- return local; // backwards compat: unmigrated projects
302
+ const cached = gsdRootCache.get(basePath);
303
+ if (cached) return cached;
304
+
305
+ const result = probeGsdRoot(basePath);
306
+ gsdRootCache.set(basePath, result);
307
+ return result;
287
308
  }
288
309
 
310
+ function probeGsdRoot(rawBasePath: string): string {
311
+ // 1. Fast path — check the input path directly
312
+ const local = join(rawBasePath, ".gsd");
313
+ if (existsSync(local)) return local;
314
+
315
+ // Resolve symlinks so path comparisons work correctly across platforms
316
+ // (e.g. macOS /var → /private/var). Use rawBasePath as fallback if not resolvable.
317
+ let basePath: string;
318
+ try { basePath = realpathSync.native(rawBasePath); } catch { basePath = rawBasePath; }
319
+
320
+ // 2. Git root anchor — used as both probe target and walk-up boundary
321
+ // Only walk if we're inside a git project — prevents escaping into
322
+ // unrelated filesystem territory when running outside any repo.
323
+ let gitRoot: string | null = null;
324
+ try {
325
+ const out = spawnSync("git", ["rev-parse", "--show-toplevel"], {
326
+ cwd: basePath,
327
+ encoding: "utf-8",
328
+ });
329
+ if (out.status === 0) {
330
+ const r = out.stdout.trim();
331
+ if (r) gitRoot = normalize(r);
332
+ }
333
+ } catch { /* git not available */ }
334
+
335
+ if (gitRoot) {
336
+ const candidate = join(gitRoot, ".gsd");
337
+ if (existsSync(candidate)) return candidate;
338
+ }
339
+
340
+ // 3. Walk up from basePath to the git root (only if we are in a subdirectory)
341
+ if (gitRoot && basePath !== gitRoot) {
342
+ let cur = dirname(basePath);
343
+ while (cur !== basePath) {
344
+ const candidate = join(cur, ".gsd");
345
+ if (existsSync(candidate)) return candidate;
346
+ if (cur === gitRoot) break;
347
+ basePath = cur;
348
+ cur = dirname(cur);
349
+ }
350
+ }
351
+
352
+ // 4. Fallback for init/creation
353
+ return local;
354
+ }
289
355
  export function milestonesDir(basePath: string): string {
290
356
  return join(gsdRoot(basePath), "milestones");
291
357
  }
@@ -149,11 +149,15 @@ function dequeueNextHook(basePath: string): HookDispatchResult | null {
149
149
 
150
150
  // Build the prompt with variable substitution
151
151
  const [mid, sid, tid] = triggerUnitId.split("/");
152
- const prompt = config.prompt
152
+ let prompt = config.prompt
153
153
  .replace(/\{milestoneId\}/g, mid ?? "")
154
154
  .replace(/\{sliceId\}/g, sid ?? "")
155
155
  .replace(/\{taskId\}/g, tid ?? "");
156
156
 
157
+ // Inject browser safety instruction for hooks that may use browser tools (#1345).
158
+ // Vite HMR and other persistent connections prevent networkidle from resolving.
159
+ prompt += "\n\n**Browser tool safety:** Do NOT use `browser_wait_for` with `condition: \"network_idle\"` — it hangs indefinitely when dev servers keep persistent connections (Vite HMR, WebSocket). Use `selector_visible`, `text_visible`, or `delay` instead.";
160
+
157
161
  return {
158
162
  hookName: config.name,
159
163
  prompt,
@@ -68,6 +68,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
68
68
  "budget_enforcement",
69
69
  "context_pause_threshold",
70
70
  "notifications",
71
+ "cmux",
71
72
  "remote_questions",
72
73
  "git",
73
74
  "post_unit_hooks",
@@ -84,6 +85,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
84
85
  "search_provider",
85
86
  "compression_strategy",
86
87
  "context_selection",
88
+ "widget_mode",
87
89
  ]);
88
90
 
89
91
  /** Canonical list of all dispatch unit types. */
@@ -164,6 +166,14 @@ export interface RemoteQuestionsConfig {
164
166
  poll_interval_seconds?: number; // clamped to 2-30
165
167
  }
166
168
 
169
+ export interface CmuxPreferences {
170
+ enabled?: boolean;
171
+ notifications?: boolean;
172
+ sidebar?: boolean;
173
+ splits?: boolean;
174
+ browser?: boolean;
175
+ }
176
+
167
177
  export interface GSDPreferences {
168
178
  version?: number;
169
179
  mode?: WorkflowMode;
@@ -182,6 +192,7 @@ export interface GSDPreferences {
182
192
  budget_enforcement?: BudgetEnforcementMode;
183
193
  context_pause_threshold?: number;
184
194
  notifications?: NotificationPreferences;
195
+ cmux?: CmuxPreferences;
185
196
  remote_questions?: RemoteQuestionsConfig;
186
197
  git?: GitPreferences;
187
198
  post_unit_hooks?: PostUnitHookConfig[];
@@ -202,6 +213,8 @@ export interface GSDPreferences {
202
213
  compression_strategy?: CompressionStrategy;
203
214
  /** Context selection mode for file inlining. "full" inlines entire files, "smart" uses semantic chunking. Default derived from token profile. */
204
215
  context_selection?: ContextSelectionMode;
216
+ /** Default widget display mode for auto-mode dashboard. "full" | "small" | "min" | "off". Default: "full". */
217
+ widget_mode?: "full" | "small" | "min" | "off";
205
218
  }
206
219
 
207
220
  export interface LoadedGSDPreferences {
@@ -33,9 +33,24 @@ export function validatePreferences(preferences: GSDPreferences): {
33
33
  const validated: GSDPreferences = {};
34
34
 
35
35
  // ─── Unknown Key Detection ──────────────────────────────────────────
36
+ // Common key migration hints for pi-level settings that don't map to GSD prefs
37
+ const KEY_MIGRATION_HINTS: Record<string, string> = {
38
+ taskIsolation: 'use "git.isolation" instead (values: worktree, branch, none)',
39
+ task_isolation: 'use "git.isolation" instead (values: worktree, branch, none)',
40
+ isolation: 'use "git.isolation" instead (values: worktree, branch, none)',
41
+ manage_gitignore: 'use "git.manage_gitignore" instead',
42
+ auto_push: 'use "git.auto_push" instead',
43
+ main_branch: 'use "git.main_branch" instead',
44
+ };
45
+
36
46
  for (const key of Object.keys(preferences)) {
37
47
  if (!KNOWN_PREFERENCE_KEYS.has(key)) {
38
- warnings.push(`unknown preference key "${key}" — ignored`);
48
+ const hint = KEY_MIGRATION_HINTS[key];
49
+ if (hint) {
50
+ warnings.push(`unknown preference key "${key}" — ${hint}`);
51
+ } else {
52
+ warnings.push(`unknown preference key "${key}" — ignored`);
53
+ }
39
54
  }
40
55
  }
41
56
 
@@ -227,6 +242,32 @@ export function validatePreferences(preferences: GSDPreferences): {
227
242
  }
228
243
  }
229
244
 
245
+ // ─── Cmux ───────────────────────────────────────────────────────────────
246
+ if (preferences.cmux !== undefined) {
247
+ if (preferences.cmux && typeof preferences.cmux === "object") {
248
+ const cmux = preferences.cmux as Record<string, unknown>;
249
+ const validatedCmux: NonNullable<GSDPreferences["cmux"]> = {};
250
+ if (cmux.enabled !== undefined) validatedCmux.enabled = !!cmux.enabled;
251
+ if (cmux.notifications !== undefined) validatedCmux.notifications = !!cmux.notifications;
252
+ if (cmux.sidebar !== undefined) validatedCmux.sidebar = !!cmux.sidebar;
253
+ if (cmux.splits !== undefined) validatedCmux.splits = !!cmux.splits;
254
+ if (cmux.browser !== undefined) validatedCmux.browser = !!cmux.browser;
255
+
256
+ const knownCmuxKeys = new Set(["enabled", "notifications", "sidebar", "splits", "browser"]);
257
+ for (const key of Object.keys(cmux)) {
258
+ if (!knownCmuxKeys.has(key)) {
259
+ warnings.push(`unknown cmux key "${key}" — ignored`);
260
+ }
261
+ }
262
+
263
+ if (Object.keys(validatedCmux).length > 0) {
264
+ validated.cmux = validatedCmux;
265
+ }
266
+ } else {
267
+ errors.push("cmux must be an object");
268
+ }
269
+ }
270
+
230
271
  // ─── Remote Questions ───────────────────────────────────────────────
231
272
  if (preferences.remote_questions !== undefined) {
232
273
  if (preferences.remote_questions && typeof preferences.remote_questions === "object") {
@@ -15,9 +15,10 @@ import { homedir } from "node:os";
15
15
  import { join } from "node:path";
16
16
  import { gsdRoot } from "./paths.js";
17
17
  import { parse as parseYaml } from "yaml";
18
- import type { PostUnitHookConfig, PreDispatchHookConfig } from "./types.js";
18
+ import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile } from "./types.js";
19
19
  import type { DynamicRoutingConfig } from "./model-router.js";
20
20
  import { normalizeStringArray } from "../shared/mod.js";
21
+ import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
21
22
 
22
23
  import {
23
24
  MODE_DEFAULTS,
@@ -44,6 +45,7 @@ export type {
44
45
  SkillDiscoveryMode,
45
46
  AutoSupervisorConfig,
46
47
  RemoteQuestionsConfig,
48
+ CmuxPreferences,
47
49
  GSDPreferences,
48
50
  LoadedGSDPreferences,
49
51
  SkillResolution,
@@ -141,6 +143,18 @@ export function loadEffectiveGSDPreferences(): LoadedGSDPreferences | null {
141
143
  };
142
144
  }
143
145
 
146
+ // Apply token-profile defaults as the lowest-priority layer so that
147
+ // `token_profile: budget` sets models and phase-skips automatically.
148
+ // Explicit user preferences always override profile defaults.
149
+ const profile = result.preferences.token_profile as TokenProfile | undefined;
150
+ if (profile) {
151
+ const profileDefaults = _resolveProfileDefaults(profile);
152
+ result = {
153
+ ...result,
154
+ preferences: mergePreferences(profileDefaults as GSDPreferences, result.preferences),
155
+ };
156
+ }
157
+
144
158
  // Apply mode defaults as the lowest-priority layer
145
159
  if (result.preferences.mode) {
146
160
  result = {
@@ -228,6 +242,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
228
242
  notifications: (base.notifications || override.notifications)
229
243
  ? { ...(base.notifications ?? {}), ...(override.notifications ?? {}) }
230
244
  : undefined,
245
+ cmux: (base.cmux || override.cmux)
246
+ ? { ...(base.cmux ?? {}), ...(override.cmux ?? {}) }
247
+ : undefined,
231
248
  remote_questions: override.remote_questions
232
249
  ? { ...(base.remote_questions ?? {}), ...override.remote_questions }
233
250
  : base.remote_questions,
@@ -28,6 +28,8 @@ Then:
28
28
 
29
29
  **Important:** Do NOT skip the success criteria and definition of done verification (steps 3-4). The milestone summary must reflect actual verified outcomes, not assumed success. If any criterion was not met, document it clearly in the summary and do not mark the milestone as passing verification.
30
30
 
31
+ **File system safety:** When scanning milestone directories for evidence, use `ls` or `find` to list directory contents first — never pass a directory path (e.g. `tasks/`, `slices/`) directly to the `read` tool. The `read` tool only accepts file paths, not directories.
32
+
31
33
  **You MUST write `{{milestoneSummaryPath}}` AND update PROJECT.md before finishing.**
32
34
 
33
35
  When done, say: "Milestone {{milestoneId}} complete."
@@ -25,9 +25,10 @@ Then research the codebase and relevant technologies. Narrate key findings and s
25
25
  2. **Skill Discovery ({{skillDiscoveryMode}}):**{{skillDiscoveryInstructions}}
26
26
  3. Explore relevant code. For small/familiar codebases, use `rg`, `find`, and targeted reads. For large or unfamiliar codebases, use `scout` to build a broad map efficiently before diving in.
27
27
  4. Use `resolve_library` / `get_library_docs` for unfamiliar libraries — skip this for libraries already used in the codebase
28
- 5. Use the **Research** output template from the inlined context above include only sections that have real content
29
- 6. If `.gsd/REQUIREMENTS.md` exists, research against it. Identify which Active requirements are table stakes, likely omissions, overbuilt risks, or domain-standard behaviors the user may or may not want.
30
- 7. Write `{{outputPath}}`
28
+ 5. **Web search budget:** You have a limited budget of web searches (max ~15 per session). Use them strategically prefer `resolve_library` / `get_library_docs` for library documentation. Do NOT repeat the same or similar queries. If a search didn't find what you need, rephrase once or move on. Target 3-5 total web searches for a typical research unit.
29
+ 6. Use the **Research** output template from the inlined context above include only sections that have real content
30
+ 7. If `.gsd/REQUIREMENTS.md` exists, research against it. Identify which Active requirements are table stakes, likely omissions, overbuilt risks, or domain-standard behaviors the user may or may not want.
31
+ 8. Write `{{outputPath}}`
31
32
 
32
33
  ## Strategic Questions to Answer
33
34
 
@@ -46,8 +46,9 @@ Research what this slice needs. Narrate key findings and surprises as you go —
46
46
  2. **Skill Discovery ({{skillDiscoveryMode}}):**{{skillDiscoveryInstructions}}
47
47
  3. Explore relevant code for this slice's scope. For targeted exploration, use `rg`, `find`, and reads. For broad or unfamiliar subsystems, use `scout` to map the relevant area first.
48
48
  4. Use `resolve_library` / `get_library_docs` for unfamiliar libraries — skip this for libraries already used in the codebase
49
- 5. Use the **Research** output template from the inlined context aboveinclude only sections that have real content. The template is already inlined above; do NOT attempt to read any template file from disk (there is no `templates/SLICE-RESEARCH.md` the correct template is already present in this prompt).
50
- 6. Write `{{outputPath}}`
49
+ 5. **Web search budget:** You have a limited budget of web searches (max ~15 per session). Use them strategically prefer `resolve_library` / `get_library_docs` for library documentation. Do NOT repeat the same or similar queries. If a search didn't find what you need, rephrase once or move on. Target 3-5 total web searches for a typical research unit.
50
+ 6. Use the **Research** output template from the inlined context above — include only sections that have real content. The template is already inlined above; do NOT attempt to read any template file from disk (there is no `templates/SLICE-RESEARCH.md` — the correct template is already present in this prompt).
51
+ 7. Write `{{outputPath}}`
51
52
 
52
53
  The slice directory already exists at `{{slicePath}}/`. Do NOT mkdir — just write the file.
53
54
 
@@ -67,4 +67,6 @@ If verdict is `needs-remediation`:
67
67
 
68
68
  **You MUST write `{{validationPath}}` before finishing.**
69
69
 
70
+ **File system safety:** When scanning milestone directories for evidence, use `ls` or `find` to list directory contents first — never pass a directory path (e.g. `tasks/`, `slices/`) directly to the `read` tool. The `read` tool only accepts file paths, not directories.
71
+
70
72
  When done, say: "Milestone {{milestoneId}} validation complete — verdict: <verdict>."
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Roadmap Mutations — shared utilities for modifying roadmap checkbox state.
3
+ *
4
+ * Extracts the duplicated "flip slice checkbox" pattern that existed in
5
+ * doctor.ts, mechanical-completion.ts, and auto-recovery.ts.
6
+ */
7
+
8
+ import { readFileSync } from "node:fs";
9
+ import { atomicWriteSync } from "./atomic-write.js";
10
+ import { resolveMilestoneFile } from "./paths.js";
11
+ import { clearParseCache } from "./files.js";
12
+
13
+ /**
14
+ * Mark a slice as done ([x]) in the milestone roadmap.
15
+ * Idempotent — no-op if already checked or if the slice isn't found.
16
+ *
17
+ * @returns true if the roadmap was modified, false if no change was needed
18
+ */
19
+ export function markSliceDoneInRoadmap(basePath: string, mid: string, sid: string): boolean {
20
+ const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
21
+ if (!roadmapFile) return false;
22
+
23
+ let content: string;
24
+ try {
25
+ content = readFileSync(roadmapFile, "utf-8");
26
+ } catch {
27
+ return false;
28
+ }
29
+
30
+ const updated = content.replace(
31
+ new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${sid}:`, "m"),
32
+ `$1[x] **${sid}:`,
33
+ );
34
+
35
+ if (updated === content) return false;
36
+
37
+ atomicWriteSync(roadmapFile, updated);
38
+ clearParseCache();
39
+ return true;
40
+ }
41
+
42
+ /**
43
+ * Mark a task as done ([x]) in the slice plan.
44
+ * Idempotent — no-op if already checked or if the task isn't found.
45
+ *
46
+ * @returns true if the plan was modified, false if no change was needed
47
+ */
48
+ export function markTaskDoneInPlan(basePath: string, planPath: string, tid: string): boolean {
49
+ let content: string;
50
+ try {
51
+ content = readFileSync(planPath, "utf-8");
52
+ } catch {
53
+ return false;
54
+ }
55
+
56
+ const updated = content.replace(
57
+ new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${tid}:`, "m"),
58
+ `$1[x] **${tid}:`,
59
+ );
60
+
61
+ if (updated === content) return false;
62
+
63
+ atomicWriteSync(planPath, updated);
64
+ clearParseCache();
65
+ return true;
66
+ }
@@ -57,9 +57,19 @@ let _lockCompromised: boolean = false;
57
57
  /** Whether we've already registered a process.on('exit') handler. */
58
58
  let _exitHandlerRegistered: boolean = false;
59
59
 
60
+ /** Snapshotted lock file path — captured at acquireSessionLock time to avoid
61
+ * gsdRoot() resolving differently in worktree vs project root contexts (#1363). */
62
+ let _snapshotLockPath: string | null = null;
63
+
64
+ /** Timestamp when the session lock was acquired — used to detect false-positive
65
+ * onCompromised events from event loop stalls within the stale window (#1362). */
66
+ let _lockAcquiredAt: number = 0;
67
+
60
68
  const LOCK_FILE = "auto.lock";
61
69
 
62
70
  function lockPath(basePath: string): string {
71
+ // If we have a snapshotted path from acquisition, use it for consistency
72
+ if (_snapshotLockPath) return _snapshotLockPath;
63
73
  return join(gsdRoot(basePath), LOCK_FILE);
64
74
  }
65
75
 
@@ -198,8 +208,19 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
198
208
  onCompromised: () => {
199
209
  // proper-lockfile detected mtime drift (system sleep, event loop stall, etc.).
200
210
  // Default handler throws inside setTimeout — an uncaught exception that crashes
201
- // or corrupts process state. Instead, set a flag so validateSessionLock() can
202
- // detect the compromise gracefully on the next dispatch cycle.
211
+ // or corrupts process state.
212
+ //
213
+ // False-positive suppression (#1362): If we're still within the stale window
214
+ // (30 min since acquisition), the mtime mismatch is from an event loop stall
215
+ // during a long LLM call — not a real takeover. Log and continue.
216
+ const elapsed = Date.now() - _lockAcquiredAt;
217
+ if (elapsed < 1_800_000) {
218
+ process.stderr.write(
219
+ `[gsd] Lock heartbeat mismatch after ${Math.round(elapsed / 1000)}s — event loop stall, continuing.\n`,
220
+ );
221
+ return; // Suppress false positive
222
+ }
223
+ // Past the stale window — this is a real compromise
203
224
  _lockCompromised = true;
204
225
  _releaseFunction = null;
205
226
  },
@@ -209,6 +230,8 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
209
230
  _lockedPath = basePath;
210
231
  _lockPid = process.pid;
211
232
  _lockCompromised = false;
233
+ _lockAcquiredAt = Date.now();
234
+ _snapshotLockPath = lp; // Snapshot the resolved path for consistent access (#1363)
212
235
 
213
236
  // Safety net: clean up lock dir on process exit if _releaseFunction
214
237
  // wasn't called (e.g., normal exit after clean completion) (#1245).
@@ -237,6 +260,16 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
237
260
  stale: 1_800_000, // 30 minutes — match primary lock settings
238
261
  update: 10_000,
239
262
  onCompromised: () => {
263
+ // Same false-positive suppression as the primary lock (#1512).
264
+ // Without this, the retry path fires _lockCompromised unconditionally
265
+ // on benign mtime drift (laptop sleep, heavy LLM event loop stalls).
266
+ const elapsed = Date.now() - _lockAcquiredAt;
267
+ if (elapsed < 1_800_000) {
268
+ process.stderr.write(
269
+ `[gsd] Lock heartbeat mismatch after ${Math.round(elapsed / 1000)}s — event loop stall, continuing.\n`,
270
+ );
271
+ return;
272
+ }
240
273
  _lockCompromised = true;
241
274
  _releaseFunction = null;
242
275
  },
@@ -245,6 +278,8 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
245
278
  _lockedPath = basePath;
246
279
  _lockPid = process.pid;
247
280
  _lockCompromised = false;
281
+ _lockAcquiredAt = Date.now();
282
+ _snapshotLockPath = lp; // Snapshot for retry path too (#1363)
248
283
 
249
284
  // Safety net — uses centralized handler to avoid double-registration
250
285
  ensureExitHandler(gsdDir);
@@ -336,6 +371,26 @@ export function updateSessionLock(
336
371
  export function validateSessionLock(basePath: string): boolean {
337
372
  // Lock was compromised by proper-lockfile (mtime drift from sleep, stall, etc.)
338
373
  if (_lockCompromised) {
374
+ // Recovery gate (#1512): Before declaring the lock lost, check if the lock
375
+ // file still contains our PID. If it does, no other process took over — the
376
+ // onCompromised fired from benign mtime drift (laptop sleep, event loop stall
377
+ // beyond the stale window). Attempt re-acquisition instead of giving up.
378
+ const lp = lockPath(basePath);
379
+ const existing = readExistingLockData(lp);
380
+ if (existing && existing.pid === process.pid) {
381
+ // Lock file still ours — try to re-acquire the OS lock
382
+ try {
383
+ const result = acquireSessionLock(basePath);
384
+ if (result.acquired) {
385
+ process.stderr.write(
386
+ `[gsd] Lock recovered after onCompromised — lock file PID matched, re-acquired.\n`,
387
+ );
388
+ return true;
389
+ }
390
+ } catch {
391
+ // Re-acquisition failed — fall through to return false
392
+ }
393
+ }
339
394
  return false;
340
395
  }
341
396
 
@@ -394,6 +449,8 @@ export function releaseSessionLock(basePath: string): void {
394
449
  _lockedPath = null;
395
450
  _lockPid = 0;
396
451
  _lockCompromised = false;
452
+ _lockAcquiredAt = 0;
453
+ _snapshotLockPath = null;
397
454
  }
398
455
 
399
456
  /**
@@ -64,11 +64,12 @@ export function isValidationTerminal(validationContent: string): boolean {
64
64
  if (!match) return false;
65
65
  const verdict = match[1].match(/verdict:\s*(\S+)/);
66
66
  if (!verdict) return false;
67
+ const v = verdict[1] === 'passed' ? 'pass' : verdict[1];
67
68
  // 'pass' and 'needs-attention' are always terminal.
68
69
  // 'needs-remediation' is treated as terminal to prevent infinite loops
69
70
  // when no remediation slices exist in the roadmap (#832). The validation
70
71
  // report is preserved on disk for manual review.
71
- return verdict[1] === 'pass' || verdict[1] === 'needs-attention' || verdict[1] === 'needs-remediation';
72
+ return v === 'pass' || v === 'needs-attention' || v === 'needs-remediation';
72
73
  }
73
74
 
74
75
  // ─── State Derivation ──────────────────────────────────────────────────────
@@ -113,6 +113,14 @@
113
113
  - Tasks execute sequentially in order (T01, T02, T03, ...)
114
114
  - est: is informational (e.g. 30m, 1h, 2h) and optional
115
115
 
116
+ Verify field rules:
117
+ - MUST be a mechanically executable command: `npm test`, `grep -q "pattern" file`, `test -f path`
118
+ - For content/document tasks: verify file existence, section count, YAML validity, or word count
119
+ NOT exact phrasing, specific formulas, or "zero TBD" aspirational criteria
120
+ - If no command can verify the output, write: "Manual review — file exists and is non-empty"
121
+ - BAD: "Sections 3.1 and 3.2 exist with exact formulas. Zero TBD/TODO."
122
+ - GOOD: `grep -c "^## " doc.md` returns >= 4 (4+ sections), `! grep -q "TBD\|TODO" doc.md`
123
+
116
124
  Integration closure rule:
117
125
  - At least one slice in any multi-boundary milestone should perform real composition/wiring, not just contract hardening
118
126
  - For the final assembly slice, verification must exercise the real entrypoint or runtime path
@@ -57,6 +57,12 @@ notifications:
57
57
  on_budget:
58
58
  on_milestone:
59
59
  on_attention:
60
+ cmux:
61
+ enabled:
62
+ notifications:
63
+ sidebar:
64
+ splits:
65
+ browser:
60
66
  remote_questions:
61
67
  channel:
62
68
  channel_id:
@@ -317,6 +317,8 @@ function makeMockDeps(
317
317
  },
318
318
  clearUnitTimeout: () => {},
319
319
  updateProgressWidget: () => {},
320
+ syncCmuxSidebar: () => {},
321
+ logCmuxEvent: () => {},
320
322
  invalidateAllCaches: () => {
321
323
  callLog.push("invalidateAllCaches");
322
324
  },
@@ -0,0 +1,98 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ buildCmuxProgress,
5
+ buildCmuxStatusLabel,
6
+ detectCmuxEnvironment,
7
+ markCmuxPromptShown,
8
+ resetCmuxPromptState,
9
+ resolveCmuxConfig,
10
+ shouldPromptToEnableCmux,
11
+ } from "../../cmux/index.ts";
12
+ import type { GSDState } from "../types.ts";
13
+
14
+ test("detectCmuxEnvironment requires workspace, surface, and socket", () => {
15
+ const detected = detectCmuxEnvironment(
16
+ {
17
+ CMUX_WORKSPACE_ID: "workspace:1",
18
+ CMUX_SURFACE_ID: "surface:2",
19
+ CMUX_SOCKET_PATH: "/tmp/cmux.sock",
20
+ },
21
+ (path) => path === "/tmp/cmux.sock",
22
+ () => true,
23
+ );
24
+ assert.equal(detected.available, true);
25
+ assert.equal(detected.cliAvailable, true);
26
+ });
27
+
28
+ test("resolveCmuxConfig enables only when preference and environment are both active", () => {
29
+ const config = resolveCmuxConfig(
30
+ { cmux: { enabled: true, notifications: true, sidebar: true, splits: true } },
31
+ {
32
+ CMUX_WORKSPACE_ID: "workspace:1",
33
+ CMUX_SURFACE_ID: "surface:2",
34
+ CMUX_SOCKET_PATH: "/tmp/cmux.sock",
35
+ },
36
+ () => true,
37
+ () => true,
38
+ );
39
+ assert.equal(config.enabled, true);
40
+ assert.equal(config.notifications, true);
41
+ assert.equal(config.sidebar, true);
42
+ assert.equal(config.splits, true);
43
+ });
44
+
45
+ test("shouldPromptToEnableCmux only prompts once per session", () => {
46
+ resetCmuxPromptState();
47
+ assert.equal(shouldPromptToEnableCmux({}, {}, () => false, () => true), false);
48
+
49
+ assert.equal(
50
+ shouldPromptToEnableCmux(
51
+ {},
52
+ {
53
+ CMUX_WORKSPACE_ID: "workspace:1",
54
+ CMUX_SURFACE_ID: "surface:2",
55
+ CMUX_SOCKET_PATH: "/tmp/cmux.sock",
56
+ },
57
+ () => true,
58
+ () => true,
59
+ ),
60
+ true,
61
+ );
62
+ markCmuxPromptShown();
63
+ assert.equal(
64
+ shouldPromptToEnableCmux(
65
+ {},
66
+ {
67
+ CMUX_WORKSPACE_ID: "workspace:1",
68
+ CMUX_SURFACE_ID: "surface:2",
69
+ CMUX_SOCKET_PATH: "/tmp/cmux.sock",
70
+ },
71
+ () => true,
72
+ () => true,
73
+ ),
74
+ false,
75
+ );
76
+ resetCmuxPromptState();
77
+ });
78
+
79
+ test("buildCmuxStatusLabel and progress prefer deepest active unit", () => {
80
+ const state: GSDState = {
81
+ activeMilestone: { id: "M001", title: "Milestone" },
82
+ activeSlice: { id: "S02", title: "Slice" },
83
+ activeTask: { id: "T03", title: "Task" },
84
+ phase: "executing",
85
+ recentDecisions: [],
86
+ blockers: [],
87
+ nextAction: "Keep going",
88
+ registry: [],
89
+ progress: {
90
+ milestones: { done: 0, total: 1 },
91
+ slices: { done: 1, total: 3 },
92
+ tasks: { done: 2, total: 5 },
93
+ },
94
+ };
95
+
96
+ assert.equal(buildCmuxStatusLabel(state), "M001 S02/T03 · executing");
97
+ assert.deepEqual(buildCmuxProgress(state), { value: 0.4, label: "2/5 tasks" });
98
+ });