@gajae-code/coding-agent 0.15.4 → 0.15.6

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 (174) hide show
  1. package/CHANGELOG.md +46 -2
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  4. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  5. package/dist/types/cli/update-cli.d.ts +27 -5
  6. package/dist/types/commands/auth-gateway.d.ts +3 -0
  7. package/dist/types/commands/gc.d.ts +13 -0
  8. package/dist/types/commands/model-presets.d.ts +21 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +99 -0
  10. package/dist/types/config/file-lock.d.ts +49 -2
  11. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  12. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  13. package/dist/types/config/keybindings.d.ts +3 -2
  14. package/dist/types/config/model-preset-registry.d.ts +368 -0
  15. package/dist/types/config/model-profile-activation.d.ts +39 -0
  16. package/dist/types/config/model-profiles.d.ts +5 -3
  17. package/dist/types/config/model-registry.d.ts +11 -5
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  20. package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +4 -4
  22. package/dist/types/extensibility/shared-events.d.ts +2 -0
  23. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  25. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  26. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
  27. package/dist/types/index.d.ts +1 -0
  28. package/dist/types/main.d.ts +3 -1
  29. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  30. package/dist/types/modes/jobs-observer.d.ts +36 -0
  31. package/dist/types/sdk/broker/broker.d.ts +3 -0
  32. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  33. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  34. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  35. package/dist/types/sdk/cli/session-cli.d.ts +42 -1
  36. package/dist/types/sdk/client/discovery.d.ts +1 -0
  37. package/dist/types/sdk/host/host.d.ts +2 -0
  38. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  39. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  40. package/dist/types/sdk/host/session-runtime.d.ts +19 -1
  41. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  42. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  43. package/dist/types/sdk/router/session-router.d.ts +9 -0
  44. package/dist/types/session/account-inventory.d.ts +1 -0
  45. package/dist/types/session/agent-session.d.ts +19 -5
  46. package/dist/types/session/fold-coordinator.d.ts +201 -0
  47. package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
  48. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  49. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  50. package/dist/types/telemetry/control.d.ts +11 -0
  51. package/dist/types/telemetry/events.d.ts +20 -0
  52. package/dist/types/telemetry/index.d.ts +3 -0
  53. package/dist/types/telemetry/transport.d.ts +22 -0
  54. package/dist/types/tools/bash-interactive.d.ts +23 -0
  55. package/dist/types/tools/index.d.ts +6 -5
  56. package/dist/types/tools/path-utils.d.ts +5 -6
  57. package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
  58. package/dist/types/web/search/providers/base.d.ts +9 -4
  59. package/dist/types/web/search/providers/xai.d.ts +2 -1
  60. package/dist/types/web/search/types.d.ts +14 -0
  61. package/package.json +10 -7
  62. package/scripts/generate-hotkeys-docs.ts +4 -0
  63. package/scripts/generate-sdk-operation-inventory.ts +3 -3
  64. package/src/async/job-manager.ts +671 -39
  65. package/src/cli/auth-broker-cli.ts +40 -12
  66. package/src/cli/auth-gateway-cli.ts +273 -97
  67. package/src/cli/config-cli.ts +2 -2
  68. package/src/cli/fast-help.ts +3 -0
  69. package/src/cli/model-presets-cli.ts +100 -0
  70. package/src/cli/update-cli.ts +142 -6
  71. package/src/cli.ts +6 -2
  72. package/src/commands/auth-gateway.ts +12 -8
  73. package/src/commands/gc.ts +19 -0
  74. package/src/commands/model-presets.ts +24 -0
  75. package/src/commands/sdk.ts +5 -0
  76. package/src/config/autorouting-tier-map.ts +33 -0
  77. package/src/config/file-lock-gc.ts +9 -4
  78. package/src/config/file-lock.ts +775 -59
  79. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  80. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  81. package/src/config/keybindings.ts +6 -2
  82. package/src/config/model-preset-registry.ts +2799 -0
  83. package/src/config/model-profile-activation.ts +115 -28
  84. package/src/config/model-profile-contract.ts +4 -1
  85. package/src/config/model-profiles.ts +56 -14
  86. package/src/config/model-registry.ts +1028 -123
  87. package/src/config/settings-schema.ts +11 -0
  88. package/src/config/settings.ts +6 -1
  89. package/src/coordinator-mcp/model-preset.ts +32 -17
  90. package/src/coordinator-mcp/policy.ts +1 -1
  91. package/src/coordinator-mcp/projection-scan.ts +571 -0
  92. package/src/coordinator-mcp/server.ts +19 -24
  93. package/src/coordinator-mcp/session-reaper.ts +9 -3
  94. package/src/extensibility/extensions/loader.ts +0 -1
  95. package/src/extensibility/extensions/types.ts +4 -2
  96. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  97. package/src/extensibility/shared-events.ts +2 -0
  98. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  99. package/src/gjc-runtime/gc-render.ts +29 -0
  100. package/src/gjc-runtime/gc-runtime.ts +174 -11
  101. package/src/gjc-runtime/launch-worktree.ts +44 -7
  102. package/src/gjc-runtime/ralplan-runtime.ts +14 -1
  103. package/src/gjc-runtime/session-state-lock.ts +1242 -141
  104. package/src/gjc-runtime/session-state-sidecar.ts +76 -3
  105. package/src/gjc-runtime/tmux-sessions.ts +145 -48
  106. package/src/index.ts +1 -0
  107. package/src/internal-urls/docs-index.generated.ts +14 -13
  108. package/src/lsp/config.ts +4 -3
  109. package/src/lsp/path-trust.ts +12 -6
  110. package/src/main.ts +9 -2
  111. package/src/modes/acp/acp-agent.ts +11 -5
  112. package/src/modes/components/jobs-overlay-model.ts +99 -16
  113. package/src/modes/components/model-selector.ts +158 -18
  114. package/src/modes/components/status-line/segments.ts +6 -1
  115. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  116. package/src/modes/controllers/input-controller.ts +23 -6
  117. package/src/modes/interactive-mode.ts +14 -1
  118. package/src/modes/jobs-observer.ts +149 -16
  119. package/src/modes/prompt-action-autocomplete.ts +26 -47
  120. package/src/modes/runtime-init.ts +1 -1
  121. package/src/modes/utils/injected-user-submission.ts +15 -1
  122. package/src/prompts/tools/bash.md +2 -2
  123. package/src/prompts/tools/fold-receipt.md +3 -0
  124. package/src/prompts/tools/job.md +1 -1
  125. package/src/sdk/acp/adapter.ts +17 -2
  126. package/src/sdk/broker/broker.ts +48 -21
  127. package/src/sdk/broker/lifecycle.ts +32 -6
  128. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  129. package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
  130. package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
  131. package/src/sdk/bus/prompt-reconciliation.ts +33 -6
  132. package/src/sdk/bus/slack-conversation.ts +5 -1
  133. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  134. package/src/sdk/cli/session-cli.ts +161 -5
  135. package/src/sdk/client/discovery.ts +3 -1
  136. package/src/sdk/host/control/dispatch.ts +3 -0
  137. package/src/sdk/host/host.ts +7 -0
  138. package/src/sdk/host/model-pin.ts +185 -25
  139. package/src/sdk/host/sdk-run-capability.ts +1 -0
  140. package/src/sdk/host/session-runtime.test.ts +1240 -107
  141. package/src/sdk/host/session-runtime.ts +1029 -115
  142. package/src/sdk/prompt-deadline-manager.ts +16 -1
  143. package/src/sdk/protocol/adapter-validation.ts +27 -0
  144. package/src/sdk/protocol/operation-inventory.generated.json +2 -2
  145. package/src/sdk/protocol/operation-registry.ts +4 -1
  146. package/src/sdk/router/session-router.ts +297 -111
  147. package/src/sdk/session.ts +83 -12
  148. package/src/session/account-inventory.ts +27 -11
  149. package/src/session/agent-session.ts +887 -95
  150. package/src/session/fold-coordinator.ts +433 -0
  151. package/src/session/internal/managed-session-storage.ts +64 -26
  152. package/src/session/sdk-run-capability-internal.ts +15 -0
  153. package/src/session/sdk-run-capability.ts +2 -0
  154. package/src/session/session-manager.ts +29 -10
  155. package/src/setup/provider-presets.json +1 -1
  156. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  157. package/src/task/executor.ts +19 -3
  158. package/src/telemetry/control.ts +26 -0
  159. package/src/telemetry/events.ts +142 -0
  160. package/src/telemetry/index.ts +3 -0
  161. package/src/telemetry/transport.ts +87 -0
  162. package/src/tools/bash-interactive.ts +182 -85
  163. package/src/tools/bash.ts +786 -67
  164. package/src/tools/index.ts +7 -5
  165. package/src/tools/path-utils.ts +77 -29
  166. package/src/tools/tmux-self-injection-guard.ts +359 -0
  167. package/src/tools/tool-catalog.generated.ts +1 -1
  168. package/src/web/search/provider.ts +120 -11
  169. package/src/web/search/providers/anthropic.ts +89 -17
  170. package/src/web/search/providers/base.ts +9 -4
  171. package/src/web/search/providers/gemini.ts +87 -15
  172. package/src/web/search/providers/openai-compatible.ts +23 -8
  173. package/src/web/search/providers/xai.ts +98 -12
  174. 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
+ }
@@ -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,
@@ -163,11 +165,12 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
163
165
  }
164
166
 
165
167
  export type PackageManagerTarget = { manager: "npm"; packageName: string };
168
+ type MigrationUpdateTarget = { method: "migrate"; path: string; previousPath?: string };
166
169
  export type UpdateTarget =
167
170
  | { method: "bun" }
168
171
  | { method: "npm"; packageName: string }
169
172
  | { method: "binary"; path: string }
170
- | { method: "migrate"; path: string; previousPath?: string };
173
+ | MigrationUpdateTarget;
171
174
 
172
175
  type PathPlatform = NodeJS.Platform;
173
176
  type PackageExists = (packageName: string, packageRoot: string) => boolean;
@@ -506,6 +509,73 @@ async function verifyInstalledRuntime(
506
509
  }
507
510
  }
508
511
 
512
+ interface MigrationTargetVerificationOptions {
513
+ runtimePath: string;
514
+ verifyChecksum: () => Promise<void>;
515
+ verifyRuntime: () => Promise<InstalledVersionVerification>;
516
+ }
517
+
518
+ interface MigrationChecksumOptions {
519
+ tag: string;
520
+ assetName: string;
521
+ filePath: string;
522
+ }
523
+
524
+ type MigrationChecksumVerifier = (options: MigrationChecksumOptions) => Promise<unknown>;
525
+
526
+ async function verifyMigrationTargetWith(
527
+ options: MigrationTargetVerificationOptions,
528
+ ): Promise<InstalledVersionVerification> {
529
+ try {
530
+ await options.verifyChecksum();
531
+ } catch {
532
+ return { ok: false, path: options.runtimePath };
533
+ }
534
+ return await options.verifyRuntime();
535
+ }
536
+
537
+ async function verifyMigrationTarget(
538
+ release: Pick<ReleaseInfo, "tag" | "version">,
539
+ runtimePath: string,
540
+ verifyChecksum: MigrationChecksumVerifier = verifyDownloadedBinaryChecksum,
541
+ verifyRuntime: (
542
+ expectedVersion: string,
543
+ runtimePath: string,
544
+ ) => Promise<InstalledVersionVerification> = verifyInstalledRuntime,
545
+ ): Promise<InstalledVersionVerification> {
546
+ return await verifyMigrationTargetWith({
547
+ runtimePath,
548
+ verifyChecksum: async () => {
549
+ await verifyChecksum({
550
+ tag: release.tag,
551
+ assetName: getBinaryName(),
552
+ filePath: runtimePath,
553
+ });
554
+ },
555
+ verifyRuntime: async () => await verifyRuntime(release.version, runtimePath),
556
+ });
557
+ }
558
+
559
+ export async function verifyMigrationTargetForTest(
560
+ options: MigrationTargetVerificationOptions,
561
+ ): Promise<InstalledVersionVerification> {
562
+ return await verifyMigrationTargetWith(options);
563
+ }
564
+
565
+ export async function verifyMigrationTargetAdapterForTest(options: {
566
+ release: Pick<ReleaseInfo, "tag" | "version">;
567
+ runtimePath: string;
568
+ verifyChecksum: MigrationChecksumVerifier;
569
+ verifyRuntime: (expectedVersion: string, runtimePath: string) => Promise<InstalledVersionVerification>;
570
+ }): Promise<InstalledVersionVerification> {
571
+ return await verifyMigrationTarget(
572
+ options.release,
573
+ options.runtimePath,
574
+ options.verifyChecksum,
575
+ options.verifyRuntime,
576
+ );
577
+ }
578
+
509
579
  function printRestartGuidance(): void {
510
580
  console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
511
581
  }
@@ -1062,6 +1132,7 @@ async function updateViaBinaryAt(
1062
1132
  export interface UpdateCommandDependencies {
1063
1133
  getLatestRelease?: (options?: LatestReleaseLookupOptions) => Promise<ReleaseInfo>;
1064
1134
  resolveUpdateTarget?: () => Promise<UpdateTarget>;
1135
+ verifyMigrationTarget?: (release: ReleaseInfo, runtimePath: string) => Promise<InstalledVersionVerification>;
1065
1136
  performUpdate?: (
1066
1137
  target: UpdateTarget,
1067
1138
  expectedVersion: string,
@@ -1073,6 +1144,7 @@ export interface UpdateCommandDependencies {
1073
1144
  restartDaemon?: (settings: Settings) => Promise<void>;
1074
1145
  recoverNotifications?: (settings: Settings) => Promise<void>;
1075
1146
  runPostUpdateRecovery?: (runtimePath: string) => Promise<void>;
1147
+ recordTelemetryEvent?: (event: TelemetryEventName, details: TelemetryDetails) => unknown;
1076
1148
  exit?: (code: number) => never;
1077
1149
  }
1078
1150
 
@@ -1265,6 +1337,24 @@ export function resolveUpdateDecision(options: {
1265
1337
  return { install: true, kind: options.comparison > 0 ? "new-version" : "force" };
1266
1338
  }
1267
1339
 
1340
+ function printVerifiedMigrationTarget(target: MigrationUpdateTarget, version: string): void {
1341
+ console.log(
1342
+ chalk.green(
1343
+ `${theme.status.success} Standalone ${APP_NAME} ${version} is already installed and verified at ${target.path}`,
1344
+ ),
1345
+ );
1346
+ if (target.previousPath) {
1347
+ console.log(chalk.yellow(`${target.previousPath} shadows it on PATH.`));
1348
+ console.log(
1349
+ chalk.cyan(
1350
+ `The standalone directory ${path.dirname(target.path)} must precede the shim directory ${path.dirname(target.previousPath)} on PATH.`,
1351
+ ),
1352
+ );
1353
+ return;
1354
+ }
1355
+ console.log(chalk.cyan(`Ensure the standalone directory ${path.dirname(target.path)} is on PATH.`));
1356
+ }
1357
+
1268
1358
  export async function runUpdateCommand(
1269
1359
  opts: UpdateCommandOptions,
1270
1360
  deps: UpdateCommandDependencies = {},
@@ -1272,9 +1362,28 @@ export async function runUpdateCommand(
1272
1362
  const channel = opts.channel ?? "stable";
1273
1363
  const lookupRelease = deps.getLatestRelease ?? getLatestRelease;
1274
1364
  const resolveTarget = deps.resolveUpdateTarget ?? resolveUpdateTarget;
1365
+ const verifyTarget = deps.verifyMigrationTarget ?? verifyMigrationTarget;
1275
1366
  const update = deps.performUpdate ?? performUpdate;
1276
1367
  const refreshDefaults = deps.refreshInstalledDefaultSkills ?? refreshInstalledDefaultSkills;
1277
1368
  const exit = deps.exit ?? process.exit;
1369
+ const recordEvent = deps.recordTelemetryEvent ?? ((event, details) => recordTelemetryEvent(event, details));
1370
+ const pendingTelemetry = new Set<Promise<void>>();
1371
+ const record = (event: TelemetryEventName, details: TelemetryDetails): void => {
1372
+ const result = recordEvent(event, details);
1373
+ if (result !== null && (typeof result === "object" || typeof result === "function") && "then" in result) {
1374
+ const pending = Promise.resolve(result as PromiseLike<unknown>).then(
1375
+ () => undefined,
1376
+ () => undefined,
1377
+ );
1378
+ pendingTelemetry.add(pending);
1379
+ void pending.finally(() => pendingTelemetry.delete(pending));
1380
+ }
1381
+ };
1382
+ const flushTelemetryBeforeExit = async (): Promise<never> => {
1383
+ await Promise.race([Promise.allSettled([...pendingTelemetry]), Bun.sleep(2000)]);
1384
+ return exit(1);
1385
+ };
1386
+ record("update_check_started", { channel });
1278
1387
 
1279
1388
  console.log(chalk.dim(`Current version: ${VERSION}`));
1280
1389
  if (channel !== "stable") {
@@ -1285,16 +1394,18 @@ export async function runUpdateCommand(
1285
1394
  try {
1286
1395
  target = await resolveTarget();
1287
1396
  } catch (err) {
1397
+ record("update_check_completed", { channel, result: "failed" });
1288
1398
  console.error(chalk.red(err instanceof Error ? err.message : String(err)));
1289
- return exit(1);
1399
+ return flushTelemetryBeforeExit();
1290
1400
  }
1291
1401
 
1292
1402
  let release: ReleaseInfo;
1293
1403
  try {
1294
1404
  release = await lookupRelease({ channel });
1295
1405
  } catch (err) {
1406
+ record("update_check_completed", { channel, result: "failed" });
1296
1407
  console.error(chalk.red(`Failed to check for updates: ${err}`));
1297
- return exit(1);
1408
+ return flushTelemetryBeforeExit();
1298
1409
  }
1299
1410
 
1300
1411
  // A config file that exists but could not be read changes which registry
@@ -1307,12 +1418,13 @@ export async function runUpdateCommand(
1307
1418
  try {
1308
1419
  comparison = compareVersions(release.version, VERSION);
1309
1420
  } catch (err) {
1421
+ record("update_check_completed", { channel, result: "failed" });
1310
1422
  console.error(
1311
1423
  chalk.red(
1312
1424
  `Failed to check for updates: the ${channel} channel reported an unparseable version "${release.version}": ${err instanceof Error ? err.message : String(err)}`,
1313
1425
  ),
1314
1426
  );
1315
- return exit(1);
1427
+ return flushTelemetryBeforeExit();
1316
1428
  }
1317
1429
 
1318
1430
  const decision = resolveUpdateDecision({
@@ -1323,7 +1435,24 @@ export async function runUpdateCommand(
1323
1435
  migrate: target?.method === "migrate",
1324
1436
  });
1325
1437
 
1438
+ if (target.method === "migrate" && decision.install && !opts.force) {
1439
+ const releaseLock = await acquireBinaryUpdateLock(target.path);
1440
+ try {
1441
+ const verification = await verifyTarget(release, target.path);
1442
+ if (verification.ok) {
1443
+ record("update_check_completed", { channel, result: "available" });
1444
+ record("update_install_started", { channel, installMethod: target.method });
1445
+ printVerifiedMigrationTarget(target, release.version);
1446
+ record("update_install_completed", { channel, result: "installed", installMethod: target.method });
1447
+ return;
1448
+ }
1449
+ } finally {
1450
+ await releaseLock();
1451
+ }
1452
+ }
1453
+
1326
1454
  if (!decision.install) {
1455
+ record("update_check_completed", { channel, result: "up_to_date" });
1327
1456
  console.log(chalk.green(`${theme.status.success} Already up to date`));
1328
1457
  return;
1329
1458
  }
@@ -1338,9 +1467,14 @@ export async function runUpdateCommand(
1338
1467
  console.log(chalk.yellow(`Forcing reinstall of ${release.version}`));
1339
1468
  }
1340
1469
 
1341
- if (opts.check) return;
1470
+ record("update_check_completed", { channel, result: "available" });
1471
+ if (opts.check) {
1472
+ record("update_install_completed", { channel, result: "skipped" });
1473
+ return;
1474
+ }
1342
1475
 
1343
1476
  let installedVersion: string | undefined;
1477
+ record("update_install_started", { channel, installMethod: target.method });
1344
1478
  try {
1345
1479
  const resolved = target ?? (await resolveTarget());
1346
1480
  const verification = await update(resolved, release.version, release.registry);
@@ -1349,16 +1483,18 @@ export async function runUpdateCommand(
1349
1483
  await (deps.runPostUpdateRecovery ?? runPostUpdateRecovery)(verification.path);
1350
1484
  } else if (!deps.performUpdate) throw new Error("verified installed runtime path is unavailable");
1351
1485
  } catch (err) {
1486
+ record("update_install_failed", { channel, result: "failed", installMethod: target.method });
1352
1487
  const prefix = installedVersion
1353
1488
  ? `Updated to ${installedVersion}, but post-update recovery failed`
1354
1489
  : "Update failed";
1355
1490
  console.error(chalk.red(`${prefix}: ${err}`));
1356
- return exit(1);
1491
+ return flushTelemetryBeforeExit();
1357
1492
  }
1358
1493
 
1359
1494
  // The installed runtime completes recovery before this old updater process
1360
1495
  // refreshes opt-in local definitions, avoiding stale-module daemon control.
1361
1496
  await refreshDefaults();
1497
+ record("update_install_completed", { channel, result: "installed", installMethod: target.method });
1362
1498
  }
1363
1499
 
1364
1500
  /**
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> {
@@ -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
+ }
@@ -838,6 +838,10 @@ class SdkSessionHelp extends Command {
838
838
  description: "Workspace directory for saved-session resolution (default: current directory)",
839
839
  }),
840
840
  op: Flags.string({ description: "Raw control or global operation" }),
841
+ scope: Flags.string({
842
+ description: "list scope: repo (default), cwd, worktree, or all",
843
+ options: ["repo", "cwd", "worktree", "all"],
844
+ }),
841
845
  query: Flags.string({ description: "Raw query name" }),
842
846
  "json-input": Flags.string({ description: "SDK request JSON object" }),
843
847
  "json-input-file": Flags.string({ description: "Read SDK request JSON from a 0600 file" }),
@@ -894,6 +898,7 @@ class SdkSessionCommand extends Command {
894
898
  allEvents: Boolean(flagRec["all-events"]),
895
899
  agentDir: flagRec["agent-dir"] as string | undefined,
896
900
  repo: flagRec.repo as string | undefined,
901
+ scope: flagRec.scope as string | undefined,
897
902
  });
898
903
  }
899
904
  }
@@ -146,6 +146,7 @@ export const TIER_MAP_SKIP_LIST = {
146
146
  },
147
147
  "vercel-ai-gateway/google/gemini-3.7-flash": { rationale: "post-rebase catalog addition from dev; not yet curated" },
148
148
  "zai/glm-5.3": { rationale: "post-rebase catalog addition from dev; not yet curated" },
149
+ "zai/glm-5.3-flash": { rationale: "post-feature catalog addition; not yet curated" },
149
150
  "cursor/gemini-3.7-flash-high": {
150
151
  rationale: "post-rebase catalog addition from dev; not yet curated",
151
152
  },
@@ -5885,6 +5886,38 @@ export const TIER_MAP_SKIP_LIST = {
5885
5886
  "zenmux/z-ai/glm-5.2": { baseline: true, rationale: "pre-feature baseline; not yet curated" },
5886
5887
  "zenmux/z-ai/glm-5.2-free": { baseline: true, rationale: "pre-feature baseline; not yet curated" },
5887
5888
  "zenmux/z-ai/glm-5v-turbo": { baseline: true, rationale: "pre-feature baseline; not yet curated" },
5889
+ "kiro/auto": { rationale: "Kiro API-key catalog addition; not yet curated" },
5890
+ "kiro/claude-haiku-4-5": { rationale: "Kiro API-key catalog addition; not yet curated" },
5891
+ "kiro/claude-haiku-4.5": { rationale: "Kiro API-key catalog addition; not yet curated" },
5892
+ "kiro/claude-opus-4-5": { rationale: "Kiro API-key catalog addition; not yet curated" },
5893
+ "kiro/claude-opus-4-6": { rationale: "Kiro API-key catalog addition; not yet curated" },
5894
+ "kiro/claude-opus-4-7": { rationale: "Kiro API-key catalog addition; not yet curated" },
5895
+ "kiro/claude-opus-4-8": { rationale: "Kiro API-key catalog addition; not yet curated" },
5896
+ "kiro/claude-opus-4.5": { rationale: "Kiro API-key catalog addition; not yet curated" },
5897
+ "kiro/claude-opus-4.6": { rationale: "Kiro API-key catalog addition; not yet curated" },
5898
+ "kiro/claude-opus-4.7": { rationale: "Kiro API-key catalog addition; not yet curated" },
5899
+ "kiro/claude-opus-4.8": { rationale: "Kiro API-key catalog addition; not yet curated" },
5900
+ "kiro/claude-opus-5": { rationale: "Kiro API-key catalog addition; not yet curated" },
5901
+ "kiro/claude-sonnet-4": { rationale: "Kiro API-key catalog addition; not yet curated" },
5902
+ "kiro/claude-sonnet-4-5": { rationale: "Kiro API-key catalog addition; not yet curated" },
5903
+ "kiro/claude-sonnet-4-6": { rationale: "Kiro API-key catalog addition; not yet curated" },
5904
+ "kiro/claude-sonnet-4.5": { rationale: "Kiro API-key catalog addition; not yet curated" },
5905
+ "kiro/claude-sonnet-4.6": { rationale: "Kiro API-key catalog addition; not yet curated" },
5906
+ "kiro/claude-sonnet-5": { rationale: "Kiro API-key catalog addition; not yet curated" },
5907
+ "kiro/deepseek-3-2": { rationale: "Kiro API-key catalog addition; not yet curated" },
5908
+ "kiro/deepseek-3.2": { rationale: "Kiro API-key catalog addition; not yet curated" },
5909
+ "kiro/glm-5": { rationale: "Kiro API-key catalog addition; not yet curated" },
5910
+ "kiro/gpt-5-6-luna": { rationale: "Kiro API-key catalog addition; not yet curated" },
5911
+ "kiro/gpt-5-6-sol": { rationale: "Kiro API-key catalog addition; not yet curated" },
5912
+ "kiro/gpt-5-6-terra": { rationale: "Kiro API-key catalog addition; not yet curated" },
5913
+ "kiro/gpt-5.6-luna": { rationale: "Kiro API-key catalog addition; not yet curated" },
5914
+ "kiro/gpt-5.6-sol": { rationale: "Kiro API-key catalog addition; not yet curated" },
5915
+ "kiro/gpt-5.6-terra": { rationale: "Kiro API-key catalog addition; not yet curated" },
5916
+ "kiro/minimax-m2-1": { rationale: "Kiro API-key catalog addition; not yet curated" },
5917
+ "kiro/minimax-m2-5": { rationale: "Kiro API-key catalog addition; not yet curated" },
5918
+ "kiro/minimax-m2.1": { rationale: "Kiro API-key catalog addition; not yet curated" },
5919
+ "kiro/minimax-m2.5": { rationale: "Kiro API-key catalog addition; not yet curated" },
5920
+ "kiro/qwen3-coder-next": { rationale: "Kiro API-key catalog addition; not yet curated" },
5888
5921
  } as const satisfies TierMapSkipList;
5889
5922
 
5890
5923
  export const CURATED_TIER_MAP: AutoroutingCuratedTierMap = {
@@ -17,7 +17,7 @@ import type {
17
17
  } from "../gjc-runtime/gc-runtime";
18
18
  import { gcPidStatusLabel } from "../gjc-runtime/gc-runtime";
19
19
  import { resolveReceiptSpoolDir } from "../harness-control-plane/receipt-spool";
20
- import { readFileLockInfoForGc, removeFileLockDirForGc } from "./file-lock";
20
+ import { readFileLockObservationForGc, removeFileLockDirForGc } from "./file-lock";
21
21
 
22
22
  const MAX_WALK_DEPTH = 6;
23
23
  /** Default per-root walk budget. Truncation is a warning, not a hard error. */
@@ -68,7 +68,8 @@ function keptMalformedRecord(lockDir: string): GcRecord {
68
68
  }
69
69
 
70
70
  async function collectLockRecord(lockDir: string, ctx: GcContext): Promise<GcRecord> {
71
- const info = await readFileLockInfoForGc(lockDir);
71
+ const observation = await readFileLockObservationForGc(lockDir);
72
+ const info = observation?.info;
72
73
  if (!info) return keptMalformedRecord(lockDir);
73
74
  if (info.owner_host_id !== undefined) {
74
75
  return {
@@ -203,7 +204,8 @@ export const fileLocksGcAdapter: GcStoreAdapter = {
203
204
  },
204
205
  async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
205
206
  const lockDir = record.path ?? record.id;
206
- const info = await readFileLockInfoForGc(lockDir);
207
+ const observation = await readFileLockObservationForGc(lockDir);
208
+ const info = observation?.info;
207
209
  if (!info) return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
208
210
  if (info.owner_host_id !== undefined) {
209
211
  return { removed: false, skipped: "host_qualified_lock_requires_owner_reclamation" };
@@ -220,13 +222,16 @@ export const fileLocksGcAdapter: GcStoreAdapter = {
220
222
  // so removal re-verifies the on-disk identity under the unlink and refuses
221
223
  // to delete a recreated LIVE lock (TOCTOU).
222
224
  try {
223
- const removal = await removeFileLockDirForGc(lockDir, info);
225
+ const removal = await removeFileLockDirForGc(lockDir, info, observation?.identity);
224
226
  if (removal === "owner_changed") {
225
227
  return { removed: false, skipped: "file_lock_owner_changed_before_delete" };
226
228
  }
227
229
  if (removal === "missing") {
228
230
  return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
229
231
  }
232
+ if (removal === "cleanup_failed") {
233
+ return { removed: false, skipped: "lock_removal_denied_by_native_identity_guard" };
234
+ }
230
235
  return { removed: true };
231
236
  } catch (error) {
232
237
  return { removed: false, error: errorMessage(error) };