@claudexor/cli 1.0.1 → 2.1.3
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/dist/acp-surface-runner.d.ts +17 -0
- package/dist/acp-surface-runner.d.ts.map +1 -0
- package/dist/acp-surface-runner.js +187 -0
- package/dist/acp-surface-runner.js.map +1 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js.map +1 -1
- package/dist/bridge-serve.d.ts +3 -0
- package/dist/bridge-serve.d.ts.map +1 -0
- package/dist/bridge-serve.js +39 -0
- package/dist/bridge-serve.js.map +1 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +12 -3
- package/dist/capabilities.js.map +1 -1
- package/dist/claude-oauth-usage.d.ts +33 -0
- package/dist/claude-oauth-usage.d.ts.map +1 -0
- package/dist/claude-oauth-usage.js +190 -0
- package/dist/claude-oauth-usage.js.map +1 -0
- package/dist/claude-statusline.d.ts +25 -0
- package/dist/claude-statusline.d.ts.map +1 -0
- package/dist/claude-statusline.js +259 -0
- package/dist/claude-statusline.js.map +1 -0
- package/dist/claudexord.js +346 -543
- package/dist/claudexord.js.map +1 -1
- package/dist/cli-io.d.ts +8 -9
- package/dist/cli-io.d.ts.map +1 -1
- package/dist/cli-io.js +11 -7
- package/dist/cli-io.js.map +1 -1
- package/dist/cli.js +475 -516
- package/dist/cli.js.map +1 -1
- package/dist/codex-quota-source.d.ts +11 -0
- package/dist/codex-quota-source.d.ts.map +1 -0
- package/dist/codex-quota-source.js +181 -0
- package/dist/codex-quota-source.js.map +1 -0
- package/dist/command-flags.d.ts +18 -0
- package/dist/command-flags.d.ts.map +1 -0
- package/dist/command-flags.js +124 -0
- package/dist/command-flags.js.map +1 -0
- package/dist/command-registry.d.ts +2 -40
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +74 -164
- package/dist/command-registry.js.map +1 -1
- package/dist/control-services.d.ts +2490 -0
- package/dist/control-services.d.ts.map +1 -0
- package/dist/control-services.js +453 -0
- package/dist/control-services.js.map +1 -0
- package/dist/credential-commands.d.ts +9 -0
- package/dist/credential-commands.d.ts.map +1 -0
- package/dist/credential-commands.js +224 -0
- package/dist/credential-commands.js.map +1 -0
- package/dist/daemon-lifecycle.d.ts +7 -3
- package/dist/daemon-lifecycle.d.ts.map +1 -1
- package/dist/daemon-lifecycle.js +40 -12
- package/dist/daemon-lifecycle.js.map +1 -1
- package/dist/daemon-run.d.ts +2 -0
- package/dist/daemon-run.d.ts.map +1 -1
- package/dist/daemon-run.js +62 -14
- package/dist/daemon-run.js.map +1 -1
- package/dist/daemon-runtime-shutdown.d.ts +58 -0
- package/dist/daemon-runtime-shutdown.d.ts.map +1 -0
- package/dist/daemon-runtime-shutdown.js +142 -0
- package/dist/daemon-runtime-shutdown.js.map +1 -0
- package/dist/decision.d.ts.map +1 -1
- package/dist/decision.js +12 -3
- package/dist/decision.js.map +1 -1
- package/dist/live.d.ts +29 -0
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +117 -6
- package/dist/live.js.map +1 -1
- package/dist/local-attachment.d.ts +14 -0
- package/dist/local-attachment.d.ts.map +1 -0
- package/dist/local-attachment.js +67 -0
- package/dist/local-attachment.js.map +1 -0
- package/dist/mcp-runner.d.ts +4 -15
- package/dist/mcp-runner.d.ts.map +1 -1
- package/dist/mcp-runner.js +276 -145
- package/dist/mcp-runner.js.map +1 -1
- package/dist/native-login.d.ts +21 -0
- package/dist/native-login.d.ts.map +1 -0
- package/dist/native-login.js +110 -0
- package/dist/native-login.js.map +1 -0
- package/dist/ops-commands.d.ts +8 -6
- package/dist/ops-commands.d.ts.map +1 -1
- package/dist/ops-commands.js +250 -168
- package/dist/ops-commands.js.map +1 -1
- package/dist/orchestrate-options.d.ts.map +1 -1
- package/dist/orchestrate-options.js +1 -1
- package/dist/orchestrate-options.js.map +1 -1
- package/dist/orphan-reaper.d.ts +4 -5
- package/dist/orphan-reaper.d.ts.map +1 -1
- package/dist/orphan-reaper.js +29 -56
- package/dist/orphan-reaper.js.map +1 -1
- package/dist/orphan-sweeper.d.ts +1 -2
- package/dist/orphan-sweeper.d.ts.map +1 -1
- package/dist/orphan-sweeper.js +29 -48
- package/dist/orphan-sweeper.js.map +1 -1
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +196 -64
- package/dist/plugins.js.map +1 -1
- package/dist/primary-output.d.ts.map +1 -1
- package/dist/primary-output.js.map +1 -1
- package/dist/profile-compatibility.d.ts +6 -0
- package/dist/profile-compatibility.d.ts.map +1 -0
- package/dist/profile-compatibility.js +21 -0
- package/dist/profile-compatibility.js.map +1 -0
- package/dist/profile-registration.d.ts +15 -0
- package/dist/profile-registration.d.ts.map +1 -0
- package/dist/profile-registration.js +68 -0
- package/dist/profile-registration.js.map +1 -0
- package/dist/project-command.d.ts +3 -0
- package/dist/project-command.d.ts.map +1 -0
- package/dist/project-command.js +87 -0
- package/dist/project-command.js.map +1 -0
- package/dist/quota-command.d.ts +3 -0
- package/dist/quota-command.d.ts.map +1 -0
- package/dist/quota-command.js +59 -0
- package/dist/quota-command.js.map +1 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +16 -4
- package/dist/registry.js.map +1 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +15 -3
- package/dist/release.js.map +1 -1
- package/dist/repl.d.ts +3 -8
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +23 -122
- package/dist/repl.js.map +1 -1
- package/dist/request-preflight.d.ts +13 -0
- package/dist/request-preflight.d.ts.map +1 -0
- package/dist/request-preflight.js +81 -0
- package/dist/request-preflight.js.map +1 -0
- package/dist/retention-service.d.ts +24 -0
- package/dist/retention-service.d.ts.map +1 -0
- package/dist/retention-service.js +100 -0
- package/dist/retention-service.js.map +1 -0
- package/dist/retry-command-specs.d.ts +3 -0
- package/dist/retry-command-specs.d.ts.map +1 -0
- package/dist/retry-command-specs.js +21 -0
- package/dist/retry-command-specs.js.map +1 -0
- package/dist/retry-command.d.ts +4 -0
- package/dist/retry-command.d.ts.map +1 -0
- package/dist/retry-command.js +57 -0
- package/dist/retry-command.js.map +1 -0
- package/dist/review-command.d.ts.map +1 -1
- package/dist/review-command.js +47 -12
- package/dist/review-command.js.map +1 -1
- package/dist/run-options.d.ts +2 -2
- package/dist/run-options.d.ts.map +1 -1
- package/dist/run-options.js +27 -5
- package/dist/run-options.js.map +1 -1
- package/dist/settings-command.d.ts.map +1 -1
- package/dist/settings-command.js +143 -188
- package/dist/settings-command.js.map +1 -1
- package/dist/settings-service.d.ts +50 -0
- package/dist/settings-service.d.ts.map +1 -1
- package/dist/settings-service.js +67 -1
- package/dist/settings-service.js.map +1 -1
- package/dist/setup-job-reducer.d.ts +9 -0
- package/dist/setup-job-reducer.d.ts.map +1 -0
- package/dist/setup-job-reducer.js +290 -0
- package/dist/setup-job-reducer.js.map +1 -0
- package/dist/setup-job-store.d.ts +250 -0
- package/dist/setup-job-store.d.ts.map +1 -0
- package/dist/setup-job-store.js +359 -0
- package/dist/setup-job-store.js.map +1 -0
- package/dist/setup-job-support.d.ts +27 -0
- package/dist/setup-job-support.d.ts.map +1 -0
- package/dist/setup-job-support.js +141 -0
- package/dist/setup-job-support.js.map +1 -0
- package/dist/setup-jobs.d.ts +437 -22
- package/dist/setup-jobs.d.ts.map +1 -1
- package/dist/setup-jobs.js +813 -423
- package/dist/setup-jobs.js.map +1 -1
- package/dist/setup-lifecycle-binding.d.ts +40 -0
- package/dist/setup-lifecycle-binding.d.ts.map +1 -0
- package/dist/setup-lifecycle-binding.js +159 -0
- package/dist/setup-lifecycle-binding.js.map +1 -0
- package/dist/setup-login-protocol.d.ts +13 -0
- package/dist/setup-login-protocol.d.ts.map +1 -0
- package/dist/setup-login-protocol.js +179 -0
- package/dist/setup-login-protocol.js.map +1 -0
- package/dist/setup-login-runner.d.ts +20 -0
- package/dist/setup-login-runner.d.ts.map +1 -0
- package/dist/setup-login-runner.js +225 -0
- package/dist/setup-login-runner.js.map +1 -0
- package/dist/setup-supervisor.d.ts +53 -0
- package/dist/setup-supervisor.d.ts.map +1 -0
- package/dist/setup-supervisor.js +177 -0
- package/dist/setup-supervisor.js.map +1 -0
- package/dist/spec-thread-scope.d.ts +3 -0
- package/dist/spec-thread-scope.d.ts.map +1 -0
- package/dist/spec-thread-scope.js +9 -0
- package/dist/spec-thread-scope.js.map +1 -0
- package/dist/spec.d.ts +9 -20
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +49 -21
- package/dist/spec.js.map +1 -1
- package/dist/thread-delivery.d.ts +18 -0
- package/dist/thread-delivery.d.ts.map +1 -0
- package/dist/thread-delivery.js +67 -0
- package/dist/thread-delivery.js.map +1 -0
- package/dist/thread-select.d.ts +9 -0
- package/dist/thread-select.d.ts.map +1 -0
- package/dist/thread-select.js +12 -0
- package/dist/thread-select.js.map +1 -0
- package/dist/trust-command.d.ts.map +1 -1
- package/dist/trust-command.js +69 -61
- package/dist/trust-command.js.map +1 -1
- package/dist/trust-services.d.ts +5 -6
- package/dist/trust-services.d.ts.map +1 -1
- package/dist/trust-services.js +42 -13
- package/dist/trust-services.js.map +1 -1
- package/package.json +24 -22
- package/dist/attachment-resolver.d.ts +0 -11
- package/dist/attachment-resolver.d.ts.map +0 -1
- package/dist/attachment-resolver.js +0 -61
- package/dist/attachment-resolver.js.map +0 -1
- package/dist/settings-display.d.ts +0 -3
- package/dist/settings-display.d.ts.map +0 -1
- package/dist/settings-display.js +0 -33
- package/dist/settings-display.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,45 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from "node:process";
|
|
3
|
-
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { createReadStream, existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { Readable, Transform } from "node:stream";
|
|
6
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
7
|
import { ArtifactStore } from "@claudexor/artifact-store";
|
|
7
|
-
import {
|
|
8
|
-
import { CLAUDEXOR_VERSION, containsSecretLikeToken, ensureDir, newId, noProjectRepoRoot, readTextSafe, userConfigDir, writeJson, } from "@claudexor/util";
|
|
8
|
+
import { CLAUDEXOR_VERSION, containsSecretLikeToken, noProjectRepoRoot, readTextSafe, userConfigDir, } from "@claudexor/util";
|
|
9
9
|
import { checkName } from "./release.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { atRiskNodeAdvisory, validateModel } from "@claudexor/core";
|
|
14
|
-
import { DecisionRecord, EffortHint, ExternalContextPolicy, ModeKind as ModeKindSchema, Portfolio, RunTelemetry, TaskContract, WorkProduct, } from "@claudexor/schema";
|
|
10
|
+
import { serveAcpBridge, serveMcpBridge } from "./bridge-serve.js";
|
|
11
|
+
import { initProjectConfig } from "@claudexor/config";
|
|
12
|
+
import { DecisionRecord, ControlSpecSession, EffortHint, ExternalContextPolicy, ModeKind as ModeKindSchema, RoutingGoal, ControlThreadListResponse, RunTelemetry, StructuredOutputConformance, TaskContract, } from "@claudexor/schema";
|
|
15
13
|
import { flagBool, flagStr, flagStringList, flagValues, parseArgs, requiredStringFlagError, } from "./args.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
14
|
+
import { print, printJson, printJsonLine, printUsageError, statusGlyph } from "./cli-io.js";
|
|
15
|
+
import { pickResumableThread } from "./thread-select.js";
|
|
16
|
+
import { KNOWN_FLAGS, VALUE_FLAGS, commandFlagScopeError, helpJson, renderHelp, } from "./command-registry.js";
|
|
18
17
|
import { buildAgentCapabilityCatalog } from "./capabilities.js";
|
|
19
|
-
import {
|
|
18
|
+
import { dispatchOpsCommand } from "./ops-commands.js";
|
|
20
19
|
import { reviewCommand } from "./review-command.js";
|
|
21
|
-
import {
|
|
20
|
+
import { controlApiFetch, followRun } from "./live.js";
|
|
21
|
+
import { retryCommand, runAgainCommand } from "./retry-command.js";
|
|
22
22
|
import { assertCliRunParamsHaveNoInlineSecrets } from "./run-secret-scan.js";
|
|
23
|
-
import {
|
|
23
|
+
import { openLocalAttachment, resolveLocalAttachment, } from "./local-attachment.js";
|
|
24
|
+
import { connectDaemonIfRunning, daemonOutcomeSummary, ensureDaemon, enqueueAndAwait, exitCodeForState, fetchApplyEligibility, runStatusForCli, } from "./daemon-run.js";
|
|
24
25
|
import { resolveDecisionBody } from "./decision.js";
|
|
25
26
|
import { primaryOutputForCli } from "./primary-output.js";
|
|
26
27
|
import { PLUGIN_TARGETS, PLUGIN_VERBS, formatPluginResult, pluginCommandErrorResult, runPluginCommand, } from "./plugins.js";
|
|
27
|
-
import {
|
|
28
|
-
import { mcpSurfaceRunner, orchestratorRunner } from "./mcp-runner.js";
|
|
28
|
+
import { buildRegistry } from "./registry.js";
|
|
29
29
|
import { settingsCommand } from "./settings-command.js";
|
|
30
|
+
import { quotaCommand } from "./quota-command.js";
|
|
30
31
|
import { trustCommand } from "./trust-command.js";
|
|
31
|
-
import {
|
|
32
|
+
import { projectCommand } from "./project-command.js";
|
|
33
|
+
import { loadFrozenSpec, readAnswers, resolveRunTestCommands } from "./spec.js";
|
|
32
34
|
import { parseAutonomy } from "./orchestrate-options.js";
|
|
33
35
|
import { runRepl } from "./repl.js";
|
|
34
36
|
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
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
38
|
const HELP = renderHelp(CLI_VERSION);
|
|
41
39
|
const MODES = new Set(["ask", "plan", "audit", "agent", "orchestrate"]);
|
|
42
|
-
/** Canonical mode ids are single words; trim and validate against the schema. */
|
|
43
40
|
function normalizeMode(s) {
|
|
44
41
|
const trimmed = s.trim();
|
|
45
42
|
const parsed = ModeKindSchema.safeParse(trimmed);
|
|
@@ -71,7 +68,7 @@ function floatFlag(args, key) {
|
|
|
71
68
|
throw new Error(`invalid --${key} '${v}' (expected a non-negative number)`);
|
|
72
69
|
return n;
|
|
73
70
|
}
|
|
74
|
-
/** Deterministic
|
|
71
|
+
/** Deterministic typed-argv gates from repeated `--test '["program","arg"]'`. */
|
|
75
72
|
function testCommands(args) {
|
|
76
73
|
return parseTestCommandFlags(flagValues(args, "test"));
|
|
77
74
|
}
|
|
@@ -79,6 +76,27 @@ function testCommands(args) {
|
|
|
79
76
|
function protectedPathApprovals(args) {
|
|
80
77
|
return parseProtectedPathApprovalFlags(flagValues(args, "allow-protected-path"));
|
|
81
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Per-run system instructions from `--instructions "<text>"` or
|
|
81
|
+
* `--instructions-file <path>` (mutually exclusive; the file form avoids
|
|
82
|
+
* ARG_MAX and keeps long instructions out of the process argv / `ps`).
|
|
83
|
+
*/
|
|
84
|
+
function resolveInstructions(args) {
|
|
85
|
+
const inline = flagStr(args, "instructions");
|
|
86
|
+
const file = flagStr(args, "instructions-file");
|
|
87
|
+
if (inline !== undefined && file !== undefined) {
|
|
88
|
+
throw new Error("pass either --instructions or --instructions-file, not both");
|
|
89
|
+
}
|
|
90
|
+
if (file !== undefined) {
|
|
91
|
+
try {
|
|
92
|
+
return readFileSync(file, "utf8");
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
throw new Error(`could not read --instructions-file ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return inline;
|
|
99
|
+
}
|
|
82
100
|
const ACCESS_PROFILES = new Set([
|
|
83
101
|
"readonly",
|
|
84
102
|
"workspace_write",
|
|
@@ -134,45 +152,58 @@ function attachmentPaths(args) {
|
|
|
134
152
|
}
|
|
135
153
|
return values;
|
|
136
154
|
}
|
|
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
155
|
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
|
-
});
|
|
156
|
+
const out = attachmentPaths(args).map(({ path, forceImage }) => resolveLocalAttachment(path, forceImage));
|
|
166
157
|
return out.length > 0 ? out : undefined;
|
|
167
158
|
}
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
159
|
+
async function uploadLocalAttachment(addr, attachment) {
|
|
160
|
+
const created = (await controlJson(addr, "/uploads", {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "content-type": "application/json" },
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
kind: attachment.kind,
|
|
165
|
+
mime: attachment.mime,
|
|
166
|
+
name: attachment.name,
|
|
167
|
+
sizeBytes: attachment.sizeBytes,
|
|
168
|
+
}),
|
|
175
169
|
}));
|
|
170
|
+
try {
|
|
171
|
+
const source = createReadStream(attachment.path, {
|
|
172
|
+
fd: openLocalAttachment(attachment),
|
|
173
|
+
autoClose: true,
|
|
174
|
+
});
|
|
175
|
+
const hash = createHash("sha256");
|
|
176
|
+
const hashingStream = new Transform({
|
|
177
|
+
transform(chunk, _encoding, callback) {
|
|
178
|
+
hash.update(chunk);
|
|
179
|
+
callback(null, chunk);
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
source.pipe(hashingStream);
|
|
183
|
+
const response = await controlApiFetch(addr, `/uploads/${encodeURIComponent(created.uploadId)}/bytes`, {
|
|
184
|
+
method: "PUT",
|
|
185
|
+
headers: { "content-type": "application/octet-stream" },
|
|
186
|
+
body: Readable.toWeb(hashingStream),
|
|
187
|
+
duplex: "half",
|
|
188
|
+
});
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
const detail = (await response.json().catch(() => ({})));
|
|
191
|
+
throw new Error(String(detail["message"] ?? detail["error"] ?? `HTTP ${response.status}`));
|
|
192
|
+
}
|
|
193
|
+
const expectedSha256 = `sha256:${hash.digest("hex")}`;
|
|
194
|
+
const resource = (await controlJson(addr, `/uploads/${encodeURIComponent(created.uploadId)}/finalize`, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: { "content-type": "application/json" },
|
|
197
|
+
body: JSON.stringify({ expectedSha256 }),
|
|
198
|
+
}));
|
|
199
|
+
return { resourceId: resource.resourceId };
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
await controlApiFetch(addr, `/uploads/${encodeURIComponent(created.uploadId)}`, {
|
|
203
|
+
method: "DELETE",
|
|
204
|
+
}).catch(() => undefined);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
176
207
|
}
|
|
177
208
|
/** Per-family reviewer model map from `--reviewer-model "openai=gpt-4o-mini,anthropic=claude-haiku"`. Fails loudly on malformed input. */
|
|
178
209
|
function reviewerModels(args) {
|
|
@@ -187,7 +218,28 @@ function reviewerPanel(args) {
|
|
|
187
218
|
return parseReviewerPanelFlags(flagValues(args, "reviewer-panel"));
|
|
188
219
|
}
|
|
189
220
|
async function orchestrate(args, mode, json, forced = {}) {
|
|
190
|
-
|
|
221
|
+
let rawPrompt = args._.slice(1).join(" ").trim();
|
|
222
|
+
// Headless prompt sources (W13): `-` reads the prompt from stdin; a file
|
|
223
|
+
// beats retyping. Exactly ONE source — ambiguity is a usage error, never a
|
|
224
|
+
// silent pick.
|
|
225
|
+
const promptFile = flagStr(args, "prompt-file");
|
|
226
|
+
if (promptFile !== undefined) {
|
|
227
|
+
if (rawPrompt && rawPrompt !== "-") {
|
|
228
|
+
return printUsageError(json, "claudexor: pass either an inline prompt or --prompt-file, not both");
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
rawPrompt = readFileSync(promptFile, "utf8").trim();
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
return printUsageError(json, `claudexor: --prompt-file: cannot read ${promptFile}: ${err instanceof Error ? err.message : String(err)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (rawPrompt === "-") {
|
|
238
|
+
rawPrompt = readFileSync(0, "utf8").trim();
|
|
239
|
+
if (!rawPrompt) {
|
|
240
|
+
return printUsageError(json, "claudexor: stdin prompt (`-`) was empty");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
191
243
|
const specPath = flagStr(args, "spec");
|
|
192
244
|
let loadedSpec = null;
|
|
193
245
|
try {
|
|
@@ -229,9 +281,14 @@ async function orchestrate(args, mode, json, forced = {}) {
|
|
|
229
281
|
return printUsageError(json, "claudexor: missing prompt");
|
|
230
282
|
}
|
|
231
283
|
const portfolioRaw = flagStr(args, "portfolio");
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
284
|
+
if (portfolioRaw !== undefined) {
|
|
285
|
+
return printUsageError(json, "claudexor: --portfolio was removed in v2; use --routing-goal auto|quality|economy");
|
|
286
|
+
}
|
|
287
|
+
const credentialProfileId = flagStr(args, "profile");
|
|
288
|
+
const routingGoalRaw = flagStr(args, "routing-goal");
|
|
289
|
+
const routingGoal = routingGoalRaw !== undefined ? RoutingGoal.safeParse(routingGoalRaw) : null;
|
|
290
|
+
if (routingGoalRaw !== undefined && !routingGoal?.success) {
|
|
291
|
+
return printUsageError(json, `claudexor: unknown --routing-goal '${routingGoalRaw}'`);
|
|
235
292
|
}
|
|
236
293
|
let reviewerEffortOverrides;
|
|
237
294
|
let resolvedReviewerModels;
|
|
@@ -239,7 +296,7 @@ async function orchestrate(args, mode, json, forced = {}) {
|
|
|
239
296
|
let resolvedWebPolicy = undefined;
|
|
240
297
|
let resolvedAccess = undefined;
|
|
241
298
|
let resolvedEffort;
|
|
242
|
-
let
|
|
299
|
+
let paidBudget;
|
|
243
300
|
let maxToolCalls;
|
|
244
301
|
let nFlag;
|
|
245
302
|
let attemptsFlag;
|
|
@@ -248,9 +305,13 @@ async function orchestrate(args, mode, json, forced = {}) {
|
|
|
248
305
|
let resolvedHarnesses;
|
|
249
306
|
let resolvedPrimaryHarness;
|
|
250
307
|
let resolvedModel;
|
|
251
|
-
let attachments;
|
|
252
308
|
let attachmentRequest;
|
|
253
309
|
let resolvedProtectedPathApprovals;
|
|
310
|
+
let resolvedInstructions;
|
|
311
|
+
let resolvedMaxSeconds;
|
|
312
|
+
let resolvedMaxTurns;
|
|
313
|
+
let resolvedDenyPaths;
|
|
314
|
+
let resolvedOutputSchema;
|
|
254
315
|
try {
|
|
255
316
|
reviewerEffortOverrides = reviewerEfforts(args);
|
|
256
317
|
resolvedReviewerModels = reviewerModels(args);
|
|
@@ -261,15 +322,41 @@ async function orchestrate(args, mode, json, forced = {}) {
|
|
|
261
322
|
resolvedHarnesses = harnessList(args);
|
|
262
323
|
resolvedPrimaryHarness = flagStr(args, "primary-harness");
|
|
263
324
|
resolvedModel = flagStr(args, "model");
|
|
264
|
-
maxUsd = floatFlag(args, "max-usd");
|
|
325
|
+
const maxUsd = floatFlag(args, "max-usd");
|
|
326
|
+
paidBudget = maxUsd === undefined ? undefined : { kind: "finite", maxUsd };
|
|
265
327
|
maxToolCalls = intFlag(args, "max-tool-calls");
|
|
266
328
|
nFlag = intFlag(args, "n");
|
|
267
329
|
attemptsFlag = intFlag(args, "attempts");
|
|
268
330
|
autonomy = parseAutonomy(flagStr(args, "autonomy"));
|
|
269
331
|
resolvedSynthesis = synthesisMode(args);
|
|
270
332
|
attachmentRequest = attachmentInputs(args);
|
|
271
|
-
attachments = attachmentsFromInputs(attachmentRequest);
|
|
272
333
|
resolvedProtectedPathApprovals = protectedPathApprovals(args);
|
|
334
|
+
resolvedInstructions = resolveInstructions(args);
|
|
335
|
+
resolvedMaxSeconds = intFlag(args, "max-seconds");
|
|
336
|
+
resolvedMaxTurns = intFlag(args, "max-turns");
|
|
337
|
+
const denyPathFlags = flagStringList(args, "deny-path");
|
|
338
|
+
resolvedDenyPaths = denyPathFlags.length > 0 ? denyPathFlags : undefined;
|
|
339
|
+
const outputSchemaPath = flagStr(args, "output-schema");
|
|
340
|
+
if (outputSchemaPath !== undefined) {
|
|
341
|
+
let raw;
|
|
342
|
+
try {
|
|
343
|
+
raw = readFileSync(outputSchemaPath, "utf8");
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
throw new Error(`--output-schema: cannot read ${outputSchemaPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
347
|
+
}
|
|
348
|
+
let parsedSchema;
|
|
349
|
+
try {
|
|
350
|
+
parsedSchema = JSON.parse(raw);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
throw new Error(`--output-schema: ${outputSchemaPath} is not valid JSON`);
|
|
354
|
+
}
|
|
355
|
+
if (!parsedSchema || typeof parsedSchema !== "object" || Array.isArray(parsedSchema)) {
|
|
356
|
+
throw new Error(`--output-schema: ${outputSchemaPath} must contain a JSON Schema object`);
|
|
357
|
+
}
|
|
358
|
+
resolvedOutputSchema = parsedSchema;
|
|
359
|
+
}
|
|
273
360
|
}
|
|
274
361
|
catch (err) {
|
|
275
362
|
return printUsageError(json, `claudexor: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -280,6 +367,7 @@ async function orchestrate(args, mode, json, forced = {}) {
|
|
|
280
367
|
tests = resolveRunTestCommands(cliTests, spec);
|
|
281
368
|
assertCliRunParamsHaveNoInlineSecrets({
|
|
282
369
|
prompt,
|
|
370
|
+
instructions: resolvedInstructions,
|
|
283
371
|
attachments: attachmentRequest,
|
|
284
372
|
mode,
|
|
285
373
|
harnesses: resolvedHarnesses,
|
|
@@ -291,7 +379,7 @@ async function orchestrate(args, mode, json, forced = {}) {
|
|
|
291
379
|
reviewerEfforts: reviewerEffortOverrides,
|
|
292
380
|
tests,
|
|
293
381
|
protectedPathApprovals: resolvedProtectedPathApprovals,
|
|
294
|
-
|
|
382
|
+
paidBudget,
|
|
295
383
|
access: resolvedAccess,
|
|
296
384
|
web: resolvedWebPolicy,
|
|
297
385
|
externalContextPolicy: resolvedWebPolicy,
|
|
@@ -305,137 +393,60 @@ async function orchestrate(args, mode, json, forced = {}) {
|
|
|
305
393
|
catch (err) {
|
|
306
394
|
return printUsageError(json, `claudexor: ${err instanceof Error ? err.message : String(err)}`);
|
|
307
395
|
}
|
|
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
396
|
if (autonomy !== undefined && mode !== "orchestrate") {
|
|
312
397
|
return printUsageError(json, `claudexor: --autonomy only applies to 'orchestrate' (got mode '${mode}')`);
|
|
313
398
|
}
|
|
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
399
|
if (maxToolCalls !== undefined && mode !== "orchestrate") {
|
|
325
400
|
return printUsageError(json, "claudexor: --max-tool-calls only applies to orchestrate runs");
|
|
326
401
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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,
|
|
402
|
+
return daemonRun(args, json, {
|
|
403
|
+
mode,
|
|
404
|
+
autonomy,
|
|
405
|
+
prompt: prompt || "audit this repository",
|
|
406
|
+
instructions: resolvedInstructions,
|
|
407
|
+
maxSeconds: resolvedMaxSeconds,
|
|
408
|
+
maxTurns: resolvedMaxTurns,
|
|
409
|
+
denyPaths: resolvedDenyPaths,
|
|
410
|
+
outputSchema: resolvedOutputSchema,
|
|
411
|
+
tests,
|
|
412
|
+
paidBudget,
|
|
413
|
+
routingGoal: routingGoal?.success ? routingGoal.data : undefined,
|
|
414
|
+
credentialProfileId,
|
|
415
|
+
maxToolCalls,
|
|
357
416
|
reviewerPanel: resolvedReviewerPanel,
|
|
358
417
|
reviewerModels: resolvedReviewerModels,
|
|
359
418
|
reviewerEfforts: reviewerEffortOverrides,
|
|
419
|
+
protectedPathApprovals: resolvedProtectedPathApprovals,
|
|
420
|
+
resolvedWebPolicy,
|
|
421
|
+
resolvedAccess,
|
|
422
|
+
resolvedEffort,
|
|
423
|
+
resolvedSynthesis,
|
|
424
|
+
resolvedHarnesses,
|
|
425
|
+
resolvedPrimaryHarness,
|
|
426
|
+
resolvedModel,
|
|
427
|
+
nFlag,
|
|
428
|
+
attemptsFlag,
|
|
429
|
+
specId: spec?.id,
|
|
430
|
+
specHash: loadedSpec?.specHash,
|
|
431
|
+
specPath: loadedSpec?.specPath,
|
|
432
|
+
attachmentRequest,
|
|
433
|
+
forced,
|
|
360
434
|
});
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
};
|
|
375
|
-
process.on("SIGINT", onSigint);
|
|
376
|
-
process.on("SIGTERM", onSigint);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* All five product modes enter through the managed daemon and control API.
|
|
438
|
+
* `--json` prints one stable `{ runId, runDir, status }` machine envelope.
|
|
439
|
+
*/
|
|
440
|
+
async function daemonRun(args, json, p) {
|
|
441
|
+
const inPlace = flagBool(args, "in-place");
|
|
442
|
+
const jsonStream = flagBool(args, "json-stream");
|
|
443
|
+
if (json && jsonStream) {
|
|
444
|
+
return printUsageError(json, "claudexor: --json prints exactly one object and --json-stream prints NDJSON; pass one, not both");
|
|
445
|
+
}
|
|
446
|
+
let client;
|
|
447
|
+
let addr;
|
|
377
448
|
try {
|
|
378
|
-
|
|
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;
|
|
449
|
+
({ client, addr } = await ensureDaemon());
|
|
439
450
|
}
|
|
440
451
|
catch (err) {
|
|
441
452
|
if (json)
|
|
@@ -448,38 +459,81 @@ async function orchestrate(args, mode, json, forced = {}) {
|
|
|
448
459
|
process.stderr.write(`claudexor: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
449
460
|
return 1;
|
|
450
461
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
462
|
+
// Thread continuation (W13): --thread <id> targets a thread explicitly;
|
|
463
|
+
// --resume picks the most recently updated one. POST /runs already funnels a
|
|
464
|
+
// threadId through the SINGLE createThreadTurn point server-side — this is
|
|
465
|
+
// parameter plumbing, never a second turn-creation path.
|
|
466
|
+
let threadId = flagStr(args, "thread");
|
|
467
|
+
if (threadId === undefined && flagBool(args, "resume")) {
|
|
468
|
+
try {
|
|
469
|
+
const res = await controlApiFetch(addr, "/threads", {
|
|
470
|
+
headers: { Authorization: `Bearer ${addr.token}` },
|
|
471
|
+
});
|
|
472
|
+
if (!res.ok)
|
|
473
|
+
throw new Error(`GET /threads failed: ${res.status}`);
|
|
474
|
+
// Parse through the typed DTO — the field is camelCase `updatedAt`, and a
|
|
475
|
+
// hand-rolled `updated_at` read silently sorts undefined and throws.
|
|
476
|
+
const list = ControlThreadListResponse.parse(await res.json());
|
|
477
|
+
const newest = pickResumableThread(list.threads);
|
|
478
|
+
if (!newest) {
|
|
479
|
+
// Stay valid on the active output surface: NDJSON stream keeps compact
|
|
480
|
+
// lines; --json prints its one object; text goes to stderr.
|
|
481
|
+
const message = "claudexor: --resume found no threads to continue";
|
|
482
|
+
if (jsonStream)
|
|
483
|
+
printJsonLine({ ok: false, exitCode: 1, error: message });
|
|
484
|
+
else if (json)
|
|
485
|
+
printJson({ ok: false, exitCode: 1, error: message });
|
|
486
|
+
else
|
|
487
|
+
process.stderr.write(`${message}\n`);
|
|
488
|
+
return 1;
|
|
489
|
+
}
|
|
490
|
+
threadId = newest.id;
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
const message = `claudexor: --resume could not list threads: ${err instanceof Error ? err.message : String(err)}`;
|
|
494
|
+
if (jsonStream)
|
|
495
|
+
printJsonLine({ ok: false, exitCode: 1, error: message });
|
|
496
|
+
else if (json)
|
|
497
|
+
printJson({ ok: false, exitCode: 1, error: message });
|
|
498
|
+
else
|
|
499
|
+
process.stderr.write(`${message}\n`);
|
|
500
|
+
return 1;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
let attachmentRefs;
|
|
504
|
+
try {
|
|
505
|
+
attachmentRefs = p.attachmentRequest
|
|
506
|
+
? await Promise.all(p.attachmentRequest.map((attachment) => uploadLocalAttachment(addr, attachment)))
|
|
507
|
+
: undefined;
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
if (json)
|
|
511
|
+
printJson({
|
|
512
|
+
ok: false,
|
|
513
|
+
exitCode: 1,
|
|
514
|
+
error: `claudexor: attachment upload failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
515
|
+
});
|
|
516
|
+
else
|
|
517
|
+
process.stderr.write(`claudexor: attachment upload failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
518
|
+
return 1;
|
|
454
519
|
}
|
|
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
520
|
const body = {
|
|
471
521
|
prompt: p.prompt,
|
|
472
|
-
...(p.
|
|
522
|
+
...(p.instructions ? { instructions: p.instructions } : {}),
|
|
523
|
+
...(p.maxSeconds !== undefined ? { maxSeconds: p.maxSeconds } : {}),
|
|
524
|
+
...(p.maxTurns !== undefined ? { maxTurns: p.maxTurns } : {}),
|
|
525
|
+
...(p.denyPaths?.length ? { denyPaths: p.denyPaths } : {}),
|
|
526
|
+
...(p.outputSchema !== undefined ? { outputSchema: p.outputSchema } : {}),
|
|
527
|
+
...(attachmentRefs ? { attachments: attachmentRefs } : {}),
|
|
473
528
|
mode: p.mode,
|
|
474
|
-
|
|
529
|
+
...(threadId ? { threadId } : {}),
|
|
475
530
|
...(p.mode === "orchestrate" && p.autonomy ? { autonomy: p.autonomy } : {}),
|
|
476
531
|
scope: { kind: "project", root: process.cwd() },
|
|
477
532
|
execution: { isolation: inPlace ? "live" : "envelope" },
|
|
478
|
-
...(
|
|
479
|
-
...(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
...(p.portfolio ? { portfolio: p.portfolio } : {}),
|
|
533
|
+
...(p.resolvedHarnesses ? { harnesses: p.resolvedHarnesses } : {}),
|
|
534
|
+
...(p.resolvedPrimaryHarness ? { primaryHarness: p.resolvedPrimaryHarness } : {}),
|
|
535
|
+
...(p.routingGoal ? { routingGoal: p.routingGoal } : {}),
|
|
536
|
+
...(p.credentialProfileId ? { credentialProfileId: p.credentialProfileId } : {}),
|
|
483
537
|
...(p.forced.race === true ? { n: p.nFlag ?? 2 } : p.nFlag !== undefined ? { n: p.nFlag } : {}),
|
|
484
538
|
...(p.attemptsFlag !== undefined ? { attempts: p.attemptsFlag } : {}),
|
|
485
539
|
...(flagBool(args, "until-clean") ? { untilClean: true } : {}),
|
|
@@ -488,11 +542,13 @@ async function daemonAgentRun(args, json, p) {
|
|
|
488
542
|
...(p.resolvedSynthesis ? { synthesis: p.resolvedSynthesis } : {}),
|
|
489
543
|
...(p.tests ? { tests: p.tests } : {}),
|
|
490
544
|
...(p.protectedPathApprovals ? { protectedPathApprovals: p.protectedPathApprovals } : {}),
|
|
491
|
-
...(p.
|
|
492
|
-
...(p.mode === "orchestrate" && p.maxToolCalls !== undefined
|
|
545
|
+
...(p.paidBudget !== undefined ? { paidBudget: p.paidBudget } : {}),
|
|
546
|
+
...(p.mode === "orchestrate" && p.maxToolCalls !== undefined
|
|
547
|
+
? { maxToolCalls: p.maxToolCalls }
|
|
548
|
+
: {}),
|
|
493
549
|
...(p.resolvedAccess ? { access: p.resolvedAccess } : {}),
|
|
494
550
|
...(p.resolvedWebPolicy ? { web: p.resolvedWebPolicy } : {}),
|
|
495
|
-
...(
|
|
551
|
+
...(p.resolvedModel ? { model: p.resolvedModel } : {}),
|
|
496
552
|
...(p.resolvedEffort ? { effort: p.resolvedEffort } : {}),
|
|
497
553
|
...(p.reviewerPanel ? { reviewerPanel: p.reviewerPanel } : {}),
|
|
498
554
|
...(p.reviewerModels ? { reviewerModels: p.reviewerModels } : {}),
|
|
@@ -502,15 +558,63 @@ async function daemonAgentRun(args, json, p) {
|
|
|
502
558
|
...(p.specPath ? { specPath: p.specPath } : {}),
|
|
503
559
|
};
|
|
504
560
|
try {
|
|
505
|
-
|
|
561
|
+
if (jsonStream) {
|
|
562
|
+
// NDJSON machine surface (W13): an EARLY runId frame first, every run
|
|
563
|
+
// event as its own line (the shared follow pipeline in json mode), and
|
|
564
|
+
// the same terminal object --json prints as the LAST line. --json keeps
|
|
565
|
+
// its exactly-one-object contract untouched.
|
|
566
|
+
const started = await enqueueAndAwait(client, addr, body, { waitForTerminal: false });
|
|
567
|
+
if (!started.runId) {
|
|
568
|
+
// Even the early-failure path stays valid NDJSON (compact, one line).
|
|
569
|
+
printJsonLine({
|
|
570
|
+
frame: "run.terminal",
|
|
571
|
+
runId: "",
|
|
572
|
+
runDir: started.runDir,
|
|
573
|
+
status: runStatusForCli(started.status),
|
|
574
|
+
jobId: started.jobId,
|
|
575
|
+
mode: p.mode,
|
|
576
|
+
...(started.error ? { error: started.error } : {}),
|
|
577
|
+
});
|
|
578
|
+
return exitCodeForState(started.status);
|
|
579
|
+
}
|
|
580
|
+
printJsonLine({
|
|
581
|
+
frame: "run.started",
|
|
582
|
+
runId: started.runId,
|
|
583
|
+
runDir: started.runDir,
|
|
584
|
+
jobId: started.jobId,
|
|
585
|
+
mode: p.mode,
|
|
586
|
+
});
|
|
587
|
+
// Per-event lines: followRun(json=true) already writes one COMPACT object
|
|
588
|
+
// per event via print(JSON.stringify(ev)).
|
|
589
|
+
await followRun(started.runId, true);
|
|
590
|
+
const final = started.jobId ? await client.status(started.jobId) : null;
|
|
591
|
+
const status = final?.state ?? started.status;
|
|
592
|
+
const out = {
|
|
593
|
+
runId: started.runId,
|
|
594
|
+
runDir: final?.runDir ?? started.runDir,
|
|
595
|
+
status: runStatusForCli(status),
|
|
596
|
+
jobId: started.jobId,
|
|
597
|
+
error: final?.error ?? started.error,
|
|
598
|
+
};
|
|
599
|
+
const reason = daemonOutcomeSummary({ ...started, status, error: out.error });
|
|
600
|
+
const applyEligibility = await fetchApplyEligibility(addr, started.runId);
|
|
601
|
+
printJsonLine({
|
|
602
|
+
frame: "run.terminal",
|
|
603
|
+
runId: out.runId,
|
|
604
|
+
runDir: out.runDir,
|
|
605
|
+
status: out.status,
|
|
606
|
+
jobId: out.jobId,
|
|
607
|
+
mode: p.mode,
|
|
608
|
+
...(out.error ? { error: out.error } : {}),
|
|
609
|
+
...(reason ? { summary: reason } : {}),
|
|
610
|
+
...(applyEligibility ? { applyEligibility } : {}),
|
|
611
|
+
});
|
|
612
|
+
return exitCodeForState(status);
|
|
613
|
+
}
|
|
506
614
|
if (json) {
|
|
507
|
-
// Pure machine surface:
|
|
508
|
-
// exactly one JSON object. No event printer, no TTY prompts.
|
|
615
|
+
// Pure machine surface: await the terminal outcome and print one JSON object.
|
|
509
616
|
const out = await enqueueAndAwait(client, addr, body, { waitForTerminal: true });
|
|
510
|
-
//
|
|
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).
|
|
617
|
+
// Preserve bench keys while adding mode and honest non-success detail.
|
|
514
618
|
const reason = daemonOutcomeSummary(out);
|
|
515
619
|
// ADD-ONLY key (bench contract keeps {runId,runDir,status}): the derived
|
|
516
620
|
// apply-gate verdict, so machine callers act on truth instead of
|
|
@@ -519,7 +623,7 @@ async function daemonAgentRun(args, json, p) {
|
|
|
519
623
|
printJson({
|
|
520
624
|
runId: out.runId,
|
|
521
625
|
runDir: out.runDir,
|
|
522
|
-
status: out.status,
|
|
626
|
+
status: runStatusForCli(out.status),
|
|
523
627
|
jobId: out.jobId,
|
|
524
628
|
mode: p.mode,
|
|
525
629
|
...(out.error ? { error: out.error } : {}),
|
|
@@ -539,19 +643,15 @@ async function daemonAgentRun(args, json, p) {
|
|
|
539
643
|
await followRun(started.runId, false);
|
|
540
644
|
const final = started.jobId ? await client.status(started.jobId) : null;
|
|
541
645
|
const status = final?.state ?? started.status;
|
|
646
|
+
const publicStatus = runStatusForCli(status);
|
|
542
647
|
print("");
|
|
543
|
-
print(`run ${started.runId} [${
|
|
648
|
+
print(`run ${started.runId} [${publicStatus}]`);
|
|
544
649
|
print(` artifacts: ${final?.runDir ?? started.runDir}`);
|
|
545
650
|
if (status === "blocked") {
|
|
546
651
|
print(` blocked (needs human): unblock with \`claudexor decision ${started.runId} --accept-risk\` or rerun with \`claudexor decision ${started.runId} --rerun --feedback "..."\``);
|
|
547
652
|
}
|
|
548
653
|
else if (exitCodeForState(status) === 0) {
|
|
549
|
-
//
|
|
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).
|
|
654
|
+
// Offer apply only after a positive gate; otherwise print inspect/unblock guidance.
|
|
555
655
|
const eligibility = await fetchApplyEligibility(addr, started.runId);
|
|
556
656
|
if (eligibility?.eligible) {
|
|
557
657
|
print(` apply with: claudexor apply ${started.runId}`);
|
|
@@ -595,7 +695,7 @@ async function decisionCommand(args, json) {
|
|
|
595
695
|
const { action, body } = resolved;
|
|
596
696
|
try {
|
|
597
697
|
const { addr } = await ensureDaemon();
|
|
598
|
-
const res = await
|
|
698
|
+
const res = await controlApiFetch(addr, `/runs/${encodeURIComponent(runId)}/decision`, {
|
|
599
699
|
method: "POST",
|
|
600
700
|
headers: { Authorization: `Bearer ${addr.token}`, "content-type": "application/json" },
|
|
601
701
|
body: JSON.stringify(body),
|
|
@@ -639,9 +739,9 @@ async function decisionCommand(args, json) {
|
|
|
639
739
|
* Resolve the ArtifactStore that owns a given run, regardless of the cwd the
|
|
640
740
|
* CLI is invoked from. Order:
|
|
641
741
|
* 1. the project store rooted at the current cwd (the common case);
|
|
642
|
-
* 2. the user
|
|
742
|
+
* 2. the v2 user store (~/.claudexor/v2/runs) used by no-project Ask runs;
|
|
643
743
|
* 3. a daemon-tracked run that started in ANOTHER project — agent/race/create
|
|
644
|
-
* runs live under
|
|
744
|
+
* runs live under that project's external runtime namespace, so we ask
|
|
645
745
|
* the daemon for the run's absolute runDir (GET /runs/:id ->
|
|
646
746
|
* summary.runDir) and rebuild a store whose runPaths(runId).root matches.
|
|
647
747
|
* Returns null when no store can be located (the run does not exist anywhere
|
|
@@ -672,15 +772,15 @@ async function resolveRunStore(runId) {
|
|
|
672
772
|
if (!conn)
|
|
673
773
|
return null;
|
|
674
774
|
const { addr } = conn;
|
|
675
|
-
const resp = await
|
|
775
|
+
const resp = await controlApiFetch(addr, `/runs/${encodeURIComponent(runId)}`, {
|
|
676
776
|
headers: { Authorization: `Bearer ${addr.token}` },
|
|
677
777
|
});
|
|
678
778
|
if (resp.ok) {
|
|
679
779
|
const detail = (await resp.json());
|
|
680
780
|
const runDir = detail.summary?.runDir;
|
|
681
781
|
if (runDir && existsSync(runDir)) {
|
|
682
|
-
//
|
|
683
|
-
//
|
|
782
|
+
// Reconstruct a store from the daemon-authoritative absolute runDir:
|
|
783
|
+
// runId -> runs -> owned runtime root.
|
|
684
784
|
const claudexorDir = resolve(runDir, "..", "..");
|
|
685
785
|
const ds = new ArtifactStore(dirname(claudexorDir), { claudexorDir });
|
|
686
786
|
if (existsSync(ds.runPaths(runId).root))
|
|
@@ -740,79 +840,66 @@ async function specCommand(args, json) {
|
|
|
740
840
|
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
841
|
}
|
|
742
842
|
try {
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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);
|
|
843
|
+
const { addr } = await ensureDaemon();
|
|
844
|
+
await controlJson(addr, "/projects", {
|
|
845
|
+
method: "POST",
|
|
846
|
+
headers: { "content-type": "application/json" },
|
|
847
|
+
body: JSON.stringify({ root: process.cwd() }),
|
|
848
|
+
});
|
|
773
849
|
if (!answersPath) {
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
850
|
+
const session = ControlSpecSession.parse(await controlJson(addr, "/spec/sessions", {
|
|
851
|
+
method: "POST",
|
|
852
|
+
headers: { "content-type": "application/json" },
|
|
853
|
+
body: JSON.stringify({
|
|
854
|
+
prompt,
|
|
855
|
+
scope: { kind: "project", root: process.cwd(), context: "auto" },
|
|
856
|
+
harnesses: groundingHarnesses,
|
|
857
|
+
n: groundingN,
|
|
858
|
+
effort: groundingEffort,
|
|
859
|
+
paidBudget: groundingMaxUsd === undefined
|
|
860
|
+
? undefined
|
|
861
|
+
: { kind: "finite", maxUsd: groundingMaxUsd },
|
|
862
|
+
web: groundingWeb,
|
|
863
|
+
reviewerPanel: groundingReviewerPanel,
|
|
864
|
+
reviewerModels: groundingReviewerModels,
|
|
865
|
+
reviewerEfforts: groundingReviewerEfforts,
|
|
866
|
+
}),
|
|
867
|
+
}));
|
|
785
868
|
if (json)
|
|
786
|
-
printJson(
|
|
869
|
+
printJson(session);
|
|
787
870
|
else {
|
|
788
|
-
print(`
|
|
789
|
-
print(`
|
|
790
|
-
|
|
871
|
+
print(`durable spec session: ${session.sessionId}`);
|
|
872
|
+
print(`grounding run: ${session.planRunId ?? "pending"}`);
|
|
873
|
+
for (const question of session.questions)
|
|
874
|
+
print(`- [${question.id}] ${question.prompt}`);
|
|
875
|
+
print("save `claudexor spec ... --json` output, fill its answers array, then pass it with --answers");
|
|
791
876
|
}
|
|
792
|
-
return 0;
|
|
877
|
+
return session.state === "failed" || session.state === "interrupted_unknown" ? 1 : 0;
|
|
878
|
+
}
|
|
879
|
+
const answers = readAnswers(answersPath);
|
|
880
|
+
if (!answers.sessionId) {
|
|
881
|
+
throw new Error("answers file must contain sessionId from the durable spec session");
|
|
793
882
|
}
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
};
|
|
883
|
+
const encodedId = encodeURIComponent(answers.sessionId);
|
|
884
|
+
await controlJson(addr, `/spec/sessions/${encodedId}/answers`, {
|
|
885
|
+
method: "POST",
|
|
886
|
+
headers: { "content-type": "application/json" },
|
|
887
|
+
body: JSON.stringify({
|
|
888
|
+
answers: answers.answers,
|
|
889
|
+
priorDecisions: answers.priorDecisions,
|
|
890
|
+
}),
|
|
891
|
+
});
|
|
892
|
+
const session = ControlSpecSession.parse(await controlJson(addr, `/spec/sessions/${encodedId}/freeze`, { method: "POST" }));
|
|
893
|
+
if (session.state !== "frozen" || !session.specPath || !session.specId || !session.specHash) {
|
|
894
|
+
throw new Error(`spec session ended as ${session.state}`);
|
|
895
|
+
}
|
|
896
|
+
const runHint = `claudexor best-of --spec ${JSON.stringify(session.specPath)}`;
|
|
809
897
|
if (json)
|
|
810
|
-
printJson(
|
|
898
|
+
printJson({ ...session, status: "frozen", runHint });
|
|
811
899
|
else {
|
|
812
|
-
print(`frozen SpecPack: ${
|
|
813
|
-
print(`
|
|
814
|
-
print(` hash: ${
|
|
815
|
-
print(` native projection: ${join(persisted.specDir, "PLANS.md")}`);
|
|
900
|
+
print(`frozen SpecPack: ${session.specId}`);
|
|
901
|
+
print(` path: ${session.specPath}`);
|
|
902
|
+
print(` hash: ${session.specHash}`);
|
|
816
903
|
print(`run: ${runHint}`);
|
|
817
904
|
}
|
|
818
905
|
return 0;
|
|
@@ -831,6 +918,15 @@ async function specCommand(args, json) {
|
|
|
831
918
|
return 1;
|
|
832
919
|
}
|
|
833
920
|
}
|
|
921
|
+
async function controlJson(addr, path, init = {}) {
|
|
922
|
+
const response = await controlApiFetch(addr, path, init);
|
|
923
|
+
const text = await response.text();
|
|
924
|
+
const body = text ? JSON.parse(text) : {};
|
|
925
|
+
if (response.ok)
|
|
926
|
+
return body;
|
|
927
|
+
const detail = body && typeof body === "object" ? body : {};
|
|
928
|
+
throw new Error(String(detail["message"] ?? detail["error"] ?? `HTTP ${response.status}`));
|
|
929
|
+
}
|
|
834
930
|
function printPreflightError(args, json, error) {
|
|
835
931
|
if (json && (args._[0] ?? "help") === "plugin") {
|
|
836
932
|
printJson(pluginCommandErrorResult(args._[1], args._[2], flagBool(args, "dry-run"), 2, error));
|
|
@@ -892,6 +988,9 @@ async function main() {
|
|
|
892
988
|
return runRepl(process.cwd());
|
|
893
989
|
}
|
|
894
990
|
const cwd = process.cwd();
|
|
991
|
+
const opsCommand = dispatchOpsCommand(cmd, args, json);
|
|
992
|
+
if (opsCommand)
|
|
993
|
+
return opsCommand;
|
|
895
994
|
switch (cmd) {
|
|
896
995
|
case "init": {
|
|
897
996
|
const res = initProjectConfig(cwd);
|
|
@@ -901,77 +1000,8 @@ async function main() {
|
|
|
901
1000
|
print(res.created ? `Created ${res.configPath}` : `Config already exists: ${res.configPath}`);
|
|
902
1001
|
return 0;
|
|
903
1002
|
}
|
|
904
|
-
case "
|
|
905
|
-
|
|
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
|
-
}
|
|
1003
|
+
case "project":
|
|
1004
|
+
return projectCommand(args, json);
|
|
975
1005
|
case "agent": {
|
|
976
1006
|
const specStrategyError = "claudexor: --spec requires a gated strategy; use 'claudexor best-of --spec <file>' or 'claudexor agent --attempts N --spec <file>'";
|
|
977
1007
|
// ONE gate for both spellings: `agent --spec` and `agent --mode agent --spec`
|
|
@@ -1032,42 +1062,20 @@ async function main() {
|
|
|
1032
1062
|
case "audit":
|
|
1033
1063
|
case "map":
|
|
1034
1064
|
return orchestrate(args, "audit", json);
|
|
1035
|
-
case "daemon":
|
|
1036
|
-
return daemonCommand(args, json);
|
|
1037
1065
|
case "settings":
|
|
1038
1066
|
return settingsCommand(args, json);
|
|
1067
|
+
case "quota":
|
|
1068
|
+
return quotaCommand(args, json);
|
|
1039
1069
|
case "trust":
|
|
1040
1070
|
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
1071
|
case "mcp": {
|
|
1048
|
-
if (args._[1] === "serve")
|
|
1049
|
-
|
|
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
|
-
}
|
|
1072
|
+
if (args._[1] === "serve")
|
|
1073
|
+
return serveMcpBridge();
|
|
1060
1074
|
return printUsageError(json, "usage: claudexor mcp serve");
|
|
1061
1075
|
}
|
|
1062
1076
|
case "acp": {
|
|
1063
|
-
if (args._[1] === "serve")
|
|
1064
|
-
|
|
1065
|
-
version: CLAUDEXOR_VERSION,
|
|
1066
|
-
runner: orchestratorRunner(),
|
|
1067
|
-
transport: { read: process.stdin, write: process.stdout },
|
|
1068
|
-
}).serve();
|
|
1069
|
-
return 0;
|
|
1070
|
-
}
|
|
1077
|
+
if (args._[1] === "serve")
|
|
1078
|
+
return serveAcpBridge();
|
|
1071
1079
|
return printUsageError(json, "usage: claudexor acp serve");
|
|
1072
1080
|
}
|
|
1073
1081
|
case "follow": {
|
|
@@ -1077,6 +1085,10 @@ async function main() {
|
|
|
1077
1085
|
}
|
|
1078
1086
|
return followRun(runId, json);
|
|
1079
1087
|
}
|
|
1088
|
+
case "retry":
|
|
1089
|
+
return retryCommand(args, json);
|
|
1090
|
+
case "run-again":
|
|
1091
|
+
return runAgainCommand(args, json);
|
|
1080
1092
|
case "review":
|
|
1081
1093
|
return reviewCommand(args, json);
|
|
1082
1094
|
case "inspect": {
|
|
@@ -1159,17 +1171,42 @@ async function main() {
|
|
|
1159
1171
|
}
|
|
1160
1172
|
if (telemetry) {
|
|
1161
1173
|
print(`web: policy=${telemetry.external_context_policy} effective=${telemetry.effective_web_mode} required=${telemetry.web_required} evidence=${telemetry.web.status}`);
|
|
1174
|
+
for (const requirement of telemetry.request_requirements.filter((item) => item.requested)) {
|
|
1175
|
+
print(`${requirement.capability}: harness=${requirement.harness_id} requested=true effective=${requirement.effective} reason=${requirement.reason}`);
|
|
1176
|
+
}
|
|
1162
1177
|
}
|
|
1163
1178
|
else if (contract.success) {
|
|
1164
1179
|
print(`web: policy=${contract.data.external_context.policy} required=${contract.data.external_context.web_required} evidence=unavailable (no telemetry.yaml)`);
|
|
1165
1180
|
}
|
|
1166
1181
|
print(`output: ${outputReadyState}${primary ? ` ${primary.path}` : ""}`);
|
|
1182
|
+
{
|
|
1183
|
+
// Structured-output contract receipt (only present when the run was
|
|
1184
|
+
// started with --output-schema); projected, never re-validated here.
|
|
1185
|
+
const conformance = StructuredOutputConformance.safeParse(store.readYaml(join(paths.finalDir, "structured_output.yaml")));
|
|
1186
|
+
if (conformance.success) {
|
|
1187
|
+
print(`structured output: ${conformance.data.status}${conformance.data.output_path ? ` ${conformance.data.output_path}` : ""}${conformance.data.reason ? ` (${conformance.data.reason})` : ""}`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1167
1190
|
if (parsedDecision.success) {
|
|
1168
1191
|
const vb = parsedDecision.data.verification_basis;
|
|
1169
1192
|
print(`decision: ${parsedDecision.data.status} outcome=${parsedDecision.data.outcome} apply=${parsedDecision.data.apply_recommendation}${vb !== "none" ? ` verified_by=${vb}` : ""}`);
|
|
1170
1193
|
const budget = parsedDecision.data.budget_summary;
|
|
1171
1194
|
print(`budget: spend=${budget.spend_usd ?? "unknown"}${budget.estimated ? " estimated" : ""}`);
|
|
1172
1195
|
}
|
|
1196
|
+
if (telemetry) {
|
|
1197
|
+
const u = telemetry.usage_totals;
|
|
1198
|
+
if (u.input_tokens !== null || u.output_tokens !== null || u.cached_input_tokens !== null) {
|
|
1199
|
+
print(`tokens: in=${u.input_tokens ?? "n/a"} out=${u.output_tokens ?? "n/a"} cached=${u.cached_input_tokens ?? "n/a"}`);
|
|
1200
|
+
}
|
|
1201
|
+
// Route receipt (INV-061 disclosure), projected verbatim from telemetry.
|
|
1202
|
+
const route = telemetry.auth_route;
|
|
1203
|
+
if (route) {
|
|
1204
|
+
print(`auth route: requested=${route.requested} effective=${route.effective ?? "undisclosed"} source=${route.source ?? "undisclosed"} reason=${route.reason}${route.harness_id ? ` (${route.harness_id}/${route.attempt_id ?? "?"})` : ""}`);
|
|
1205
|
+
if (route.model_mismatch) {
|
|
1206
|
+
print(`model mismatch: requested=${route.model_mismatch.requested} observed=${route.model_mismatch.observed}`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1173
1210
|
if (telemetry && (telemetry.web.attempted || telemetry.web.required)) {
|
|
1174
1211
|
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
1212
|
}
|
|
@@ -1203,127 +1240,43 @@ async function main() {
|
|
|
1203
1240
|
if (!runId) {
|
|
1204
1241
|
return printUsageError(json, "usage: claudexor apply <run_id> [--mode apply|commit|branch|pr] [--dry-run]");
|
|
1205
1242
|
}
|
|
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
1243
|
const rawMode = flagStr(args, "mode") ?? "apply";
|
|
1299
|
-
if (!
|
|
1244
|
+
if (!["apply", "commit", "branch", "pr"].includes(rawMode)) {
|
|
1300
1245
|
if (json)
|
|
1301
1246
|
printJson({ runId, error: `unsupported apply mode: ${rawMode}` });
|
|
1302
1247
|
else
|
|
1303
1248
|
print(`unsupported apply mode: ${rawMode}`);
|
|
1304
1249
|
return 2;
|
|
1305
1250
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1251
|
+
const { addr } = await ensureDaemon();
|
|
1252
|
+
const dryRun = flagBool(args, "dry-run");
|
|
1253
|
+
const response = await controlApiFetch(addr, `/runs/${encodeURIComponent(runId)}/apply${dryRun ? "/check" : ""}`, {
|
|
1254
|
+
method: "POST",
|
|
1255
|
+
headers: { "content-type": "application/json" },
|
|
1256
|
+
body: JSON.stringify(dryRun
|
|
1257
|
+
? { target: { kind: "original_project" } }
|
|
1258
|
+
: {
|
|
1259
|
+
target: { kind: "original_project" },
|
|
1260
|
+
mode: rawMode,
|
|
1261
|
+
message: `claudexor: apply ${runId}`,
|
|
1262
|
+
}),
|
|
1263
|
+
});
|
|
1264
|
+
const text = await response.text();
|
|
1265
|
+
const result = text ? JSON.parse(text) : {};
|
|
1319
1266
|
if (json)
|
|
1320
|
-
printJson(
|
|
1267
|
+
printJson({ runId, ...(dryRun ? { dryRun: true } : {}), ...result });
|
|
1268
|
+
else if (!response.ok)
|
|
1269
|
+
print(String(result["message"] ?? result["error"] ?? text));
|
|
1270
|
+
else if (dryRun)
|
|
1271
|
+
print(result["ok"] === true ? "patch applies cleanly" : "patch does not apply");
|
|
1321
1272
|
else
|
|
1322
|
-
print(`${
|
|
1323
|
-
(
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1273
|
+
print(`${String(result["mode"] ?? rawMode)}: applied=${String(result["applied"] ?? false)}` +
|
|
1274
|
+
(typeof result["commit"] === "string"
|
|
1275
|
+
? ` commit=${result["commit"].slice(0, 8)}`
|
|
1276
|
+
: "") +
|
|
1277
|
+
(typeof result["branch"] === "string" ? ` branch=${result["branch"]}` : "") +
|
|
1278
|
+
(typeof result["detail"] === "string" ? ` (${result["detail"]})` : ""));
|
|
1279
|
+
return response.ok && (dryRun ? result["ok"] === true : result["applied"] === true) ? 0 : 1;
|
|
1327
1280
|
}
|
|
1328
1281
|
case "decision":
|
|
1329
1282
|
return decisionCommand(args, json);
|
|
@@ -1421,7 +1374,9 @@ async function main() {
|
|
|
1421
1374
|
print(`modes: ${catalog.modes.join(", ")}`);
|
|
1422
1375
|
print(`available harnesses: ${catalog.availableHarnesses.join(", ") || "(none)"}`);
|
|
1423
1376
|
for (const h of catalog.harnesses) {
|
|
1424
|
-
const model = h.configuredModel
|
|
1377
|
+
const model = h.configuredModel
|
|
1378
|
+
? ` model=${h.configuredModel}${h.configuredModelValid === false ? " (REJECTED)" : ""}`
|
|
1379
|
+
: "";
|
|
1425
1380
|
print(` ${statusGlyph(h.status)} ${h.id}: ${h.status}; intents=${h.enabledIntents.join(",") || "-"}; models=${h.models.count} (${h.models.source})${model}`);
|
|
1426
1381
|
}
|
|
1427
1382
|
print(`mcp tools: ${catalog.mcpTools.join(", ")}`);
|
|
@@ -1443,7 +1398,11 @@ async function main() {
|
|
|
1443
1398
|
// exit 0 — scripts must not mistake a typo'd verb for success. --json
|
|
1444
1399
|
// callers get the machine envelope on stdout (stdout purity contract).
|
|
1445
1400
|
if (json) {
|
|
1446
|
-
printJson({
|
|
1401
|
+
printJson({
|
|
1402
|
+
ok: false,
|
|
1403
|
+
exitCode: 2,
|
|
1404
|
+
error: `claudexor: unknown command '${cmd}' (see \`claudexor help --json\`)`,
|
|
1405
|
+
});
|
|
1447
1406
|
return 2;
|
|
1448
1407
|
}
|
|
1449
1408
|
process.stderr.write(`claudexor: unknown command '${cmd}'\n\n${HELP}\n`);
|