@gajae-code/coding-agent 0.4.4 → 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 (68) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/types/cli/fast-help.d.ts +1 -0
  3. package/dist/types/cli/setup-cli.d.ts +2 -0
  4. package/dist/types/commands/harness.d.ts +3 -0
  5. package/dist/types/commands/setup.d.ts +6 -0
  6. package/dist/types/config/model-registry.d.ts +3 -0
  7. package/dist/types/config/models-config-schema.d.ts +5 -0
  8. package/dist/types/coordinator/contract.d.ts +1 -1
  9. package/dist/types/coordinator-mcp/server.d.ts +8 -2
  10. package/dist/types/harness-control-plane/finalize.d.ts +5 -0
  11. package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
  12. package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
  13. package/dist/types/harness-control-plane/receipts.d.ts +46 -0
  14. package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
  15. package/dist/types/harness-control-plane/types.d.ts +9 -1
  16. package/dist/types/main.d.ts +2 -2
  17. package/dist/types/modes/utils/abort-message.d.ts +4 -0
  18. package/dist/types/session/session-manager.d.ts +8 -0
  19. package/dist/types/setup/hermes-setup.d.ts +7 -0
  20. package/dist/types/task/fork-context-advisory.d.ts +13 -0
  21. package/dist/types/task/receipt.d.ts +1 -0
  22. package/dist/types/task/roi-reconciliation.d.ts +27 -0
  23. package/dist/types/task/types.d.ts +10 -0
  24. package/package.json +8 -7
  25. package/scripts/build-binary.ts +4 -0
  26. package/src/cli/fast-help.ts +80 -0
  27. package/src/cli/setup-cli.ts +12 -3
  28. package/src/cli.ts +107 -16
  29. package/src/commands/coordinator.ts +44 -1
  30. package/src/commands/harness.ts +92 -9
  31. package/src/commands/mcp-serve.ts +3 -2
  32. package/src/commands/setup.ts +4 -0
  33. package/src/config/models-config-schema.ts +1 -0
  34. package/src/coordinator/contract.ts +1 -0
  35. package/src/coordinator-mcp/server.ts +385 -182
  36. package/src/cursor.ts +30 -2
  37. package/src/gjc-runtime/launch-worktree.ts +12 -1
  38. package/src/gjc-runtime/session-state-sidecar.ts +38 -0
  39. package/src/harness-control-plane/finalize.ts +39 -5
  40. package/src/harness-control-plane/owner.ts +9 -1
  41. package/src/harness-control-plane/phase-rollup.ts +96 -0
  42. package/src/harness-control-plane/receipt-ingest.ts +127 -0
  43. package/src/harness-control-plane/receipts.ts +229 -1
  44. package/src/harness-control-plane/rpc-adapter.ts +8 -0
  45. package/src/harness-control-plane/types.ts +29 -1
  46. package/src/internal-urls/docs-index.generated.ts +6 -5
  47. package/src/main.ts +7 -3
  48. package/src/modes/components/status-line.ts +6 -6
  49. package/src/modes/controllers/event-controller.ts +5 -4
  50. package/src/modes/interactive-mode.ts +4 -5
  51. package/src/modes/print-mode.ts +1 -1
  52. package/src/modes/theme/theme.ts +2 -2
  53. package/src/modes/utils/abort-message.ts +41 -0
  54. package/src/modes/utils/context-usage.ts +15 -8
  55. package/src/modes/utils/ui-helpers.ts +5 -6
  56. package/src/sdk.ts +9 -4
  57. package/src/session/agent-session.ts +16 -5
  58. package/src/session/session-manager.ts +20 -0
  59. package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
  60. package/src/setup/hermes-setup.ts +63 -8
  61. package/src/task/fork-context-advisory.ts +99 -0
  62. package/src/task/index.ts +31 -2
  63. package/src/task/receipt.ts +2 -0
  64. package/src/task/roi-reconciliation.ts +90 -0
  65. package/src/task/types.ts +7 -0
  66. package/src/tools/index.ts +2 -2
  67. package/src/tools/subagent-render.ts +10 -1
  68. package/src/utils/title-generator.ts +16 -2
@@ -48,6 +48,8 @@ export interface SetupCommandArgs {
48
48
  repo?: string;
49
49
  profile?: string;
50
50
  sessionCommand?: string;
51
+ noWorktree?: boolean;
52
+ worktreeName?: string;
51
53
  stateRoot?: string;
52
54
  mutation?: string[];
53
55
  artifactByteCap?: string;
@@ -119,6 +121,10 @@ export function parseSetupArgs(args: string[]): SetupCommandArgs | undefined {
119
121
  flags.profile = args[++i];
120
122
  } else if (arg === "--session-command") {
121
123
  flags.sessionCommand = args[++i];
124
+ } else if (arg === "--no-worktree") {
125
+ flags.noWorktree = true;
126
+ } else if (arg === "--worktree-name") {
127
+ flags.worktreeName = args[++i];
122
128
  } else if (arg === "--state-root") {
123
129
  flags.stateRoot = args[++i];
124
130
  } else if (arg === "--mutation") {
@@ -493,7 +499,8 @@ ${chalk.bold("Provider example:")}
493
499
  ${chalk.bold("Hermes example:")}
494
500
  ${APP_NAME} setup hermes --root /path/to/repo
495
501
  ${APP_NAME} setup hermes --root /path/to/repo --profile my-bot --repo gajae-code --profile-dir /path/to/hermes/profile --install
496
- ${APP_NAME} setup hermes --root /path/to/repo --session-command "gjc --model <provider/model>"
502
+ ${APP_NAME} setup hermes --root /path/to/repo --worktree-name hermes-gajae-code
503
+ ${APP_NAME} setup hermes --root /path/to/repo --session-command "gjc --worktree hermes-custom --model <provider/model>"
497
504
 
498
505
  ${chalk.bold("Options:")}
499
506
  -c, --check Check if dependencies are installed without installing
@@ -511,7 +518,9 @@ ${chalk.bold("Options:")}
511
518
  --root Allowed Hermes MCP workdir/artifact root (repeatable)
512
519
  --profile Hermes MCP profile namespace
513
520
  --repo Hermes MCP repo namespace
514
- --session-command Explicit GJC session command; omitted by default
521
+ --session-command Explicit GJC session command; disables generated worktree flags
522
+ --no-worktree Disable default GJC --worktree isolation for Hermes sessions
523
+ --worktree-name Named GJC --worktree branch for Hermes sessions
515
524
  --mutation Hermes MCP mutation classes: sessions,questions,reports,all
516
525
  --target Hermes config file target for config-only install
517
526
  --profile-dir Hermes profile directory for full setup install
@@ -522,7 +531,7 @@ ${chalk.bold("Examples:")}
522
531
  ${APP_NAME} setup defaults --check Check bundled GJC default workflow skills are installed
523
532
  ${APP_NAME} setup hooks Install native Codex skill-state hooks
524
533
  ${APP_NAME} setup hooks --check Check native Codex skill-state hooks
525
- ${APP_NAME} setup hermes Render a model-agnostic Hermes MCP setup preview
534
+ ${APP_NAME} setup hermes --root /path/to/repo Render a model-agnostic Hermes MCP setup preview
526
535
  ${APP_NAME} setup python Install Python execution dependencies
527
536
  ${APP_NAME} setup stt Install speech-to-text dependencies
528
537
  ${APP_NAME} setup stt --check Check if STT dependencies are available
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) },
@@ -54,7 +43,7 @@ const commands: CommandEntry[] = [
54
43
 
55
44
  async function showHelp(config: CliConfig): Promise<void> {
56
45
  const { renderRootHelp } = await import("@gajae-code/utils/cli");
57
- const { getExtraHelpText } = await import("./cli/args");
46
+ const { getExtraHelpText } = await import("./cli/fast-help");
58
47
  renderRootHelp(config);
59
48
  const extra = getExtraHelpText();
60
49
  if (extra.trim().length > 0) {
@@ -62,6 +51,93 @@ async function showHelp(config: CliConfig): Promise<void> {
62
51
  }
63
52
  }
64
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
+
65
141
  /**
66
142
  * Determine whether argv[0] is a known subcommand name.
67
143
  * If not, the entire argv is treated as args to the default "launch" command.
@@ -109,6 +185,21 @@ export async function runCli(argv: string[]): Promise<void> {
109
185
  await runSmokeTest();
110
186
  return;
111
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();
112
203
  // --help and --version are handled by run() directly, don't rewrite those.
113
204
  // Everything else that isn't a known subcommand routes to "launch".
114
205
  const first = argv[0];
@@ -4,6 +4,7 @@ import {
4
4
  COORDINATOR_MCP_SERVER_NAME,
5
5
  COORDINATOR_MCP_TOOL_NAMES,
6
6
  } from "../coordinator/contract";
7
+ import { buildCoordinatorMcpConfig } from "../coordinator-mcp/policy";
7
8
 
8
9
  function writeJson(value: unknown): void {
9
10
  process.stdout.write(`${JSON.stringify(value, null, 2)}
@@ -24,6 +25,38 @@ function coordinatorContractPayload(): {
24
25
  };
25
26
  }
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
+
27
60
  export default class Coordinator extends Command {
28
61
  static description = "Inspect GJC coordinator MCP bridge contracts";
29
62
  static strict = false;
@@ -39,7 +72,7 @@ export default class Coordinator extends Command {
39
72
  async run(): Promise<void> {
40
73
  const { args, flags } = await this.parse(Coordinator);
41
74
  const action = args.action ?? "check";
42
- if (action !== "check" && action !== "tools") {
75
+ if (action !== "check" && action !== "tools" && action !== "doctor") {
43
76
  const payload = { ok: false, reason: "unknown_coordinator_subcommand", subcommand: action };
44
77
  if (flags.json) writeJson(payload);
45
78
  else
@@ -48,6 +81,16 @@ export default class Coordinator extends Command {
48
81
  process.exit(1);
49
82
  }
50
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
+ }
51
94
  const payload = coordinatorContractPayload();
52
95
  if (flags.json) {
53
96
  writeJson(action === "tools" ? { ok: true, tools: payload.tools } : payload);
@@ -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: {
@@ -11,7 +11,7 @@ function writeJson(value: unknown): void {
11
11
  }
12
12
 
13
13
  export function validateMcpServeSubcommandForTest(server: string | undefined): void {
14
- if (server !== "coordinator") throw new Error(`unknown_mcp_serve_subcommand:${server ?? ""}`);
14
+ if (server !== "coordinator" && server !== "hermes") throw new Error(`unknown_mcp_serve_subcommand:${server ?? ""}`);
15
15
  }
16
16
 
17
17
  export default class McpServe extends Command {
@@ -19,7 +19,7 @@ export default class McpServe extends Command {
19
19
  static strict = false;
20
20
 
21
21
  static args = {
22
- server: Args.string({ description: "MCP server to run (coordinator)", required: false }),
22
+ server: Args.string({ description: "MCP server to run (coordinator or hermes alias)", required: false }),
23
23
  };
24
24
 
25
25
  static flags = {
@@ -39,6 +39,7 @@ export default class McpServe extends Command {
39
39
  } else {
40
40
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
41
41
  }
42
+ process.exitCode = 1;
42
43
  return;
43
44
  }
44
45
 
@@ -28,6 +28,8 @@ export default class Setup extends Command {
28
28
  repo: Flags.string({ description: "Hermes MCP repo namespace" }),
29
29
  profile: Flags.string({ description: "Hermes MCP profile namespace" }),
30
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" }),
31
33
  "state-root": Flags.string({ description: "Hermes MCP coordination state root" }),
32
34
  mutation: Flags.string({
33
35
  description: "Hermes MCP mutation classes: sessions,questions,reports,all",
@@ -68,6 +70,8 @@ export default class Setup extends Command {
68
70
  repo: flags.repo,
69
71
  profile: flags.profile,
70
72
  sessionCommand: flags["session-command"],
73
+ noWorktree: flags["no-worktree"],
74
+ worktreeName: flags["worktree-name"],
71
75
  stateRoot: flags["state-root"],
72
76
  mutation: flags.mutation,
73
77
  artifactByteCap: flags["artifact-byte-cap"],
@@ -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(),
@@ -9,6 +9,7 @@ export const COORDINATOR_MCP_TOOL_NAMES = [
9
9
  "gjc_coordinator_list_artifacts",
10
10
  "gjc_coordinator_read_artifact",
11
11
  "gjc_coordinator_read_coordination_status",
12
+ "gjc_coordinator_register_session",
12
13
  "gjc_coordinator_start_session",
13
14
  "gjc_coordinator_send_prompt",
14
15
  "gjc_coordinator_submit_question_answer",