@gajae-code/coding-agent 0.4.2 → 0.4.4

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 (115) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/types/async/job-manager.d.ts +44 -1
  3. package/dist/types/cli/setup-cli.d.ts +14 -1
  4. package/dist/types/commands/coordinator.d.ts +19 -0
  5. package/dist/types/commands/mcp-serve.d.ts +24 -0
  6. package/dist/types/commands/setup.d.ts +41 -0
  7. package/dist/types/commit/model-selection.d.ts +1 -1
  8. package/dist/types/config/model-registry.d.ts +3 -1
  9. package/dist/types/config/model-resolver.d.ts +1 -19
  10. package/dist/types/config/models-config-schema.d.ts +12 -0
  11. package/dist/types/config/settings-schema.d.ts +15 -1
  12. package/dist/types/coordinator/contract.d.ts +4 -0
  13. package/dist/types/coordinator-mcp/policy.d.ts +24 -0
  14. package/dist/types/coordinator-mcp/safety.d.ts +26 -0
  15. package/dist/types/coordinator-mcp/server.d.ts +52 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +13 -0
  17. package/dist/types/gjc-runtime/goal-mode-request.d.ts +8 -1
  18. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  19. package/dist/types/harness-control-plane/types.d.ts +7 -2
  20. package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
  21. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  22. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  23. package/dist/types/modes/shared/agent-wire/command-contract.d.ts +18 -0
  24. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +84 -0
  25. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +14 -7
  26. package/dist/types/modes/shared/agent-wire/event-observation.d.ts +37 -0
  27. package/dist/types/modes/shared/agent-wire/protocol.d.ts +13 -34
  28. package/dist/types/session/agent-session.d.ts +12 -1
  29. package/dist/types/session/session-manager.d.ts +1 -1
  30. package/dist/types/setup/hermes-setup.d.ts +71 -0
  31. package/dist/types/task/render.d.ts +7 -1
  32. package/dist/types/tools/bash.d.ts +2 -0
  33. package/dist/types/tools/browser/actions.d.ts +54 -0
  34. package/dist/types/tools/browser.d.ts +80 -0
  35. package/dist/types/tools/image-gen.d.ts +1 -0
  36. package/dist/types/tools/index.d.ts +3 -1
  37. package/dist/types/tools/job.d.ts +1 -1
  38. package/dist/types/tools/subagent-render.d.ts +25 -0
  39. package/dist/types/tools/subagent.d.ts +5 -1
  40. package/package.json +7 -7
  41. package/src/async/job-manager.ts +163 -2
  42. package/src/cli/setup-cli.ts +86 -2
  43. package/src/cli.ts +2 -0
  44. package/src/commands/coordinator.ts +70 -0
  45. package/src/commands/mcp-serve.ts +62 -0
  46. package/src/commands/setup.ts +30 -1
  47. package/src/commands/ultragoal.ts +7 -1
  48. package/src/commit/agentic/index.ts +2 -2
  49. package/src/commit/model-selection.ts +7 -22
  50. package/src/commit/pipeline.ts +2 -2
  51. package/src/config/model-registry.ts +17 -9
  52. package/src/config/model-resolver.ts +14 -84
  53. package/src/config/models-config-schema.ts +2 -0
  54. package/src/config/settings-schema.ts +14 -1
  55. package/src/coordinator/contract.ts +20 -0
  56. package/src/coordinator-mcp/policy.ts +160 -0
  57. package/src/coordinator-mcp/safety.ts +80 -0
  58. package/src/coordinator-mcp/server.ts +1316 -0
  59. package/src/extensibility/extensions/types.ts +13 -0
  60. package/src/gjc-runtime/goal-mode-request.ts +21 -1
  61. package/src/gjc-runtime/session-state-sidecar.ts +79 -0
  62. package/src/harness-control-plane/owner.ts +3 -3
  63. package/src/harness-control-plane/rpc-adapter.ts +7 -1
  64. package/src/harness-control-plane/types.ts +8 -11
  65. package/src/internal-urls/docs-index.generated.ts +6 -5
  66. package/src/memories/index.ts +1 -1
  67. package/src/modes/acp/acp-agent.ts +17 -9
  68. package/src/modes/acp/acp-event-mapper.ts +33 -1
  69. package/src/modes/components/custom-editor.ts +19 -3
  70. package/src/modes/components/hook-selector.ts +109 -5
  71. package/src/modes/controllers/extension-ui-controller.ts +16 -1
  72. package/src/modes/controllers/input-controller.ts +27 -7
  73. package/src/modes/controllers/selector-controller.ts +7 -1
  74. package/src/modes/interactive-mode.ts +3 -1
  75. package/src/modes/rpc/rpc-client.ts +16 -3
  76. package/src/modes/rpc/rpc-mode.ts +5 -2
  77. package/src/modes/shared/agent-wire/command-contract.ts +18 -0
  78. package/src/modes/shared/agent-wire/event-contract.ts +147 -0
  79. package/src/modes/shared/agent-wire/event-envelope.ts +35 -16
  80. package/src/modes/shared/agent-wire/event-observation.ts +397 -0
  81. package/src/modes/shared/agent-wire/protocol.ts +24 -81
  82. package/src/modes/utils/context-usage.ts +2 -2
  83. package/src/prompts/agents/architect.md +6 -0
  84. package/src/prompts/agents/critic.md +6 -0
  85. package/src/prompts/agents/explore.md +1 -1
  86. package/src/prompts/agents/plan.md +1 -1
  87. package/src/prompts/agents/planner.md +8 -1
  88. package/src/prompts/agents/reviewer.md +1 -1
  89. package/src/prompts/tools/browser.md +3 -2
  90. package/src/runtime-mcp/manager.ts +15 -2
  91. package/src/sdk.ts +3 -1
  92. package/src/session/agent-session.ts +66 -4
  93. package/src/session/session-manager.ts +1 -1
  94. package/src/setup/hermes/templates/operator-instructions.v1.md +29 -0
  95. package/src/setup/hermes-setup.ts +429 -0
  96. package/src/task/agents.ts +1 -1
  97. package/src/task/index.ts +2 -0
  98. package/src/task/render.ts +14 -0
  99. package/src/tools/ask.ts +30 -10
  100. package/src/tools/bash.ts +6 -1
  101. package/src/tools/browser/actions.ts +189 -0
  102. package/src/tools/browser.ts +91 -1
  103. package/src/tools/image-gen.ts +42 -15
  104. package/src/tools/index.ts +7 -1
  105. package/src/tools/inspect-image.ts +10 -8
  106. package/src/tools/job.ts +12 -2
  107. package/src/tools/monitor.ts +98 -17
  108. package/src/tools/renderers.ts +2 -0
  109. package/src/tools/subagent-render.ts +160 -0
  110. package/src/tools/subagent.ts +49 -7
  111. package/src/utils/commit-message-generator.ts +6 -13
  112. package/src/utils/title-generator.ts +1 -1
  113. package/dist/types/harness-control-plane/frame-mapper.d.ts +0 -29
  114. package/src/harness-control-plane/frame-mapper.ts +0 -286
  115. package/src/priority.json +0 -37
@@ -5,7 +5,7 @@ import { Args, Command, Flags } from "@gajae-code/utils/cli";
5
5
  import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
8
- const COMPONENTS: SetupComponent[] = ["defaults", "hooks", "provider", "python", "stt"];
8
+ const COMPONENTS: SetupComponent[] = ["defaults", "hermes", "hooks", "provider", "python", "stt"];
9
9
 
10
10
  export default class Setup extends Command {
11
11
  static description = "Install GJC defaults or optional feature dependencies";
@@ -22,6 +22,22 @@ export default class Setup extends Command {
22
22
  check: Flags.boolean({ char: "c", description: "Check if dependencies are installed" }),
23
23
  force: Flags.boolean({ char: "f", description: "Overwrite existing default workflow skill files" }),
24
24
  json: Flags.boolean({ description: "Output status as JSON" }),
25
+ smoke: Flags.boolean({ description: "Run Hermes MCP setup smoke checks" }),
26
+ install: Flags.boolean({ description: "Install generated Hermes setup files" }),
27
+ root: Flags.string({ description: "Allowed Hermes MCP workdir/artifact root (repeatable)", multiple: true }),
28
+ repo: Flags.string({ description: "Hermes MCP repo namespace" }),
29
+ profile: Flags.string({ description: "Hermes MCP profile namespace" }),
30
+ "session-command": Flags.string({ description: "Explicit GJC session command for Hermes to launch" }),
31
+ "state-root": Flags.string({ description: "Hermes MCP coordination state root" }),
32
+ mutation: Flags.string({
33
+ description: "Hermes MCP mutation classes: sessions,questions,reports,all",
34
+ multiple: true,
35
+ }),
36
+ "artifact-byte-cap": Flags.string({ description: "Hermes MCP artifact read byte cap" }),
37
+ "server-key": Flags.string({ description: "Hermes MCP server key in coordinator config" }),
38
+ "gjc-command": Flags.string({ description: "Command used to start `gjc mcp-serve coordinator`" }),
39
+ target: Flags.string({ description: "Hermes config file target for config-only install" }),
40
+ "profile-dir": Flags.string({ description: "Hermes profile directory for full setup install" }),
25
41
  preset: Flags.string({ description: "Provider preset: minimax, minimax-cn, or glm" }),
26
42
  compat: Flags.string({ description: "Provider compatibility: openai or anthropic" }),
27
43
  provider: Flags.string({ description: "Provider id to add to models.yml" }),
@@ -46,6 +62,19 @@ export default class Setup extends Command {
46
62
  apiKeyEnv: flags["api-key-env"],
47
63
  model: flags.model,
48
64
  modelsPath: flags["models-path"],
65
+ smoke: flags.smoke,
66
+ install: flags.install,
67
+ root: flags.root,
68
+ repo: flags.repo,
69
+ profile: flags.profile,
70
+ sessionCommand: flags["session-command"],
71
+ stateRoot: flags["state-root"],
72
+ mutation: flags.mutation,
73
+ artifactByteCap: flags["artifact-byte-cap"],
74
+ serverKey: flags["server-key"],
75
+ gjcCommand: flags["gjc-command"],
76
+ target: flags.target,
77
+ profileDir: flags["profile-dir"],
49
78
  },
50
79
  };
51
80
  await initTheme();
@@ -1,6 +1,7 @@
1
1
  import { Command } from "@gajae-code/utils/cli";
2
2
  import {
3
3
  GJC_SESSION_FILE_ENV,
4
+ GJC_SESSION_ID_ENV,
4
5
  isUltragoalCreateGoalsInvocation,
5
6
  readUltragoalGjcObjective,
6
7
  writeCurrentSessionGoalModeState,
@@ -28,6 +29,11 @@ export default class Ultragoal extends Command {
28
29
  sessionFile: process.env[GJC_SESSION_FILE_ENV],
29
30
  objective,
30
31
  });
31
- await writePendingGoalModeRequest({ cwd, objective, goalsPath });
32
+ await writePendingGoalModeRequest({
33
+ cwd,
34
+ objective,
35
+ goalsPath,
36
+ sessionId: process.env[GJC_SESSION_ID_ENV],
37
+ });
32
38
  }
33
39
  }
@@ -5,7 +5,7 @@ import { applyChangelogProposals } from "../../commit/changelog";
5
5
  import { detectChangelogBoundaries } from "../../commit/changelog/detect";
6
6
  import { parseUnreleasedSection } from "../../commit/changelog/parse";
7
7
  import { formatCommitMessage } from "../../commit/message";
8
- import { resolvePrimaryModel, resolveSmolModel } from "../../commit/model-selection";
8
+ import { resolvePrimaryModel, resolveSecondaryCommitModel } from "../../commit/model-selection";
9
9
  import type { CommitCommandArgs, ConventionalAnalysis } from "../../commit/types";
10
10
  import { ModelRegistry } from "../../config/model-registry";
11
11
  import { Settings } from "../../config/settings";
@@ -47,7 +47,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
47
47
  const { model: primaryModel, apiKey: primaryApiKey } = primaryModelResult;
48
48
  process.stdout.write(` └─ ${primaryModel.name}\n`);
49
49
 
50
- const { model: agentModel, thinkingLevel: agentThinkingLevel } = await resolveSmolModel(
50
+ const { model: agentModel, thinkingLevel: agentThinkingLevel } = await resolveSecondaryCommitModel(
51
51
  settings,
52
52
  modelRegistry,
53
53
  primaryModel,
@@ -1,14 +1,7 @@
1
1
  import type { ThinkingLevel } from "@gajae-code/agent-core";
2
2
  import type { Api, Model } from "@gajae-code/ai";
3
- import { MODEL_ROLE_IDS } from "../config/model-registry";
4
- import {
5
- type ModelLookupRegistry,
6
- parseModelPattern,
7
- resolveModelRoleValue,
8
- resolveRoleSelection,
9
- } from "../config/model-resolver";
3
+ import { type ModelLookupRegistry, resolveModelRoleValue, resolveRoleSelection } from "../config/model-resolver";
10
4
  import type { Settings } from "../config/settings";
11
- import MODEL_PRIO from "../priority.json" with { type: "json" };
12
5
 
13
6
  export interface ResolvedCommitModel {
14
7
  model: Model<Api>;
@@ -29,7 +22,7 @@ export async function resolvePrimaryModel(
29
22
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
30
23
  const resolved = override
31
24
  ? resolveModelRoleValue(override, available, { settings, matchPreferences, modelRegistry })
32
- : resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available, modelRegistry);
25
+ : resolveRoleSelection(["default"], settings, available, modelRegistry);
33
26
  const model = resolved?.model;
34
27
  if (!model) {
35
28
  throw new Error("No model available for commit generation");
@@ -41,25 +34,17 @@ export async function resolvePrimaryModel(
41
34
  return { model, apiKey, thinkingLevel: resolved?.thinkingLevel };
42
35
  }
43
36
 
44
- export async function resolveSmolModel(
37
+ export async function resolveSecondaryCommitModel(
45
38
  settings: Settings,
46
39
  modelRegistry: CommitModelRegistry,
47
40
  fallbackModel: Model<Api>,
48
41
  fallbackApiKey: string,
49
42
  ): Promise<ResolvedCommitModel> {
50
43
  const available = modelRegistry.getAvailable();
51
- const resolvedSmol = resolveRoleSelection(["smol"], settings, available, modelRegistry);
52
- if (resolvedSmol?.model) {
53
- const apiKey = await modelRegistry.getApiKey(resolvedSmol.model);
54
- if (apiKey) return { model: resolvedSmol.model, apiKey, thinkingLevel: resolvedSmol.thinkingLevel };
55
- }
56
-
57
- const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
58
- for (const pattern of MODEL_PRIO.smol) {
59
- const candidate = parseModelPattern(pattern, available, matchPreferences, { modelRegistry }).model;
60
- if (!candidate) continue;
61
- const apiKey = await modelRegistry.getApiKey(candidate);
62
- if (apiKey) return { model: candidate, apiKey };
44
+ const resolved = resolveRoleSelection(["default"], settings, available, modelRegistry);
45
+ if (resolved?.model) {
46
+ const apiKey = await modelRegistry.getApiKey(resolved.model);
47
+ if (apiKey) return { model: resolved.model, apiKey, thinkingLevel: resolved.thinkingLevel };
63
48
  }
64
49
 
65
50
  return { model: fallbackModel, apiKey: fallbackApiKey };
@@ -18,7 +18,7 @@ import {
18
18
  import { runChangelogFlow } from "./changelog";
19
19
  import { runMapReduceAnalysis, shouldUseMapReduce } from "./map-reduce";
20
20
  import { formatCommitMessage } from "./message";
21
- import { resolvePrimaryModel, resolveSmolModel } from "./model-selection";
21
+ import { resolvePrimaryModel, resolveSecondaryCommitModel } from "./model-selection";
22
22
  import summaryRetryPrompt from "./prompts/summary-retry.md" with { type: "text" };
23
23
  import typesDescriptionPrompt from "./prompts/types-description.md" with { type: "text" };
24
24
  import type { CommitCommandArgs, ConventionalAnalysis } from "./types";
@@ -56,7 +56,7 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
56
56
  model: smolModel,
57
57
  apiKey: smolApiKey,
58
58
  thinkingLevel: smolThinkingLevel,
59
- } = await resolveSmolModel(settings, modelRegistry, primaryModel, primaryApiKey);
59
+ } = await resolveSecondaryCommitModel(settings, modelRegistry, primaryModel, primaryApiKey);
60
60
 
61
61
  let stagedFiles = await git.diff.changedFiles(cwd, { cached: true });
62
62
  if (stagedFiles.length === 0) {
@@ -62,7 +62,7 @@ export function isAuthenticated(apiKey: string | undefined | null): apiKey is st
62
62
  return Boolean(apiKey) && apiKey !== kNoAuth;
63
63
  }
64
64
 
65
- export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "designer" | "commit" | "task";
65
+ export type ModelRole = "default";
66
66
 
67
67
  export interface ModelRoleInfo {
68
68
  tag?: string;
@@ -72,16 +72,9 @@ export interface ModelRoleInfo {
72
72
 
73
73
  export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
74
74
  default: { tag: "DEFAULT", name: "Default", color: "success" },
75
- smol: { tag: "SMOL", name: "Fast", color: "warning" },
76
- slow: { tag: "SLOW", name: "Thinking", color: "accent" },
77
- vision: { tag: "VISION", name: "Vision", color: "error" },
78
- plan: { tag: "PLAN", name: "Architect", color: "muted" },
79
- designer: { tag: "DESIGNER", name: "Designer", color: "muted" },
80
- commit: { tag: "COMMIT", name: "Commit", color: "dim" },
81
- task: { tag: "TASK", name: "Subtask", color: "muted" },
82
75
  };
83
76
 
84
- export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "vision", "plan", "designer", "commit", "task"];
77
+ export const MODEL_ROLE_IDS: ModelRole[] = ["default"];
85
78
 
86
79
  export type GjcModelAssignmentTargetId = "default" | "executor" | "architect" | "planner" | "critic";
87
80
 
@@ -688,6 +681,7 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
688
681
  if (override.reasoning !== undefined) result.reasoning = override.reasoning;
689
682
  if (override.thinking !== undefined) result.thinking = override.thinking as ThinkingConfig;
690
683
  if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
684
+ if (override.output !== undefined) result.output = override.output as ("text" | "image")[];
691
685
  if (override.cacheRetention !== undefined) result.cacheRetention = override.cacheRetention;
692
686
  if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
693
687
  if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
@@ -718,6 +712,7 @@ interface CustomModelDefinitionLike {
718
712
  reasoning?: boolean;
719
713
  thinking?: ThinkingConfig;
720
714
  input?: ("text" | "image")[];
715
+ output?: ("text" | "image")[];
721
716
  cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
722
717
  contextWindow?: number;
723
718
  maxTokens?: number;
@@ -743,6 +738,7 @@ type CustomModelOverlay = {
743
738
  reasoning?: boolean;
744
739
  thinking?: ThinkingConfig;
745
740
  input?: ("text" | "image")[];
741
+ output?: ("text" | "image")[];
746
742
  cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
747
743
  contextWindow?: number;
748
744
  maxTokens?: number;
@@ -818,6 +814,7 @@ function buildCustomModelOverlay(
818
814
  reasoning: modelDef.reasoning,
819
815
  thinking: modelDef.thinking as ThinkingConfig | undefined,
820
816
  input: modelDef.input as ("text" | "image")[] | undefined,
817
+ output: modelDef.output as ("text" | "image")[] | undefined,
821
818
  cost: modelDef.cost,
822
819
  contextWindow: modelDef.contextWindow,
823
820
  maxTokens: modelDef.maxTokens,
@@ -910,6 +907,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
910
907
  reference?.cost ??
911
908
  (options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
912
909
  const input = resolvedModel.input ?? reference?.input ?? (options.useDefaults ? ["text"] : undefined);
910
+ const output = resolvedModel.output ?? reference?.output;
913
911
  return enrichModelThinking({
914
912
  id: resolvedModel.id,
915
913
  name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
@@ -919,6 +917,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
919
917
  reasoning: resolvedModel.reasoning ?? reference?.reasoning ?? (options.useDefaults ? false : undefined),
920
918
  thinking: resolvedModel.thinking ?? reference?.thinking,
921
919
  input: input as ("text" | "image")[],
920
+ output: output as ("text" | "image")[] | undefined,
922
921
  cost,
923
922
  contextWindow:
924
923
  resolvedModel.contextWindow ?? reference?.contextWindow ?? (options.useDefaults ? 128000 : undefined),
@@ -1213,6 +1212,7 @@ export class ModelRegistry {
1213
1212
  reasoning: customModel.reasoning ?? existingModel.reasoning,
1214
1213
  thinking: customModel.thinking ?? existingModel.thinking,
1215
1214
  input: customModel.input ?? existingModel.input,
1215
+ output: customModel.output ?? existingModel.output,
1216
1216
  cost: customModel.cost ?? existingModel.cost,
1217
1217
  contextWindow: customModel.contextWindow ?? existingModel.contextWindow,
1218
1218
  maxTokens: customModel.maxTokens ?? existingModel.maxTokens,
@@ -2325,6 +2325,14 @@ export class ModelRegistry {
2325
2325
  fallback: 3,
2326
2326
  };
2327
2327
  return [...variants].sort((left, right) => {
2328
+ // Prefer vision-capable variants over configured provider order so an
2329
+ // ambiguous canonical id never resolves to a text-only namesake when a
2330
+ // vision-capable variant of the same id is available.
2331
+ const leftVision = left.model.input.includes("image") ? 0 : 1;
2332
+ const rightVision = right.model.input.includes("image") ? 0 : 1;
2333
+ if (leftVision !== rightVision) {
2334
+ return leftVision - rightVision;
2335
+ }
2328
2336
  const leftProviderRank = providerRank.get(left.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
2329
2337
  const rightProviderRank = providerRank.get(right.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
2330
2338
  if (leftProviderRank !== rightProviderRank) {
@@ -15,7 +15,6 @@ import {
15
15
  import { fuzzyMatch } from "@gajae-code/tui";
16
16
  import { logger } from "@gajae-code/utils";
17
17
  import chalk from "chalk";
18
- import MODEL_PRIO from "../priority.json" with { type: "json" };
19
18
  import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
20
19
  import { isAuthenticated, kNoAuth, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
21
20
  import type { Settings } from "./settings";
@@ -246,6 +245,15 @@ function pickPreferredModel(candidates: Model<Api>[], context: ModelPreferenceCo
246
245
  return (aProviderUsage ?? Number.POSITIVE_INFINITY) - (bProviderUsage ?? Number.POSITIVE_INFINITY);
247
246
  }
248
247
 
248
+ // Prefer vision-capable variants over configured provider/registration order
249
+ // so an ambiguous id never resolves to a text-only namesake when a
250
+ // vision-capable variant of the same id is available.
251
+ const aVision = a.input.includes("image") ? 0 : 1;
252
+ const bVision = b.input.includes("image") ? 0 : 1;
253
+ if (aVision !== bVision) {
254
+ return aVision - bVision;
255
+ }
256
+
249
257
  const aDeprioritized = context.deprioritizedProviders.has(a.provider);
250
258
  const bDeprioritized = context.deprioritizedProviders.has(b.provider);
251
259
  if (aDeprioritized !== bDeprioritized) {
@@ -520,7 +528,7 @@ function normalizeModelPatternList(value: string | string[] | undefined): string
520
528
  }
521
529
 
522
530
  function isSessionInheritedAgentPattern(value: string): boolean {
523
- return value === DEFAULT_MODEL_ROLE || value === `${PREFIX_MODEL_ROLE}${DEFAULT_MODEL_ROLE}` || value === "pi/task";
531
+ return value === DEFAULT_MODEL_ROLE || value === `${PREFIX_MODEL_ROLE}${DEFAULT_MODEL_ROLE}`;
524
532
  }
525
533
 
526
534
  function resolveConfiguredRolePattern(value: string, settings?: Settings): string[] | undefined {
@@ -535,8 +543,7 @@ function resolveConfiguredRolePattern(value: string, settings?: Settings): strin
535
543
  if (!role) return [normalized];
536
544
 
537
545
  const configured = settings?.getModelRole(role)?.trim();
538
- const roleDefaults = normalizeModelPatternList(MODEL_PRIO[role as keyof typeof MODEL_PRIO]);
539
- const resolved = configured ? normalizeModelPatternList(configured) : roleDefaults;
546
+ const resolved = configured ? normalizeModelPatternList(configured) : undefined;
540
547
  if (!resolved || resolved.length === 0) {
541
548
  return undefined;
542
549
  }
@@ -545,7 +552,7 @@ function resolveConfiguredRolePattern(value: string, settings?: Settings): strin
545
552
  }
546
553
 
547
554
  /**
548
- * Expand a role alias like "pi/smol" to the configured model string.
555
+ * Expand a role alias like "pi/default" to the configured model string.
549
556
  */
550
557
  export function expandRoleAlias(value: string, settings?: Settings): string {
551
558
  const normalized = value.trim();
@@ -582,9 +589,8 @@ export function resolveAgentModelPatterns(options: AgentModelPatternResolutionOp
582
589
  const configuredAgentPatterns = resolveConfiguredModelPatterns(agentModel, settings);
583
590
  const singleAgentPattern = normalizedAgentPatterns.length === 1 ? normalizedAgentPatterns[0] : undefined;
584
591
  const agentInheritsSessionModel = singleAgentPattern ? isSessionInheritedAgentPattern(singleAgentPattern) : false;
585
- if (configuredAgentPatterns.length > 0) {
586
- if (!agentInheritsSessionModel) return configuredAgentPatterns;
587
- if (singleAgentPattern === "pi/task") return configuredAgentPatterns;
592
+ if (configuredAgentPatterns.length > 0 && !agentInheritsSessionModel) {
593
+ return configuredAgentPatterns;
588
594
  }
589
595
 
590
596
  const fallback =
@@ -1325,79 +1331,3 @@ export async function restoreModelFromSession(
1325
1331
  // No models available
1326
1332
  return { model: undefined, fallbackMessage: undefined };
1327
1333
  }
1328
-
1329
- /**
1330
- * Find a smol/fast model using the priority chain.
1331
- * Tries exact matches first, then fuzzy matches.
1332
- *
1333
- * @param modelRegistry The model registry to search
1334
- * @param savedModel Optional saved model string from settings (provider/modelId)
1335
- * @returns The best available smol model, or undefined if none found
1336
- */
1337
- export async function findSmolModel(
1338
- modelRegistry: ModelLookupRegistry,
1339
- savedModel?: string,
1340
- ): Promise<Model<Api> | undefined> {
1341
- const availableModels = modelRegistry.getAvailable();
1342
- if (availableModels.length === 0) return undefined;
1343
-
1344
- // 1. Try saved model from settings
1345
- if (savedModel) {
1346
- const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
1347
- if (match) return match;
1348
- }
1349
-
1350
- // 2. Try priority chain
1351
- for (const pattern of MODEL_PRIO.smol) {
1352
- // Try exact match with provider prefix
1353
- const providerMatch = availableModels.find(m => `${m.provider}/${m.id}`.toLowerCase() === pattern);
1354
- if (providerMatch) return providerMatch;
1355
-
1356
- // Try exact match first
1357
- const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
1358
- if (exactMatch) return exactMatch;
1359
-
1360
- // Try fuzzy match (substring)
1361
- const fuzzyMatch = availableModels.find(m => m.id.toLowerCase().includes(pattern));
1362
- if (fuzzyMatch) return fuzzyMatch;
1363
- }
1364
-
1365
- // 3. Fallback to first available (same as default)
1366
- return availableModels[0];
1367
- }
1368
-
1369
- /**
1370
- * Find a slow/comprehensive model using the priority chain.
1371
- * Prioritizes reasoning and OpenAI code backend models for thorough analysis.
1372
- *
1373
- * @param modelRegistry The model registry to search
1374
- * @param savedModel Optional saved model string from settings (provider/modelId)
1375
- * @returns The best available slow model, or undefined if none found
1376
- */
1377
- export async function findSlowModel(
1378
- modelRegistry: ModelLookupRegistry,
1379
- savedModel?: string,
1380
- ): Promise<Model<Api> | undefined> {
1381
- const availableModels = modelRegistry.getAvailable();
1382
- if (availableModels.length === 0) return undefined;
1383
-
1384
- // 1. Try saved model from settings
1385
- if (savedModel) {
1386
- const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
1387
- if (match) return match;
1388
- }
1389
-
1390
- // 2. Try priority chain
1391
- for (const pattern of MODEL_PRIO.slow) {
1392
- // Try exact match first
1393
- const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
1394
- if (exactMatch) return exactMatch;
1395
-
1396
- // Try fuzzy match (substring)
1397
- const fuzzyMatch = availableModels.find(m => m.id.toLowerCase().includes(pattern.toLowerCase()));
1398
- if (fuzzyMatch) return fuzzyMatch;
1399
- }
1400
-
1401
- // 3. Fallback to first available (same as default)
1402
- return availableModels[0];
1403
- }
@@ -135,6 +135,7 @@ const ModelDefinitionSchema = z
135
135
  reasoning: z.boolean().optional(),
136
136
  thinking: ModelThinkingSchema.optional(),
137
137
  input: z.array(z.enum(["text", "image"])).optional(),
138
+ output: z.array(z.enum(["text", "image"])).optional(),
138
139
  cost: z
139
140
  .object({
140
141
  input: z.number(),
@@ -161,6 +162,7 @@ export const ModelOverrideSchema = z
161
162
  reasoning: z.boolean().optional(),
162
163
  thinking: ModelThinkingSchema.optional(),
163
164
  input: z.array(z.enum(["text", "image"])).optional(),
165
+ output: z.array(z.enum(["text", "image"])).optional(),
164
166
  cost: z
165
167
  .object({
166
168
  input: z.number().optional(),
@@ -938,6 +938,18 @@ export const SETTINGS_SCHEMA = {
938
938
  },
939
939
  },
940
940
 
941
+ busyPromptMode: {
942
+ type: "enum",
943
+ values: ["steer", "queue"] as const,
944
+ default: "steer",
945
+ ui: {
946
+ tab: "interaction",
947
+ label: "Busy Prompt Mode",
948
+ description:
949
+ "What a submitted prompt does while the agent is busy: steer (interrupt the active turn) or queue (run after the active turn completes)",
950
+ },
951
+ },
952
+
941
953
  // Input and startup
942
954
  doubleEscapeAction: {
943
955
  type: "enum",
@@ -2627,7 +2639,7 @@ export const SETTINGS_SCHEMA = {
2627
2639
  },
2628
2640
  "providers.image": {
2629
2641
  type: "enum",
2630
- values: ["auto", "openai", "gemini", "openrouter"] as const,
2642
+ values: ["auto", "openai", "gemini", "openrouter", "antigravity"] as const,
2631
2643
  default: "auto",
2632
2644
  ui: {
2633
2645
  tab: "providers",
@@ -2642,6 +2654,7 @@ export const SETTINGS_SCHEMA = {
2642
2654
  { value: "openai", label: "OpenAI", description: "Uses the active GPT Responses/Codex model" },
2643
2655
  { value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
2644
2656
  { value: "openrouter", label: "OpenRouter", description: "Requires OPENROUTER_API_KEY" },
2657
+ { value: "antigravity", label: "Antigravity", description: "Requires login with google-antigravity" },
2645
2658
  ],
2646
2659
  },
2647
2660
  },
@@ -0,0 +1,20 @@
1
+ export const COORDINATOR_MCP_PROTOCOL_VERSION = "2024-11-05";
2
+ export const COORDINATOR_MCP_SERVER_NAME = "gjc-coordinator-mcp";
3
+
4
+ export const COORDINATOR_MCP_TOOL_NAMES = [
5
+ "gjc_coordinator_list_sessions",
6
+ "gjc_coordinator_read_status",
7
+ "gjc_coordinator_read_tail",
8
+ "gjc_coordinator_list_questions",
9
+ "gjc_coordinator_list_artifacts",
10
+ "gjc_coordinator_read_artifact",
11
+ "gjc_coordinator_read_coordination_status",
12
+ "gjc_coordinator_start_session",
13
+ "gjc_coordinator_send_prompt",
14
+ "gjc_coordinator_submit_question_answer",
15
+ "gjc_coordinator_read_turn",
16
+ "gjc_coordinator_await_turn",
17
+ "gjc_coordinator_report_status",
18
+ ] as const;
19
+
20
+ export type CoordinatorToolName = (typeof COORDINATOR_MCP_TOOL_NAMES)[number];
@@ -0,0 +1,160 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+
4
+ export type CoordinatorMutationClass = "sessions" | "questions" | "reports";
5
+
6
+ export interface CoordinatorNamespace {
7
+ profile: string | null;
8
+ repo: string | null;
9
+ }
10
+
11
+ export interface CoordinatorMcpConfig {
12
+ allowedRoots: string[];
13
+ mutationClasses: Set<CoordinatorMutationClass>;
14
+ artifactByteCap: number;
15
+ namespace: CoordinatorNamespace;
16
+ stateRoot: string;
17
+ sessionCommand: string | null;
18
+ }
19
+
20
+ export interface CoordinatorMutationRequest {
21
+ allow_mutation?: boolean;
22
+ }
23
+
24
+ const DEFAULT_ARTIFACT_BYTE_CAP = 64 * 1024;
25
+ const MAX_ARTIFACT_BYTE_CAP = 1024 * 1024;
26
+ const MUTATION_CLASSES = new Set<CoordinatorMutationClass>(["sessions", "questions", "reports"]);
27
+ const LEGACY_MUTATION_CLASS_ALIASES = new Map<string, CoordinatorMutationClass>([
28
+ ["session", "sessions"],
29
+ ["prompt", "sessions"],
30
+ ["question", "questions"],
31
+ ["report", "reports"],
32
+ ]);
33
+
34
+ function parseList(value: string | undefined): string[] {
35
+ return (value ?? "")
36
+ .split(/[\n,;:]+/)
37
+ .map(part => part.trim())
38
+ .filter(Boolean);
39
+ }
40
+
41
+ function parseRootList(value: string | undefined): string[] {
42
+ const normalized = (value ?? "").replace(/[\n,;]+/g, path.delimiter);
43
+ return normalized
44
+ .split(path.delimiter)
45
+ .map(part => part.trim())
46
+ .filter(Boolean);
47
+ }
48
+
49
+ function parseMutationClasses(value: string | undefined): Set<CoordinatorMutationClass> {
50
+ const classes = new Set<CoordinatorMutationClass>();
51
+ for (const raw of parseList(value)) {
52
+ const normalized = raw.toLowerCase();
53
+ if (normalized === "all") {
54
+ for (const mutationClass of MUTATION_CLASSES) classes.add(mutationClass);
55
+ continue;
56
+ }
57
+ const mutationClass = LEGACY_MUTATION_CLASS_ALIASES.get(normalized) ?? normalized;
58
+ if (MUTATION_CLASSES.has(mutationClass as CoordinatorMutationClass))
59
+ classes.add(mutationClass as CoordinatorMutationClass);
60
+ }
61
+ return classes;
62
+ }
63
+
64
+ function parseByteCap(value: string | undefined): number {
65
+ const parsed = Number.parseInt(value ?? "", 10);
66
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_ARTIFACT_BYTE_CAP;
67
+ return Math.min(parsed, MAX_ARTIFACT_BYTE_CAP);
68
+ }
69
+
70
+ function cleanScope(value: string | undefined): string | null {
71
+ const trimmed = value?.trim();
72
+ if (!trimmed) return null;
73
+ return trimmed.replace(/[^a-zA-Z0-9_.-]+/g, "-").slice(0, 100) || null;
74
+ }
75
+
76
+ export function buildCoordinatorMcpConfig(env: NodeJS.ProcessEnv = process.env): CoordinatorMcpConfig {
77
+ const stateRoot =
78
+ env.GJC_COORDINATOR_MCP_STATE_ROOT?.trim() || path.join(process.cwd(), ".gjc", "state", "coordinator-mcp");
79
+ return {
80
+ allowedRoots: parseRootList(env.GJC_COORDINATOR_MCP_WORKDIR_ROOTS).map(root => path.resolve(root)),
81
+ mutationClasses: parseMutationClasses(
82
+ env.GJC_COORDINATOR_MCP_MUTATIONS ?? env.GJC_COORDINATOR_MCP_ENABLE_MUTATION_CLASSES,
83
+ ),
84
+ artifactByteCap: parseByteCap(
85
+ env.GJC_COORDINATOR_MCP_ARTIFACT_BYTE_CAP ?? env.GJC_COORDINATOR_MCP_ARTIFACT_MAX_BYTES,
86
+ ),
87
+ namespace: {
88
+ profile: cleanScope(env.GJC_COORDINATOR_MCP_PROFILE),
89
+ repo: cleanScope(env.GJC_COORDINATOR_MCP_REPO),
90
+ },
91
+ stateRoot: path.resolve(stateRoot),
92
+ sessionCommand: env.GJC_COORDINATOR_MCP_SESSION_COMMAND?.trim() || null,
93
+ };
94
+ }
95
+
96
+ async function realpathIfExists(value: string): Promise<string> {
97
+ try {
98
+ return await fs.realpath(value);
99
+ } catch (error) {
100
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
101
+ const parent = await fs.realpath(path.dirname(value));
102
+ return path.join(parent, path.basename(value));
103
+ }
104
+ }
105
+
106
+ function isInside(candidate: string, root: string): boolean {
107
+ const relative = path.relative(root, candidate);
108
+ return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
109
+ }
110
+
111
+ async function canonicalAllowedRoots(config: CoordinatorMcpConfig): Promise<string[]> {
112
+ const roots = await Promise.all(config.allowedRoots.map(root => realpathIfExists(root)));
113
+ return roots.map(root => path.resolve(root));
114
+ }
115
+
116
+ export async function assertCoordinatorWorkdir(config: CoordinatorMcpConfig, cwd: unknown): Promise<string> {
117
+ if (typeof cwd !== "string" || cwd.trim().length === 0) throw new Error("coordinator_workdir_required");
118
+ if (config.allowedRoots.length === 0) throw new Error("coordinator_workdir_roots_required");
119
+ const requested = path.resolve(cwd);
120
+ const canonicalRequested = await realpathIfExists(requested);
121
+ const roots = await canonicalAllowedRoots(config);
122
+ if (!roots.some(root => isInside(canonicalRequested, root))) {
123
+ throw new Error(`coordinator_workdir_outside_allowed_roots:${requested}`);
124
+ }
125
+ return requested;
126
+ }
127
+
128
+ export async function assertCoordinatorArtifactPath(
129
+ config: CoordinatorMcpConfig,
130
+ artifactPath: unknown,
131
+ ): Promise<{ path: string; byteCap: number }> {
132
+ if (typeof artifactPath !== "string" || artifactPath.trim().length === 0)
133
+ throw new Error("coordinator_artifact_path_required");
134
+ if (config.allowedRoots.length === 0) throw new Error("coordinator_artifact_roots_required");
135
+ const requested = path.resolve(artifactPath);
136
+ const canonicalRequested = await realpathIfExists(requested);
137
+ const roots = await canonicalAllowedRoots(config);
138
+ if (!roots.some(root => isInside(canonicalRequested, root))) {
139
+ throw new Error(`coordinator_artifact_outside_allowed_roots:${requested}`);
140
+ }
141
+ return { path: requested, byteCap: config.artifactByteCap };
142
+ }
143
+
144
+ export function requireCoordinatorMutation(
145
+ config: CoordinatorMcpConfig,
146
+ mutationClass: CoordinatorMutationClass,
147
+ request: CoordinatorMutationRequest,
148
+ ): void {
149
+ if (!config.mutationClasses.has(mutationClass))
150
+ throw new Error(`coordinator_mutation_class_disabled:${mutationClass}`);
151
+ if (request.allow_mutation !== true) throw new Error(`coordinator_mutation_call_not_allowed:${mutationClass}`);
152
+ }
153
+
154
+ export function coordinatorNamespacePath(config: CoordinatorMcpConfig): string {
155
+ return path.join(
156
+ config.stateRoot,
157
+ config.namespace.profile ?? "unscoped-profile",
158
+ config.namespace.repo ?? "unscoped-repo",
159
+ );
160
+ }