@gajae-code/coding-agent 0.3.0 → 0.3.2

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 (213) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +7 -0
  4. package/dist/types/cli/args.d.ts +3 -1
  5. package/dist/types/commands/deep-interview.d.ts +3 -0
  6. package/dist/types/commands/launch.d.ts +6 -0
  7. package/dist/types/config/keybindings.d.ts +5 -0
  8. package/dist/types/config/model-profile-activation.d.ts +30 -0
  9. package/dist/types/config/model-profiles.d.ts +19 -0
  10. package/dist/types/config/model-registry.d.ts +8 -0
  11. package/dist/types/config/model-resolver.d.ts +1 -1
  12. package/dist/types/config/models-config-schema.d.ts +47 -0
  13. package/dist/types/config/settings-schema.d.ts +14 -4
  14. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  15. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  16. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  17. package/dist/types/eval/py/executor.d.ts +2 -0
  18. package/dist/types/eval/py/kernel.d.ts +2 -0
  19. package/dist/types/exec/bash-executor.d.ts +10 -0
  20. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  21. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  22. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  23. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  24. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  25. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -1
  26. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  27. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  28. package/dist/types/hooks/skill-state.d.ts +21 -0
  29. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  30. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  31. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  32. package/dist/types/internal-urls/types.d.ts +4 -0
  33. package/dist/types/lsp/index.d.ts +10 -10
  34. package/dist/types/main.d.ts +10 -1
  35. package/dist/types/modes/bridge/auth.d.ts +12 -0
  36. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  37. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  38. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  39. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  41. package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  43. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  44. package/dist/types/modes/components/model-selector.d.ts +6 -1
  45. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  46. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  47. package/dist/types/modes/components/status-line.d.ts +2 -0
  48. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  49. package/dist/types/modes/controllers/selector-controller.d.ts +9 -0
  50. package/dist/types/modes/index.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +1 -0
  52. package/dist/types/modes/jobs-observer.d.ts +57 -0
  53. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  54. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  55. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  56. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  57. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  58. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  59. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  60. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  61. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  62. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  63. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  64. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  65. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  66. package/dist/types/modes/types.d.ts +2 -0
  67. package/dist/types/sdk.d.ts +3 -1
  68. package/dist/types/session/agent-session.d.ts +11 -1
  69. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  70. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  71. package/dist/types/task/executor.d.ts +1 -0
  72. package/dist/types/task/id.d.ts +7 -0
  73. package/dist/types/task/index.d.ts +5 -0
  74. package/dist/types/task/receipt.d.ts +85 -0
  75. package/dist/types/task/spawn-gate.d.ts +38 -0
  76. package/dist/types/task/types.d.ts +143 -11
  77. package/dist/types/tools/cron.d.ts +6 -0
  78. package/dist/types/tools/hindsight-recall.d.ts +0 -2
  79. package/dist/types/tools/hindsight-reflect.d.ts +0 -2
  80. package/dist/types/tools/hindsight-retain.d.ts +0 -2
  81. package/dist/types/tools/index.d.ts +6 -4
  82. package/dist/types/tools/path-utils.d.ts +1 -0
  83. package/dist/types/tools/subagent.d.ts +15 -0
  84. package/package.json +7 -7
  85. package/scripts/build-binary.ts +7 -0
  86. package/src/async/job-manager.ts +36 -0
  87. package/src/cli/args.ts +19 -2
  88. package/src/commands/deep-interview.ts +1 -0
  89. package/src/commands/harness.ts +289 -19
  90. package/src/commands/launch.ts +10 -2
  91. package/src/commands/state.ts +2 -1
  92. package/src/commands/team.ts +22 -4
  93. package/src/config/keybindings.ts +6 -0
  94. package/src/config/model-profile-activation.ts +157 -0
  95. package/src/config/model-profiles.ts +155 -0
  96. package/src/config/model-registry.ts +19 -0
  97. package/src/config/model-resolver.ts +3 -2
  98. package/src/config/models-config-schema.ts +36 -0
  99. package/src/config/settings-schema.ts +16 -3
  100. package/src/dap/client.ts +17 -3
  101. package/src/debug/crash-diagnostics.ts +223 -0
  102. package/src/debug/runtime-gauges.ts +20 -0
  103. package/src/deep-interview/render-middleware.ts +6 -0
  104. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  105. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  106. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -3
  107. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
  108. package/src/defaults/gjc-defaults.ts +7 -0
  109. package/src/eval/py/executor.ts +21 -1
  110. package/src/eval/py/kernel.ts +15 -0
  111. package/src/exec/bash-executor.ts +41 -0
  112. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  113. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  114. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  115. package/src/gjc-runtime/state-migrations.ts +54 -7
  116. package/src/gjc-runtime/state-runtime.ts +461 -64
  117. package/src/gjc-runtime/state-schema.ts +192 -0
  118. package/src/gjc-runtime/state-writer.ts +32 -1
  119. package/src/gjc-runtime/team-runtime.ts +177 -105
  120. package/src/gjc-runtime/ultragoal-runtime.ts +231 -38
  121. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  122. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  123. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  124. package/src/harness-control-plane/control-endpoint.ts +19 -8
  125. package/src/harness-control-plane/owner.ts +57 -10
  126. package/src/harness-control-plane/state-machine.ts +2 -1
  127. package/src/hooks/skill-state.ts +176 -26
  128. package/src/internal-urls/agent-protocol.ts +68 -21
  129. package/src/internal-urls/artifact-protocol.ts +12 -17
  130. package/src/internal-urls/docs-index.generated.ts +8 -10
  131. package/src/internal-urls/registry-helpers.ts +19 -16
  132. package/src/internal-urls/types.ts +4 -0
  133. package/src/lsp/client.ts +18 -2
  134. package/src/main.ts +88 -6
  135. package/src/modes/bridge/auth.ts +41 -0
  136. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  137. package/src/modes/bridge/bridge-mode.ts +520 -0
  138. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  139. package/src/modes/bridge/event-stream.ts +70 -0
  140. package/src/modes/components/custom-editor.ts +101 -0
  141. package/src/modes/components/custom-provider-wizard.ts +318 -0
  142. package/src/modes/components/hook-selector.ts +61 -18
  143. package/src/modes/components/jobs-overlay-model.ts +109 -0
  144. package/src/modes/components/jobs-overlay.ts +172 -0
  145. package/src/modes/components/model-selector.ts +108 -18
  146. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  147. package/src/modes/components/status-line/presets.ts +7 -5
  148. package/src/modes/components/status-line/segments.ts +25 -0
  149. package/src/modes/components/status-line/types.ts +2 -0
  150. package/src/modes/components/status-line.ts +9 -1
  151. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  152. package/src/modes/controllers/input-controller.ts +97 -9
  153. package/src/modes/controllers/selector-controller.ts +86 -1
  154. package/src/modes/index.ts +1 -0
  155. package/src/modes/interactive-mode.ts +27 -0
  156. package/src/modes/jobs-observer.ts +204 -0
  157. package/src/modes/rpc/host-tools.ts +1 -186
  158. package/src/modes/rpc/host-uris.ts +1 -235
  159. package/src/modes/rpc/rpc-client.ts +25 -10
  160. package/src/modes/rpc/rpc-mode.ts +12 -381
  161. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  162. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  163. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  164. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  165. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  166. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  167. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  168. package/src/modes/shared/agent-wire/responses.ts +17 -0
  169. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  170. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  171. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  172. package/src/modes/types.ts +2 -0
  173. package/src/prompts/memories/consolidation.md +1 -1
  174. package/src/prompts/memories/read-path.md +6 -7
  175. package/src/prompts/memories/unavailable.md +2 -2
  176. package/src/prompts/tools/bash.md +1 -1
  177. package/src/prompts/tools/irc.md +1 -1
  178. package/src/prompts/tools/read.md +2 -2
  179. package/src/prompts/tools/recall.md +1 -0
  180. package/src/prompts/tools/reflect.md +1 -0
  181. package/src/prompts/tools/retain.md +1 -0
  182. package/src/prompts/tools/subagent.md +12 -7
  183. package/src/prompts/tools/task-summary.md +3 -9
  184. package/src/prompts/tools/task.md +5 -1
  185. package/src/sdk.ts +5 -1
  186. package/src/session/agent-session.ts +214 -38
  187. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  188. package/src/skill-state/workflow-state-contract.ts +7 -4
  189. package/src/skill-state/workflow-state-version.ts +3 -0
  190. package/src/slash-commands/builtin-registry.ts +9 -1
  191. package/src/task/executor.ts +31 -5
  192. package/src/task/id.ts +33 -0
  193. package/src/task/index.ts +259 -67
  194. package/src/task/output-manager.ts +5 -4
  195. package/src/task/receipt.ts +297 -0
  196. package/src/task/render.ts +48 -131
  197. package/src/task/spawn-gate.ts +132 -0
  198. package/src/task/types.ts +48 -7
  199. package/src/tools/ask.ts +73 -33
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash.ts +1 -1
  203. package/src/tools/cron.ts +48 -0
  204. package/src/tools/find.ts +4 -1
  205. package/src/tools/hindsight-recall.ts +0 -2
  206. package/src/tools/hindsight-reflect.ts +0 -2
  207. package/src/tools/hindsight-retain.ts +0 -2
  208. package/src/tools/index.ts +6 -18
  209. package/src/tools/path-utils.ts +3 -2
  210. package/src/tools/read.ts +4 -3
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/skill.ts +6 -1
  213. package/src/tools/subagent.ts +237 -84
@@ -36,6 +36,12 @@ export default class Index extends Command {
36
36
  plan: Flags.string({
37
37
  description: "Plan model for architectural planning (or GJC_PLAN_MODEL env)",
38
38
  }),
39
+ mpreset: Flags.string({
40
+ description: "Model profile preset to activate for this session",
41
+ }),
42
+ default: Flags.boolean({
43
+ description: "Persist --mpreset as the default model profile",
44
+ }),
39
45
  provider: Flags.string({
40
46
  description: "Provider to use (legacy; prefer --model)",
41
47
  }),
@@ -52,8 +58,8 @@ export default class Index extends Command {
52
58
  description: "Allow starting in ~ without auto-switching to a temp dir",
53
59
  }),
54
60
  mode: Flags.string({
55
- description: "Output mode: text (default), json, rpc, or rpc-ui",
56
- options: ["text", "json", "rpc", "acp", "rpc-ui"],
61
+ description: "Output mode: text (default), json, rpc, acp, rpc-ui, or bridge",
62
+ options: ["text", "json", "rpc", "acp", "rpc-ui", "bridge"],
57
63
  }),
58
64
  print: Flags.boolean({
59
65
  char: "p",
@@ -136,6 +142,8 @@ export default class Index extends Command {
136
142
  `# Launch in a sibling git worktree\n ${APP_NAME} --worktree`,
137
143
  `# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`,
138
144
  `# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`,
145
+ `# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-standard`,
146
+ `# Persist a model profile as the default\n ${APP_NAME} --mpreset opencode-go-pro --default`,
139
147
  `# Export a session file to HTML\n ${APP_NAME} --export ~/.gjc/agent/sessions/--path--/session.jsonl`,
140
148
  ];
141
149
 
@@ -9,9 +9,10 @@ export default class State extends Command {
9
9
  '$ gjc state write --input \'{"state":{"interview_id":"abc"}}\' --mode deep-interview --json',
10
10
  "$ gjc state clear --mode deep-interview",
11
11
  "$ gjc state deep-interview read --json",
12
- '$ gjc state ralplan write --input \'{"phase":"approval","active":true}\' --json',
12
+ '$ gjc state ralplan write --input \'{"phase":"planner","active":true}\' --json',
13
13
  "$ gjc state team contract",
14
14
  "$ gjc state deep-interview handoff --to ralplan --json",
15
+ "$ gjc state doctor --skill ralplan --json",
15
16
  ];
16
17
 
17
18
  async run(): Promise<void> {
@@ -1,4 +1,5 @@
1
1
  import { Args, Command, Flags } from "@gajae-code/utils/cli";
2
+ import { renderCliWriteReceipt } from "../gjc-runtime/cli-write-receipt";
2
3
  import { renderTeamStatusMarkdown } from "../gjc-runtime/state-renderer";
3
4
  import {
4
5
  buildTeamHudSummary,
@@ -45,6 +46,23 @@ function formatTaskCounts(counts: Record<string, number>): string {
45
46
  .join(" ");
46
47
  }
47
48
 
49
+ function snapshotWriteReceipt(snapshot: GjcTeamSnapshot): Record<string, unknown> {
50
+ return {
51
+ ok: true,
52
+ team_name: snapshot.team_name,
53
+ phase: snapshot.phase,
54
+ state_dir: snapshot.state_dir,
55
+ tmux_session: snapshot.tmux_session,
56
+ tmux_target: snapshot.tmux_target,
57
+ worker_count: snapshot.workers.length,
58
+ task_counts: snapshot.task_counts,
59
+ };
60
+ }
61
+
62
+ function writeReceipt(value: Record<string, unknown>): void {
63
+ process.stdout.write(renderCliWriteReceipt(value));
64
+ }
65
+
48
66
  function parseInputFlag(argv: string[]): Record<string, unknown> {
49
67
  const index = argv.indexOf("--input");
50
68
  if (index < 0) return {};
@@ -124,7 +142,7 @@ export default class Team extends Command {
124
142
  const snapshot = await monitorGjcTeamSnapshot(teamName);
125
143
  await syncTeamHud(snapshot);
126
144
  if (json) {
127
- writeJson(snapshot);
145
+ writeReceipt(snapshotWriteReceipt(snapshot));
128
146
  return;
129
147
  }
130
148
  writeText([
@@ -141,7 +159,7 @@ export default class Team extends Command {
141
159
  const snapshot = await shutdownGjcTeam(teamName);
142
160
  await syncTeamHud(snapshot);
143
161
  if (json) {
144
- writeJson(snapshot);
162
+ writeReceipt(snapshotWriteReceipt(snapshot));
145
163
  return;
146
164
  }
147
165
  writeText([`team: ${snapshot.team_name}`, `phase: ${snapshot.phase}`, `state: ${snapshot.state_dir}`]);
@@ -174,7 +192,7 @@ export default class Team extends Command {
174
192
  // API operations without a resolvable snapshot leave HUD state unchanged.
175
193
  }
176
194
  }
177
- writeJson(result);
195
+ writeReceipt(result as Record<string, unknown>);
178
196
  return;
179
197
  }
180
198
 
@@ -183,7 +201,7 @@ export default class Team extends Command {
183
201
  const snapshot = await startGjcTeam({ ...options, dryRun });
184
202
  await syncTeamHud(snapshot);
185
203
  if (json) {
186
- writeJson(snapshot);
204
+ writeReceipt(snapshotWriteReceipt(snapshot));
187
205
  return;
188
206
  }
189
207
  writeText([
@@ -38,6 +38,7 @@ interface AppKeybindings {
38
38
  "app.session.fork": true;
39
39
  "app.session.resume": true;
40
40
  "app.session.observe": true;
41
+ "app.jobs.open": true;
41
42
  "app.session.togglePath": true;
42
43
  "app.session.toggleSort": true;
43
44
  "app.session.rename": true;
@@ -149,6 +150,11 @@ export const KEYBINDINGS = {
149
150
  defaultKeys: "ctrl+s",
150
151
  description: "Observe subagent sessions",
151
152
  },
153
+
154
+ "app.jobs.open": {
155
+ defaultKeys: "alt+j",
156
+ description: "Open monitor/cron jobs overlay",
157
+ },
152
158
  "app.session.togglePath": {
153
159
  defaultKeys: "ctrl+p",
154
160
  description: "Toggle session path display",
@@ -0,0 +1,157 @@
1
+ import type { ThinkingLevel } from "@gajae-code/agent-core";
2
+ import type { Api, Model } from "@gajae-code/ai";
3
+ import type { AgentSession } from "../session/agent-session";
4
+ import {
5
+ aggregateModelProfileRequiredProviders,
6
+ formatAvailableProfileNames,
7
+ resolveProfileBindings,
8
+ } from "./model-profiles";
9
+ import { type GjcModelAssignmentTargetId, isAuthenticated, type ModelRegistry } from "./model-registry";
10
+ import { resolveModelRoleValue } from "./model-resolver";
11
+ import type { Settings } from "./settings";
12
+
13
+ export interface PrepareModelProfileActivationOptions {
14
+ session: Pick<AgentSession, "model" | "thinkingLevel" | "sessionId">;
15
+ modelRegistry: Pick<
16
+ ModelRegistry,
17
+ | "getModelProfile"
18
+ | "getModelProfiles"
19
+ | "getAvailableModelProfileNames"
20
+ | "getApiKeyForProvider"
21
+ | "getAll"
22
+ | "resolveCanonicalModel"
23
+ | "getCanonicalVariants"
24
+ | "getCanonicalId"
25
+ >;
26
+ settings: Pick<Settings, "get">;
27
+ profileName: string;
28
+ }
29
+
30
+ export interface PreparedModelProfileActivation {
31
+ profileName: string;
32
+ session: Pick<AgentSession, "model" | "thinkingLevel" | "sessionId" | "setModelTemporary">;
33
+ settings: Pick<Settings, "get" | "override" | "set" | "flush">;
34
+ previousModel: Model<Api> | undefined;
35
+ previousThinkingLevel: ThinkingLevel | undefined;
36
+ previousAgentModelOverrides: Record<string, string>;
37
+ defaultModel: Model<Api> | undefined;
38
+ defaultThinkingLevel: ThinkingLevel | undefined;
39
+ agentModelOverrides: Record<string, string>;
40
+ }
41
+
42
+ export function formatModelProfileCredentialError(profileName: string, providers: readonly string[]): string {
43
+ return `Model profile "${profileName}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
44
+ }
45
+
46
+ export async function prepareModelProfileActivation(
47
+ options: PrepareModelProfileActivationOptions,
48
+ ): Promise<PreparedModelProfileActivation> {
49
+ const profile = options.modelRegistry.getModelProfile(options.profileName);
50
+ if (!profile) {
51
+ const available = formatAvailableProfileNames(options.modelRegistry.getModelProfiles());
52
+ throw new Error(`Unknown model profile "${options.profileName}". Available profiles: ${available}`);
53
+ }
54
+
55
+ const missingProviders: string[] = [];
56
+ for (const provider of aggregateModelProfileRequiredProviders(profile.requiredProviders, profile)) {
57
+ const apiKey = await options.modelRegistry.getApiKeyForProvider(provider, options.session.sessionId);
58
+ if (!isAuthenticated(apiKey)) {
59
+ missingProviders.push(provider);
60
+ }
61
+ }
62
+ if (missingProviders.length > 0) {
63
+ throw new Error(formatModelProfileCredentialError(options.profileName, missingProviders));
64
+ }
65
+
66
+ const availableModels = options.modelRegistry.getAll();
67
+ const bindings = resolveProfileBindings(profile);
68
+ const resolvedDefault = bindings.defaultSelector
69
+ ? resolveModelRoleValue(bindings.defaultSelector, availableModels, {
70
+ settings: options.settings as Settings,
71
+ modelRegistry: options.modelRegistry,
72
+ })
73
+ : undefined;
74
+ if (bindings.defaultSelector && !resolvedDefault?.model) {
75
+ throw new Error(
76
+ `Model profile "${options.profileName}" default selector did not resolve: ${bindings.defaultSelector}`,
77
+ );
78
+ }
79
+
80
+ const agentModelOverrides: Record<string, string> = {};
81
+ for (const [role, selector] of Object.entries(bindings.agentModelOverrides) as [
82
+ GjcModelAssignmentTargetId,
83
+ string,
84
+ ][]) {
85
+ const resolved = resolveModelRoleValue(selector, availableModels, {
86
+ settings: options.settings as Settings,
87
+ modelRegistry: options.modelRegistry,
88
+ });
89
+ if (!resolved.model) {
90
+ throw new Error(`Model profile "${options.profileName}" ${role} selector did not resolve: ${selector}`);
91
+ }
92
+ agentModelOverrides[role] = selector;
93
+ }
94
+
95
+ return {
96
+ profileName: options.profileName,
97
+ session: options.session as PreparedModelProfileActivation["session"],
98
+ settings: options.settings as PreparedModelProfileActivation["settings"],
99
+ previousModel: options.session.model,
100
+ previousThinkingLevel: options.session.thinkingLevel,
101
+ previousAgentModelOverrides: { ...options.settings.get("task.agentModelOverrides") },
102
+ defaultModel: resolvedDefault?.model,
103
+ defaultThinkingLevel: resolvedDefault?.thinkingLevel,
104
+ agentModelOverrides,
105
+ };
106
+ }
107
+
108
+ export async function applyPreparedModelProfileActivation(
109
+ prepared: PreparedModelProfileActivation,
110
+ options: { persistDefault?: boolean } = {},
111
+ ): Promise<void> {
112
+ const previousModel = prepared.previousModel;
113
+ const previousThinkingLevel = prepared.previousThinkingLevel;
114
+ const previousAgentModelOverrides = prepared.previousAgentModelOverrides;
115
+ const previousPersistedDefault = prepared.settings.get("modelProfile.default");
116
+ let modelChanged = false;
117
+ let overridesChanged = false;
118
+ let defaultChanged = false;
119
+
120
+ try {
121
+ if (prepared.defaultModel) {
122
+ await prepared.session.setModelTemporary(prepared.defaultModel, prepared.defaultThinkingLevel);
123
+ modelChanged = true;
124
+ }
125
+ if (Object.keys(prepared.agentModelOverrides).length > 0) {
126
+ prepared.settings.override("task.agentModelOverrides", {
127
+ ...prepared.settings.get("task.agentModelOverrides"),
128
+ ...prepared.agentModelOverrides,
129
+ });
130
+ overridesChanged = true;
131
+ }
132
+ if (options.persistDefault) {
133
+ prepared.settings.set("modelProfile.default", prepared.profileName);
134
+ defaultChanged = true;
135
+ await prepared.settings.flush();
136
+ }
137
+ } catch (error) {
138
+ if (defaultChanged) {
139
+ prepared.settings.set("modelProfile.default", previousPersistedDefault);
140
+ }
141
+ if (overridesChanged) {
142
+ prepared.settings.override("task.agentModelOverrides", previousAgentModelOverrides);
143
+ }
144
+ if (modelChanged && previousModel) {
145
+ await prepared.session.setModelTemporary(previousModel, previousThinkingLevel);
146
+ }
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ export async function activateModelProfile(
152
+ options: PrepareModelProfileActivationOptions,
153
+ applyOptions: { persistDefault?: boolean } = {},
154
+ ): Promise<void> {
155
+ const prepared = await prepareModelProfileActivation(options);
156
+ await applyPreparedModelProfileActivation(prepared, applyOptions);
157
+ }
@@ -0,0 +1,155 @@
1
+ import type { GjcModelAssignmentTargetId } from "./model-registry";
2
+ import type { ModelsConfig } from "./models-config-schema";
3
+
4
+ export type ModelProfileRole = GjcModelAssignmentTargetId;
5
+
6
+ export interface ModelProfileDefinition {
7
+ name: string;
8
+ requiredProviders: string[];
9
+ modelMapping: Partial<Record<ModelProfileRole, string>>;
10
+ source: "builtin" | "user";
11
+ }
12
+
13
+ export interface ResolvedProfileBinding {
14
+ defaultSelector?: string;
15
+ agentModelOverrides: Partial<Record<Exclude<ModelProfileRole, "default">, string>>;
16
+ }
17
+
18
+ function parseModelSelectorProvider(selector: string): string | undefined {
19
+ const slashIdx = selector.indexOf("/");
20
+ if (slashIdx <= 0) return undefined;
21
+ return selector.slice(0, slashIdx);
22
+ }
23
+
24
+ export function deriveModelProfileMappedProviders(definition: Pick<ModelProfileDefinition, "modelMapping">): string[] {
25
+ const providers = new Set<string>();
26
+ for (const selector of Object.values(definition.modelMapping)) {
27
+ if (!selector) continue;
28
+ const provider = parseModelSelectorProvider(selector);
29
+ if (provider) providers.add(provider);
30
+ }
31
+ return [...providers].sort((a, b) => a.localeCompare(b));
32
+ }
33
+
34
+ export function aggregateModelProfileRequiredProviders(
35
+ requiredProviders: readonly string[],
36
+ definition: Pick<ModelProfileDefinition, "modelMapping">,
37
+ ): string[] {
38
+ const providers = new Set(requiredProviders);
39
+ for (const provider of deriveModelProfileMappedProviders(definition)) {
40
+ providers.add(provider);
41
+ }
42
+ return [...providers];
43
+ }
44
+
45
+ const profile = (
46
+ name: string,
47
+ requiredProviders: string[],
48
+ modelMapping: Record<ModelProfileRole, string>,
49
+ ): ModelProfileDefinition => ({
50
+ name,
51
+ requiredProviders: aggregateModelProfileRequiredProviders(requiredProviders, { modelMapping }),
52
+ modelMapping,
53
+ source: "builtin",
54
+ });
55
+
56
+ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
57
+ profile("opencode-go-eco", ["opencode-go"], {
58
+ default: "opencode-go/deepseek-v4-flash",
59
+ executor: "opencode-go/qwen3.5-plus",
60
+ architect: "opencode-go/glm-5",
61
+ planner: "opencode-go/minimax-m2.5",
62
+ critic: "opencode-go/kimi-k2.5",
63
+ }),
64
+ profile("opencode-go-standard", ["opencode-go"], {
65
+ default: "opencode-go/kimi-k2.6",
66
+ executor: "opencode-go/qwen3.6-plus",
67
+ architect: "opencode-go/glm-5.1",
68
+ planner: "opencode-go/minimax-m2.7",
69
+ critic: "opencode-go/deepseek-v4-pro",
70
+ }),
71
+ profile("opencode-go-pro", ["opencode-go"], {
72
+ default: "opencode-go/qwen3.7-max",
73
+ executor: "opencode-go/kimi-k2.6",
74
+ architect: "opencode-go/deepseek-v4-pro:high",
75
+ planner: "opencode-go/glm-5.1:high",
76
+ critic: "opencode-go/minimax-m2.7:high",
77
+ }),
78
+ profile("codex-eco", ["openai-codex"], {
79
+ default: "openai-codex/gpt-5.4-mini",
80
+ executor: "openai-codex/gpt-5.4-nano",
81
+ architect: "openai-codex/gpt-5.4-mini",
82
+ planner: "openai-codex/gpt-5.4-mini",
83
+ critic: "openai-codex/gpt-5.4-mini",
84
+ }),
85
+ profile("codex-standard", ["openai-codex"], {
86
+ default: "openai-codex/gpt-5.4:medium",
87
+ executor: "openai-codex/gpt-5.4:low",
88
+ architect: "openai-codex/gpt-5.4:xhigh",
89
+ planner: "openai-codex/gpt-5.4:medium",
90
+ critic: "openai-codex/gpt-5.4:high",
91
+ }),
92
+ profile("codex-pro", ["openai-codex"], {
93
+ default: "openai-codex/gpt-5.5",
94
+ executor: "openai-codex/gpt-5.2-codex",
95
+ architect: "openai-codex/gpt-5.1-codex-max:high",
96
+ planner: "openai-codex/gpt-5.5:high",
97
+ critic: "openai-codex/gpt-5.3-codex-spark:high",
98
+ }),
99
+ profile("opencode-go-codex-eco", ["opencode-go", "openai-codex"], {
100
+ default: "opencode-go/deepseek-v4-flash",
101
+ executor: "opencode-go/qwen3.5-plus",
102
+ architect: "openai-codex/gpt-5.4-mini",
103
+ planner: "openai-codex/gpt-5.4-mini",
104
+ critic: "openai-codex/gpt-5.4-mini",
105
+ }),
106
+ profile("opencode-go-codex-standard", ["opencode-go", "openai-codex"], {
107
+ default: "opencode-go/kimi-k2.6",
108
+ executor: "opencode-go/qwen3.6-plus",
109
+ architect: "openai-codex/gpt-5.4",
110
+ planner: "openai-codex/gpt-5.4",
111
+ critic: "openai-codex/gpt-5.4",
112
+ }),
113
+ profile("opencode-go-codex-pro", ["opencode-go", "openai-codex"], {
114
+ default: "opencode-go/qwen3.7-max",
115
+ executor: "opencode-go/kimi-k2.6",
116
+ architect: "openai-codex/gpt-5.1-codex-max:high",
117
+ planner: "openai-codex/gpt-5.5:high",
118
+ critic: "openai-codex/gpt-5.3-codex-spark:high",
119
+ }),
120
+ ];
121
+
122
+ export function mergeModelProfiles(userProfiles?: ModelsConfig["profiles"]): Map<string, ModelProfileDefinition> {
123
+ const profiles = new Map<string, ModelProfileDefinition>();
124
+ for (const definition of BUILTIN_MODEL_PROFILES) {
125
+ profiles.set(definition.name, {
126
+ ...definition,
127
+ requiredProviders: [...definition.requiredProviders],
128
+ modelMapping: { ...definition.modelMapping },
129
+ });
130
+ }
131
+ for (const [name, definition] of Object.entries(userProfiles ?? {})) {
132
+ const modelMapping = { ...definition.model_mapping };
133
+ profiles.set(name, {
134
+ name,
135
+ requiredProviders: aggregateModelProfileRequiredProviders(definition.required_providers, { modelMapping }),
136
+ modelMapping,
137
+ source: "user",
138
+ });
139
+ }
140
+ return profiles;
141
+ }
142
+
143
+ export function resolveProfileBindings(definition: ModelProfileDefinition): ResolvedProfileBinding {
144
+ const { default: defaultSelector, executor, architect, planner, critic } = definition.modelMapping;
145
+ const agentModelOverrides: ResolvedProfileBinding["agentModelOverrides"] = {};
146
+ if (executor !== undefined) agentModelOverrides.executor = executor;
147
+ if (architect !== undefined) agentModelOverrides.architect = architect;
148
+ if (planner !== undefined) agentModelOverrides.planner = planner;
149
+ if (critic !== undefined) agentModelOverrides.critic = critic;
150
+ return { defaultSelector, agentModelOverrides };
151
+ }
152
+
153
+ export function formatAvailableProfileNames(profiles: ReadonlyMap<string, ModelProfileDefinition>): string {
154
+ return [...profiles.keys()].sort((a, b) => a.localeCompare(b)).join(", ");
155
+ }
@@ -43,6 +43,7 @@ import {
43
43
  formatCanonicalVariantSelector,
44
44
  type ModelEquivalenceConfig,
45
45
  } from "./model-equivalence";
46
+ import { type ModelProfileDefinition, mergeModelProfiles } from "./model-profiles";
46
47
  import {
47
48
  type ModelOverride,
48
49
  type ModelsConfig,
@@ -511,6 +512,7 @@ interface CustomModelsResult {
511
512
  configuredProviders?: Set<string>;
512
513
  equivalence?: ModelEquivalenceConfig;
513
514
  modelBindings?: NonNullable<ModelsConfig["modelBindings"]>;
515
+ profiles?: ModelsConfig["profiles"];
514
516
  error?: ConfigError;
515
517
  found: boolean;
516
518
  }
@@ -954,6 +956,7 @@ export class ModelRegistry {
954
956
  #modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
955
957
  #equivalenceConfig: ModelEquivalenceConfig | undefined;
956
958
  #configuredModelBindings: NonNullable<ModelsConfig["modelBindings"]> | undefined;
959
+ #modelProfiles: Map<string, ModelProfileDefinition> = mergeModelProfiles();
957
960
  #modelBindingsTargetSettings: Settings | undefined;
958
961
  #appliedModelBindingRoles = new Set<string>();
959
962
  #appliedAgentModelBindingOverrides = new Set<string>();
@@ -1093,6 +1096,7 @@ export class ModelRegistry {
1093
1096
  configuredProviders = new Set(),
1094
1097
  equivalence,
1095
1098
  modelBindings,
1099
+ profiles,
1096
1100
  error: configError,
1097
1101
  } = this.#loadCustomModels();
1098
1102
  this.#configError = configError;
@@ -1103,6 +1107,7 @@ export class ModelRegistry {
1103
1107
  this.#modelOverrides = modelOverrides;
1104
1108
  this.#equivalenceConfig = equivalence;
1105
1109
  this.#configuredModelBindings = modelBindings;
1110
+ this.#modelProfiles = mergeModelProfiles(profiles);
1106
1111
 
1107
1112
  this.#addImplicitDiscoverableProviders(configuredProviders);
1108
1113
  const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
@@ -1343,6 +1348,7 @@ export class ModelRegistry {
1343
1348
  discoverableProviders: [],
1344
1349
  configuredProviders: new Set(),
1345
1350
  error,
1351
+ profiles: undefined,
1346
1352
  found: true,
1347
1353
  };
1348
1354
  } else if (status === "not-found") {
@@ -1353,6 +1359,7 @@ export class ModelRegistry {
1353
1359
  keylessProviders: new Set(),
1354
1360
  discoverableProviders: [],
1355
1361
  configuredProviders: new Set(),
1362
+ profiles: undefined,
1356
1363
  found: false,
1357
1364
  };
1358
1365
  }
@@ -1441,10 +1448,22 @@ export class ModelRegistry {
1441
1448
  configuredProviders,
1442
1449
  equivalence: value.equivalence,
1443
1450
  modelBindings: value.modelBindings,
1451
+ profiles: value.profiles,
1444
1452
  found: true,
1445
1453
  };
1446
1454
  }
1447
1455
 
1456
+ getModelProfiles(): Map<string, ModelProfileDefinition> {
1457
+ return new Map(this.#modelProfiles);
1458
+ }
1459
+
1460
+ getModelProfile(name: string): ModelProfileDefinition | undefined {
1461
+ return this.#modelProfiles.get(name);
1462
+ }
1463
+
1464
+ getAvailableModelProfileNames(): string[] {
1465
+ return [...this.#modelProfiles.keys()].sort((a, b) => a.localeCompare(b));
1466
+ }
1448
1467
  applyConfiguredModelBindings(targetSettings: Settings): void {
1449
1468
  this.#modelBindingsTargetSettings = targetSettings;
1450
1469
  this.#applyConfiguredModelBindingsToTarget();
@@ -764,6 +764,7 @@ export async function resolveModelOverrideWithAuthFallback(
764
764
  parentActiveModelPattern: string | undefined,
765
765
  modelRegistry: ModelLookupRegistry & Pick<ModelRegistry, "getApiKey">,
766
766
  settings?: Settings,
767
+ sessionId?: string,
767
768
  ): Promise<{
768
769
  model?: Model<Api>;
769
770
  thinkingLevel?: ThinkingLevel;
@@ -775,7 +776,7 @@ export async function resolveModelOverrideWithAuthFallback(
775
776
  return { ...primary, authFallbackUsed: false };
776
777
  }
777
778
 
778
- const primaryKey = await modelRegistry.getApiKey(primary.model);
779
+ const primaryKey = await modelRegistry.getApiKey(primary.model, sessionId);
779
780
  if (primaryKey === kNoAuth || isAuthenticated(primaryKey)) {
780
781
  return { ...primary, authFallbackUsed: false };
781
782
  }
@@ -787,7 +788,7 @@ export async function resolveModelOverrideWithAuthFallback(
787
788
  if (modelsAreEqual(fallback.model, primary.model)) {
788
789
  return { ...primary, authFallbackUsed: false };
789
790
  }
790
- const fallbackKey = await modelRegistry.getApiKey(fallback.model);
791
+ const fallbackKey = await modelRegistry.getApiKey(fallback.model, sessionId);
791
792
  if (!isAuthenticated(fallbackKey)) {
792
793
  return { ...primary, authFallbackUsed: false };
793
794
  }
@@ -76,6 +76,39 @@ const ModelBindingsSchema = z.object({
76
76
  modelRoles: z.record(z.string(), z.string().min(1)).optional(),
77
77
  agentModelOverrides: z.record(z.string(), z.string().min(1)).optional(),
78
78
  });
79
+ export const ProfileRoleSchema = z.enum(["default", "executor", "architect", "planner", "critic"]);
80
+
81
+ function isValidProfileModelSelector(value: string): boolean {
82
+ if (value.includes(",")) return false;
83
+ const slashIdx = value.indexOf("/");
84
+ if (slashIdx <= 0 || slashIdx === value.length - 1) return false;
85
+ const provider = value.slice(0, slashIdx);
86
+ const modelId = value.slice(slashIdx + 1);
87
+ if (!provider || !modelId) return false;
88
+ const parts = modelId.split(":");
89
+ if (parts.length > 2) return false;
90
+ const [base, suffix] = parts;
91
+ if (!base) return false;
92
+ return suffix === undefined || ["minimal", "low", "medium", "high", "xhigh"].includes(suffix);
93
+ }
94
+
95
+ export const ProfileModelSelectorSchema = z
96
+ .string()
97
+ .min(1)
98
+ .refine(value => isValidProfileModelSelector(value), {
99
+ message: "Expected provider/modelId with optional :effort suffix",
100
+ });
101
+
102
+ export const ProfileModelMappingSchema = z.partialRecord(ProfileRoleSchema, ProfileModelSelectorSchema);
103
+
104
+ export const ProfileDefinitionSchema = z
105
+ .object({
106
+ required_providers: z.array(z.string().min(1)).min(1),
107
+ model_mapping: ProfileModelMappingSchema,
108
+ })
109
+ .strict();
110
+
111
+ export const ProfilesSchema = z.record(z.string().min(1), ProfileDefinitionSchema);
79
112
 
80
113
  const ModelDefinitionSchema = z
81
114
  .object({
@@ -205,7 +238,10 @@ export const ModelsConfigSchema = z
205
238
  providers: z.record(z.string(), ProviderConfigSchema).optional(),
206
239
  modelBindings: ModelBindingsSchema.optional(),
207
240
  equivalence: EquivalenceConfigSchema.optional(),
241
+ profiles: ProfilesSchema.optional(),
208
242
  })
209
243
  .strict();
210
244
 
211
245
  export type ModelsConfig = z.infer<typeof ModelsConfigSchema>;
246
+ export type ModelProfileConfig = z.infer<typeof ProfileDefinitionSchema>;
247
+ export type ModelProfilesConfig = z.infer<typeof ProfilesSchema>;
@@ -74,6 +74,7 @@ export type StatusLineSegmentId =
74
74
  | "git"
75
75
  | "pr"
76
76
  | "subagents"
77
+ | "jobs"
77
78
  | "token_in"
78
79
  | "token_out"
79
80
  | "token_total"
@@ -313,6 +314,16 @@ export const SETTINGS_SCHEMA = {
313
314
  disabledExtensions: { type: "array", default: DEFAULT_DISABLED_EXTENSIONS },
314
315
 
315
316
  modelRoles: { type: "record", default: EMPTY_STRING_RECORD },
317
+ "modelProfile.default": {
318
+ type: "string",
319
+ default: undefined,
320
+ ui: {
321
+ tab: "model",
322
+ label: "Default Model Profile",
323
+ description: "Model profile applied automatically at startup",
324
+ options: "runtime",
325
+ },
326
+ },
316
327
 
317
328
  modelTags: { type: "record", default: EMPTY_MODEL_TAGS_RECORD },
318
329
 
@@ -2352,11 +2363,12 @@ export const SETTINGS_SCHEMA = {
2352
2363
 
2353
2364
  "task.maxConcurrency": {
2354
2365
  type: "number",
2355
- default: 32,
2366
+ default: 8,
2356
2367
  ui: {
2357
2368
  tab: "tasks",
2358
2369
  label: "Max Concurrent Tasks",
2359
- description: "Concurrent limit for subagents",
2370
+ description:
2371
+ "Safer concurrent limit for subagents; higher fan-out still requires an explicit plan above 4 tasks.",
2360
2372
  options: [
2361
2373
  { value: "0", label: "Unlimited" },
2362
2374
  { value: "1", label: "1 task" },
@@ -2408,7 +2420,8 @@ export const SETTINGS_SCHEMA = {
2408
2420
  ui: {
2409
2421
  tab: "tasks",
2410
2422
  label: "Fork Context Max Tokens",
2411
- description: "Approximate token cap for fork-context seeds. 0 uses 25% of the target model context window.",
2423
+ description:
2424
+ "Approximate token cap for explicit full fork-context seeds. 0 uses 15% of the target model context window, with a 15k fallback when the window is unknown.",
2412
2425
  },
2413
2426
  },
2414
2427
 
package/src/dap/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { logger, ptree } from "@gajae-code/utils";
2
+ import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
2
3
  import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
3
4
  import { ToolAbortError } from "../tools/tool-errors";
4
5
  import type {
@@ -532,15 +533,28 @@ export class DapClient {
532
533
  }
533
534
  }
534
535
 
535
- #handleProcessExit(): void {
536
+ async #handleProcessExit(): Promise<void> {
536
537
  if (this.#disposed) return;
537
538
  this.#disposed = true;
538
539
  const stderr = this.proc.peekStderr().trim();
539
540
  const exitCode = this.proc.exitCode;
541
+ const crashNotice = formatCrashDiagnosticNotice(
542
+ await writeCrashReport(
543
+ {
544
+ kind: "dap",
545
+ command: [this.adapter.resolvedCommand, ...this.adapter.args],
546
+ exitCode,
547
+ stderr,
548
+ protocol: this.adapter.connectMode ?? "stdio",
549
+ },
550
+ { cwd: this.cwd },
551
+ ),
552
+ );
553
+ const diagnosticSuffix = crashNotice ? `\n${crashNotice}` : "";
540
554
  const error = new Error(
541
555
  stderr
542
- ? `DAP adapter exited (code ${exitCode}): ${stderr}`
543
- : `DAP adapter exited unexpectedly (code ${exitCode})`,
556
+ ? `DAP adapter exited (code ${exitCode}): ${stderr}${diagnosticSuffix}`
557
+ : `DAP adapter exited unexpectedly (code ${exitCode})${diagnosticSuffix}`,
544
558
  );
545
559
  this.#rejectPendingRequests(error);
546
560
  }