@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.
- package/CHANGELOG.md +13 -0
- package/dist/types/async/job-manager.d.ts +44 -1
- package/dist/types/cli/setup-cli.d.ts +14 -1
- package/dist/types/commands/coordinator.d.ts +19 -0
- package/dist/types/commands/mcp-serve.d.ts +24 -0
- package/dist/types/commands/setup.d.ts +41 -0
- package/dist/types/commit/model-selection.d.ts +1 -1
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/model-resolver.d.ts +1 -19
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +15 -1
- package/dist/types/coordinator/contract.d.ts +4 -0
- package/dist/types/coordinator-mcp/policy.d.ts +24 -0
- package/dist/types/coordinator-mcp/safety.d.ts +26 -0
- package/dist/types/coordinator-mcp/server.d.ts +52 -0
- package/dist/types/extensibility/extensions/types.d.ts +13 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +8 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/harness-control-plane/types.d.ts +7 -2
- package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +84 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +14 -7
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +37 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +13 -34
- package/dist/types/session/agent-session.d.ts +12 -1
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/setup/hermes-setup.d.ts +71 -0
- package/dist/types/task/render.d.ts +7 -1
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/browser/actions.d.ts +54 -0
- package/dist/types/tools/browser.d.ts +80 -0
- package/dist/types/tools/image-gen.d.ts +1 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/subagent-render.d.ts +25 -0
- package/dist/types/tools/subagent.d.ts +5 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +163 -2
- package/src/cli/setup-cli.ts +86 -2
- package/src/cli.ts +2 -0
- package/src/commands/coordinator.ts +70 -0
- package/src/commands/mcp-serve.ts +62 -0
- package/src/commands/setup.ts +30 -1
- package/src/commands/ultragoal.ts +7 -1
- package/src/commit/agentic/index.ts +2 -2
- package/src/commit/model-selection.ts +7 -22
- package/src/commit/pipeline.ts +2 -2
- package/src/config/model-registry.ts +17 -9
- package/src/config/model-resolver.ts +14 -84
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +14 -1
- package/src/coordinator/contract.ts +20 -0
- package/src/coordinator-mcp/policy.ts +160 -0
- package/src/coordinator-mcp/safety.ts +80 -0
- package/src/coordinator-mcp/server.ts +1316 -0
- package/src/extensibility/extensions/types.ts +13 -0
- package/src/gjc-runtime/goal-mode-request.ts +21 -1
- package/src/gjc-runtime/session-state-sidecar.ts +79 -0
- package/src/harness-control-plane/owner.ts +3 -3
- package/src/harness-control-plane/rpc-adapter.ts +7 -1
- package/src/harness-control-plane/types.ts +8 -11
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-agent.ts +17 -9
- package/src/modes/acp/acp-event-mapper.ts +33 -1
- package/src/modes/components/custom-editor.ts +19 -3
- package/src/modes/components/hook-selector.ts +109 -5
- package/src/modes/controllers/extension-ui-controller.ts +16 -1
- package/src/modes/controllers/input-controller.ts +27 -7
- package/src/modes/controllers/selector-controller.ts +7 -1
- package/src/modes/interactive-mode.ts +3 -1
- package/src/modes/rpc/rpc-client.ts +16 -3
- package/src/modes/rpc/rpc-mode.ts +5 -2
- package/src/modes/shared/agent-wire/command-contract.ts +18 -0
- package/src/modes/shared/agent-wire/event-contract.ts +147 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +35 -16
- package/src/modes/shared/agent-wire/event-observation.ts +397 -0
- package/src/modes/shared/agent-wire/protocol.ts +24 -81
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/prompts/agents/architect.md +6 -0
- package/src/prompts/agents/critic.md +6 -0
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/plan.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/tools/browser.md +3 -2
- package/src/runtime-mcp/manager.ts +15 -2
- package/src/sdk.ts +3 -1
- package/src/session/agent-session.ts +66 -4
- package/src/session/session-manager.ts +1 -1
- package/src/setup/hermes/templates/operator-instructions.v1.md +29 -0
- package/src/setup/hermes-setup.ts +429 -0
- package/src/task/agents.ts +1 -1
- package/src/task/index.ts +2 -0
- package/src/task/render.ts +14 -0
- package/src/tools/ask.ts +30 -10
- package/src/tools/bash.ts +6 -1
- package/src/tools/browser/actions.ts +189 -0
- package/src/tools/browser.ts +91 -1
- package/src/tools/image-gen.ts +42 -15
- package/src/tools/index.ts +7 -1
- package/src/tools/inspect-image.ts +10 -8
- package/src/tools/job.ts +12 -2
- package/src/tools/monitor.ts +98 -17
- package/src/tools/renderers.ts +2 -0
- package/src/tools/subagent-render.ts +160 -0
- package/src/tools/subagent.ts +49 -7
- package/src/utils/commit-message-generator.ts +6 -13
- package/src/utils/title-generator.ts +1 -1
- package/dist/types/harness-control-plane/frame-mapper.d.ts +0 -29
- package/src/harness-control-plane/frame-mapper.ts +0 -286
- package/src/priority.json +0 -37
package/src/commands/setup.ts
CHANGED
|
@@ -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({
|
|
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,
|
|
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
|
|
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 {
|
|
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(["
|
|
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
|
|
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
|
|
52
|
-
if (
|
|
53
|
-
const apiKey = await modelRegistry.getApiKey(
|
|
54
|
-
if (apiKey) return { model:
|
|
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 };
|
package/src/commit/pipeline.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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"
|
|
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"
|
|
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}
|
|
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
|
|
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/
|
|
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
|
-
|
|
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
|
+
}
|