@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
@@ -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
+ }
@@ -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/task",
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,
@@ -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<{ choice: string | undefined; timedOut: boolean; navigation?: "back" | "forward" }> => {
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 customResult = await promptForCustomInput();
301
- if (customResult.input === undefined) {
319
+ const input = inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input;
320
+ if (input === undefined) {
302
321
  break;
303
322
  }
304
- customInput = customResult.input;
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 customResult = await promptForCustomInput();
369
- if (customResult.input !== undefined) {
370
- customInput = customResult.input;
388
+ const input = inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input;
389
+ if (input !== undefined) {
390
+ customInput = input;
371
391
  selectedOptions = [];
372
392
  }
373
- // If editor was dismissed (undefined), keep prior selectedOptions/customInput intact
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 };