@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.
- package/CHANGELOG.md +40 -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 +3 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +8 -2
- package/dist/types/harness-control-plane/finalize.d.ts +5 -0
- 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/receipts.d.ts +46 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/types.d.ts +9 -1
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/utils/abort-message.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +8 -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 +1 -0
- package/dist/types/task/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +10 -0
- 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 +107 -16
- package/src/commands/coordinator.ts +44 -1
- package/src/commands/harness.ts +92 -9
- package/src/commands/mcp-serve.ts +3 -2
- package/src/commands/setup.ts +4 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +385 -182
- package/src/cursor.ts +30 -2
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +38 -0
- package/src/harness-control-plane/finalize.ts +39 -5
- package/src/harness-control-plane/owner.ts +9 -1
- 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/receipts.ts +229 -1
- package/src/harness-control-plane/rpc-adapter.ts +8 -0
- package/src/harness-control-plane/types.ts +29 -1
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/main.ts +7 -3
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/controllers/event-controller.ts +5 -4
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/print-mode.ts +1 -1
- 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 +9 -4
- package/src/session/agent-session.ts +16 -5
- package/src/session/session-manager.ts +20 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
- package/src/setup/hermes-setup.ts +63 -8
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +31 -2
- package/src/task/receipt.ts +2 -0
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +7 -0
- package/src/tools/index.ts +2 -2
- package/src/tools/subagent-render.ts +10 -1
- package/src/utils/title-generator.ts +16 -2
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,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/
|
|
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);
|
package/src/commands/harness.ts
CHANGED
|
@@ -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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
914
|
-
// No live owner: submission is blocked (never echoed-as-accepted).
|
|
915
|
-
|
|
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
|
|
package/src/commands/setup.ts
CHANGED
|
@@ -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",
|