@sayknow-cli/coding-agent 0.3.1 → 0.3.4

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 (216) hide show
  1. package/bin/skc.js +4 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli/plugin-cli.d.ts +2 -0
  4. package/dist/types/cli.d.ts +6 -0
  5. package/dist/types/commands/mcp.d.ts +70 -0
  6. package/dist/types/commands/plugin.d.ts +6 -0
  7. package/dist/types/commands/session.d.ts +6 -0
  8. package/dist/types/config/keybindings.d.ts +2 -2
  9. package/dist/types/config/model-profile-activation.d.ts +8 -1
  10. package/dist/types/config/model-profiles.d.ts +2 -2
  11. package/dist/types/config/model-registry.d.ts +3 -3
  12. package/dist/types/config/models-config-schema.d.ts +0 -5
  13. package/dist/types/config/settings-schema.d.ts +79 -68
  14. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  15. package/dist/types/export/html/template.generated.d.ts +1 -1
  16. package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
  17. package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
  18. package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
  19. package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
  20. package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
  21. package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
  22. package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
  23. package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
  24. package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
  25. package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
  26. package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
  27. package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
  28. package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
  29. package/dist/types/i18n/messages/en.d.ts +0 -1
  30. package/dist/types/main.d.ts +2 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  32. package/dist/types/modes/components/model-selector.d.ts +8 -0
  33. package/dist/types/modes/components/settings-defs.d.ts +0 -1
  34. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  35. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  36. package/dist/types/modes/theme/theme.d.ts +1 -1
  37. package/dist/types/notifications/html-format.d.ts +11 -0
  38. package/dist/types/notifications/index.d.ts +149 -1
  39. package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
  40. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  41. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  42. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  43. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  44. package/dist/types/notifications/recent-activity.d.ts +35 -0
  45. package/dist/types/notifications/telegram-daemon.d.ts +114 -16
  46. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  47. package/dist/types/notifications/topic-registry.d.ts +12 -9
  48. package/dist/types/runtime-mcp/types.d.ts +7 -0
  49. package/dist/types/sdk.d.ts +2 -0
  50. package/dist/types/session/agent-session.d.ts +14 -4
  51. package/dist/types/session/blob-store.d.ts +25 -0
  52. package/dist/types/session/session-manager.d.ts +57 -0
  53. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  54. package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
  55. package/dist/types/skc-runtime/state-renderer.d.ts +5 -0
  56. package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
  57. package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
  58. package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
  59. package/dist/types/skc-runtime/ultragoal-guard.d.ts +37 -1
  60. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +80 -0
  61. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +9 -1
  64. package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
  65. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  66. package/dist/types/tools/computer-gc.d.ts +23 -0
  67. package/dist/types/tools/cron.d.ts +36 -61
  68. package/dist/types/tools/index.d.ts +3 -2
  69. package/dist/types/tools/resource-gc.d.ts +54 -0
  70. package/dist/types/utils/changelog.d.ts +1 -0
  71. package/dist/types/web/insane/url-guard.d.ts +6 -3
  72. package/dist/types/web/scrapers/types.d.ts +5 -0
  73. package/dist/types/web/scrapers/utils.d.ts +7 -1
  74. package/dist/types/web/search/index.d.ts +1 -0
  75. package/dist/types/web/search/providers/utils.d.ts +11 -4
  76. package/package.json +11 -9
  77. package/scripts/g004-tmux-smoke.ts +100 -0
  78. package/scripts/g005-daemon-smoke.ts +180 -0
  79. package/scripts/g011-daemon-path-smoke.ts +153 -0
  80. package/src/cli/args.ts +0 -1
  81. package/src/cli/fast-help.ts +29 -2
  82. package/src/cli/mcp-cli.ts +272 -0
  83. package/src/cli/plugin-cli.ts +66 -3
  84. package/src/cli/web-search-cli.ts +5 -0
  85. package/src/cli.ts +30 -12
  86. package/src/commands/mcp.ts +117 -0
  87. package/src/commands/plugin.ts +4 -0
  88. package/src/commands/session.ts +18 -0
  89. package/src/config/keybindings.ts +2 -2
  90. package/src/config/model-profile-activation.ts +62 -8
  91. package/src/config/model-profiles.ts +3 -4
  92. package/src/config/model-registry.ts +3 -6
  93. package/src/config/models-config-schema.ts +1 -1
  94. package/src/config/settings-schema.ts +88 -103
  95. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  96. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
  97. package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
  98. package/src/defaults/skc/skills/team/SKILL.md +5 -3
  99. package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
  100. package/src/export/html/index.ts +2 -2
  101. package/src/export/html/template.generated.ts +1 -1
  102. package/src/export/html/template.js +0 -12
  103. package/src/extensibility/extensions/runner.ts +1 -0
  104. package/src/extensibility/skc-plugins/compiler.ts +351 -0
  105. package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
  106. package/src/extensibility/skc-plugins/index.ts +9 -0
  107. package/src/extensibility/skc-plugins/injection.ts +109 -0
  108. package/src/extensibility/skc-plugins/installer.ts +434 -0
  109. package/src/extensibility/skc-plugins/loader.ts +3 -1
  110. package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
  111. package/src/extensibility/skc-plugins/observability.ts +84 -0
  112. package/src/extensibility/skc-plugins/paths.ts +1 -1
  113. package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
  114. package/src/extensibility/skc-plugins/registry.ts +180 -0
  115. package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
  116. package/src/extensibility/skc-plugins/schema.ts +250 -20
  117. package/src/extensibility/skc-plugins/session-validation.ts +147 -0
  118. package/src/extensibility/skc-plugins/types.ts +199 -3
  119. package/src/extensibility/skc-plugins/validation.ts +80 -0
  120. package/src/extensibility/skills.ts +15 -0
  121. package/src/goals/tools/goal-tool.ts +14 -1
  122. package/src/hooks/skill-state.ts +57 -0
  123. package/src/i18n/messages/de.ts +0 -1
  124. package/src/i18n/messages/en.ts +0 -1
  125. package/src/i18n/messages/es.ts +0 -1
  126. package/src/i18n/messages/fr.ts +0 -1
  127. package/src/i18n/messages/ja.ts +0 -1
  128. package/src/i18n/messages/ko.ts +0 -1
  129. package/src/i18n/messages/zh.ts +0 -1
  130. package/src/internal-urls/docs-index.generated.ts +14 -11
  131. package/src/main.ts +14 -3
  132. package/src/modes/bridge/bridge-mode.ts +11 -0
  133. package/src/modes/components/assistant-message.ts +49 -1
  134. package/src/modes/components/custom-editor.ts +2 -0
  135. package/src/modes/components/footer.ts +2 -3
  136. package/src/modes/components/hook-editor.ts +1 -1
  137. package/src/modes/components/hook-selector.ts +67 -43
  138. package/src/modes/components/model-selector.ts +65 -12
  139. package/src/modes/components/settings-defs.ts +1 -2
  140. package/src/modes/components/settings-selector.ts +2 -7
  141. package/src/modes/components/status-line/git-utils.ts +25 -0
  142. package/src/modes/components/status-line.ts +10 -11
  143. package/src/modes/components/welcome.ts +2 -3
  144. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  145. package/src/modes/controllers/selector-controller.ts +59 -11
  146. package/src/modes/interactive-mode.ts +4 -1
  147. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  148. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  149. package/src/modes/theme/defaults/index.ts +2 -0
  150. package/src/modes/theme/theme.ts +0 -4
  151. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  152. package/src/notifications/html-format.ts +38 -0
  153. package/src/notifications/index.ts +242 -12
  154. package/src/notifications/lifecycle-commands.ts +238 -0
  155. package/src/notifications/lifecycle-control-runtime.ts +405 -0
  156. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  157. package/src/notifications/operator-runtime.ts +171 -0
  158. package/src/notifications/rate-limit-pool.ts +19 -0
  159. package/src/notifications/recent-activity.ts +132 -0
  160. package/src/notifications/telegram-daemon.ts +778 -257
  161. package/src/notifications/telegram-reference.ts +25 -7
  162. package/src/notifications/topic-registry.ts +23 -9
  163. package/src/prompts/agents/executor.md +2 -2
  164. package/src/prompts/system/system-prompt.md +2 -2
  165. package/src/prompts/tools/cron.md +5 -3
  166. package/src/prompts/tools/read.md +1 -1
  167. package/src/runtime-mcp/transports/stdio.ts +38 -4
  168. package/src/runtime-mcp/types.ts +7 -0
  169. package/src/sdk.ts +162 -10
  170. package/src/session/agent-session.ts +210 -74
  171. package/src/session/blob-store.ts +196 -8
  172. package/src/session/session-manager.ts +762 -12
  173. package/src/skc-runtime/launch-tmux.ts +68 -19
  174. package/src/skc-runtime/psmux-detect.ts +239 -0
  175. package/src/skc-runtime/state-renderer.ts +13 -0
  176. package/src/skc-runtime/team-runtime.ts +56 -23
  177. package/src/skc-runtime/tmux-common.ts +88 -4
  178. package/src/skc-runtime/tmux-sessions.ts +111 -9
  179. package/src/skc-runtime/ultragoal-guard.ts +192 -8
  180. package/src/skc-runtime/ultragoal-runtime.ts +296 -16
  181. package/src/slash-commands/builtin-registry.ts +23 -3
  182. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  183. package/src/slash-commands/helpers/parse.ts +2 -1
  184. package/src/system-prompt.ts +9 -0
  185. package/src/task/executor.ts +31 -7
  186. package/src/task/index.ts +2 -0
  187. package/src/tools/ask.ts +5 -1
  188. package/src/tools/bash.ts +9 -0
  189. package/src/tools/browser/tab-supervisor.ts +86 -2
  190. package/src/tools/composer-bash-policy.ts +96 -0
  191. package/src/tools/computer-gc.ts +66 -0
  192. package/src/tools/computer.ts +2 -0
  193. package/src/tools/cron.ts +75 -112
  194. package/src/tools/fetch.ts +18 -2
  195. package/src/tools/index.ts +5 -9
  196. package/src/tools/read.ts +25 -55
  197. package/src/tools/renderers.ts +0 -2
  198. package/src/tools/resource-gc.ts +291 -0
  199. package/src/tools/ultragoal-ask-guard.ts +7 -1
  200. package/src/utils/changelog.ts +8 -0
  201. package/src/web/insane/url-guard.ts +18 -14
  202. package/src/web/scrapers/types.ts +143 -45
  203. package/src/web/scrapers/utils.ts +70 -19
  204. package/src/web/search/index.ts +1 -0
  205. package/src/web/search/providers/utils.ts +22 -5
  206. package/vendor/insane-search/MANIFEST.json +3 -1
  207. package/vendor/insane-search/engine/__init__.py +14 -0
  208. package/vendor/insane-search/engine/content_safety.py +151 -0
  209. package/vendor/insane-search/engine/fetch_chain.py +32 -0
  210. package/vendor/insane-search/engine/tests/test_u8.py +216 -0
  211. package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
  212. package/dist/types/tools/inspect-image.d.ts +0 -31
  213. package/src/prompts/tools/inspect-image-system.md +0 -20
  214. package/src/prompts/tools/inspect-image.md +0 -32
  215. package/src/tools/inspect-image-renderer.ts +0 -103
  216. package/src/tools/inspect-image.ts +0 -172
@@ -1,7 +1,7 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import * as path from "node:path";
3
- import { safeStderrWrite } from "@sayknow-cli/utils";
4
3
  import { VERSION } from "@sayknow-cli/utils/dirs";
4
+ import { safeStderrWrite } from "@sayknow-cli/utils/safe-stderr";
5
5
  import type { Args } from "../cli/args";
6
6
  import { tmuxRuntimeSessionPath } from "./session-layout";
7
7
  import { SKC_COORDINATOR_SESSION_ID_ENV, SKC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
@@ -9,6 +9,7 @@ import {
9
9
  buildSkcTmuxProfileCommands,
10
10
  buildSkcTmuxSessionName,
11
11
  buildSkcTmuxSessionSlug,
12
+ resolveSkcTmuxBinary,
12
13
  resolveSkcTmuxCommand,
13
14
  SKC_DEFAULT_TMUX_SESSION,
14
15
  SKC_TMUX_COMMAND_ENV,
@@ -31,6 +32,7 @@ export {
31
32
  export const SKC_TMUX_LAUNCHED_ENV = "SKC_TMUX_LAUNCHED";
32
33
  export const SKC_LAUNCH_POLICY_ENV = "SKC_LAUNCH_POLICY";
33
34
  export const SKC_TMUX_WINDOW_LABEL_MAX_WIDTH = 48;
35
+ export const SKC_PSMUX_PROFILE_FORCE_ENV = "SKC_PSMUX_PROFILE_FORCE";
34
36
 
35
37
  type LaunchPolicy = "direct" | "tmux";
36
38
 
@@ -166,6 +168,18 @@ function formatTmuxLaunchDiagnostic(stage: string, stderr?: string): string {
166
168
  return `skc --tmux failed after creating tmux session: ${stage}.${suffix}\n`;
167
169
  }
168
170
 
171
+ function formatTmuxUnavailableDiagnostic(platform: NodeJS.Platform, tmuxCommand: string): string {
172
+ if (platform === "win32") {
173
+ return (
174
+ `skc --tmux requested but no tmux executable was found; starting without a tmux-backed session. ` +
175
+ `SKC searched for psmux, pmux, and tmux on PATH (got \`${tmuxCommand}\`). ` +
176
+ "Install psmux (https://github.com/psmux/psmux) for native Windows tmux support, or use WSL with real tmux. " +
177
+ "You can also point SKC at a specific binary via SKC_TMUX_COMMAND.\n"
178
+ );
179
+ }
180
+ return `skc --tmux requested but no ${tmuxCommand} executable was found; starting without a tmux-backed session.\n`;
181
+ }
182
+
169
183
  function shellQuote(value: string): string {
170
184
  if (value.length === 0) return "''";
171
185
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -199,14 +213,26 @@ function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, r
199
213
  export function applySkcTmuxProfile(context: SkcTmuxProfileContext): SkcTmuxProfileResult {
200
214
  const env = context.env ?? process.env;
201
215
  const branchSlug = context.branch ? buildSkcTmuxSessionSlug(context.branch) : (context.branchSlug ?? null);
202
- const commands = buildSkcTmuxProfileCommands(context.target, env, {
203
- branch: context.branch ?? null,
204
- branchSlug,
205
- project: context.project ?? null,
206
- sessionId: context.sessionId ?? env[SKC_COORDINATOR_SESSION_ID_ENV] ?? null,
207
- sessionStateFile: context.sessionStateFile ?? env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV] ?? null,
208
- version: context.version ?? null,
209
- });
216
+ // The psmux UX filter (mouse / set-clipboard / mode-style /
217
+ // set-window-option) now lives in buildSkcTmuxProfileCommands so every
218
+ // caller — skc --tmux planning, skc session create, skc team bootstrap —
219
+ // applies the same drop set when the active multiplexer is psmux. We pass
220
+ // the resolved tmuxCommand through the new opts seam so the filter
221
+ // engages for this exact command, not whatever the resolver returns at
222
+ // profile-build time.
223
+ const commands = buildSkcTmuxProfileCommands(
224
+ context.target,
225
+ env,
226
+ {
227
+ branch: context.branch ?? null,
228
+ branchSlug,
229
+ project: context.project ?? null,
230
+ sessionId: context.sessionId ?? env[SKC_COORDINATOR_SESSION_ID_ENV] ?? null,
231
+ sessionStateFile: context.sessionStateFile ?? env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV] ?? null,
232
+ version: context.version ?? null,
233
+ },
234
+ { tmuxCommand: context.tmuxCommand },
235
+ );
210
236
  if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
211
237
  const spawnSync = context.spawnSync ?? defaultSpawnSync;
212
238
  const cwd = context.cwd ?? process.cwd();
@@ -280,20 +306,33 @@ function truncateVisibleTail(value: string, maxWidth: number): string {
280
306
  return `…${result}`;
281
307
  }
282
308
 
309
+ const SKC_TMUX_WINDOW_BRANCH_SEPARATOR = "-";
310
+
311
+ function sanitizeTmuxWindowTitleSegment(value: string): string {
312
+ return value.replace(/:+/g, "-");
313
+ }
314
+
315
+ function sanitizeTmuxWindowProjectName(project: string): string {
316
+ const trimmed = project.trim();
317
+ if (!trimmed || /^\.+$/.test(trimmed)) return "skc";
318
+ if (trimmed.startsWith(".")) return sanitizeTmuxWindowTitleSegment(`dot-${trimmed.replace(/^\.+/, "")}`);
319
+ return sanitizeTmuxWindowTitleSegment(trimmed);
320
+ }
321
+
283
322
  export function buildSkcTmuxWindowTitle(cwd: string, branch: string | null | undefined): string {
284
- const project = path.basename(path.resolve(cwd)) || "skc";
285
- const trimmedBranch = branch?.trim();
323
+ const project = sanitizeTmuxWindowProjectName(path.basename(path.resolve(cwd)) || "skc");
324
+ const trimmedBranch = sanitizeTmuxWindowTitleSegment(branch?.trim() ?? "");
286
325
  if (!trimmedBranch) return truncateVisible(project, SKC_TMUX_WINDOW_LABEL_MAX_WIDTH);
287
326
 
288
- const separatorWidth = visibleWidth(":");
327
+ const separatorWidth = visibleWidth(SKC_TMUX_WINDOW_BRANCH_SEPARATOR);
289
328
  const projectWidth = visibleWidth(project);
290
- const fullTitle = `${project}:${trimmedBranch}`;
329
+ const fullTitle = `${project}${SKC_TMUX_WINDOW_BRANCH_SEPARATOR}${trimmedBranch}`;
291
330
  if (visibleWidth(fullTitle) <= SKC_TMUX_WINDOW_LABEL_MAX_WIDTH) return fullTitle;
292
331
 
293
332
  const remainingBranchWidth = SKC_TMUX_WINDOW_LABEL_MAX_WIDTH - projectWidth - separatorWidth;
294
333
  if (remainingBranchWidth <= 0) return truncateVisible(project, SKC_TMUX_WINDOW_LABEL_MAX_WIDTH);
295
334
 
296
- return `${project}:${truncateVisibleTail(trimmedBranch, remainingBranchWidth)}`;
335
+ return `${project}${SKC_TMUX_WINDOW_BRANCH_SEPARATOR}${truncateVisibleTail(trimmedBranch, remainingBranchWidth)}`;
297
336
  }
298
337
 
299
338
  function buildTmuxRenameWindowArgs(title: string, target?: string): string[] {
@@ -315,8 +354,10 @@ function renameExistingTmuxWindowIfNeeded(context: TmuxLaunchContext): void {
315
354
  if (!env.TMUX || env[SKC_TMUX_LAUNCHED_ENV] === "1") return;
316
355
  if (parseLaunchPolicy(env) === "direct") return;
317
356
 
318
- const platform = context.platform ?? process.platform;
319
- if (platform === "win32") return;
357
+ // Note: Windows is intentionally allowed here. Psmux supports
358
+ // `rename-window` and we want the leader window to inherit the
359
+ // sanitized project-branch title even on native Windows, where
360
+ // skc --tmux runs through PowerShell to a psmux backend.
320
361
 
321
362
  const tty = context.tty ?? { stdin: Boolean(process.stdin.isTTY), stdout: Boolean(process.stdout.isTTY) };
322
363
  if (!isInteractiveRootLaunch(context.parsed, tty)) return;
@@ -375,7 +416,12 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
375
416
  const branch = context.worktreeBranch ?? context.currentBranch ?? readCurrentBranch(cwd);
376
417
  const project = context.project ?? cwd;
377
418
  const sessionName = buildSkcTmuxSessionName(env, { branch });
378
- const tmuxCommand = resolveSkcTmuxCommand(env);
419
+ // Pick the most appropriate tmux binary for this platform. On native Windows
420
+ // the resolver walks psmux / pmux / tmux and uses the first one present on
421
+ // PATH, so the default `skc --tmux` flow lands on a real multiplexer even
422
+ // without an explicit SKC_TMUX_COMMAND override.
423
+ const resolvedBinary = resolveSkcTmuxBinary({ platform, env });
424
+ const tmuxCommand = resolvedBinary.command;
379
425
  const sessionId = env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
380
426
  // The session ROOT is keyed by the active SKC session (SKC_SESSION_ID), NOT the
381
427
  // coordinator/tmux identity. Fall back to the coordinator id only for standalone
@@ -385,7 +431,10 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
385
431
  env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
386
432
  tmuxRuntimeSessionPath(cwd, skcSessionId, buildSkcTmuxSessionSlug(sessionName));
387
433
  const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
388
- if (!tmuxAvailable) return undefined;
434
+ if (!tmuxAvailable) {
435
+ (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxUnavailableDiagnostic(platform, tmuxCommand));
436
+ return undefined;
437
+ }
389
438
  const existingSessionName = allowsExistingTmuxAttach(context.parsed, env)
390
439
  ? "existingBranchSessionName" in context
391
440
  ? (context.existingBranchSessionName ?? undefined)
@@ -451,7 +500,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
451
500
 
452
501
  if (plan.attachSessionName) {
453
502
  const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.attachSessionName}`], options);
454
- return attached.exitCode === 0;
503
+ if (attached.exitCode === 0) return true;
455
504
  }
456
505
 
457
506
  const created = spawnSync(plan.tmuxCommand, plan.newSessionArgs, options);
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Windows psmux detection and tmux-binary resolution.
3
+ *
4
+ * Recent psmux releases (see docs/compatibility.md in the psmux repo) close
5
+ * the round-trip gap for set-option / show-options user options and the
6
+ * set-window-option profile values skc emits, which is what unblocks the
7
+ * native Windows skc --tmux path. This module detects that capability so skc
8
+ * can pick psmux when tmux is missing on Windows, and so callers can decide
9
+ * whether to treat a given tmux binary as psmux (affecting e.g. the untagged
10
+ * diagnostic wording and namespace handling).
11
+ *
12
+ * The probe is intentionally lightweight: it runs a single tmux -V (or
13
+ * --version) once per process and caches the verdict. Cache invalidation
14
+ * knobs:
15
+ * - force: true re-probes on every call (used by tests).
16
+ * - SKC_PSMUX_FORCE_DETECT=1 re-probes each call.
17
+ * - SKC_PSMUX_DETECTION=off skips probing entirely.
18
+ */
19
+
20
+ export const SKC_PSMUX_COMMAND_ENV = "SKC_PSMUX_COMMAND";
21
+ export const SKC_PSMUX_DETECTION_ENV = "SKC_PSMUX_DETECTION";
22
+ export const SKC_PSMUX_FORCE_DETECT_ENV = "SKC_PSMUX_FORCE_DETECT";
23
+
24
+ /** Names that psmux installs as the canonical executable / alias. */
25
+ export const PSMUX_BINARY_NAMES = ["psmux", "pmux", "tmux"] as const;
26
+
27
+ /** Substrings that uniquely identify psmux in version/help output. */
28
+ const PSMUX_VERSION_MARKERS = ["psmux", "pmux"] as const;
29
+
30
+ export type PsmuxSpawnRunner = (
31
+ command: string,
32
+ args: string[],
33
+ ) => { exitCode: number | null; stdout?: string; stderr?: string };
34
+
35
+ /**
36
+ * Resolves a tmux-class binary name (e.g. "psmux", "tmux") to an absolute
37
+ * filesystem path or returns null when the binary cannot be located. The
38
+ * default implementation uses `Bun.which`; production callers leave it
39
+ * alone and unit tests inject a stub via `__setBinaryResolverForTests`
40
+ * so the version-banner probe can be exercised hermetically.
41
+ */
42
+ export type BinaryResolver = (candidate: string) => string | null;
43
+
44
+ const DEFAULT_BINARY_RESOLVER: BinaryResolver = candidate => {
45
+ if (!candidate) return null;
46
+ const stripped = candidate.trim().replace(/^["']|["']$/g, "");
47
+ if (!stripped) return null;
48
+ if (Bun.which(stripped)) return stripped;
49
+ return null;
50
+ };
51
+
52
+ let activeBinaryResolver: BinaryResolver = DEFAULT_BINARY_RESOLVER;
53
+
54
+ /** @internal Test-only seam; production code never calls this. */
55
+ export function __setBinaryResolverForTests(resolver: BinaryResolver | null): void {
56
+ activeBinaryResolver = resolver ?? DEFAULT_BINARY_RESOLVER;
57
+ }
58
+
59
+ interface CacheEntry {
60
+ command: string;
61
+ isPsmux: boolean;
62
+ }
63
+
64
+ const detectionCache = new Map<string, CacheEntry>();
65
+
66
+ export function envDisabled(value: string | undefined): boolean {
67
+ const normalized = value?.trim().toLowerCase();
68
+ return normalized === "0" || normalized === "false" || normalized === "off" || normalized === "no";
69
+ }
70
+
71
+ /**
72
+ * SKC_PSMUX_FORCE_DETECT opt-in re-probe switch. Any non-empty value other
73
+ * than a "disabled" sentinel forces a fresh probe on every call. The unset
74
+ * (undefined) case must NOT force probing, otherwise the in-process cache
75
+ * never engages.
76
+ */
77
+ function envForcesProbe(value: string | undefined): boolean {
78
+ if (value === undefined) return false;
79
+ if (envDisabled(value)) return false;
80
+ return value.trim().length > 0;
81
+ }
82
+
83
+ function readSpawnRunner(): PsmuxSpawnRunner {
84
+ return (command, args) => {
85
+ try {
86
+ const result = Bun.spawnSync({
87
+ cmd: [command, ...args],
88
+ stdout: "pipe",
89
+ stderr: "pipe",
90
+ env: process.env,
91
+ });
92
+ return {
93
+ exitCode: result.exitCode,
94
+ stdout: result.stdout.toString(),
95
+ stderr: result.stderr.toString(),
96
+ };
97
+ } catch {
98
+ return { exitCode: -1, stdout: "", stderr: "" };
99
+ }
100
+ };
101
+ }
102
+
103
+ function probeVersionOutput(command: string, runner: PsmuxSpawnRunner): string {
104
+ const flags = ["-V", "--version"];
105
+ for (const flag of flags) {
106
+ const result = runner(command, [flag]);
107
+ const text = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.toLowerCase();
108
+ if (result.exitCode === 0 && text.trim().length > 0) return text;
109
+ }
110
+ return "";
111
+ }
112
+
113
+ function outputMentionsPsmux(output: string): boolean {
114
+ if (!output) return false;
115
+ return PSMUX_VERSION_MARKERS.some(marker => output.includes(marker));
116
+ }
117
+
118
+ function resolveBinaryPath(candidate: string): string | null {
119
+ return activeBinaryResolver(candidate);
120
+ }
121
+
122
+ function detectPsmuxForCommand(command: string, runner: PsmuxSpawnRunner): boolean {
123
+ const resolved = resolveBinaryPath(command);
124
+ if (!resolved) return false;
125
+ const output = probeVersionOutput(resolved, runner);
126
+ return outputMentionsPsmux(output);
127
+ }
128
+
129
+ /**
130
+ * Decide whether command resolves to a psmux binary by probing its version
131
+ * output. The result is cached per process unless force is set or
132
+ * SKC_PSMUX_FORCE_DETECT=1.
133
+ */
134
+ export function detectPsmux(
135
+ command: string,
136
+ options: { force?: boolean; env?: NodeJS.ProcessEnv; runner?: PsmuxSpawnRunner } = {},
137
+ ): boolean {
138
+ const env = options.env ?? process.env;
139
+ const explicit = env[SKC_PSMUX_COMMAND_ENV]?.trim();
140
+ if (explicit) {
141
+ // The override is authoritative on its own — we trust the user's
142
+ // SKC_PSMUX_COMMAND value when they name a psmux-class binary, even
143
+ // when the binary cannot be located on PATH in the current process.
144
+ // This keeps the override usable from CI runners and from test
145
+ // environments where Bun.which would otherwise return null.
146
+ const normalized = explicit.toLowerCase();
147
+ if (
148
+ normalized === "psmux" ||
149
+ normalized === "pmux" ||
150
+ normalized.endsWith("/psmux") ||
151
+ normalized.endsWith("/pmux") ||
152
+ normalized.endsWith("\\psmux") ||
153
+ normalized.endsWith("\\pmux")
154
+ ) {
155
+ return true;
156
+ }
157
+ const explicitPath = resolveBinaryPath(explicit);
158
+ if (explicitPath && explicitPath === resolveBinaryPath(command)) return true;
159
+ }
160
+ if (envDisabled(env[SKC_PSMUX_DETECTION_ENV])) return false;
161
+ const force = options.force === true || envForcesProbe(env[SKC_PSMUX_FORCE_DETECT_ENV]);
162
+ const useCache = !force && !options.force;
163
+ if (useCache) {
164
+ const cached = detectionCache.get(command);
165
+ if (cached) return cached.isPsmux;
166
+ }
167
+ const runner = options.runner ?? readSpawnRunner();
168
+ const isPsmux = detectPsmuxForCommand(command, runner);
169
+ if (useCache) detectionCache.set(command, { command, isPsmux });
170
+ return isPsmux;
171
+ }
172
+
173
+ export interface ResolveSkcTmuxBinaryOptions {
174
+ platform?: NodeJS.Platform;
175
+ env?: NodeJS.ProcessEnv;
176
+ runner?: PsmuxSpawnRunner;
177
+ }
178
+
179
+ export interface ResolvedTmuxBinary {
180
+ command: string;
181
+ isPsmux: boolean;
182
+ viaExplicitOverride: boolean;
183
+ }
184
+
185
+ /**
186
+ * Resolve the tmux command SKC should invoke. Honors the existing
187
+ * SKC_TMUX_COMMAND / SKC_TEAM_TMUX_COMMAND overrides; on Windows when no
188
+ * override is set, psmux (installed as psmux, pmux, or tmux) is picked
189
+ * automatically so the default skc --tmux flow lands on a real multiplexer.
190
+ */
191
+ export function resolveSkcTmuxBinary(options: ResolveSkcTmuxBinaryOptions = {}): ResolvedTmuxBinary {
192
+ const env = options.env ?? process.env;
193
+ const platform = options.platform ?? process.platform;
194
+ const runner = options.runner ?? readSpawnRunner();
195
+ const explicit = env.SKC_TMUX_COMMAND?.trim() || env.SKC_TEAM_TMUX_COMMAND?.trim();
196
+ if (explicit) {
197
+ const isPsmux = detectPsmux(explicit, { env, runner });
198
+ return { command: explicit, isPsmux, viaExplicitOverride: true };
199
+ }
200
+ if (platform === "win32") {
201
+ for (const candidate of PSMUX_BINARY_NAMES) {
202
+ if (resolveBinaryPath(candidate)) {
203
+ const isPsmux = detectPsmux(candidate, { env, runner });
204
+ return { command: candidate, isPsmux, viaExplicitOverride: false };
205
+ }
206
+ }
207
+ }
208
+ const tmuxPath = resolveBinaryPath("tmux");
209
+ if (tmuxPath) {
210
+ const isPsmux = detectPsmux("tmux", { env, runner });
211
+ return { command: "tmux", isPsmux, viaExplicitOverride: false };
212
+ }
213
+ return { command: "tmux", isPsmux: false, viaExplicitOverride: false };
214
+ }
215
+
216
+ /** Test-only helper: drop the in-process detection cache. */
217
+ export function clearPsmuxDetectionCache(): void {
218
+ detectionCache.clear();
219
+ }
220
+
221
+ export interface PsmuxProbe {
222
+ command: string;
223
+ versionOutput: string;
224
+ isPsmux: boolean;
225
+ }
226
+
227
+ export function probePsmux(
228
+ command: string,
229
+ options: { env?: NodeJS.ProcessEnv; runner?: PsmuxSpawnRunner; force?: boolean } = {},
230
+ ): PsmuxProbe {
231
+ const env = options.env ?? process.env;
232
+ const runner = options.runner ?? readSpawnRunner();
233
+ const resolved = resolveBinaryPath(command);
234
+ if (!resolved) return { command, versionOutput: "", isPsmux: false };
235
+ if (options.force) clearPsmuxDetectionCache();
236
+ const output = probeVersionOutput(resolved, runner);
237
+ const isPsmux = outputMentionsPsmux(output) || env[SKC_PSMUX_COMMAND_ENV]?.trim() === resolved;
238
+ return { command: resolved, versionOutput: output, isPsmux };
239
+ }
@@ -256,6 +256,11 @@ export function renderUltragoalStatusMarkdown(summary: {
256
256
  currentGoal?: { id: string; status: string; title?: string; objective?: string };
257
257
  counts: Record<string, number>;
258
258
  goals: unknown[];
259
+ nudgeBudget?: number;
260
+ nudgeCount?: number;
261
+ nudgeRemaining?: number;
262
+ nudgeGoalId?: string;
263
+ nudgeTargetKind?: string;
259
264
  }): string {
260
265
  if (!summary.exists)
261
266
  return `# ultragoal status\n\n- status: missing\n- No ultragoal plan found at ${summary.paths.goalsPath}. Run \`skc ultragoal create-goals --brief "..."\` first.\n`;
@@ -270,6 +275,14 @@ export function renderUltragoalStatusMarkdown(summary: {
270
275
  ];
271
276
  if (summary.skcObjective) lines.push(`- objective: ${summary.skcObjective}`);
272
277
  if (summary.currentGoal) lines.push(`- current: ${summary.currentGoal.id} (${summary.currentGoal.status})`);
278
+ if (summary.nudgeBudget !== undefined && summary.nudgeGoalId) {
279
+ const used = summary.nudgeCount ?? 0;
280
+ const remaining = summary.nudgeRemaining ?? Math.max(0, summary.nudgeBudget - used);
281
+ const target = summary.nudgeTargetKind ? ` target=${summary.nudgeTargetKind}` : "";
282
+ lines.push(
283
+ `- nudge: ${summary.nudgeGoalId} ${used}/${summary.nudgeBudget} used (${remaining} remaining)${target}`,
284
+ );
285
+ }
273
286
  lines.push(`- goals_path: ${summary.paths.goalsPath}`);
274
287
  if (summary.paths.ledgerPath) lines.push(`- ledger_path: ${summary.paths.ledgerPath}`);
275
288
  return `${lines.join("\n")}\n`;
@@ -5,7 +5,7 @@ import type { WorkflowHudSummary } from "../skill-state/active-state";
5
5
  import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
6
6
  import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
7
7
  import type { GcPidProbe, GcRecord } from "./gc-runtime";
8
- import { applySkcTmuxProfile, SKC_TMUX_LAUNCHED_ENV } from "./launch-tmux";
8
+ import { applySkcTmuxProfile } from "./launch-tmux";
9
9
  import { modeStatePath, sessionIdFromDirName, sessionReportsDir, teamStateRoot } from "./session-layout";
10
10
  import { resolveSkcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
11
11
  import {
@@ -595,6 +595,16 @@ function teamDir(stateRoot: string, teamName: string): string {
595
595
  function shellQuote(value: string): string {
596
596
  return `'${value.replace(/'/g, "'\\''")}'`;
597
597
  }
598
+
599
+ /**
600
+ * PowerShell-safe single-quote escape: doubles single quotes inside a
601
+ * single-quoted PowerShell literal ('it''s ok') and uses the same
602
+ * surrounding quotes. Used to build worker command strings that psmux
603
+ * will hand to a Windows ConPTY pane running PowerShell.
604
+ */
605
+ function powershellQuote(value: string): string {
606
+ return `'${value.replace(/'/g, "''")}'`;
607
+ }
598
608
  function safePathSegment(kind: string, value: string): string {
599
609
  assertSafeId(kind, value);
600
610
  return value;
@@ -1812,7 +1822,7 @@ async function ensureWorkerWorktree(
1812
1822
 
1813
1823
  function buildTeamTmuxLeaderRequirementMessage(detail?: string): string {
1814
1824
  const suffix = detail?.trim() ? `:${detail.trim()}` : "";
1815
- return `skc_team_requires_tmux_leader: run \`skc --tmux\` first, then run \`skc team ...\` inside that tmux-backed leader session, or use \`skc team --dry-run\` for state-only smoke tests${suffix}`;
1825
+ return `skc_team_requires_tmux_leader: start a tmux session first (run \`skc --tmux\`, or launch tmux yourself), then run \`skc team ...\` inside it, or use \`skc team --dry-run\` for state-only smoke tests${suffix}`;
1816
1826
  }
1817
1827
  function readSkcTmuxProfileValue(tmuxCommand: string, sessionName: string): string {
1818
1828
  const result = Bun.spawnSync(
@@ -1826,7 +1836,7 @@ function readSkcTmuxProfileValue(tmuxCommand: string, sessionName: string): stri
1826
1836
  return result.stdout.toString().trim();
1827
1837
  }
1828
1838
 
1829
- function retagSkcLaunchedTmuxSession(tmuxCommand: string, sessionName: string): boolean {
1839
+ function tagTmuxSessionAsSkcLeader(tmuxCommand: string, sessionName: string): boolean {
1830
1840
  const result = Bun.spawnSync(
1831
1841
  [
1832
1842
  tmuxCommand,
@@ -1845,25 +1855,39 @@ function retagSkcLaunchedTmuxSession(tmuxCommand: string, sessionName: string):
1845
1855
  }
1846
1856
 
1847
1857
  function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEnv): SkcTmuxLeaderContext {
1858
+ if (Bun.which(tmuxCommand) === null)
1859
+ throw new Error(buildTeamTmuxLeaderRequirementMessage(`tmux_not_installed:${tmuxCommand}`));
1848
1860
  const paneTarget = env.TMUX_PANE?.trim();
1849
1861
  const args = paneTarget
1850
1862
  ? ["display-message", "-p", "-t", paneTarget, "#S:#I #{pane_id}"]
1851
1863
  : ["display-message", "-p", "#S:#I #{pane_id}"];
1852
1864
  const result = Bun.spawnSync([tmuxCommand, ...args], { stdout: "pipe", stderr: "pipe" });
1853
- if (result.exitCode !== 0) throw new Error(buildTeamTmuxLeaderRequirementMessage(result.stderr.toString()));
1865
+ if (result.exitCode !== 0) {
1866
+ // Distinguish "you are not inside any tmux session" from a genuine tmux
1867
+ // query failure so the caller gets actionable guidance instead of raw
1868
+ // tmux stderr. `skc team` needs a tmux leader; outside tmux there is none.
1869
+ const insideTmux = Boolean(env.TMUX?.trim() || env.TMUX_PANE?.trim());
1870
+ const stderr = result.stderr.toString().trim();
1871
+ throw new Error(
1872
+ buildTeamTmuxLeaderRequirementMessage(
1873
+ insideTmux ? `tmux_query_failed${stderr ? `:${stderr}` : ""}` : "not_inside_tmux",
1874
+ ),
1875
+ );
1876
+ }
1854
1877
  const [sessionAndWindow = "", leaderPaneId = ""] = result.stdout.toString().trim().split(/\s+/);
1855
1878
  const [sessionName = "", windowIndex = ""] = sessionAndWindow.split(":");
1856
1879
  if (!sessionName || !windowIndex || !leaderPaneId.startsWith("%"))
1857
1880
  throw new Error(buildTeamTmuxLeaderRequirementMessage(`invalid_tmux_context:${result.stdout.toString().trim()}`));
1858
1881
  if (readSkcTmuxProfileValue(tmuxCommand, sessionName) !== SKC_TMUX_PROFILE_VALUE) {
1859
- // Self-heal: a pane launched through `skc --tmux` exports
1860
- // SKC_TMUX_LAUNCHED=1, but the session can lose (or never receive) the
1861
- // @skc-profile user-option tag when startup attach fails mid-way or the
1862
- // registry write races. That stranded-but-genuinely-SKC leader pane
1863
- // previously hard-failed as unmanaged_tmux_session; re-tag it instead.
1864
- const launchedBySkc = env[SKC_TMUX_LAUNCHED_ENV] === "1";
1865
- const retagged = launchedBySkc && retagSkcLaunchedTmuxSession(tmuxCommand, sessionName);
1866
- if (!retagged || readSkcTmuxProfileValue(tmuxCommand, sessionName) !== SKC_TMUX_PROFILE_VALUE)
1882
+ // Adopt any real tmux leader as a SKC team leader — including a session
1883
+ // the user created outside `skc --tmux` — by writing SKC's @skc-profile
1884
+ // ownership tag and reading it back. A provider that round-trips tmux
1885
+ // user options (real tmux) keeps the tag and is adopted; one that does
1886
+ // not (e.g. psmux on Windows) drops it, so the readback still fails and
1887
+ // the leader is rejected as unmanaged. This also self-heals a genuine
1888
+ // `skc --tmux` pane that lost its @skc-profile tag mid-startup.
1889
+ const tagged = tagTmuxSessionAsSkcLeader(tmuxCommand, sessionName);
1890
+ if (!tagged || readSkcTmuxProfileValue(tmuxCommand, sessionName) !== SKC_TMUX_PROFILE_VALUE)
1867
1891
  throw new Error(
1868
1892
  buildTeamTmuxLeaderRequirementMessage(
1869
1893
  `unmanaged_tmux_session:${sessionName} — ${buildSkcTmuxUntaggedSessionHint(tmuxCommand)}`,
@@ -1881,7 +1905,15 @@ export function resolveSkcWorkerCommand(cwd = process.cwd(), env: NodeJS.Process
1881
1905
  if (entrypoint && path.basename(entrypoint).startsWith("skc")) return shellQuote(path.resolve(cwd, entrypoint));
1882
1906
  return "skc";
1883
1907
  }
1884
- function buildWorkerCommand(config: SkcTeamConfig, worker: SkcTeamWorker): string {
1908
+ /** @internal Exported for unit tests. */
1909
+ export function buildWorkerCommand(
1910
+ config: SkcTeamConfig,
1911
+ worker: SkcTeamWorker,
1912
+ platform: NodeJS.Platform = process.platform,
1913
+ ): string {
1914
+ const quote = platform === "win32" ? powershellQuote : shellQuote;
1915
+ const envAssignment = (key: string, value: string): string =>
1916
+ platform === "win32" ? `$env:${key} = ${quote(value)};` : `${key}=${quote(value)}`;
1885
1917
  const workspace = worker.worktree_path
1886
1918
  ? `Worker worktree: ${worker.worktree_path}.`
1887
1919
  : `Worker cwd: ${config.leader.cwd}.`;
@@ -1894,17 +1926,18 @@ function buildWorkerCommand(config: SkcTeamConfig, worker: SkcTeamWorker): strin
1894
1926
  `Before claiming work, send startup ACK: skc team api worker-startup-ack --input '{"team_name":"${config.team_name}","worker_id":"${worker.id}","protocol_version":"1"}' --json.`,
1895
1927
  `Use skc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; keep heartbeat current during long work, record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
1896
1928
  ].join("\n");
1897
- const env = [
1898
- `SKC_TEAM_WORKER=${shellQuote(`${config.team_name}/${worker.id}`)}`,
1899
- `SKC_TEAM_INTERNAL_WORKER=${shellQuote(`${config.team_name}/${worker.id}`)}`,
1900
- `SKC_TEAM_NAME=${shellQuote(config.team_name)}`,
1901
- `SKC_TEAM_WORKER_ID=${shellQuote(worker.id)}`,
1902
- `SKC_TEAM_STATE_ROOT=${shellQuote(config.state_root)}`,
1903
- `SKC_TEAM_LEADER_CWD=${shellQuote(config.leader.cwd)}`,
1904
- `SKC_TEAM_DISPLAY_NAME=${shellQuote(config.display_name)}`,
1905
- ...(worker.worktree_path ? [`SKC_TEAM_WORKTREE_PATH=${shellQuote(worker.worktree_path)}`] : []),
1929
+ const envLines = [
1930
+ envAssignment("SKC_TEAM_WORKER", `${config.team_name}/${worker.id}`),
1931
+ envAssignment("SKC_TEAM_INTERNAL_WORKER", `${config.team_name}/${worker.id}`),
1932
+ envAssignment("SKC_TEAM_NAME", config.team_name),
1933
+ envAssignment("SKC_TEAM_WORKER_ID", worker.id),
1934
+ envAssignment("SKC_TEAM_STATE_ROOT", config.state_root),
1935
+ envAssignment("SKC_TEAM_LEADER_CWD", config.leader.cwd),
1936
+ envAssignment("SKC_TEAM_DISPLAY_NAME", config.display_name),
1937
+ ...(worker.worktree_path ? [envAssignment("SKC_TEAM_WORKTREE_PATH", worker.worktree_path)] : []),
1906
1938
  ];
1907
- return `${env.join(" ")} ${config.worker_command} ${shellQuote(prompt)}`;
1939
+ const joined = platform === "win32" ? envLines.join(" ") : envLines.join(" ");
1940
+ return `${joined} ${config.worker_command} ${quote(prompt)}`;
1908
1941
  }
1909
1942
  interface SkcTeamInitialLane {
1910
1943
  label: string;