@gajae-code/coding-agent 0.12.0 → 0.12.1

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 (223) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +54 -0
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +18 -0
  16. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  18. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  19. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  21. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  22. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  23. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  24. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  25. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  26. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  27. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  28. package/dist/types/reminders/star-reminder.d.ts +3 -13
  29. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  30. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  31. package/dist/types/sdk/broker/transport.d.ts +2 -0
  32. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  33. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  34. package/dist/types/sdk/bus/config.d.ts +6 -2
  35. package/dist/types/sdk/bus/index.d.ts +29 -0
  36. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  37. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  38. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  39. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  40. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  41. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  42. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  43. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  44. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  45. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  46. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  47. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  48. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  49. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  50. package/dist/types/sdk/prompt-status.d.ts +23 -3
  51. package/dist/types/sdk/session.d.ts +6 -0
  52. package/dist/types/session/agent-session.d.ts +38 -2
  53. package/dist/types/session/artifacts.d.ts +8 -0
  54. package/dist/types/session/blob-store.d.ts +57 -4
  55. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  56. package/dist/types/session/session-manager.d.ts +23 -6
  57. package/dist/types/session/session-storage.d.ts +4 -0
  58. package/dist/types/session/streaming-output.d.ts +36 -7
  59. package/dist/types/setup/credential-import.d.ts +12 -0
  60. package/dist/types/skill-state/active-state.d.ts +12 -0
  61. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  62. package/dist/types/task/executor.d.ts +3 -2
  63. package/dist/types/task/index.d.ts +3 -1
  64. package/dist/types/task/provider-retry-status.d.ts +8 -0
  65. package/dist/types/task/receipt.d.ts +7 -1
  66. package/dist/types/task/types.d.ts +23 -0
  67. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  68. package/dist/types/tools/bash-interactive.d.ts +4 -1
  69. package/dist/types/tools/bash.d.ts +3 -1
  70. package/dist/types/tools/image-gen.d.ts +4 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/output-meta.d.ts +23 -2
  73. package/dist/types/tools/path-utils.d.ts +1 -0
  74. package/dist/types/tools/read.d.ts +0 -6
  75. package/dist/types/tools/review.d.ts +5 -7
  76. package/dist/types/tools/subagent.d.ts +2 -0
  77. package/dist/types/utils/gh.d.ts +13 -0
  78. package/dist/types/utils/image-resize.d.ts +1 -0
  79. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  80. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  81. package/dist/types/web/search/providers/xai.d.ts +2 -0
  82. package/package.json +10 -8
  83. package/scripts/acp-conformance-agent.ts +218 -0
  84. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  85. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  86. package/scripts/resident-memory-bench.ts +1341 -0
  87. package/scripts/run-acp-conformance.ts +285 -0
  88. package/src/async/job-manager.ts +78 -20
  89. package/src/cli/fast-help.ts +1 -1
  90. package/src/cli/plugin-cli.ts +176 -18
  91. package/src/commands/team.ts +19 -1
  92. package/src/config/file-lock.ts +11 -2
  93. package/src/config/settings-schema.ts +51 -0
  94. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  95. package/src/coordinator-mcp/server.ts +5 -12
  96. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  97. package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
  98. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  99. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
  100. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  101. package/src/defaults/gjc-defaults.ts +4 -4
  102. package/src/exa/mcp-client.ts +12 -3
  103. package/src/exec/bash-executor.ts +144 -27
  104. package/src/extensibility/extensions/runner.ts +15 -0
  105. package/src/extensibility/extensions/types.ts +18 -1
  106. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  107. package/src/extensibility/gjc-plugins/index.ts +19 -2
  108. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  109. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  110. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  111. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  112. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  113. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  114. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  115. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  116. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  117. package/src/extensibility/gjc-plugins/types.ts +141 -0
  118. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  119. package/src/gjc-runtime/launch-tmux.ts +25 -6
  120. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  121. package/src/gjc-runtime/ralplan-runtime.ts +411 -17
  122. package/src/gjc-runtime/team-launch.ts +8 -0
  123. package/src/gjc-runtime/team-runtime.ts +140 -32
  124. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  126. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  127. package/src/gjc-runtime/ultragoal-runtime.ts +401 -611
  128. package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
  129. package/src/gjc-runtime/workflow-manifest.ts +4 -68
  130. package/src/internal-urls/docs-index.generated.ts +15 -15
  131. package/src/modes/DESIGN.md +51 -0
  132. package/src/modes/acp/acp-agent.ts +298 -186
  133. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  134. package/src/modes/components/notifications-settings-editor.ts +12 -1
  135. package/src/modes/components/oauth-selector.ts +68 -30
  136. package/src/modes/components/session-observer-overlay.ts +305 -86
  137. package/src/modes/components/settings-selector.ts +56 -5
  138. package/src/modes/components/status-line/gh.ts +16 -0
  139. package/src/modes/components/tool-status-header.ts +3 -14
  140. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  141. package/src/modes/controllers/command-controller.ts +4 -0
  142. package/src/modes/controllers/extension-ui-controller.ts +7 -0
  143. package/src/modes/controllers/selector-controller.ts +45 -0
  144. package/src/modes/runtime-init.ts +3 -0
  145. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  146. package/src/prompts/system/eager-todo.md +8 -4
  147. package/src/prompts/system/system-prompt.md +11 -4
  148. package/src/prompts/tools/bash.md +5 -5
  149. package/src/prompts/tools/search-tool-bm25.md +5 -0
  150. package/src/prompts/tools/task.md +1 -1
  151. package/src/reminders/star-reminder.ts +4 -42
  152. package/src/runtime-mcp/manager.ts +19 -4
  153. package/src/sdk/acp/adapter.ts +22 -6
  154. package/src/sdk/broker/transport.ts +35 -3
  155. package/src/sdk/bus/attachment-registry.ts +5 -1
  156. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  157. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  158. package/src/sdk/bus/config.ts +18 -2
  159. package/src/sdk/bus/index.ts +661 -126
  160. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  161. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  162. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  163. package/src/sdk/bus/reconciliation-store.ts +95 -16
  164. package/src/sdk/bus/rich-draft.ts +1 -1
  165. package/src/sdk/bus/rich-render.ts +2 -2
  166. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  167. package/src/sdk/bus/telegram-cli.ts +33 -8
  168. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  169. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  170. package/src/sdk/bus/telegram-daemon.ts +916 -23
  171. package/src/sdk/bus/telegram-reference.ts +40 -3
  172. package/src/sdk/bus/threaded-render.ts +48 -3
  173. package/src/sdk/bus/topic-registry.ts +20 -0
  174. package/src/sdk/host/query/revision-store.ts +36 -2
  175. package/src/sdk/host/reverse-leases.ts +44 -7
  176. package/src/sdk/prompt-status.ts +16 -3
  177. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  178. package/src/sdk/session.ts +88 -9
  179. package/src/session/agent-session.ts +822 -172
  180. package/src/session/artifacts.ts +22 -0
  181. package/src/session/auth-broker-config.ts +10 -3
  182. package/src/session/blob-store.ts +915 -24
  183. package/src/session/internal/managed-session-scope.ts +44 -6
  184. package/src/session/internal/managed-session-storage.ts +48 -6
  185. package/src/session/session-manager.ts +1614 -435
  186. package/src/session/session-storage.ts +9 -1
  187. package/src/session/streaming-output.ts +238 -68
  188. package/src/setup/credential-import.ts +75 -13
  189. package/src/skill-state/active-state.ts +32 -0
  190. package/src/skill-state/workflow-hud.ts +17 -0
  191. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  192. package/src/task/executor.ts +122 -39
  193. package/src/task/index.ts +23 -14
  194. package/src/task/provider-retry-status.ts +16 -0
  195. package/src/task/receipt.ts +23 -7
  196. package/src/task/render.ts +6 -2
  197. package/src/task/types.ts +108 -0
  198. package/src/tools/ask-answer-registry.ts +12 -6
  199. package/src/tools/ask.ts +86 -55
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash-interactive.ts +8 -3
  203. package/src/tools/bash.ts +503 -60
  204. package/src/tools/find.ts +1 -0
  205. package/src/tools/image-gen.ts +29 -5
  206. package/src/tools/index.ts +4 -0
  207. package/src/tools/output-meta.ts +108 -14
  208. package/src/tools/path-utils.ts +3 -2
  209. package/src/tools/read.ts +18 -0
  210. package/src/tools/review.ts +5 -7
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/subagent-render.ts +6 -2
  213. package/src/tools/subagent.ts +6 -0
  214. package/src/tools/telegram-send.ts +6 -3
  215. package/src/utils/gh.ts +39 -0
  216. package/src/utils/image-resize.ts +3 -0
  217. package/src/web/search/providers/anthropic.ts +4 -4
  218. package/src/web/search/providers/kimi.ts +15 -5
  219. package/src/web/search/providers/searxng.ts +47 -4
  220. package/src/web/search/providers/xai.ts +7 -2
  221. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  222. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  223. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -551,6 +551,40 @@ export const GJC_TEAM_API_OPERATIONS = [
551
551
  "write-task-approval",
552
552
  ] as const;
553
553
 
554
+ export type GjcTeamApiOperation = (typeof GJC_TEAM_API_OPERATIONS)[number];
555
+
556
+ export class UnknownGjcTeamApiOperationError extends Error {
557
+ readonly code = "unknown_team_api_operation";
558
+ readonly operation: string;
559
+ readonly suggestions: readonly string[];
560
+
561
+ constructor(operation: string, suggestions: readonly string[]) {
562
+ const guidance =
563
+ suggestions.length > 0
564
+ ? `did you mean ${suggestions.join(" or ")}?`
565
+ : "run gjc team api --help for supported operations";
566
+ super(`unknown_team_api_operation:${operation}; ${guidance}`);
567
+ this.name = "UnknownGjcTeamApiOperationError";
568
+ this.operation = operation;
569
+ this.suggestions = suggestions;
570
+ }
571
+ }
572
+
573
+ function isGjcTeamApiOperation(operation: string): operation is GjcTeamApiOperation {
574
+ return (GJC_TEAM_API_OPERATIONS as readonly string[]).includes(operation);
575
+ }
576
+
577
+ function unknownGjcTeamApiOperationSuggestions(operation: string): readonly string[] {
578
+ if (operation === "heartbeat") return ["read-worker-heartbeat", "update-worker-heartbeat"];
579
+ if (operation === "get-task") return ["read-task"];
580
+ return [];
581
+ }
582
+
583
+ function resolveGjcTeamApiOperation(operation: string): GjcTeamApiOperation {
584
+ if (isGjcTeamApiOperation(operation)) return operation;
585
+ throw new UnknownGjcTeamApiOperationError(operation, unknownGjcTeamApiOperationSuggestions(operation));
586
+ }
587
+
554
588
  function currentTimeMs(): number {
555
589
  return gjcTeamRuntimeTestSeams?.nowMs?.() ?? Date.now();
556
590
  }
@@ -1494,6 +1528,7 @@ async function relaunchWorkerPaneForMemoryGuard(input: {
1494
1528
  startupAckPath: string;
1495
1529
  replacementToken: string;
1496
1530
  startupAckTimeoutMs: number;
1531
+ env: NodeJS.ProcessEnv;
1497
1532
  }): Promise<string> {
1498
1533
  if (input.config.dry_run)
1499
1534
  return `%memory-guard-${input.worker.id}-${stableHash(`${input.worker.id}:${now()}`).slice(0, 8)}`;
@@ -1502,6 +1537,7 @@ async function relaunchWorkerPaneForMemoryGuard(input: {
1502
1537
  input.worker,
1503
1538
  input.platform,
1504
1539
  `Send startup ACK before resuming: gjc team api worker-startup-ack --input '{"team_name":"${input.config.team_name}","worker_id":"${input.worker.id}","protocol_version":"1","replacement_token":"${input.replacementToken}"}' --json. ${GJC_TEAM_CONTINUATION_PROMPT}`,
1540
+ input.env,
1505
1541
  );
1506
1542
  const workerCwd = input.worker.worktree_path ?? input.config.leader.cwd;
1507
1543
  const useSendKeysFallback = shouldDispatchWorkerWithSendKeys(input.config.tmux_command, input.platform);
@@ -1602,7 +1638,12 @@ async function applyWorkerMemoryGuardUnlocked(input: {
1602
1638
  allowAutomaticAction?: boolean;
1603
1639
  replacementToken?: string;
1604
1640
  dir: string;
1605
- taskMutation: GjcTeamTaskMutationCapability;
1641
+ /**
1642
+ * Acquire the team task-mutation fence only for the blocked-state transition.
1643
+ * Must not be held across the successor startup-ack wait: concurrent
1644
+ * `worker-startup-ack` must publish while replacement is in flight.
1645
+ */
1646
+ withTaskMutation: <T>(fn: (capability: GjcTeamTaskMutationCapability) => Promise<T>) => Promise<T>;
1606
1647
  }): Promise<Record<string, unknown>> {
1607
1648
  const dir = input.dir;
1608
1649
  const config = await readConfig(dir);
@@ -1782,16 +1823,18 @@ async function applyWorkerMemoryGuardUnlocked(input: {
1782
1823
  ledger = retried.ledger;
1783
1824
  await writeWorkerMemoryGuardLedger(dir, ledger);
1784
1825
  if (retried.finalBlocked)
1785
- await finalizeWorkerMemoryGuardBlockedState({
1786
- teamName: input.teamName,
1787
- dir,
1788
- worker,
1789
- task,
1790
- taskMutation: input.taskMutation,
1791
- reason: checkpoint.reason,
1792
- cwd: input.cwd,
1793
- env: input.env,
1794
- });
1826
+ await input.withTaskMutation(taskMutation =>
1827
+ finalizeWorkerMemoryGuardBlockedState({
1828
+ teamName: input.teamName,
1829
+ dir,
1830
+ worker,
1831
+ task,
1832
+ taskMutation,
1833
+ reason: checkpoint.reason,
1834
+ cwd: input.cwd,
1835
+ env: input.env,
1836
+ }),
1837
+ );
1795
1838
  await appendWorkerMemoryGuardAction({
1796
1839
  dir,
1797
1840
  teamName: input.teamName,
@@ -1853,6 +1896,7 @@ async function applyWorkerMemoryGuardUnlocked(input: {
1853
1896
  startupAckPath,
1854
1897
  replacementToken,
1855
1898
  startupAckTimeoutMs,
1899
+ env: input.env,
1856
1900
  });
1857
1901
  } catch (error) {
1858
1902
  await restorePredecessorStartupState();
@@ -1868,16 +1912,18 @@ async function applyWorkerMemoryGuardUnlocked(input: {
1868
1912
  ledger = retried.ledger;
1869
1913
  await writeWorkerMemoryGuardLedger(dir, ledger);
1870
1914
  if (retried.finalBlocked)
1871
- await finalizeWorkerMemoryGuardBlockedState({
1872
- teamName: input.teamName,
1873
- dir,
1874
- worker,
1875
- task,
1876
- taskMutation: input.taskMutation,
1877
- reason,
1878
- cwd: input.cwd,
1879
- env: input.env,
1880
- });
1915
+ await input.withTaskMutation(taskMutation =>
1916
+ finalizeWorkerMemoryGuardBlockedState({
1917
+ teamName: input.teamName,
1918
+ dir,
1919
+ worker,
1920
+ task,
1921
+ taskMutation,
1922
+ reason,
1923
+ cwd: input.cwd,
1924
+ env: input.env,
1925
+ }),
1926
+ );
1881
1927
  await appendWorkerMemoryGuardAction({
1882
1928
  dir,
1883
1929
  teamName: input.teamName,
@@ -2043,12 +2089,17 @@ async function applyWorkerMemoryGuardUnlocked(input: {
2043
2089
  }
2044
2090
 
2045
2091
  async function applyWorkerMemoryGuard(
2046
- input: Omit<Parameters<typeof applyWorkerMemoryGuardUnlocked>[0], "dir" | "taskMutation">,
2092
+ input: Omit<Parameters<typeof applyWorkerMemoryGuardUnlocked>[0], "dir" | "withTaskMutation">,
2047
2093
  ): Promise<Record<string, unknown>> {
2048
2094
  const dir = await findTeamDir(input.teamName, input.cwd, input.env);
2049
- return withGjcTeamTaskMutation(taskStore(dir), taskMutation =>
2050
- applyWorkerMemoryGuardUnlocked({ ...input, dir, taskMutation }),
2051
- );
2095
+ // Do not hold withGjcTeamTaskMutation across relaunchWorkerPaneForMemoryGuard's
2096
+ // startup-ack poll (default 120s). That fence would serialize concurrent
2097
+ // worker-startup-ack publication and hang selector-replacement under CI load.
2098
+ return applyWorkerMemoryGuardUnlocked({
2099
+ ...input,
2100
+ dir,
2101
+ withTaskMutation: fn => withGjcTeamTaskMutation(taskStore(dir), fn),
2102
+ });
2052
2103
  }
2053
2104
  async function readPhase(dir: string): Promise<GjcTeamPhase> {
2054
2105
  try {
@@ -2484,7 +2535,11 @@ function tagTmuxSessionAsGjcLeader(tmuxCommand: string, sessionName: string): bo
2484
2535
  return result.exitCode === 0;
2485
2536
  }
2486
2537
 
2487
- function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEnv): GjcTmuxLeaderContext {
2538
+ function readCurrentTmuxLeaderContext(
2539
+ tmuxCommand: string,
2540
+ env: NodeJS.ProcessEnv,
2541
+ adoptUnmanagedSession = true,
2542
+ ): GjcTmuxLeaderContext {
2488
2543
  if (Bun.which(tmuxCommand) === null)
2489
2544
  throw new Error(buildTeamTmuxLeaderRequirementMessage(`tmux_not_installed:${tmuxCommand}`));
2490
2545
  // Prefer the explicit GJC-managed session name propagated by `gjc --tmux`
@@ -2517,7 +2572,7 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
2517
2572
  const [sessionName = "", windowIndex = ""] = sessionAndWindow.split(":");
2518
2573
  if (!sessionName || !windowIndex || !leaderPaneId.startsWith("%"))
2519
2574
  throw new Error(buildTeamTmuxLeaderRequirementMessage(`invalid_tmux_context:${result.stdout.toString().trim()}`));
2520
- if (readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE) {
2575
+ if (adoptUnmanagedSession && readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE) {
2521
2576
  // Adopt any real tmux leader as a GJC team leader — including a session
2522
2577
  // the user created outside `gjc --tmux` — by writing GJC's @gjc-profile
2523
2578
  // ownership tag and reading it back. A provider that round-trips tmux
@@ -2540,6 +2595,20 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
2540
2595
  target: `${sessionName}:${windowIndex}`,
2541
2596
  };
2542
2597
  }
2598
+ /**
2599
+ * Check whether the current process can launch a team without changing tmux state.
2600
+ * Unlike the launch path, this never adopts or tags an unmanaged tmux session.
2601
+ */
2602
+ export function probeGjcTeamAvailability(
2603
+ env: NodeJS.ProcessEnv = process.env,
2604
+ ): { available: true } | { available: false; reason: string } {
2605
+ try {
2606
+ readCurrentTmuxLeaderContext(resolveGjcTmuxCommand(env), env, false);
2607
+ return { available: true };
2608
+ } catch (error) {
2609
+ return { available: false, reason: error instanceof Error ? error.message : String(error) };
2610
+ }
2611
+ }
2543
2612
  function isBunVirtualPath(candidate: string | undefined): boolean {
2544
2613
  const normalized = candidate?.trim().replace(/\\/g, "/").toLowerCase();
2545
2614
  return (
@@ -2718,7 +2787,7 @@ async function startTmuxSession(
2718
2787
  const splitDirection: string = worker.index === 1 ? "-h" : "-v";
2719
2788
  const splitTarget: string =
2720
2789
  worker.index === 1 ? config.tmux_target : (rightStackRootPaneId ?? config.tmux_target);
2721
- const workerCommand = buildWorkerCommand(config, worker);
2790
+ const workerCommand = buildWorkerCommand(config, worker, process.platform, undefined, env);
2722
2791
  const workerCwd = worker.worktree_path ?? config.leader.cwd;
2723
2792
  const useSendKeysFallback = shouldDispatchWorkerWithSendKeys(config.tmux_command);
2724
2793
  const splitArgs = [
@@ -4247,11 +4316,13 @@ function parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallbackMs: numb
4247
4316
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallbackMs;
4248
4317
  }
4249
4318
 
4250
- function parseHeartbeatStaleMs(env: NodeJS.ProcessEnv): number {
4319
+ /** Positive stale windows are clamped so a worker can publish strictly before expiry. */
4320
+ export function parseHeartbeatStaleMs(env: NodeJS.ProcessEnv): number {
4251
4321
  const raw = env.GJC_TEAM_HEARTBEAT_STALE_MS?.trim();
4252
4322
  if (!raw) return 120_000;
4253
4323
  const parsed = Number(raw);
4254
- return Number.isFinite(parsed) ? parsed : 120_000;
4324
+ if (!Number.isFinite(parsed)) return 120_000;
4325
+ return parsed > 0 ? Math.max(3, parsed) : parsed;
4255
4326
  }
4256
4327
  async function writeLifecycleNudge(
4257
4328
  dir: string,
@@ -4751,6 +4822,41 @@ export async function updateGjcWorkerHeartbeat(
4751
4822
  updateWorkerHeartbeat(workerRuntime, teamName, worker, heartbeat, cwd, env),
4752
4823
  );
4753
4824
  }
4825
+ /**
4826
+ * Refresh liveness without discarding metadata written by other heartbeat
4827
+ * producers. The read/merge/write sequence shares the team mutation fence with
4828
+ * CLI heartbeat updates, so a runtime tick cannot overwrite a newer turn count.
4829
+ */
4830
+ export async function refreshGjcWorkerHeartbeat(
4831
+ teamName: string,
4832
+ worker: string,
4833
+ pid: number,
4834
+ cwd = process.cwd(),
4835
+ env: NodeJS.ProcessEnv = process.env,
4836
+ ): Promise<WorkerHeartbeatFile> {
4837
+ const dir = await findTeamDir(teamName, cwd, env);
4838
+ return withGjcTeamMutationFence(dir, async () => {
4839
+ const current = await readWorkerHeartbeat(workerRuntime, teamName, worker, cwd, env);
4840
+ const processStartTime = await readLinuxProcessStartTime(pid);
4841
+ const turnCount = current?.turn_count;
4842
+ return updateWorkerHeartbeat(
4843
+ workerRuntime,
4844
+ teamName,
4845
+ worker,
4846
+ {
4847
+ ...current,
4848
+ pid,
4849
+ last_turn_at: now(),
4850
+ turn_count: Number.isSafeInteger(turnCount) && (turnCount ?? -1) >= 0 ? (turnCount ?? 0) : 0,
4851
+ alive: true,
4852
+ // Never carry another process's incarnation across a pid change.
4853
+ process_start_time: processStartTime ?? (current?.pid === pid ? current.process_start_time : undefined),
4854
+ },
4855
+ cwd,
4856
+ env,
4857
+ );
4858
+ });
4859
+ }
4754
4860
  export async function writeGjcWorkerInbox(
4755
4861
  teamName: string,
4756
4862
  worker: string,
@@ -4896,12 +5002,13 @@ export async function executeGjcTeamApiOperation(
4896
5002
  cwd = process.cwd(),
4897
5003
  env: NodeJS.ProcessEnv = process.env,
4898
5004
  ): Promise<unknown> {
5005
+ const resolvedOperation = resolveGjcTeamApiOperation(operation);
4899
5006
  const teamName = String(input.team_name ?? input.teamName ?? "").trim();
4900
5007
  if (!teamName) throw new Error("missing_team_name");
4901
5008
  const workerInput = input.worker ?? input.worker_id ?? input.workerId;
4902
5009
  const worker = String(workerInput ?? "worker-1");
4903
5010
  const explicitWorker = workerInput == null ? undefined : String(workerInput);
4904
- switch (operation) {
5011
+ switch (resolvedOperation) {
4905
5012
  case "list-tasks":
4906
5013
  return { tasks: await listGjcTeamTasks(teamName, cwd, env) };
4907
5014
  case "read-task":
@@ -5247,7 +5354,8 @@ export async function executeGjcTeamApiOperation(
5247
5354
  case "read-shutdown-ack":
5248
5355
  return readGjcShutdownAck(teamName, worker, cwd, env);
5249
5356
  default:
5250
- throw new Error(`unknown_team_api_operation:${operation}`);
5357
+ resolvedOperation satisfies never;
5358
+ throw new UnknownGjcTeamApiOperationError(operation, []);
5251
5359
  }
5252
5360
  }
5253
5361
 
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Runtime-owned `gjc team` worker heartbeat.
3
+ *
4
+ * Worker liveness used to be published only when the model remembered to call
5
+ * `gjc team api update-worker-heartbeat` between turns. A worker inside a
6
+ * single tool call longer than `GJC_TEAM_HEARTBEAT_STALE_MS` (default 120s) was
7
+ * therefore reported as stale and had its task claim requeued mid-flight — even
8
+ * though the claim lease it invalidated is 30 minutes long. This module makes
9
+ * the worker process itself publish liveness while a turn is in flight, so the
10
+ * heartbeat records what the process is doing instead of what the model
11
+ * remembered to report.
12
+ *
13
+ * The published record carries the writer's real pid, so leader-side diagnostics
14
+ * name the process that is actually working rather than a placeholder.
15
+ */
16
+ import { logger } from "@gajae-code/utils";
17
+ import { parseHeartbeatStaleMs, refreshGjcWorkerHeartbeat, type WorkerHeartbeatFile } from "./team-runtime";
18
+
19
+ /** Publish several times per stale window so one missed tick cannot cross it. */
20
+ const HEARTBEAT_INTERVAL_DIVISOR = 3;
21
+ const MIN_HEARTBEAT_INTERVAL_MS = 1;
22
+ const MAX_HEARTBEAT_INTERVAL_MS = 30_000;
23
+
24
+ export interface GjcTeamWorkerIdentity {
25
+ teamName: string;
26
+ workerId: string;
27
+ }
28
+
29
+ /** Resolves the team identity injected into a worker pane by `gjc team` startup. */
30
+ export function resolveGjcTeamWorkerIdentity(env: NodeJS.ProcessEnv = process.env): GjcTeamWorkerIdentity | undefined {
31
+ const teamName = env.GJC_TEAM_NAME?.trim();
32
+ const workerId = env.GJC_TEAM_WORKER_ID?.trim() || env.GJC_TEAM_INTERNAL_WORKER?.split("/").pop()?.trim();
33
+ return teamName && workerId ? { teamName, workerId } : undefined;
34
+ }
35
+
36
+ /** Refresh cadence derived from the leader's stale window; `0` disables publishing. */
37
+ export function resolveGjcTeamWorkerHeartbeatIntervalMs(env: NodeJS.ProcessEnv = process.env): number {
38
+ const staleMs = parseHeartbeatStaleMs(env);
39
+ if (staleMs <= 0) return 0;
40
+ return Math.min(
41
+ MAX_HEARTBEAT_INTERVAL_MS,
42
+ Math.max(MIN_HEARTBEAT_INTERVAL_MS, Math.floor(staleMs / HEARTBEAT_INTERVAL_DIVISOR)),
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Publish one runtime-owned heartbeat for the worker this process is running as.
48
+ * Returns `undefined` when the process is not a team worker.
49
+ *
50
+ * `turn_count` and process-incarnation metadata are merged under the same
51
+ * mutation fence used by CLI heartbeat updates.
52
+ */
53
+ export async function writeGjcTeamWorkerRuntimeHeartbeat(
54
+ cwd: string = process.cwd(),
55
+ env: NodeJS.ProcessEnv = process.env,
56
+ ): Promise<WorkerHeartbeatFile | undefined> {
57
+ const identity = resolveGjcTeamWorkerIdentity(env);
58
+ if (!identity) return undefined;
59
+ return refreshGjcWorkerHeartbeat(identity.teamName, identity.workerId, process.pid, cwd, env);
60
+ }
61
+
62
+ export interface GjcTeamWorkerHeartbeatReporterOptions {
63
+ write: () => Promise<void>;
64
+ intervalMs: number;
65
+ }
66
+
67
+ /**
68
+ * Publishes a worker heartbeat while a turn is in flight.
69
+ *
70
+ * Writes never overlap; a slow or failing write is logged and skipped rather
71
+ * than queued, because only the most recent record matters. The timer is
72
+ * unreferenced so it can never hold the process open.
73
+ */
74
+ export class GjcTeamWorkerHeartbeatReporter {
75
+ readonly #write: () => Promise<void>;
76
+ readonly #intervalMs: number;
77
+ #timer: NodeJS.Timeout | undefined;
78
+ #inFlight: Promise<void> | undefined;
79
+ #disposed = false;
80
+
81
+ constructor(options: GjcTeamWorkerHeartbeatReporterOptions) {
82
+ this.#write = options.write;
83
+ this.#intervalMs = options.intervalMs;
84
+ }
85
+
86
+ /** Reporter for the current process, or `undefined` when it is not a team worker. */
87
+ static forProcess(
88
+ cwd: () => string,
89
+ env: NodeJS.ProcessEnv = process.env,
90
+ ): GjcTeamWorkerHeartbeatReporter | undefined {
91
+ if (!resolveGjcTeamWorkerIdentity(env)) return undefined;
92
+ const intervalMs = resolveGjcTeamWorkerHeartbeatIntervalMs(env);
93
+ if (intervalMs <= 0) return undefined;
94
+ return new GjcTeamWorkerHeartbeatReporter({
95
+ intervalMs,
96
+ write: async () => {
97
+ await writeGjcTeamWorkerRuntimeHeartbeat(cwd(), env);
98
+ },
99
+ });
100
+ }
101
+
102
+ get isRunning(): boolean {
103
+ return this.#timer !== undefined;
104
+ }
105
+
106
+ /** Publish immediately, then keep publishing until {@link stop} or {@link dispose}. */
107
+ start(): void {
108
+ if (this.#disposed || this.#timer) return;
109
+ this.#publish();
110
+ this.#timer = setInterval(() => this.#publish(), this.#intervalMs);
111
+ this.#timer.unref?.();
112
+ }
113
+
114
+ /** Stop periodic publishing after one final record, so the idle stamp is fresh. */
115
+ stop(): void {
116
+ if (!this.#timer) return;
117
+ clearInterval(this.#timer);
118
+ this.#timer = undefined;
119
+ this.#publish();
120
+ }
121
+
122
+ /** Await the write in flight, if any. */
123
+ async flush(): Promise<void> {
124
+ while (this.#inFlight) {
125
+ await this.#inFlight;
126
+ }
127
+ }
128
+
129
+ /** Permanently stop publishing. Idempotent. */
130
+ dispose(): void {
131
+ this.#disposed = true;
132
+ if (this.#timer) {
133
+ clearInterval(this.#timer);
134
+ this.#timer = undefined;
135
+ }
136
+ }
137
+
138
+ #publish(): void {
139
+ if (this.#disposed || this.#inFlight) return;
140
+ const run = this.#write()
141
+ .catch((error: unknown) => {
142
+ logger.warn("GJC team worker heartbeat publish failed", { error: String(error) });
143
+ })
144
+ .finally(() => {
145
+ if (this.#inFlight === run) this.#inFlight = undefined;
146
+ });
147
+ this.#inFlight = run;
148
+ }
149
+ }
@@ -110,6 +110,16 @@ export interface TmuxServerProof {
110
110
  startTime?: string;
111
111
  cgroup?: CgroupInfo;
112
112
  sessionNames?: string[];
113
+ /**
114
+ * Whether `pid` is kernel-proved evidence about the live tmux server.
115
+ *
116
+ * Only Linux can prove a server PID (`/proc` + cgroup classification), so
117
+ * probes on other platforms may report a placeholder PID with this flag set
118
+ * to `false`. Guards must not build a `#{pid}` predicate from an unproven
119
+ * PID: no live tmux server can ever satisfy it. Omitted means proven, which
120
+ * keeps every probe that reads a real `#{pid}` unchanged.
121
+ */
122
+ pidProven?: boolean;
113
123
  }
114
124
 
115
125
  export interface PlanRequest {
@@ -407,6 +407,7 @@ export function createGjcTmuxSession(
407
407
  pid: 1,
408
408
  startTime: "not-applicable",
409
409
  cgroup: { classification: "not_applicable" },
410
+ pidProven: false,
410
411
  };
411
412
  }
412
413
  const result = Bun.spawnSync([tmuxCommand, "display-message", "-p", "#{pid}"], {
@@ -534,6 +535,7 @@ export function createGjcTmuxSession(
534
535
  env,
535
536
  server.pid,
536
537
  server.startTime,
538
+ server.pidProven,
537
539
  );
538
540
  },
539
541
  });
@@ -551,7 +553,7 @@ export function createGjcTmuxSession(
551
553
  tagCreatedTmuxSession(
552
554
  nativeSessionId,
553
555
  sessionName,
554
- outcome.server_pid,
556
+ { pid: outcome.server_pid, pidProven: server.pidProven },
555
557
  env,
556
558
  {
557
559
  sessionId,
@@ -571,6 +573,7 @@ export function createGjcTmuxSession(
571
573
  env,
572
574
  outcome.server_pid,
573
575
  outcome.server_start_time,
576
+ server.pidProven,
574
577
  );
575
578
  } catch (cleanupError) {
576
579
  throw new AggregateError([tagError, cleanupError], "gjc_tmux_profile_tag_failed_cleanup_failed");
@@ -597,6 +600,7 @@ export function createGjcTmuxSession(
597
600
  env,
598
601
  outcome.server_pid,
599
602
  outcome.server_start_time,
603
+ finalServer.pidProven,
600
604
  );
601
605
  } catch (cleanupError) {
602
606
  throw new AggregateError(
@@ -619,6 +623,7 @@ export function createGjcTmuxSession(
619
623
  env,
620
624
  outcome.server_pid,
621
625
  outcome.server_start_time,
626
+ server.pidProven,
622
627
  );
623
628
  } catch (cleanupError) {
624
629
  throw new AggregateError(
@@ -645,8 +650,18 @@ function tmuxCommandArgument(value: string): string {
645
650
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
646
651
  }
647
652
 
653
+ /**
654
+ * Build the guard predicate for an exact tmux session mutation.
655
+ *
656
+ * The `#{pid}` clause is emitted only when the server proof actually proved a
657
+ * PID. Non-Linux probes report a placeholder PID (see {@link TmuxServerProof}),
658
+ * and pinning `#{pid}` to a placeholder produces a predicate no live tmux
659
+ * server can satisfy, which refuses every mutation on those platforms. The
660
+ * remaining clauses still pin the exact session id, session name and owner
661
+ * generation.
662
+ */
648
663
  function guardedTmuxSessionPredicate(
649
- expectedPid: number,
664
+ expectedServer: { pid: number; pidProven?: boolean },
650
665
  nativeSessionId: string,
651
666
  sessionName: string,
652
667
  expectedOwnerGeneration?: string,
@@ -654,13 +669,14 @@ function guardedTmuxSessionPredicate(
654
669
  const ownerGenerationPredicate = expectedOwnerGeneration
655
670
  ? `#{==:#{${GJC_TMUX_OWNER_GENERATION_OPTION}},${expectedOwnerGeneration}}`
656
671
  : "1";
657
- return `#{&&:#{==:#{pid},${expectedPid}},#{&&:#{==:#{session_id},${nativeSessionId}},#{&&:#{==:#{session_name},${sessionName}},${ownerGenerationPredicate}}}}`;
672
+ const serverPidPredicate = expectedServer.pidProven === false ? "1" : `#{==:#{pid},${expectedServer.pid}}`;
673
+ return `#{&&:${serverPidPredicate},#{&&:#{==:#{session_id},${nativeSessionId}},#{&&:#{==:#{session_name},${sessionName}},${ownerGenerationPredicate}}}}`;
658
674
  }
659
675
 
660
676
  function runGuardedTmuxSessionCommand(
661
677
  nativeSessionId: string,
662
678
  sessionName: string,
663
- expectedPid: number,
679
+ expectedServer: { pid: number; pidProven?: boolean },
664
680
  env: NodeJS.ProcessEnv,
665
681
  thenCommand: string,
666
682
  expectedOwnerGeneration?: string,
@@ -671,7 +687,7 @@ function runGuardedTmuxSessionCommand(
671
687
  "-t",
672
688
  normalizeExactTmuxTarget(nativeSessionId, env, "session"),
673
689
  "-F",
674
- guardedTmuxSessionPredicate(expectedPid, nativeSessionId, sessionName, expectedOwnerGeneration),
690
+ guardedTmuxSessionPredicate(expectedServer, nativeSessionId, sessionName, expectedOwnerGeneration),
675
691
  `${thenCommand} ; display-message -p __gjc_tmux_guarded_mutation_ok__`,
676
692
  "display-message -p __gjc_tmux_guarded_mutation_refused__",
677
693
  ],
@@ -683,7 +699,7 @@ function runGuardedTmuxSessionCommand(
683
699
  function tagCreatedTmuxSession(
684
700
  nativeSessionId: string,
685
701
  sessionName: string,
686
- expectedPid: number,
702
+ expectedServer: { pid: number; pidProven?: boolean },
687
703
  env: NodeJS.ProcessEnv,
688
704
  metadata: {
689
705
  branch?: string | null;
@@ -701,7 +717,7 @@ function tagCreatedTmuxSession(
701
717
  const commands = buildGjcTmuxProfileCommands(target, env, metadata, { tmuxCommand })
702
718
  .map(command => command.args.map(tmuxCommandArgument).join(" "))
703
719
  .join(" ; ");
704
- runGuardedTmuxSessionCommand(nativeSessionId, sessionName, expectedPid, env, commands);
720
+ runGuardedTmuxSessionCommand(nativeSessionId, sessionName, expectedServer, env, commands);
705
721
  }
706
722
 
707
723
  function cleanupExactCreatedTmuxSession(
@@ -711,6 +727,7 @@ function cleanupExactCreatedTmuxSession(
711
727
  env: NodeJS.ProcessEnv,
712
728
  expectedPid: number,
713
729
  expectedStartTime: string,
730
+ expectedPidProven?: boolean,
714
731
  ): void {
715
732
  const server = requireSafeTmuxServerForMutation(tmuxCommand, env);
716
733
  if (server.pid !== expectedPid || server.startTime !== expectedStartTime)
@@ -718,7 +735,7 @@ function cleanupExactCreatedTmuxSession(
718
735
  runGuardedTmuxSessionCommand(
719
736
  nativeSessionId,
720
737
  sessionName,
721
- expectedPid,
738
+ { pid: expectedPid, pidProven: expectedPidProven ?? server.pidProven },
722
739
  env,
723
740
  `kill-session -t ${tmuxCommandArgument(normalizeExactTmuxTarget(nativeSessionId, env, "session"))}`,
724
741
  );
@@ -727,7 +744,7 @@ function cleanupExactCreatedTmuxSession(
727
744
  function requireSafeTmuxServerForMutation(
728
745
  tmuxCommand: string,
729
746
  env: NodeJS.ProcessEnv,
730
- ): { pid: number; startTime: string } {
747
+ ): { pid: number; startTime: string; pidProven?: boolean } {
731
748
  if (mutationServerProofTestDependency) {
732
749
  const proof = mutationServerProofTestDependency(tmuxCommand, env);
733
750
  if (
@@ -739,7 +756,7 @@ function requireSafeTmuxServerForMutation(
739
756
  return proof as { pid: number; startTime: string };
740
757
  return { pid: 1, startTime: "test" };
741
758
  }
742
- if (process.platform !== "linux") return { pid: 1, startTime: "not-applicable" };
759
+ if (process.platform !== "linux") return { pid: 1, startTime: "not-applicable", pidProven: false };
743
760
  const result = Bun.spawnSync([tmuxCommand, "display-message", "-p", "#{pid}"], {
744
761
  stdout: "pipe",
745
762
  stderr: "pipe",
@@ -953,7 +970,7 @@ export function removeGjcTmuxSession(
953
970
  runGuardedTmuxSessionCommand(
954
971
  nativeSessionId,
955
972
  session.name,
956
- finalServer.pid,
973
+ finalServer,
957
974
  env,
958
975
  `kill-session -t '${nativeSessionId}'`,
959
976
  expectedIdentity?.ownerGeneration,
@@ -962,6 +979,13 @@ export function removeGjcTmuxSession(
962
979
  }
963
980
 
964
981
  async function readProcessStartTime(pid: number): Promise<string | null> {
982
+ // `/proc/<pid>/stat` only exists on Linux. Everywhere else the natives
983
+ // process reference carries the same kernel-derived start identity (macOS
984
+ // reports `darwin:<start_tvsec>:<start_tvusec>` from the BSD proc info), and
985
+ // callers only ever compare these values for equality against another value
986
+ // produced here. Returning null off Linux made every owner-identity proof
987
+ // unverifiable, so no session could be closed there.
988
+ if (process.platform !== "linux") return Process.fromPid(pid)?.incarnation ?? null;
965
989
  return readLinuxProcStartTime(pid);
966
990
  }
967
991
 
@@ -973,6 +997,27 @@ function exactManagedOwnerSupervisor(supervisorPid: number, supervisorStartTime:
973
997
  return supervisor;
974
998
  }
975
999
 
1000
+ /**
1001
+ * Deliver SIGTERM to exactly one already-proved owner PID.
1002
+ *
1003
+ * `Process.signalRoot` routes through the owned pidfd on Linux and the owned
1004
+ * process handle on Windows, so PID reuse cannot redirect the signal. macOS has
1005
+ * no equivalent kernel authority and the native binding deliberately fails
1006
+ * closed there, which would leave every macOS force-close unable to signal its
1007
+ * owner at all. On that platform the caller has already re-proved the PID's
1008
+ * start-time incarnation immediately before this call — the same evidence the
1009
+ * native macOS signal path re-validates — so deliver to that exact PID.
1010
+ */
1011
+ function signalManagedOwnerTerm(supervisor: Process, pid: number): boolean {
1012
+ if (process.platform !== "darwin") return supervisor.signalRoot(15);
1013
+ try {
1014
+ process.kill(pid, "SIGTERM");
1015
+ return true;
1016
+ } catch {
1017
+ return false;
1018
+ }
1019
+ }
1020
+
976
1021
  async function readCurrentGeneration(stateDir: string, sessionId: string): Promise<string | null> {
977
1022
  try {
978
1023
  const value: unknown = JSON.parse(
@@ -1185,7 +1230,7 @@ export async function forceCloseGjcTmuxSession(
1185
1230
  deps.signalTerm(pid);
1186
1231
  } else {
1187
1232
  const supervisor = exactManagedOwnerSupervisor(pid, identity.startTime);
1188
- if (!supervisor.signalRoot(15)) throw new Error("managed_owner_supervisor_signal_failed");
1233
+ if (!signalManagedOwnerTerm(supervisor, pid)) throw new Error("managed_owner_supervisor_signal_failed");
1189
1234
  operatorVerdict = supervisor
1190
1235
  .waitForExit({ timeoutMs: FORCE_CLOSE_VERDICT_TIMEOUT_MS - 500 })
1191
1236
  .then(async exited => {
@@ -1227,7 +1272,7 @@ export async function forceCloseGjcTmuxSession(
1227
1272
  runGuardedTmuxSessionCommand(
1228
1273
  nativeSessionId,
1229
1274
  session.name,
1230
- initialServer.pid,
1275
+ initialServer,
1231
1276
  env,
1232
1277
  `kill-session -t '${nativeSessionId}'`,
1233
1278
  identity.generation,