@claudexor/cli 2.1.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/accounts-projection.d.ts +26 -0
  2. package/dist/accounts-projection.d.ts.map +1 -0
  3. package/dist/accounts-projection.js +90 -0
  4. package/dist/accounts-projection.js.map +1 -0
  5. package/dist/acp-surface-runner.d.ts.map +1 -1
  6. package/dist/acp-surface-runner.js +12 -16
  7. package/dist/acp-surface-runner.js.map +1 -1
  8. package/dist/bridge-serve.d.ts +9 -0
  9. package/dist/bridge-serve.d.ts.map +1 -1
  10. package/dist/bridge-serve.js +20 -1
  11. package/dist/bridge-serve.js.map +1 -1
  12. package/dist/claude-oauth-usage.d.ts +8 -2
  13. package/dist/claude-oauth-usage.d.ts.map +1 -1
  14. package/dist/claude-oauth-usage.js +27 -12
  15. package/dist/claude-oauth-usage.js.map +1 -1
  16. package/dist/claude-statusline.d.ts +5 -2
  17. package/dist/claude-statusline.d.ts.map +1 -1
  18. package/dist/claude-statusline.js +4 -2
  19. package/dist/claude-statusline.js.map +1 -1
  20. package/dist/claudexord.d.ts +7 -1
  21. package/dist/claudexord.d.ts.map +1 -1
  22. package/dist/claudexord.js +116 -19
  23. package/dist/claudexord.js.map +1 -1
  24. package/dist/cli.js +105 -276
  25. package/dist/cli.js.map +1 -1
  26. package/dist/codex-quota-source.d.ts +9 -3
  27. package/dist/codex-quota-source.d.ts.map +1 -1
  28. package/dist/codex-quota-source.js +82 -9
  29. package/dist/codex-quota-source.js.map +1 -1
  30. package/dist/command-flags.d.ts +1 -1
  31. package/dist/command-flags.d.ts.map +1 -1
  32. package/dist/command-flags.js +8 -9
  33. package/dist/command-flags.js.map +1 -1
  34. package/dist/command-registry.d.ts.map +1 -1
  35. package/dist/command-registry.js +19 -54
  36. package/dist/command-registry.js.map +1 -1
  37. package/dist/control-services.d.ts +163 -349
  38. package/dist/control-services.d.ts.map +1 -1
  39. package/dist/control-services.js +116 -146
  40. package/dist/control-services.js.map +1 -1
  41. package/dist/credential-commands.d.ts.map +1 -1
  42. package/dist/credential-commands.js +70 -8
  43. package/dist/credential-commands.js.map +1 -1
  44. package/dist/daemon-run.d.ts +49 -7
  45. package/dist/daemon-run.d.ts.map +1 -1
  46. package/dist/daemon-run.js +152 -30
  47. package/dist/daemon-run.js.map +1 -1
  48. package/dist/decision.js +2 -2
  49. package/dist/decision.js.map +1 -1
  50. package/dist/delegation-belt-descriptor.d.ts +43 -0
  51. package/dist/delegation-belt-descriptor.d.ts.map +1 -0
  52. package/dist/delegation-belt-descriptor.js +75 -0
  53. package/dist/delegation-belt-descriptor.js.map +1 -0
  54. package/dist/live.d.ts +5 -2
  55. package/dist/live.d.ts.map +1 -1
  56. package/dist/live.js +29 -12
  57. package/dist/live.js.map +1 -1
  58. package/dist/mcp-runner.d.ts.map +1 -1
  59. package/dist/mcp-runner.js +49 -22
  60. package/dist/mcp-runner.js.map +1 -1
  61. package/dist/native-login.js +3 -4
  62. package/dist/native-login.js.map +1 -1
  63. package/dist/plan-question-loop.d.ts +46 -0
  64. package/dist/plan-question-loop.d.ts.map +1 -0
  65. package/dist/plan-question-loop.js +149 -0
  66. package/dist/plan-question-loop.js.map +1 -0
  67. package/dist/plugins.d.ts.map +1 -1
  68. package/dist/plugins.js +1 -3
  69. package/dist/plugins.js.map +1 -1
  70. package/dist/primary-output.d.ts.map +1 -1
  71. package/dist/primary-output.js +9 -17
  72. package/dist/primary-output.js.map +1 -1
  73. package/dist/profile-compatibility.d.ts +6 -0
  74. package/dist/profile-compatibility.d.ts.map +1 -0
  75. package/dist/profile-compatibility.js +21 -0
  76. package/dist/profile-compatibility.js.map +1 -0
  77. package/dist/profile-registration.d.ts.map +1 -1
  78. package/dist/profile-registration.js +16 -0
  79. package/dist/profile-registration.js.map +1 -1
  80. package/dist/project-command.d.ts.map +1 -1
  81. package/dist/project-command.js +41 -2
  82. package/dist/project-command.js.map +1 -1
  83. package/dist/quota-command.js +8 -1
  84. package/dist/quota-command.js.map +1 -1
  85. package/dist/release-command.d.ts +12 -0
  86. package/dist/release-command.d.ts.map +1 -0
  87. package/dist/release-command.js +70 -0
  88. package/dist/release-command.js.map +1 -0
  89. package/dist/release.d.ts +59 -0
  90. package/dist/release.d.ts.map +1 -1
  91. package/dist/release.js +181 -0
  92. package/dist/release.js.map +1 -1
  93. package/dist/repl.d.ts +4 -3
  94. package/dist/repl.d.ts.map +1 -1
  95. package/dist/repl.js +27 -4
  96. package/dist/repl.js.map +1 -1
  97. package/dist/request-preflight.d.ts.map +1 -1
  98. package/dist/request-preflight.js +0 -2
  99. package/dist/request-preflight.js.map +1 -1
  100. package/dist/retention-service.d.ts.map +1 -1
  101. package/dist/retention-service.js +21 -0
  102. package/dist/retention-service.js.map +1 -1
  103. package/dist/settings-command.d.ts +38 -0
  104. package/dist/settings-command.d.ts.map +1 -1
  105. package/dist/settings-command.js +97 -15
  106. package/dist/settings-command.js.map +1 -1
  107. package/dist/settings-service.d.ts +5 -5
  108. package/dist/settings-service.d.ts.map +1 -1
  109. package/dist/settings-service.js +4 -1
  110. package/dist/settings-service.js.map +1 -1
  111. package/dist/setup-job-store.d.ts +9 -9
  112. package/dist/setup-job-store.d.ts.map +1 -1
  113. package/dist/setup-jobs.d.ts +13 -13
  114. package/dist/setup-jobs.d.ts.map +1 -1
  115. package/dist/thread-select.d.ts +6 -2
  116. package/dist/thread-select.d.ts.map +1 -1
  117. package/dist/thread-select.js +20 -3
  118. package/dist/thread-select.js.map +1 -1
  119. package/package.json +23 -24
  120. package/dist/orchestrate-options.d.ts +0 -9
  121. package/dist/orchestrate-options.d.ts.map +0 -1
  122. package/dist/orchestrate-options.js +0 -16
  123. package/dist/orchestrate-options.js.map +0 -1
  124. package/dist/spec.d.ts +0 -73
  125. package/dist/spec.d.ts.map +0 -1
  126. package/dist/spec.js +0 -466
  127. package/dist/spec.js.map +0 -1
package/dist/cli.js CHANGED
@@ -5,11 +5,11 @@ import { createHash } from "node:crypto";
5
5
  import { Readable, Transform } from "node:stream";
6
6
  import { dirname, join, relative, resolve, sep } from "node:path";
7
7
  import { ArtifactStore } from "@claudexor/artifact-store";
8
- import { CLAUDEXOR_VERSION, containsSecretLikeToken, noProjectRepoRoot, readTextSafe, userConfigDir, } from "@claudexor/util";
9
- import { checkName } from "./release.js";
10
- import { serveAcpBridge, serveMcpBridge } from "./bridge-serve.js";
8
+ import { CLAUDEXOR_VERSION, noProjectRepoRoot, readTextSafe, userConfigDir } from "@claudexor/util";
9
+ import { releaseCommand } from "./release-command.js";
10
+ import { serveAcpBridge, serveBeltBridge, serveMcpBridge } from "./bridge-serve.js";
11
11
  import { initProjectConfig } from "@claudexor/config";
12
- import { DecisionRecord, ControlSpecSession, EffortHint, ExternalContextPolicy, ModeKind as ModeKindSchema, RoutingGoal, ControlThreadListResponse, RunTelemetry, StructuredOutputConformance, TaskContract, } from "@claudexor/schema";
12
+ import { DecisionRecord, EffortHint, ExternalContextPolicy, ModeKind as ModeKindSchema, RoutingGoal, ControlThreadListResponse, RunTelemetry, StructuredOutputConformance, TaskContract, runOutcomeLabel, } from "@claudexor/schema";
13
13
  import { flagBool, flagStr, flagStringList, flagValues, parseArgs, requiredStringFlagError, } from "./args.js";
14
14
  import { print, printJson, printJsonLine, printUsageError, statusGlyph } from "./cli-io.js";
15
15
  import { pickResumableThread } from "./thread-select.js";
@@ -21,7 +21,8 @@ import { controlApiFetch, followRun } from "./live.js";
21
21
  import { retryCommand, runAgainCommand } from "./retry-command.js";
22
22
  import { assertCliRunParamsHaveNoInlineSecrets } from "./run-secret-scan.js";
23
23
  import { openLocalAttachment, resolveLocalAttachment, } from "./local-attachment.js";
24
- import { connectDaemonIfRunning, daemonOutcomeSummary, ensureDaemon, enqueueAndAwait, exitCodeForState, fetchApplyEligibility, runStatusForCli, } from "./daemon-run.js";
24
+ import { connectDaemonIfRunning, daemonOutcomeSummary, ensureDaemon, enqueueAndAwait, exitCodeForState, fetchApplyEligibility, fetchCouncil, fetchOutcomeBanner, } from "./daemon-run.js";
25
+ import { runPlanQuestionLoop } from "./plan-question-loop.js";
25
26
  import { resolveDecisionBody } from "./decision.js";
26
27
  import { primaryOutputForCli } from "./primary-output.js";
27
28
  import { PLUGIN_TARGETS, PLUGIN_VERBS, formatPluginResult, pluginCommandErrorResult, runPluginCommand, } from "./plugins.js";
@@ -30,13 +31,11 @@ import { settingsCommand } from "./settings-command.js";
30
31
  import { quotaCommand } from "./quota-command.js";
31
32
  import { trustCommand } from "./trust-command.js";
32
33
  import { projectCommand } from "./project-command.js";
33
- import { loadFrozenSpec, readAnswers, resolveRunTestCommands } from "./spec.js";
34
- import { parseAutonomy } from "./orchestrate-options.js";
35
34
  import { runRepl } from "./repl.js";
36
35
  import { parseProtectedPathApprovalFlags, parseTestCommandFlags, parseReviewerEffortFlags, parseReviewerModelFlags, parseReviewerPanelFlags, } from "./run-options.js";
37
36
  const CLI_VERSION = CLAUDEXOR_VERSION;
38
37
  const HELP = renderHelp(CLI_VERSION);
39
- const MODES = new Set(["ask", "plan", "audit", "agent", "orchestrate"]);
38
+ const MODES = new Set(["ask", "plan", "agent"]);
40
39
  function normalizeMode(s) {
41
40
  const trimmed = s.trim();
42
41
  const parsed = ModeKindSchema.safeParse(trimmed);
@@ -240,44 +239,8 @@ async function orchestrate(args, mode, json, forced = {}) {
240
239
  return printUsageError(json, "claudexor: stdin prompt (`-`) was empty");
241
240
  }
242
241
  }
243
- const specPath = flagStr(args, "spec");
244
- let loadedSpec = null;
245
- try {
246
- loadedSpec = specPath ? loadFrozenSpec(specPath) : null;
247
- }
248
- catch (err) {
249
- return printUsageError(json, `claudexor: ${err instanceof Error ? err.message : String(err)}`);
250
- }
251
- const prompt = loadedSpec
252
- ? [
253
- rawPrompt || loadedSpec.spec.intent.raw,
254
- "",
255
- "Use this frozen Claudexor SpecPack as the contract. Do not re-litigate settled choices; implement against the acceptance criteria and tests.",
256
- "",
257
- `Spec id: ${loadedSpec.spec.id} v${loadedSpec.spec.version}`,
258
- `Spec hash: ${loadedSpec.specHash}`,
259
- "",
260
- "## Summary",
261
- loadedSpec.spec.summary || "(none)",
262
- "",
263
- "## Acceptance Criteria",
264
- ...(loadedSpec.spec.success_criteria.length
265
- ? loadedSpec.spec.success_criteria.map((c) => `- [${c.id}] ${c.behavior}`)
266
- : ["- (none)"]),
267
- "",
268
- "## Non-goals",
269
- ...(loadedSpec.spec.non_goals.length
270
- ? loadedSpec.spec.non_goals.map((x) => `- ${x}`)
271
- : ["- (none)"]),
272
- "",
273
- "## Forbidden approaches",
274
- ...(loadedSpec.spec.forbidden_approaches.length
275
- ? loadedSpec.spec.forbidden_approaches.map((x) => `- ${x}`)
276
- : ["- (none)"]),
277
- ].join("\n")
278
- : rawPrompt;
279
- const spec = loadedSpec?.spec ?? null;
280
- if (!prompt && mode !== "audit") {
242
+ const prompt = rawPrompt;
243
+ if (!prompt) {
281
244
  return printUsageError(json, "claudexor: missing prompt");
282
245
  }
283
246
  const portfolioRaw = flagStr(args, "portfolio");
@@ -297,10 +260,10 @@ async function orchestrate(args, mode, json, forced = {}) {
297
260
  let resolvedAccess = undefined;
298
261
  let resolvedEffort;
299
262
  let paidBudget;
300
- let maxToolCalls;
301
263
  let nFlag;
302
264
  let attemptsFlag;
303
- let autonomy;
265
+ let delegate;
266
+ let council;
304
267
  let resolvedSynthesis = undefined;
305
268
  let resolvedHarnesses;
306
269
  let resolvedPrimaryHarness;
@@ -324,10 +287,10 @@ async function orchestrate(args, mode, json, forced = {}) {
324
287
  resolvedModel = flagStr(args, "model");
325
288
  const maxUsd = floatFlag(args, "max-usd");
326
289
  paidBudget = maxUsd === undefined ? undefined : { kind: "finite", maxUsd };
327
- maxToolCalls = intFlag(args, "max-tool-calls");
328
290
  nFlag = intFlag(args, "n");
329
291
  attemptsFlag = intFlag(args, "attempts");
330
- autonomy = parseAutonomy(flagStr(args, "autonomy"));
292
+ delegate = flagBool(args, "delegate") ? true : undefined;
293
+ council = flagBool(args, "council") ? true : undefined;
331
294
  resolvedSynthesis = synthesisMode(args);
332
295
  attachmentRequest = attachmentInputs(args);
333
296
  resolvedProtectedPathApprovals = protectedPathApprovals(args);
@@ -364,7 +327,7 @@ async function orchestrate(args, mode, json, forced = {}) {
364
327
  let tests;
365
328
  try {
366
329
  const cliTests = testCommands(args) ?? [];
367
- tests = resolveRunTestCommands(cliTests, spec);
330
+ tests = cliTests.length > 0 ? cliTests : undefined;
368
331
  assertCliRunParamsHaveNoInlineSecrets({
369
332
  prompt,
370
333
  instructions: resolvedInstructions,
@@ -384,24 +347,21 @@ async function orchestrate(args, mode, json, forced = {}) {
384
347
  web: resolvedWebPolicy,
385
348
  externalContextPolicy: resolvedWebPolicy,
386
349
  synthesis: resolvedSynthesis,
387
- autonomy,
388
- specId: spec?.id,
389
- specHash: loadedSpec?.specHash,
390
- specPath: loadedSpec?.specPath,
391
350
  });
392
351
  }
393
352
  catch (err) {
394
353
  return printUsageError(json, `claudexor: ${err instanceof Error ? err.message : String(err)}`);
395
354
  }
396
- if (autonomy !== undefined && mode !== "orchestrate") {
397
- return printUsageError(json, `claudexor: --autonomy only applies to 'orchestrate' (got mode '${mode}')`);
355
+ if (delegate && mode !== "agent") {
356
+ return printUsageError(json, `claudexor: --delegate is an agent strategy (got mode '${mode}')`);
398
357
  }
399
- if (maxToolCalls !== undefined && mode !== "orchestrate") {
400
- return printUsageError(json, "claudexor: --max-tool-calls only applies to orchestrate runs");
358
+ if (council && mode !== "plan") {
359
+ return printUsageError(json, `claudexor: --council is a plan strategy (got mode '${mode}')`);
401
360
  }
402
361
  return daemonRun(args, json, {
403
362
  mode,
404
- autonomy,
363
+ delegate,
364
+ council,
405
365
  prompt: prompt || "audit this repository",
406
366
  instructions: resolvedInstructions,
407
367
  maxSeconds: resolvedMaxSeconds,
@@ -412,7 +372,6 @@ async function orchestrate(args, mode, json, forced = {}) {
412
372
  paidBudget,
413
373
  routingGoal: routingGoal?.success ? routingGoal.data : undefined,
414
374
  credentialProfileId,
415
- maxToolCalls,
416
375
  reviewerPanel: resolvedReviewerPanel,
417
376
  reviewerModels: resolvedReviewerModels,
418
377
  reviewerEfforts: reviewerEffortOverrides,
@@ -426,9 +385,6 @@ async function orchestrate(args, mode, json, forced = {}) {
426
385
  resolvedModel,
427
386
  nFlag,
428
387
  attemptsFlag,
429
- specId: spec?.id,
430
- specHash: loadedSpec?.specHash,
431
- specPath: loadedSpec?.specPath,
432
388
  attachmentRequest,
433
389
  forced,
434
390
  });
@@ -459,10 +415,11 @@ async function daemonRun(args, json, p) {
459
415
  process.stderr.write(`claudexor: ${err instanceof Error ? err.message : String(err)}\n`);
460
416
  return 1;
461
417
  }
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.
418
+ // Thread continuation (W13/D10): --thread <id> targets a thread explicitly;
419
+ // --resume picks the most recently updated one. When a threadId is present
420
+ // the run is enqueued through POST /threads/:id/turns (enqueueAndAwait routes
421
+ // it there) the ONE turn-creation path that owns scope, lineage, and the
422
+ // continuation packet. POST /runs itself now refuses threadId.
466
423
  let threadId = flagStr(args, "thread");
467
424
  if (threadId === undefined && flagBool(args, "resume")) {
468
425
  try {
@@ -474,11 +431,13 @@ async function daemonRun(args, json, p) {
474
431
  // Parse through the typed DTO — the field is camelCase `updatedAt`, and a
475
432
  // hand-rolled `updated_at` read silently sorts undefined and throws.
476
433
  const list = ControlThreadListResponse.parse(await res.json());
477
- const newest = pickResumableThread(list.threads);
434
+ // Scope to the current project (D28): --resume must never cross into
435
+ // another project's threads.
436
+ const newest = pickResumableThread(list.threads, process.cwd());
478
437
  if (!newest) {
479
438
  // Stay valid on the active output surface: NDJSON stream keeps compact
480
439
  // lines; --json prints its one object; text goes to stderr.
481
- const message = "claudexor: --resume found no threads to continue";
440
+ const message = "claudexor: --resume found no threads to continue in this project";
482
441
  if (jsonStream)
483
442
  printJsonLine({ ok: false, exitCode: 1, error: message });
484
443
  else if (json)
@@ -527,7 +486,8 @@ async function daemonRun(args, json, p) {
527
486
  ...(attachmentRefs ? { attachments: attachmentRefs } : {}),
528
487
  mode: p.mode,
529
488
  ...(threadId ? { threadId } : {}),
530
- ...(p.mode === "orchestrate" && p.autonomy ? { autonomy: p.autonomy } : {}),
489
+ ...(p.delegate ? { delegate: true } : {}),
490
+ ...(p.council ? { council: true } : {}),
531
491
  scope: { kind: "project", root: process.cwd() },
532
492
  execution: { isolation: inPlace ? "live" : "envelope" },
533
493
  ...(p.resolvedHarnesses ? { harnesses: p.resolvedHarnesses } : {}),
@@ -537,15 +497,12 @@ async function daemonRun(args, json, p) {
537
497
  ...(p.forced.race === true ? { n: p.nFlag ?? 2 } : p.nFlag !== undefined ? { n: p.nFlag } : {}),
538
498
  ...(p.attemptsFlag !== undefined ? { attempts: p.attemptsFlag } : {}),
539
499
  ...(flagBool(args, "until-clean") ? { untilClean: true } : {}),
540
- ...(p.forced.swarm === true || flagBool(args, "swarm") ? { swarm: true } : {}),
500
+ ...(p.forced.deepScan === true || flagBool(args, "deep-scan") ? { deepScan: true } : {}),
541
501
  ...(p.forced.create === true || flagBool(args, "create") ? { create: true } : {}),
542
502
  ...(p.resolvedSynthesis ? { synthesis: p.resolvedSynthesis } : {}),
543
503
  ...(p.tests ? { tests: p.tests } : {}),
544
504
  ...(p.protectedPathApprovals ? { protectedPathApprovals: p.protectedPathApprovals } : {}),
545
505
  ...(p.paidBudget !== undefined ? { paidBudget: p.paidBudget } : {}),
546
- ...(p.mode === "orchestrate" && p.maxToolCalls !== undefined
547
- ? { maxToolCalls: p.maxToolCalls }
548
- : {}),
549
506
  ...(p.resolvedAccess ? { access: p.resolvedAccess } : {}),
550
507
  ...(p.resolvedWebPolicy ? { web: p.resolvedWebPolicy } : {}),
551
508
  ...(p.resolvedModel ? { model: p.resolvedModel } : {}),
@@ -553,9 +510,6 @@ async function daemonRun(args, json, p) {
553
510
  ...(p.reviewerPanel ? { reviewerPanel: p.reviewerPanel } : {}),
554
511
  ...(p.reviewerModels ? { reviewerModels: p.reviewerModels } : {}),
555
512
  ...(p.reviewerEfforts ? { reviewerEfforts: p.reviewerEfforts } : {}),
556
- ...(p.specId ? { specId: p.specId } : {}),
557
- ...(p.specHash ? { specHash: p.specHash } : {}),
558
- ...(p.specPath ? { specPath: p.specPath } : {}),
559
513
  };
560
514
  try {
561
515
  if (jsonStream) {
@@ -570,7 +524,7 @@ async function daemonRun(args, json, p) {
570
524
  frame: "run.terminal",
571
525
  runId: "",
572
526
  runDir: started.runDir,
573
- status: runStatusForCli(started.status),
527
+ status: started.status,
574
528
  jobId: started.jobId,
575
529
  mode: p.mode,
576
530
  ...(started.error ? { error: started.error } : {}),
@@ -592,12 +546,13 @@ async function daemonRun(args, json, p) {
592
546
  const out = {
593
547
  runId: started.runId,
594
548
  runDir: final?.runDir ?? started.runDir,
595
- status: runStatusForCli(status),
549
+ status: status,
596
550
  jobId: started.jobId,
597
551
  error: final?.error ?? started.error,
598
552
  };
599
553
  const reason = daemonOutcomeSummary({ ...started, status, error: out.error });
600
554
  const applyEligibility = await fetchApplyEligibility(addr, started.runId);
555
+ const outcomeBanner = await fetchOutcomeBanner(addr, started.runId);
601
556
  printJsonLine({
602
557
  frame: "run.terminal",
603
558
  runId: out.runId,
@@ -607,6 +562,7 @@ async function daemonRun(args, json, p) {
607
562
  mode: p.mode,
608
563
  ...(out.error ? { error: out.error } : {}),
609
564
  ...(reason ? { summary: reason } : {}),
565
+ ...(outcomeBanner ? { outcomeBanner } : {}),
610
566
  ...(applyEligibility ? { applyEligibility } : {}),
611
567
  });
612
568
  return exitCodeForState(status);
@@ -620,14 +576,16 @@ async function daemonRun(args, json, p) {
620
576
  // apply-gate verdict, so machine callers act on truth instead of
621
577
  // re-implying eligibility from status.
622
578
  const applyEligibility = await fetchApplyEligibility(addr, out.runId);
579
+ const outcomeBanner = await fetchOutcomeBanner(addr, out.runId);
623
580
  printJson({
624
581
  runId: out.runId,
625
582
  runDir: out.runDir,
626
- status: runStatusForCli(out.status),
583
+ status: out.status,
627
584
  jobId: out.jobId,
628
585
  mode: p.mode,
629
586
  ...(out.error ? { error: out.error } : {}),
630
587
  ...(reason ? { summary: reason } : {}),
588
+ ...(outcomeBanner ? { outcomeBanner } : {}),
631
589
  ...(applyEligibility ? { applyEligibility } : {}),
632
590
  });
633
591
  return exitCodeForState(out.status);
@@ -643,24 +601,55 @@ async function daemonRun(args, json, p) {
643
601
  await followRun(started.runId, false);
644
602
  const final = started.jobId ? await client.status(started.jobId) : null;
645
603
  const status = final?.state ?? started.status;
646
- const publicStatus = runStatusForCli(status);
604
+ const publicStatus = status;
647
605
  print("");
648
606
  print(`run ${started.runId} [${publicStatus}]`);
607
+ // Server-owned outcome headline (D18), printed verbatim above any output.
608
+ const terminalBanner = await fetchOutcomeBanner(addr, started.runId);
609
+ if (terminalBanner)
610
+ print(` ${terminalBanner}`);
649
611
  print(` artifacts: ${final?.runDir ?? started.runDir}`);
650
- if (status === "blocked") {
651
- print(` blocked (needs human): unblock with \`claudexor decision ${started.runId} --accept-risk\` or rerun with \`claudexor decision ${started.runId} --rerun --feedback "..."\``);
652
- }
653
- else if (exitCodeForState(status) === 0) {
612
+ // A succeeded lifecycle exits 0 — INCLUDING a "Done · needs review" run
613
+ // (review blocked / checks failed). The apply-eligibility verdict (state
614
+ // needs_review) is the ONE source for the unblock guidance (D8).
615
+ // The plan question loop advances this past each answered follow-up turn.
616
+ let effectiveRunId = started.runId;
617
+ if (exitCodeForState(status) === 0) {
618
+ // Plan runs: server-derived readiness (D17) + interactive answer loop.
619
+ if (p.mode === "plan") {
620
+ // Council disclosure (INV-031): membership + merge, projected by the
621
+ // server (never a client re-derivation).
622
+ if (p.council) {
623
+ const council = await fetchCouncil(addr, started.runId);
624
+ if (council) {
625
+ print(` council: merged by ${council.mergedBy ?? "(none)"} from ${council.drafted} of ${council.requested} member(s)${council.degraded ? " (degraded)" : ""}`);
626
+ const failed = council.members.filter((m) => m.status === "failed");
627
+ if (failed.length > 0) {
628
+ print(` council failures: ${failed.map((m) => m.harnessId).join(", ")}`);
629
+ }
630
+ }
631
+ }
632
+ effectiveRunId = await runPlanQuestionLoop({
633
+ client,
634
+ addr,
635
+ threadId,
636
+ runId: started.runId,
637
+ interactive: !json && !jsonStream, // TTY plan turns answer inline
638
+ });
639
+ }
654
640
  // Offer apply only after a positive gate; otherwise print inspect/unblock guidance.
655
- const eligibility = await fetchApplyEligibility(addr, started.runId);
641
+ const eligibility = await fetchApplyEligibility(addr, effectiveRunId);
656
642
  if (eligibility?.eligible) {
657
- print(` apply with: claudexor apply ${started.runId}`);
643
+ print(` apply with: claudexor apply ${effectiveRunId}`);
644
+ }
645
+ else if (eligibility?.state === "needs_review") {
646
+ print(` needs review: unblock with \`claudexor decision ${effectiveRunId} --accept-risk\` or rerun with \`claudexor decision ${effectiveRunId} --rerun --feedback "..."\``);
658
647
  }
659
648
  else if (eligibility?.requiredAction) {
660
649
  print(` not applyable yet: ${eligibility.requiredAction}`);
661
650
  }
662
651
  else {
663
- print(` inspect with: claudexor inspect ${started.runId}`);
652
+ print(` inspect with: claudexor inspect ${effectiveRunId}`);
664
653
  }
665
654
  }
666
655
  return exitCodeForState(status);
@@ -739,7 +728,7 @@ async function decisionCommand(args, json) {
739
728
  * Resolve the ArtifactStore that owns a given run, regardless of the cwd the
740
729
  * CLI is invoked from. Order:
741
730
  * 1. the project store rooted at the current cwd (the common case);
742
- * 2. the v2 user store (~/.claudexor/v2/runs) used by no-project Ask runs;
731
+ * 2. the user store (~/.claudexor/v3/runs) used by no-project Ask runs;
743
732
  * 3. a daemon-tracked run that started in ANOTHER project — agent/race/create
744
733
  * runs live under that project's external runtime namespace, so we ask
745
734
  * the daemon for the run's absolute runDir (GET /runs/:id ->
@@ -793,131 +782,6 @@ async function resolveRunStore(runId) {
793
782
  }
794
783
  return null;
795
784
  }
796
- async function specCommand(args, json) {
797
- const prompt = args._.slice(1).join(" ").trim();
798
- if (!prompt) {
799
- return printUsageError(json, "claudexor: missing spec prompt");
800
- }
801
- if (containsSecretLikeToken(prompt)) {
802
- return printUsageError(json, "claudexor spec: prompt contains a secret-like token; specs are durable artifacts, so store secrets by ref and retry with a sanitized prompt");
803
- }
804
- const answersPath = flagStr(args, "answers");
805
- // The grounding step is a real (multi-)harness plan run, so the same
806
- // routing/cost/review controls as the plan verb apply — but ONLY that step
807
- // spawns a run. Parse them up-front so malformed values fail loudly on
808
- // every path, and refuse ALL grounding-only flags on the --answers path,
809
- // where no grounding run exists for them to control.
810
- let groundingEffort;
811
- let groundingMaxUsd;
812
- let groundingHarnesses;
813
- let groundingN;
814
- let groundingWeb;
815
- let groundingReviewerPanel;
816
- let groundingReviewerModels;
817
- let groundingReviewerEfforts;
818
- try {
819
- groundingEffort = effortHint(args);
820
- groundingMaxUsd = floatFlag(args, "max-usd");
821
- groundingHarnesses = harnessList(args);
822
- groundingN = intFlag(args, "n");
823
- groundingWeb = webPolicy(args);
824
- groundingReviewerPanel = reviewerPanel(args);
825
- groundingReviewerModels = reviewerModels(args);
826
- groundingReviewerEfforts = reviewerEfforts(args);
827
- }
828
- catch (err) {
829
- return printUsageError(json, `claudexor: ${err instanceof Error ? err.message : String(err)}`);
830
- }
831
- if (answersPath &&
832
- (groundingEffort !== undefined ||
833
- groundingMaxUsd !== undefined ||
834
- groundingHarnesses !== undefined ||
835
- groundingN !== undefined ||
836
- groundingWeb !== undefined ||
837
- groundingReviewerPanel !== undefined ||
838
- groundingReviewerModels !== undefined ||
839
- groundingReviewerEfforts !== undefined)) {
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");
841
- }
842
- try {
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
- });
849
- if (!answersPath) {
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
- }));
868
- if (json)
869
- printJson(session);
870
- else {
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");
876
- }
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");
882
- }
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)}`;
897
- if (json)
898
- printJson({ ...session, status: "frozen", runHint });
899
- else {
900
- print(`frozen SpecPack: ${session.specId}`);
901
- print(` path: ${session.specPath}`);
902
- print(` hash: ${session.specHash}`);
903
- print(`run: ${runHint}`);
904
- }
905
- return 0;
906
- }
907
- catch (err) {
908
- // Same runtime-error envelope contract as the run/race commands: --json
909
- // callers get a machine-readable {ok:false} on stdout, never bare stderr.
910
- if (json)
911
- printJson({
912
- ok: false,
913
- exitCode: 1,
914
- error: `claudexor spec: ${err instanceof Error ? err.message : String(err)}`,
915
- });
916
- else
917
- process.stderr.write(`claudexor spec: ${err instanceof Error ? err.message : String(err)}\n`);
918
- return 1;
919
- }
920
- }
921
785
  async function controlJson(addr, path, init = {}) {
922
786
  const response = await controlApiFetch(addr, path, init);
923
787
  const text = await response.text();
@@ -976,7 +840,7 @@ async function main() {
976
840
  if (valueFlagError)
977
841
  return printPreflightError(args, json, valueFlagError);
978
842
  // Registry-enforced per-command flag scope: a KNOWN flag outside the
979
- // command's declared set (e.g. `spec --model`, `ask --force`) fails loudly
843
+ // command's declared set (e.g. `plan --create`, `ask --force`) fails loudly
980
844
  // instead of being silently ignored. Data-driven from CLI_COMMANDS for
981
845
  // every verb (this replaced the old hand-listed plugin/--force special cases).
982
846
  const scopeError = commandFlagScopeError(cmd, Object.keys(args.flags));
@@ -1003,65 +867,44 @@ async function main() {
1003
867
  case "project":
1004
868
  return projectCommand(args, json);
1005
869
  case "agent": {
1006
- const specStrategyError = "claudexor: --spec requires a gated strategy; use 'claudexor best-of --spec <file>' or 'claudexor agent --attempts N --spec <file>'";
1007
- // ONE gate for both spellings: `agent --spec` and `agent --mode agent --spec`
1008
- // must enforce the same gated-strategy requirement (a flag spelling must
1009
- // never bypass a policy the bare verb enforces).
1010
- const agentSpecGateError = () => {
1011
- if (!flagStr(args, "spec") || flagBool(args, "until-clean"))
1012
- return null;
1013
- try {
1014
- const hasGatedStrategy = intFlag(args, "attempts") !== undefined || intFlag(args, "n") !== undefined;
1015
- return hasGatedStrategy ? null : specStrategyError;
1016
- }
1017
- catch (err) {
1018
- return `claudexor: ${err instanceof Error ? err.message : String(err)}`;
1019
- }
1020
- };
1021
870
  const modeStr = flagStr(args, "mode");
1022
871
  if (modeStr !== undefined) {
1023
872
  const mode = normalizeMode(modeStr);
1024
873
  if (!MODES.has(mode)) {
1025
874
  return printUsageError(json, `claudexor: unknown --mode '${modeStr}'. valid: ${[...MODES].join(", ")}`);
1026
875
  }
1027
- if ((mode === "ask" || mode === "audit") && flagStr(args, "spec")) {
1028
- return printUsageError(json, specStrategyError);
1029
- }
1030
- if (mode === "agent") {
1031
- const gateError = agentSpecGateError();
1032
- if (gateError)
1033
- return printUsageError(json, gateError);
1034
- }
1035
876
  return orchestrate(args, mode, json);
1036
877
  }
1037
- const gateError = agentSpecGateError();
1038
- if (gateError)
1039
- return printUsageError(json, gateError);
1040
878
  return orchestrate(args, "agent", json);
1041
879
  }
1042
880
  case "ask":
1043
881
  return orchestrate(args, "ask", json);
1044
- case "explore":
1045
- return orchestrate(args, "audit", json, { swarm: true });
1046
882
  case "best-of":
1047
883
  return orchestrate(args, "agent", json, { race: true });
1048
- // RENAMED verbs hard-error with the new name — no silent aliases (the
1049
- // same doctrine as retired mode ids: a stale script must fail loudly).
884
+ // RETIRED verb spellings hard-error with the new spelling — no silent
885
+ // aliases (same doctrine as retired mode ids: stale scripts fail loudly).
1050
886
  case "run":
1051
- return printPreflightError(args, json, "claudexor: the 'run' verb was renamed; use 'claudexor agent' (same flags)");
1052
887
  case "race":
1053
- return printPreflightError(args, json, "claudexor: the 'race' verb was renamed; use 'claudexor best-of' (same flags)");
888
+ case "audit":
889
+ case "map":
890
+ case "explore":
1054
891
  case "orchestrate":
1055
- return orchestrate(args, "orchestrate", json);
892
+ case "spec": {
893
+ const replacement = cmd === "run"
894
+ ? "claudexor agent (same flags)"
895
+ : cmd === "race"
896
+ ? "claudexor best-of (same flags)"
897
+ : cmd === "orchestrate"
898
+ ? "claudexor agent --delegate <prompt> (the harness spawns bounded isolated sub-runs; suggest-only planning is ordinary claudexor plan)"
899
+ : cmd === "spec"
900
+ ? "claudexor plan <prompt> then Implement (the plan lifecycle surfaces open questions and freezes the plan on implement)"
901
+ : "claudexor ask --deep-scan <prompt>";
902
+ return printPreflightError(args, json, `claudexor: the '${cmd}' verb was retired; use ${replacement}`);
903
+ }
1056
904
  case "plan":
1057
905
  return orchestrate(args, "plan", json);
1058
- case "spec":
1059
- return specCommand(args, json);
1060
906
  case "create":
1061
907
  return orchestrate(args, "agent", json, { create: true });
1062
- case "audit":
1063
- case "map":
1064
- return orchestrate(args, "audit", json);
1065
908
  case "settings":
1066
909
  return settingsCommand(args, json);
1067
910
  case "quota":
@@ -1071,6 +914,10 @@ async function main() {
1071
914
  case "mcp": {
1072
915
  if (args._[1] === "serve")
1073
916
  return serveMcpBridge();
917
+ // Internal: the scoped delegation belt injected into a delegate agent
918
+ // run's sandbox (D32). Not a user-facing verb.
919
+ if (args._[1] === "serve-belt")
920
+ return serveBeltBridge();
1074
921
  return printUsageError(json, "usage: claudexor mcp serve");
1075
922
  }
1076
923
  case "acp": {
@@ -1189,7 +1036,8 @@ async function main() {
1189
1036
  }
1190
1037
  if (parsedDecision.success) {
1191
1038
  const vb = parsedDecision.data.verification_basis;
1192
- print(`decision: ${parsedDecision.data.status} outcome=${parsedDecision.data.outcome} apply=${parsedDecision.data.apply_recommendation}${vb !== "none" ? ` verified_by=${vb}` : ""}`);
1039
+ const f = parsedDecision.data.facts;
1040
+ print(`decision: ${runOutcomeLabel(f)} (lifecycle=${f.lifecycle} checks=${f.checks} review=${f.review}${f.reason ? ` reason=${f.reason}` : ""}) apply=${parsedDecision.data.apply_recommendation}${vb !== "none" ? ` verified_by=${vb}` : ""}`);
1193
1041
  const budget = parsedDecision.data.budget_summary;
1194
1042
  print(`budget: spend=${budget.spend_usd ?? "unknown"}${budget.estimated ? " estimated" : ""}`);
1195
1043
  }
@@ -1280,27 +1128,8 @@ async function main() {
1280
1128
  }
1281
1129
  case "decision":
1282
1130
  return decisionCommand(args, json);
1283
- case "release": {
1284
- if (args._[1] === "check-name") {
1285
- const name = args._[2] ?? "claudexor";
1286
- const checks = await checkName(name);
1287
- if (json)
1288
- printJson({ name, checks });
1289
- else {
1290
- print(`naming gate for "${name}":`);
1291
- for (const c of checks) {
1292
- const tag = c.availability === "free"
1293
- ? "[free] "
1294
- : c.availability === "taken"
1295
- ? "[taken] "
1296
- : "[unknown]";
1297
- print(` ${tag} ${c.registry}: ${c.detail}`);
1298
- }
1299
- }
1300
- return 0;
1301
- }
1302
- return printUsageError(json, "usage: claudexor release check-name <name>");
1303
- }
1131
+ case "release":
1132
+ return releaseCommand(args, json);
1304
1133
  case "plugin": {
1305
1134
  const sub = args._[1];
1306
1135
  const target = args._[2];