@claudexor/cli 1.0.1
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/LICENSE +21 -0
- package/README.md +22 -0
- package/dist/args.d.ts +12 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +88 -0
- package/dist/args.js.map +1 -0
- package/dist/attachment-resolver.d.ts +11 -0
- package/dist/attachment-resolver.d.ts.map +1 -0
- package/dist/attachment-resolver.js +61 -0
- package/dist/attachment-resolver.js.map +1 -0
- package/dist/capabilities.d.ts +13 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +90 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/claudexord.d.ts +3 -0
- package/dist/claudexord.d.ts.map +1 -0
- package/dist/claudexord.js +564 -0
- package/dist/claudexord.js.map +1 -0
- package/dist/cli-io.d.ts +28 -0
- package/dist/cli-io.d.ts.map +1 -0
- package/dist/cli-io.js +37 -0
- package/dist/cli-io.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1459 -0
- package/dist/cli.js.map +1 -0
- package/dist/command-registry.d.ts +105 -0
- package/dist/command-registry.d.ts.map +1 -0
- package/dist/command-registry.js +516 -0
- package/dist/command-registry.js.map +1 -0
- package/dist/daemon-lifecycle.d.ts +18 -0
- package/dist/daemon-lifecycle.d.ts.map +1 -0
- package/dist/daemon-lifecycle.js +62 -0
- package/dist/daemon-lifecycle.js.map +1 -0
- package/dist/daemon-run.d.ts +86 -0
- package/dist/daemon-run.d.ts.map +1 -0
- package/dist/daemon-run.js +269 -0
- package/dist/daemon-run.js.map +1 -0
- package/dist/decision.d.ts +18 -0
- package/dist/decision.d.ts.map +1 -0
- package/dist/decision.js +46 -0
- package/dist/decision.js.map +1 -0
- package/dist/live.d.ts +26 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +311 -0
- package/dist/live.js.map +1 -0
- package/dist/mcp-runner.d.ts +41 -0
- package/dist/mcp-runner.d.ts.map +1 -0
- package/dist/mcp-runner.js +306 -0
- package/dist/mcp-runner.js.map +1 -0
- package/dist/ops-commands.d.ts +11 -0
- package/dist/ops-commands.d.ts.map +1 -0
- package/dist/ops-commands.js +358 -0
- package/dist/ops-commands.js.map +1 -0
- package/dist/orchestrate-options.d.ts +9 -0
- package/dist/orchestrate-options.d.ts.map +1 -0
- package/dist/orchestrate-options.js +16 -0
- package/dist/orchestrate-options.js.map +1 -0
- package/dist/orphan-reaper.d.ts +9 -0
- package/dist/orphan-reaper.d.ts.map +1 -0
- package/dist/orphan-reaper.js +97 -0
- package/dist/orphan-reaper.js.map +1 -0
- package/dist/orphan-sweeper.d.ts +7 -0
- package/dist/orphan-sweeper.d.ts.map +1 -0
- package/dist/orphan-sweeper.js +289 -0
- package/dist/orphan-sweeper.js.map +1 -0
- package/dist/plugins.d.ts +44 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +1383 -0
- package/dist/plugins.js.map +1 -0
- package/dist/primary-output.d.ts +14 -0
- package/dist/primary-output.d.ts.map +1 -0
- package/dist/primary-output.js +34 -0
- package/dist/primary-output.js.map +1 -0
- package/dist/registry.d.ts +26 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +71 -0
- package/dist/registry.js.map +1 -0
- package/dist/release.d.ts +14 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +49 -0
- package/dist/release.js.map +1 -0
- package/dist/repl.d.ts +26 -0
- package/dist/repl.d.ts.map +1 -0
- package/dist/repl.js +277 -0
- package/dist/repl.js.map +1 -0
- package/dist/review-command.d.ts +3 -0
- package/dist/review-command.d.ts.map +1 -0
- package/dist/review-command.js +83 -0
- package/dist/review-command.js.map +1 -0
- package/dist/reviewer-options.d.ts +18 -0
- package/dist/reviewer-options.d.ts.map +1 -0
- package/dist/reviewer-options.js +110 -0
- package/dist/reviewer-options.js.map +1 -0
- package/dist/run-options.d.ts +8 -0
- package/dist/run-options.d.ts.map +1 -0
- package/dist/run-options.js +50 -0
- package/dist/run-options.js.map +1 -0
- package/dist/run-secret-scan.d.ts +2 -0
- package/dist/run-secret-scan.d.ts.map +1 -0
- package/dist/run-secret-scan.js +5 -0
- package/dist/run-secret-scan.js.map +1 -0
- package/dist/settings-command.d.ts +3 -0
- package/dist/settings-command.d.ts.map +1 -0
- package/dist/settings-command.js +202 -0
- package/dist/settings-command.js.map +1 -0
- package/dist/settings-display.d.ts +3 -0
- package/dist/settings-display.d.ts.map +1 -0
- package/dist/settings-display.js +33 -0
- package/dist/settings-display.js.map +1 -0
- package/dist/settings-service.d.ts +15 -0
- package/dist/settings-service.d.ts.map +1 -0
- package/dist/settings-service.js +91 -0
- package/dist/settings-service.js.map +1 -0
- package/dist/setup-jobs.d.ts +28 -0
- package/dist/setup-jobs.d.ts.map +1 -0
- package/dist/setup-jobs.js +477 -0
- package/dist/setup-jobs.js.map +1 -0
- package/dist/spec.d.ts +84 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +438 -0
- package/dist/spec.js.map +1 -0
- package/dist/trust-command.d.ts +3 -0
- package/dist/trust-command.d.ts.map +1 -0
- package/dist/trust-command.js +80 -0
- package/dist/trust-command.js.map +1 -0
- package/dist/trust-services.d.ts +7 -0
- package/dist/trust-services.d.ts.map +1 -0
- package/dist/trust-services.js +35 -0
- package/dist/trust-services.js.map +1 -0
- package/package.json +59 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1459 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { Orchestrator } from "@claudexor/orchestrator";
|
|
6
|
+
import { ArtifactStore } from "@claudexor/artifact-store";
|
|
7
|
+
import { DELIVER_MODES, checkPatch, deliver, validateApplyGate, } from "@claudexor/delivery";
|
|
8
|
+
import { CLAUDEXOR_VERSION, containsSecretLikeToken, ensureDir, newId, noProjectRepoRoot, readTextSafe, userConfigDir, writeJson, } from "@claudexor/util";
|
|
9
|
+
import { checkName } from "./release.js";
|
|
10
|
+
import { defaultClaudexorTools, serveClaudexorMcp } from "@claudexor/mcp-server";
|
|
11
|
+
import { AcpServer } from "@claudexor/acp-server";
|
|
12
|
+
import { initProjectConfig, loadConfig } from "@claudexor/config";
|
|
13
|
+
import { atRiskNodeAdvisory, validateModel } from "@claudexor/core";
|
|
14
|
+
import { DecisionRecord, EffortHint, ExternalContextPolicy, ModeKind as ModeKindSchema, Portfolio, RunTelemetry, TaskContract, WorkProduct, } from "@claudexor/schema";
|
|
15
|
+
import { flagBool, flagStr, flagStringList, flagValues, parseArgs, requiredStringFlagError, } from "./args.js";
|
|
16
|
+
import { authSourceAvailability, checksSummary, print, printJson, printUsageError, statusGlyph } from "./cli-io.js";
|
|
17
|
+
import { KNOWN_FLAGS, VALUE_FLAGS, commandFlagScopeError, helpJson, renderHelp } from "./command-registry.js";
|
|
18
|
+
import { buildAgentCapabilityCatalog } from "./capabilities.js";
|
|
19
|
+
import { authCommand, daemonCommand, modelsCommand, secretsCommand } from "./ops-commands.js";
|
|
20
|
+
import { reviewCommand } from "./review-command.js";
|
|
21
|
+
import { followRun, formatRunEventLine, promptQuestionsOnTty } from "./live.js";
|
|
22
|
+
import { assertCliRunParamsHaveNoInlineSecrets } from "./run-secret-scan.js";
|
|
23
|
+
import { connectDaemonIfRunning, daemonOutcomeSummary, ensureDaemon, enqueueAndAwait, exitCodeForState, fetchApplyEligibility, } from "./daemon-run.js";
|
|
24
|
+
import { resolveDecisionBody } from "./decision.js";
|
|
25
|
+
import { primaryOutputForCli } from "./primary-output.js";
|
|
26
|
+
import { PLUGIN_TARGETS, PLUGIN_VERBS, formatPluginResult, pluginCommandErrorResult, runPluginCommand, } from "./plugins.js";
|
|
27
|
+
import { buildGateway, buildRegistry, harnessModels } from "./registry.js";
|
|
28
|
+
import { mcpSurfaceRunner, orchestratorRunner } from "./mcp-runner.js";
|
|
29
|
+
import { settingsCommand } from "./settings-command.js";
|
|
30
|
+
import { trustCommand } from "./trust-command.js";
|
|
31
|
+
import { extractQuestionsFromPlan, freezeSpecFromGrounding, loadFrozenSpec, loadPreviousSpec, persistSpec, readAnswers, resolveRunTestCommands, } from "./spec.js";
|
|
32
|
+
import { parseAutonomy } from "./orchestrate-options.js";
|
|
33
|
+
import { runRepl } from "./repl.js";
|
|
34
|
+
import { parseProtectedPathApprovalFlags, parseTestCommandFlags, parseReviewerEffortFlags, parseReviewerModelFlags, parseReviewerPanelFlags, } from "./run-options.js";
|
|
35
|
+
// Single version SSOT: the generated CLAUDEXOR_VERSION constant (from the root
|
|
36
|
+
// package.json) so the banner / --version can never ship stale or drift.
|
|
37
|
+
const CLI_VERSION = CLAUDEXOR_VERSION;
|
|
38
|
+
// The help text is a rendered view of the command registry (the ONE owner of
|
|
39
|
+
// the CLI surface — command-registry.ts). Never hand-edit a help literal.
|
|
40
|
+
const HELP = renderHelp(CLI_VERSION);
|
|
41
|
+
const MODES = new Set(["ask", "plan", "audit", "agent", "orchestrate"]);
|
|
42
|
+
/** Canonical mode ids are single words; trim and validate against the schema. */
|
|
43
|
+
function normalizeMode(s) {
|
|
44
|
+
const trimmed = s.trim();
|
|
45
|
+
const parsed = ModeKindSchema.safeParse(trimmed);
|
|
46
|
+
if (!parsed.success)
|
|
47
|
+
return trimmed;
|
|
48
|
+
return parsed.data;
|
|
49
|
+
}
|
|
50
|
+
function harnessList(args) {
|
|
51
|
+
const values = flagStringList(args, "harness");
|
|
52
|
+
return values.length > 0 ? values : undefined;
|
|
53
|
+
}
|
|
54
|
+
/** Invalid numeric flag values FAIL LOUDLY: `--n abc` must never silently run with the default. */
|
|
55
|
+
function intFlag(args, key) {
|
|
56
|
+
const v = flagStr(args, key);
|
|
57
|
+
if (v === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
const n = Number.parseInt(v, 10);
|
|
60
|
+
if (!Number.isFinite(n) || String(n) !== v.trim())
|
|
61
|
+
throw new Error(`invalid --${key} '${v}' (expected an integer)`);
|
|
62
|
+
return n;
|
|
63
|
+
}
|
|
64
|
+
function floatFlag(args, key) {
|
|
65
|
+
const v = flagStr(args, key);
|
|
66
|
+
if (v === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
// Number() parses the WHOLE string ('1abc' -> NaN), unlike parseFloat.
|
|
69
|
+
const n = Number(v.trim());
|
|
70
|
+
if (!Number.isFinite(n) || n < 0 || v.trim() === "")
|
|
71
|
+
throw new Error(`invalid --${key} '${v}' (expected a non-negative number)`);
|
|
72
|
+
return n;
|
|
73
|
+
}
|
|
74
|
+
/** Deterministic gate commands from `--test "<cmd>"`; repeat flag or separate with `;;`. */
|
|
75
|
+
function testCommands(args) {
|
|
76
|
+
return parseTestCommandFlags(flagValues(args, "test"));
|
|
77
|
+
}
|
|
78
|
+
/** Typed approval for protected gate/test path changes; never inferred from prompt text. */
|
|
79
|
+
function protectedPathApprovals(args) {
|
|
80
|
+
return parseProtectedPathApprovalFlags(flagValues(args, "allow-protected-path"));
|
|
81
|
+
}
|
|
82
|
+
const ACCESS_PROFILES = new Set([
|
|
83
|
+
"readonly",
|
|
84
|
+
"workspace_write",
|
|
85
|
+
"full",
|
|
86
|
+
"external_sandbox_full",
|
|
87
|
+
"inherit_native",
|
|
88
|
+
]);
|
|
89
|
+
/** Access profile from `--access`. Invalid profiles FAIL LOUDLY (a typo must never silently run with the default write profile). */
|
|
90
|
+
function accessProfile(args) {
|
|
91
|
+
const v = flagStr(args, "access");
|
|
92
|
+
if (v === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (!ACCESS_PROFILES.has(v)) {
|
|
95
|
+
throw new Error(`invalid --access '${v}' (expected readonly|workspace_write|full|external_sandbox_full|inherit_native)`);
|
|
96
|
+
}
|
|
97
|
+
return v;
|
|
98
|
+
}
|
|
99
|
+
function effortHint(args) {
|
|
100
|
+
const v = flagStr(args, "effort");
|
|
101
|
+
if (v === undefined)
|
|
102
|
+
return undefined;
|
|
103
|
+
const parsed = EffortHint.safeParse(v);
|
|
104
|
+
if (!parsed.success)
|
|
105
|
+
throw new Error(`invalid --effort '${v}' (expected low|medium|high|xhigh|max)`);
|
|
106
|
+
return parsed.data;
|
|
107
|
+
}
|
|
108
|
+
function synthesisMode(args) {
|
|
109
|
+
const v = flagStr(args, "synthesis");
|
|
110
|
+
if (v === undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
if (v !== "auto" && v !== "always" && v !== "never") {
|
|
113
|
+
throw new Error(`invalid --synthesis '${v}' (expected auto|always|never)`);
|
|
114
|
+
}
|
|
115
|
+
return v;
|
|
116
|
+
}
|
|
117
|
+
function webPolicy(args) {
|
|
118
|
+
const v = flagStr(args, "web");
|
|
119
|
+
if (v === undefined)
|
|
120
|
+
return undefined;
|
|
121
|
+
const parsed = ExternalContextPolicy.safeParse(v);
|
|
122
|
+
if (!parsed.success)
|
|
123
|
+
throw new Error(`invalid --web '${v}' (expected off|auto|cached|live)`);
|
|
124
|
+
return parsed.data;
|
|
125
|
+
}
|
|
126
|
+
function attachmentPaths(args) {
|
|
127
|
+
const values = [];
|
|
128
|
+
for (const [key, forceImage] of [
|
|
129
|
+
["attach", false],
|
|
130
|
+
["image", true],
|
|
131
|
+
]) {
|
|
132
|
+
for (const path of flagStringList(args, key))
|
|
133
|
+
values.push({ path, forceImage });
|
|
134
|
+
}
|
|
135
|
+
return values;
|
|
136
|
+
}
|
|
137
|
+
function imageMimeFor(path) {
|
|
138
|
+
switch (extname(path).toLowerCase()) {
|
|
139
|
+
case ".png":
|
|
140
|
+
return "image/png";
|
|
141
|
+
case ".jpg":
|
|
142
|
+
case ".jpeg":
|
|
143
|
+
return "image/jpeg";
|
|
144
|
+
case ".gif":
|
|
145
|
+
return "image/gif";
|
|
146
|
+
case ".webp":
|
|
147
|
+
return "image/webp";
|
|
148
|
+
default:
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function attachmentInputs(args) {
|
|
153
|
+
const out = attachmentPaths(args).map(({ path, forceImage }) => {
|
|
154
|
+
const resolved = resolve(path);
|
|
155
|
+
if (!existsSync(resolved) || !lstatSync(resolved).isFile())
|
|
156
|
+
throw new Error(`attachment must be an existing file: ${path}`);
|
|
157
|
+
const imageMime = imageMimeFor(resolved);
|
|
158
|
+
const kind = forceImage || imageMime ? "image" : "file";
|
|
159
|
+
return {
|
|
160
|
+
kind,
|
|
161
|
+
mime: imageMime ?? "application/octet-stream",
|
|
162
|
+
name: basename(resolved),
|
|
163
|
+
path: resolved,
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
return out.length > 0 ? out : undefined;
|
|
167
|
+
}
|
|
168
|
+
function attachmentsFromInputs(inputs) {
|
|
169
|
+
return inputs?.map((a) => ({
|
|
170
|
+
id: newId("att"),
|
|
171
|
+
kind: a.kind,
|
|
172
|
+
mime: a.mime,
|
|
173
|
+
name: a.name,
|
|
174
|
+
path: a.path,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
/** Per-family reviewer model map from `--reviewer-model "openai=gpt-4o-mini,anthropic=claude-haiku"`. Fails loudly on malformed input. */
|
|
178
|
+
function reviewerModels(args) {
|
|
179
|
+
return parseReviewerModelFlags(flagValues(args, "reviewer-model"));
|
|
180
|
+
}
|
|
181
|
+
/** Per-family reviewer effort map from `--reviewer-effort "openai=xhigh,anthropic=high"`. */
|
|
182
|
+
function reviewerEfforts(args) {
|
|
183
|
+
return parseReviewerEffortFlags(flagValues(args, "reviewer-effort"));
|
|
184
|
+
}
|
|
185
|
+
/** Ordered explicit reviewer panel from `--reviewer-panel "claude=claude-opus-4-8:max,cursor=gpt-5.5-extra-high"`. */
|
|
186
|
+
function reviewerPanel(args) {
|
|
187
|
+
return parseReviewerPanelFlags(flagValues(args, "reviewer-panel"));
|
|
188
|
+
}
|
|
189
|
+
async function orchestrate(args, mode, json, forced = {}) {
|
|
190
|
+
const rawPrompt = args._.slice(1).join(" ").trim();
|
|
191
|
+
const specPath = flagStr(args, "spec");
|
|
192
|
+
let loadedSpec = null;
|
|
193
|
+
try {
|
|
194
|
+
loadedSpec = specPath ? loadFrozenSpec(specPath) : null;
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
return printUsageError(json, `claudexor: ${err instanceof Error ? err.message : String(err)}`);
|
|
198
|
+
}
|
|
199
|
+
const prompt = loadedSpec
|
|
200
|
+
? [
|
|
201
|
+
rawPrompt || loadedSpec.spec.intent.raw,
|
|
202
|
+
"",
|
|
203
|
+
"Use this frozen Claudexor SpecPack as the contract. Do not re-litigate settled choices; implement against the acceptance criteria and tests.",
|
|
204
|
+
"",
|
|
205
|
+
`Spec id: ${loadedSpec.spec.id} v${loadedSpec.spec.version}`,
|
|
206
|
+
`Spec hash: ${loadedSpec.specHash}`,
|
|
207
|
+
"",
|
|
208
|
+
"## Summary",
|
|
209
|
+
loadedSpec.spec.summary || "(none)",
|
|
210
|
+
"",
|
|
211
|
+
"## Acceptance Criteria",
|
|
212
|
+
...(loadedSpec.spec.success_criteria.length
|
|
213
|
+
? loadedSpec.spec.success_criteria.map((c) => `- [${c.id}] ${c.behavior}`)
|
|
214
|
+
: ["- (none)"]),
|
|
215
|
+
"",
|
|
216
|
+
"## Non-goals",
|
|
217
|
+
...(loadedSpec.spec.non_goals.length
|
|
218
|
+
? loadedSpec.spec.non_goals.map((x) => `- ${x}`)
|
|
219
|
+
: ["- (none)"]),
|
|
220
|
+
"",
|
|
221
|
+
"## Forbidden approaches",
|
|
222
|
+
...(loadedSpec.spec.forbidden_approaches.length
|
|
223
|
+
? loadedSpec.spec.forbidden_approaches.map((x) => `- ${x}`)
|
|
224
|
+
: ["- (none)"]),
|
|
225
|
+
].join("\n")
|
|
226
|
+
: rawPrompt;
|
|
227
|
+
const spec = loadedSpec?.spec ?? null;
|
|
228
|
+
if (!prompt && mode !== "audit") {
|
|
229
|
+
return printUsageError(json, "claudexor: missing prompt");
|
|
230
|
+
}
|
|
231
|
+
const portfolioRaw = flagStr(args, "portfolio");
|
|
232
|
+
const portfolio = portfolioRaw !== undefined ? Portfolio.safeParse(portfolioRaw) : null;
|
|
233
|
+
if (portfolioRaw !== undefined && !portfolio?.success) {
|
|
234
|
+
return printUsageError(json, `claudexor: unknown --portfolio '${portfolioRaw}'`);
|
|
235
|
+
}
|
|
236
|
+
let reviewerEffortOverrides;
|
|
237
|
+
let resolvedReviewerModels;
|
|
238
|
+
let resolvedReviewerPanel;
|
|
239
|
+
let resolvedWebPolicy = undefined;
|
|
240
|
+
let resolvedAccess = undefined;
|
|
241
|
+
let resolvedEffort;
|
|
242
|
+
let maxUsd;
|
|
243
|
+
let maxToolCalls;
|
|
244
|
+
let nFlag;
|
|
245
|
+
let attemptsFlag;
|
|
246
|
+
let autonomy;
|
|
247
|
+
let resolvedSynthesis = undefined;
|
|
248
|
+
let resolvedHarnesses;
|
|
249
|
+
let resolvedPrimaryHarness;
|
|
250
|
+
let resolvedModel;
|
|
251
|
+
let attachments;
|
|
252
|
+
let attachmentRequest;
|
|
253
|
+
let resolvedProtectedPathApprovals;
|
|
254
|
+
try {
|
|
255
|
+
reviewerEffortOverrides = reviewerEfforts(args);
|
|
256
|
+
resolvedReviewerModels = reviewerModels(args);
|
|
257
|
+
resolvedReviewerPanel = reviewerPanel(args);
|
|
258
|
+
resolvedWebPolicy = webPolicy(args);
|
|
259
|
+
resolvedAccess = accessProfile(args);
|
|
260
|
+
resolvedEffort = effortHint(args);
|
|
261
|
+
resolvedHarnesses = harnessList(args);
|
|
262
|
+
resolvedPrimaryHarness = flagStr(args, "primary-harness");
|
|
263
|
+
resolvedModel = flagStr(args, "model");
|
|
264
|
+
maxUsd = floatFlag(args, "max-usd");
|
|
265
|
+
maxToolCalls = intFlag(args, "max-tool-calls");
|
|
266
|
+
nFlag = intFlag(args, "n");
|
|
267
|
+
attemptsFlag = intFlag(args, "attempts");
|
|
268
|
+
autonomy = parseAutonomy(flagStr(args, "autonomy"));
|
|
269
|
+
resolvedSynthesis = synthesisMode(args);
|
|
270
|
+
attachmentRequest = attachmentInputs(args);
|
|
271
|
+
attachments = attachmentsFromInputs(attachmentRequest);
|
|
272
|
+
resolvedProtectedPathApprovals = protectedPathApprovals(args);
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
return printUsageError(json, `claudexor: ${err instanceof Error ? err.message : String(err)}`);
|
|
276
|
+
}
|
|
277
|
+
let tests;
|
|
278
|
+
try {
|
|
279
|
+
const cliTests = testCommands(args) ?? [];
|
|
280
|
+
tests = resolveRunTestCommands(cliTests, spec);
|
|
281
|
+
assertCliRunParamsHaveNoInlineSecrets({
|
|
282
|
+
prompt,
|
|
283
|
+
attachments: attachmentRequest,
|
|
284
|
+
mode,
|
|
285
|
+
harnesses: resolvedHarnesses,
|
|
286
|
+
primaryHarness: resolvedPrimaryHarness,
|
|
287
|
+
model: resolvedModel,
|
|
288
|
+
effort: resolvedEffort,
|
|
289
|
+
reviewerPanel: resolvedReviewerPanel,
|
|
290
|
+
reviewerModels: resolvedReviewerModels,
|
|
291
|
+
reviewerEfforts: reviewerEffortOverrides,
|
|
292
|
+
tests,
|
|
293
|
+
protectedPathApprovals: resolvedProtectedPathApprovals,
|
|
294
|
+
maxUsd,
|
|
295
|
+
access: resolvedAccess,
|
|
296
|
+
web: resolvedWebPolicy,
|
|
297
|
+
externalContextPolicy: resolvedWebPolicy,
|
|
298
|
+
synthesis: resolvedSynthesis,
|
|
299
|
+
autonomy,
|
|
300
|
+
specId: spec?.id,
|
|
301
|
+
specHash: loadedSpec?.specHash,
|
|
302
|
+
specPath: loadedSpec?.specPath,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
return printUsageError(json, `claudexor: ${err instanceof Error ? err.message : String(err)}`);
|
|
307
|
+
}
|
|
308
|
+
// `--autonomy` only governs the orchestrate executor; on any other
|
|
309
|
+
// mode it would silently do nothing, so reject it loudly (a misplaced flag is
|
|
310
|
+
// an error, not a no-op).
|
|
311
|
+
if (autonomy !== undefined && mode !== "orchestrate") {
|
|
312
|
+
return printUsageError(json, `claudexor: --autonomy only applies to 'orchestrate' (got mode '${mode}')`);
|
|
313
|
+
}
|
|
314
|
+
// The mutating run paths are DAEMON-TRACKED: they enqueue via the daemon
|
|
315
|
+
// (auto-started if needed) so the control-api can see/unblock them and a
|
|
316
|
+
// blocked run is unblockable through `claudexor decision`. This covers `agent`
|
|
317
|
+
// and an `orchestrate` run that actually ACTS (auto_safe/auto_full execute the
|
|
318
|
+
// plan against the tree). Read-only routes (ask/plan/audit and orchestrate in
|
|
319
|
+
// the default `suggest` autonomy, where the plan IS the work product) have
|
|
320
|
+
// nothing to apply or unblock, so they stay in-process.
|
|
321
|
+
const orchestrateExecutes = mode === "orchestrate" && autonomy !== undefined && autonomy !== "suggest";
|
|
322
|
+
// --max-tool-calls caps the orchestrate EXECUTOR's plan steps; on any other
|
|
323
|
+
// mode it would be a silent no-op knob (INV-023) — refuse loudly.
|
|
324
|
+
if (maxToolCalls !== undefined && mode !== "orchestrate") {
|
|
325
|
+
return printUsageError(json, "claudexor: --max-tool-calls only applies to orchestrate runs");
|
|
326
|
+
}
|
|
327
|
+
if (mode === "agent" || orchestrateExecutes) {
|
|
328
|
+
return daemonAgentRun(args, json, {
|
|
329
|
+
mode,
|
|
330
|
+
autonomy,
|
|
331
|
+
prompt,
|
|
332
|
+
tests,
|
|
333
|
+
portfolio: portfolio?.success ? portfolio.data : undefined,
|
|
334
|
+
maxUsd,
|
|
335
|
+
maxToolCalls,
|
|
336
|
+
reviewerPanel: resolvedReviewerPanel,
|
|
337
|
+
reviewerModels: resolvedReviewerModels,
|
|
338
|
+
reviewerEfforts: reviewerEffortOverrides,
|
|
339
|
+
protectedPathApprovals: resolvedProtectedPathApprovals,
|
|
340
|
+
resolvedWebPolicy,
|
|
341
|
+
resolvedAccess,
|
|
342
|
+
resolvedEffort,
|
|
343
|
+
resolvedSynthesis,
|
|
344
|
+
nFlag,
|
|
345
|
+
attemptsFlag,
|
|
346
|
+
specId: spec?.id,
|
|
347
|
+
specHash: loadedSpec?.specHash,
|
|
348
|
+
specPath: loadedSpec?.specPath,
|
|
349
|
+
attachmentRequest,
|
|
350
|
+
forced,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
const orch = new Orchestrator({
|
|
354
|
+
registry: buildRegistry(),
|
|
355
|
+
portfolio: portfolio?.success ? portfolio.data : undefined,
|
|
356
|
+
maxUsd: maxUsd ?? null,
|
|
357
|
+
reviewerPanel: resolvedReviewerPanel,
|
|
358
|
+
reviewerModels: resolvedReviewerModels,
|
|
359
|
+
reviewerEfforts: reviewerEffortOverrides,
|
|
360
|
+
});
|
|
361
|
+
// Ctrl-C on a direct (in-process) run cancels GRACEFULLY: abort the run
|
|
362
|
+
// signal so harness children die via the process-group kill plumbing, gates
|
|
363
|
+
// are skipped, and the run ends with a typed cancelled terminal — instead
|
|
364
|
+
// of the node process dying and orphaning children + a terminal-less
|
|
365
|
+
// events.jsonl. A second Ctrl-C force-exits.
|
|
366
|
+
const cancelController = new AbortController();
|
|
367
|
+
let sigints = 0;
|
|
368
|
+
const onSigint = () => {
|
|
369
|
+
sigints += 1;
|
|
370
|
+
if (sigints >= 2)
|
|
371
|
+
process.exit(130);
|
|
372
|
+
process.stderr.write("\ncancelling run (Ctrl-C again to force-quit)...\n");
|
|
373
|
+
cancelController.abort();
|
|
374
|
+
};
|
|
375
|
+
process.on("SIGINT", onSigint);
|
|
376
|
+
process.on("SIGTERM", onSigint);
|
|
377
|
+
try {
|
|
378
|
+
const res = await orch.run({
|
|
379
|
+
repoRoot: process.cwd(),
|
|
380
|
+
prompt: prompt || "audit this repository",
|
|
381
|
+
attachments,
|
|
382
|
+
mode,
|
|
383
|
+
signal: cancelController.signal,
|
|
384
|
+
// Only `suggest` (or unset) reaches the in-process path; auto_safe/auto_full
|
|
385
|
+
// are routed to the daemon above. Forward it so an explicit --autonomy
|
|
386
|
+
// suggest is honoured rather than silently dropped.
|
|
387
|
+
...(autonomy ? { autonomy } : {}),
|
|
388
|
+
harnesses: resolvedHarnesses,
|
|
389
|
+
primaryHarness: resolvedPrimaryHarness,
|
|
390
|
+
portfolio: portfolio?.success ? portfolio.data : undefined,
|
|
391
|
+
n: forced.race === true ? (nFlag ?? 2) : nFlag,
|
|
392
|
+
attempts: attemptsFlag ?? null,
|
|
393
|
+
untilClean: flagBool(args, "until-clean"),
|
|
394
|
+
swarm: forced.swarm === true || flagBool(args, "swarm"),
|
|
395
|
+
create: forced.create === true || flagBool(args, "create"),
|
|
396
|
+
tests,
|
|
397
|
+
protectedPathApprovals: resolvedProtectedPathApprovals,
|
|
398
|
+
maxUsd: maxUsd ?? null,
|
|
399
|
+
maxToolCalls: maxToolCalls ?? null,
|
|
400
|
+
access: resolvedAccess,
|
|
401
|
+
web: resolvedWebPolicy,
|
|
402
|
+
externalContextPolicy: resolvedWebPolicy,
|
|
403
|
+
model: resolvedModel,
|
|
404
|
+
effort: resolvedEffort,
|
|
405
|
+
synthesis: resolvedSynthesis,
|
|
406
|
+
specId: spec?.id,
|
|
407
|
+
specHash: loadedSpec?.specHash,
|
|
408
|
+
specPath: loadedSpec?.specPath,
|
|
409
|
+
inPlace: flagBool(args, "in-place"),
|
|
410
|
+
// Live progress + interactive answers on a TTY; --json stays a pure
|
|
411
|
+
// machine surface (no printer, no prompts — questions decline benignly).
|
|
412
|
+
...(json
|
|
413
|
+
? {}
|
|
414
|
+
: {
|
|
415
|
+
onEvent: (ev) => {
|
|
416
|
+
const line = formatRunEventLine(ev);
|
|
417
|
+
if (line)
|
|
418
|
+
print(line);
|
|
419
|
+
},
|
|
420
|
+
onInteraction: (ctx) => promptQuestionsOnTty(ctx.request.interaction_id, ctx.request.questions, ctx.timeoutAt),
|
|
421
|
+
}),
|
|
422
|
+
});
|
|
423
|
+
if (json) {
|
|
424
|
+
// One machine surface: on a non-success terminal, ADD a top-level `error`
|
|
425
|
+
// (mirroring the daemon path's `error`) so a JSON consumer reads the reason
|
|
426
|
+
// the same way regardless of which run path produced it. Add-only: the full
|
|
427
|
+
// result (runId/runDir/status/mode/winner/summary/candidates) is preserved.
|
|
428
|
+
printJson(res.status === "success" ? res : { ...res, error: res.summary });
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
print(`run ${res.runId} [${res.status}] mode=${res.mode} winner=${res.winner ?? "none"}`);
|
|
432
|
+
print(` artifacts: ${res.runDir}`);
|
|
433
|
+
for (const c of res.candidates)
|
|
434
|
+
print(` - ${c.attemptId} ${c.harnessId} [${c.status}]`);
|
|
435
|
+
print("");
|
|
436
|
+
print(res.summary);
|
|
437
|
+
}
|
|
438
|
+
return res.status === "success" ? 0 : 1;
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
if (json)
|
|
442
|
+
printJson({
|
|
443
|
+
ok: false,
|
|
444
|
+
exitCode: 1,
|
|
445
|
+
error: `claudexor: ${err instanceof Error ? err.message : String(err)}`,
|
|
446
|
+
});
|
|
447
|
+
else
|
|
448
|
+
process.stderr.write(`claudexor: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
449
|
+
return 1;
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
process.removeListener("SIGINT", onSigint);
|
|
453
|
+
process.removeListener("SIGTERM", onSigint);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* DAEMON-TRACKED mutating run (the unified `agent`/`best-of`/`create` path, and an
|
|
458
|
+
* `orchestrate` run with auto_safe/auto_full autonomy). Auto-starts
|
|
459
|
+
* the daemon if needed, enqueues via the control API (so the run is registered
|
|
460
|
+
* and the control-api can see/unblock it), streams its events to the TTY (text
|
|
461
|
+
* mode) and resolves the runDir from the daemon (the run lives under the daemon
|
|
462
|
+
* dir, not project-local) so apply/decision/inspect can find it. `--json` prints
|
|
463
|
+
* exactly one object `{ runId, runDir, status }` — the SWE/TB benchmark runner
|
|
464
|
+
* parses this to read <runDir>/final/patch.diff.
|
|
465
|
+
*/
|
|
466
|
+
async function daemonAgentRun(args, json, p) {
|
|
467
|
+
// The run is always project-scoped (the cwd is its repo); execution is
|
|
468
|
+
// live in-place only under the explicit --in-place convergence path.
|
|
469
|
+
const inPlace = flagBool(args, "in-place");
|
|
470
|
+
const body = {
|
|
471
|
+
prompt: p.prompt,
|
|
472
|
+
...(p.attachmentRequest ? { attachments: p.attachmentRequest } : {}),
|
|
473
|
+
mode: p.mode,
|
|
474
|
+
// Autonomy only governs the orchestrate executor; send it only for that mode.
|
|
475
|
+
...(p.mode === "orchestrate" && p.autonomy ? { autonomy: p.autonomy } : {}),
|
|
476
|
+
scope: { kind: "project", root: process.cwd() },
|
|
477
|
+
execution: { isolation: inPlace ? "live" : "envelope" },
|
|
478
|
+
...(harnessList(args) ? { harnesses: harnessList(args) } : {}),
|
|
479
|
+
...(flagStr(args, "primary-harness")
|
|
480
|
+
? { primaryHarness: flagStr(args, "primary-harness") }
|
|
481
|
+
: {}),
|
|
482
|
+
...(p.portfolio ? { portfolio: p.portfolio } : {}),
|
|
483
|
+
...(p.forced.race === true ? { n: p.nFlag ?? 2 } : p.nFlag !== undefined ? { n: p.nFlag } : {}),
|
|
484
|
+
...(p.attemptsFlag !== undefined ? { attempts: p.attemptsFlag } : {}),
|
|
485
|
+
...(flagBool(args, "until-clean") ? { untilClean: true } : {}),
|
|
486
|
+
...(p.forced.swarm === true || flagBool(args, "swarm") ? { swarm: true } : {}),
|
|
487
|
+
...(p.forced.create === true || flagBool(args, "create") ? { create: true } : {}),
|
|
488
|
+
...(p.resolvedSynthesis ? { synthesis: p.resolvedSynthesis } : {}),
|
|
489
|
+
...(p.tests ? { tests: p.tests } : {}),
|
|
490
|
+
...(p.protectedPathApprovals ? { protectedPathApprovals: p.protectedPathApprovals } : {}),
|
|
491
|
+
...(p.maxUsd !== undefined ? { maxUsd: p.maxUsd } : {}),
|
|
492
|
+
...(p.mode === "orchestrate" && p.maxToolCalls !== undefined ? { maxToolCalls: p.maxToolCalls } : {}),
|
|
493
|
+
...(p.resolvedAccess ? { access: p.resolvedAccess } : {}),
|
|
494
|
+
...(p.resolvedWebPolicy ? { web: p.resolvedWebPolicy } : {}),
|
|
495
|
+
...(flagStr(args, "model") ? { model: flagStr(args, "model") } : {}),
|
|
496
|
+
...(p.resolvedEffort ? { effort: p.resolvedEffort } : {}),
|
|
497
|
+
...(p.reviewerPanel ? { reviewerPanel: p.reviewerPanel } : {}),
|
|
498
|
+
...(p.reviewerModels ? { reviewerModels: p.reviewerModels } : {}),
|
|
499
|
+
...(p.reviewerEfforts ? { reviewerEfforts: p.reviewerEfforts } : {}),
|
|
500
|
+
...(p.specId ? { specId: p.specId } : {}),
|
|
501
|
+
...(p.specHash ? { specHash: p.specHash } : {}),
|
|
502
|
+
...(p.specPath ? { specPath: p.specPath } : {}),
|
|
503
|
+
};
|
|
504
|
+
try {
|
|
505
|
+
const { client, addr } = await ensureDaemon();
|
|
506
|
+
if (json) {
|
|
507
|
+
// Pure machine surface: enqueue, wait for the terminal outcome, print
|
|
508
|
+
// exactly one JSON object. No event printer, no TTY prompts.
|
|
509
|
+
const out = await enqueueAndAwait(client, addr, body, { waitForTerminal: true });
|
|
510
|
+
// Keep the documented bench-parser keys {runId,runDir,status}; ADD `mode`,
|
|
511
|
+
// `error` (only for real errors), and `summary` (a machine-readable reason
|
|
512
|
+
// for EVERY non-success terminal incl. `blocked`, which carries no error)
|
|
513
|
+
// so this path matches the in-process one (P2: one machine surface).
|
|
514
|
+
const reason = daemonOutcomeSummary(out);
|
|
515
|
+
// ADD-ONLY key (bench contract keeps {runId,runDir,status}): the derived
|
|
516
|
+
// apply-gate verdict, so machine callers act on truth instead of
|
|
517
|
+
// re-implying eligibility from status.
|
|
518
|
+
const applyEligibility = await fetchApplyEligibility(addr, out.runId);
|
|
519
|
+
printJson({
|
|
520
|
+
runId: out.runId,
|
|
521
|
+
runDir: out.runDir,
|
|
522
|
+
status: out.status,
|
|
523
|
+
jobId: out.jobId,
|
|
524
|
+
mode: p.mode,
|
|
525
|
+
...(out.error ? { error: out.error } : {}),
|
|
526
|
+
...(reason ? { summary: reason } : {}),
|
|
527
|
+
...(applyEligibility ? { applyEligibility } : {}),
|
|
528
|
+
});
|
|
529
|
+
return exitCodeForState(out.status);
|
|
530
|
+
}
|
|
531
|
+
// Text mode: enqueue, then live-stream the run through the shared follow
|
|
532
|
+
// pipeline (replay + push + interactive TTY question answering), then print
|
|
533
|
+
// the honest terminal line + artifacts dir resolved from the daemon.
|
|
534
|
+
const started = await enqueueAndAwait(client, addr, body, { waitForTerminal: false });
|
|
535
|
+
if (!started.runId) {
|
|
536
|
+
print(`run did not start: ${started.status}${started.error ? ` — ${started.error}` : ""}`);
|
|
537
|
+
return exitCodeForState(started.status);
|
|
538
|
+
}
|
|
539
|
+
await followRun(started.runId, false);
|
|
540
|
+
const final = started.jobId ? await client.status(started.jobId) : null;
|
|
541
|
+
const status = final?.state ?? started.status;
|
|
542
|
+
print("");
|
|
543
|
+
print(`run ${started.runId} [${status}]`);
|
|
544
|
+
print(` artifacts: ${final?.runDir ?? started.runDir}`);
|
|
545
|
+
if (status === "blocked") {
|
|
546
|
+
print(` blocked (needs human): unblock with \`claudexor decision ${started.runId} --accept-risk\` or rerun with \`claudexor decision ${started.runId} --rerun --feedback "..."\``);
|
|
547
|
+
}
|
|
548
|
+
else if (exitCodeForState(status) === 0) {
|
|
549
|
+
// Honest post-run hint: "apply with" is printed ONLY when the apply
|
|
550
|
+
// gate verifiably accepts the patch. A null verdict means no patch
|
|
551
|
+
// artifact (answer/no-op runs) or an unreachable detail endpoint —
|
|
552
|
+
// either way, promising `claudexor apply` would be a lie, so print the
|
|
553
|
+
// inspect handle instead. Ineligible runs get the honest unblock hint
|
|
554
|
+
// (ungated/review_not_run used to get a doomed apply command here).
|
|
555
|
+
const eligibility = await fetchApplyEligibility(addr, started.runId);
|
|
556
|
+
if (eligibility?.eligible) {
|
|
557
|
+
print(` apply with: claudexor apply ${started.runId}`);
|
|
558
|
+
}
|
|
559
|
+
else if (eligibility?.requiredAction) {
|
|
560
|
+
print(` not applyable yet: ${eligibility.requiredAction}`);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
print(` inspect with: claudexor inspect ${started.runId}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return exitCodeForState(status);
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
if (json)
|
|
570
|
+
printJson({
|
|
571
|
+
ok: false,
|
|
572
|
+
exitCode: 1,
|
|
573
|
+
error: `claudexor: ${err instanceof Error ? err.message : String(err)}`,
|
|
574
|
+
});
|
|
575
|
+
else
|
|
576
|
+
process.stderr.write(`claudexor: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
577
|
+
return 1;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* `claudexor decision <runId> ...` — the CLI safety net that unblocks a
|
|
582
|
+
* daemon-tracked blocked run (the surface that closes the un-unblockable gap).
|
|
583
|
+
* Maps the flag to a typed RunDecisionAction and POSTs to /runs/:id/decision via
|
|
584
|
+
* the daemon control API, printing the response honestly.
|
|
585
|
+
*/
|
|
586
|
+
async function decisionCommand(args, json) {
|
|
587
|
+
const runId = args._[1];
|
|
588
|
+
if (!runId) {
|
|
589
|
+
return printUsageError(json, 'usage: claudexor decision <run_id> --accept-risk | --override | --revert | --accept-clean-patch [--apply-mode <m>] | --rerun --feedback "<text>"');
|
|
590
|
+
}
|
|
591
|
+
const resolved = resolveDecisionBody(args);
|
|
592
|
+
if (!resolved.ok) {
|
|
593
|
+
return printUsageError(json, `claudexor decision: ${resolved.message}`);
|
|
594
|
+
}
|
|
595
|
+
const { action, body } = resolved;
|
|
596
|
+
try {
|
|
597
|
+
const { addr } = await ensureDaemon();
|
|
598
|
+
const res = await fetch(`${addr.baseUrl}/runs/${encodeURIComponent(runId)}/decision`, {
|
|
599
|
+
method: "POST",
|
|
600
|
+
headers: { Authorization: `Bearer ${addr.token}`, "content-type": "application/json" },
|
|
601
|
+
body: JSON.stringify(body),
|
|
602
|
+
});
|
|
603
|
+
const text = await res.text();
|
|
604
|
+
const data = text ? JSON.parse(text) : {};
|
|
605
|
+
if (!res.ok) {
|
|
606
|
+
// A typed decision rejection (e.g. revert refused: tree diverged) carries
|
|
607
|
+
// its reason in `message`; transport/gate failures use `error`. Surface
|
|
608
|
+
// whichever is present so the concrete reason is never lost behind "HTTP 409".
|
|
609
|
+
const msg = typeof data["error"] === "string"
|
|
610
|
+
? data["error"]
|
|
611
|
+
: typeof data["message"] === "string"
|
|
612
|
+
? data["message"]
|
|
613
|
+
: `decision failed (HTTP ${res.status})`;
|
|
614
|
+
if (json)
|
|
615
|
+
printJson({ accepted: false, status: "rejected", message: msg });
|
|
616
|
+
else
|
|
617
|
+
process.stderr.write(`claudexor decision: ${msg}\n`);
|
|
618
|
+
return 1;
|
|
619
|
+
}
|
|
620
|
+
if (json) {
|
|
621
|
+
printJson(data);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
const accepted = data["accepted"] === true;
|
|
625
|
+
print(`decision ${action} on ${runId}: ${accepted ? "accepted" : "rejected"} [${String(data["status"] ?? "?")}]`);
|
|
626
|
+
if (typeof data["newRunId"] === "string")
|
|
627
|
+
print(` new run: ${data["newRunId"]}`);
|
|
628
|
+
if (typeof data["message"] === "string")
|
|
629
|
+
print(` ${data["message"]}`);
|
|
630
|
+
}
|
|
631
|
+
return data["accepted"] === true ? 0 : 1;
|
|
632
|
+
}
|
|
633
|
+
catch (err) {
|
|
634
|
+
process.stderr.write(`claudexor decision: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
635
|
+
return 1;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Resolve the ArtifactStore that owns a given run, regardless of the cwd the
|
|
640
|
+
* CLI is invoked from. Order:
|
|
641
|
+
* 1. the project store rooted at the current cwd (the common case);
|
|
642
|
+
* 2. the user-level store (~/.claudexor/runs) used by no-project Ask runs;
|
|
643
|
+
* 3. a daemon-tracked run that started in ANOTHER project — agent/race/create
|
|
644
|
+
* runs live under `<thatProjectRoot>/.claudexor/runs/<runId>`, so we ask
|
|
645
|
+
* the daemon for the run's absolute runDir (GET /runs/:id ->
|
|
646
|
+
* summary.runDir) and rebuild a store whose runPaths(runId).root matches.
|
|
647
|
+
* Returns null when no store can be located (the run does not exist anywhere
|
|
648
|
+
* reachable). Never throws on daemon unavailability — it falls through.
|
|
649
|
+
*/
|
|
650
|
+
async function resolveRunStore(runId) {
|
|
651
|
+
// An id that fails the store's shape fence (separators, `..`) can never
|
|
652
|
+
// name a run: report it as "no such run" through the normal typed path —
|
|
653
|
+
// the fence must not turn a typo'd id into a raw crash that breaks --json
|
|
654
|
+
// purity on inspect/apply/follow.
|
|
655
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(runId))
|
|
656
|
+
return null;
|
|
657
|
+
// 1. project cwd store
|
|
658
|
+
const cwdStore = new ArtifactStore(process.cwd());
|
|
659
|
+
if (existsSync(cwdStore.runPaths(runId).root))
|
|
660
|
+
return { store: cwdStore, root: cwdStore.runPaths(runId).root };
|
|
661
|
+
// 2. user-level (no-project Ask) store
|
|
662
|
+
const userStore = new ArtifactStore(noProjectRepoRoot(), { claudexorDir: userConfigDir() });
|
|
663
|
+
if (existsSync(userStore.runPaths(runId).root))
|
|
664
|
+
return { store: userStore, root: userStore.runPaths(runId).root };
|
|
665
|
+
// 3. daemon-tracked run in another project: ask the daemon for its runDir.
|
|
666
|
+
// Connect ONLY to an already-running daemon — never auto-spawn one for a
|
|
667
|
+
// read-only lookup (a typo'd id must report "no such run", not silently
|
|
668
|
+
// launch a background daemon). Acting paths (decision/enqueue) still use
|
|
669
|
+
// ensureDaemon().
|
|
670
|
+
try {
|
|
671
|
+
const conn = await connectDaemonIfRunning();
|
|
672
|
+
if (!conn)
|
|
673
|
+
return null;
|
|
674
|
+
const { addr } = conn;
|
|
675
|
+
const resp = await fetch(`${addr.baseUrl}/runs/${encodeURIComponent(runId)}`, {
|
|
676
|
+
headers: { Authorization: `Bearer ${addr.token}` },
|
|
677
|
+
});
|
|
678
|
+
if (resp.ok) {
|
|
679
|
+
const detail = (await resp.json());
|
|
680
|
+
const runDir = detail.summary?.runDir;
|
|
681
|
+
if (runDir && existsSync(runDir)) {
|
|
682
|
+
// runDir = <repoRoot>/.claudexor/runs/<runId>; reconstruct a store whose
|
|
683
|
+
// runPaths(runId).root === runDir: runId -> runs -> .claudexor.
|
|
684
|
+
const claudexorDir = resolve(runDir, "..", "..");
|
|
685
|
+
const ds = new ArtifactStore(dirname(claudexorDir), { claudexorDir });
|
|
686
|
+
if (existsSync(ds.runPaths(runId).root))
|
|
687
|
+
return { store: ds, root: ds.runPaths(runId).root };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
/* daemon unavailable: fall through */
|
|
693
|
+
}
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
async function specCommand(args, json) {
|
|
697
|
+
const prompt = args._.slice(1).join(" ").trim();
|
|
698
|
+
if (!prompt) {
|
|
699
|
+
return printUsageError(json, "claudexor: missing spec prompt");
|
|
700
|
+
}
|
|
701
|
+
if (containsSecretLikeToken(prompt)) {
|
|
702
|
+
return printUsageError(json, "claudexor spec: prompt contains a secret-like token; specs are durable artifacts, so store secrets by ref and retry with a sanitized prompt");
|
|
703
|
+
}
|
|
704
|
+
const answersPath = flagStr(args, "answers");
|
|
705
|
+
// The grounding step is a real (multi-)harness plan run, so the same
|
|
706
|
+
// routing/cost/review controls as the plan verb apply — but ONLY that step
|
|
707
|
+
// spawns a run. Parse them up-front so malformed values fail loudly on
|
|
708
|
+
// every path, and refuse ALL grounding-only flags on the --answers path,
|
|
709
|
+
// where no grounding run exists for them to control.
|
|
710
|
+
let groundingEffort;
|
|
711
|
+
let groundingMaxUsd;
|
|
712
|
+
let groundingHarnesses;
|
|
713
|
+
let groundingN;
|
|
714
|
+
let groundingWeb;
|
|
715
|
+
let groundingReviewerPanel;
|
|
716
|
+
let groundingReviewerModels;
|
|
717
|
+
let groundingReviewerEfforts;
|
|
718
|
+
try {
|
|
719
|
+
groundingEffort = effortHint(args);
|
|
720
|
+
groundingMaxUsd = floatFlag(args, "max-usd");
|
|
721
|
+
groundingHarnesses = harnessList(args);
|
|
722
|
+
groundingN = intFlag(args, "n");
|
|
723
|
+
groundingWeb = webPolicy(args);
|
|
724
|
+
groundingReviewerPanel = reviewerPanel(args);
|
|
725
|
+
groundingReviewerModels = reviewerModels(args);
|
|
726
|
+
groundingReviewerEfforts = reviewerEfforts(args);
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
return printUsageError(json, `claudexor: ${err instanceof Error ? err.message : String(err)}`);
|
|
730
|
+
}
|
|
731
|
+
if (answersPath &&
|
|
732
|
+
(groundingEffort !== undefined ||
|
|
733
|
+
groundingMaxUsd !== undefined ||
|
|
734
|
+
groundingHarnesses !== undefined ||
|
|
735
|
+
groundingN !== undefined ||
|
|
736
|
+
groundingWeb !== undefined ||
|
|
737
|
+
groundingReviewerPanel !== undefined ||
|
|
738
|
+
groundingReviewerModels !== undefined ||
|
|
739
|
+
groundingReviewerEfforts !== undefined)) {
|
|
740
|
+
return printUsageError(json, "claudexor spec: --harness/--n/--web/--effort/--max-usd/--reviewer-panel/--reviewer-model/--reviewer-effort control the grounding plan run and only apply when generating questions; drop them when re-running with --answers");
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
const answers = answersPath ? readAnswers(answersPath) : null;
|
|
744
|
+
let planRunId = answers?.planRunId ?? "";
|
|
745
|
+
let planDir = answers?.planDir ?? "";
|
|
746
|
+
let planText = planDir ? (readTextSafe(join(planDir, "final", "plan.md")) ?? "") : "";
|
|
747
|
+
if (!planText) {
|
|
748
|
+
if (answersPath) {
|
|
749
|
+
throw new Error("answers file does not contain a usable planDir/final/plan.md; re-run without --answers to generate a fresh questions file");
|
|
750
|
+
}
|
|
751
|
+
const orch = new Orchestrator({
|
|
752
|
+
registry: buildRegistry(),
|
|
753
|
+
reviewerPanel: groundingReviewerPanel,
|
|
754
|
+
reviewerModels: groundingReviewerModels,
|
|
755
|
+
reviewerEfforts: groundingReviewerEfforts,
|
|
756
|
+
});
|
|
757
|
+
const plan = await orch.run({
|
|
758
|
+
repoRoot: process.cwd(),
|
|
759
|
+
prompt,
|
|
760
|
+
mode: "plan",
|
|
761
|
+
harnesses: groundingHarnesses,
|
|
762
|
+
n: groundingN,
|
|
763
|
+
access: "readonly",
|
|
764
|
+
web: groundingWeb,
|
|
765
|
+
effort: groundingEffort,
|
|
766
|
+
maxUsd: groundingMaxUsd ?? null,
|
|
767
|
+
});
|
|
768
|
+
planRunId = plan.runId;
|
|
769
|
+
planDir = plan.runDir;
|
|
770
|
+
planText = readTextSafe(join(plan.runDir, "final", "plan.md")) ?? plan.summary;
|
|
771
|
+
}
|
|
772
|
+
const questions = extractQuestionsFromPlan(planText);
|
|
773
|
+
if (!answersPath) {
|
|
774
|
+
const draftDir = join(process.cwd(), ".claudexor", "specs", "drafts", planRunId);
|
|
775
|
+
ensureDir(draftDir);
|
|
776
|
+
const questionsPath = join(draftDir, "questions.json");
|
|
777
|
+
writeJson(questionsPath, { prompt, planRunId, planDir, questions, answers: [] });
|
|
778
|
+
const result = {
|
|
779
|
+
status: "questions",
|
|
780
|
+
planRunId,
|
|
781
|
+
planDir,
|
|
782
|
+
questionsPath,
|
|
783
|
+
questions,
|
|
784
|
+
};
|
|
785
|
+
if (json)
|
|
786
|
+
printJson(result);
|
|
787
|
+
else {
|
|
788
|
+
print(`plan grounding run: ${planRunId}`);
|
|
789
|
+
print(`questions: ${questionsPath}`);
|
|
790
|
+
print(`answer with: claudexor spec ${JSON.stringify(prompt)} --answers ${questionsPath}`);
|
|
791
|
+
}
|
|
792
|
+
return 0;
|
|
793
|
+
}
|
|
794
|
+
const spec = await freezeSpecFromGrounding(prompt, planText, answers ?? readAnswers(answersPath));
|
|
795
|
+
const persisted = persistSpec(process.cwd(), spec, planText, loadPreviousSpec(flagStr(args, "previous")));
|
|
796
|
+
const specJsonPath = join(persisted.specDir, "spec.json");
|
|
797
|
+
const runHint = `claudexor best-of --spec ${JSON.stringify(specJsonPath)}`;
|
|
798
|
+
const result = {
|
|
799
|
+
status: "frozen",
|
|
800
|
+
planRunId,
|
|
801
|
+
planDir,
|
|
802
|
+
specId: spec.id,
|
|
803
|
+
specDir: persisted.specDir,
|
|
804
|
+
specHash: persisted.specHash,
|
|
805
|
+
runHint,
|
|
806
|
+
questions,
|
|
807
|
+
changes: persisted.changes,
|
|
808
|
+
};
|
|
809
|
+
if (json)
|
|
810
|
+
printJson(result);
|
|
811
|
+
else {
|
|
812
|
+
print(`frozen SpecPack: ${spec.id} v${spec.version}`);
|
|
813
|
+
print(` dir: ${persisted.specDir}`);
|
|
814
|
+
print(` hash: ${persisted.specHash}`);
|
|
815
|
+
print(` native projection: ${join(persisted.specDir, "PLANS.md")}`);
|
|
816
|
+
print(`run: ${runHint}`);
|
|
817
|
+
}
|
|
818
|
+
return 0;
|
|
819
|
+
}
|
|
820
|
+
catch (err) {
|
|
821
|
+
// Same runtime-error envelope contract as the run/race commands: --json
|
|
822
|
+
// callers get a machine-readable {ok:false} on stdout, never bare stderr.
|
|
823
|
+
if (json)
|
|
824
|
+
printJson({
|
|
825
|
+
ok: false,
|
|
826
|
+
exitCode: 1,
|
|
827
|
+
error: `claudexor spec: ${err instanceof Error ? err.message : String(err)}`,
|
|
828
|
+
});
|
|
829
|
+
else
|
|
830
|
+
process.stderr.write(`claudexor spec: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
831
|
+
return 1;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function printPreflightError(args, json, error) {
|
|
835
|
+
if (json && (args._[0] ?? "help") === "plugin") {
|
|
836
|
+
printJson(pluginCommandErrorResult(args._[1], args._[2], flagBool(args, "dry-run"), 2, error));
|
|
837
|
+
return 2;
|
|
838
|
+
}
|
|
839
|
+
return printUsageError(json, error);
|
|
840
|
+
}
|
|
841
|
+
function listCliArtifacts(root) {
|
|
842
|
+
if (!existsSync(root))
|
|
843
|
+
return [];
|
|
844
|
+
const out = [];
|
|
845
|
+
const walk = (dir) => {
|
|
846
|
+
for (const name of readdirSync(dir)) {
|
|
847
|
+
const abs = join(dir, name);
|
|
848
|
+
const rel = relative(root, abs).split(sep).join("/");
|
|
849
|
+
const st = lstatSync(abs);
|
|
850
|
+
out.push(st.isDirectory() ? `${rel}/` : rel);
|
|
851
|
+
if (st.isDirectory())
|
|
852
|
+
walk(abs);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
walk(root);
|
|
856
|
+
return out.sort();
|
|
857
|
+
}
|
|
858
|
+
// KNOWN_FLAGS / VALUE_FLAGS (imported above) and the per-command scope check
|
|
859
|
+
// are projections of the command registry. Unknown flags FAIL LOUDLY: `--harnes
|
|
860
|
+
// codex` must never silently run all harnesses.
|
|
861
|
+
async function main() {
|
|
862
|
+
const args = parseArgs(process.argv.slice(2));
|
|
863
|
+
// --version / --help are standard CLI affordances, not unknown flags.
|
|
864
|
+
if (flagBool(args, "version")) {
|
|
865
|
+
process.stdout.write(`${CLI_VERSION}\n`);
|
|
866
|
+
return 0;
|
|
867
|
+
}
|
|
868
|
+
if (flagBool(args, "help")) {
|
|
869
|
+
process.stdout.write(HELP);
|
|
870
|
+
return 0;
|
|
871
|
+
}
|
|
872
|
+
const json = flagBool(args, "json");
|
|
873
|
+
const cmd = args._[0] ?? "help";
|
|
874
|
+
const unknownFlags = Object.keys(args.flags).filter((f) => !KNOWN_FLAGS.has(f));
|
|
875
|
+
if (unknownFlags.length > 0) {
|
|
876
|
+
const error = `claudexor: unknown flag(s): ${unknownFlags.map((f) => `--${f}`).join(", ")} (see \`claudexor help\`)`;
|
|
877
|
+
return printPreflightError(args, json, error);
|
|
878
|
+
}
|
|
879
|
+
const valueFlagError = requiredStringFlagError(args, VALUE_FLAGS);
|
|
880
|
+
if (valueFlagError)
|
|
881
|
+
return printPreflightError(args, json, valueFlagError);
|
|
882
|
+
// Registry-enforced per-command flag scope: a KNOWN flag outside the
|
|
883
|
+
// command's declared set (e.g. `spec --model`, `ask --force`) fails loudly
|
|
884
|
+
// instead of being silently ignored. Data-driven from CLI_COMMANDS for
|
|
885
|
+
// every verb (this replaced the old hand-listed plugin/--force special cases).
|
|
886
|
+
const scopeError = commandFlagScopeError(cmd, Object.keys(args.flags));
|
|
887
|
+
if (scopeError)
|
|
888
|
+
return printPreflightError(args, json, scopeError);
|
|
889
|
+
// No arguments at all = the interactive REPL: a thread of turns over the
|
|
890
|
+
// current project with native session continuity (chat is the normal loop).
|
|
891
|
+
if (args._.length === 0 && process.stdin.isTTY) {
|
|
892
|
+
return runRepl(process.cwd());
|
|
893
|
+
}
|
|
894
|
+
const cwd = process.cwd();
|
|
895
|
+
switch (cmd) {
|
|
896
|
+
case "init": {
|
|
897
|
+
const res = initProjectConfig(cwd);
|
|
898
|
+
if (json)
|
|
899
|
+
printJson(res);
|
|
900
|
+
else
|
|
901
|
+
print(res.created ? `Created ${res.configPath}` : `Config already exists: ${res.configPath}`);
|
|
902
|
+
return 0;
|
|
903
|
+
}
|
|
904
|
+
case "doctor": {
|
|
905
|
+
const cfg = loadConfig(cwd);
|
|
906
|
+
const only = flagStr(args, "harness");
|
|
907
|
+
const onlyList = only
|
|
908
|
+
? only
|
|
909
|
+
.split(",")
|
|
910
|
+
.map((s) => s.trim())
|
|
911
|
+
.filter(Boolean)
|
|
912
|
+
: undefined;
|
|
913
|
+
const gateway = buildGateway({ includeFakes: flagBool(args, "all") });
|
|
914
|
+
// Scope discovery to the requested harness(es) (P14): a single-harness
|
|
915
|
+
// query no longer probes every adapter (incl. paid smokes) just to filter.
|
|
916
|
+
const statuses = await gateway.statusAll({ cwd }, onlyList);
|
|
917
|
+
// An explicit --harness typo must FAIL LOUDLY, not silently succeed over an
|
|
918
|
+
// empty list (the scoped probe returns nothing for an unknown id).
|
|
919
|
+
if (onlyList) {
|
|
920
|
+
const got = new Set(statuses.map((s) => s.id));
|
|
921
|
+
const unknown = onlyList.filter((id) => !got.has(id));
|
|
922
|
+
if (unknown.length)
|
|
923
|
+
return printUsageError(json, `claudexor: unknown harness(es): ${unknown.join(", ")} (run \`claudexor harness list --all\`)`);
|
|
924
|
+
}
|
|
925
|
+
const filtered = statuses;
|
|
926
|
+
// a configured default model that the harness does not recognize must
|
|
927
|
+
// not be silently masked by a smoke that ran a DIFFERENT model. Validate
|
|
928
|
+
// each harness's configured default against its model truth source (live
|
|
929
|
+
// inventory or manifest hints via the shared harnessModels SSOT) and
|
|
930
|
+
// surface a violation honestly as INVALID (strict model truth — no "unverified").
|
|
931
|
+
const configuredChecks = new Map();
|
|
932
|
+
await Promise.all(filtered.map(async (s) => {
|
|
933
|
+
const configured = cfg.global.harnesses[s.id]?.default_model ?? null;
|
|
934
|
+
if (!configured)
|
|
935
|
+
return;
|
|
936
|
+
const truth = await harnessModels(s.id, process.cwd(), true);
|
|
937
|
+
const check = validateModel(configured, truth.models.map((m) => m.id), truth.source === "api" ? "api" : "manifest");
|
|
938
|
+
configuredChecks.set(s.id, { configured, check });
|
|
939
|
+
}));
|
|
940
|
+
const modelNote = (s) => {
|
|
941
|
+
const entry = configuredChecks.get(s.id);
|
|
942
|
+
if (!entry || entry.check.status === "ok")
|
|
943
|
+
return null;
|
|
944
|
+
return ` model: INVALID — ${entry.check.message}`;
|
|
945
|
+
};
|
|
946
|
+
if (json) {
|
|
947
|
+
printJson({
|
|
948
|
+
harnesses: filtered.map((s) => ({
|
|
949
|
+
...s,
|
|
950
|
+
configured_model: configuredChecks.get(s.id)?.configured ?? null,
|
|
951
|
+
configured_model_check: configuredChecks.get(s.id)?.check ?? null,
|
|
952
|
+
})),
|
|
953
|
+
node_advisory: atRiskNodeAdvisory(),
|
|
954
|
+
});
|
|
955
|
+
return 0;
|
|
956
|
+
}
|
|
957
|
+
for (const s of filtered) {
|
|
958
|
+
const ver = s.manifest?.version ? ` ${s.manifest.version}` : "";
|
|
959
|
+
print(`${statusGlyph(s.status)} ${s.id}${ver}`);
|
|
960
|
+
if (s.enabledIntents.length)
|
|
961
|
+
print(` intents: ${s.enabledIntents.join(", ")}`);
|
|
962
|
+
print(` auth sources: ${authSourceAvailability(s)}`);
|
|
963
|
+
print(` checks: ${checksSummary(s)}`);
|
|
964
|
+
if (s.reasons.length)
|
|
965
|
+
print(` reasons: ${s.reasons.join(", ")}`);
|
|
966
|
+
const mn = modelNote(s);
|
|
967
|
+
if (mn)
|
|
968
|
+
print(mn);
|
|
969
|
+
}
|
|
970
|
+
const advisory = atRiskNodeAdvisory();
|
|
971
|
+
if (advisory)
|
|
972
|
+
print(`advisory: ${advisory}`);
|
|
973
|
+
return 0;
|
|
974
|
+
}
|
|
975
|
+
case "agent": {
|
|
976
|
+
const specStrategyError = "claudexor: --spec requires a gated strategy; use 'claudexor best-of --spec <file>' or 'claudexor agent --attempts N --spec <file>'";
|
|
977
|
+
// ONE gate for both spellings: `agent --spec` and `agent --mode agent --spec`
|
|
978
|
+
// must enforce the same gated-strategy requirement (a flag spelling must
|
|
979
|
+
// never bypass a policy the bare verb enforces).
|
|
980
|
+
const agentSpecGateError = () => {
|
|
981
|
+
if (!flagStr(args, "spec") || flagBool(args, "until-clean"))
|
|
982
|
+
return null;
|
|
983
|
+
try {
|
|
984
|
+
const hasGatedStrategy = intFlag(args, "attempts") !== undefined || intFlag(args, "n") !== undefined;
|
|
985
|
+
return hasGatedStrategy ? null : specStrategyError;
|
|
986
|
+
}
|
|
987
|
+
catch (err) {
|
|
988
|
+
return `claudexor: ${err instanceof Error ? err.message : String(err)}`;
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
const modeStr = flagStr(args, "mode");
|
|
992
|
+
if (modeStr !== undefined) {
|
|
993
|
+
const mode = normalizeMode(modeStr);
|
|
994
|
+
if (!MODES.has(mode)) {
|
|
995
|
+
return printUsageError(json, `claudexor: unknown --mode '${modeStr}'. valid: ${[...MODES].join(", ")}`);
|
|
996
|
+
}
|
|
997
|
+
if ((mode === "ask" || mode === "audit") && flagStr(args, "spec")) {
|
|
998
|
+
return printUsageError(json, specStrategyError);
|
|
999
|
+
}
|
|
1000
|
+
if (mode === "agent") {
|
|
1001
|
+
const gateError = agentSpecGateError();
|
|
1002
|
+
if (gateError)
|
|
1003
|
+
return printUsageError(json, gateError);
|
|
1004
|
+
}
|
|
1005
|
+
return orchestrate(args, mode, json);
|
|
1006
|
+
}
|
|
1007
|
+
const gateError = agentSpecGateError();
|
|
1008
|
+
if (gateError)
|
|
1009
|
+
return printUsageError(json, gateError);
|
|
1010
|
+
return orchestrate(args, "agent", json);
|
|
1011
|
+
}
|
|
1012
|
+
case "ask":
|
|
1013
|
+
return orchestrate(args, "ask", json);
|
|
1014
|
+
case "explore":
|
|
1015
|
+
return orchestrate(args, "audit", json, { swarm: true });
|
|
1016
|
+
case "best-of":
|
|
1017
|
+
return orchestrate(args, "agent", json, { race: true });
|
|
1018
|
+
// RENAMED verbs hard-error with the new name — no silent aliases (the
|
|
1019
|
+
// same doctrine as retired mode ids: a stale script must fail loudly).
|
|
1020
|
+
case "run":
|
|
1021
|
+
return printPreflightError(args, json, "claudexor: the 'run' verb was renamed; use 'claudexor agent' (same flags)");
|
|
1022
|
+
case "race":
|
|
1023
|
+
return printPreflightError(args, json, "claudexor: the 'race' verb was renamed; use 'claudexor best-of' (same flags)");
|
|
1024
|
+
case "orchestrate":
|
|
1025
|
+
return orchestrate(args, "orchestrate", json);
|
|
1026
|
+
case "plan":
|
|
1027
|
+
return orchestrate(args, "plan", json);
|
|
1028
|
+
case "spec":
|
|
1029
|
+
return specCommand(args, json);
|
|
1030
|
+
case "create":
|
|
1031
|
+
return orchestrate(args, "agent", json, { create: true });
|
|
1032
|
+
case "audit":
|
|
1033
|
+
case "map":
|
|
1034
|
+
return orchestrate(args, "audit", json);
|
|
1035
|
+
case "daemon":
|
|
1036
|
+
return daemonCommand(args, json);
|
|
1037
|
+
case "settings":
|
|
1038
|
+
return settingsCommand(args, json);
|
|
1039
|
+
case "trust":
|
|
1040
|
+
return trustCommand(args, json);
|
|
1041
|
+
case "auth":
|
|
1042
|
+
return authCommand(args, json);
|
|
1043
|
+
case "models":
|
|
1044
|
+
return modelsCommand(args, json);
|
|
1045
|
+
case "secrets":
|
|
1046
|
+
return secretsCommand(args, json);
|
|
1047
|
+
case "mcp": {
|
|
1048
|
+
if (args._[1] === "serve") {
|
|
1049
|
+
// SDK-owned protocol core; mutating verbs are daemon-tracked, so a
|
|
1050
|
+
// run started from an MCP host is visible/unblockable like a CLI run.
|
|
1051
|
+
serveClaudexorMcp({
|
|
1052
|
+
version: CLAUDEXOR_VERSION,
|
|
1053
|
+
tools: defaultClaudexorTools(mcpSurfaceRunner()),
|
|
1054
|
+
transport: { read: process.stdin, write: process.stdout },
|
|
1055
|
+
});
|
|
1056
|
+
// Serve until stdin closes (the SDK handle owns the transport).
|
|
1057
|
+
await new Promise((resolve) => process.stdin.once("close", resolve));
|
|
1058
|
+
return 0;
|
|
1059
|
+
}
|
|
1060
|
+
return printUsageError(json, "usage: claudexor mcp serve");
|
|
1061
|
+
}
|
|
1062
|
+
case "acp": {
|
|
1063
|
+
if (args._[1] === "serve") {
|
|
1064
|
+
await new AcpServer({
|
|
1065
|
+
version: CLAUDEXOR_VERSION,
|
|
1066
|
+
runner: orchestratorRunner(),
|
|
1067
|
+
transport: { read: process.stdin, write: process.stdout },
|
|
1068
|
+
}).serve();
|
|
1069
|
+
return 0;
|
|
1070
|
+
}
|
|
1071
|
+
return printUsageError(json, "usage: claudexor acp serve");
|
|
1072
|
+
}
|
|
1073
|
+
case "follow": {
|
|
1074
|
+
const runId = args._[1];
|
|
1075
|
+
if (!runId) {
|
|
1076
|
+
return printUsageError(json, "usage: claudexor follow <run_id>");
|
|
1077
|
+
}
|
|
1078
|
+
return followRun(runId, json);
|
|
1079
|
+
}
|
|
1080
|
+
case "review":
|
|
1081
|
+
return reviewCommand(args, json);
|
|
1082
|
+
case "inspect": {
|
|
1083
|
+
const runId = args._[1];
|
|
1084
|
+
if (!runId) {
|
|
1085
|
+
return printUsageError(json, "usage: claudexor inspect <run_id>");
|
|
1086
|
+
}
|
|
1087
|
+
// Resolve the owning store from any cwd: project store, user-level Ask
|
|
1088
|
+
// store, or a daemon-tracked run that started in another project.
|
|
1089
|
+
const resolved = await resolveRunStore(runId);
|
|
1090
|
+
if (!resolved) {
|
|
1091
|
+
if (json)
|
|
1092
|
+
printJson({ runId, error: `no such run ${runId}` });
|
|
1093
|
+
else
|
|
1094
|
+
print(`no such run ${runId}`);
|
|
1095
|
+
return 1;
|
|
1096
|
+
}
|
|
1097
|
+
const store = resolved.store;
|
|
1098
|
+
const paths = store.runPaths(runId);
|
|
1099
|
+
const decision = store.readYaml(join(paths.arbitrationDir, "decision.yaml"));
|
|
1100
|
+
const workProduct = store.readYaml(join(paths.finalDir, "work_product.yaml"));
|
|
1101
|
+
const contract = TaskContract.safeParse(store.readYaml(join(paths.contextDir, "task.yaml")));
|
|
1102
|
+
const primary = primaryOutputForCli(paths.root, contract.success ? contract.data.mode.kind : undefined);
|
|
1103
|
+
// The CLI projects the orchestrator-owned telemetry artifact and NEVER
|
|
1104
|
+
// recomputes evidence from raw events (single-owner rule); a missing
|
|
1105
|
+
// artifact (legacy run) renders "telemetry unavailable".
|
|
1106
|
+
const parsedTelemetry = RunTelemetry.safeParse(store.readYaml(join(paths.finalDir, "telemetry.yaml")));
|
|
1107
|
+
const telemetry = parsedTelemetry.success ? parsedTelemetry.data : null;
|
|
1108
|
+
const toolErrors = telemetry
|
|
1109
|
+
? telemetry.attempts.flatMap((a) => a.tool_errors
|
|
1110
|
+
.filter((e) => !e.recovered && e.kind === "web")
|
|
1111
|
+
.map((e) => ({
|
|
1112
|
+
attemptId: a.attempt_id,
|
|
1113
|
+
tool: e.tool,
|
|
1114
|
+
target: e.target ?? undefined,
|
|
1115
|
+
summary: e.summary,
|
|
1116
|
+
})))
|
|
1117
|
+
: [];
|
|
1118
|
+
const toolWarnings = telemetry
|
|
1119
|
+
? telemetry.attempts.flatMap((a) => a.tool_errors
|
|
1120
|
+
.filter((e) => !e.recovered && e.kind !== "web")
|
|
1121
|
+
.map((e) => ({
|
|
1122
|
+
attemptId: a.attempt_id,
|
|
1123
|
+
tool: e.tool,
|
|
1124
|
+
target: e.target ?? undefined,
|
|
1125
|
+
summary: e.summary,
|
|
1126
|
+
})))
|
|
1127
|
+
: [];
|
|
1128
|
+
const artifacts = listCliArtifacts(paths.root).filter((p) => !p.endsWith("/"));
|
|
1129
|
+
const outputReadyState = primary?.kind === "diagnostic"
|
|
1130
|
+
? "diagnostic"
|
|
1131
|
+
: primary?.text.trim()
|
|
1132
|
+
? "ready"
|
|
1133
|
+
: readTextSafe(join(paths.finalDir, "failure.yaml"))
|
|
1134
|
+
? "diagnostic"
|
|
1135
|
+
: "finalizing";
|
|
1136
|
+
const parsedDecision = DecisionRecord.safeParse(decision);
|
|
1137
|
+
const summary = readTextSafe(join(paths.finalDir, "summary.md"));
|
|
1138
|
+
if (json) {
|
|
1139
|
+
printJson({
|
|
1140
|
+
runId,
|
|
1141
|
+
runDir: paths.root,
|
|
1142
|
+
outputReadyState,
|
|
1143
|
+
contract: contract.success ? contract.data : null,
|
|
1144
|
+
telemetry,
|
|
1145
|
+
toolErrors,
|
|
1146
|
+
toolWarnings,
|
|
1147
|
+
primaryOutput: primary,
|
|
1148
|
+
decision,
|
|
1149
|
+
work_product: workProduct,
|
|
1150
|
+
artifacts,
|
|
1151
|
+
});
|
|
1152
|
+
// exit-code parity with the text mode: read-only runs have no decision record
|
|
1153
|
+
return summary || primary ? 0 : 1;
|
|
1154
|
+
}
|
|
1155
|
+
print(`run ${runId} @ ${paths.root}`);
|
|
1156
|
+
if (contract.success) {
|
|
1157
|
+
print(`mode: ${contract.data.mode.kind}`);
|
|
1158
|
+
print(`access: requested=${contract.data.access.requested_profile} effective=${contract.data.access.effective_profile}`);
|
|
1159
|
+
}
|
|
1160
|
+
if (telemetry) {
|
|
1161
|
+
print(`web: policy=${telemetry.external_context_policy} effective=${telemetry.effective_web_mode} required=${telemetry.web_required} evidence=${telemetry.web.status}`);
|
|
1162
|
+
}
|
|
1163
|
+
else if (contract.success) {
|
|
1164
|
+
print(`web: policy=${contract.data.external_context.policy} required=${contract.data.external_context.web_required} evidence=unavailable (no telemetry.yaml)`);
|
|
1165
|
+
}
|
|
1166
|
+
print(`output: ${outputReadyState}${primary ? ` ${primary.path}` : ""}`);
|
|
1167
|
+
if (parsedDecision.success) {
|
|
1168
|
+
const vb = parsedDecision.data.verification_basis;
|
|
1169
|
+
print(`decision: ${parsedDecision.data.status} outcome=${parsedDecision.data.outcome} apply=${parsedDecision.data.apply_recommendation}${vb !== "none" ? ` verified_by=${vb}` : ""}`);
|
|
1170
|
+
const budget = parsedDecision.data.budget_summary;
|
|
1171
|
+
print(`budget: spend=${budget.spend_usd ?? "unknown"}${budget.estimated ? " estimated" : ""}`);
|
|
1172
|
+
}
|
|
1173
|
+
if (telemetry && (telemetry.web.attempted || telemetry.web.required)) {
|
|
1174
|
+
print(`web evidence: status=${telemetry.web.status} tool=${telemetry.web.tool ?? "none"} target=${telemetry.web.target ?? "none"}${telemetry.web.error_summary ? ` error=${telemetry.web.error_summary}` : ""}`);
|
|
1175
|
+
}
|
|
1176
|
+
if (toolErrors.length) {
|
|
1177
|
+
print("tool errors (unrecovered):");
|
|
1178
|
+
for (const err of toolErrors.slice(-8))
|
|
1179
|
+
print(` - ${err.attemptId} ${err.tool}: ${err.summary}${err.target ? ` (${err.target})` : ""}`);
|
|
1180
|
+
}
|
|
1181
|
+
if (toolWarnings.length) {
|
|
1182
|
+
print("tool warnings (non-blocking):");
|
|
1183
|
+
for (const err of toolWarnings.slice(-8))
|
|
1184
|
+
print(` - ${err.attemptId} ${err.tool}: ${err.summary}${err.target ? ` (${err.target})` : ""}`);
|
|
1185
|
+
}
|
|
1186
|
+
if (primary?.text.trim()) {
|
|
1187
|
+
print("");
|
|
1188
|
+
print(primary.text.trim());
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
print(summary ?? "(no summary — run may not exist)");
|
|
1192
|
+
}
|
|
1193
|
+
if (artifacts.length) {
|
|
1194
|
+
print("");
|
|
1195
|
+
print("artifacts:");
|
|
1196
|
+
for (const a of artifacts.slice(0, 40))
|
|
1197
|
+
print(` - ${a}`);
|
|
1198
|
+
}
|
|
1199
|
+
return summary || primary ? 0 : 1;
|
|
1200
|
+
}
|
|
1201
|
+
case "apply": {
|
|
1202
|
+
const runId = args._[1];
|
|
1203
|
+
if (!runId) {
|
|
1204
|
+
return printUsageError(json, "usage: claudexor apply <run_id> [--mode apply|commit|branch|pr] [--dry-run]");
|
|
1205
|
+
}
|
|
1206
|
+
// Resolve the owning store from any cwd (project / user Ask / daemon-tracked
|
|
1207
|
+
// run in another project) before reading the patch artifact.
|
|
1208
|
+
const resolved = await resolveRunStore(runId);
|
|
1209
|
+
if (!resolved) {
|
|
1210
|
+
if (json)
|
|
1211
|
+
printJson({ runId, error: `no such run ${runId}` });
|
|
1212
|
+
else
|
|
1213
|
+
print(`no such run ${runId}`);
|
|
1214
|
+
return 1;
|
|
1215
|
+
}
|
|
1216
|
+
const store = resolved.store;
|
|
1217
|
+
const paths = store.runPaths(runId);
|
|
1218
|
+
const patch = readTextSafe(join(paths.finalDir, "patch.diff"));
|
|
1219
|
+
if (!patch || patch.trim().length === 0) {
|
|
1220
|
+
if (json)
|
|
1221
|
+
printJson({ runId, error: `no patch found for run ${runId}` });
|
|
1222
|
+
else
|
|
1223
|
+
print(`no patch found for run ${runId}`);
|
|
1224
|
+
return 1;
|
|
1225
|
+
}
|
|
1226
|
+
if (containsSecretLikeToken(patch)) {
|
|
1227
|
+
if (json)
|
|
1228
|
+
printJson({ runId, error: "patch contains secret-like token; refusing apply" });
|
|
1229
|
+
else
|
|
1230
|
+
print("patch contains secret-like token; refusing apply");
|
|
1231
|
+
return 1;
|
|
1232
|
+
}
|
|
1233
|
+
// Apply policy has ONE owner (delivery.validateApplyGate) shared with the
|
|
1234
|
+
// Control API; the CLI only adapts artifact reads into it. No duplicated
|
|
1235
|
+
// pre-checks here: a NEEDS_HUMAN run unblocked through the typed decision
|
|
1236
|
+
// endpoint (arbitration/operator_decision.yaml, hash-bound) must apply
|
|
1237
|
+
// identically from BOTH surfaces.
|
|
1238
|
+
const applyDecision = DecisionRecord.safeParse(store.readYaml(join(paths.arbitrationDir, "decision.yaml")));
|
|
1239
|
+
const workProduct = WorkProduct.safeParse(store.readYaml(join(paths.finalDir, "work_product.yaml")));
|
|
1240
|
+
const contract = TaskContract.safeParse(store.readYaml(join(paths.contextDir, "task.yaml")));
|
|
1241
|
+
const operatorDecisionRaw = store.readYaml(join(paths.arbitrationDir, "operator_decision.yaml"));
|
|
1242
|
+
const operatorDecision = operatorDecisionRaw && typeof operatorDecisionRaw["action"] === "string"
|
|
1243
|
+
? {
|
|
1244
|
+
action: operatorDecisionRaw["action"],
|
|
1245
|
+
patch_sha256: typeof operatorDecisionRaw["patch_sha256"] === "string"
|
|
1246
|
+
? operatorDecisionRaw["patch_sha256"]
|
|
1247
|
+
: undefined,
|
|
1248
|
+
}
|
|
1249
|
+
: null;
|
|
1250
|
+
// The default apply target is the run's ORIGINAL project (from its contract),
|
|
1251
|
+
// not the current working directory — so a daemon-tracked run resolved via
|
|
1252
|
+
// the registry applies correctly from any cwd (namespace unification).
|
|
1253
|
+
// Fall back to cwd only for a legacy run with no readable contract.
|
|
1254
|
+
const applyRoot = contract.success ? contract.data.repo.root : process.cwd();
|
|
1255
|
+
// Artifact-only path: the live daemon job state is unavailable, but the
|
|
1256
|
+
// orchestrator records the terminal run status in work_product.meta.status.
|
|
1257
|
+
// Feed it (mapped to daemon-state vocab) into the shared gate so the CLI
|
|
1258
|
+
// enforces the SAME terminal-state bar the Control API does — e.g. a
|
|
1259
|
+
// convergence run that persists decision.status=success but terminal
|
|
1260
|
+
// not_converged (stale diff after a required review) is refused identically.
|
|
1261
|
+
const recordedStatus = workProduct.success
|
|
1262
|
+
? workProduct.data.meta?.["status"]
|
|
1263
|
+
: undefined;
|
|
1264
|
+
const recordedState = recordedStatus
|
|
1265
|
+
? recordedStatus === "success"
|
|
1266
|
+
? "succeeded"
|
|
1267
|
+
: recordedStatus
|
|
1268
|
+
: null;
|
|
1269
|
+
const gateError = validateApplyGate({
|
|
1270
|
+
state: recordedState,
|
|
1271
|
+
decision: applyDecision.success ? applyDecision.data : null,
|
|
1272
|
+
workProduct: workProduct.success ? workProduct.data : null,
|
|
1273
|
+
patch,
|
|
1274
|
+
originalRepoRoot: contract.success ? contract.data.repo.root : null,
|
|
1275
|
+
targetRepoRoot: applyRoot,
|
|
1276
|
+
operatorDecision,
|
|
1277
|
+
});
|
|
1278
|
+
if (gateError) {
|
|
1279
|
+
if (json)
|
|
1280
|
+
printJson({ runId, error: gateError });
|
|
1281
|
+
else
|
|
1282
|
+
print(gateError);
|
|
1283
|
+
return 1;
|
|
1284
|
+
}
|
|
1285
|
+
if (flagBool(args, "dry-run")) {
|
|
1286
|
+
const r = await checkPatch(applyRoot, patch);
|
|
1287
|
+
if (json)
|
|
1288
|
+
printJson({
|
|
1289
|
+
runId,
|
|
1290
|
+
dryRun: true,
|
|
1291
|
+
applies: r.ok,
|
|
1292
|
+
...(r.ok ? {} : { error: r.stderr.trim() }),
|
|
1293
|
+
});
|
|
1294
|
+
else
|
|
1295
|
+
print(r.ok ? "patch applies cleanly" : `patch does not apply: ${r.stderr.trim()}`);
|
|
1296
|
+
return r.ok ? 0 : 1;
|
|
1297
|
+
}
|
|
1298
|
+
const rawMode = flagStr(args, "mode") ?? "apply";
|
|
1299
|
+
if (!DELIVER_MODES.has(rawMode)) {
|
|
1300
|
+
if (json)
|
|
1301
|
+
printJson({ runId, error: `unsupported apply mode: ${rawMode}` });
|
|
1302
|
+
else
|
|
1303
|
+
print(`unsupported apply mode: ${rawMode}`);
|
|
1304
|
+
return 2;
|
|
1305
|
+
}
|
|
1306
|
+
// `apply` mutates the tree; `artifact_only` produces no mutation and the
|
|
1307
|
+
// patch artifact was already emitted by the run — reject it here (it stays
|
|
1308
|
+
// valid on the control-api for clients that want a dry materialization).
|
|
1309
|
+
if (rawMode === "artifact_only") {
|
|
1310
|
+
const msg = "apply --mode artifact_only is a no-op (the patch artifact already exists at <runDir>/final/patch.diff); use apply|branch|commit|pr to mutate, or read the artifact directly";
|
|
1311
|
+
if (json)
|
|
1312
|
+
printJson({ runId, error: msg });
|
|
1313
|
+
else
|
|
1314
|
+
print(msg);
|
|
1315
|
+
return 2;
|
|
1316
|
+
}
|
|
1317
|
+
const mode = rawMode;
|
|
1318
|
+
const res = await deliver(applyRoot, patch, { mode, message: `claudexor: apply ${runId}` });
|
|
1319
|
+
if (json)
|
|
1320
|
+
printJson(res);
|
|
1321
|
+
else
|
|
1322
|
+
print(`${res.mode}: applied=${res.applied}` +
|
|
1323
|
+
(res.commit ? ` commit=${res.commit.slice(0, 8)}` : "") +
|
|
1324
|
+
(res.branch ? ` branch=${res.branch}` : "") +
|
|
1325
|
+
(res.detail ? ` (${res.detail})` : ""));
|
|
1326
|
+
return res.applied ? 0 : 1;
|
|
1327
|
+
}
|
|
1328
|
+
case "decision":
|
|
1329
|
+
return decisionCommand(args, json);
|
|
1330
|
+
case "release": {
|
|
1331
|
+
if (args._[1] === "check-name") {
|
|
1332
|
+
const name = args._[2] ?? "claudexor";
|
|
1333
|
+
const checks = await checkName(name);
|
|
1334
|
+
if (json)
|
|
1335
|
+
printJson({ name, checks });
|
|
1336
|
+
else {
|
|
1337
|
+
print(`naming gate for "${name}":`);
|
|
1338
|
+
for (const c of checks) {
|
|
1339
|
+
const tag = c.availability === "free"
|
|
1340
|
+
? "[free] "
|
|
1341
|
+
: c.availability === "taken"
|
|
1342
|
+
? "[taken] "
|
|
1343
|
+
: "[unknown]";
|
|
1344
|
+
print(` ${tag} ${c.registry}: ${c.detail}`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return 0;
|
|
1348
|
+
}
|
|
1349
|
+
return printUsageError(json, "usage: claudexor release check-name <name>");
|
|
1350
|
+
}
|
|
1351
|
+
case "plugin": {
|
|
1352
|
+
const sub = args._[1];
|
|
1353
|
+
const target = args._[2];
|
|
1354
|
+
const dryRun = flagBool(args, "dry-run");
|
|
1355
|
+
if (!sub || !PLUGIN_VERBS.includes(sub)) {
|
|
1356
|
+
const error = "usage: claudexor plugin <install|status|doctor|repair|uninstall> <cursor|claude|codex|opencode|all> [--dry-run] [--force] [--json]";
|
|
1357
|
+
if (json)
|
|
1358
|
+
printJson(pluginCommandErrorResult(sub, target, dryRun, 2, error));
|
|
1359
|
+
else
|
|
1360
|
+
print(error);
|
|
1361
|
+
return 2;
|
|
1362
|
+
}
|
|
1363
|
+
if (!target || !PLUGIN_TARGETS.includes(target)) {
|
|
1364
|
+
const error = `claudexor: unknown plugin target '${target ?? ""}' (expected ${PLUGIN_TARGETS.join("|")})`;
|
|
1365
|
+
if (json)
|
|
1366
|
+
printJson(pluginCommandErrorResult(sub, target, dryRun, 2, error));
|
|
1367
|
+
else
|
|
1368
|
+
process.stderr.write(`${error}\n`);
|
|
1369
|
+
return 2;
|
|
1370
|
+
}
|
|
1371
|
+
if (args._.length > 3) {
|
|
1372
|
+
const error = `claudexor: unexpected plugin argument(s): ${args._.slice(3).join(" ")}`;
|
|
1373
|
+
if (json)
|
|
1374
|
+
printJson(pluginCommandErrorResult(sub, target, dryRun, 2, error));
|
|
1375
|
+
else
|
|
1376
|
+
process.stderr.write(`${error}\n`);
|
|
1377
|
+
return 2;
|
|
1378
|
+
}
|
|
1379
|
+
try {
|
|
1380
|
+
const r = await runPluginCommand(sub, target, {
|
|
1381
|
+
dryRun,
|
|
1382
|
+
force: flagBool(args, "force"),
|
|
1383
|
+
json,
|
|
1384
|
+
});
|
|
1385
|
+
if (json)
|
|
1386
|
+
printJson(r);
|
|
1387
|
+
else
|
|
1388
|
+
print(formatPluginResult(r));
|
|
1389
|
+
return r.exitCode;
|
|
1390
|
+
}
|
|
1391
|
+
catch (err) {
|
|
1392
|
+
if (json) {
|
|
1393
|
+
printJson(pluginCommandErrorResult(sub, target, dryRun, 1, err instanceof Error ? err.message : String(err)));
|
|
1394
|
+
return 1;
|
|
1395
|
+
}
|
|
1396
|
+
throw err;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
case "harness": {
|
|
1400
|
+
const sub = args._[1];
|
|
1401
|
+
if (sub === "list") {
|
|
1402
|
+
// Fakes are test fixtures, not real harnesses; `--all` reveals them.
|
|
1403
|
+
const includeFakes = flagBool(args, "all");
|
|
1404
|
+
const ids = [...buildRegistry({ includeFakes }).keys()];
|
|
1405
|
+
if (json)
|
|
1406
|
+
printJson({ harnesses: ids });
|
|
1407
|
+
else
|
|
1408
|
+
ids.forEach((id) => print(id));
|
|
1409
|
+
return 0;
|
|
1410
|
+
}
|
|
1411
|
+
return printUsageError(json, "usage: claudexor harness list [--all]");
|
|
1412
|
+
}
|
|
1413
|
+
case "capabilities": {
|
|
1414
|
+
// The derived AgentCapabilityCatalog — same composer as the daemon's
|
|
1415
|
+
// GET /agent-capabilities and the MCP claudexor_capabilities tool.
|
|
1416
|
+
const catalog = await buildAgentCapabilityCatalog();
|
|
1417
|
+
if (json)
|
|
1418
|
+
printJson(catalog);
|
|
1419
|
+
else {
|
|
1420
|
+
print(`claudexor ${catalog.version} — capability catalog`);
|
|
1421
|
+
print(`modes: ${catalog.modes.join(", ")}`);
|
|
1422
|
+
print(`available harnesses: ${catalog.availableHarnesses.join(", ") || "(none)"}`);
|
|
1423
|
+
for (const h of catalog.harnesses) {
|
|
1424
|
+
const model = h.configuredModel ? ` model=${h.configuredModel}${h.configuredModelValid === false ? " (REJECTED)" : ""}` : "";
|
|
1425
|
+
print(` ${statusGlyph(h.status)} ${h.id}: ${h.status}; intents=${h.enabledIntents.join(",") || "-"}; models=${h.models.count} (${h.models.source})${model}`);
|
|
1426
|
+
}
|
|
1427
|
+
print(`mcp tools: ${catalog.mcpTools.join(", ")}`);
|
|
1428
|
+
print(`run-control keys: ${catalog.runControlKeys.join(", ")}`);
|
|
1429
|
+
print(`full JSON: claudexor capabilities --json (or GET /agent-capabilities on the daemon)`);
|
|
1430
|
+
}
|
|
1431
|
+
return 0;
|
|
1432
|
+
}
|
|
1433
|
+
case "help":
|
|
1434
|
+
// `help --json` is the machine-readable command catalog (agents parse
|
|
1435
|
+
// it instead of scraping the text help).
|
|
1436
|
+
if (json)
|
|
1437
|
+
printJson(helpJson(CLI_VERSION));
|
|
1438
|
+
else
|
|
1439
|
+
print(HELP);
|
|
1440
|
+
return 0;
|
|
1441
|
+
default:
|
|
1442
|
+
// Unknown command is an ERROR (exit 2), not a silent help print with
|
|
1443
|
+
// exit 0 — scripts must not mistake a typo'd verb for success. --json
|
|
1444
|
+
// callers get the machine envelope on stdout (stdout purity contract).
|
|
1445
|
+
if (json) {
|
|
1446
|
+
printJson({ ok: false, exitCode: 2, error: `claudexor: unknown command '${cmd}' (see \`claudexor help --json\`)` });
|
|
1447
|
+
return 2;
|
|
1448
|
+
}
|
|
1449
|
+
process.stderr.write(`claudexor: unknown command '${cmd}'\n\n${HELP}\n`);
|
|
1450
|
+
return 2;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
main()
|
|
1454
|
+
.then((code) => process.exit(code))
|
|
1455
|
+
.catch((err) => {
|
|
1456
|
+
process.stderr.write(`claudexor: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1457
|
+
process.exit(1);
|
|
1458
|
+
});
|
|
1459
|
+
//# sourceMappingURL=cli.js.map
|