@gajae-code/coding-agent 0.4.3 → 0.4.5

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 (92) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/types/async/job-manager.d.ts +19 -1
  3. package/dist/types/cli/fast-help.d.ts +1 -0
  4. package/dist/types/cli/setup-cli.d.ts +16 -1
  5. package/dist/types/commands/coordinator.d.ts +19 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/commands/mcp-serve.d.ts +24 -0
  8. package/dist/types/commands/setup.d.ts +47 -0
  9. package/dist/types/config/model-registry.d.ts +3 -0
  10. package/dist/types/config/models-config-schema.d.ts +5 -0
  11. package/dist/types/coordinator/contract.d.ts +4 -0
  12. package/dist/types/coordinator-mcp/policy.d.ts +24 -0
  13. package/dist/types/coordinator-mcp/safety.d.ts +26 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +58 -0
  15. package/dist/types/extensibility/extensions/types.d.ts +13 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  17. package/dist/types/harness-control-plane/finalize.d.ts +5 -0
  18. package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
  19. package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
  20. package/dist/types/harness-control-plane/receipts.d.ts +46 -0
  21. package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
  22. package/dist/types/harness-control-plane/types.d.ts +9 -1
  23. package/dist/types/main.d.ts +2 -2
  24. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  25. package/dist/types/modes/utils/abort-message.d.ts +4 -0
  26. package/dist/types/session/session-manager.d.ts +8 -0
  27. package/dist/types/setup/hermes-setup.d.ts +78 -0
  28. package/dist/types/task/fork-context-advisory.d.ts +13 -0
  29. package/dist/types/task/receipt.d.ts +1 -0
  30. package/dist/types/task/render.d.ts +7 -1
  31. package/dist/types/task/roi-reconciliation.d.ts +27 -0
  32. package/dist/types/task/types.d.ts +10 -0
  33. package/dist/types/tools/subagent-render.d.ts +25 -0
  34. package/dist/types/tools/subagent.d.ts +5 -1
  35. package/package.json +8 -7
  36. package/scripts/build-binary.ts +4 -0
  37. package/src/async/job-manager.ts +43 -1
  38. package/src/cli/fast-help.ts +80 -0
  39. package/src/cli/setup-cli.ts +95 -2
  40. package/src/cli.ts +109 -16
  41. package/src/commands/coordinator.ts +113 -0
  42. package/src/commands/harness.ts +92 -9
  43. package/src/commands/mcp-serve.ts +63 -0
  44. package/src/commands/setup.ts +34 -1
  45. package/src/config/models-config-schema.ts +1 -0
  46. package/src/coordinator/contract.ts +21 -0
  47. package/src/coordinator-mcp/policy.ts +160 -0
  48. package/src/coordinator-mcp/safety.ts +80 -0
  49. package/src/coordinator-mcp/server.ts +1519 -0
  50. package/src/cursor.ts +30 -2
  51. package/src/extensibility/extensions/types.ts +13 -0
  52. package/src/gjc-runtime/launch-worktree.ts +12 -1
  53. package/src/gjc-runtime/session-state-sidecar.ts +117 -0
  54. package/src/harness-control-plane/finalize.ts +39 -5
  55. package/src/harness-control-plane/owner.ts +9 -1
  56. package/src/harness-control-plane/phase-rollup.ts +96 -0
  57. package/src/harness-control-plane/receipt-ingest.ts +127 -0
  58. package/src/harness-control-plane/receipts.ts +229 -1
  59. package/src/harness-control-plane/rpc-adapter.ts +8 -0
  60. package/src/harness-control-plane/types.ts +29 -1
  61. package/src/internal-urls/docs-index.generated.ts +6 -4
  62. package/src/main.ts +7 -3
  63. package/src/modes/components/hook-selector.ts +109 -5
  64. package/src/modes/components/status-line.ts +6 -6
  65. package/src/modes/controllers/event-controller.ts +5 -4
  66. package/src/modes/controllers/extension-ui-controller.ts +16 -1
  67. package/src/modes/interactive-mode.ts +4 -5
  68. package/src/modes/print-mode.ts +1 -1
  69. package/src/modes/theme/theme.ts +2 -2
  70. package/src/modes/utils/abort-message.ts +41 -0
  71. package/src/modes/utils/context-usage.ts +15 -8
  72. package/src/modes/utils/ui-helpers.ts +5 -6
  73. package/src/prompts/agents/architect.md +6 -0
  74. package/src/prompts/agents/critic.md +6 -0
  75. package/src/prompts/agents/planner.md +8 -1
  76. package/src/sdk.ts +9 -4
  77. package/src/session/agent-session.ts +22 -5
  78. package/src/session/session-manager.ts +20 -0
  79. package/src/setup/hermes/templates/operator-instructions.v1.md +30 -0
  80. package/src/setup/hermes-setup.ts +484 -0
  81. package/src/task/fork-context-advisory.ts +99 -0
  82. package/src/task/index.ts +33 -2
  83. package/src/task/receipt.ts +2 -0
  84. package/src/task/render.ts +14 -0
  85. package/src/task/roi-reconciliation.ts +90 -0
  86. package/src/task/types.ts +7 -0
  87. package/src/tools/ask.ts +30 -10
  88. package/src/tools/index.ts +2 -2
  89. package/src/tools/renderers.ts +2 -0
  90. package/src/tools/subagent-render.ts +169 -0
  91. package/src/tools/subagent.ts +49 -7
  92. package/src/utils/title-generator.ts +16 -2
package/src/cli.ts CHANGED
@@ -1,24 +1,11 @@
1
1
  #!/usr/bin/env bun
2
- import { installH2Fetch } from "@gajae-code/ai";
3
- import { APP_NAME, MIN_BUN_VERSION, procmgr, VERSION } from "@gajae-code/utils";
4
-
5
- // Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
6
- // discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
7
- // can't toggle from JS, so we patch globalThis.fetch to pass
8
- // `protocol: "http2"` per request, with transparent HTTP/1.1 fallback on
9
- // `HTTP2Unsupported`. See @gajae-code/ai/utils/h2-fetch for details.
10
- installH2Fetch();
11
-
12
- // Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
13
- // Otherwise every child bun process (subagents, plugin installs, ptree spawns,
14
- // etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
15
- procmgr.scrubProcessEnv();
16
2
 
17
3
  /**
18
4
  * CLI entry point — registers all commands explicitly and delegates to the
19
5
  * lightweight CLI runner from pi-utils.
20
6
  */
21
- import { type CliConfig, type CommandEntry, run } from "@gajae-code/utils/cli";
7
+ import { Args, type CliConfig, Command, type CommandEntry, Flags, run } from "@gajae-code/utils/cli";
8
+ import { APP_NAME, MIN_BUN_VERSION, VERSION } from "@gajae-code/utils/dirs";
22
9
 
23
10
  if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
24
11
  process.stderr.write(
@@ -28,6 +15,8 @@ if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
28
15
  }
29
16
 
30
17
  process.title = APP_NAME;
18
+ const rootHelpFlags = ["--help", "-h", "help"];
19
+ const versionFlags = ["--version", "-v"];
31
20
 
32
21
  const commands: CommandEntry[] = [
33
22
  { name: "codex-native-hook", load: () => import("./commands/codex-native-hook").then(m => m.default) },
@@ -36,10 +25,12 @@ const commands: CommandEntry[] = [
36
25
  { name: "skills", load: () => import("./commands/skills").then(m => m.default) },
37
26
  { name: "session", load: () => import("./commands/session").then(m => m.default) },
38
27
  { name: "harness", load: () => import("./commands/harness").then(m => m.default) },
28
+ { name: "coordinator", load: () => import("./commands/coordinator").then(m => m.default) },
39
29
  { name: "team", load: () => import("./commands/team").then(m => m.default) },
40
30
  { name: "ultragoal", load: () => import("./commands/ultragoal").then(m => m.default) },
41
31
  { name: "ralplan", load: () => import("./commands/ralplan").then(m => m.default) },
42
32
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
33
+ { name: "mcp-serve", load: () => import("./commands/mcp-serve").then(m => m.default) },
43
34
  {
44
35
  name: "contribute-pr",
45
36
  aliases: ["contribution-prep"],
@@ -52,7 +43,7 @@ const commands: CommandEntry[] = [
52
43
 
53
44
  async function showHelp(config: CliConfig): Promise<void> {
54
45
  const { renderRootHelp } = await import("@gajae-code/utils/cli");
55
- const { getExtraHelpText } = await import("./cli/args");
46
+ const { getExtraHelpText } = await import("./cli/fast-help");
56
47
  renderRootHelp(config);
57
48
  const extra = getExtraHelpText();
58
49
  if (extra.trim().length > 0) {
@@ -60,6 +51,93 @@ async function showHelp(config: CliConfig): Promise<void> {
60
51
  }
61
52
  }
62
53
 
54
+ async function installRuntimeGlobals(): Promise<void> {
55
+ const [{ installH2Fetch }, { procmgr }] = await Promise.all([import("@gajae-code/ai"), import("@gajae-code/utils")]);
56
+ // Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
57
+ // discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
58
+ // can't toggle from JS, so we patch globalThis.fetch to pass
59
+ // `protocol: "http2"` per request, with transparent HTTP/1.1 fallback on
60
+ // `HTTP2Unsupported`. See @gajae-code/ai/utils/h2-fetch for details.
61
+ installH2Fetch();
62
+
63
+ // Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
64
+ // Otherwise every child bun process (subagents, plugin installs, ptree spawns,
65
+ // etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
66
+ procmgr.scrubProcessEnv();
67
+ }
68
+
69
+ class RootHelpCommand extends Command {
70
+ static description = "Red-claw AI coding assistant";
71
+ static hidden = true;
72
+ static args = {
73
+ messages: Args.string({
74
+ description: "Messages to send (prefix files with @)",
75
+ required: false,
76
+ multiple: true,
77
+ }),
78
+ };
79
+ static flags = {
80
+ model: Flags.string({ description: 'Model to use (fuzzy match: "opus", "gpt-5.2", or "openai/gpt-5.2")' }),
81
+ smol: Flags.string({ description: "Smol/fast model for lightweight tasks (or GJC_SMOL_MODEL env)" }),
82
+ slow: Flags.string({ description: "Slow/reasoning model for thorough analysis (or GJC_SLOW_MODEL env)" }),
83
+ plan: Flags.string({ description: "Plan model for architectural planning (or GJC_PLAN_MODEL env)" }),
84
+ mpreset: Flags.string({ description: "Model profile preset to activate for this session" }),
85
+ default: Flags.boolean({ description: "Persist --mpreset as the default model profile" }),
86
+ provider: Flags.string({ description: "Provider to use (legacy; prefer --model)" }),
87
+ "api-key": Flags.string({ description: "API key (defaults to env vars)" }),
88
+ "system-prompt": Flags.string({ description: "System prompt (default: coding assistant prompt)" }),
89
+ "append-system-prompt": Flags.string({ description: "Append text or file contents to the system prompt" }),
90
+ "allow-home": Flags.boolean({ description: "Allow starting in ~ without auto-switching to a temp dir" }),
91
+ mode: Flags.string({
92
+ description: "Output mode: text (default), json, rpc, acp, rpc-ui, or bridge",
93
+ options: ["text", "json", "rpc", "acp", "rpc-ui", "bridge"],
94
+ }),
95
+ print: Flags.boolean({ char: "p", description: "Non-interactive mode: process prompt and exit" }),
96
+ continue: Flags.boolean({ char: "c", description: "Continue previous session" }),
97
+ resume: Flags.string({ char: "r", description: "Resume a session (by ID prefix, path, or picker if omitted)" }),
98
+ "session-dir": Flags.string({ description: "Directory for session storage and lookup" }),
99
+ "no-session": Flags.boolean({ description: "Don't save session (ephemeral)" }),
100
+ models: Flags.string({ description: "Comma-separated model patterns for Ctrl+P cycling" }),
101
+ "no-tools": Flags.boolean({ description: "Disable all built-in tools" }),
102
+ "no-lsp": Flags.boolean({ description: "Disable LSP tools, formatting, and diagnostics" }),
103
+ "no-pty": Flags.boolean({ description: "Disable PTY-based interactive bash execution" }),
104
+ tmux: Flags.boolean({ description: "Launch interactive startup inside tmux" }),
105
+ tools: Flags.string({ description: "Comma-separated list of tools to enable (default: all)" }),
106
+ thinking: Flags.string({
107
+ description: "Set thinking level: ultra, high, medium, low",
108
+ options: ["ultra", "high", "medium", "low"],
109
+ }),
110
+ hook: Flags.string({ description: "Load a hook/extension file (can be used multiple times)", multiple: true }),
111
+ extension: Flags.string({
112
+ char: "e",
113
+ description: "Load an extension file (can be used multiple times)",
114
+ multiple: true,
115
+ }),
116
+ "no-extensions": Flags.boolean({ description: "Disable extension discovery (explicit -e paths still work)" }),
117
+ "no-skills": Flags.boolean({ description: "Disable skills discovery and loading" }),
118
+ skills: Flags.string({ description: "Comma-separated glob patterns to filter skills (e.g., git-*,docker)" }),
119
+ "no-rules": Flags.boolean({ description: "Disable rules discovery and loading" }),
120
+ export: Flags.string({ description: "Export session file to HTML and exit" }),
121
+ "list-models": Flags.string({ description: "List available models (with optional fuzzy search)" }),
122
+ "no-title": Flags.boolean({ description: "Disable title auto-generation" }),
123
+ };
124
+ static examples = [
125
+ `# Interactive mode\n ${APP_NAME}`,
126
+ `# Interactive mode with initial prompt\n ${APP_NAME} "List all .ts files in src/"`,
127
+ `# Include files in initial message\n ${APP_NAME} @prompt.md @image.png "What color is the sky?"`,
128
+ `# Non-interactive mode (process and exit)\n ${APP_NAME} -p "List all .ts files in src/"`,
129
+ `# Continue previous session\n ${APP_NAME} --continue "What did we discuss?"`,
130
+ `# Launch in a sibling git worktree\n ${APP_NAME} --worktree`,
131
+ `# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`,
132
+ `# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`,
133
+ `# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-standard`,
134
+ `# Persist a model profile as the default\n ${APP_NAME} --mpreset opencode-go-pro --default`,
135
+ `# Export a session file to HTML\n ${APP_NAME} --export ~/.gjc/agent/sessions/--path--/session.jsonl`,
136
+ ];
137
+ static strict = false;
138
+ async run(): Promise<void> {}
139
+ }
140
+
63
141
  /**
64
142
  * Determine whether argv[0] is a known subcommand name.
65
143
  * If not, the entire argv is treated as args to the default "launch" command.
@@ -107,6 +185,21 @@ export async function runCli(argv: string[]): Promise<void> {
107
185
  await runSmokeTest();
108
186
  return;
109
187
  }
188
+ if (rootHelpFlags.includes(argv[0] ?? "")) {
189
+ const { renderRootHelp } = await import("@gajae-code/utils/cli");
190
+ const { getExtraHelpText } = await import("./cli/fast-help");
191
+ renderRootHelp({ bin: APP_NAME, version: VERSION, commands: new Map([["launch", RootHelpCommand]]) });
192
+ const extra = getExtraHelpText();
193
+ if (extra.trim().length > 0) {
194
+ process.stdout.write(`\n${extra}\n`);
195
+ }
196
+ return;
197
+ }
198
+ if (versionFlags.includes(argv[0] ?? "")) {
199
+ process.stdout.write(`${APP_NAME}/${VERSION}\n`);
200
+ return;
201
+ }
202
+ await installRuntimeGlobals();
110
203
  // --help and --version are handled by run() directly, don't rewrite those.
111
204
  // Everything else that isn't a known subcommand routes to "launch".
112
205
  const first = argv[0];
@@ -0,0 +1,113 @@
1
+ import { Args, Command, Flags } from "@gajae-code/utils/cli";
2
+ import {
3
+ COORDINATOR_MCP_PROTOCOL_VERSION,
4
+ COORDINATOR_MCP_SERVER_NAME,
5
+ COORDINATOR_MCP_TOOL_NAMES,
6
+ } from "../coordinator/contract";
7
+ import { buildCoordinatorMcpConfig } from "../coordinator-mcp/policy";
8
+
9
+ function writeJson(value: unknown): void {
10
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
11
+ `);
12
+ }
13
+
14
+ function coordinatorContractPayload(): {
15
+ ok: true;
16
+ server: { name: string; protocolVersion: string };
17
+ readOnly: true;
18
+ tools: string[];
19
+ } {
20
+ return {
21
+ ok: true,
22
+ server: { name: COORDINATOR_MCP_SERVER_NAME, protocolVersion: COORDINATOR_MCP_PROTOCOL_VERSION },
23
+ readOnly: true,
24
+ tools: [...COORDINATOR_MCP_TOOL_NAMES],
25
+ };
26
+ }
27
+
28
+ function coordinatorDoctorPayload(): {
29
+ ok: boolean;
30
+ checks: Array<{ id: string; status: "pass" | "warn" | "fail"; detail: string }>;
31
+ } {
32
+ const config = buildCoordinatorMcpConfig(process.env);
33
+ const checks: Array<{ id: string; status: "pass" | "warn" | "fail"; detail: string }> = [];
34
+ checks.push({
35
+ id: "workdir_roots",
36
+ status: config.allowedRoots.length > 0 ? "pass" : "fail",
37
+ detail:
38
+ config.allowedRoots.length > 0 ? config.allowedRoots.join(":") : "GJC_COORDINATOR_MCP_WORKDIR_ROOTS is empty",
39
+ });
40
+ checks.push({
41
+ id: "session_mutations",
42
+ status: config.mutationClasses.has("sessions") ? "pass" : "fail",
43
+ detail: config.mutationClasses.has("sessions") ? "sessions mutation enabled" : "sessions mutation disabled",
44
+ });
45
+ checks.push({
46
+ id: "session_command",
47
+ status: config.sessionCommand ? "pass" : "warn",
48
+ detail:
49
+ config.sessionCommand ??
50
+ "GJC_COORDINATOR_MCP_SESSION_COMMAND is unset; registration can still reuse visible sessions",
51
+ });
52
+ checks.push({
53
+ id: "namespace",
54
+ status: config.namespace.profile && config.namespace.repo ? "pass" : "warn",
55
+ detail: `profile=${config.namespace.profile ?? "<unset>"} repo=${config.namespace.repo ?? "<unset>"}`,
56
+ });
57
+ return { ok: checks.every(check => check.status !== "fail"), checks };
58
+ }
59
+
60
+ export default class Coordinator extends Command {
61
+ static description = "Inspect GJC coordinator MCP bridge contracts";
62
+ static strict = false;
63
+
64
+ static args = {
65
+ action: Args.string({ description: "Action to run (check or tools)", required: false }),
66
+ };
67
+
68
+ static flags = {
69
+ json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: false }),
70
+ };
71
+
72
+ async run(): Promise<void> {
73
+ const { args, flags } = await this.parse(Coordinator);
74
+ const action = args.action ?? "check";
75
+ if (action !== "check" && action !== "tools" && action !== "doctor") {
76
+ const payload = { ok: false, reason: "unknown_coordinator_subcommand", subcommand: action };
77
+ if (flags.json) writeJson(payload);
78
+ else
79
+ process.stderr.write(`unknown_coordinator_subcommand:${action}
80
+ `);
81
+ process.exit(1);
82
+ }
83
+
84
+ if (action === "doctor") {
85
+ const doctor = coordinatorDoctorPayload();
86
+ if (flags.json) {
87
+ writeJson(doctor);
88
+ return;
89
+ }
90
+ process.stdout.write(`ok: ${doctor.ok}\n`);
91
+ for (const check of doctor.checks) process.stdout.write(`${check.status}\t${check.id}\t${check.detail}\n`);
92
+ return;
93
+ }
94
+ const payload = coordinatorContractPayload();
95
+ if (flags.json) {
96
+ writeJson(action === "tools" ? { ok: true, tools: payload.tools } : payload);
97
+ return;
98
+ }
99
+ if (action === "tools") {
100
+ for (const tool of payload.tools)
101
+ process.stdout.write(`${tool}
102
+ `);
103
+ return;
104
+ }
105
+ process.stdout.write(
106
+ `server: ${payload.server.name}
107
+ protocol: ${payload.server.protocolVersion}
108
+ readOnly: true
109
+ tools: ${payload.tools.length}
110
+ `,
111
+ );
112
+ }
113
+ }
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { execFileSync } from "node:child_process";
12
12
  import { randomBytes } from "node:crypto";
13
- import { existsSync } from "node:fs";
13
+ import { existsSync, readFileSync } from "node:fs";
14
14
  import { Args, Command, Flags } from "@gajae-code/utils/cli";
15
15
  import { resolveGjcTmuxCommand, sanitizeTmuxToken } from "../gjc-runtime/tmux-common";
16
16
  import { classifyRecovery } from "../harness-control-plane/classifier";
@@ -247,6 +247,30 @@ interface OwnerExitEvidence {
247
247
  transient: boolean;
248
248
  /** ISO timestamp of the most recent non-terminal RPC-derived owner event, if any (observability only). */
249
249
  lastRpcActivityAt: string | null;
250
+ /**
251
+ * True when the owner started (reported live) but died before accepting the first prompt.
252
+ * This is a startup blocker, not a healthy live gate: callers must recover before submit.
253
+ */
254
+ startupBlocker: boolean;
255
+ /** Explicit, human-actionable recovery guidance for the surfaced exit reason. */
256
+ recoveryGuidance: string;
257
+ }
258
+
259
+ function ownerExitGuidance(reason: string, startupBlocker: boolean): string {
260
+ if (startupBlocker) {
261
+ return "owner started and reported live but exited before accepting the first prompt; run `gjc harness recover --session <id>` to respawn the owner, then resubmit the prompt";
262
+ }
263
+ switch (reason) {
264
+ case "owner-exited-after-prompt-acceptance":
265
+ return "owner exited after accepting a prompt; run `gjc harness recover --session <id>` to preserve in-flight work and classify the vanish before resubmitting";
266
+ case "owner-lease-expired":
267
+ case "owner-endpoint-unreachable":
268
+ return "owner lease is stale or its endpoint did not route; run `gjc harness recover --session <id>` to respawn or take over the owner";
269
+ case "owner-liveness-unknown-permission-denied":
270
+ return "owner liveness cannot be probed (permission denied); verify the owner process out-of-band before recover";
271
+ default:
272
+ return "no live owner holds this session; run `gjc harness recover --session <id>` to (re)spawn an owner, then resubmit";
273
+ }
250
274
  }
251
275
 
252
276
  async function buildOwnerExitEvidence(root: string, state: SessionState): Promise<OwnerExitEvidence> {
@@ -286,6 +310,12 @@ async function buildOwnerExitEvidence(root: string, state: SessionState): Promis
286
310
  } else {
287
311
  reason = "owner-endpoint-unreachable";
288
312
  }
313
+ // A just-started owner that emitted `owner_started` (so it reported live) but is now terminal
314
+ // without ever accepting a prompt died during startup. Surface this as an explicit, actionable
315
+ // startup blocker rather than letting `submit` fall through to a misleading `owner-not-live` gate.
316
+ const ownerStarted = events.some(event => event.kind === "owner_started");
317
+ const startupBlocker = terminal && ownerStarted && !promptAcceptedSeen && !completedSeen;
318
+ if (startupBlocker) reason = "owner-died-before-first-prompt";
289
319
  return {
290
320
  reason,
291
321
  leaseStatus,
@@ -301,6 +331,8 @@ async function buildOwnerExitEvidence(root: string, state: SessionState): Promis
301
331
  terminal,
302
332
  transient,
303
333
  lastRpcActivityAt,
334
+ startupBlocker,
335
+ recoveryGuidance: ownerExitGuidance(reason, startupBlocker),
304
336
  };
305
337
  }
306
338
 
@@ -404,6 +436,29 @@ async function markVanishedOwnerBlocked(
404
436
  return state;
405
437
  }
406
438
 
439
+ const OWNER_STARTUP_BLOCKER = "owner-died-before-first-prompt";
440
+
441
+ /**
442
+ * Persist an explicit startup blocker when an owner started, reported live, but died before
443
+ * accepting the first prompt. This makes the failure an actionable lifecycle state instead of a
444
+ * silent `owner-not-live` gate, so observe/recover surface it and recover can respawn the owner.
445
+ */
446
+ async function markStartupOwnerBlocked(
447
+ root: string,
448
+ state: SessionState,
449
+ ownerExit: OwnerExitEvidence,
450
+ ): Promise<SessionState> {
451
+ if (!ownerExit.startupBlocker) return state;
452
+ if (state.lifecycle === "completed" || state.lifecycle === "retired") return state;
453
+ state.lifecycle = "blocked";
454
+ state.blockers = state.blockers.includes(OWNER_STARTUP_BLOCKER)
455
+ ? state.blockers
456
+ : [...state.blockers, OWNER_STARTUP_BLOCKER];
457
+ state.updatedAt = nowIso();
458
+ await writeSessionState(root, state);
459
+ return state;
460
+ }
461
+
407
462
  function resolveRetryBudget(input: Record<string, unknown>): RetryBudget {
408
463
  const supplied = input.retryBudget;
409
464
  if (supplied && typeof supplied === "object" && !Array.isArray(supplied)) {
@@ -453,6 +508,7 @@ export default class Harness extends Command {
453
508
 
454
509
  static flags = {
455
510
  input: Flags.string({ description: "JSON object input for the verb", default: "" }),
511
+ "prompt-file": Flags.string({ description: "Read submit prompt text from a file (submit verb only)" }),
456
512
  session: Flags.string({ char: "s", description: "Session id (re-grab a session)" }),
457
513
  cursor: Flags.string({ description: "Event cursor for events --follow (exclusive)", default: "0" }),
458
514
  follow: Flags.boolean({ description: "Tail the owner-written event log", default: false }),
@@ -472,6 +528,14 @@ export default class Harness extends Command {
472
528
  let root = resolveHarnessRoot();
473
529
  try {
474
530
  const input = parseInput(flags.input);
531
+ const promptFile = flags["prompt-file"];
532
+ if (promptFile !== undefined) {
533
+ if (verb !== "submit") throw new Error("prompt_file_only_supported_for_submit");
534
+ if (typeof input.prompt === "string" && input.prompt.length > 0) {
535
+ throw new Error("prompt_file_conflicts_with_input_prompt");
536
+ }
537
+ input.prompt = readFileSync(promptFile, "utf8");
538
+ }
475
539
  const sessionId = flags.session ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
476
540
  const expectedWorkspace = typeof input.workspace === "string" ? resolveInputWorkspace(input) : undefined;
477
541
  if (verb !== "start" && sessionId) {
@@ -834,10 +898,12 @@ export default class Harness extends Command {
834
898
  state = await reconcileCompletedOwnerExited(root, state, observation, completedTerminalEvent);
835
899
  const vanishedOwnerBlock = needsVanishedOwnerBlock(state, observation, completedTerminalEvent);
836
900
  state = await markVanishedOwnerBlocked(root, state, observation, completedTerminalEvent);
837
- const ownerExit =
838
- !ownerLive && (vanishedOwnerBlock || completedTerminalEvent)
839
- ? await buildOwnerExitEvidence(root, state)
840
- : null;
901
+ // Build owner-exit evidence whenever the owner is gone so a startup death (owner started,
902
+ // reported live, then died before the first prompt) is detectable, not just vanish/completion.
903
+ const ownerExit = !ownerLive ? await buildOwnerExitEvidence(root, state) : null;
904
+ const startupBlocked = ownerExit?.startupBlocker ?? false;
905
+ if (ownerExit && startupBlocked) state = await markStartupOwnerBlocked(root, state, ownerExit);
906
+ const includeOwnerExit = Boolean(ownerExit && (vanishedOwnerBlock || completedTerminalEvent || startupBlocked));
841
907
  writeJson(
842
908
  buildResponse(state, ownerLive, {
843
909
  observation: { ...observation, lifecycle: state.lifecycle },
@@ -848,7 +914,10 @@ export default class Harness extends Command {
848
914
  ...(completedTerminalEvent && !ownerLive
849
915
  ? { completedOwnerExited: true, terminalResult: completedTerminalEvent }
850
916
  : {}),
851
- ...(ownerExit ? { ownerExit } : {}),
917
+ ...(startupBlocked
918
+ ? { startupBlocked: true, blockerReason: OWNER_STARTUP_BLOCKER, guidance: ownerExit?.recoveryGuidance }
919
+ : {}),
920
+ ...(includeOwnerExit && ownerExit ? { ownerExit } : {}),
852
921
  }),
853
922
  );
854
923
  }
@@ -910,9 +979,22 @@ export default class Harness extends Command {
910
979
  async #submit(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
911
980
  const sessionId = requireSessionId(input, flagSession);
912
981
  if (await this.#tryOwnerRoute(root, sessionId, "submit", { ...input, sessionId })) return;
913
- const state = await loadState(root, sessionId);
914
- // No live owner: submission is blocked (never echoed-as-accepted).
915
- writeJson(buildResponse(state, false, { accepted: false, submitted: false, reason: "owner-not-live" }, false));
982
+ let state = await loadState(root, sessionId);
983
+ // No live owner: submission is blocked (never echoed-as-accepted). Surface owner exit
984
+ // evidence + explicit recovery guidance so the caller is not left with a bare gate.
985
+ const ownerExit = await buildOwnerExitEvidence(root, state);
986
+ // An owner that started, reported live, then died before accepting the first prompt is a
987
+ // startup blocker, not a healthy `owner-not-live` gate — persist it and report it as such.
988
+ if (ownerExit.startupBlocker) state = await markStartupOwnerBlocked(root, state, ownerExit);
989
+ const reason = ownerExit.startupBlocker ? ownerExit.reason : "owner-not-live";
990
+ writeJson(
991
+ buildResponse(
992
+ state,
993
+ false,
994
+ { accepted: false, submitted: false, reason, ownerExit, guidance: ownerExit.recoveryGuidance },
995
+ false,
996
+ ),
997
+ );
916
998
  process.exitCode = 1;
917
999
  }
918
1000
 
@@ -1030,6 +1112,7 @@ export default class Harness extends Command {
1030
1112
  decision,
1031
1113
  observation: { ...observation, lifecycle: state.lifecycle },
1032
1114
  ownerExit: afterExit,
1115
+ guidance: afterExit.recoveryGuidance,
1033
1116
  ...(restoredOwner
1034
1117
  ? {
1035
1118
  restoreAttempt: {
@@ -0,0 +1,63 @@
1
+ import { Args, Command, Flags } from "@gajae-code/utils/cli";
2
+ import {
3
+ COORDINATOR_MCP_PROTOCOL_VERSION,
4
+ COORDINATOR_MCP_SERVER_NAME,
5
+ COORDINATOR_MCP_TOOL_NAMES,
6
+ } from "../coordinator/contract";
7
+ import { runCoordinatorMcpStdio } from "../coordinator-mcp/server";
8
+
9
+ function writeJson(value: unknown): void {
10
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
11
+ }
12
+
13
+ export function validateMcpServeSubcommandForTest(server: string | undefined): void {
14
+ if (server !== "coordinator" && server !== "hermes") throw new Error(`unknown_mcp_serve_subcommand:${server ?? ""}`);
15
+ }
16
+
17
+ export default class McpServe extends Command {
18
+ static description = "Serve GJC MCP compatibility bridges";
19
+ static strict = false;
20
+
21
+ static args = {
22
+ server: Args.string({ description: "MCP server to run (coordinator or hermes alias)", required: false }),
23
+ };
24
+
25
+ static flags = {
26
+ json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: false }),
27
+ check: Flags.boolean({ description: "Validate server configuration and print a smoke summary", default: false }),
28
+ };
29
+
30
+ async run(): Promise<void> {
31
+ const { args, flags } = await this.parse(McpServe);
32
+ const server = args.server ?? "";
33
+ try {
34
+ validateMcpServeSubcommandForTest(server);
35
+ } catch (error) {
36
+ const subcommand = server;
37
+ if (flags.json) {
38
+ writeJson({ ok: false, reason: "unknown_mcp_serve_subcommand", subcommand });
39
+ } else {
40
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
41
+ }
42
+ process.exitCode = 1;
43
+ return;
44
+ }
45
+
46
+ if (flags.check) {
47
+ const payload = {
48
+ ok: true,
49
+ server: { name: COORDINATOR_MCP_SERVER_NAME, protocolVersion: COORDINATOR_MCP_PROTOCOL_VERSION },
50
+ readOnly: true,
51
+ tools: [...COORDINATOR_MCP_TOOL_NAMES],
52
+ };
53
+ if (flags.json) writeJson(payload);
54
+ else
55
+ process.stdout.write(
56
+ `server: ${payload.server.name}\nprotocol: ${payload.server.protocolVersion}\ntools: ${payload.tools.length}\n`,
57
+ );
58
+ return;
59
+ }
60
+
61
+ await runCoordinatorMcpStdio();
62
+ }
63
+ }
@@ -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,24 @@ 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
+ "no-worktree": Flags.boolean({ description: "Disable default GJC --worktree isolation for Hermes sessions" }),
32
+ "worktree-name": Flags.string({ description: "Named GJC --worktree branch for Hermes sessions" }),
33
+ "state-root": Flags.string({ description: "Hermes MCP coordination state root" }),
34
+ mutation: Flags.string({
35
+ description: "Hermes MCP mutation classes: sessions,questions,reports,all",
36
+ multiple: true,
37
+ }),
38
+ "artifact-byte-cap": Flags.string({ description: "Hermes MCP artifact read byte cap" }),
39
+ "server-key": Flags.string({ description: "Hermes MCP server key in coordinator config" }),
40
+ "gjc-command": Flags.string({ description: "Command used to start `gjc mcp-serve coordinator`" }),
41
+ target: Flags.string({ description: "Hermes config file target for config-only install" }),
42
+ "profile-dir": Flags.string({ description: "Hermes profile directory for full setup install" }),
25
43
  preset: Flags.string({ description: "Provider preset: minimax, minimax-cn, or glm" }),
26
44
  compat: Flags.string({ description: "Provider compatibility: openai or anthropic" }),
27
45
  provider: Flags.string({ description: "Provider id to add to models.yml" }),
@@ -46,6 +64,21 @@ export default class Setup extends Command {
46
64
  apiKeyEnv: flags["api-key-env"],
47
65
  model: flags.model,
48
66
  modelsPath: flags["models-path"],
67
+ smoke: flags.smoke,
68
+ install: flags.install,
69
+ root: flags.root,
70
+ repo: flags.repo,
71
+ profile: flags.profile,
72
+ sessionCommand: flags["session-command"],
73
+ noWorktree: flags["no-worktree"],
74
+ worktreeName: flags["worktree-name"],
75
+ stateRoot: flags["state-root"],
76
+ mutation: flags.mutation,
77
+ artifactByteCap: flags["artifact-byte-cap"],
78
+ serverKey: flags["server-key"],
79
+ gjcCommand: flags["gjc-command"],
80
+ target: flags.target,
81
+ profileDir: flags["profile-dir"],
49
82
  },
50
83
  };
51
84
  await initTheme();
@@ -36,6 +36,7 @@ export const OpenAICompatSchema = z.object({
36
36
  allowsSyntheticReasoningContentForToolCalls: z.boolean().optional(),
37
37
  requiresAssistantContentForToolCalls: z.boolean().optional(),
38
38
  supportsToolChoice: z.boolean().optional(),
39
+ supportsForcedToolChoice: z.boolean().optional(),
39
40
  disableReasoningOnForcedToolChoice: z.boolean().optional(),
40
41
  disableReasoningOnToolChoice: z.boolean().optional(),
41
42
  thinkingFormat: z.enum(["openai", "openrouter", "zai", "qwen", "qwen-chat-template"]).optional(),
@@ -0,0 +1,21 @@
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_register_session",
13
+ "gjc_coordinator_start_session",
14
+ "gjc_coordinator_send_prompt",
15
+ "gjc_coordinator_submit_question_answer",
16
+ "gjc_coordinator_read_turn",
17
+ "gjc_coordinator_await_turn",
18
+ "gjc_coordinator_report_status",
19
+ ] as const;
20
+
21
+ export type CoordinatorToolName = (typeof COORDINATOR_MCP_TOOL_NAMES)[number];