@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
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { YAML } from "bun";
|
|
6
|
+
import {
|
|
7
|
+
COORDINATOR_MCP_PROTOCOL_VERSION,
|
|
8
|
+
COORDINATOR_MCP_SERVER_NAME,
|
|
9
|
+
COORDINATOR_MCP_TOOL_NAMES,
|
|
10
|
+
} from "../coordinator/contract";
|
|
11
|
+
import { createCoordinatorMcpServer } from "../coordinator-mcp/server";
|
|
12
|
+
import operatorInstructionsTemplate from "./hermes/templates/operator-instructions.v1.md" with { type: "text" };
|
|
13
|
+
|
|
14
|
+
export type HermesMutationClass = "sessions" | "questions" | "reports";
|
|
15
|
+
export type HermesSetupMode = "render" | "install" | "check" | "smoke";
|
|
16
|
+
|
|
17
|
+
export interface HermesSetupFlags {
|
|
18
|
+
json?: boolean;
|
|
19
|
+
check?: boolean;
|
|
20
|
+
smoke?: boolean;
|
|
21
|
+
install?: boolean;
|
|
22
|
+
force?: boolean;
|
|
23
|
+
root?: string[];
|
|
24
|
+
repo?: string;
|
|
25
|
+
profile?: string;
|
|
26
|
+
sessionCommand?: string;
|
|
27
|
+
stateRoot?: string;
|
|
28
|
+
mutation?: string[];
|
|
29
|
+
artifactByteCap?: string;
|
|
30
|
+
serverKey?: string;
|
|
31
|
+
gjcCommand?: string;
|
|
32
|
+
target?: string;
|
|
33
|
+
profileDir?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CoordinatorSetupSpec {
|
|
37
|
+
schemaVersion: 1;
|
|
38
|
+
coordinator: "hermes";
|
|
39
|
+
serverKey: string;
|
|
40
|
+
serverName: typeof COORDINATOR_MCP_SERVER_NAME;
|
|
41
|
+
protocolVersion: typeof COORDINATOR_MCP_PROTOCOL_VERSION;
|
|
42
|
+
gjcCommand: string;
|
|
43
|
+
args: ["mcp-serve", "coordinator"];
|
|
44
|
+
roots: string[];
|
|
45
|
+
namespace: {
|
|
46
|
+
profile?: string;
|
|
47
|
+
repo?: string;
|
|
48
|
+
};
|
|
49
|
+
sessionCommand?: string;
|
|
50
|
+
stateRoot?: string;
|
|
51
|
+
mutationPolicy: {
|
|
52
|
+
classes: HermesMutationClass[];
|
|
53
|
+
perCallConsentRequired: true;
|
|
54
|
+
};
|
|
55
|
+
artifactByteCap?: number;
|
|
56
|
+
installTarget?: {
|
|
57
|
+
kind: "profile-dir" | "config-file";
|
|
58
|
+
path: string;
|
|
59
|
+
};
|
|
60
|
+
operatorTemplateVersion: 1;
|
|
61
|
+
contractDocVersion: 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface HermesSetupResult {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
mode: HermesSetupMode;
|
|
67
|
+
files_written: string[];
|
|
68
|
+
previews: Array<{ path: string; content: string }>;
|
|
69
|
+
warnings: string[];
|
|
70
|
+
smoke: null | {
|
|
71
|
+
ok: boolean;
|
|
72
|
+
protocolVersion: string;
|
|
73
|
+
serverName: string;
|
|
74
|
+
requiredTools: string[];
|
|
75
|
+
missingTools: string[];
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class HermesSetupError extends Error {
|
|
80
|
+
readonly exitCode: number;
|
|
81
|
+
constructor(message: string, exitCode: number) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = "HermesSetupError";
|
|
84
|
+
this.exitCode = exitCode;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const MUTATION_CLASSES: HermesMutationClass[] = ["sessions", "questions", "reports"];
|
|
89
|
+
const MANAGED_BY = "gjc";
|
|
90
|
+
const SETUP_SCHEMA_VERSION = "1";
|
|
91
|
+
const DEFAULT_SERVER_KEY = "gjc_coordinator";
|
|
92
|
+
const DEFAULT_GJC_COMMAND = "gjc";
|
|
93
|
+
const DEFAULT_TIMEOUT = 180;
|
|
94
|
+
const DEFAULT_CONNECT_TIMEOUT = 60;
|
|
95
|
+
|
|
96
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
97
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function optionalTrim(value: string | undefined): string | undefined {
|
|
101
|
+
const trimmed = value?.trim();
|
|
102
|
+
return trimmed ? trimmed : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeRoots(roots: string[] | undefined): string[] {
|
|
106
|
+
if (!roots || roots.length === 0) {
|
|
107
|
+
throw new HermesSetupError("Hermes setup requires at least one --root <path>.", 2);
|
|
108
|
+
}
|
|
109
|
+
const seen = new Set<string>();
|
|
110
|
+
const normalized: string[] = [];
|
|
111
|
+
const home = path.resolve(os.homedir());
|
|
112
|
+
for (const root of roots) {
|
|
113
|
+
const trimmed = root.trim();
|
|
114
|
+
if (!trimmed) {
|
|
115
|
+
throw new HermesSetupError("Hermes setup root entries must not be empty.", 2);
|
|
116
|
+
}
|
|
117
|
+
const resolved = path.resolve(trimmed);
|
|
118
|
+
if (resolved === path.parse(resolved).root || resolved === path.resolve("/home") || resolved === home) {
|
|
119
|
+
throw new HermesSetupError(`Refusing broad Hermes MCP root: ${resolved}`, 2);
|
|
120
|
+
}
|
|
121
|
+
if (!seen.has(resolved)) {
|
|
122
|
+
seen.add(resolved);
|
|
123
|
+
normalized.push(resolved);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return normalized;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseMutationClasses(values: string[] | undefined): HermesMutationClass[] {
|
|
130
|
+
if (!values || values.length === 0) return [];
|
|
131
|
+
const classes: HermesMutationClass[] = [];
|
|
132
|
+
for (const raw of values) {
|
|
133
|
+
for (const part of raw.split(",")) {
|
|
134
|
+
const value = part.trim();
|
|
135
|
+
if (!value) continue;
|
|
136
|
+
if (value === "all") {
|
|
137
|
+
for (const cls of MUTATION_CLASSES) {
|
|
138
|
+
if (!classes.includes(cls)) classes.push(cls);
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!MUTATION_CLASSES.includes(value as HermesMutationClass)) {
|
|
143
|
+
throw new HermesSetupError(`Invalid Hermes mutation class: ${value}`, 2);
|
|
144
|
+
}
|
|
145
|
+
if (!classes.includes(value as HermesMutationClass)) classes.push(value as HermesMutationClass);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return classes;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseByteCap(value: string | undefined): number | undefined {
|
|
152
|
+
if (value === undefined) return undefined;
|
|
153
|
+
const parsed = Number(value);
|
|
154
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
155
|
+
throw new HermesSetupError("--artifact-byte-cap must be a positive integer.", 2);
|
|
156
|
+
}
|
|
157
|
+
return parsed;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeInstallTarget(flags: HermesSetupFlags): CoordinatorSetupSpec["installTarget"] {
|
|
161
|
+
if (flags.target && flags.profileDir) {
|
|
162
|
+
throw new HermesSetupError("Use exactly one of --target or --profile-dir for Hermes setup install targets.", 2);
|
|
163
|
+
}
|
|
164
|
+
if (!flags.target && !flags.profileDir) return undefined;
|
|
165
|
+
return flags.profileDir
|
|
166
|
+
? { kind: "profile-dir", path: path.resolve(flags.profileDir) }
|
|
167
|
+
: { kind: "config-file", path: path.resolve(flags.target!) };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function buildHermesSetupSpec(flags: HermesSetupFlags): CoordinatorSetupSpec {
|
|
171
|
+
const roots = normalizeRoots(flags.root);
|
|
172
|
+
return {
|
|
173
|
+
schemaVersion: 1,
|
|
174
|
+
coordinator: "hermes",
|
|
175
|
+
serverKey: optionalTrim(flags.serverKey) ?? DEFAULT_SERVER_KEY,
|
|
176
|
+
serverName: COORDINATOR_MCP_SERVER_NAME,
|
|
177
|
+
protocolVersion: COORDINATOR_MCP_PROTOCOL_VERSION,
|
|
178
|
+
gjcCommand: optionalTrim(flags.gjcCommand) ?? DEFAULT_GJC_COMMAND,
|
|
179
|
+
args: ["mcp-serve", "coordinator"],
|
|
180
|
+
roots,
|
|
181
|
+
namespace: {
|
|
182
|
+
...(optionalTrim(flags.profile) ? { profile: optionalTrim(flags.profile) } : {}),
|
|
183
|
+
...(optionalTrim(flags.repo) ? { repo: optionalTrim(flags.repo) } : {}),
|
|
184
|
+
},
|
|
185
|
+
...(optionalTrim(flags.sessionCommand) ? { sessionCommand: flags.sessionCommand } : {}),
|
|
186
|
+
...(optionalTrim(flags.stateRoot) ? { stateRoot: path.resolve(flags.stateRoot!) } : {}),
|
|
187
|
+
mutationPolicy: {
|
|
188
|
+
classes: parseMutationClasses(flags.mutation),
|
|
189
|
+
perCallConsentRequired: true,
|
|
190
|
+
},
|
|
191
|
+
...(parseByteCap(flags.artifactByteCap) ? { artifactByteCap: parseByteCap(flags.artifactByteCap) } : {}),
|
|
192
|
+
...(normalizeInstallTarget(flags) ? { installTarget: normalizeInstallTarget(flags) } : {}),
|
|
193
|
+
operatorTemplateVersion: 1,
|
|
194
|
+
contractDocVersion: 1,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function canonicalize(value: unknown): unknown {
|
|
199
|
+
if (Array.isArray(value)) return value.map(item => canonicalize(item));
|
|
200
|
+
if (!isRecord(value)) return value;
|
|
201
|
+
const output: Record<string, unknown> = {};
|
|
202
|
+
for (const key of Object.keys(value).sort()) {
|
|
203
|
+
const item = value[key];
|
|
204
|
+
if (item !== undefined) output[key] = canonicalize(item);
|
|
205
|
+
}
|
|
206
|
+
return output;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function signaturePayload(spec: CoordinatorSetupSpec): Record<string, unknown> {
|
|
210
|
+
return {
|
|
211
|
+
args: spec.args,
|
|
212
|
+
artifactByteCap: spec.artifactByteCap,
|
|
213
|
+
command: spec.gjcCommand,
|
|
214
|
+
contractDocVersion: spec.contractDocVersion,
|
|
215
|
+
coordinator: spec.coordinator,
|
|
216
|
+
mutationClasses: spec.mutationPolicy.classes,
|
|
217
|
+
namespace: spec.namespace,
|
|
218
|
+
operatorTemplateVersion: spec.operatorTemplateVersion,
|
|
219
|
+
roots: spec.roots,
|
|
220
|
+
schemaVersion: spec.schemaVersion,
|
|
221
|
+
serverKey: spec.serverKey,
|
|
222
|
+
sessionCommand: spec.sessionCommand,
|
|
223
|
+
stateRoot: spec.stateRoot,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function computeHermesSetupSignature(spec: CoordinatorSetupSpec): string {
|
|
228
|
+
const canonical = JSON.stringify(canonicalize(signaturePayload(spec)));
|
|
229
|
+
return crypto.createHash("sha256").update(canonical).digest("hex");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function renderHermesServerBlock(spec: CoordinatorSetupSpec): Record<string, unknown> {
|
|
233
|
+
const env: Record<string, string> = {
|
|
234
|
+
GJC_COORDINATOR_MCP_WORKDIR_ROOTS: spec.roots.join(path.delimiter),
|
|
235
|
+
GJC_COORDINATOR_MCP_SETUP_MANAGED_BY: MANAGED_BY,
|
|
236
|
+
GJC_COORDINATOR_MCP_SETUP_SCHEMA_VERSION: SETUP_SCHEMA_VERSION,
|
|
237
|
+
GJC_COORDINATOR_MCP_SETUP_SIGNATURE: computeHermesSetupSignature(spec),
|
|
238
|
+
};
|
|
239
|
+
if (spec.namespace.profile) env.GJC_COORDINATOR_MCP_PROFILE = spec.namespace.profile;
|
|
240
|
+
if (spec.namespace.repo) env.GJC_COORDINATOR_MCP_REPO = spec.namespace.repo;
|
|
241
|
+
if (spec.stateRoot) env.GJC_COORDINATOR_MCP_STATE_ROOT = spec.stateRoot;
|
|
242
|
+
if (spec.mutationPolicy.classes.length > 0)
|
|
243
|
+
env.GJC_COORDINATOR_MCP_MUTATIONS = spec.mutationPolicy.classes.join(",");
|
|
244
|
+
if (spec.artifactByteCap !== undefined) env.GJC_COORDINATOR_MCP_ARTIFACT_BYTE_CAP = String(spec.artifactByteCap);
|
|
245
|
+
if (spec.sessionCommand) env.GJC_COORDINATOR_MCP_SESSION_COMMAND = spec.sessionCommand;
|
|
246
|
+
return {
|
|
247
|
+
command: spec.gjcCommand,
|
|
248
|
+
args: spec.args,
|
|
249
|
+
env,
|
|
250
|
+
timeout: DEFAULT_TIMEOUT,
|
|
251
|
+
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
|
|
252
|
+
enabled: true,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function renderConfigYaml(spec: CoordinatorSetupSpec): string {
|
|
257
|
+
return YAML.stringify({ mcp_servers: { [spec.serverKey]: renderHermesServerBlock(spec) } }, null, 2);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function renderOperatorTemplate(spec: CoordinatorSetupSpec): string {
|
|
261
|
+
return operatorInstructionsTemplate
|
|
262
|
+
.replaceAll("{{SERVER_KEY}}", spec.serverKey)
|
|
263
|
+
.replaceAll("{{TOOL_PREFIX}}", "gjc_coordinator")
|
|
264
|
+
.replaceAll("{{TEMPLATE_VERSION}}", String(spec.operatorTemplateVersion));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function serverBlockIsManaged(block: unknown): boolean {
|
|
268
|
+
if (!isRecord(block)) return false;
|
|
269
|
+
const env = block.env;
|
|
270
|
+
return (
|
|
271
|
+
isRecord(env) &&
|
|
272
|
+
env.GJC_COORDINATOR_MCP_SETUP_MANAGED_BY === MANAGED_BY &&
|
|
273
|
+
env.GJC_COORDINATOR_MCP_SETUP_SCHEMA_VERSION === SETUP_SCHEMA_VERSION &&
|
|
274
|
+
typeof env.GJC_COORDINATOR_MCP_SETUP_SIGNATURE === "string"
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function readYamlConfig(configPath: string): Promise<Record<string, unknown>> {
|
|
279
|
+
const exists = await Bun.file(configPath).exists();
|
|
280
|
+
if (!exists) return {};
|
|
281
|
+
const content = await Bun.file(configPath).text();
|
|
282
|
+
if (!content.trim()) return {};
|
|
283
|
+
const parsed = YAML.parse(content);
|
|
284
|
+
if (!isRecord(parsed)) {
|
|
285
|
+
throw new HermesSetupError(`Hermes config must be a YAML object: ${configPath}`, 2);
|
|
286
|
+
}
|
|
287
|
+
return parsed;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function backupFile(filePath: string): Promise<string | null> {
|
|
291
|
+
if (!(await Bun.file(filePath).exists())) return null;
|
|
292
|
+
const stamp = new Date().toISOString().replaceAll(":", "").replaceAll(".", "");
|
|
293
|
+
const backupPath = `${filePath}.bak.${stamp}`;
|
|
294
|
+
await Bun.write(backupPath, Bun.file(filePath));
|
|
295
|
+
return backupPath;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function mergeHermesConfig(
|
|
299
|
+
existing: Record<string, unknown>,
|
|
300
|
+
spec: CoordinatorSetupSpec,
|
|
301
|
+
force: boolean,
|
|
302
|
+
): Record<string, unknown> {
|
|
303
|
+
const currentServers = isRecord(existing.mcp_servers) ? existing.mcp_servers : {};
|
|
304
|
+
const existingBlock = currentServers[spec.serverKey];
|
|
305
|
+
if (existingBlock !== undefined && !serverBlockIsManaged(existingBlock) && !force) {
|
|
306
|
+
throw new HermesSetupError(`Hermes MCP server '${spec.serverKey}' already exists and is not managed by GJC.`, 3);
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
...existing,
|
|
310
|
+
mcp_servers: {
|
|
311
|
+
...currentServers,
|
|
312
|
+
[spec.serverKey]: renderHermesServerBlock(spec),
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function configPathForTarget(spec: CoordinatorSetupSpec): string | null {
|
|
318
|
+
if (!spec.installTarget) return null;
|
|
319
|
+
if (spec.installTarget.kind === "config-file") return spec.installTarget.path;
|
|
320
|
+
return path.join(spec.installTarget.path, "config.yaml");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function operatorPathForTarget(spec: CoordinatorSetupSpec): string | null {
|
|
324
|
+
if (spec.installTarget?.kind !== "profile-dir") return null;
|
|
325
|
+
return path.join(spec.installTarget.path, "skills", "autonomous-ai-agents", "gajae-code", "SKILL.md");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function installConfig(spec: CoordinatorSetupSpec, force: boolean): Promise<string[]> {
|
|
329
|
+
const configPath = configPathForTarget(spec);
|
|
330
|
+
if (!configPath) return [];
|
|
331
|
+
const existing = await readYamlConfig(configPath);
|
|
332
|
+
const merged = mergeHermesConfig(existing, spec, force);
|
|
333
|
+
if (force) await backupFile(configPath);
|
|
334
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
335
|
+
await Bun.write(configPath, YAML.stringify(merged, null, 2));
|
|
336
|
+
const written = [configPath];
|
|
337
|
+
const operatorPath = operatorPathForTarget(spec);
|
|
338
|
+
if (operatorPath) {
|
|
339
|
+
if ((await Bun.file(operatorPath).exists()) && !force) {
|
|
340
|
+
const current = await Bun.file(operatorPath).text();
|
|
341
|
+
if (
|
|
342
|
+
!current.includes("GJC Hermes operator instructions") ||
|
|
343
|
+
!current.includes(`Server key: ${spec.serverKey}`)
|
|
344
|
+
) {
|
|
345
|
+
throw new HermesSetupError(
|
|
346
|
+
`Operator instruction target already exists and is not managed by GJC: ${operatorPath}`,
|
|
347
|
+
3,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (force) await backupFile(operatorPath);
|
|
352
|
+
await fs.mkdir(path.dirname(operatorPath), { recursive: true });
|
|
353
|
+
await Bun.write(operatorPath, renderOperatorTemplate(spec));
|
|
354
|
+
written.push(operatorPath);
|
|
355
|
+
}
|
|
356
|
+
return written;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function runSmoke(spec: CoordinatorSetupSpec): Promise<HermesSetupResult["smoke"]> {
|
|
360
|
+
const requiredTools = [...COORDINATOR_MCP_TOOL_NAMES];
|
|
361
|
+
const server = createCoordinatorMcpServer({ env: {} });
|
|
362
|
+
const listed = await server.handleJsonRpc({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} });
|
|
363
|
+
const advertised = new Set((listed.result?.tools ?? []).map((tool: { name: string }) => tool.name));
|
|
364
|
+
const missingTools = requiredTools.filter(tool => !advertised.has(tool));
|
|
365
|
+
return {
|
|
366
|
+
ok: missingTools.length === 0,
|
|
367
|
+
protocolVersion: spec.protocolVersion,
|
|
368
|
+
serverName: spec.serverName,
|
|
369
|
+
requiredTools,
|
|
370
|
+
missingTools,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function runHermesSetup(flags: HermesSetupFlags): Promise<HermesSetupResult> {
|
|
375
|
+
const spec = buildHermesSetupSpec(flags);
|
|
376
|
+
if (flags.install && !spec.installTarget) {
|
|
377
|
+
throw new HermesSetupError("Hermes setup --install requires --target or --profile-dir.", 2);
|
|
378
|
+
}
|
|
379
|
+
if (!flags.install && spec.installTarget && !flags.check && !flags.smoke) {
|
|
380
|
+
throw new HermesSetupError(
|
|
381
|
+
"Hermes setup target/profile-dir writes require --install; omit the target for render-only output.",
|
|
382
|
+
2,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
const mode: HermesSetupMode = flags.smoke ? "smoke" : flags.check ? "check" : flags.install ? "install" : "render";
|
|
386
|
+
const configPath = configPathForTarget(spec) ?? "hermes-config.yaml";
|
|
387
|
+
const previews = [
|
|
388
|
+
{ path: configPath, content: renderConfigYaml(spec) },
|
|
389
|
+
{ path: operatorPathForTarget(spec) ?? "operator-instructions.v1.md", content: renderOperatorTemplate(spec) },
|
|
390
|
+
];
|
|
391
|
+
const files_written = flags.install ? await installConfig(spec, Boolean(flags.force)) : [];
|
|
392
|
+
const smoke = flags.smoke ? await runSmoke(spec) : null;
|
|
393
|
+
if (smoke && !smoke.ok) {
|
|
394
|
+
throw new HermesSetupError(`Hermes MCP smoke failed; missing tools: ${smoke.missingTools.join(", ")}`, 4);
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
ok: true,
|
|
398
|
+
mode,
|
|
399
|
+
files_written,
|
|
400
|
+
previews,
|
|
401
|
+
warnings: spec.sessionCommand
|
|
402
|
+
? [
|
|
403
|
+
"Using explicit GJC_COORDINATOR_MCP_SESSION_COMMAND exactly as supplied; provider/model validation is not performed.",
|
|
404
|
+
]
|
|
405
|
+
: ["No session command supplied; spawned sessions use the default GJC command/model resolution."],
|
|
406
|
+
smoke,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function formatHermesSetupResult(result: HermesSetupResult): string {
|
|
411
|
+
const lines = [`Hermes setup ${result.mode} complete.`];
|
|
412
|
+
if (result.files_written.length > 0) {
|
|
413
|
+
lines.push("Written:");
|
|
414
|
+
for (const file of result.files_written) lines.push(`- ${file}`);
|
|
415
|
+
}
|
|
416
|
+
if (result.files_written.length === 0) {
|
|
417
|
+
lines.push("No files written. Use --install with --target or --profile-dir to apply.");
|
|
418
|
+
for (const preview of result.previews) lines.push(`Preview: ${preview.path}`);
|
|
419
|
+
}
|
|
420
|
+
for (const warning of result.warnings) lines.push(`Warning: ${warning}`);
|
|
421
|
+
if (result.smoke) {
|
|
422
|
+
lines.push(`Smoke: ${result.smoke.ok ? "passed" : "failed"} (${result.smoke.requiredTools.length} tools)`);
|
|
423
|
+
}
|
|
424
|
+
return lines.join("\n");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function hermesSetupExitCode(error: unknown): number {
|
|
428
|
+
return error instanceof HermesSetupError ? error.exitCode : 1;
|
|
429
|
+
}
|
package/src/task/agents.ts
CHANGED
|
@@ -59,7 +59,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
59
59
|
name: "task",
|
|
60
60
|
description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
|
|
61
61
|
spawns: "*",
|
|
62
|
-
model: "pi/
|
|
62
|
+
model: "pi/default",
|
|
63
63
|
thinkingLevel: Effort.Medium,
|
|
64
64
|
hide: true,
|
|
65
65
|
},
|
package/src/task/index.ts
CHANGED
|
@@ -1324,6 +1324,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1324
1324
|
progressMap.set(index, {
|
|
1325
1325
|
...structuredClone(progress),
|
|
1326
1326
|
});
|
|
1327
|
+
AsyncJobManager.instance()?.recordSubagentProgress(task.id, progress);
|
|
1327
1328
|
emitProgress();
|
|
1328
1329
|
},
|
|
1329
1330
|
authStorage: this.session.authStorage,
|
|
@@ -1384,6 +1385,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1384
1385
|
progressMap.set(index, {
|
|
1385
1386
|
...structuredClone(progress),
|
|
1386
1387
|
});
|
|
1388
|
+
AsyncJobManager.instance()?.recordSubagentProgress(task.id, progress);
|
|
1387
1389
|
emitProgress();
|
|
1388
1390
|
},
|
|
1389
1391
|
authStorage: this.session.authStorage,
|
package/src/task/render.ts
CHANGED
|
@@ -690,6 +690,20 @@ function renderAgentProgress(
|
|
|
690
690
|
return lines;
|
|
691
691
|
}
|
|
692
692
|
|
|
693
|
+
/**
|
|
694
|
+
* Public wrapper to render a single subagent's live `AgentProgress` for the
|
|
695
|
+
* `subagent` await panel. Reuses the internal task-progress renderer so the
|
|
696
|
+
* await panel stays at parity with the inline task panel.
|
|
697
|
+
*/
|
|
698
|
+
export function renderSubagentLiveProgress(
|
|
699
|
+
progress: AgentProgress,
|
|
700
|
+
expanded: boolean,
|
|
701
|
+
theme: Theme,
|
|
702
|
+
spinnerFrame?: number,
|
|
703
|
+
): string[] {
|
|
704
|
+
return renderAgentProgress(progress, true, expanded, theme, spinnerFrame);
|
|
705
|
+
}
|
|
706
|
+
|
|
693
707
|
/**
|
|
694
708
|
* Render review result with combined verdict + findings in tree structure.
|
|
695
709
|
*/
|
package/src/tools/ask.ts
CHANGED
|
@@ -175,6 +175,7 @@ interface UIContext {
|
|
|
175
175
|
onLeft?: () => void;
|
|
176
176
|
onRight?: () => void;
|
|
177
177
|
helpText?: string;
|
|
178
|
+
customInput?: { optionLabel: string; onSubmit: (text: string) => void };
|
|
178
179
|
},
|
|
179
180
|
): Promise<string | undefined>;
|
|
180
181
|
editor(
|
|
@@ -194,6 +195,7 @@ async function askSingleQuestion(
|
|
|
194
195
|
): Promise<SelectionResult> {
|
|
195
196
|
const { recommended, timeout, signal, initialSelection, navigation, scrollTitleRows } = options;
|
|
196
197
|
const doneLabel = getDoneOptionLabel();
|
|
198
|
+
const otherOptionLabel = options.otherOptionLabel ?? OTHER_OPTION;
|
|
197
199
|
let selectedOptions = [...(initialSelection?.selectedOptions ?? [])];
|
|
198
200
|
let customInput = initialSelection?.customInput;
|
|
199
201
|
let timedOut = false;
|
|
@@ -202,11 +204,20 @@ async function askSingleQuestion(
|
|
|
202
204
|
prompt: string,
|
|
203
205
|
optionsToShow: string[],
|
|
204
206
|
initialIndex?: number,
|
|
205
|
-
): Promise<{
|
|
207
|
+
): Promise<{
|
|
208
|
+
choice: string | undefined;
|
|
209
|
+
timedOut: boolean;
|
|
210
|
+
navigation?: "back" | "forward";
|
|
211
|
+
inlineInput?: string;
|
|
212
|
+
}> => {
|
|
206
213
|
let timeoutTriggered = false;
|
|
207
214
|
const onTimeout = () => {
|
|
208
215
|
timeoutTriggered = true;
|
|
209
216
|
};
|
|
217
|
+
// Inline custom input: the TUI selector keeps the question and option
|
|
218
|
+
// list on screen and collects the "Other" text below the list, instead
|
|
219
|
+
// of swapping to a separate editor screen that hides the question.
|
|
220
|
+
let inlineInput: string | undefined;
|
|
210
221
|
let navigationAction: "back" | "forward" | undefined;
|
|
211
222
|
const baseHelpText = navigation
|
|
212
223
|
? "up/down navigate enter select ←/→ question esc cancel"
|
|
@@ -222,6 +233,12 @@ async function askSingleQuestion(
|
|
|
222
233
|
scrollTitleRows,
|
|
223
234
|
onTimeout,
|
|
224
235
|
helpText,
|
|
236
|
+
customInput: {
|
|
237
|
+
optionLabel: otherOptionLabel,
|
|
238
|
+
onSubmit: (text: string) => {
|
|
239
|
+
inlineInput = text;
|
|
240
|
+
},
|
|
241
|
+
},
|
|
225
242
|
onLeft: navigation?.allowBack
|
|
226
243
|
? () => {
|
|
227
244
|
navigationAction = "back";
|
|
@@ -240,16 +257,17 @@ async function askSingleQuestion(
|
|
|
240
257
|
if (!timeoutTriggered && choice === undefined && typeof timeout === "number") {
|
|
241
258
|
timeoutTriggered = Date.now() - startMs >= timeout;
|
|
242
259
|
}
|
|
243
|
-
return { choice, timedOut: timeoutTriggered, navigation: navigationAction };
|
|
260
|
+
return { choice, timedOut: timeoutTriggered, navigation: navigationAction, inlineInput };
|
|
244
261
|
};
|
|
245
262
|
|
|
263
|
+
// Fallback for UI contexts that don't support inline custom input (they
|
|
264
|
+
// resolve the "Other" label without invoking customInput.onSubmit).
|
|
246
265
|
const promptForCustomInput = async (): Promise<{ input: string | undefined }> => {
|
|
247
266
|
const dialogOptions = signal ? { signal } : undefined;
|
|
248
267
|
const showCustomInput = () => ui.editor("Enter your response:", undefined, dialogOptions, { promptStyle: true });
|
|
249
268
|
const input = signal ? await untilAborted(signal, showCustomInput) : await showCustomInput();
|
|
250
269
|
return { input };
|
|
251
270
|
};
|
|
252
|
-
const otherOptionLabel = options.otherOptionLabel ?? OTHER_OPTION;
|
|
253
271
|
|
|
254
272
|
const promptWithProgress = navigation?.progressText ? `${question} (${navigation.progressText})` : question;
|
|
255
273
|
if (multi) {
|
|
@@ -278,6 +296,7 @@ async function askSingleQuestion(
|
|
|
278
296
|
choice,
|
|
279
297
|
timedOut: selectTimedOut,
|
|
280
298
|
navigation: arrowNavigation,
|
|
299
|
+
inlineInput,
|
|
281
300
|
} = await selectOption(`${prefix}${promptWithProgress}`, opts, cursorIndex);
|
|
282
301
|
|
|
283
302
|
if (arrowNavigation) {
|
|
@@ -297,11 +316,11 @@ async function askSingleQuestion(
|
|
|
297
316
|
timedOut = true;
|
|
298
317
|
break;
|
|
299
318
|
}
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
319
|
+
const input = inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input;
|
|
320
|
+
if (input === undefined) {
|
|
302
321
|
break;
|
|
303
322
|
}
|
|
304
|
-
customInput =
|
|
323
|
+
customInput = input;
|
|
305
324
|
break;
|
|
306
325
|
}
|
|
307
326
|
|
|
@@ -353,6 +372,7 @@ async function askSingleQuestion(
|
|
|
353
372
|
choice,
|
|
354
373
|
timedOut: selectTimedOut,
|
|
355
374
|
navigation: arrowNavigation,
|
|
375
|
+
inlineInput,
|
|
356
376
|
} = await selectOption(promptWithProgress, optionsWithNavigation, initialIndex);
|
|
357
377
|
timedOut = selectTimedOut;
|
|
358
378
|
|
|
@@ -365,12 +385,12 @@ async function askSingleQuestion(
|
|
|
365
385
|
}
|
|
366
386
|
} else if (choice === otherOptionLabel) {
|
|
367
387
|
if (!selectTimedOut) {
|
|
368
|
-
const
|
|
369
|
-
if (
|
|
370
|
-
customInput =
|
|
388
|
+
const input = inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input;
|
|
389
|
+
if (input !== undefined) {
|
|
390
|
+
customInput = input;
|
|
371
391
|
selectedOptions = [];
|
|
372
392
|
}
|
|
373
|
-
// If
|
|
393
|
+
// If input was dismissed (undefined), keep prior selectedOptions/customInput intact
|
|
374
394
|
}
|
|
375
395
|
} else {
|
|
376
396
|
selectedOptions = [stripRecommendedSuffix(choice)];
|
package/src/tools/bash.ts
CHANGED
|
@@ -609,6 +609,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
609
609
|
label?: string;
|
|
610
610
|
ctx?: AgentToolContext;
|
|
611
611
|
onRawLine?: (line: string, jobId: string) => void;
|
|
612
|
+
shouldAcceptRawLine?: (jobId: string) => boolean;
|
|
613
|
+
lifecycle?: import("../async").AsyncJobLifecycleCleanup;
|
|
612
614
|
} = {},
|
|
613
615
|
): Promise<{ jobId: string; label: string; commandCwd: string }> {
|
|
614
616
|
const manager = AsyncJobManager.instance();
|
|
@@ -624,12 +626,14 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
624
626
|
let cursorOffset = 0;
|
|
625
627
|
let lineBuffer = "";
|
|
626
628
|
const dispatchLines = (chunk: string) => {
|
|
629
|
+
if (opts.shouldAcceptRawLine?.(currentJobId) === false) return;
|
|
627
630
|
if (!onRawLine) return;
|
|
628
631
|
lineBuffer += chunk;
|
|
629
632
|
let newlineIndex = lineBuffer.indexOf("\n");
|
|
630
633
|
while (newlineIndex !== -1) {
|
|
631
634
|
const line = lineBuffer.slice(0, newlineIndex);
|
|
632
635
|
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
636
|
+
if (opts.shouldAcceptRawLine?.(currentJobId) === false) return;
|
|
633
637
|
try {
|
|
634
638
|
onRawLine(line, currentJobId);
|
|
635
639
|
} catch (error) {
|
|
@@ -642,6 +646,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
642
646
|
};
|
|
643
647
|
const flushTrailingLine = () => {
|
|
644
648
|
if (!onRawLine) return;
|
|
649
|
+
if (opts.shouldAcceptRawLine?.(currentJobId) === false) return;
|
|
645
650
|
if (lineBuffer.length === 0) return;
|
|
646
651
|
const remainder = lineBuffer;
|
|
647
652
|
lineBuffer = "";
|
|
@@ -693,7 +698,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
693
698
|
throw error instanceof Error ? error : new Error(String(error));
|
|
694
699
|
}
|
|
695
700
|
},
|
|
696
|
-
{ ownerId, metadata: { monitor: true } },
|
|
701
|
+
{ ownerId, metadata: { monitor: true }, lifecycle: opts.lifecycle },
|
|
697
702
|
);
|
|
698
703
|
currentJobId = jobId;
|
|
699
704
|
return { jobId, label, commandCwd: prepared.commandCwd };
|