@gajae-code/coding-agent 0.4.4 → 0.5.0

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 (132) hide show
  1. package/CHANGELOG.md +83 -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 +6 -0
  5. package/dist/types/commands/setup.d.ts +6 -0
  6. package/dist/types/config/model-profile-activation.d.ts +11 -2
  7. package/dist/types/config/model-profiles.d.ts +7 -0
  8. package/dist/types/config/model-registry.d.ts +6 -0
  9. package/dist/types/config/model-resolver.d.ts +2 -0
  10. package/dist/types/config/models-config-schema.d.ts +35 -0
  11. package/dist/types/config/settings-schema.d.ts +4 -3
  12. package/dist/types/coordinator/contract.d.ts +1 -1
  13. package/dist/types/coordinator-mcp/server.d.ts +8 -2
  14. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
  15. package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
  16. package/dist/types/harness-control-plane/finalize.d.ts +5 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +1 -1
  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/receipt-spool.d.ts +19 -0
  21. package/dist/types/harness-control-plane/receipts.d.ts +46 -0
  22. package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
  23. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  24. package/dist/types/harness-control-plane/types.d.ts +13 -1
  25. package/dist/types/hindsight/mental-models.d.ts +5 -5
  26. package/dist/types/main.d.ts +2 -2
  27. package/dist/types/modes/components/model-selector.d.ts +1 -12
  28. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  29. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  30. package/dist/types/modes/utils/abort-message.d.ts +4 -0
  31. package/dist/types/sdk.d.ts +5 -0
  32. package/dist/types/session/agent-session.d.ts +2 -0
  33. package/dist/types/session/blob-store.d.ts +20 -1
  34. package/dist/types/session/session-manager.d.ts +32 -6
  35. package/dist/types/session/streaming-output.d.ts +3 -2
  36. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  37. package/dist/types/setup/hermes-setup.d.ts +7 -0
  38. package/dist/types/task/fork-context-advisory.d.ts +13 -0
  39. package/dist/types/task/receipt.d.ts +2 -0
  40. package/dist/types/task/roi-reconciliation.d.ts +27 -0
  41. package/dist/types/task/types.d.ts +17 -0
  42. package/dist/types/thinking-metadata.d.ts +16 -0
  43. package/dist/types/thinking.d.ts +3 -12
  44. package/dist/types/tools/index.d.ts +2 -0
  45. package/dist/types/tools/resolve.d.ts +0 -10
  46. package/dist/types/utils/tool-choice.d.ts +14 -1
  47. package/package.json +8 -7
  48. package/scripts/build-binary.ts +4 -0
  49. package/src/cli/fast-help.ts +80 -0
  50. package/src/cli/setup-cli.ts +12 -3
  51. package/src/cli.ts +112 -17
  52. package/src/commands/coordinator.ts +44 -1
  53. package/src/commands/harness.ts +128 -11
  54. package/src/commands/launch.ts +2 -2
  55. package/src/commands/mcp-serve.ts +3 -2
  56. package/src/commands/session.ts +3 -1
  57. package/src/commands/setup.ts +4 -0
  58. package/src/config/model-profile-activation.ts +15 -3
  59. package/src/config/model-profiles.ts +255 -56
  60. package/src/config/model-resolver.ts +9 -6
  61. package/src/config/models-config-schema.ts +2 -0
  62. package/src/config/settings-schema.ts +6 -3
  63. package/src/coordinator/contract.ts +1 -0
  64. package/src/coordinator-mcp/server.ts +427 -193
  65. package/src/cursor.ts +46 -4
  66. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  67. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  68. package/src/export/html/index.ts +13 -9
  69. package/src/gjc-runtime/launch-worktree.ts +12 -1
  70. package/src/gjc-runtime/session-state-sidecar.ts +38 -0
  71. package/src/gjc-runtime/team-runtime.ts +33 -7
  72. package/src/gjc-runtime/tmux-common.ts +15 -0
  73. package/src/gjc-runtime/tmux-sessions.ts +19 -11
  74. package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
  75. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  76. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  77. package/src/harness-control-plane/finalize.ts +39 -5
  78. package/src/harness-control-plane/owner.ts +87 -28
  79. package/src/harness-control-plane/phase-rollup.ts +96 -0
  80. package/src/harness-control-plane/receipt-ingest.ts +127 -0
  81. package/src/harness-control-plane/receipt-spool.ts +128 -0
  82. package/src/harness-control-plane/receipts.ts +229 -1
  83. package/src/harness-control-plane/rpc-adapter.ts +8 -0
  84. package/src/harness-control-plane/state-machine.ts +27 -6
  85. package/src/harness-control-plane/storage.ts +23 -0
  86. package/src/harness-control-plane/types.ts +33 -1
  87. package/src/hindsight/mental-models.ts +17 -16
  88. package/src/internal-urls/docs-index.generated.ts +8 -7
  89. package/src/main.ts +7 -3
  90. package/src/modes/components/assistant-message.ts +26 -14
  91. package/src/modes/components/diff.ts +97 -0
  92. package/src/modes/components/model-selector.ts +353 -181
  93. package/src/modes/components/status-line.ts +6 -6
  94. package/src/modes/components/tool-execution.ts +30 -13
  95. package/src/modes/controllers/event-controller.ts +5 -4
  96. package/src/modes/controllers/selector-controller.ts +33 -42
  97. package/src/modes/interactive-mode.ts +4 -5
  98. package/src/modes/print-mode.ts +1 -1
  99. package/src/modes/rpc/rpc-client.ts +3 -2
  100. package/src/modes/rpc/rpc-mode.ts +44 -14
  101. package/src/modes/rpc/rpc-types.ts +5 -2
  102. package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
  103. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  104. package/src/modes/theme/theme.ts +2 -2
  105. package/src/modes/utils/abort-message.ts +41 -0
  106. package/src/modes/utils/context-usage.ts +15 -8
  107. package/src/modes/utils/ui-helpers.ts +5 -6
  108. package/src/sdk.ts +38 -6
  109. package/src/secrets/obfuscator.ts +102 -27
  110. package/src/session/agent-session.ts +121 -25
  111. package/src/session/blob-store.ts +89 -3
  112. package/src/session/session-manager.ts +328 -57
  113. package/src/session/streaming-output.ts +185 -122
  114. package/src/session/tool-choice-queue.ts +23 -0
  115. package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
  116. package/src/setup/hermes-setup.ts +63 -8
  117. package/src/task/executor.ts +69 -6
  118. package/src/task/fork-context-advisory.ts +99 -0
  119. package/src/task/index.ts +31 -2
  120. package/src/task/receipt.ts +7 -0
  121. package/src/task/render.ts +21 -1
  122. package/src/task/roi-reconciliation.ts +90 -0
  123. package/src/task/types.ts +15 -0
  124. package/src/thinking-metadata.ts +51 -0
  125. package/src/thinking.ts +26 -46
  126. package/src/tools/bash.ts +1 -1
  127. package/src/tools/index.ts +4 -2
  128. package/src/tools/resolve.ts +93 -18
  129. package/src/tools/subagent-render.ts +10 -1
  130. package/src/utils/edit-mode.ts +1 -1
  131. package/src/utils/title-generator.ts +16 -2
  132. package/src/utils/tool-choice.ts +45 -16
@@ -0,0 +1,80 @@
1
+ import { APP_NAME, CONFIG_DIR_NAME } from "@gajae-code/utils/dirs";
2
+
3
+ export function getExtraHelpText(): string {
4
+ return `Environment Variables:
5
+ # Core Providers
6
+ ANTHROPIC_API_KEY - Anthropic Claude models
7
+ ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth (takes precedence over API key)
8
+ CLAUDE_CODE_USE_FOUNDRY - Enable Anthropic Foundry mode (uses Foundry endpoint + mTLS)
9
+ FOUNDRY_BASE_URL - Anthropic Foundry base URL (e.g., https://<foundry-host>)
10
+ ANTHROPIC_FOUNDRY_API_KEY - Anthropic token used as Authorization: Bearer <token> in Foundry mode
11
+ ANTHROPIC_CUSTOM_HEADERS - Extra Foundry headers (e.g., "user-id: USERNAME")
12
+ CLAUDE_CODE_CLIENT_CERT - Client certificate (PEM path or inline PEM) for mTLS
13
+ CLAUDE_CODE_CLIENT_KEY - Client private key (PEM path or inline PEM) for mTLS
14
+ NODE_EXTRA_CA_CERTS - CA bundle path (or inline PEM) for server certificate validation
15
+ OPENAI_API_KEY - OpenAI GPT models
16
+ GEMINI_API_KEY - Google Gemini models
17
+ GITHUB_TOKEN - GitHub Copilot (or GH_TOKEN, COPILOT_GITHUB_TOKEN)
18
+
19
+ # Additional LLM Providers
20
+ AZURE_OPENAI_API_KEY - Azure OpenAI models
21
+ GROQ_API_KEY - Groq models
22
+ CEREBRAS_API_KEY - Cerebras models
23
+ XAI_API_KEY - xAI Grok models
24
+ OPENROUTER_API_KEY - OpenRouter aggregated models
25
+ KILO_API_KEY - Kilo Gateway models
26
+ MISTRAL_API_KEY - Mistral models
27
+ ZAI_API_KEY - z.ai models (ZhipuAI/GLM)
28
+ MINIMAX_API_KEY - MiniMax models
29
+ OPENCODE_API_KEY - OpenCode Zen/OpenCode Go models
30
+ CURSOR_ACCESS_TOKEN - Cursor AI models
31
+ AI_GATEWAY_API_KEY - Vercel AI Gateway
32
+
33
+ # Cloud Providers
34
+ AWS_PROFILE - AWS Bedrock (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY)
35
+ GOOGLE_CLOUD_PROJECT - Google Vertex AI (requires GOOGLE_CLOUD_LOCATION)
36
+ GOOGLE_APPLICATION_CREDENTIALS - Service account for Vertex AI
37
+
38
+ # Search & Tools
39
+ EXA_API_KEY - Exa web search
40
+ BRAVE_API_KEY - Brave web search
41
+ PERPLEXITY_API_KEY - Perplexity web search (API)
42
+ PERPLEXITY_COOKIES - Perplexity web search (session cookie)
43
+ TAVILY_API_KEY - Tavily web search
44
+ ANTHROPIC_SEARCH_API_KEY - Anthropic search provider
45
+
46
+ # Configuration
47
+ GJC_CODING_AGENT_DIR - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
48
+ GJC_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)
49
+ GJC_SMOL_MODEL - Override smol/fast model (see --smol)
50
+ GJC_SLOW_MODEL - Override slow/reasoning model (see --slow)
51
+ GJC_PLAN_MODEL - Override planning model (see --plan)
52
+ GJC_NO_PTY - Disable PTY-based interactive bash execution
53
+ --tmux - Launch interactive startup inside a new tmux session
54
+ gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions
55
+ GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
56
+ GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
57
+
58
+ For complete environment variable reference, see:
59
+ docs/environment-variables.md
60
+ Available Tools (default-enabled unless noted):
61
+ read - Read file contents
62
+ bash - Execute bash commands
63
+ edit - Edit files with find/replace
64
+ write - Write files (creates/overwrites)
65
+ grep - Search file contents
66
+ find - Find files by glob pattern
67
+ lsp - Language server protocol (code intelligence)
68
+ python - Execute Python code (requires: ${APP_NAME} setup python)
69
+ notebook - Edit Jupyter notebooks
70
+ inspect_image - Analyze images with a vision model
71
+ browser - Browser automation (Puppeteer)
72
+ task - Launch sub-agents for parallel tasks
73
+ todo_write - Manage todo/task lists
74
+ web_search - Search the web
75
+ ask - Ask user questions (interactive mode only)
76
+
77
+ Useful Commands:
78
+ ${APP_NAME} --list-models - List configured provider models
79
+ ${APP_NAME} --help - Show this help`;
80
+ }
@@ -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,33 +1,26 @@
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, formatBunRuntimeError, 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(
25
- `error: Bun runtime must be >= ${MIN_BUN_VERSION} (found v${Bun.version}). Please upgrade: bun upgrade\n`,
12
+ formatBunRuntimeError({
13
+ currentVersion: Bun.version,
14
+ minVersion: MIN_BUN_VERSION,
15
+ execPath: process.execPath,
16
+ }),
26
17
  );
27
18
  process.exit(1);
28
19
  }
29
20
 
30
21
  process.title = APP_NAME;
22
+ const rootHelpFlags = ["--help", "-h", "help"];
23
+ const versionFlags = ["--version", "-v"];
31
24
 
32
25
  const commands: CommandEntry[] = [
33
26
  { name: "codex-native-hook", load: () => import("./commands/codex-native-hook").then(m => m.default) },
@@ -54,7 +47,7 @@ const commands: CommandEntry[] = [
54
47
 
55
48
  async function showHelp(config: CliConfig): Promise<void> {
56
49
  const { renderRootHelp } = await import("@gajae-code/utils/cli");
57
- const { getExtraHelpText } = await import("./cli/args");
50
+ const { getExtraHelpText } = await import("./cli/fast-help");
58
51
  renderRootHelp(config);
59
52
  const extra = getExtraHelpText();
60
53
  if (extra.trim().length > 0) {
@@ -62,6 +55,93 @@ async function showHelp(config: CliConfig): Promise<void> {
62
55
  }
63
56
  }
64
57
 
58
+ async function installRuntimeGlobals(): Promise<void> {
59
+ const [{ installH2Fetch }, { procmgr }] = await Promise.all([import("@gajae-code/ai"), import("@gajae-code/utils")]);
60
+ // Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
61
+ // discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
62
+ // can't toggle from JS, so we patch globalThis.fetch to pass
63
+ // `protocol: "http2"` per request, with transparent HTTP/1.1 fallback on
64
+ // `HTTP2Unsupported`. See @gajae-code/ai/utils/h2-fetch for details.
65
+ installH2Fetch();
66
+
67
+ // Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
68
+ // Otherwise every child bun process (subagents, plugin installs, ptree spawns,
69
+ // etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
70
+ procmgr.scrubProcessEnv();
71
+ }
72
+
73
+ class RootHelpCommand extends Command {
74
+ static description = "Red-claw AI coding assistant";
75
+ static hidden = true;
76
+ static args = {
77
+ messages: Args.string({
78
+ description: "Messages to send (prefix files with @)",
79
+ required: false,
80
+ multiple: true,
81
+ }),
82
+ };
83
+ static flags = {
84
+ model: Flags.string({ description: 'Model to use (fuzzy match: "opus", "gpt-5.2", or "openai/gpt-5.2")' }),
85
+ smol: Flags.string({ description: "Smol/fast model for lightweight tasks (or GJC_SMOL_MODEL env)" }),
86
+ slow: Flags.string({ description: "Slow/reasoning model for thorough analysis (or GJC_SLOW_MODEL env)" }),
87
+ plan: Flags.string({ description: "Plan model for architectural planning (or GJC_PLAN_MODEL env)" }),
88
+ mpreset: Flags.string({ description: "Model profile preset to activate for this session" }),
89
+ default: Flags.boolean({ description: "Persist --mpreset as the default model profile" }),
90
+ provider: Flags.string({ description: "Provider to use (legacy; prefer --model)" }),
91
+ "api-key": Flags.string({ description: "API key (defaults to env vars)" }),
92
+ "system-prompt": Flags.string({ description: "System prompt (default: coding assistant prompt)" }),
93
+ "append-system-prompt": Flags.string({ description: "Append text or file contents to the system prompt" }),
94
+ "allow-home": Flags.boolean({ description: "Allow starting in ~ without auto-switching to a temp dir" }),
95
+ mode: Flags.string({
96
+ description: "Output mode: text (default), json, rpc, acp, rpc-ui, or bridge",
97
+ options: ["text", "json", "rpc", "acp", "rpc-ui", "bridge"],
98
+ }),
99
+ print: Flags.boolean({ char: "p", description: "Non-interactive mode: process prompt and exit" }),
100
+ continue: Flags.boolean({ char: "c", description: "Continue previous session" }),
101
+ resume: Flags.string({ char: "r", description: "Resume a session (by ID prefix, path, or picker if omitted)" }),
102
+ "session-dir": Flags.string({ description: "Directory for session storage and lookup" }),
103
+ "no-session": Flags.boolean({ description: "Don't save session (ephemeral)" }),
104
+ models: Flags.string({ description: "Comma-separated model patterns for Ctrl+P cycling" }),
105
+ "no-tools": Flags.boolean({ description: "Disable all built-in tools" }),
106
+ "no-lsp": Flags.boolean({ description: "Disable LSP tools, formatting, and diagnostics" }),
107
+ "no-pty": Flags.boolean({ description: "Disable PTY-based interactive bash execution" }),
108
+ tmux: Flags.boolean({ description: "Launch interactive startup inside tmux" }),
109
+ tools: Flags.string({ description: "Comma-separated list of tools to enable (default: all)" }),
110
+ thinking: Flags.string({
111
+ description: "Set thinking level: ultra, high, medium, low",
112
+ options: ["ultra", "high", "medium", "low"],
113
+ }),
114
+ hook: Flags.string({ description: "Load a hook/extension file (can be used multiple times)", multiple: true }),
115
+ extension: Flags.string({
116
+ char: "e",
117
+ description: "Load an extension file (can be used multiple times)",
118
+ multiple: true,
119
+ }),
120
+ "no-extensions": Flags.boolean({ description: "Disable extension discovery (explicit -e paths still work)" }),
121
+ "no-skills": Flags.boolean({ description: "Disable skills discovery and loading" }),
122
+ skills: Flags.string({ description: "Comma-separated glob patterns to filter skills (e.g., git-*,docker)" }),
123
+ "no-rules": Flags.boolean({ description: "Disable rules discovery and loading" }),
124
+ export: Flags.string({ description: "Export session file to HTML and exit" }),
125
+ "list-models": Flags.string({ description: "List available models (with optional fuzzy search)" }),
126
+ "no-title": Flags.boolean({ description: "Disable title auto-generation" }),
127
+ };
128
+ static examples = [
129
+ `# Interactive mode\n ${APP_NAME}`,
130
+ `# Interactive mode with initial prompt\n ${APP_NAME} "List all .ts files in src/"`,
131
+ `# Include files in initial message\n ${APP_NAME} @prompt.md @image.png "What color is the sky?"`,
132
+ `# Non-interactive mode (process and exit)\n ${APP_NAME} -p "List all .ts files in src/"`,
133
+ `# Continue previous session\n ${APP_NAME} --continue "What did we discuss?"`,
134
+ `# Launch in a sibling git worktree\n ${APP_NAME} --worktree`,
135
+ `# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`,
136
+ `# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`,
137
+ `# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-medium`,
138
+ `# Persist a model profile as the default\n ${APP_NAME} --mpreset opencodego --default`,
139
+ `# Export a session file to HTML\n ${APP_NAME} --export ~/.gjc/agent/sessions/--path--/session.jsonl`,
140
+ ];
141
+ static strict = false;
142
+ async run(): Promise<void> {}
143
+ }
144
+
65
145
  /**
66
146
  * Determine whether argv[0] is a known subcommand name.
67
147
  * If not, the entire argv is treated as args to the default "launch" command.
@@ -109,6 +189,21 @@ export async function runCli(argv: string[]): Promise<void> {
109
189
  await runSmokeTest();
110
190
  return;
111
191
  }
192
+ if (rootHelpFlags.includes(argv[0] ?? "")) {
193
+ const { renderRootHelp } = await import("@gajae-code/utils/cli");
194
+ const { getExtraHelpText } = await import("./cli/fast-help");
195
+ renderRootHelp({ bin: APP_NAME, version: VERSION, commands: new Map([["launch", RootHelpCommand]]) });
196
+ const extra = getExtraHelpText();
197
+ if (extra.trim().length > 0) {
198
+ process.stdout.write(`\n${extra}\n`);
199
+ }
200
+ return;
201
+ }
202
+ if (versionFlags.includes(argv[0] ?? "")) {
203
+ process.stdout.write(`${APP_NAME}/${VERSION}\n`);
204
+ return;
205
+ }
206
+ await installRuntimeGlobals();
112
207
  // --help and --version are handled by run() directly, don't rewrite those.
113
208
  // Everything else that isn't a known subcommand routes to "launch".
114
209
  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,17 +10,19 @@
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
+ import * as path from "node:path";
14
15
  import { Args, Command, Flags } from "@gajae-code/utils/cli";
15
16
  import { resolveGjcTmuxCommand, sanitizeTmuxToken } from "../gjc-runtime/tmux-common";
16
17
  import { classifyRecovery } from "../harness-control-plane/classifier";
17
18
  import { callEndpoint, EndpointUnreachableError } from "../harness-control-plane/control-endpoint";
18
19
  import { type ResolvedOwner, RuntimeOwner, resolveOwner } from "../harness-control-plane/owner";
19
20
  import { preserveDirtyWorktree } from "../harness-control-plane/preserve";
21
+ import { RECEIPT_SPOOL_DIR_ENV } from "../harness-control-plane/receipt-spool";
20
22
  import { buildReceipt, requiresVanishBeforeAction, type VanishEvidence } from "../harness-control-plane/receipts";
21
23
  import { GajaeCodeRpc } from "../harness-control-plane/rpc-adapter";
22
24
  import { classifyLeaseStatus, readLease } from "../harness-control-plane/session-lease";
23
- import { buildResponse, buildStateView } from "../harness-control-plane/state-machine";
25
+ import { buildResponse, buildStateView, submitUnavailableReason } from "../harness-control-plane/state-machine";
24
26
  import {
25
27
  canonicalWorkspacePath,
26
28
  generateSessionId,
@@ -247,6 +249,30 @@ interface OwnerExitEvidence {
247
249
  transient: boolean;
248
250
  /** ISO timestamp of the most recent non-terminal RPC-derived owner event, if any (observability only). */
249
251
  lastRpcActivityAt: string | null;
252
+ /**
253
+ * True when the owner started (reported live) but died before accepting the first prompt.
254
+ * This is a startup blocker, not a healthy live gate: callers must recover before submit.
255
+ */
256
+ startupBlocker: boolean;
257
+ /** Explicit, human-actionable recovery guidance for the surfaced exit reason. */
258
+ recoveryGuidance: string;
259
+ }
260
+
261
+ function ownerExitGuidance(reason: string, startupBlocker: boolean): string {
262
+ if (startupBlocker) {
263
+ 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";
264
+ }
265
+ switch (reason) {
266
+ case "owner-exited-after-prompt-acceptance":
267
+ return "owner exited after accepting a prompt; run `gjc harness recover --session <id>` to preserve in-flight work and classify the vanish before resubmitting";
268
+ case "owner-lease-expired":
269
+ case "owner-endpoint-unreachable":
270
+ return "owner lease is stale or its endpoint did not route; run `gjc harness recover --session <id>` to respawn or take over the owner";
271
+ case "owner-liveness-unknown-permission-denied":
272
+ return "owner liveness cannot be probed (permission denied); verify the owner process out-of-band before recover";
273
+ default:
274
+ return "no live owner holds this session; run `gjc harness recover --session <id>` to (re)spawn an owner, then resubmit";
275
+ }
250
276
  }
251
277
 
252
278
  async function buildOwnerExitEvidence(root: string, state: SessionState): Promise<OwnerExitEvidence> {
@@ -286,6 +312,12 @@ async function buildOwnerExitEvidence(root: string, state: SessionState): Promis
286
312
  } else {
287
313
  reason = "owner-endpoint-unreachable";
288
314
  }
315
+ // A just-started owner that emitted `owner_started` (so it reported live) but is now terminal
316
+ // without ever accepting a prompt died during startup. Surface this as an explicit, actionable
317
+ // startup blocker rather than letting `submit` fall through to a misleading `owner-not-live` gate.
318
+ const ownerStarted = events.some(event => event.kind === "owner_started");
319
+ const startupBlocker = terminal && ownerStarted && !promptAcceptedSeen && !completedSeen;
320
+ if (startupBlocker) reason = "owner-died-before-first-prompt";
289
321
  return {
290
322
  reason,
291
323
  leaseStatus,
@@ -301,6 +333,8 @@ async function buildOwnerExitEvidence(root: string, state: SessionState): Promis
301
333
  terminal,
302
334
  transient,
303
335
  lastRpcActivityAt,
336
+ startupBlocker,
337
+ recoveryGuidance: ownerExitGuidance(reason, startupBlocker),
304
338
  };
305
339
  }
306
340
 
@@ -404,6 +438,29 @@ async function markVanishedOwnerBlocked(
404
438
  return state;
405
439
  }
406
440
 
441
+ const OWNER_STARTUP_BLOCKER = "owner-died-before-first-prompt";
442
+
443
+ /**
444
+ * Persist an explicit startup blocker when an owner started, reported live, but died before
445
+ * accepting the first prompt. This makes the failure an actionable lifecycle state instead of a
446
+ * silent `owner-not-live` gate, so observe/recover surface it and recover can respawn the owner.
447
+ */
448
+ async function markStartupOwnerBlocked(
449
+ root: string,
450
+ state: SessionState,
451
+ ownerExit: OwnerExitEvidence,
452
+ ): Promise<SessionState> {
453
+ if (!ownerExit.startupBlocker) return state;
454
+ if (state.lifecycle === "completed" || state.lifecycle === "retired") return state;
455
+ state.lifecycle = "blocked";
456
+ state.blockers = state.blockers.includes(OWNER_STARTUP_BLOCKER)
457
+ ? state.blockers
458
+ : [...state.blockers, OWNER_STARTUP_BLOCKER];
459
+ state.updatedAt = nowIso();
460
+ await writeSessionState(root, state);
461
+ return state;
462
+ }
463
+
407
464
  function resolveRetryBudget(input: Record<string, unknown>): RetryBudget {
408
465
  const supplied = input.retryBudget;
409
466
  if (supplied && typeof supplied === "object" && !Array.isArray(supplied)) {
@@ -453,10 +510,14 @@ export default class Harness extends Command {
453
510
 
454
511
  static flags = {
455
512
  input: Flags.string({ description: "JSON object input for the verb", default: "" }),
513
+ "prompt-file": Flags.string({ description: "Read submit prompt text from a file (submit verb only)" }),
456
514
  session: Flags.string({ char: "s", description: "Session id (re-grab a session)" }),
457
515
  cursor: Flags.string({ description: "Event cursor for events --follow (exclusive)", default: "0" }),
458
516
  follow: Flags.boolean({ description: "Tail the owner-written event log", default: false }),
459
517
  json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: true }),
518
+ "receipt-spool-dir": Flags.string({
519
+ description: "Append persisted ReceiptEnvelope records to spool.jsonl under this directory",
520
+ }),
460
521
  };
461
522
 
462
523
  static examples = [
@@ -471,7 +532,20 @@ export default class Harness extends Command {
471
532
  const verb = String(args.verb);
472
533
  let root = resolveHarnessRoot();
473
534
  try {
535
+ const receiptSpoolDir = flags["receipt-spool-dir"];
536
+ if (receiptSpoolDir !== undefined) {
537
+ if (!receiptSpoolDir.trim()) throw new Error("receipt_spool_dir_empty");
538
+ process.env[RECEIPT_SPOOL_DIR_ENV] = path.resolve(receiptSpoolDir.trim());
539
+ }
474
540
  const input = parseInput(flags.input);
541
+ const promptFile = flags["prompt-file"];
542
+ if (promptFile !== undefined) {
543
+ if (verb !== "submit") throw new Error("prompt_file_only_supported_for_submit");
544
+ if (typeof input.prompt === "string" && input.prompt.length > 0) {
545
+ throw new Error("prompt_file_conflicts_with_input_prompt");
546
+ }
547
+ input.prompt = readFileSync(promptFile, "utf8");
548
+ }
475
549
  const sessionId = flags.session ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
476
550
  const expectedWorkspace = typeof input.workspace === "string" ? resolveInputWorkspace(input) : undefined;
477
551
  if (verb !== "start" && sessionId) {
@@ -612,6 +686,9 @@ export default class Harness extends Command {
612
686
  }
613
687
  const sessionName = deterministicHarnessTmuxSessionName(sessionId);
614
688
  const envAssignments = [`GJC_HARNESS_STATE_ROOT=${shellQuote(root)}`];
689
+ if (process.env[RECEIPT_SPOOL_DIR_ENV]) {
690
+ envAssignments.push(`${RECEIPT_SPOOL_DIR_ENV}=${shellQuote(process.env[RECEIPT_SPOOL_DIR_ENV])}`);
691
+ }
615
692
  if (process.env.GJC_HARNESS_RPC_COMMAND) {
616
693
  envAssignments.push(`GJC_HARNESS_RPC_COMMAND=${shellQuote(process.env.GJC_HARNESS_RPC_COMMAND)}`);
617
694
  }
@@ -651,6 +728,9 @@ export default class Harness extends Command {
651
728
  env: {
652
729
  ...process.env,
653
730
  GJC_HARNESS_STATE_ROOT: root,
731
+ ...(process.env[RECEIPT_SPOOL_DIR_ENV]
732
+ ? { [RECEIPT_SPOOL_DIR_ENV]: process.env[RECEIPT_SPOOL_DIR_ENV] }
733
+ : {}),
654
734
  ...(process.env.GJC_HARNESS_TEST_NODE_MODULES
655
735
  ? { GJC_HARNESS_TEST_NODE_MODULES: process.env.GJC_HARNESS_TEST_NODE_MODULES }
656
736
  : {}),
@@ -814,7 +894,9 @@ export default class Harness extends Command {
814
894
  ): Promise<boolean> {
815
895
  const owner = await resolveOwner(root, sessionId);
816
896
  if (!owner.live || !owner.socketPath) return false;
897
+ const priorSpoolDir = input[RECEIPT_SPOOL_DIR_ENV];
817
898
  try {
899
+ if (process.env[RECEIPT_SPOOL_DIR_ENV]) input[RECEIPT_SPOOL_DIR_ENV] = process.env[RECEIPT_SPOOL_DIR_ENV];
818
900
  const res = (await callEndpoint(owner.socketPath, { verb, input })) as { ok?: boolean };
819
901
  writeJson(res);
820
902
  if (res?.ok === false) process.exitCode = 1;
@@ -822,6 +904,9 @@ export default class Harness extends Command {
822
904
  } catch (error) {
823
905
  if (error instanceof EndpointUnreachableError) return false;
824
906
  throw error;
907
+ } finally {
908
+ if (priorSpoolDir === undefined) delete input[RECEIPT_SPOOL_DIR_ENV];
909
+ else input[RECEIPT_SPOOL_DIR_ENV] = priorSpoolDir;
825
910
  }
826
911
  }
827
912
 
@@ -834,10 +919,12 @@ export default class Harness extends Command {
834
919
  state = await reconcileCompletedOwnerExited(root, state, observation, completedTerminalEvent);
835
920
  const vanishedOwnerBlock = needsVanishedOwnerBlock(state, observation, completedTerminalEvent);
836
921
  state = await markVanishedOwnerBlocked(root, state, observation, completedTerminalEvent);
837
- const ownerExit =
838
- !ownerLive && (vanishedOwnerBlock || completedTerminalEvent)
839
- ? await buildOwnerExitEvidence(root, state)
840
- : null;
922
+ // Build owner-exit evidence whenever the owner is gone so a startup death (owner started,
923
+ // reported live, then died before the first prompt) is detectable, not just vanish/completion.
924
+ const ownerExit = !ownerLive ? await buildOwnerExitEvidence(root, state) : null;
925
+ const startupBlocked = ownerExit?.startupBlocker ?? false;
926
+ if (ownerExit && startupBlocked) state = await markStartupOwnerBlocked(root, state, ownerExit);
927
+ const includeOwnerExit = Boolean(ownerExit && (vanishedOwnerBlock || completedTerminalEvent || startupBlocked));
841
928
  writeJson(
842
929
  buildResponse(state, ownerLive, {
843
930
  observation: { ...observation, lifecycle: state.lifecycle },
@@ -848,7 +935,10 @@ export default class Harness extends Command {
848
935
  ...(completedTerminalEvent && !ownerLive
849
936
  ? { completedOwnerExited: true, terminalResult: completedTerminalEvent }
850
937
  : {}),
851
- ...(ownerExit ? { ownerExit } : {}),
938
+ ...(startupBlocked
939
+ ? { startupBlocked: true, blockerReason: OWNER_STARTUP_BLOCKER, guidance: ownerExit?.recoveryGuidance }
940
+ : {}),
941
+ ...(includeOwnerExit && ownerExit ? { ownerExit } : {}),
852
942
  }),
853
943
  );
854
944
  }
@@ -909,10 +999,36 @@ export default class Harness extends Command {
909
999
 
910
1000
  async #submit(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
911
1001
  const sessionId = requireSessionId(input, flagSession);
912
- 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));
1002
+ let state = await loadState(root, sessionId);
1003
+ const noOwnerGate = submitUnavailableReason(state.lifecycle, false);
1004
+ if (!noOwnerGate || noOwnerGate === "owner-not-live") {
1005
+ if (await this.#tryOwnerRoute(root, sessionId, "submit", { ...input, sessionId })) return;
1006
+ state = await loadState(root, sessionId);
1007
+ }
1008
+ const blockedByOwnerLiveness = state.blockers.some(
1009
+ blocker => isOwnerLivenessBlocker(blocker) || blocker === OWNER_STARTUP_BLOCKER,
1010
+ );
1011
+ const lifecycleGate = submitUnavailableReason(state.lifecycle, false);
1012
+ if (lifecycleGate && lifecycleGate !== "owner-not-live" && !blockedByOwnerLiveness) {
1013
+ writeJson(buildResponse(state, false, { accepted: false, submitted: false, reason: lifecycleGate }, false));
1014
+ process.exitCode = 1;
1015
+ return;
1016
+ }
1017
+ // No live owner: submission is blocked (never echoed-as-accepted). Surface owner exit
1018
+ // evidence + explicit recovery guidance so the caller is not left with a bare gate.
1019
+ const ownerExit = await buildOwnerExitEvidence(root, state);
1020
+ // An owner that started, reported live, then died before accepting the first prompt is a
1021
+ // startup blocker, not a healthy `owner-not-live` gate — persist it and report it as such.
1022
+ if (ownerExit.startupBlocker) state = await markStartupOwnerBlocked(root, state, ownerExit);
1023
+ const reason = ownerExit.startupBlocker ? ownerExit.reason : "owner-not-live";
1024
+ writeJson(
1025
+ buildResponse(
1026
+ state,
1027
+ false,
1028
+ { accepted: false, submitted: false, reason, ownerExit, guidance: ownerExit.recoveryGuidance },
1029
+ false,
1030
+ ),
1031
+ );
916
1032
  process.exitCode = 1;
917
1033
  }
918
1034
 
@@ -1030,6 +1146,7 @@ export default class Harness extends Command {
1030
1146
  decision,
1031
1147
  observation: { ...observation, lifecycle: state.lifecycle },
1032
1148
  ownerExit: afterExit,
1149
+ guidance: afterExit.recoveryGuidance,
1033
1150
  ...(restoredOwner
1034
1151
  ? {
1035
1152
  restoreAttempt: {
@@ -142,8 +142,8 @@ export default class Index extends Command {
142
142
  `# Launch in a sibling git worktree\n ${APP_NAME} --worktree`,
143
143
  `# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`,
144
144
  `# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`,
145
- `# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-standard`,
146
- `# Persist a model profile as the default\n ${APP_NAME} --mpreset opencode-go-pro --default`,
145
+ `# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-medium`,
146
+ `# Persist a model profile as the default\n ${APP_NAME} --mpreset opencodego --default`,
147
147
  `# Export a session file to HTML\n ${APP_NAME} --export ~/.gjc/agent/sessions/--path--/session.jsonl`,
148
148
  ];
149
149
 
@@ -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