@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.
- package/CHANGELOG.md +83 -0
- package/dist/types/cli/fast-help.d.ts +1 -0
- package/dist/types/cli/setup-cli.d.ts +2 -0
- package/dist/types/commands/harness.d.ts +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +35 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +8 -2
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/harness-control-plane/finalize.d.ts +5 -0
- package/dist/types/harness-control-plane/owner.d.ts +1 -1
- package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +46 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/types.d.ts +13 -1
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/utils/abort-message.d.ts +4 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/blob-store.d.ts +20 -1
- package/dist/types/session/session-manager.d.ts +32 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/setup/hermes-setup.d.ts +7 -0
- package/dist/types/task/fork-context-advisory.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +17 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +8 -7
- package/scripts/build-binary.ts +4 -0
- package/src/cli/fast-help.ts +80 -0
- package/src/cli/setup-cli.ts +12 -3
- package/src/cli.ts +112 -17
- package/src/commands/coordinator.ts +44 -1
- package/src/commands/harness.ts +128 -11
- package/src/commands/launch.ts +2 -2
- package/src/commands/mcp-serve.ts +3 -2
- package/src/commands/session.ts +3 -1
- package/src/commands/setup.ts +4 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +255 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +427 -193
- package/src/cursor.ts +46 -4
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/export/html/index.ts +13 -9
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +38 -0
- package/src/gjc-runtime/team-runtime.ts +33 -7
- package/src/gjc-runtime/tmux-common.ts +15 -0
- package/src/gjc-runtime/tmux-sessions.ts +19 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/finalize.ts +39 -5
- package/src/harness-control-plane/owner.ts +87 -28
- package/src/harness-control-plane/phase-rollup.ts +96 -0
- package/src/harness-control-plane/receipt-ingest.ts +127 -0
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/receipts.ts +229 -1
- package/src/harness-control-plane/rpc-adapter.ts +8 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +23 -0
- package/src/harness-control-plane/types.ts +33 -1
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/main.ts +7 -3
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/model-selector.ts +353 -181
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/event-controller.ts +5 -4
- package/src/modes/controllers/selector-controller.ts +33 -42
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +44 -14
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/utils/abort-message.ts +41 -0
- package/src/modes/utils/context-usage.ts +15 -8
- package/src/modes/utils/ui-helpers.ts +5 -6
- package/src/sdk.ts +38 -6
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +121 -25
- package/src/session/blob-store.ts +89 -3
- package/src/session/session-manager.ts +328 -57
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
- package/src/setup/hermes-setup.ts +63 -8
- package/src/task/executor.ts +69 -6
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +31 -2
- package/src/task/receipt.ts +7 -0
- package/src/task/render.ts +21 -1
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +15 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +4 -2
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +10 -1
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/title-generator.ts +16 -2
- 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
|
+
}
|
package/src/cli/setup-cli.ts
CHANGED
|
@@ -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 --
|
|
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;
|
|
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
|
|
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
|
-
|
|
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/
|
|
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);
|
package/src/commands/harness.ts
CHANGED
|
@@ -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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
913
|
-
const
|
|
914
|
-
|
|
915
|
-
|
|
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: {
|
package/src/commands/launch.ts
CHANGED
|
@@ -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-
|
|
146
|
-
`# Persist a model profile as the default\n ${APP_NAME} --mpreset
|
|
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
|
|