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
package/README.md CHANGED
@@ -455,7 +455,9 @@ auto_report: true
455
455
 
456
456
  ### Agent Instructions
457
457
 
458
- Create an `agent-instructions.md` file in your project root to inject persistent per-project behavioral guidance into every agent session. This file is loaded automatically and provides project-specific context the LLM should always have coding standards, architectural decisions, domain terminology, or workflow preferences.
458
+ Place an `AGENTS.md` file in any directory to provide persistent behavioral guidance for that scope. Pi core loads `AGENTS.md` automatically (with `CLAUDE.md` as a fallback) at both user and project levels. Use these files for coding standards, architectural decisions, domain terminology, or workflow preferences.
459
+
460
+ > **Note:** The legacy `agent-instructions.md` format (`~/.gsd/agent-instructions.md` and `.gsd/agent-instructions.md`) is deprecated and no longer loaded. Migrate any existing instructions to `AGENTS.md` or `CLAUDE.md`.
459
461
 
460
462
  ### Debug Mode
461
463
 
package/dist/cli.js CHANGED
@@ -327,7 +327,10 @@ if (isPrintMode) {
327
327
  markStartup('createAgentSession');
328
328
  if (extensionsResult.errors.length > 0) {
329
329
  for (const err of extensionsResult.errors) {
330
- process.stderr.write(`[gsd] Extension load error: ${err.error}\n`);
330
+ // Downgrade conflicts with built-in tools to warnings (#1347)
331
+ const isSuperseded = err.error.includes("supersedes");
332
+ const prefix = isSuperseded ? "Extension conflict" : "Extension load error";
333
+ process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`);
331
334
  }
332
335
  }
333
336
  // Apply --model override if specified
@@ -456,7 +459,9 @@ const { session, extensionsResult } = await createAgentSession({
456
459
  markStartup('createAgentSession');
457
460
  if (extensionsResult.errors.length > 0) {
458
461
  for (const err of extensionsResult.errors) {
459
- process.stderr.write(`[gsd] Extension load error: ${err.error}\n`);
462
+ const isSuperseded = err.error.includes("supersedes");
463
+ const prefix = isSuperseded ? "Extension conflict" : "Extension load error";
464
+ process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`);
460
465
  }
461
466
  }
462
467
  // Restore scoped models from settings on startup.
@@ -9,7 +9,7 @@ export declare function getNewerManagedResourceVersion(agentDir: string, current
9
9
  * - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
10
10
  * - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
11
11
  * - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
12
- * - GSD-WORKFLOW.md is read directly from bundled path via GSD_WORKFLOW_PATH env var
12
+ * - GSD-WORKFLOW.md ~/.gsd/agent/GSD-WORKFLOW.md (fallback for env var miss)
13
13
  *
14
14
  * Skips the copy when the managed-resources.json version matches the current
15
15
  * GSD version, avoiding ~128ms of synchronous cpSync on every startup.
@@ -18,6 +18,9 @@ import { loadRegistry, readManifestFromEntryPath, isExtensionEnabled, ensureRegi
18
18
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
19
19
  const distResources = join(packageRoot, 'dist', 'resources');
20
20
  const srcResources = join(packageRoot, 'src', 'resources');
21
+ // Use dist/resources only if it has the full expected structure.
22
+ // A partial build (tsc without copy-resources) creates dist/resources/extensions/
23
+ // but not agents/ or skills/, causing initResources to sync from an incomplete source.
21
24
  const resourcesDir = (existsSync(distResources) && existsSync(join(distResources, 'agents')))
22
25
  ? distResources
23
26
  : srcResources;
@@ -220,7 +223,7 @@ function copyDirRecursive(src, dest) {
220
223
  * - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
221
224
  * - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
222
225
  * - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
223
- * - GSD-WORKFLOW.md is read directly from bundled path via GSD_WORKFLOW_PATH env var
226
+ * - GSD-WORKFLOW.md ~/.gsd/agent/GSD-WORKFLOW.md (fallback for env var miss)
224
227
  *
225
228
  * Skips the copy when the managed-resources.json version matches the current
226
229
  * GSD version, avoiding ~128ms of synchronous cpSync on every startup.
@@ -247,6 +250,15 @@ export function initResources(agentDir) {
247
250
  syncResourceDir(bundledExtensionsDir, join(agentDir, 'extensions'));
248
251
  syncResourceDir(join(resourcesDir, 'agents'), join(agentDir, 'agents'));
249
252
  syncResourceDir(join(resourcesDir, 'skills'), join(agentDir, 'skills'));
253
+ // Sync GSD-WORKFLOW.md to agentDir as a fallback for when GSD_WORKFLOW_PATH
254
+ // env var is not set (e.g. fork/dev builds, alternative entry points).
255
+ const workflowSrc = join(resourcesDir, 'GSD-WORKFLOW.md');
256
+ if (existsSync(workflowSrc)) {
257
+ try {
258
+ copyFileSync(workflowSrc, join(agentDir, 'GSD-WORKFLOW.md'));
259
+ }
260
+ catch { /* non-fatal */ }
261
+ }
250
262
  // Ensure all newly copied files are owner-writable so the next run can
251
263
  // overwrite them (covers extensions, agents, and skills in one walk).
252
264
  makeTreeWritable(agentDir);
@@ -52,14 +52,12 @@ export function createAwaitTool(getManager) {
52
52
  const running = watched.filter((j) => j.status === "running");
53
53
  if (running.length === 0) {
54
54
  const result = formatResults(watched);
55
- manager.acknowledgeDeliveries(watched.map((j) => j.id));
56
55
  return { content: [{ type: "text", text: result }], details: undefined };
57
56
  }
58
57
  // Wait for at least one to complete
59
58
  await Promise.race(running.map((j) => j.promise));
60
59
  // Collect all completed results (more may have finished while waiting)
61
60
  const completed = watched.filter((j) => j.status !== "running");
62
- manager.acknowledgeDeliveries(completed.map((j) => j.id));
63
61
  const stillRunning = watched.filter((j) => j.status === "running");
64
62
  let result = formatResults(completed);
65
63
  if (stillRunning.length > 0) {
@@ -101,12 +101,6 @@ export class AsyncJobManager {
101
101
  getAllJobs() {
102
102
  return [...this.jobs.values()];
103
103
  }
104
- /**
105
- * No-op. Retained for API compatibility with await_job tool.
106
- */
107
- acknowledgeDeliveries(_jobIds) {
108
- // Delivery is fire-once; no retries to cancel.
109
- }
110
104
  /**
111
105
  * Cleanup all timers and resources.
112
106
  */
@@ -2,7 +2,7 @@
2
2
  * Output analysis, digest generation, highlights extraction, and output retrieval.
3
3
  */
4
4
  import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, } from "@gsd/pi-coding-agent";
5
- import { ERROR_PATTERN_UNION, WARNING_PATTERN_UNION, READINESS_PATTERN_UNION, BUILD_COMPLETE_PATTERN_UNION, TEST_RESULT_PATTERN_UNION, URL_PATTERN, PORT_PATTERN_SOURCE, LINE_DEDUP_MAX, } from "./types.js";
5
+ import { ERROR_PATTERN_UNION, WARNING_PATTERN_UNION, READINESS_PATTERN_UNION, BUILD_COMPLETE_PATTERN_UNION, TEST_RESULT_PATTERN_UNION, URL_PATTERN, PORT_PATTERN_SOURCE, } from "./types.js";
6
6
  import { addEvent, pushAlert } from "./process-manager.js";
7
7
  import { transitionToReady } from "./readiness-detector.js";
8
8
  import { formatUptime, formatTimeAgo } from "./utilities.js";
@@ -78,24 +78,6 @@ export function analyzeLine(bg, line, stream) {
78
78
  pushAlert(bg, "recovered — errors cleared");
79
79
  }
80
80
  }
81
- // Dedup tracking — evict oldest entry when map exceeds LINE_DEDUP_MAX (LRU via Map insertion order)
82
- bg.totalRawLines++;
83
- const lineHash = line.trim().slice(0, 100);
84
- const existing = bg.lineDedup.get(lineHash);
85
- if (existing !== undefined) {
86
- // Re-insert to update insertion order (move to tail = most recent)
87
- bg.lineDedup.delete(lineHash);
88
- bg.lineDedup.set(lineHash, existing + 1);
89
- }
90
- else {
91
- if (bg.lineDedup.size >= LINE_DEDUP_MAX) {
92
- // Evict oldest entry (Map iteration order = insertion order = LRU at head)
93
- const oldest = bg.lineDedup.keys().next().value;
94
- if (oldest !== undefined)
95
- bg.lineDedup.delete(oldest);
96
- }
97
- bg.lineDedup.set(lineHash, 1);
98
- }
99
81
  }
100
82
  // ── Digest Generation ──────────────────────────────────────────────────────
101
83
  export function generateDigest(bg, mutate = false) {
@@ -135,12 +135,8 @@ export function startProcess(opts) {
135
135
  group: opts.group || null,
136
136
  lastErrorCount: 0,
137
137
  lastWarningCount: 0,
138
- commandHistory: [],
139
- lineDedup: new Map(),
140
- totalRawLines: 0,
141
138
  stdoutLineCount: 0,
142
139
  stderrLineCount: 0,
143
- envKeys: Object.keys(opts.env || {}),
144
140
  restartCount: 0,
145
141
  startConfig: {
146
142
  command,
@@ -5,8 +5,6 @@
5
5
  export const MAX_BUFFER_LINES = 5000;
6
6
  export const MAX_EVENTS = 200;
7
7
  export const DEAD_PROCESS_TTL = 10 * 60 * 1000;
8
- /** Maximum unique entries in the per-process lineDedup Map before LRU eviction. */
9
- export const LINE_DEDUP_MAX = 500;
10
8
  export const PORT_PROBE_TIMEOUT = 500;
11
9
  export const READY_POLL_INTERVAL = 250;
12
10
  export const DEFAULT_READY_TIMEOUT = 30000;
@@ -0,0 +1,321 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { promisify } from "node:util";
4
+ const execFileAsync = promisify(execFile);
5
+ const DEFAULT_SOCKET_PATH = "/tmp/cmux.sock";
6
+ const STATUS_KEY = "gsd";
7
+ const lastSidebarSnapshots = new Map();
8
+ let cmuxPromptedThisSession = false;
9
+ let cachedCliAvailability = null;
10
+ export function detectCmuxEnvironment(env = process.env, socketExists = existsSync, cliAvailable = isCmuxCliAvailable) {
11
+ const socketPath = env.CMUX_SOCKET_PATH ?? DEFAULT_SOCKET_PATH;
12
+ const workspaceId = env.CMUX_WORKSPACE_ID?.trim() || undefined;
13
+ const surfaceId = env.CMUX_SURFACE_ID?.trim() || undefined;
14
+ const available = Boolean(workspaceId && surfaceId && socketExists(socketPath));
15
+ return {
16
+ available,
17
+ cliAvailable: cliAvailable(),
18
+ socketPath,
19
+ workspaceId,
20
+ surfaceId,
21
+ };
22
+ }
23
+ export function resolveCmuxConfig(preferences, env = process.env, socketExists = existsSync, cliAvailable = isCmuxCliAvailable) {
24
+ const detected = detectCmuxEnvironment(env, socketExists, cliAvailable);
25
+ const cmux = preferences?.cmux ?? {};
26
+ const enabled = detected.available && cmux.enabled === true;
27
+ return {
28
+ ...detected,
29
+ enabled,
30
+ notifications: enabled && cmux.notifications !== false,
31
+ sidebar: enabled && cmux.sidebar !== false,
32
+ splits: enabled && cmux.splits === true,
33
+ browser: enabled && cmux.browser === true,
34
+ };
35
+ }
36
+ export function shouldPromptToEnableCmux(preferences, env = process.env, socketExists = existsSync, cliAvailable = isCmuxCliAvailable) {
37
+ if (cmuxPromptedThisSession)
38
+ return false;
39
+ const detected = detectCmuxEnvironment(env, socketExists, cliAvailable);
40
+ if (!detected.available)
41
+ return false;
42
+ return preferences?.cmux?.enabled === undefined;
43
+ }
44
+ export function markCmuxPromptShown() {
45
+ cmuxPromptedThisSession = true;
46
+ }
47
+ export function resetCmuxPromptState() {
48
+ cmuxPromptedThisSession = false;
49
+ }
50
+ export function isCmuxCliAvailable() {
51
+ if (cachedCliAvailability !== null)
52
+ return cachedCliAvailability;
53
+ try {
54
+ execFileSync("cmux", ["--help"], { stdio: "ignore", timeout: 1000 });
55
+ cachedCliAvailability = true;
56
+ }
57
+ catch {
58
+ cachedCliAvailability = false;
59
+ }
60
+ return cachedCliAvailability;
61
+ }
62
+ export function supportsOsc777Notifications(env = process.env) {
63
+ const termProgram = env.TERM_PROGRAM?.toLowerCase() ?? "";
64
+ return termProgram === "ghostty" || termProgram === "wezterm" || termProgram === "iterm.app";
65
+ }
66
+ export function emitOsc777Notification(title, body) {
67
+ if (!supportsOsc777Notifications())
68
+ return;
69
+ const safeTitle = normalizeNotificationText(title).replace(/;/g, ",");
70
+ const safeBody = normalizeNotificationText(body).replace(/;/g, ",");
71
+ process.stdout.write(`\x1b]777;notify;${safeTitle};${safeBody}\x07`);
72
+ }
73
+ export function buildCmuxStatusLabel(state) {
74
+ const parts = [];
75
+ if (state.activeMilestone)
76
+ parts.push(state.activeMilestone.id);
77
+ if (state.activeSlice)
78
+ parts.push(state.activeSlice.id);
79
+ if (state.activeTask) {
80
+ const prev = parts.pop();
81
+ parts.push(prev ? `${prev}/${state.activeTask.id}` : state.activeTask.id);
82
+ }
83
+ if (parts.length === 0)
84
+ return state.phase;
85
+ return `${parts.join(" ")} · ${state.phase}`;
86
+ }
87
+ export function buildCmuxProgress(state) {
88
+ const progress = state.progress;
89
+ if (!progress)
90
+ return null;
91
+ const choose = (done, total, label) => {
92
+ if (total <= 0)
93
+ return null;
94
+ return { value: Math.max(0, Math.min(1, done / total)), label: `${done}/${total} ${label}` };
95
+ };
96
+ return choose(progress.tasks?.done ?? 0, progress.tasks?.total ?? 0, "tasks")
97
+ ?? choose(progress.slices?.done ?? 0, progress.slices?.total ?? 0, "slices")
98
+ ?? choose(progress.milestones.done, progress.milestones.total, "milestones");
99
+ }
100
+ function phaseVisuals(phase) {
101
+ switch (phase) {
102
+ case "blocked":
103
+ return { icon: "triangle-alert", color: "#ef4444" };
104
+ case "paused":
105
+ return { icon: "pause", color: "#f59e0b" };
106
+ case "complete":
107
+ case "completing-milestone":
108
+ return { icon: "check", color: "#22c55e" };
109
+ case "planning":
110
+ case "researching":
111
+ case "replanning-slice":
112
+ return { icon: "compass", color: "#3b82f6" };
113
+ case "validating-milestone":
114
+ case "verifying":
115
+ return { icon: "shield-check", color: "#06b6d4" };
116
+ default:
117
+ return { icon: "rocket", color: "#4ade80" };
118
+ }
119
+ }
120
+ function sidebarSnapshotKey(config) {
121
+ return config.workspaceId ?? "default";
122
+ }
123
+ export class CmuxClient {
124
+ config;
125
+ constructor(config) {
126
+ this.config = config;
127
+ }
128
+ static fromPreferences(preferences) {
129
+ return new CmuxClient(resolveCmuxConfig(preferences));
130
+ }
131
+ getConfig() {
132
+ return this.config;
133
+ }
134
+ canRun() {
135
+ return this.config.available && this.config.cliAvailable;
136
+ }
137
+ appendWorkspace(args) {
138
+ return this.config.workspaceId ? [...args, "--workspace", this.config.workspaceId] : args;
139
+ }
140
+ appendSurface(args, surfaceId) {
141
+ return surfaceId ? [...args, "--surface", surfaceId] : args;
142
+ }
143
+ runSync(args) {
144
+ if (!this.canRun())
145
+ return null;
146
+ try {
147
+ return execFileSync("cmux", args, {
148
+ encoding: "utf-8",
149
+ timeout: 3000,
150
+ env: process.env,
151
+ });
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ async runAsync(args) {
158
+ if (!this.canRun())
159
+ return null;
160
+ try {
161
+ const result = await execFileAsync("cmux", args, {
162
+ encoding: "utf-8",
163
+ timeout: 5000,
164
+ env: process.env,
165
+ });
166
+ return result.stdout;
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ getCapabilities() {
173
+ const stdout = this.runSync(["capabilities", "--json"]);
174
+ return stdout ? parseJson(stdout) : null;
175
+ }
176
+ identify() {
177
+ const stdout = this.runSync(["identify", "--json"]);
178
+ return stdout ? parseJson(stdout) : null;
179
+ }
180
+ setStatus(label, phase) {
181
+ if (!this.config.sidebar)
182
+ return;
183
+ const visuals = phaseVisuals(phase);
184
+ this.runSync(this.appendWorkspace([
185
+ "set-status",
186
+ STATUS_KEY,
187
+ label,
188
+ "--icon",
189
+ visuals.icon,
190
+ "--color",
191
+ visuals.color,
192
+ ]));
193
+ }
194
+ clearStatus() {
195
+ if (!this.config.sidebar)
196
+ return;
197
+ this.runSync(this.appendWorkspace(["clear-status", STATUS_KEY]));
198
+ }
199
+ setProgress(progress) {
200
+ if (!this.config.sidebar)
201
+ return;
202
+ if (!progress) {
203
+ this.runSync(this.appendWorkspace(["clear-progress"]));
204
+ return;
205
+ }
206
+ this.runSync(this.appendWorkspace([
207
+ "set-progress",
208
+ progress.value.toFixed(3),
209
+ "--label",
210
+ progress.label,
211
+ ]));
212
+ }
213
+ log(message, level = "info", source = "gsd") {
214
+ if (!this.config.sidebar)
215
+ return;
216
+ this.runSync(this.appendWorkspace([
217
+ "log",
218
+ "--level",
219
+ level,
220
+ "--source",
221
+ source,
222
+ "--",
223
+ message,
224
+ ]));
225
+ }
226
+ notify(title, body, subtitle) {
227
+ if (!this.config.notifications)
228
+ return false;
229
+ const args = ["notify", "--title", title, "--body", body];
230
+ if (subtitle)
231
+ args.push("--subtitle", subtitle);
232
+ return this.runSync(args) !== null;
233
+ }
234
+ async listSurfaceIds() {
235
+ const stdout = await this.runAsync(this.appendWorkspace(["list-surfaces", "--json", "--id-format", "both"]));
236
+ const parsed = stdout ? parseJson(stdout) : null;
237
+ return extractSurfaceIds(parsed);
238
+ }
239
+ async createSplit(direction) {
240
+ if (!this.config.splits)
241
+ return null;
242
+ const before = new Set(await this.listSurfaceIds());
243
+ const args = ["new-split", direction];
244
+ const scopedArgs = this.appendSurface(this.appendWorkspace(args), this.config.surfaceId);
245
+ await this.runAsync(scopedArgs);
246
+ const after = await this.listSurfaceIds();
247
+ for (const id of after) {
248
+ if (!before.has(id))
249
+ return id;
250
+ }
251
+ return null;
252
+ }
253
+ async sendSurface(surfaceId, text) {
254
+ const payload = text.endsWith("\n") ? text : `${text}\n`;
255
+ const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
256
+ return stdout !== null;
257
+ }
258
+ }
259
+ export function syncCmuxSidebar(preferences, state) {
260
+ const client = CmuxClient.fromPreferences(preferences);
261
+ const config = client.getConfig();
262
+ if (!config.sidebar)
263
+ return;
264
+ const label = buildCmuxStatusLabel(state);
265
+ const progress = buildCmuxProgress(state);
266
+ const snapshot = JSON.stringify({ label, progress, phase: state.phase });
267
+ const key = sidebarSnapshotKey(config);
268
+ if (lastSidebarSnapshots.get(key) === snapshot)
269
+ return;
270
+ client.setStatus(label, state.phase);
271
+ client.setProgress(progress);
272
+ lastSidebarSnapshots.set(key, snapshot);
273
+ }
274
+ export function clearCmuxSidebar(preferences) {
275
+ const config = resolveCmuxConfig(preferences);
276
+ if (!config.available || !config.cliAvailable)
277
+ return;
278
+ const client = new CmuxClient({ ...config, enabled: true, sidebar: true });
279
+ const key = sidebarSnapshotKey(config);
280
+ client.clearStatus();
281
+ client.setProgress(null);
282
+ lastSidebarSnapshots.delete(key);
283
+ }
284
+ export function logCmuxEvent(preferences, message, level = "info") {
285
+ CmuxClient.fromPreferences(preferences).log(message, level);
286
+ }
287
+ export function shellEscape(value) {
288
+ return `'${value.replace(/'/g, `'\\''`)}'`;
289
+ }
290
+ function normalizeNotificationText(value) {
291
+ return value.replace(/\r?\n/g, " ").trim();
292
+ }
293
+ function parseJson(text) {
294
+ try {
295
+ return JSON.parse(text);
296
+ }
297
+ catch {
298
+ return null;
299
+ }
300
+ }
301
+ function extractSurfaceIds(value) {
302
+ const found = new Set();
303
+ const visit = (node) => {
304
+ if (Array.isArray(node)) {
305
+ for (const item of node)
306
+ visit(item);
307
+ return;
308
+ }
309
+ if (!node || typeof node !== "object")
310
+ return;
311
+ for (const [key, child] of Object.entries(node)) {
312
+ if (typeof child === "string"
313
+ && (key === "surface_id" || key === "surface" || (key === "id" && child.includes("surface")))) {
314
+ found.add(child);
315
+ }
316
+ visit(child);
317
+ }
318
+ };
319
+ visit(value);
320
+ return Array.from(found);
321
+ }
@@ -327,6 +327,11 @@ export default function (pi) {
327
327
  return new Text(text, 0, 0);
328
328
  },
329
329
  });
330
+ // ── Session cleanup ─────────────────────────────────────────────────────
331
+ pi.on("session_shutdown", async () => {
332
+ searchCache.clear();
333
+ docCache.clear();
334
+ });
330
335
  // ── Startup notification ─────────────────────────────────────────────────
331
336
  pi.on("session_start", async (_event, ctx) => {
332
337
  if (!getApiKey()) {
@@ -8,9 +8,9 @@
8
8
  import { readFile, writeFile } from "node:fs/promises";
9
9
  import { existsSync, statSync } from "node:fs";
10
10
  import { resolve } from "node:path";
11
- import { CURSOR_MARKER, Editor, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
11
+ import { Editor, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
12
12
  import { Type } from "@sinclair/typebox";
13
- import { makeUI } from "./shared/mod.js";
13
+ import { makeUI, maskEditorLine } from "./shared/mod.js";
14
14
  import { parseSecretsManifest, formatSecretsManifest } from "./gsd/files.js";
15
15
  import { resolveMilestoneFile } from "./gsd/paths.js";
16
16
  // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -21,34 +21,6 @@ function maskPreview(value) {
21
21
  return "*".repeat(value.length);
22
22
  return `${value.slice(0, 4)}${"*".repeat(Math.max(4, value.length - 8))}${value.slice(-4)}`;
23
23
  }
24
- /**
25
- * Replace editor visible text with masked characters while preserving ANSI cursor/sequencer codes.
26
- */
27
- function maskEditorLine(line) {
28
- // Keep border / metadata lines readable.
29
- if (line.startsWith("─")) {
30
- return line;
31
- }
32
- let output = "";
33
- let i = 0;
34
- while (i < line.length) {
35
- if (line.startsWith(CURSOR_MARKER, i)) {
36
- output += CURSOR_MARKER;
37
- i += CURSOR_MARKER.length;
38
- continue;
39
- }
40
- const ansiMatch = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
41
- if (ansiMatch) {
42
- output += ansiMatch[0];
43
- i += ansiMatch[0].length;
44
- continue;
45
- }
46
- const ch = line[i];
47
- output += ch === " " ? " " : "*";
48
- i += 1;
49
- }
50
- return output;
51
- }
52
24
  function shellEscapeSingle(value) {
53
25
  return `'${value.replace(/'/g, `'\\''`)}'`;
54
26
  }
@@ -326,6 +326,11 @@ export default function (pi) {
326
326
  return new Text(text, 0, 0);
327
327
  },
328
328
  });
329
+ // ── Session cleanup ─────────────────────────────────────────────────────
330
+ pi.on("session_shutdown", async () => {
331
+ resultCache.clear();
332
+ client = null;
333
+ });
329
334
  // ── Startup notification ─────────────────────────────────────────────────
330
335
  pi.on("session_start", async (_event, ctx) => {
331
336
  if (process.env.GEMINI_API_KEY)