@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/claudexord.js
CHANGED
|
@@ -1,564 +1,367 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { appendFileSync,
|
|
3
|
-
import {
|
|
4
|
-
import { DaemonClient, DaemonServer, InteractionRegistry, RunEventBus,
|
|
2
|
+
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { DaemonClient, commandProjection, interactionProjection, operatorDecisionProjection, runEventProjection, JournalManager, DaemonServer, InteractionRegistry, ProjectPartitions, projectProjection, RunEventBus, ResourceStore, quotaProjection, threadHeadPingProjection, threadProjection, daemonDir, defaultSocketPath, acquireDaemonWriterLease, ensureToken, ensureDaemonRuntimeRoot, logPath, socketAlive, } from "@claudexor/daemon";
|
|
5
5
|
import { DaemonControlApiServer, normalizeRunStartRequest } from "@claudexor/control-api";
|
|
6
|
-
import { armDaemonLifecycle, runStartupCrashGc } from "./daemon-lifecycle.js";
|
|
6
|
+
import { armDaemonLifecycle, logLine, runStartupCrashGc } from "./daemon-lifecycle.js";
|
|
7
7
|
import { Orchestrator } from "@claudexor/orchestrator";
|
|
8
|
-
import { loadConfig
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { invalidateDoctorCache, validateModel } from "@claudexor/core";
|
|
16
|
-
import { resolveAttachments } from "./attachment-resolver.js";
|
|
17
|
-
import { buildGateway, buildRegistry, harnessModels } from "./registry.js";
|
|
18
|
-
import { buildAgentCapabilityCatalog } from "./capabilities.js";
|
|
19
|
-
import { applyHarnessSettingsPatches, assertSettingsPatchValid } from "./settings-service.js";
|
|
8
|
+
import { loadConfig } from "@claudexor/config";
|
|
9
|
+
import { ensureThreadWorktree } from "@claudexor/workspace";
|
|
10
|
+
import { noProjectRepoRoot, redactSecrets } from "@claudexor/util";
|
|
11
|
+
import { scheduleStartupRetention } from "./retention-service.js";
|
|
12
|
+
import { controlServices } from "./control-services.js";
|
|
13
|
+
import { AuthReadinessService } from "@claudexor/gateway";
|
|
14
|
+
import { buildGateway, buildRegistry } from "./registry.js";
|
|
20
15
|
import { createSetupJobManager } from "./setup-jobs.js";
|
|
21
|
-
import {
|
|
16
|
+
import { SetupJobStore } from "./setup-job-store.js";
|
|
17
|
+
import { SetupLifecycleBinding } from "./setup-lifecycle-binding.js";
|
|
18
|
+
import { DaemonRuntimeShutdown } from "./daemon-runtime-shutdown.js";
|
|
19
|
+
import { refreshCodexQuota } from "./codex-quota-source.js";
|
|
20
|
+
import { refreshClaudeStatuslineQuota } from "./claude-statusline.js";
|
|
21
|
+
import { refreshClaudeOauthUsageQuota } from "./claude-oauth-usage.js";
|
|
22
22
|
const NO_PROJECT_ROOT = noProjectRepoRoot();
|
|
23
23
|
async function main() {
|
|
24
|
-
|
|
25
|
-
// never be group/world readable (mkdir mode only applies on creation).
|
|
26
|
-
mkdirSync(daemonDir(), { recursive: true, mode: 0o700 });
|
|
27
|
-
chmodSync(daemonDir(), 0o700);
|
|
28
|
-
const token = ensureToken();
|
|
24
|
+
ensureDaemonRuntimeRoot();
|
|
29
25
|
const socketPath = defaultSocketPath();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
token
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
});
|
|
67
|
-
// Fail loud on bogus socket-caller thread/turn ids (job settles failed).
|
|
68
|
-
const { threadId, turnId } = threads.assertKnownIds(p.threadId, p.turnId);
|
|
69
|
-
// Resolve the execution tree: an ISOLATED thread runs in its persistent
|
|
70
|
-
// worktree (lazily created); in-place threads and ordinary runs use the
|
|
71
|
-
// project root. Config/artifacts stay anchored to repoRoot either way.
|
|
72
|
-
let executionRoot;
|
|
73
|
-
let inPlace = p.execution.isolation === "live";
|
|
74
|
-
if (threadId && repoRoot !== NO_PROJECT_ROOT) {
|
|
75
|
-
const thread = threads.getThread(threadId);
|
|
76
|
-
if (thread?.workspace.mode === "isolated") {
|
|
77
|
-
const wt = await ensureThreadWorktree(repoRoot, threadId);
|
|
78
|
-
executionRoot = wt.path;
|
|
79
|
-
// Persist the base ONLY on creation: `apply` advances base_sha, and a
|
|
80
|
-
// later turn must not clobber it back to the worktree HEAD (which never
|
|
81
|
-
// moves, since turns don't commit) — that would re-apply old work.
|
|
82
|
-
if (wt.created)
|
|
83
|
-
threads.setThreadWorktree(threadId, wt.path, wt.baseSha);
|
|
84
|
-
inPlace = true; // isolated turns run in-place WITHIN the worktree
|
|
85
|
-
}
|
|
26
|
+
const writerLease = acquireDaemonWriterLease(socketPath);
|
|
27
|
+
let shutdownRuntime = null;
|
|
28
|
+
// Release wave round-12 BLOCK: the single-writer lease may only be released
|
|
29
|
+
// after a CLEAN shutdown — a failed/partial shutdown keeps components that
|
|
30
|
+
// can still write, and releasing would let a successor acquire ownership
|
|
31
|
+
// beside them. On failure the lease dies with the process instead.
|
|
32
|
+
let releaseWriterLease = true;
|
|
33
|
+
let lifecycle = null;
|
|
34
|
+
let quotaPollTimer = null;
|
|
35
|
+
try {
|
|
36
|
+
const token = ensureToken();
|
|
37
|
+
if (await socketAlive(socketPath)) {
|
|
38
|
+
throw new Error(`a claudexor daemon is already listening on ${socketPath}; stop it first`);
|
|
39
|
+
}
|
|
40
|
+
await runStartupCrashGc({ daemonDir: daemonDir(), logPath: logPath() });
|
|
41
|
+
const bus = new RunEventBus();
|
|
42
|
+
const journalManager = new JournalManager(daemonDir());
|
|
43
|
+
const commandStoreSlot = journalManager.registerProjection(commandProjection());
|
|
44
|
+
const interactionStoreSlot = journalManager.registerProjection(interactionProjection());
|
|
45
|
+
const operatorDecisionStoreSlot = journalManager.registerProjection(operatorDecisionProjection());
|
|
46
|
+
const runEventStoreSlot = journalManager.registerProjection(runEventProjection());
|
|
47
|
+
const projectStoreSlot = journalManager.registerProjection(projectProjection());
|
|
48
|
+
const quotaStoreSlot = journalManager.registerProjection(quotaProjection([
|
|
49
|
+
refreshCodexQuota,
|
|
50
|
+
refreshClaudeStatuslineQuota,
|
|
51
|
+
() => refreshClaudeOauthUsageQuota(),
|
|
52
|
+
]));
|
|
53
|
+
// Sidebar invalidation ping (W12): a GLOBAL-partition emitter every
|
|
54
|
+
// ThreadStore (global + per-project) writes through, so any thread
|
|
55
|
+
// mutation reaches the app's single global stream. The ping is auxiliary
|
|
56
|
+
// invalidation — it must never fail the mutation that triggered it
|
|
57
|
+
// (mirrors the runner's turn-binding policy).
|
|
58
|
+
const threadHeadPingSlot = journalManager.registerProjection(threadHeadPingProjection());
|
|
59
|
+
const threadHeadPing = (ping) => {
|
|
60
|
+
try {
|
|
61
|
+
threadHeadPingSlot.current().ping(ping);
|
|
86
62
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
63
|
+
catch {
|
|
64
|
+
/* invalidation ping must never fail the thread mutation */
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const threadStoreSlot = journalManager.registerProjection(threadProjection(threadHeadPing));
|
|
68
|
+
const setupStoreSlot = journalManager.registerProjection({
|
|
69
|
+
name: "setup",
|
|
70
|
+
create: (journal) => new SetupJobStore(daemonDir(), { journal }),
|
|
71
|
+
validate: (store) => store.validateProjection(),
|
|
72
|
+
});
|
|
73
|
+
journalManager.start();
|
|
74
|
+
const pollQuota = () => {
|
|
75
|
+
try {
|
|
76
|
+
void quotaStoreSlot.current().pollStale();
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
};
|
|
80
|
+
quotaPollTimer = setInterval(pollQuota, 60_000).unref();
|
|
81
|
+
pollQuota();
|
|
82
|
+
const threads = new ProjectPartitions(daemonDir(), projectStoreSlot, commandStoreSlot, interactionStoreSlot, operatorDecisionStoreSlot, runEventStoreSlot, threadStoreSlot, threadHeadPing);
|
|
83
|
+
const interactions = new InteractionRegistry({
|
|
84
|
+
forRequest: (params) => threads.interactionsForRequest(params),
|
|
85
|
+
all: () => threads.interactionStores(),
|
|
86
|
+
});
|
|
87
|
+
const resources = new ResourceStore(join(daemonDir(), "resource-store"));
|
|
88
|
+
const server = new DaemonServer({
|
|
89
|
+
socketPath,
|
|
90
|
+
token,
|
|
91
|
+
commands: threads,
|
|
92
|
+
onRunTerminal: (runId, threadId) => {
|
|
93
|
+
interactions.dropForRun(runId);
|
|
94
|
+
// Run-terminal is the one W12 path with no thread-store mutation to
|
|
95
|
+
// ride — the terminal changes the thread's presented state, so ping.
|
|
96
|
+
if (threadId)
|
|
97
|
+
threads.pingThreadHead(threadId);
|
|
98
|
+
},
|
|
99
|
+
onTurnEnqueueFailed: (turnId, error, code) => threads.setTurnEnqueueError(turnId, error, code),
|
|
100
|
+
onShutdownRequested: () => shutdownRuntime?.beginShutdown("socket-rpc stop") ??
|
|
101
|
+
Promise.reject(new Error("daemon shutdown coordinator is not initialized")),
|
|
102
|
+
runner: async (params, ctx) => {
|
|
103
|
+
const p = normalizeRunStartRequest(params);
|
|
104
|
+
const mode = p.mode;
|
|
105
|
+
const noProjectAsk = mode === "ask" && p.scope.kind === "none";
|
|
106
|
+
const repoRoot = p.scope.kind === "project" ? p.scope.root : NO_PROJECT_ROOT;
|
|
107
|
+
if (noProjectAsk)
|
|
108
|
+
mkdirSync(NO_PROJECT_ROOT, { recursive: true, mode: 0o700 });
|
|
109
|
+
const orchestrator = new Orchestrator({
|
|
110
|
+
registry: buildRegistry(),
|
|
111
|
+
routingGoal: p.routingGoal,
|
|
112
|
+
quotaSnapshots: () => quotaStoreSlot.current().read().snapshots,
|
|
113
|
+
quotaEventSink: (harnessId, event) => quotaStoreSlot.current().ingest(harnessId, event),
|
|
114
|
+
reviewerPanel: p.reviewerPanel,
|
|
115
|
+
reviewerModels: p.reviewerModels && typeof p.reviewerModels === "object" ? p.reviewerModels : undefined,
|
|
116
|
+
reviewerEfforts: p.reviewerEfforts && typeof p.reviewerEfforts === "object"
|
|
117
|
+
? p.reviewerEfforts
|
|
118
|
+
: undefined,
|
|
119
|
+
});
|
|
120
|
+
const { threadId, turnId } = threads.assertKnownIds(p.threadId, p.turnId);
|
|
121
|
+
let executionRoot;
|
|
122
|
+
let inPlace = p.execution.isolation === "live";
|
|
123
|
+
if (threadId && repoRoot !== NO_PROJECT_ROOT) {
|
|
124
|
+
const thread = threads.getThread(threadId);
|
|
125
|
+
if (thread?.workspace.mode === "isolated") {
|
|
126
|
+
const wt = await ensureThreadWorktree(repoRoot, threadId);
|
|
127
|
+
executionRoot = wt.path;
|
|
128
|
+
// Only creation owns the base; apply advances it independently.
|
|
129
|
+
if (wt.created)
|
|
130
|
+
threads.setThreadWorktree(threadId, wt.path, wt.baseSha);
|
|
131
|
+
inPlace = true; // isolated turns run in-place WITHIN the worktree
|
|
104
132
|
}
|
|
105
133
|
}
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
const onRunStart = (info) => {
|
|
135
|
+
ctx.onRunStart?.(info);
|
|
136
|
+
if (!threadId)
|
|
137
|
+
return;
|
|
138
|
+
try {
|
|
139
|
+
if (turnId) {
|
|
140
|
+
threads.bindTurnRun(turnId, info.runId);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const turn = threads.createTurn(threadId, String(p.prompt ?? ""), {
|
|
144
|
+
parentRunId: typeof p.parentRunId === "string" ? p.parentRunId : null,
|
|
145
|
+
});
|
|
146
|
+
threads.bindTurnRun(turn.id, info.runId);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
/* turn binding must never fail the run */
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
// maxSeconds: a hard wall-clock deadline for the WHOLE run (run-scoped,
|
|
154
|
+
// never per-attempt). Combine the daemon's per-run cancel signal with a
|
|
155
|
+
// deadline that aborts with a typed STRING reason so the terminal is
|
|
156
|
+
// `cancelled` + wall_clock_exceeded rather than a bare user cancel.
|
|
157
|
+
const maxSeconds = typeof p.maxSeconds === "number" && p.maxSeconds > 0
|
|
158
|
+
? // Defense in depth against a setTimeout 32-bit-ms overflow (the
|
|
159
|
+
// schema already caps at 7 days for the control-API path).
|
|
160
|
+
Math.min(p.maxSeconds, 604_800)
|
|
161
|
+
: null;
|
|
162
|
+
// INV-135 selection precedence: explicit per-turn profile beats the
|
|
163
|
+
// thread's sticky profile beats the engine default — and an explicit
|
|
164
|
+
// NULL forces the default ladder past the sticky profile (release
|
|
165
|
+
// wave round-11). The winner scopes BOTH the run spec and the
|
|
166
|
+
// resume-session lookup (resume never crosses profiles).
|
|
167
|
+
const requestedProfileId = p.credentialProfileId === null
|
|
168
|
+
? null
|
|
169
|
+
: typeof p.credentialProfileId === "string" && p.credentialProfileId
|
|
170
|
+
? p.credentialProfileId
|
|
171
|
+
: threadId
|
|
172
|
+
? (threads.getThread(threadId)?.credential_profile_id ?? null)
|
|
173
|
+
: null;
|
|
174
|
+
let deadlineTimer;
|
|
175
|
+
let runSignal = ctx.signal;
|
|
176
|
+
if (maxSeconds !== null) {
|
|
177
|
+
const deadline = new AbortController();
|
|
178
|
+
deadlineTimer = setTimeout(() => deadline.abort("wall_clock_exceeded"), maxSeconds * 1000);
|
|
179
|
+
deadlineTimer.unref?.();
|
|
180
|
+
runSignal = ctx.signal ? AbortSignal.any([ctx.signal, deadline.signal]) : deadline.signal;
|
|
108
181
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
attachments: turnId
|
|
134
|
-
? (threads.getTurn(turnId)?.attachments ?? [])
|
|
135
|
-
: resolveAttachments(p.attachments),
|
|
136
|
-
// Agent-driven browser opt-in (Playwright MCP). The orchestrator gates it
|
|
137
|
-
// on the harness's browser_tool capability + web policy != off.
|
|
138
|
-
browser: p.browser === true,
|
|
139
|
-
mode: p.mode,
|
|
140
|
-
contextMode: noProjectAsk
|
|
141
|
-
? "off"
|
|
142
|
-
: p.scope.kind === "project"
|
|
143
|
-
? p.scope.context
|
|
182
|
+
return orchestrator
|
|
183
|
+
.run({
|
|
184
|
+
onEvent: (event) => {
|
|
185
|
+
if (event.type === "harness.event") {
|
|
186
|
+
const payload = event.payload;
|
|
187
|
+
const harnessId = typeof payload["harness_id"] === "string" ? payload["harness_id"] : "";
|
|
188
|
+
if (harnessId)
|
|
189
|
+
quotaStoreSlot.current().ingest(harnessId, payload);
|
|
190
|
+
}
|
|
191
|
+
threads.recordRunEvent(p, event);
|
|
192
|
+
bus.publish(event);
|
|
193
|
+
},
|
|
194
|
+
onInteraction: (ctx2) => interactions.register(ctx2, p),
|
|
195
|
+
interactionTimeoutMs: loadConfig(repoRoot).global.interaction_timeout_ms,
|
|
196
|
+
threadId,
|
|
197
|
+
executionRoot,
|
|
198
|
+
resumeSessions: threadId ? threads.resumeMap(threadId, requestedProfileId) : undefined,
|
|
199
|
+
onSessionObserved: threadId
|
|
200
|
+
? (harnessId, nativeSessionId, observedModel, profileId) =>
|
|
201
|
+
// The EVENT's profile is the cache truth (INV-135): rotation
|
|
202
|
+
// makes the effective profile differ from the requested one,
|
|
203
|
+
// and a mislabeled session would resume under the wrong
|
|
204
|
+
// account on the next turn.
|
|
205
|
+
threads.recordSession(threadId, harnessId, nativeSessionId, observedModel, profileId ?? null)
|
|
144
206
|
: undefined,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
port: Number(process.env.CLAUDEXOR_CONTROL_PORT ?? 0),
|
|
201
|
-
bus,
|
|
202
|
-
services: controlServices(interactions, threads),
|
|
207
|
+
authPreference: p.authPreference,
|
|
208
|
+
credentialProfileId: requestedProfileId,
|
|
209
|
+
autonomy: p.autonomy,
|
|
210
|
+
answerInteraction: async (subRunId, interactionId, answers) => interactions.answer(subRunId, interactionId, answers).status === "delivered",
|
|
211
|
+
repoRoot,
|
|
212
|
+
prompt: String(p.prompt ?? ""),
|
|
213
|
+
instructions: typeof p.instructions === "string" ? p.instructions : undefined,
|
|
214
|
+
denyPaths: Array.isArray(p.denyPaths) ? p.denyPaths : undefined,
|
|
215
|
+
maxTurns: typeof p.maxTurns === "number" && p.maxTurns > 0 ? p.maxTurns : undefined,
|
|
216
|
+
outputSchema: p.outputSchema && typeof p.outputSchema === "object" && !Array.isArray(p.outputSchema)
|
|
217
|
+
? p.outputSchema
|
|
218
|
+
: undefined,
|
|
219
|
+
attachments: turnId
|
|
220
|
+
? (threads.getTurn(turnId)?.attachments ?? [])
|
|
221
|
+
: resources.resolve(p.attachments),
|
|
222
|
+
browser: p.browser === true,
|
|
223
|
+
mode: p.mode,
|
|
224
|
+
contextMode: noProjectAsk
|
|
225
|
+
? "off"
|
|
226
|
+
: p.scope.kind === "project"
|
|
227
|
+
? p.scope.context
|
|
228
|
+
: undefined,
|
|
229
|
+
harnesses: p.harnesses,
|
|
230
|
+
primaryHarness: p.primaryHarness,
|
|
231
|
+
routingGoal: p.routingGoal,
|
|
232
|
+
n: p.n,
|
|
233
|
+
attempts: p.attempts ?? null,
|
|
234
|
+
untilClean: p.untilClean === true,
|
|
235
|
+
swarm: p.swarm === true,
|
|
236
|
+
create: p.create === true,
|
|
237
|
+
synthesis: p.synthesis,
|
|
238
|
+
paidBudget: p.paidBudget,
|
|
239
|
+
maxToolCalls: p.maxToolCalls ?? null,
|
|
240
|
+
access: p.access,
|
|
241
|
+
web: p.web ?? p.externalContextPolicy,
|
|
242
|
+
externalContextPolicy: p.externalContextPolicy ?? p.web,
|
|
243
|
+
model: p.model,
|
|
244
|
+
models: p.models,
|
|
245
|
+
effort: p.effort,
|
|
246
|
+
tests: Array.isArray(p.tests) ? p.tests : undefined,
|
|
247
|
+
protectedPathApprovals: Array.isArray(p.protectedPathApprovals)
|
|
248
|
+
? p.protectedPathApprovals
|
|
249
|
+
: undefined,
|
|
250
|
+
specId: typeof p.specId === "string" ? p.specId : undefined,
|
|
251
|
+
specHash: typeof p.specHash === "string" ? p.specHash : undefined,
|
|
252
|
+
specPath: typeof p.specPath === "string" ? p.specPath : undefined,
|
|
253
|
+
inPlace,
|
|
254
|
+
signal: runSignal,
|
|
255
|
+
onRunStart,
|
|
256
|
+
})
|
|
257
|
+
.finally(() => {
|
|
258
|
+
if (deadlineTimer)
|
|
259
|
+
clearTimeout(deadlineTimer);
|
|
260
|
+
});
|
|
261
|
+
},
|
|
203
262
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
throw new Error("legacy repoRoot field is not accepted; use scope.kind=project with scope.root");
|
|
224
|
-
const scope = p["scope"];
|
|
225
|
-
if (!scope || typeof scope !== "object" || Array.isArray(scope))
|
|
226
|
-
throw new Error(`project scope is required for ${purpose}`);
|
|
227
|
-
const s = scope;
|
|
228
|
-
if (s["kind"] !== "project")
|
|
229
|
-
throw new Error(`project scope is required for ${purpose}`);
|
|
230
|
-
const root = typeof s["root"] === "string" ? s["root"].trim() : "";
|
|
231
|
-
if (!root)
|
|
232
|
-
throw new Error(`project scope root is required for ${purpose}`);
|
|
233
|
-
if (!isAbsolute(root))
|
|
234
|
-
throw new Error("project root must be an absolute path");
|
|
235
|
-
return root;
|
|
236
|
-
}
|
|
237
|
-
/** Deliver an isolated thread's accumulated worktree diff to its project. */
|
|
238
|
-
async function applyThreadDiff(threads, id, opts) {
|
|
239
|
-
const thread = threads.getThread(id);
|
|
240
|
-
if (!thread)
|
|
241
|
-
throw Object.assign(new Error(`no such thread: ${id}`), { status: 404 });
|
|
242
|
-
const ws = thread.workspace;
|
|
243
|
-
if (ws.mode !== "isolated" || !ws.worktree_path || !thread.repo) {
|
|
244
|
-
throw Object.assign(new Error("thread has no isolated worktree to apply (in-place threads write the project directly)"), { status: 400 });
|
|
245
|
-
}
|
|
246
|
-
const projectRoot = thread.repo.root;
|
|
247
|
-
const base = ws.base_sha ?? "HEAD";
|
|
248
|
-
const patch = await diffStaged(ws.worktree_path, base);
|
|
249
|
-
if (!patch.trim())
|
|
250
|
-
return { applied: false, status: "empty", headMoved: false, detail: "no changes to apply" };
|
|
251
|
-
if (containsSecretLikeToken(patch))
|
|
252
|
-
return {
|
|
253
|
-
applied: false,
|
|
254
|
-
status: "rejected",
|
|
255
|
-
headMoved: false,
|
|
256
|
-
detail: "patch contains a secret-like token; refusing apply",
|
|
257
|
-
};
|
|
258
|
-
// Best-effort: did the PROJECT advance past where this thread branched? (a
|
|
259
|
-
// warning, not a blocker — git apply --3way still merges or fails loudly.)
|
|
260
|
-
let headMoved = false;
|
|
261
|
-
try {
|
|
262
|
-
const head = (await git(projectRoot, ["rev-parse", "HEAD"])).stdout.trim();
|
|
263
|
-
const mb = (await git(projectRoot, ["merge-base", "HEAD", base])).stdout.trim();
|
|
264
|
-
headMoved = mb !== "" && head !== "" && mb !== head;
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
/* best-effort */
|
|
268
|
-
}
|
|
269
|
-
const mode = (["apply", "branch", "commit", "pr"].includes(opts.mode) ? opts.mode : "apply");
|
|
270
|
-
const delivered = await deliver(projectRoot, patch, {
|
|
271
|
-
mode,
|
|
272
|
-
branch: opts.branch,
|
|
273
|
-
message: opts.message,
|
|
274
|
-
});
|
|
275
|
-
if (delivered.applied) {
|
|
276
|
-
// Re-base the thread on the new project state so the next apply diffs only new work.
|
|
277
|
-
threads.setThreadWorktree(id, ws.worktree_path, await snapshotTree(ws.worktree_path));
|
|
278
|
-
}
|
|
279
|
-
const status = !delivered.applied
|
|
280
|
-
? "conflict"
|
|
281
|
-
: mode === "branch"
|
|
282
|
-
? "branched"
|
|
283
|
-
: mode === "commit"
|
|
284
|
-
? "committed"
|
|
285
|
-
: mode === "pr"
|
|
286
|
-
? "pr_opened"
|
|
287
|
-
: "applied";
|
|
288
|
-
return { applied: delivered.applied, status, headMoved, detail: delivered.detail ?? null };
|
|
289
|
-
}
|
|
290
|
-
function controlServices(interactions, threads) {
|
|
291
|
-
const secretStore = new SecretStore();
|
|
292
|
-
const setupJobs = createSetupJobManager();
|
|
293
|
-
mkdirSync(NO_PROJECT_ROOT, { recursive: true, mode: 0o700 });
|
|
294
|
-
return {
|
|
295
|
-
createThread: async (input) => threads.createThread((input ?? {})),
|
|
296
|
-
listThreads: async () => ({ threads: threads.listThreads() }),
|
|
297
|
-
threadDetail: async (id) => {
|
|
298
|
-
const thread = threads.getThread(id);
|
|
299
|
-
if (!thread)
|
|
300
|
-
throw Object.assign(new Error(`no such thread: ${id}`), { status: 404 });
|
|
301
|
-
return {
|
|
302
|
-
thread: thread,
|
|
303
|
-
sessions: threads.sessionsForThread(id),
|
|
304
|
-
turns: threads.turnsFor(id),
|
|
305
|
-
};
|
|
306
|
-
},
|
|
307
|
-
createThreadTurn: async (id, prompt, opts) => threads.createTurn(id, prompt, {
|
|
308
|
-
kind: opts.kind,
|
|
309
|
-
parentRunId: opts.parentRunId,
|
|
310
|
-
planRunId: opts.planRunId,
|
|
311
|
-
attachments: resolveAttachments(opts.attachments),
|
|
312
|
-
}),
|
|
313
|
-
updateThread: async (id, patch) => threads.updateThread(id, {
|
|
314
|
-
title: patch.title,
|
|
315
|
-
state: patch.state,
|
|
316
|
-
primaryHarness: patch.primaryHarness,
|
|
317
|
-
eligibleHarnesses: patch.eligibleHarnesses,
|
|
318
|
-
}),
|
|
319
|
-
applyThread: async (id, opts) => applyThreadDiff(threads, id, opts),
|
|
320
|
-
setTurnEnqueueError: (turnId, message, code, retryable) => threads.setTurnEnqueueError(turnId, message, code, retryable ?? true),
|
|
321
|
-
// User-level trust surface (narrow by design): list per-repo trust files
|
|
322
|
-
// and grant/revoke ONE flag — the same file/writer `claudexor trust` owns.
|
|
323
|
-
listTrust: listTrustService,
|
|
324
|
-
updateTrust: updateTrustService,
|
|
325
|
-
pendingInteractions: (runId) => interactions.pendingForRun(runId),
|
|
326
|
-
answerInteraction: (runId, interactionId, answers) => interactions.answer(runId, interactionId, answers),
|
|
327
|
-
harnesses: async () => {
|
|
328
|
-
const statuses = await buildGateway({ includeFakes: false }).statusAll({
|
|
329
|
-
cwd: NO_PROJECT_ROOT,
|
|
330
|
-
});
|
|
331
|
-
// The doctor's configured-model truth check rides the status DTO
|
|
332
|
-
// so the UI renders the same honesty the CLI prints (never a green
|
|
333
|
-
// harness with a doomed configured model).
|
|
334
|
-
const cfg = loadConfig(NO_PROJECT_ROOT);
|
|
335
|
-
return {
|
|
336
|
-
harnesses: await Promise.all(statuses.map(async (s) => {
|
|
337
|
-
const configured = cfg.global.harnesses[s.id]?.default_model ?? null;
|
|
338
|
-
if (!configured)
|
|
339
|
-
return { ...s, configuredModel: null, configuredModelCheck: null };
|
|
340
|
-
const truth = await harnessModels(s.id, NO_PROJECT_ROOT, true);
|
|
341
|
-
const check = validateModel(configured, truth.models.map((m) => m.id), truth.source === "api" ? "api" : "manifest");
|
|
342
|
-
return { ...s, configuredModel: configured, configuredModelCheck: check };
|
|
343
|
-
})),
|
|
344
|
-
};
|
|
345
|
-
},
|
|
346
|
-
harnessModels: async (input) => harnessModels(input.harnessId, NO_PROJECT_ROOT),
|
|
347
|
-
// Derived catalog for external agents; same composer the CLI verb and the
|
|
348
|
-
// MCP tool use, so all three surfaces answer identically.
|
|
349
|
-
agentCapabilities: async () => buildAgentCapabilityCatalog(),
|
|
350
|
-
createSetupJob: async (input) => setupJobs.create(input),
|
|
351
|
-
listSetupJobs: async () => ({ jobs: setupJobs.list() }),
|
|
352
|
-
setupJobStatus: async (input) => setupJobs.status(input),
|
|
353
|
-
cancelSetupJob: async (input) => setupJobs.cancel(input),
|
|
354
|
-
confirmSetupJob: async (input) => setupJobs.confirm(input),
|
|
355
|
-
settings: async () => {
|
|
356
|
-
const cfg = loadConfig(NO_PROJECT_ROOT);
|
|
357
|
-
return {
|
|
358
|
-
sources: cfg.sources,
|
|
359
|
-
defaultPortfolio: cfg.global.default_portfolio,
|
|
360
|
-
interactionTimeoutMs: cfg.global.interaction_timeout_ms,
|
|
361
|
-
routing: {
|
|
362
|
-
defaultPolicy: cfg.global.routing.default_policy,
|
|
363
|
-
primaryHarness: cfg.global.routing.primary_harness,
|
|
364
|
-
eligibleHarnesses: cfg.global.routing.eligible_harnesses,
|
|
365
|
-
envInheritance: cfg.global.routing.env_inheritance,
|
|
366
|
-
authPreference: cfg.global.routing.auth_preference,
|
|
367
|
-
},
|
|
368
|
-
budget: {
|
|
369
|
-
maxUsdPerRun: cfg.global.budget.max_usd_per_run,
|
|
370
|
-
},
|
|
371
|
-
runtime: {
|
|
372
|
-
reviewerTimeoutMs: cfg.global.runtime.reviewer_timeout_ms,
|
|
373
|
-
harnessInactivityTimeoutMs: cfg.global.runtime.harness_inactivity_timeout_ms,
|
|
374
|
-
transientRetry: {
|
|
375
|
-
maxRetries: cfg.global.runtime.transient_retry.max_retries,
|
|
376
|
-
initialDelayMs: cfg.global.runtime.transient_retry.initial_delay_ms,
|
|
377
|
-
maxDelayMs: cfg.global.runtime.transient_retry.max_delay_ms,
|
|
378
|
-
},
|
|
379
|
-
},
|
|
380
|
-
harnesses: Object.fromEntries(Object.entries(cfg.global.harnesses).map(([id, h]) => [
|
|
381
|
-
id,
|
|
382
|
-
{
|
|
383
|
-
enabled: h.enabled,
|
|
384
|
-
defaultModel: h.default_model,
|
|
385
|
-
effort: h.effort,
|
|
386
|
-
maxTurns: h.max_turns,
|
|
387
|
-
maxRounds: h.max_rounds,
|
|
388
|
-
maxUsd: h.max_usd,
|
|
389
|
-
toolsAllow: h.tools_allow,
|
|
390
|
-
toolsDeny: h.tools_deny,
|
|
391
|
-
fallbackModel: h.fallback_model,
|
|
392
|
-
web: h.web,
|
|
393
|
-
authPreference: h.auth_preference,
|
|
394
|
-
},
|
|
395
|
-
])),
|
|
396
|
-
};
|
|
397
|
-
},
|
|
398
|
-
updateSettings: async (patch) => {
|
|
399
|
-
// FAIL LOUDLY on malformed patches: a typo'd field name or bad enum must
|
|
400
|
-
// surface as a 4xx, never be silently dropped.
|
|
401
|
-
const p = ControlSettingsUpdateRequest.parse(patch ?? {});
|
|
402
|
-
await assertSettingsPatchValid(p);
|
|
403
|
-
const nullableName = (value, current) => {
|
|
404
|
-
if (value === undefined)
|
|
405
|
-
return current;
|
|
406
|
-
if (value === null)
|
|
407
|
-
return null;
|
|
408
|
-
return value;
|
|
409
|
-
};
|
|
410
|
-
const updated = updateGlobalConfig((cfg) => ({
|
|
411
|
-
...cfg,
|
|
412
|
-
default_portfolio: p.defaultPortfolio ?? cfg.default_portfolio,
|
|
413
|
-
interaction_timeout_ms: p.interactionTimeoutMs ?? cfg.interaction_timeout_ms,
|
|
414
|
-
routing: {
|
|
415
|
-
...cfg.routing,
|
|
416
|
-
primary_harness: nullableName(p.primaryHarness, cfg.routing.primary_harness),
|
|
417
|
-
default_policy: p.routingPolicy ?? cfg.routing.default_policy,
|
|
418
|
-
env_inheritance: p.envInheritance ?? cfg.routing.env_inheritance,
|
|
419
|
-
eligible_harnesses: p.eligibleHarnesses ?? cfg.routing.eligible_harnesses,
|
|
420
|
-
auth_preference: p.authPreference ?? cfg.routing.auth_preference,
|
|
421
|
-
},
|
|
422
|
-
budget: {
|
|
423
|
-
...cfg.budget,
|
|
424
|
-
max_usd_per_run: p.clearMaxUsdPerRun === true ? null : (p.maxUsdPerRun ?? cfg.budget.max_usd_per_run),
|
|
263
|
+
const authReadiness = new AuthReadinessService(buildGateway({ includeFakes: false }), {
|
|
264
|
+
cwd: NO_PROJECT_ROOT,
|
|
265
|
+
});
|
|
266
|
+
const setupBinding = new SetupLifecycleBinding(setupStoreSlot, (store) => createSetupJobManager({
|
|
267
|
+
rootDir: daemonDir(),
|
|
268
|
+
store,
|
|
269
|
+
onCredentialStateMayHaveChanged: (harness) => authReadiness.invalidate(harness),
|
|
270
|
+
}));
|
|
271
|
+
let control = null;
|
|
272
|
+
shutdownRuntime = new DaemonRuntimeShutdown({
|
|
273
|
+
daemon: server,
|
|
274
|
+
setup: setupBinding,
|
|
275
|
+
control: () => control,
|
|
276
|
+
journal: {
|
|
277
|
+
close: () => {
|
|
278
|
+
if (quotaPollTimer)
|
|
279
|
+
clearInterval(quotaPollTimer);
|
|
280
|
+
threads.close();
|
|
281
|
+
journalManager.close();
|
|
425
282
|
},
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
// extractQuestionsFromPlan parses into single/multi/text questions with options.
|
|
469
|
-
// The instruction + its parser are a co-located contract pair in spec.ts.
|
|
470
|
-
// Prior tiers' answers: carried so each round goes DEEPER (adaptive interview).
|
|
471
|
-
const priorDecisions = Array.isArray(p["priorDecisions"])
|
|
472
|
-
? p["priorDecisions"].filter((d) => !!d &&
|
|
473
|
-
typeof d === "object" &&
|
|
474
|
-
typeof d.question === "string" &&
|
|
475
|
-
typeof d.answer === "string")
|
|
476
|
-
: [];
|
|
477
|
-
const plan = await new Orchestrator({ registry: buildRegistry() }).run({
|
|
478
|
-
repoRoot,
|
|
479
|
-
prompt: buildGroundingPrompt(prompt, priorDecisions),
|
|
480
|
-
mode: "plan",
|
|
481
|
-
harnesses: Array.isArray(p["harnesses"])
|
|
482
|
-
? p["harnesses"].filter((x) => typeof x === "string")
|
|
483
|
-
: undefined,
|
|
484
|
-
access: "readonly",
|
|
283
|
+
},
|
|
284
|
+
log: (message) => logLine(logPath(), message),
|
|
285
|
+
});
|
|
286
|
+
// The daemon owns its services whether or not the HTTP surface is up —
|
|
287
|
+
// the startup retention pass below consumes them directly.
|
|
288
|
+
const selfClient = new DaemonClient(socketPath, token);
|
|
289
|
+
const services = controlServices(interactions, () => projectStoreSlot.current(), threads, setupBinding, journalManager, authReadiness, resources, () => quotaStoreSlot.current(), () => selfClient.list());
|
|
290
|
+
control =
|
|
291
|
+
process.env.CLAUDEXOR_NO_CONTROL_API === "1"
|
|
292
|
+
? null
|
|
293
|
+
: new DaemonControlApiServer({
|
|
294
|
+
token,
|
|
295
|
+
daemon: new DaemonClient(socketPath, token),
|
|
296
|
+
port: Number(process.env.CLAUDEXOR_CONTROL_PORT ?? 0),
|
|
297
|
+
bus,
|
|
298
|
+
services,
|
|
299
|
+
});
|
|
300
|
+
lifecycle = armDaemonLifecycle({
|
|
301
|
+
daemonDir: daemonDir(),
|
|
302
|
+
logPath: logPath(),
|
|
303
|
+
beginShutdown: (reason) => shutdownRuntime.beginShutdown(reason),
|
|
304
|
+
});
|
|
305
|
+
await setupBinding.start();
|
|
306
|
+
if (!shutdownRuntime.requested())
|
|
307
|
+
await server.start();
|
|
308
|
+
if (!shutdownRuntime.requested()) {
|
|
309
|
+
appendFileSync(logPath(), `[${new Date().toISOString()}] claudexord listening on ${socketPath}\n`);
|
|
310
|
+
}
|
|
311
|
+
if (control && !shutdownRuntime.requested()) {
|
|
312
|
+
const controlAddr = await control.start();
|
|
313
|
+
if (!shutdownRuntime.requested()) {
|
|
314
|
+
writeFileSync(join(daemonDir(), "control-api.json"), `${JSON.stringify({ ...controlAddr, tokenPath: join(daemonDir(), "token") }, null, 2)}\n`, { mode: 0o600 });
|
|
315
|
+
appendFileSync(logPath(), `[${new Date().toISOString()}] claudexor control-api listening on http://${controlAddr.host}:${controlAddr.port}\n`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else if (!control && !shutdownRuntime.requested()) {
|
|
319
|
+
appendFileSync(logPath(), `[${new Date().toISOString()}] claudexor control-api disabled by CLAUDEXOR_NO_CONTROL_API=1\n`);
|
|
320
|
+
}
|
|
321
|
+
if (!shutdownRuntime.requested()) {
|
|
322
|
+
scheduleStartupRetention(services.runRetention, {
|
|
323
|
+
logPath: logPath(),
|
|
324
|
+
shuttingDown: () => shutdownRuntime.requested(),
|
|
485
325
|
});
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
// (e.g. tests:[123]) rather than silently filtering it.
|
|
507
|
-
const strArr = (v, field) => {
|
|
508
|
-
if (v === undefined)
|
|
509
|
-
return undefined;
|
|
510
|
-
if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
|
|
511
|
-
throw new Error(`spec freeze: "${field}" must be an array of strings`);
|
|
326
|
+
}
|
|
327
|
+
await shutdownRuntime.wait();
|
|
328
|
+
lifecycle.finalize();
|
|
329
|
+
appendFileSync(logPath(), `[${new Date().toISOString()}] claudexord shut down\n`);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
try {
|
|
333
|
+
appendFileSync(logPath(), `[${new Date().toISOString()}] daemon lifecycle FAILED: ${redactSecrets(error instanceof Error ? `${error.name}: ${error.message}` : String(error))}\n`);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
/* preserve the lifecycle failure */
|
|
337
|
+
}
|
|
338
|
+
if (shutdownRuntime) {
|
|
339
|
+
try {
|
|
340
|
+
await shutdownRuntime.beginShutdown("startup failure");
|
|
341
|
+
lifecycle?.finalize();
|
|
342
|
+
}
|
|
343
|
+
catch (shutdownError) {
|
|
344
|
+
try {
|
|
345
|
+
appendFileSync(logPath(), `[${new Date().toISOString()}] shutdown FAILED: ${redactSecrets(shutdownError instanceof Error ? shutdownError.message : String(shutdownError))}\n`);
|
|
512
346
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
// sent as `answers` (the v0.13 freeze-drops-prior-tiers bug, #8/#9).
|
|
529
|
-
const priorDecisions = Array.isArray(p["priorDecisions"])
|
|
530
|
-
? p["priorDecisions"].filter((d) => !!d &&
|
|
531
|
-
typeof d.question === "string" &&
|
|
532
|
-
typeof d.answer === "string")
|
|
533
|
-
: [];
|
|
534
|
-
const priorLines = priorDecisions.map((d) => `Interview (prior tier) — ${d.question} → ${d.answer}`);
|
|
535
|
-
const explicitTradeoffs = strArr(p["decided_tradeoffs"], "decided_tradeoffs") ?? [];
|
|
536
|
-
const mergedTradeoffs = [...priorLines, ...explicitTradeoffs];
|
|
537
|
-
const spec = await freezeSpecFromGrounding(prompt, plan, {
|
|
538
|
-
answers,
|
|
539
|
-
summary: strOpt(p["summary"], "summary"),
|
|
540
|
-
success_criteria: strArr(p["success_criteria"], "success_criteria"),
|
|
541
|
-
non_goals: strArr(p["non_goals"], "non_goals"),
|
|
542
|
-
forbidden_approaches: strArr(p["forbidden_approaches"], "forbidden_approaches"),
|
|
543
|
-
// undefined-when-empty keeps single-tier behavior byte-identical.
|
|
544
|
-
decided_tradeoffs: mergedTradeoffs.length ? mergedTradeoffs : undefined,
|
|
545
|
-
tests: strArr(p["tests"], "tests"),
|
|
546
|
-
});
|
|
547
|
-
const persisted = persistSpec(repoRoot, spec, plan);
|
|
548
|
-
// specPath = the frozen SpecPack file an Implement run reads (a bare specId
|
|
549
|
-
// does not load content). Single producer; the layout matches persistSpec.
|
|
550
|
-
return {
|
|
551
|
-
specId: spec.id,
|
|
552
|
-
specDir: persisted.specDir,
|
|
553
|
-
specPath: join(persisted.specDir, "spec.json"),
|
|
554
|
-
specHash: persisted.specHash,
|
|
555
|
-
changes: persisted.changes,
|
|
556
|
-
};
|
|
557
|
-
},
|
|
558
|
-
};
|
|
347
|
+
catch {
|
|
348
|
+
/* preserve the shutdown failure */
|
|
349
|
+
}
|
|
350
|
+
releaseWriterLease = false;
|
|
351
|
+
throw new AggregateError([error, shutdownError], "claudexord failed and could not complete shutdown");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
if (quotaPollTimer)
|
|
358
|
+
clearInterval(quotaPollTimer);
|
|
359
|
+
if (releaseWriterLease)
|
|
360
|
+
writerLease.release();
|
|
361
|
+
}
|
|
559
362
|
}
|
|
560
363
|
main().catch((err) => {
|
|
561
364
|
process.stderr.write(`claudexord: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
562
|
-
process.
|
|
365
|
+
process.exitCode = 1;
|
|
563
366
|
});
|
|
564
367
|
//# sourceMappingURL=claudexord.js.map
|