@gajae-code/coding-agent 0.15.5 → 0.16.0

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 (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. package/src/web/search/types.ts +16 -0
@@ -0,0 +1,100 @@
1
+ import { getAgentDir } from "@gajae-code/utils";
2
+ import {
3
+ getModelPresetRegistryStatus,
4
+ type ModelPresetRegistryStatus,
5
+ refreshModelPresetRegistry,
6
+ rollbackModelPresetRegistry,
7
+ setModelPresetRegistryDisabled,
8
+ setModelPresetRegistryPin,
9
+ } from "../config/model-preset-registry";
10
+
11
+ export const MODEL_PRESETS_ACTIONS = ["status", "refresh", "rollback", "pin", "unpin", "disable", "enable"] as const;
12
+ export type ModelPresetsAction = (typeof MODEL_PRESETS_ACTIONS)[number];
13
+
14
+ export interface ModelPresetsCommandArgs {
15
+ action: ModelPresetsAction;
16
+ revision?: string;
17
+ json?: boolean;
18
+ agentDir?: string;
19
+ }
20
+
21
+ function parseRevision(value: string | undefined, action: "rollback" | "pin"): number {
22
+ if (!value || !/^\d+$/.test(value)) throw new Error(`${action} requires a positive accepted revision.`);
23
+ const revision = Number(value);
24
+ if (!Number.isSafeInteger(revision) || revision <= 0)
25
+ throw new Error(`${action} requires a positive accepted revision.`);
26
+ return revision;
27
+ }
28
+
29
+ function humanStatus(status: ModelPresetRegistryStatus): string {
30
+ const lines = [
31
+ `source: ${status.source}`,
32
+ `cache: ${status.cacheHealth}`,
33
+ `contract: ${status.contractVersion}`,
34
+ `active revision: ${status.activeRevisionId ?? status.activeRevision ?? "none"}`,
35
+ `highest seen revision: ${status.highestSeenRevision ?? "none"}`,
36
+ `pinned revision: ${status.pinnedRevision ?? "none"}`,
37
+ `disabled: ${status.disabled ? "yes" : "no"}`,
38
+ `profiles: ${status.profileCount}`,
39
+ `model presets: ${status.presetCount}`,
40
+ `history: ${status.historyRevisions.length > 0 ? status.historyRevisions.join(", ") : "none"}`,
41
+ ];
42
+ if (status.manifestSha256) lines.push(`manifest sha256: ${status.manifestSha256}`);
43
+ if (status.snapshotSha256) lines.push(`snapshot sha256: ${status.snapshotSha256}`);
44
+ if (status.profilesSha256) lines.push(`profiles sha256: ${status.profilesSha256}`);
45
+ if (status.presetsSha256) lines.push(`presets sha256: ${status.presetsSha256}`);
46
+ if (status.keyId) lines.push(`signature key: ${status.keyId}`);
47
+ if (status.sourceRevision) lines.push(`source revision: ${status.sourceRevision}`);
48
+ if (status.acceptedAt) lines.push(`accepted: ${status.acceptedAt}`);
49
+ if (status.lastCheckedAt) lines.push(`last checked: ${status.lastCheckedAt}`);
50
+ if (status.retainedProfiles.length > 0)
51
+ lines.push(`retained removed profiles: ${status.retainedProfiles.join(", ")}`);
52
+ if (status.retainedPresets.length > 0) lines.push(`retained removed presets: ${status.retainedPresets.join(", ")}`);
53
+ if (status.lastError) lines.push(`last error: ${status.lastError}`);
54
+ return `${lines.join("\n")}\n`;
55
+ }
56
+
57
+ function writeStatus(status: ModelPresetRegistryStatus, json: boolean): void {
58
+ process.stdout.write(json ? `${JSON.stringify(status)}\n` : humanStatus(status));
59
+ }
60
+
61
+ export async function runModelPresetsCommand(args: ModelPresetsCommandArgs): Promise<void> {
62
+ const agentDir = args.agentDir ?? getAgentDir();
63
+ const dependencies = { agentDir };
64
+ switch (args.action) {
65
+ case "status":
66
+ writeStatus(getModelPresetRegistryStatus(dependencies), args.json === true);
67
+ return;
68
+ case "refresh": {
69
+ const result = await refreshModelPresetRegistry(dependencies);
70
+ if (args.json) {
71
+ process.stdout.write(`${JSON.stringify({ result, status: getModelPresetRegistryStatus(dependencies) })}\n`);
72
+ } else {
73
+ process.stdout.write(
74
+ result.status === "updated"
75
+ ? `Accepted preset registry revision ${result.revision}.\n`
76
+ : result.status === "not_modified"
77
+ ? "Preset registry is already current.\n"
78
+ : "Preset registry refresh is disabled.\n",
79
+ );
80
+ }
81
+ return;
82
+ }
83
+ case "rollback":
84
+ await rollbackModelPresetRegistry({ ...dependencies, revision: parseRevision(args.revision, "rollback") });
85
+ break;
86
+ case "pin":
87
+ await setModelPresetRegistryPin({ ...dependencies, revision: parseRevision(args.revision, "pin") });
88
+ break;
89
+ case "unpin":
90
+ await setModelPresetRegistryPin(dependencies);
91
+ break;
92
+ case "disable":
93
+ await setModelPresetRegistryDisabled({ ...dependencies, disabled: true });
94
+ break;
95
+ case "enable":
96
+ await setModelPresetRegistryDisabled({ ...dependencies, disabled: false });
97
+ break;
98
+ }
99
+ writeStatus(getModelPresetRegistryStatus(dependencies), args.json === true);
100
+ }
@@ -73,6 +73,11 @@ export const ROOT_LAUNCH_FLAGS = {
73
73
  optionalValue: true,
74
74
  }),
75
75
  "no-title": Flags.boolean({ description: "Disable title auto-generation" }),
76
+ master: Flags.boolean({ description: "Launch an interactive master session" }),
77
+ scope: Flags.string({
78
+ description: "Master scope: repo (default), pwd, or global",
79
+ options: ["repo", "pwd", "global"],
80
+ }),
76
81
  };
77
82
 
78
83
  export type ConsumerLaunchFlagName = "hook" | "extension" | "no-extensions" | "no-skills" | "skills";
@@ -9,7 +9,7 @@ import * as fs from "node:fs";
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
11
  import { pipeline } from "node:stream/promises";
12
- import { $which, APP_NAME, isCompiledBinary, isEnoent, VERSION } from "@gajae-code/utils";
12
+ import { $which, APP_NAME, isCompiledBinary, isEnoent, redactCrashSecrets, VERSION } from "@gajae-code/utils";
13
13
  import { $ } from "bun";
14
14
  import chalk from "chalk";
15
15
  import { Settings } from "../config/settings";
@@ -17,6 +17,8 @@ import { isUpdateChannel, UPDATE_CHANNELS, type UpdateChannel } from "../config/
17
17
  import { installDefaultGjcDefinitions } from "../defaults/gjc-defaults";
18
18
  import { theme } from "../modes/theme/theme";
19
19
  import { getNotificationConfig, type NotificationProvider, resolveNotificationProvider } from "../sdk/bus/config";
20
+ import type { TelemetryDetails, TelemetryEventName } from "../telemetry";
21
+ import { recordTelemetryEvent } from "../telemetry";
20
22
  import { runDaemonCommand } from "./daemon-cli";
21
23
  import {
22
24
  fetchGithubChannelRelease,
@@ -53,6 +55,7 @@ export interface InstalledVersionVerification {
53
55
  ok: boolean;
54
56
  actual?: string;
55
57
  path?: string;
58
+ versionOutput?: string;
56
59
  smokeTestFailed?: boolean;
57
60
  smokeTestOutput?: string;
58
61
  cleanupWarning?: string;
@@ -460,6 +463,71 @@ export function parseReportedVersionForTest(output: string): string | undefined
460
463
  return parseReportedVersion(output);
461
464
  }
462
465
 
466
+ const VERIFICATION_OUTPUT_MAX_LENGTH = 512;
467
+ const ANSI_ESCAPE = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/gu;
468
+ const UNSAFE_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
469
+
470
+ function sanitizeVerificationOutput(...streams: Array<string | undefined>): string | undefined {
471
+ const output = streams
472
+ .map(stream => stream?.replace(ANSI_ESCAPE, "").replace(UNSAFE_CONTROL, " ").replace(/\s+/g, " ").trim() ?? "")
473
+ .filter(Boolean)
474
+ .filter((stream, index, all) => all.indexOf(stream) === index)
475
+ .join(" ");
476
+ if (!output) return undefined;
477
+ const redacted = redactCrashSecrets(output);
478
+ if (redacted.length <= VERIFICATION_OUTPUT_MAX_LENGTH) return redacted;
479
+ return `${redacted.slice(0, VERIFICATION_OUTPUT_MAX_LENGTH - 3)}...`;
480
+ }
481
+
482
+ export function sanitizeVerificationOutputForTest(
483
+ stderr: string | undefined,
484
+ stdout: string | undefined,
485
+ ): string | undefined {
486
+ return sanitizeVerificationOutput(stderr, stdout);
487
+ }
488
+
489
+ interface InstalledVersionCommandResult {
490
+ exitCode: number | null;
491
+ stdout: string;
492
+ stderr: string;
493
+ }
494
+
495
+ type InstalledVersionCommandRunner = (runtimePath: string) => Promise<InstalledVersionCommandResult>;
496
+
497
+ async function verifyInstalledVersionWith(
498
+ expectedVersion: string,
499
+ runtimePath: string | undefined,
500
+ runVersion: InstalledVersionCommandRunner,
501
+ ): Promise<InstalledVersionVerification> {
502
+ if (!runtimePath) return { ok: false };
503
+ try {
504
+ const result = await runVersion(runtimePath);
505
+ if (result.exitCode !== 0) {
506
+ return {
507
+ ok: false,
508
+ path: runtimePath,
509
+ versionOutput: sanitizeVerificationOutput(result.stderr, result.stdout),
510
+ };
511
+ }
512
+ const actual = parseReportedVersion(result.stdout);
513
+ return { ok: actual === expectedVersion, actual, path: runtimePath };
514
+ } catch (error) {
515
+ return {
516
+ ok: false,
517
+ path: runtimePath,
518
+ versionOutput: sanitizeVerificationOutput(error instanceof Error ? error.message : String(error)),
519
+ };
520
+ }
521
+ }
522
+
523
+ export async function verifyInstalledVersionForTest(options: {
524
+ expectedVersion: string;
525
+ runtimePath: string | undefined;
526
+ runVersion: InstalledVersionCommandRunner;
527
+ }): Promise<InstalledVersionVerification> {
528
+ return await verifyInstalledVersionWith(options.expectedVersion, options.runtimePath, options.runVersion);
529
+ }
530
+
463
531
  /**
464
532
  * Run the resolved gjc binary and check if it reports the expected version.
465
533
  */
@@ -467,15 +535,14 @@ async function verifyInstalledVersion(
467
535
  expectedVersion: string,
468
536
  runtimePath: string | undefined = resolveGjcPath(),
469
537
  ): Promise<InstalledVersionVerification> {
470
- if (!runtimePath) return { ok: false };
471
- try {
472
- const result = await $`${runtimePath} --version`.quiet().nothrow();
473
- if (result.exitCode !== 0) return { ok: false, path: runtimePath };
474
- const actual = parseReportedVersion(result.text());
475
- return { ok: actual === expectedVersion, actual, path: runtimePath };
476
- } catch {
477
- return { ok: false, path: runtimePath };
478
- }
538
+ return await verifyInstalledVersionWith(expectedVersion, runtimePath, async resolvedPath => {
539
+ const result = await $`${resolvedPath} --version`.quiet().nothrow();
540
+ return {
541
+ exitCode: result.exitCode,
542
+ stdout: result.stdout.toString(),
543
+ stderr: result.stderr.toString(),
544
+ };
545
+ });
479
546
  }
480
547
 
481
548
  async function verifyInstalledRuntime(
@@ -663,7 +730,8 @@ function formatVerificationFailure(result: InstalledVersionVerification, expecte
663
730
  if (result.actual) {
664
731
  return `${APP_NAME} at ${result.path} still reports ${result.actual} (expected ${expectedVersion})`;
665
732
  }
666
- return `could not verify updated version${result.path ? ` at ${result.path}` : ""}`;
733
+ const outputSuffix = result.versionOutput ? `: ${result.versionOutput}` : "";
734
+ return `could not verify updated version${result.path ? ` at ${result.path}` : ""}${outputSuffix}`;
667
735
  }
668
736
 
669
737
  export function formatVerificationFailureForTest(
@@ -1142,6 +1210,7 @@ export interface UpdateCommandDependencies {
1142
1210
  restartDaemon?: (settings: Settings) => Promise<void>;
1143
1211
  recoverNotifications?: (settings: Settings) => Promise<void>;
1144
1212
  runPostUpdateRecovery?: (runtimePath: string) => Promise<void>;
1213
+ recordTelemetryEvent?: (event: TelemetryEventName, details: TelemetryDetails) => unknown;
1145
1214
  exit?: (code: number) => never;
1146
1215
  }
1147
1216
 
@@ -1363,6 +1432,24 @@ export async function runUpdateCommand(
1363
1432
  const update = deps.performUpdate ?? performUpdate;
1364
1433
  const refreshDefaults = deps.refreshInstalledDefaultSkills ?? refreshInstalledDefaultSkills;
1365
1434
  const exit = deps.exit ?? process.exit;
1435
+ const recordEvent = deps.recordTelemetryEvent ?? ((event, details) => recordTelemetryEvent(event, details));
1436
+ const pendingTelemetry = new Set<Promise<void>>();
1437
+ const record = (event: TelemetryEventName, details: TelemetryDetails): void => {
1438
+ const result = recordEvent(event, details);
1439
+ if (result !== null && (typeof result === "object" || typeof result === "function") && "then" in result) {
1440
+ const pending = Promise.resolve(result as PromiseLike<unknown>).then(
1441
+ () => undefined,
1442
+ () => undefined,
1443
+ );
1444
+ pendingTelemetry.add(pending);
1445
+ void pending.finally(() => pendingTelemetry.delete(pending));
1446
+ }
1447
+ };
1448
+ const flushTelemetryBeforeExit = async (): Promise<never> => {
1449
+ await Promise.race([Promise.allSettled([...pendingTelemetry]), Bun.sleep(2000)]);
1450
+ return exit(1);
1451
+ };
1452
+ record("update_check_started", { channel });
1366
1453
 
1367
1454
  console.log(chalk.dim(`Current version: ${VERSION}`));
1368
1455
  if (channel !== "stable") {
@@ -1373,16 +1460,18 @@ export async function runUpdateCommand(
1373
1460
  try {
1374
1461
  target = await resolveTarget();
1375
1462
  } catch (err) {
1463
+ record("update_check_completed", { channel, result: "failed" });
1376
1464
  console.error(chalk.red(err instanceof Error ? err.message : String(err)));
1377
- return exit(1);
1465
+ return flushTelemetryBeforeExit();
1378
1466
  }
1379
1467
 
1380
1468
  let release: ReleaseInfo;
1381
1469
  try {
1382
1470
  release = await lookupRelease({ channel });
1383
1471
  } catch (err) {
1472
+ record("update_check_completed", { channel, result: "failed" });
1384
1473
  console.error(chalk.red(`Failed to check for updates: ${err}`));
1385
- return exit(1);
1474
+ return flushTelemetryBeforeExit();
1386
1475
  }
1387
1476
 
1388
1477
  // A config file that exists but could not be read changes which registry
@@ -1395,12 +1484,13 @@ export async function runUpdateCommand(
1395
1484
  try {
1396
1485
  comparison = compareVersions(release.version, VERSION);
1397
1486
  } catch (err) {
1487
+ record("update_check_completed", { channel, result: "failed" });
1398
1488
  console.error(
1399
1489
  chalk.red(
1400
1490
  `Failed to check for updates: the ${channel} channel reported an unparseable version "${release.version}": ${err instanceof Error ? err.message : String(err)}`,
1401
1491
  ),
1402
1492
  );
1403
- return exit(1);
1493
+ return flushTelemetryBeforeExit();
1404
1494
  }
1405
1495
 
1406
1496
  const decision = resolveUpdateDecision({
@@ -1416,7 +1506,10 @@ export async function runUpdateCommand(
1416
1506
  try {
1417
1507
  const verification = await verifyTarget(release, target.path);
1418
1508
  if (verification.ok) {
1509
+ record("update_check_completed", { channel, result: "available" });
1510
+ record("update_install_started", { channel, installMethod: target.method });
1419
1511
  printVerifiedMigrationTarget(target, release.version);
1512
+ record("update_install_completed", { channel, result: "installed", installMethod: target.method });
1420
1513
  return;
1421
1514
  }
1422
1515
  } finally {
@@ -1425,6 +1518,7 @@ export async function runUpdateCommand(
1425
1518
  }
1426
1519
 
1427
1520
  if (!decision.install) {
1521
+ record("update_check_completed", { channel, result: "up_to_date" });
1428
1522
  console.log(chalk.green(`${theme.status.success} Already up to date`));
1429
1523
  return;
1430
1524
  }
@@ -1439,9 +1533,14 @@ export async function runUpdateCommand(
1439
1533
  console.log(chalk.yellow(`Forcing reinstall of ${release.version}`));
1440
1534
  }
1441
1535
 
1442
- if (opts.check) return;
1536
+ record("update_check_completed", { channel, result: "available" });
1537
+ if (opts.check) {
1538
+ record("update_install_completed", { channel, result: "skipped" });
1539
+ return;
1540
+ }
1443
1541
 
1444
1542
  let installedVersion: string | undefined;
1543
+ record("update_install_started", { channel, installMethod: target.method });
1445
1544
  try {
1446
1545
  const resolved = target ?? (await resolveTarget());
1447
1546
  const verification = await update(resolved, release.version, release.registry);
@@ -1450,16 +1549,18 @@ export async function runUpdateCommand(
1450
1549
  await (deps.runPostUpdateRecovery ?? runPostUpdateRecovery)(verification.path);
1451
1550
  } else if (!deps.performUpdate) throw new Error("verified installed runtime path is unavailable");
1452
1551
  } catch (err) {
1552
+ record("update_install_failed", { channel, result: "failed", installMethod: target.method });
1453
1553
  const prefix = installedVersion
1454
1554
  ? `Updated to ${installedVersion}, but post-update recovery failed`
1455
1555
  : "Update failed";
1456
1556
  console.error(chalk.red(`${prefix}: ${err}`));
1457
- return exit(1);
1557
+ return flushTelemetryBeforeExit();
1458
1558
  }
1459
1559
 
1460
1560
  // The installed runtime completes recovery before this old updater process
1461
1561
  // refreshes opt-in local definitions, avoiding stale-module daemon control.
1462
1562
  await refreshDefaults();
1563
+ record("update_install_completed", { channel, result: "installed", installMethod: target.method });
1463
1564
  }
1464
1565
 
1465
1566
  /**
package/src/cli.ts CHANGED
@@ -54,6 +54,7 @@ export const commands: CommandEntry[] = [
54
54
  { name: "daemon", load: () => import("./commands/daemon").then(m => m.default) },
55
55
  { name: "web-search", aliases: ["q"], load: () => import("./commands/web-search").then(m => m.default) },
56
56
  { name: "local-provider", load: () => import("./commands/local-provider").then(m => m.default) },
57
+ { name: "model-presets", load: () => import("./commands/model-presets").then(m => m.default) },
57
58
  { name: "mcp-serve", load: () => import("./commands/mcp-serve").then(m => m.default) },
58
59
  { name: "mcp", load: () => import("./commands/mcp").then(m => m.default) },
59
60
  {
@@ -364,6 +365,7 @@ function routeLegacyRootArgv(argv: readonly string[]): string[] | undefined {
364
365
  export function routeModelsAlias(argv: readonly string[]): string[] | undefined {
365
366
  if (argv[0] !== "models") return undefined;
366
367
  const rest = argv.slice(1);
368
+ if (rest[0] === "presets") return ["model-presets", ...rest.slice(1)];
367
369
  if (rest.length === 0) return ["launch", "--list-models"];
368
370
  // Pure search tokens become a single fuzzy pattern (matches --list-models).
369
371
  if (rest.every(token => !token.startsWith("-") && !token.startsWith("@"))) {
@@ -461,7 +463,9 @@ export async function runCli(argv: string[]): Promise<void> {
461
463
  }
462
464
  const normalizedArgv = normalizeResumeAlias(argv);
463
465
  const legacyArgv = routeLegacyRootArgv(normalizedArgv);
464
- if (!legacyArgv && hasRootHelpFlag(normalizedArgv)) {
466
+ const modelPresetsArgv =
467
+ normalizedArgv[0] === "models" && normalizedArgv[1] === "presets" ? routeModelsAlias(normalizedArgv) : undefined;
468
+ if (!legacyArgv && !modelPresetsArgv && hasRootHelpFlag(normalizedArgv)) {
465
469
  const { renderRootHelp } = await import("@gajae-code/utils/cli");
466
470
  const { getExtraHelpText } = await import("./cli/fast-help");
467
471
  renderRootHelp({ bin: APP_NAME, version: VERSION, commands: new Map([["launch", RootHelpCommand]]) });
@@ -475,7 +479,7 @@ export async function runCli(argv: string[]): Promise<void> {
475
479
  process.stdout.write(`${APP_NAME}/${VERSION}\n`);
476
480
  return;
477
481
  }
478
- const runArgv = legacyArgv ?? routeRootArgv(normalizedArgv);
482
+ const runArgv = legacyArgv ?? modelPresetsArgv ?? routeRootArgv(normalizedArgv);
479
483
  if (isStatsHelpFastPath(runArgv)) {
480
484
  showStatsFastHelp();
481
485
  return;
@@ -22,24 +22,27 @@ export default class AuthGateway extends Command {
22
22
  };
23
23
 
24
24
  static flags = {
25
- json: Flags.boolean({ description: "Output JSON (token/status)" }),
25
+ json: Flags.boolean({ description: "Output JSON (token/status/check)" }),
26
26
  bind: Flags.string({ description: "Bind address for `serve` (host:port)", char: "b" }),
27
+ provider: Flags.string({
28
+ description: "Required provider scope for `serve`; also filters `status` and `check`",
29
+ }),
27
30
  regenerate: Flags.boolean({ description: "Regenerate the gateway bearer token (token)" }),
28
31
  "no-auth": Flags.boolean({
29
32
  description:
30
- "Disable inbound bearer-token auth (serve). Useful when bound to loopback — any caller is allowed.",
33
+ "Disable inbound bearer-token auth (serve). Loopback non-browser clients without an Origin header are allowed.",
31
34
  }),
32
35
  };
33
36
 
34
37
  static examples = [
35
- "# Boot the gateway against the configured broker\n gjc auth-gateway serve",
36
- "# Boot on a non-default port\n gjc auth-gateway serve --bind=127.0.0.1:4000",
38
+ "# Boot a provider-scoped gateway against the configured broker\n gjc auth-gateway serve --provider=openai-codex",
39
+ "# Boot on a non-default port\n gjc auth-gateway serve --provider=openai-codex --bind=127.0.0.1:4000",
37
40
  "# Print the gateway bearer token (creates one on first run)\n gjc auth-gateway token",
38
41
  "# Rotate the gateway bearer token\n gjc auth-gateway token --regenerate",
39
- "# Run on loopback without any bearer (anyone on this host can call)\n gjc auth-gateway serve --no-auth",
40
- "# Show local gateway + broker config status\n gjc auth-gateway status",
41
- "# Probe each broker credential to see which one is producing 401s\n gjc auth-gateway check",
42
- "# Same, machine-readable for scripts\n gjc auth-gateway check --json",
42
+ "# Run the scoped gateway on loopback for non-browser local clients without a bearer\n gjc auth-gateway serve --provider=openai-codex --no-auth",
43
+ "# Show scoped local gateway + broker config status\n gjc auth-gateway status --provider=openai-codex",
44
+ "# Probe credentials for one gateway scope\n gjc auth-gateway check --provider=openai-codex",
45
+ "# Same, machine-readable for scripts\n gjc auth-gateway check --provider=openai-codex --json",
43
46
  ];
44
47
 
45
48
  async run(): Promise<void> {
@@ -53,6 +56,7 @@ export default class AuthGateway extends Command {
53
56
  flags: {
54
57
  json: flags.json,
55
58
  bind: flags.bind,
59
+ provider: flags.provider,
56
60
  regenerate: flags.regenerate,
57
61
  noAuth: flags["no-auth"],
58
62
  },
@@ -20,6 +20,11 @@ function resolveDiskPolicyFromSettings(): Partial<GcDiskPolicy> | undefined {
20
20
  export default class Gc extends Command {
21
21
  static description = "Garbage-collect stale GJC session/PID records (dry-run by default)";
22
22
  static strict = false;
23
+ // The hand parser in gc-runtime owns the real syntax (repeatable space-form
24
+ // operands, dash-prefix rejection, orphan-operand rules); delegate help so the
25
+ // public output is the authoritative gcHelpText() instead of generic flag
26
+ // metadata that would advertise unsupported `--flag=<value>` forms.
27
+ static delegateHelp = true;
23
28
  static flags = {
24
29
  json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: false }),
25
30
  prune: Flags.boolean({ description: "Remove stale records (default: report only)", default: false }),
@@ -33,6 +38,18 @@ export default class Gc extends Command {
33
38
  description: "Quarantine a corrupt session-index suffix and retain its valid prefix",
34
39
  default: false,
35
40
  }),
41
+ "empty-delete-receipts": Flags.boolean({
42
+ description: "Report (and with --prune, prune) empty .gjc-delete-* receipts under --root/--manifest",
43
+ default: false,
44
+ }),
45
+ root: Flags.string({
46
+ description: "Operand root for --empty-delete-receipts (repeatable)",
47
+ multiple: true,
48
+ }),
49
+ manifest: Flags.string({
50
+ description: 'JSON {"roots":[...]} file for --empty-delete-receipts (repeatable)',
51
+ multiple: true,
52
+ }),
36
53
  };
37
54
 
38
55
  static examples = [
@@ -44,6 +61,8 @@ export default class Gc extends Command {
44
61
  "gjc gc --disk --json",
45
62
  "gjc gc --disk --prune",
46
63
  "gjc gc --repair-session-index --json",
64
+ "gjc gc --empty-delete-receipts --root ~/.gjc/agent/session-states",
65
+ "gjc gc --empty-delete-receipts --manifest receipts-manifest.json --prune --json",
47
66
  ];
48
67
 
49
68
  async run(): Promise<void> {
@@ -2,20 +2,30 @@
2
2
  * Root command for the coding agent CLI.
3
3
  */
4
4
 
5
- import { APP_NAME, setProjectDir } from "@gajae-code/utils";
5
+ import { APP_NAME, getAgentDir, setProjectDir } from "@gajae-code/utils";
6
6
  import { Args, Command } from "@gajae-code/utils/cli";
7
7
  import { assertLocalLaunchArgs, parseArgs } from "../cli/args";
8
8
  import { ROOT_LAUNCH_FLAGS } from "../cli/root-flags";
9
9
  import { writeCoordinatorAtomic } from "../coordinator-mcp/durability";
10
10
  import { launchDefaultTmuxIfNeeded } from "../gjc-runtime/launch-tmux";
11
- import { type PreparedLaunchWorktree, prepareLaunchWorktree } from "../gjc-runtime/launch-worktree";
11
+ import {
12
+ type GjcLaunchWorktreePlan,
13
+ type PreparedLaunchWorktree,
14
+ parseLaunchWorktreeMode,
15
+ planLaunchWorktree,
16
+ prepareLaunchWorktree,
17
+ } from "../gjc-runtime/launch-worktree";
18
+ import { type LaunchWorktreeReservation, reserveLaunchWorktree } from "../gjc-runtime/launch-worktree-reservation";
12
19
  import {
13
20
  GJC_COORDINATOR_SESSION_ID_ENV,
14
21
  GJC_COORDINATOR_SESSION_STATE_FILE_ENV,
15
22
  GJC_TMUX_OWNER_GENERATION_ENV,
16
23
  } from "../gjc-runtime/session-state-sidecar";
17
24
  import { runRootCommand } from "../main";
25
+ import { assertMasterLaunchArgs } from "../master-mode/context";
18
26
  import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
27
+ import { type IndexedSession, SessionIndex } from "../sdk/broker/session-index";
28
+ import { worktreeOccupant } from "../sdk/broker/worktree-occupancy";
19
29
 
20
30
  export async function persistCoordinatorLaunchFailure(
21
31
  error: unknown,
@@ -56,6 +66,54 @@ export async function persistCoordinatorLaunchFailure(
56
66
  await writeCoordinatorAtomic(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
57
67
  }
58
68
 
69
+ function worktreeInUseError(worktreePath: string, occupant?: string): Error {
70
+ const holder = occupant ? `session ${occupant}` : "another launch";
71
+ return new Error(
72
+ `worktree_in_use:${worktreePath} is already held by ${holder}. ` +
73
+ "Use gjc --worktree <name> for a separate checkout, or stop that session.",
74
+ );
75
+ }
76
+
77
+ async function reserveLaunchWorktreeOrFail(agentDir: string, worktreePath: string): Promise<LaunchWorktreeReservation> {
78
+ const reservation = await reserveLaunchWorktree(agentDir, worktreePath);
79
+ if (!reservation) throw worktreeInUseError(worktreePath);
80
+ return reservation;
81
+ }
82
+
83
+ /** Test seam for the path-keyed atomic reservation. */
84
+ export async function reserveLaunchWorktreeForTest(
85
+ agentDir: string,
86
+ worktreePath: string,
87
+ ): Promise<LaunchWorktreeReservation> {
88
+ return await reserveLaunchWorktreeOrFail(agentDir, worktreePath);
89
+ }
90
+
91
+ async function assertLaunchWorktreeUnoccupied(worktree: GjcLaunchWorktreePlan | { enabled: false }): Promise<void> {
92
+ if (!worktree.enabled) return;
93
+ let sessions: readonly IndexedSession[];
94
+ try {
95
+ sessions = (await new SessionIndex(getAgentDir()).open()).listSessionIdentities();
96
+ } catch {
97
+ throw new Error(`worktree_occupancy_unavailable:${worktree.worktreePath}`);
98
+ }
99
+ const occupant = worktreeOccupant(sessions, worktree.worktreePath);
100
+ if (occupant) throw worktreeInUseError(worktree.worktreePath, occupant);
101
+ }
102
+
103
+ async function reserveLaunchWorktreePreflight(
104
+ worktree: GjcLaunchWorktreePlan | { enabled: false },
105
+ ): Promise<LaunchWorktreeReservation | undefined> {
106
+ if (!worktree.enabled) return undefined;
107
+ const reservation = await reserveLaunchWorktreeOrFail(getAgentDir(), worktree.worktreePath);
108
+ try {
109
+ await assertLaunchWorktreeUnoccupied(worktree);
110
+ return reservation;
111
+ } catch (error) {
112
+ await reservation.release();
113
+ throw error;
114
+ }
115
+ }
116
+
59
117
  export default class Index extends Command {
60
118
  static description = "Red-claw AI coding assistant";
61
119
  static hidden = true;
@@ -93,34 +151,44 @@ export default class Index extends Command {
93
151
  const { args } = prepareAcpTerminalAuthArgs(this.argv);
94
152
  const parsed = parseArgs([...args], "deferred");
95
153
  if (parsed.mode !== "acp") assertLocalLaunchArgs(parsed);
154
+ assertMasterLaunchArgs(parsed);
96
155
  if (parsed.help || parsed.version) {
97
156
  await runRootCommand(parsed, args);
98
157
  return;
99
158
  }
100
159
 
101
160
  let launch: PreparedLaunchWorktree;
161
+ let reservation: LaunchWorktreeReservation | undefined;
102
162
  try {
163
+ const plannedWorktree = planLaunchWorktree(process.cwd(), parseLaunchWorktreeMode(args).mode);
164
+ reservation = await reserveLaunchWorktreePreflight(plannedWorktree);
103
165
  launch = prepareLaunchWorktree(process.cwd(), args);
104
166
  } catch (error) {
167
+ await reservation?.release();
105
168
  await persistCoordinatorLaunchFailure(error, process.cwd());
106
169
  throw error;
107
170
  }
108
- if (launch.worktree.enabled) {
109
- process.chdir(launch.cwd);
110
- setProjectDir(launch.cwd);
171
+ try {
172
+ if (launch.worktree.enabled) {
173
+ process.chdir(launch.cwd);
174
+ setProjectDir(launch.cwd);
175
+ }
176
+ const launchParsed = parseArgs(launch.args, "deferred");
177
+ if (launchParsed.mode !== "acp") assertLocalLaunchArgs(launchParsed);
178
+ assertMasterLaunchArgs(launchParsed);
179
+ if (
180
+ launchDefaultTmuxIfNeeded({
181
+ parsed: launchParsed,
182
+ rawArgs: launch.args,
183
+ cwd: launch.cwd,
184
+ worktreeBranch: launch.worktree.enabled && !launch.worktree.detached ? launch.worktree.branchName : null,
185
+ project: launch.cwd,
186
+ })
187
+ )
188
+ return;
189
+ await runRootCommand(launchParsed, launch.args);
190
+ } finally {
191
+ await reservation?.release();
111
192
  }
112
- const launchParsed = parseArgs(launch.args, "deferred");
113
- if (launchParsed.mode !== "acp") assertLocalLaunchArgs(launchParsed);
114
- if (
115
- launchDefaultTmuxIfNeeded({
116
- parsed: launchParsed,
117
- rawArgs: launch.args,
118
- cwd: launch.cwd,
119
- worktreeBranch: launch.worktree.enabled && !launch.worktree.detached ? launch.worktree.branchName : null,
120
- project: launch.cwd,
121
- })
122
- )
123
- return;
124
- await runRootCommand(launchParsed, launch.args);
125
193
  }
126
194
  }
@@ -0,0 +1,24 @@
1
+ import { Args, Command, Flags } from "@gajae-code/utils/cli";
2
+ import { MODEL_PRESETS_ACTIONS, type ModelPresetsAction, runModelPresetsCommand } from "../cli/model-presets-cli";
3
+
4
+ export default class ModelPresets extends Command {
5
+ static description = "Manage the signed model preset registry";
6
+
7
+ static args = {
8
+ action: Args.string({ description: "Action", required: false, options: MODEL_PRESETS_ACTIONS }),
9
+ revision: Args.string({ description: "Accepted revision for pin or rollback", required: false }),
10
+ };
11
+
12
+ static flags = {
13
+ json: Flags.boolean({ description: "Output JSON" }),
14
+ };
15
+
16
+ async run(): Promise<void> {
17
+ const { args, flags } = await this.parse(ModelPresets);
18
+ await runModelPresetsCommand({
19
+ action: (args.action ?? "status") as ModelPresetsAction,
20
+ revision: args.revision,
21
+ json: flags.json,
22
+ });
23
+ }
24
+ }