@sayknow-cli/coding-agent 0.3.5 → 0.3.7

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 (238) hide show
  1. package/README.md +76 -0
  2. package/dist/types/capability/mcp.d.ts +2 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/telegram-cli.d.ts +13 -0
  6. package/dist/types/cli/update-cli.d.ts +24 -1
  7. package/dist/types/commands/local-provider.d.ts +34 -0
  8. package/dist/types/commands/telegram.d.ts +22 -0
  9. package/dist/types/config/keybindings.d.ts +12 -6
  10. package/dist/types/config/model-registry.d.ts +5 -0
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +284 -4
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/config/telegram-autostart.d.ts +14 -0
  15. package/dist/types/config/telegram-env-bridge.d.ts +18 -0
  16. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  17. package/dist/types/defaults/skc-defaults.d.ts +7 -0
  18. package/dist/types/discovery/cursor.d.ts +4 -1
  19. package/dist/types/discovery/index.d.ts +0 -1
  20. package/dist/types/discovery/windsurf.d.ts +3 -1
  21. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  22. package/dist/types/i18n/messages/en.d.ts +1 -0
  23. package/dist/types/modes/components/custom-editor.d.ts +6 -1
  24. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  25. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  26. package/dist/types/modes/components/model-selector.d.ts +2 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +5 -1
  28. package/dist/types/modes/components/skill-message.d.ts +1 -0
  29. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  30. package/dist/types/modes/components/status-line.d.ts +15 -3
  31. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  32. package/dist/types/modes/components/welcome.d.ts +9 -1
  33. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  34. package/dist/types/modes/controllers/input-controller.d.ts +6 -1
  35. package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
  36. package/dist/types/modes/interactive-mode.d.ts +2 -0
  37. package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
  38. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  39. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
  40. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  41. package/dist/types/modes/theme/theme.d.ts +1 -1
  42. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  43. package/dist/types/modes/types.d.ts +5 -1
  44. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  45. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  46. package/dist/types/notifications/html-format.d.ts +2 -0
  47. package/dist/types/notifications/recent-activity.d.ts +5 -1
  48. package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
  49. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  50. package/dist/types/notifications/telegram-daemon.d.ts +4 -10
  51. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  52. package/dist/types/notifications/threaded-render.d.ts +1 -0
  53. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  54. package/dist/types/runtime-mcp/config.d.ts +2 -0
  55. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  56. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  57. package/dist/types/runtime-mcp/types.d.ts +6 -0
  58. package/dist/types/sdk.d.ts +1 -1
  59. package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
  60. package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
  61. package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
  62. package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
  63. package/dist/types/skc-runtime/state-writer.d.ts +1 -0
  64. package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
  65. package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
  66. package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
  67. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
  68. package/dist/types/slash-commands/types.d.ts +2 -0
  69. package/dist/types/system-prompt.d.ts +7 -0
  70. package/dist/types/tools/skill.d.ts +1 -1
  71. package/dist/types/utils/cmux-workspace.d.ts +52 -0
  72. package/dist/types/web/search/index.d.ts +4 -2
  73. package/dist/types/web/search/provider.d.ts +14 -0
  74. package/dist/types/web/search/providers/utils.d.ts +42 -13
  75. package/dist/types/web/search/render.d.ts +1 -0
  76. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  77. package/package.json +8 -8
  78. package/scripts/build-binary.ts +3 -0
  79. package/scripts/verify-insane-vendor.ts +0 -5
  80. package/src/capability/mcp.ts +2 -0
  81. package/src/cli/local-provider-smoke.ts +632 -0
  82. package/src/cli/notify-cli.ts +4 -2
  83. package/src/cli/telegram-cli.ts +156 -0
  84. package/src/cli/update-cli.ts +179 -13
  85. package/src/cli/web-search-cli.ts +5 -7
  86. package/src/cli.ts +20 -1
  87. package/src/commands/launch.ts +47 -2
  88. package/src/commands/local-provider.ts +62 -0
  89. package/src/commands/notify.ts +1 -1
  90. package/src/commands/ralplan.ts +1 -0
  91. package/src/commands/telegram.ts +47 -0
  92. package/src/config/keybindings.ts +15 -6
  93. package/src/config/mcp-schema.json +4 -0
  94. package/src/config/model-registry.ts +55 -1
  95. package/src/config/models-config-schema.ts +9 -0
  96. package/src/config/settings-schema.ts +256 -4
  97. package/src/config/settings.ts +11 -0
  98. package/src/config/telegram-autostart.ts +107 -0
  99. package/src/config/telegram-env-bridge.ts +92 -0
  100. package/src/coordinator-mcp/server.ts +276 -34
  101. package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
  103. package/src/defaults/skc-defaults.ts +16 -0
  104. package/src/discovery/builtin.ts +12 -0
  105. package/src/discovery/cursor.ts +5 -84
  106. package/src/discovery/gemini.ts +3 -89
  107. package/src/discovery/index.ts +4 -1
  108. package/src/discovery/mcp-json.ts +11 -0
  109. package/src/discovery/opencode.ts +5 -122
  110. package/src/discovery/windsurf.ts +4 -82
  111. package/src/edit/read-file.ts +9 -1
  112. package/src/extensibility/extensions/runner.ts +2 -1
  113. package/src/extensibility/extensions/types.ts +11 -0
  114. package/src/extensibility/skills.ts +30 -2
  115. package/src/hooks/skill-state.ts +33 -73
  116. package/src/i18n/messages/de.ts +1 -0
  117. package/src/i18n/messages/en.ts +1 -0
  118. package/src/i18n/messages/es.ts +1 -0
  119. package/src/i18n/messages/fr.ts +1 -0
  120. package/src/i18n/messages/ja.settings.ts +28 -0
  121. package/src/i18n/messages/ja.ts +1 -0
  122. package/src/i18n/messages/ko.settings.ts +28 -0
  123. package/src/i18n/messages/ko.ts +1 -0
  124. package/src/i18n/messages/zh.settings.ts +28 -0
  125. package/src/i18n/messages/zh.ts +1 -0
  126. package/src/internal-urls/docs-index.generated.ts +12 -7
  127. package/src/main.ts +5 -2
  128. package/src/migrate/executor.ts +23 -6
  129. package/src/modes/acp/acp-agent.ts +7 -6
  130. package/src/modes/components/agent-dashboard.ts +3 -35
  131. package/src/modes/components/custom-editor.ts +26 -0
  132. package/src/modes/components/custom-model-preset-wizard.ts +30 -189
  133. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  134. package/src/modes/components/hook-selector.ts +21 -3
  135. package/src/modes/components/model-selector.ts +246 -11
  136. package/src/modes/components/session-observer-overlay.ts +3 -3
  137. package/src/modes/components/session-selector.ts +19 -9
  138. package/src/modes/components/settings-selector.ts +551 -11
  139. package/src/modes/components/skill-message.ts +18 -10
  140. package/src/modes/components/status-line/presets.ts +11 -0
  141. package/src/modes/components/status-line/segments.ts +16 -19
  142. package/src/modes/components/status-line/types.ts +6 -2
  143. package/src/modes/components/status-line.ts +281 -117
  144. package/src/modes/components/tree-selector.ts +28 -8
  145. package/src/modes/components/two-column-body.ts +36 -0
  146. package/src/modes/components/welcome.ts +165 -30
  147. package/src/modes/controllers/command-controller.ts +26 -2
  148. package/src/modes/controllers/event-controller.ts +102 -4
  149. package/src/modes/controllers/extension-ui-controller.ts +21 -2
  150. package/src/modes/controllers/input-controller.ts +214 -47
  151. package/src/modes/controllers/selector-controller.ts +35 -33
  152. package/src/modes/interactive-mode.ts +36 -30
  153. package/src/modes/prompt-action-autocomplete.ts +48 -8
  154. package/src/modes/rpc/rpc-types.ts +1 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
  156. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  157. package/src/modes/slash-command-visibility.ts +9 -0
  158. package/src/modes/theme/theme.ts +5 -1
  159. package/src/modes/tmux-scroll.ts +92 -0
  160. package/src/modes/types.ts +8 -1
  161. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  162. package/src/modes/utils/terminal-bell.ts +48 -0
  163. package/src/modes/utils/ui-helpers.ts +76 -3
  164. package/src/notifications/daemon-paths.ts +22 -0
  165. package/src/notifications/html-format.ts +75 -1
  166. package/src/notifications/index.ts +32 -2
  167. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  168. package/src/notifications/recent-activity.ts +32 -8
  169. package/src/notifications/telegram-daemon-cli.ts +125 -13
  170. package/src/notifications/telegram-daemon-control.ts +9 -0
  171. package/src/notifications/telegram-daemon.ts +88 -46
  172. package/src/notifications/telegram-reference.ts +36 -14
  173. package/src/notifications/threaded-render.ts +14 -6
  174. package/src/prompts/agents/architect.md +7 -4
  175. package/src/prompts/agents/critic.md +15 -6
  176. package/src/prompts/agents/planner.md +8 -3
  177. package/src/prompts/system/system-prompt.md +20 -2
  178. package/src/prompts/tools/bash.md +1 -1
  179. package/src/runtime-mcp/config-writer.ts +28 -0
  180. package/src/runtime-mcp/config.ts +7 -0
  181. package/src/runtime-mcp/loader.ts +3 -0
  182. package/src/runtime-mcp/manager.ts +3 -0
  183. package/src/runtime-mcp/types.ts +6 -0
  184. package/src/sdk.ts +10 -11
  185. package/src/session/agent-session.ts +103 -13
  186. package/src/session/session-manager.ts +58 -18
  187. package/src/skc-runtime/launch-tmux.ts +34 -8
  188. package/src/skc-runtime/launch-worktree.ts +16 -2
  189. package/src/skc-runtime/ralplan-runtime.ts +22 -6
  190. package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
  191. package/src/skc-runtime/session-state-sidecar.ts +167 -24
  192. package/src/skc-runtime/state-runtime.ts +8 -5
  193. package/src/skc-runtime/state-writer.ts +5 -5
  194. package/src/skc-runtime/team-runtime.ts +21 -6
  195. package/src/skc-runtime/tmux-common.ts +16 -0
  196. package/src/skc-runtime/tmux-sessions.ts +13 -9
  197. package/src/skc-runtime/ultragoal-guard.ts +150 -24
  198. package/src/skc-runtime/ultragoal-runtime.ts +16 -124
  199. package/src/skc-runtime/workflow-manifest.generated.json +0 -8
  200. package/src/skc-runtime/workflow-manifest.ts +0 -1
  201. package/src/slash-commands/builtin-registry.ts +136 -23
  202. package/src/slash-commands/types.ts +2 -0
  203. package/src/system-prompt.ts +8 -0
  204. package/src/task/executor.ts +5 -0
  205. package/src/tools/ast-grep.ts +10 -7
  206. package/src/tools/bash.ts +39 -14
  207. package/src/tools/index.ts +8 -1
  208. package/src/tools/skill.ts +1 -1
  209. package/src/utils/cmux-workspace.ts +233 -0
  210. package/src/utils/title-generator.ts +2 -0
  211. package/src/web/kagi.ts +1 -1
  212. package/src/web/parallel.ts +2 -2
  213. package/src/web/search/index.ts +137 -52
  214. package/src/web/search/provider.ts +80 -0
  215. package/src/web/search/providers/anthropic.ts +1 -1
  216. package/src/web/search/providers/brave.ts +1 -1
  217. package/src/web/search/providers/codex.ts +1 -1
  218. package/src/web/search/providers/duckduckgo.ts +1 -1
  219. package/src/web/search/providers/exa.ts +1 -1
  220. package/src/web/search/providers/gemini.ts +7 -3
  221. package/src/web/search/providers/jina.ts +1 -1
  222. package/src/web/search/providers/kimi.ts +6 -1
  223. package/src/web/search/providers/openai-compatible.ts +1 -1
  224. package/src/web/search/providers/parallel.ts +1 -1
  225. package/src/web/search/providers/perplexity.ts +2 -2
  226. package/src/web/search/providers/searxng.ts +1 -1
  227. package/src/web/search/providers/synthetic.ts +1 -1
  228. package/src/web/search/providers/tavily.ts +1 -1
  229. package/src/web/search/providers/utils.ts +67 -17
  230. package/src/web/search/providers/xai.ts +1 -1
  231. package/src/web/search/providers/zai.ts +1 -1
  232. package/src/web/search/render.ts +3 -0
  233. package/src/workflow/workflow-intent-diff.ts +199 -0
  234. package/vendor/insane-search/MANIFEST.json +1 -3
  235. package/vendor/insane-search/engine/tests/test_u1.py +0 -28
  236. package/vendor/insane-search/engine/transport.py +2 -3
  237. package/vendor/insane-search/engine/validators.py +0 -14
  238. package/src/defaults/skc/rules/ponytail.md +0 -68
@@ -118,6 +118,20 @@ function findWorktreeByPath(entries: GitWorktreeEntry[], worktreePath: string):
118
118
  return entries.find(entry => path.resolve(entry.path) === resolved) ?? null;
119
119
  }
120
120
 
121
+ function describeWorktreeEntry(entry: GitWorktreeEntry): string {
122
+ return entry.detached ? `detached HEAD ${entry.head}` : (entry.branchRef ?? `HEAD ${entry.head}`);
123
+ }
124
+
125
+ function formatWorktreeTargetMismatch(plan: SkcLaunchWorktreePlan, existing: GitWorktreeEntry): string {
126
+ const expected = plan.detached ? `detached HEAD ${plan.baseRef}` : `branch refs/heads/${plan.branchName ?? ""}`;
127
+ return [
128
+ `worktree_target_mismatch:${plan.worktreePath}`,
129
+ `SKC launch worktree target is already registered for ${describeWorktreeEntry(existing)}, but this launch expects ${expected}.`,
130
+ `Path: ${plan.worktreePath}`,
131
+ "Refusing to delete or reuse the conflicting worktree automatically. Safe remediation: inspect the path, commit/stash any work, then remove or prune the stale worktree with git worktree remove <path> when it is no longer needed, or choose a different --worktree name.",
132
+ ].join("\n");
133
+ }
134
+
121
135
  function hasBranchInUse(entries: GitWorktreeEntry[], branchName: string, worktreePath: string): boolean {
122
136
  const expectedRef = `refs/heads/${branchName}`;
123
137
  const resolvedPath = path.resolve(worktreePath);
@@ -228,7 +242,7 @@ export function ensureLaunchWorktree(
228
242
  let dirty = isWorktreeDirty(plan.worktreePath);
229
243
  if (plan.detached) {
230
244
  if (!existingAtPath.detached) {
231
- throw new Error(`worktree_target_mismatch:${plan.worktreePath}`);
245
+ throw new Error(formatWorktreeTargetMismatch(plan, existingAtPath));
232
246
  }
233
247
  if (existingAtPath.head !== plan.baseRef) {
234
248
  if (dirty) throw new Error(`worktree_dirty:${plan.worktreePath}`);
@@ -236,7 +250,7 @@ export function ensureLaunchWorktree(
236
250
  dirty = false;
237
251
  }
238
252
  } else if (existingAtPath.branchRef !== expectedBranchRef) {
239
- throw new Error(`worktree_target_mismatch:${plan.worktreePath}`);
253
+ throw new Error(formatWorktreeTargetMismatch(plan, existingAtPath));
240
254
  }
241
255
  return {
242
256
  ...plan,
@@ -11,7 +11,7 @@ import {
11
11
  type RalplanIndexRow,
12
12
  summarizeRalplanIndex,
13
13
  } from "./ledger-event-renderer";
14
- import { isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
14
+ import { isRestrictedRoleAgentBash, SKC_RALPLAN_ARTIFACT_ENV } from "./restricted-role-agent-bash";
15
15
  import { modeStatePath, sessionPlansDir } from "./session-layout";
16
16
  import { resolveSkcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
17
17
  import { migrateWorkflowState } from "./state-migrations";
@@ -35,8 +35,9 @@ import {
35
35
  * that lives in the bundled `/skill:ralplan` skill — but it accepts every documented flag so
36
36
  * scripted users see a useful response and the active run is visible to the TUI.
37
37
  *
38
- * 2. **Artifact write**: `skc ralplan --write --stage <type> --stage_n <N> --artifact
39
- * <path-or-string> [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
38
+ * 2. **Artifact write**: `skc ralplan --write --stage <type> --stage_n <N>
39
+ * (--artifact <path-or-string> | --artifact-env SKC_RALPLAN_ARTIFACT)
40
+ * [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
40
41
  * / Critic / revision / post-interview / ADR / final markdown under `.skc/plans/ralplan/<run-id>/`, maintains
41
42
  * an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
42
43
  * the HUD chip to reflect the latest persisted stage.
@@ -81,6 +82,7 @@ const VALUE_FLAGS = new Set([
81
82
  "--stage",
82
83
  "--stage_n",
83
84
  "--artifact",
85
+ "--artifact-env",
84
86
  "--run-id",
85
87
  "--session-id",
86
88
  "--architect",
@@ -409,8 +411,16 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
409
411
  const stageN = parseStageN(flagValue(args, "--stage_n"));
410
412
 
411
413
  const rawArtifact = flagValue(args, "--artifact");
412
- if (rawArtifact === undefined || rawArtifact === "") {
413
- throw new RalplanCommandError(2, "--artifact is required for ralplan --write");
414
+ const artifactEnvName = flagValue(args, "--artifact-env");
415
+ const artifactSources = [rawArtifact, artifactEnvName].filter(value => value !== undefined);
416
+ if (artifactSources.length === 0 || artifactSources.some(value => value === "")) {
417
+ throw new RalplanCommandError(2, "--artifact or --artifact-env is required for ralplan --write");
418
+ }
419
+ if (artifactSources.length > 1) {
420
+ throw new RalplanCommandError(2, "--artifact and --artifact-env are mutually exclusive");
421
+ }
422
+ if (artifactEnvName !== undefined && artifactEnvName !== SKC_RALPLAN_ARTIFACT_ENV) {
423
+ throw new RalplanCommandError(2, `--artifact-env must be ${SKC_RALPLAN_ARTIFACT_ENV}`);
414
424
  }
415
425
 
416
426
  const session = resolveSkcSessionForWrite(cwd, {
@@ -430,7 +440,13 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
430
440
  const runId = explicitRunId || (await readActiveRunId(cwd, sessionId)) || sessionIdRaw || defaultRunId();
431
441
  assertSafePathComponent(runId, "run-id");
432
442
 
433
- const artifact = await resolveArtifactContent(rawArtifact, cwd);
443
+ const artifact =
444
+ artifactEnvName !== undefined
445
+ ? (process.env[SKC_RALPLAN_ARTIFACT_ENV] ?? "")
446
+ : await resolveArtifactContent(rawArtifact!, cwd);
447
+ if (artifact === "") {
448
+ throw new RalplanCommandError(2, "artifact content is empty");
449
+ }
434
450
  return { stage: stage as RalplanStage, stageN, runId, artifact, sessionId, json: hasFlag(args, "--json") };
435
451
  }
436
452
 
@@ -1,4 +1,5 @@
1
1
  export const SKC_RESTRICTED_ROLE_AGENT_BASH_ENV = "SKC_RESTRICTED_ROLE_AGENT_BASH";
2
+ export const SKC_RALPLAN_ARTIFACT_ENV = "SKC_RALPLAN_ARTIFACT";
2
3
 
3
4
  export function isRestrictedRoleAgentBash(): boolean {
4
5
  return process.env[SKC_RESTRICTED_ROLE_AGENT_BASH_ENV] === "1";
@@ -1,13 +1,18 @@
1
+ import * as fsSync from "node:fs";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import type { AssistantMessage } from "@sayknow-cli/ai";
4
- import { logger } from "@sayknow-cli/utils";
5
+ import { logger, postmortem } from "@sayknow-cli/utils";
5
6
 
6
7
  export const SKC_COORDINATOR_SESSION_STATE_FILE_ENV = "SKC_COORDINATOR_SESSION_STATE_FILE";
7
8
  export const SKC_COORDINATOR_SESSION_ID_ENV = "SKC_COORDINATOR_SESSION_ID";
9
+ export const SKC_COORDINATOR_SESSION_BRANCH_ENV = "SKC_COORDINATOR_SESSION_BRANCH";
8
10
 
9
11
  export type RuntimeState = "ready_for_input" | "running" | "needs_user_input" | "completed" | "errored";
10
12
 
13
+ type FinalResponseSource = "agent_end" | "launch_error";
14
+ const MAX_PUBLIC_ERROR_MESSAGE_LENGTH = 2000;
15
+
11
16
  interface RuntimeStateEvent {
12
17
  type: string;
13
18
  messages?: unknown[];
@@ -17,6 +22,7 @@ interface RuntimeStateContext {
17
22
  sessionId: string;
18
23
  cwd: string;
19
24
  sessionFile?: string | null;
25
+ branch?: string | null;
20
26
  }
21
27
 
22
28
  interface RuntimeStateSidecarPayload {
@@ -26,6 +32,7 @@ interface RuntimeStateSidecarPayload {
26
32
  ready_for_input?: unknown;
27
33
  cwd?: unknown;
28
34
  session_file?: unknown;
35
+ final_response?: { source?: unknown };
29
36
  }
30
37
 
31
38
  export type TerminalRuntimeStateStatus =
@@ -103,7 +110,7 @@ function assistantText(assistant: AssistantMessage | undefined): string | null {
103
110
  function finalResponseForEvent(event: RuntimeStateEvent): {
104
111
  text: string | null;
105
112
  format: "markdown";
106
- source: "agent_end";
113
+ source: FinalResponseSource;
107
114
  artifact_path: null;
108
115
  truncated: false;
109
116
  } | null {
@@ -127,6 +134,118 @@ function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
127
134
  return null;
128
135
  }
129
136
 
137
+ function readPreviousPayload(stateFile: string): Record<string, unknown> {
138
+ try {
139
+ return JSON.parse(fsSync.readFileSync(stateFile, "utf8")) as Record<string, unknown>;
140
+ } catch {
141
+ return {};
142
+ }
143
+ }
144
+
145
+ function shouldPreserveTerminalPayload(previous: RuntimeStateSidecarPayload): boolean {
146
+ if (previous.state !== "completed" && previous.state !== "errored") return false;
147
+ const source = previous.final_response?.source;
148
+ return source === "agent_end" || source === "launch_error";
149
+ }
150
+
151
+ function branchForContext(context: RuntimeStateContext): string | null {
152
+ return context.branch ?? (process.env[SKC_COORDINATOR_SESSION_BRANCH_ENV]?.trim() || null);
153
+ }
154
+
155
+ function basePayload(input: {
156
+ context: RuntimeStateContext;
157
+ previous: Record<string, unknown>;
158
+ state: RuntimeState;
159
+ now: string;
160
+ source: string;
161
+ event: string;
162
+ reason: string | null;
163
+ }): Record<string, unknown> {
164
+ return {
165
+ schema_version: 1,
166
+ session_id: process.env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || input.context.sessionId,
167
+ state: input.state,
168
+ ready_for_input: input.state === "completed" || input.state === "ready_for_input",
169
+ updated_at: input.now,
170
+ current_turn_id: typeof input.previous.current_turn_id === "string" ? input.previous.current_turn_id : null,
171
+ last_turn_id: typeof input.previous.last_turn_id === "string" ? input.previous.last_turn_id : null,
172
+ live: input.state === "running",
173
+ reason: input.reason,
174
+ source: input.source,
175
+ event: input.event,
176
+ cwd: input.context.cwd,
177
+ workdir: input.context.cwd,
178
+ branch: branchForContext(input.context),
179
+ session_file: input.context.sessionFile ?? null,
180
+ };
181
+ }
182
+ function publicSafeErrorMessage(message: string): string {
183
+ const normalized = message.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
184
+ if (normalized.length <= MAX_PUBLIC_ERROR_MESSAGE_LENGTH) return normalized;
185
+ return `${normalized.slice(0, MAX_PUBLIC_ERROR_MESSAGE_LENGTH)}…`;
186
+ }
187
+
188
+ function errorMessageForPostmortem(reason: postmortem.Reason): string {
189
+ return publicSafeErrorMessage(`SKC process cleanup ran for ${reason}`);
190
+ }
191
+
192
+ function numericProcessExitCode(defaultCode: number | null): number | null {
193
+ return typeof process.exitCode === "number" ? process.exitCode : defaultCode;
194
+ }
195
+
196
+ function postmortemExitDetails(reason: postmortem.Reason): {
197
+ state: RuntimeState;
198
+ reason: string;
199
+ exitKind: string;
200
+ exitCode: number | null;
201
+ signal: string | null;
202
+ error?: { code: string; message: string; recoverable: true };
203
+ } {
204
+ if (reason === postmortem.Reason.EXIT || reason === postmortem.Reason.MANUAL) {
205
+ const exitCode = numericProcessExitCode(0) ?? 0;
206
+ const state: RuntimeState = exitCode === 0 ? "completed" : "errored";
207
+ return {
208
+ state,
209
+ reason: reason === postmortem.Reason.EXIT ? "process_exit" : "manual_cleanup",
210
+ exitKind: reason,
211
+ exitCode,
212
+ signal: null,
213
+ ...(state === "errored"
214
+ ? {
215
+ error: {
216
+ code: "process_exit",
217
+ message: publicSafeErrorMessage(`SKC process exited with code ${exitCode}`),
218
+ recoverable: true,
219
+ },
220
+ }
221
+ : {}),
222
+ };
223
+ }
224
+ const signalByReason: Partial<Record<postmortem.Reason, string>> = {
225
+ [postmortem.Reason.SIGINT]: "SIGINT",
226
+ [postmortem.Reason.SIGTERM]: "SIGTERM",
227
+ [postmortem.Reason.SIGHUP]: "SIGHUP",
228
+ };
229
+ return {
230
+ state: "errored",
231
+ reason,
232
+ exitKind: reason,
233
+ exitCode: numericProcessExitCode(null),
234
+ signal: signalByReason[reason] ?? null,
235
+ error: { code: reason, message: errorMessageForPostmortem(reason), recoverable: true },
236
+ };
237
+ }
238
+
239
+ function writeStateFileSync(stateFile: string, payload: Record<string, unknown>): void {
240
+ fsSync.mkdirSync(path.dirname(stateFile), { recursive: true });
241
+ fsSync.writeFileSync(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
242
+ }
243
+
244
+ async function writeStateFile(stateFile: string, payload: Record<string, unknown>): Promise<void> {
245
+ await fs.mkdir(path.dirname(stateFile), { recursive: true });
246
+ await Bun.write(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
247
+ }
248
+
130
249
  export async function persistCoordinatorRuntimeStateFromEvent(
131
250
  event: RuntimeStateEvent,
132
251
  context: RuntimeStateContext,
@@ -136,42 +255,66 @@ export async function persistCoordinatorRuntimeStateFromEvent(
136
255
  const state = stateForEvent(event);
137
256
  if (!state) return;
138
257
  const now = new Date().toISOString();
139
- let previous: Record<string, unknown> = {};
140
- try {
141
- previous = JSON.parse(await Bun.file(stateFile).text()) as Record<string, unknown>;
142
- } catch {
143
- previous = {};
144
- }
258
+ const previous = readPreviousPayload(stateFile);
145
259
  const finalResponse = finalResponseForEvent(event);
146
260
  const payload = {
147
- schema_version: 1,
148
- session_id: process.env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId,
149
- state,
150
- ready_for_input: state === "completed" || state === "ready_for_input",
151
- updated_at: now,
152
- current_turn_id: typeof previous.current_turn_id === "string" ? previous.current_turn_id : null,
153
- last_turn_id: typeof previous.last_turn_id === "string" ? previous.last_turn_id : null,
154
- live: typeof previous.live === "boolean" ? previous.live : null,
155
- reason: null,
156
- source: "agent_session_event",
157
- event: event.type,
158
- cwd: context.cwd,
159
- session_file: context.sessionFile ?? null,
261
+ ...basePayload({ context, previous, state, now, source: "agent_session_event", event: event.type, reason: null }),
262
+ ...(state === "completed" || state === "errored" ? { ended_at: now } : {}),
160
263
  ...(finalResponse ? { final_response: finalResponse } : {}),
161
264
  ...(state === "errored"
162
265
  ? {
163
266
  error: {
164
267
  code: "agent_error",
165
- message: lastAssistant(event.messages)?.errorMessage ?? "agent_error",
268
+ message: publicSafeErrorMessage(lastAssistant(event.messages)?.errorMessage ?? "agent_error"),
166
269
  recoverable: true,
167
270
  },
168
271
  }
169
272
  : {}),
170
273
  };
171
274
  try {
172
- await fs.mkdir(path.dirname(stateFile), { recursive: true });
173
- await Bun.write(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
275
+ await writeStateFile(stateFile, payload);
174
276
  } catch (error) {
175
277
  logger.warn("Failed to persist coordinator runtime state", { error: String(error), stateFile });
176
278
  }
177
279
  }
280
+
281
+ export function persistCoordinatorRuntimeStateFromPostmortem(
282
+ reason: postmortem.Reason,
283
+ context: RuntimeStateContext,
284
+ ): void {
285
+ const stateFile = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
286
+ if (!stateFile) return;
287
+ const previous = readPreviousPayload(stateFile);
288
+ if (shouldPreserveTerminalPayload(previous as RuntimeStateSidecarPayload)) return;
289
+ const now = new Date().toISOString();
290
+ const details = postmortemExitDetails(reason);
291
+ const payload = {
292
+ ...basePayload({
293
+ context,
294
+ previous,
295
+ state: details.state,
296
+ now,
297
+ source: "process_postmortem",
298
+ event: "process_exit",
299
+ reason: details.reason,
300
+ }),
301
+ ended_at: now,
302
+ detected_at: now,
303
+ exit_kind: details.exitKind,
304
+ exit_code: details.exitCode,
305
+ signal: details.signal,
306
+ ...(details.error ? { error: details.error } : {}),
307
+ };
308
+ try {
309
+ writeStateFileSync(stateFile, payload);
310
+ } catch (error) {
311
+ logger.warn("Failed to persist coordinator runtime state during postmortem", { error: String(error), stateFile });
312
+ }
313
+ }
314
+
315
+ export function registerCoordinatorRuntimeStateFinalizer(context: RuntimeStateContext): () => void {
316
+ if (!process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()) return () => {};
317
+ return postmortem.register("coordinator-runtime-state", reason => {
318
+ persistCoordinatorRuntimeStateFromPostmortem(reason, context);
319
+ });
320
+ }
@@ -847,11 +847,14 @@ async function writeJsonAtomic(
847
847
  forced: options?.force ?? false,
848
848
  },
849
849
  });
850
- // `writeResult.revision` is computed inside the writer lock, so it is the revision this
851
- // write actually owns. Prefer it over a post-lock file re-read, which a concurrent writer
852
- // could have advanced before the read — that race could otherwise let this payload be
853
- // published with another writer's newer revision.
854
- return { warning, stamped: (await readJsonFile(filePath)) ?? {}, revision: writeResult.revision };
850
+ // `writeResult.stamped` and `.revision` are computed inside the writer lock, so they are
851
+ // the envelope/revision this write actually owns. Never post-lock re-read here: a concurrent
852
+ // writer could advance the file before that read and make this payload publish another
853
+ // writer's newer revision into the derived active-state cache.
854
+ if (!writeResult.written || !isPlainObject(writeResult.stamped)) {
855
+ throw new Error(`state writer did not return a stamped workflow envelope for ${filePath}`);
856
+ }
857
+ return { warning, stamped: writeResult.stamped, revision: writeResult.revision };
855
858
  }
856
859
 
857
860
  function parseFieldsFlag(args: readonly string[]): StateProjectionField[] | undefined {
@@ -99,7 +99,7 @@ export interface GuardedStateWriterOptions extends StateWriterOptions {
99
99
  }
100
100
 
101
101
  export type GuardedWriteResult =
102
- | { path: string; written: true; revision: number }
102
+ | { path: string; written: true; revision: number; stamped: unknown }
103
103
  | { path: string; written: false; reason: "stale-skip"; revision: number };
104
104
 
105
105
  export interface StateWriterOptions {
@@ -511,7 +511,7 @@ async function writeGuardedResolvedJsonAtomic(
511
511
  const next = stampStateRevision(withWorkflowReceipt(value, buildReceipt(options)), currentRevision + 1);
512
512
  await atomicWrite(filePath, jsonText(next));
513
513
  await maybeAudit(filePath, options);
514
- return { path: filePath, written: true, revision: currentRevision + 1 };
514
+ return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
515
515
  }
516
516
 
517
517
  const incomingSourceRevision =
@@ -526,7 +526,7 @@ async function writeGuardedResolvedJsonAtomic(
526
526
  );
527
527
  await atomicWrite(filePath, jsonText(next));
528
528
  await maybeAudit(filePath, options);
529
- return { path: filePath, written: true, revision: currentRevision + 1 };
529
+ return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
530
530
  },
531
531
  options.lock,
532
532
  );
@@ -574,7 +574,7 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
574
574
  }
575
575
  await atomicWrite(filePath, jsonText(next));
576
576
  await maybeAudit(filePath, options);
577
- return { path: filePath, written: true, revision: currentRevision + 1 };
577
+ return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
578
578
  }
579
579
 
580
580
  const incomingSourceRevision =
@@ -599,7 +599,7 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
599
599
  }
600
600
  await atomicWrite(filePath, jsonText(next));
601
601
  await maybeAudit(filePath, options);
602
- return { path: filePath, written: true, revision: currentRevision + 1 };
602
+ return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
603
603
  },
604
604
  options.lock,
605
605
  );
@@ -1896,13 +1896,27 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
1896
1896
  }
1897
1897
  return { sessionName, windowIndex, leaderPaneId, target: `${sessionName}:${windowIndex}` };
1898
1898
  }
1899
- export function resolveSkcWorkerCommand(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
1899
+ export function resolveSkcWorkerCommand(
1900
+ cwd = process.cwd(),
1901
+ env: NodeJS.ProcessEnv = process.env,
1902
+ platform: NodeJS.Platform = process.platform,
1903
+ argv: string[] = process.argv,
1904
+ execPath = process.execPath,
1905
+ ): string {
1900
1906
  const explicit = env.SKC_TEAM_WORKER_COMMAND?.trim();
1901
1907
  if (explicit) return explicit;
1902
- const entrypoint = process.argv[1];
1903
- if (entrypoint?.endsWith(".ts"))
1904
- return `${shellQuote(process.execPath)} ${shellQuote(path.resolve(cwd, entrypoint))}`;
1905
- if (entrypoint && path.basename(entrypoint).startsWith("skc")) return shellQuote(path.resolve(cwd, entrypoint));
1908
+ const entrypoint = argv[1];
1909
+ if (!entrypoint) return "skc";
1910
+ const pathModule = platform === "win32" ? path.win32 : path;
1911
+ const resolvedEntrypoint = pathModule.isAbsolute(entrypoint) ? entrypoint : pathModule.resolve(cwd, entrypoint);
1912
+ if (platform === "win32") {
1913
+ if (entrypoint.endsWith(".ts") || entrypoint.endsWith(".js") || entrypoint.endsWith(".mjs"))
1914
+ return `${powershellQuote(execPath)} ${powershellQuote(resolvedEntrypoint)}`;
1915
+ if (pathModule.basename(entrypoint).startsWith("skc")) return powershellQuote(resolvedEntrypoint);
1916
+ return "skc";
1917
+ }
1918
+ if (entrypoint.endsWith(".ts")) return `${shellQuote(execPath)} ${shellQuote(resolvedEntrypoint)}`;
1919
+ if (path.basename(entrypoint).startsWith("skc")) return shellQuote(path.resolve(cwd, entrypoint));
1906
1920
  return "skc";
1907
1921
  }
1908
1922
  /** @internal Exported for unit tests. */
@@ -1937,7 +1951,8 @@ export function buildWorkerCommand(
1937
1951
  ...(worker.worktree_path ? [envAssignment("SKC_TEAM_WORKTREE_PATH", worker.worktree_path)] : []),
1938
1952
  ];
1939
1953
  const joined = platform === "win32" ? envLines.join(" ") : envLines.join(" ");
1940
- return `${joined} ${config.worker_command} ${quote(prompt)}`;
1954
+ const invocation = platform === "win32" ? `& ${config.worker_command}` : config.worker_command;
1955
+ return `${joined} ${invocation} ${quote(prompt)}`;
1941
1956
  }
1942
1957
  interface SkcTeamInitialLane {
1943
1958
  label: string;
@@ -86,6 +86,22 @@ export function buildSkcTmuxExactOptionTarget(
86
86
  return `=${sessionName}:`;
87
87
  }
88
88
 
89
+ /**
90
+ * Build the exact-session target for tmux *session-scoped* commands such as
91
+ * `attach-session` and `kill-session`. Native tmux accepts `=NAME` for an
92
+ * exact session match, but Windows psmux 3.3.x rejects that target form for
93
+ * session commands even though the bare `NAME` resolves. Keep native tmux on
94
+ * exact targets and intentionally use the bare session name for psmux.
95
+ */
96
+ export function buildSkcTmuxExactSessionTarget(
97
+ sessionName: string,
98
+ opts: { env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform; binary?: ResolvedTmuxBinary } = {},
99
+ ): string {
100
+ const binary = opts.binary ?? resolveSkcTmuxBinary({ env: opts.env, platform: opts.platform });
101
+ if (binary.isPsmux) return sessionName;
102
+ return `=${sessionName}`;
103
+ }
104
+
89
105
  export const SKC_TMUX_UNTAGGED_REASON = "skc_tmux_session_untagged";
90
106
 
91
107
  export function buildSkcTmuxUntaggedSessionHint(tmuxCommand: string): string {
@@ -1,6 +1,7 @@
1
1
  import { resolveSkcTmuxBinary } from "./psmux-detect";
2
2
  import {
3
3
  buildSkcTmuxExactOptionTarget,
4
+ buildSkcTmuxExactSessionTarget,
4
5
  buildSkcTmuxProfileCommands,
5
6
  buildSkcTmuxSessionName,
6
7
  buildSkcTmuxUntaggedSessionError,
@@ -60,7 +61,7 @@ function runTmux(args: string[], env: NodeJS.ProcessEnv = process.env): string {
60
61
 
61
62
  function tryKillSession(sessionName: string, env: NodeJS.ProcessEnv): void {
62
63
  try {
63
- runTmux(["kill-session", "-t", `=${sessionName}`], env);
64
+ runTmux(["kill-session", "-t", buildSkcTmuxExactSessionTarget(sessionName, { env })], env);
64
65
  } catch {
65
66
  // Best-effort cleanup only; preserve the original create/tag failure.
66
67
  }
@@ -360,7 +361,7 @@ export function removeSkcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv
360
361
  if (readProfileForExactTarget(session.name, env) !== SKC_TMUX_PROFILE_VALUE) {
361
362
  throw new Error(`skc_tmux_session_not_managed:${sessionName}`);
362
363
  }
363
- runTmux(["kill-session", "-t", `=${session.name}`], env);
364
+ runTmux(["kill-session", "-t", buildSkcTmuxExactSessionTarget(session.name, { env })], env);
364
365
  return session;
365
366
  }
366
367
 
@@ -404,18 +405,21 @@ export function forceCloseSkcTmuxSession(
404
405
  }
405
406
  }
406
407
  // Intentionally NOT refusing live/attached panes — force-close is hard-kill.
407
- runTmux(["kill-session", "-t", `=${session.name}`], env);
408
+ runTmux(["kill-session", "-t", buildSkcTmuxExactSessionTarget(session.name, { env })], env);
408
409
  return session;
409
410
  }
410
411
 
411
412
  export function attachSkcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): never {
412
413
  const session = statusSkcTmuxSession(sessionName, env);
413
414
  const tmuxCommand = resolveSkcTmuxCommand(env);
414
- const result = Bun.spawnSync([tmuxCommand, "attach-session", "-t", `=${session.name}`], {
415
- stdin: "inherit",
416
- stdout: "inherit",
417
- stderr: "inherit",
418
- env,
419
- });
415
+ const result = Bun.spawnSync(
416
+ [tmuxCommand, "attach-session", "-t", buildSkcTmuxExactSessionTarget(session.name, { env })],
417
+ {
418
+ stdin: "inherit",
419
+ stdout: "inherit",
420
+ stderr: "inherit",
421
+ env,
422
+ },
423
+ );
420
424
  process.exit(result.exitCode ?? 1);
421
425
  }