@claudexor/cli 3.0.4 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist/about-command.d.ts +32 -0
  2. package/dist/about-command.d.ts.map +1 -0
  3. package/dist/about-command.js +40 -0
  4. package/dist/about-command.js.map +1 -0
  5. package/dist/acp-auth-command.d.ts +3 -0
  6. package/dist/acp-auth-command.d.ts.map +1 -0
  7. package/dist/acp-auth-command.js +18 -0
  8. package/dist/acp-auth-command.js.map +1 -0
  9. package/dist/acp-auth.d.ts +13 -0
  10. package/dist/acp-auth.d.ts.map +1 -0
  11. package/dist/acp-auth.js +47 -0
  12. package/dist/acp-auth.js.map +1 -0
  13. package/dist/acp-surface-runner.d.ts +12 -0
  14. package/dist/acp-surface-runner.d.ts.map +1 -1
  15. package/dist/acp-surface-runner.js +113 -11
  16. package/dist/acp-surface-runner.js.map +1 -1
  17. package/dist/bridge-serve.d.ts.map +1 -1
  18. package/dist/bridge-serve.js +2 -0
  19. package/dist/bridge-serve.js.map +1 -1
  20. package/dist/capabilities.d.ts.map +1 -1
  21. package/dist/capabilities.js +2 -1
  22. package/dist/capabilities.js.map +1 -1
  23. package/dist/claudexord.d.ts +21 -0
  24. package/dist/claudexord.d.ts.map +1 -1
  25. package/dist/claudexord.js +105 -21
  26. package/dist/claudexord.js.map +1 -1
  27. package/dist/cli-error.d.ts +97 -0
  28. package/dist/cli-error.d.ts.map +1 -0
  29. package/dist/cli-error.js +308 -0
  30. package/dist/cli-error.js.map +1 -0
  31. package/dist/cli-io.d.ts +9 -0
  32. package/dist/cli-io.d.ts.map +1 -1
  33. package/dist/cli-io.js +10 -1
  34. package/dist/cli-io.js.map +1 -1
  35. package/dist/cli.js +110 -116
  36. package/dist/cli.js.map +1 -1
  37. package/dist/codex-device-login.d.ts +117 -0
  38. package/dist/codex-device-login.d.ts.map +1 -0
  39. package/dist/codex-device-login.js +227 -0
  40. package/dist/codex-device-login.js.map +1 -0
  41. package/dist/command-flags.d.ts +0 -6
  42. package/dist/command-flags.d.ts.map +1 -1
  43. package/dist/command-flags.js +12 -4
  44. package/dist/command-flags.js.map +1 -1
  45. package/dist/command-help.d.ts +46 -0
  46. package/dist/command-help.d.ts.map +1 -0
  47. package/dist/command-help.js +92 -0
  48. package/dist/command-help.js.map +1 -0
  49. package/dist/command-registry.d.ts +2 -0
  50. package/dist/command-registry.d.ts.map +1 -1
  51. package/dist/command-registry.js +14 -7
  52. package/dist/command-registry.js.map +1 -1
  53. package/dist/control-services.d.ts +54 -27
  54. package/dist/control-services.d.ts.map +1 -1
  55. package/dist/control-services.js +56 -30
  56. package/dist/control-services.js.map +1 -1
  57. package/dist/credential-commands.d.ts.map +1 -1
  58. package/dist/credential-commands.js +44 -1
  59. package/dist/credential-commands.js.map +1 -1
  60. package/dist/daemon-outcome.d.ts +24 -0
  61. package/dist/daemon-outcome.d.ts.map +1 -0
  62. package/dist/daemon-outcome.js +59 -0
  63. package/dist/daemon-outcome.js.map +1 -0
  64. package/dist/daemon-run.d.ts +23 -4
  65. package/dist/daemon-run.d.ts.map +1 -1
  66. package/dist/daemon-run.js +66 -27
  67. package/dist/daemon-run.js.map +1 -1
  68. package/dist/delegation-belt-descriptor.d.ts +31 -13
  69. package/dist/delegation-belt-descriptor.d.ts.map +1 -1
  70. package/dist/delegation-belt-descriptor.js +43 -15
  71. package/dist/delegation-belt-descriptor.js.map +1 -1
  72. package/dist/live.d.ts +13 -1
  73. package/dist/live.d.ts.map +1 -1
  74. package/dist/live.js +56 -7
  75. package/dist/live.js.map +1 -1
  76. package/dist/mcp-runner.d.ts.map +1 -1
  77. package/dist/mcp-runner.js +60 -6
  78. package/dist/mcp-runner.js.map +1 -1
  79. package/dist/native-login.d.ts +10 -1
  80. package/dist/native-login.d.ts.map +1 -1
  81. package/dist/native-login.js +32 -10
  82. package/dist/native-login.js.map +1 -1
  83. package/dist/ops-commands.d.ts +5 -1
  84. package/dist/ops-commands.d.ts.map +1 -1
  85. package/dist/ops-commands.js +45 -52
  86. package/dist/ops-commands.js.map +1 -1
  87. package/dist/plan-implement-readiness.d.ts +32 -0
  88. package/dist/plan-implement-readiness.d.ts.map +1 -0
  89. package/dist/plan-implement-readiness.js +51 -0
  90. package/dist/plan-implement-readiness.js.map +1 -0
  91. package/dist/plugins.d.ts.map +1 -1
  92. package/dist/plugins.js +73 -20
  93. package/dist/plugins.js.map +1 -1
  94. package/dist/project-command.d.ts +15 -0
  95. package/dist/project-command.d.ts.map +1 -1
  96. package/dist/project-command.js +75 -20
  97. package/dist/project-command.js.map +1 -1
  98. package/dist/quota-command.d.ts.map +1 -1
  99. package/dist/quota-command.js +2 -6
  100. package/dist/quota-command.js.map +1 -1
  101. package/dist/release-command.d.ts.map +1 -1
  102. package/dist/release-command.js +31 -6
  103. package/dist/release-command.js.map +1 -1
  104. package/dist/release.d.ts +41 -10
  105. package/dist/release.d.ts.map +1 -1
  106. package/dist/release.js +70 -48
  107. package/dist/release.js.map +1 -1
  108. package/dist/repo-asset-authority.d.ts +89 -0
  109. package/dist/repo-asset-authority.d.ts.map +1 -0
  110. package/dist/repo-asset-authority.js +175 -0
  111. package/dist/repo-asset-authority.js.map +1 -0
  112. package/dist/retry-command.d.ts +15 -0
  113. package/dist/retry-command.d.ts.map +1 -1
  114. package/dist/retry-command.js +7 -4
  115. package/dist/retry-command.js.map +1 -1
  116. package/dist/review-command.d.ts.map +1 -1
  117. package/dist/review-command.js +2 -6
  118. package/dist/review-command.js.map +1 -1
  119. package/dist/reviewer-options.d.ts +11 -1
  120. package/dist/reviewer-options.d.ts.map +1 -1
  121. package/dist/reviewer-options.js +24 -9
  122. package/dist/reviewer-options.js.map +1 -1
  123. package/dist/run-options.d.ts.map +1 -1
  124. package/dist/run-options.js +13 -1
  125. package/dist/run-options.js.map +1 -1
  126. package/dist/settings-command.d.ts.map +1 -1
  127. package/dist/settings-command.js +2 -6
  128. package/dist/settings-command.js.map +1 -1
  129. package/dist/settings-service.d.ts +101 -10
  130. package/dist/settings-service.d.ts.map +1 -1
  131. package/dist/settings-service.js +201 -12
  132. package/dist/settings-service.js.map +1 -1
  133. package/dist/setup-job-store.d.ts +16 -5
  134. package/dist/setup-job-store.d.ts.map +1 -1
  135. package/dist/setup-job-store.js +24 -4
  136. package/dist/setup-job-store.js.map +1 -1
  137. package/dist/setup-jobs.d.ts +14 -4
  138. package/dist/setup-jobs.d.ts.map +1 -1
  139. package/dist/setup-jobs.js +204 -23
  140. package/dist/setup-jobs.js.map +1 -1
  141. package/dist/setup-login-inline.d.ts +71 -0
  142. package/dist/setup-login-inline.d.ts.map +1 -0
  143. package/dist/setup-login-inline.js +207 -0
  144. package/dist/setup-login-inline.js.map +1 -0
  145. package/dist/setup-login-protocol.d.ts +3 -2
  146. package/dist/setup-login-protocol.d.ts.map +1 -1
  147. package/dist/setup-login-protocol.js +16 -1
  148. package/dist/setup-login-protocol.js.map +1 -1
  149. package/dist/setup-login-runner.d.ts +9 -0
  150. package/dist/setup-login-runner.d.ts.map +1 -1
  151. package/dist/setup-login-runner.js +212 -5
  152. package/dist/setup-login-runner.js.map +1 -1
  153. package/dist/thread-delivery.d.ts.map +1 -1
  154. package/dist/thread-delivery.js +3 -1
  155. package/dist/thread-delivery.js.map +1 -1
  156. package/dist/thread-execution-workspace.d.ts +22 -0
  157. package/dist/thread-execution-workspace.d.ts.map +1 -0
  158. package/dist/thread-execution-workspace.js +21 -0
  159. package/dist/thread-execution-workspace.js.map +1 -0
  160. package/package.json +23 -23
package/dist/cli.js CHANGED
@@ -7,21 +7,25 @@ import { dirname, join, relative, resolve, sep } from "node:path";
7
7
  import { ArtifactStore } from "@claudexor/artifact-store";
8
8
  import { CLAUDEXOR_VERSION, noProjectRepoRoot, readTextSafe, userConfigDir } from "@claudexor/util";
9
9
  import { releaseCommand } from "./release-command.js";
10
- import { serveAcpBridge, serveBeltBridge, serveMcpBridge } from "./bridge-serve.js";
10
+ import { serveBeltBridge, serveMcpBridge } from "./bridge-serve.js";
11
+ import { dispatchAcpCommand } from "./acp-auth-command.js";
11
12
  import { initProjectConfig } from "@claudexor/config";
12
- import { DecisionRecord, EffortHint, ExternalContextPolicy, ModeKind as ModeKindSchema, RoutingGoal, ControlThreadListResponse, RunTelemetry, StructuredOutputConformance, TaskContract, runOutcomeLabel, } from "@claudexor/schema";
13
+ import { DecisionRecord, EFFORT_HINT_HELP, EffortHint, ExternalContextPolicy, ModeKind as ModeKindSchema, RoutingGoal, ControlThreadListResponse, RunTelemetry, StructuredOutputConformance, TaskContract, runOutcomeLabel, } from "@claudexor/schema";
13
14
  import { flagBool, flagStr, flagStringList, flagValues, parseArgs, requiredStringFlagError, } from "./args.js";
14
15
  import { print, printJson, printJsonLine, printUsageError, statusGlyph } from "./cli-io.js";
16
+ import { controlProblemError, minIntError, renderCliFailure, usageError } from "./cli-error.js";
17
+ import { handleHelpRequest } from "./command-help.js";
15
18
  import { pickResumableThread } from "./thread-select.js";
16
19
  import { KNOWN_FLAGS, VALUE_FLAGS, commandFlagScopeError, helpJson, renderHelp, } from "./command-registry.js";
17
20
  import { buildAgentCapabilityCatalog } from "./capabilities.js";
21
+ import { aboutJson, renderAbout } from "./about-command.js";
18
22
  import { dispatchOpsCommand } from "./ops-commands.js";
19
23
  import { reviewCommand } from "./review-command.js";
20
24
  import { controlApiFetch, followRun } from "./live.js";
21
25
  import { retryCommand, runAgainCommand } from "./retry-command.js";
22
26
  import { assertCliRunParamsHaveNoInlineSecrets } from "./run-secret-scan.js";
23
27
  import { openLocalAttachment, resolveLocalAttachment, } from "./local-attachment.js";
24
- import { connectDaemonIfRunning, daemonOutcomeSummary, ensureDaemon, enqueueAndAwait, exitCodeForState, fetchApplyEligibility, fetchCouncil, fetchOutcomeBanner, } from "./daemon-run.js";
28
+ import { connectDaemonIfRunning, daemonOutcomeProblemFields, daemonOutcomeSummary, ensureDaemon, enqueueAndAwait, exitCodeForState, fetchApplyEligibility, fetchCouncil, fetchOutcomeBanner, fetchRunDetail, fetchRunOutcomeFacts, projectApplyEligibility, projectOutcomeBanner, projectRunOutcomeFacts, mergeDaemonRunOutcome, } from "./daemon-run.js";
25
29
  import { runPlanQuestionLoop } from "./plan-question-loop.js";
26
30
  import { resolveDecisionBody } from "./decision.js";
27
31
  import { primaryOutputForCli } from "./primary-output.js";
@@ -119,7 +123,7 @@ function effortHint(args) {
119
123
  return undefined;
120
124
  const parsed = EffortHint.safeParse(v);
121
125
  if (!parsed.success)
122
- throw new Error(`invalid --effort '${v}' (expected low|medium|high|xhigh|max)`);
126
+ throw new Error(`invalid --effort '${v}' (expected a ${EFFORT_HINT_HELP})`);
123
127
  return parsed.data;
124
128
  }
125
129
  function synthesisMode(args) {
@@ -288,7 +292,12 @@ async function orchestrate(args, mode, json, forced = {}) {
288
292
  const maxUsd = floatFlag(args, "max-usd");
289
293
  paidBudget = maxUsd === undefined ? undefined : { kind: "finite", maxUsd };
290
294
  nFlag = intFlag(args, "n");
295
+ // Structured min-value validation (GH #28): `--n 0` is a usage field error, not a Zod dump.
296
+ if (nFlag !== undefined && nFlag < 1)
297
+ throw minIntError("n", 1);
291
298
  attemptsFlag = intFlag(args, "attempts");
299
+ if (attemptsFlag !== undefined && attemptsFlag < 1)
300
+ throw minIntError("attempts", 1);
292
301
  delegate = flagBool(args, "delegate") ? true : undefined;
293
302
  council = flagBool(args, "council") ? true : undefined;
294
303
  resolvedSynthesis = synthesisMode(args);
@@ -322,7 +331,8 @@ async function orchestrate(args, mode, json, forced = {}) {
322
331
  }
323
332
  }
324
333
  catch (err) {
325
- return printUsageError(json, `claudexor: ${err instanceof Error ? err.message : String(err)}`);
334
+ // Projector: typed field errors / domain codes survive; a plain flag-parse Error is usage (exit 2).
335
+ return renderCliFailure(json, err, { defaultCategory: "usage", messagePrefix: "claudexor:" });
326
336
  }
327
337
  let tests;
328
338
  try {
@@ -350,7 +360,8 @@ async function orchestrate(args, mode, json, forced = {}) {
350
360
  });
351
361
  }
352
362
  catch (err) {
353
- return printUsageError(json, `claudexor: ${err instanceof Error ? err.message : String(err)}`);
363
+ // Preserves the typed `inline_secret_rejected` code (never echoing the token).
364
+ return renderCliFailure(json, err, { defaultCategory: "usage", messagePrefix: "claudexor:" });
354
365
  }
355
366
  if (delegate && mode !== "agent") {
356
367
  return printUsageError(json, `claudexor: --delegate is an agent strategy (got mode '${mode}')`);
@@ -405,15 +416,13 @@ async function daemonRun(args, json, p) {
405
416
  ({ client, addr } = await ensureDaemon());
406
417
  }
407
418
  catch (err) {
408
- if (json)
409
- printJson({
410
- ok: false,
411
- exitCode: 1,
412
- error: `claudexor: ${err instanceof Error ? err.message : String(err)}`,
413
- });
414
- else
415
- process.stderr.write(`claudexor: ${err instanceof Error ? err.message : String(err)}\n`);
416
- return 1;
419
+ // D-7 projector: one failure envelope with message/code (never a legacy
420
+ // partial {ok,exitCode,error}). --json-stream stays valid NDJSON via the
421
+ // compact-line stream option.
422
+ return renderCliFailure(json || jsonStream, err, {
423
+ messagePrefix: "claudexor:",
424
+ stream: jsonStream,
425
+ });
417
426
  }
418
427
  // Thread continuation (W13/D10): --thread <id> targets a thread explicitly;
419
428
  // --resume picks the most recently updated one. When a threadId is present
@@ -435,28 +444,17 @@ async function daemonRun(args, json, p) {
435
444
  // another project's threads.
436
445
  const newest = pickResumableThread(list.threads, process.cwd());
437
446
  if (!newest) {
438
- // Stay valid on the active output surface: NDJSON stream keeps compact
447
+ // D-7 projector on the active surface: NDJSON stream keeps compact
439
448
  // lines; --json prints its one object; text goes to stderr.
440
- const message = "claudexor: --resume found no threads to continue in this project";
441
- if (jsonStream)
442
- printJsonLine({ ok: false, exitCode: 1, error: message });
443
- else if (json)
444
- printJson({ ok: false, exitCode: 1, error: message });
445
- else
446
- process.stderr.write(`${message}\n`);
447
- return 1;
449
+ return renderCliFailure(json || jsonStream, new Error("--resume found no threads to continue in this project"), { messagePrefix: "claudexor:", stream: jsonStream });
448
450
  }
449
451
  threadId = newest.id;
450
452
  }
451
453
  catch (err) {
452
- const message = `claudexor: --resume could not list threads: ${err instanceof Error ? err.message : String(err)}`;
453
- if (jsonStream)
454
- printJsonLine({ ok: false, exitCode: 1, error: message });
455
- else if (json)
456
- printJson({ ok: false, exitCode: 1, error: message });
457
- else
458
- process.stderr.write(`${message}\n`);
459
- return 1;
454
+ // Flatten any cause (transport error, a malformed-threads Zod parse) into a
455
+ // single operational message so the exit code stays 1 and no raw Zod field
456
+ // dump leaks; the projector owns the envelope on every surface.
457
+ return renderCliFailure(json || jsonStream, new Error(`--resume could not list threads: ${err instanceof Error ? err.message : String(err)}`), { messagePrefix: "claudexor:", stream: jsonStream });
460
458
  }
461
459
  }
462
460
  let attachmentRefs;
@@ -466,15 +464,9 @@ async function daemonRun(args, json, p) {
466
464
  : undefined;
467
465
  }
468
466
  catch (err) {
469
- if (json)
470
- printJson({
471
- ok: false,
472
- exitCode: 1,
473
- error: `claudexor: attachment upload failed: ${err instanceof Error ? err.message : String(err)}`,
474
- });
475
- else
476
- process.stderr.write(`claudexor: attachment upload failed: ${err instanceof Error ? err.message : String(err)}\n`);
477
- return 1;
467
+ // D-7 projector: message/code envelope, NDJSON-safe. (Previously this branch
468
+ // ignored --json-stream and fell through to a stderr line.)
469
+ return renderCliFailure(json || jsonStream, new Error(`attachment upload failed: ${err instanceof Error ? err.message : String(err)}`), { messagePrefix: "claudexor:", stream: jsonStream });
478
470
  }
479
471
  const body = {
480
472
  prompt: p.prompt,
@@ -528,6 +520,7 @@ async function daemonRun(args, json, p) {
528
520
  jobId: started.jobId,
529
521
  mode: p.mode,
530
522
  ...(started.error ? { error: started.error } : {}),
523
+ ...daemonOutcomeProblemFields(started),
531
524
  });
532
525
  return exitCodeForState(started.status);
533
526
  }
@@ -542,17 +535,13 @@ async function daemonRun(args, json, p) {
542
535
  // per event via print(JSON.stringify(ev)).
543
536
  await followRun(started.runId, true);
544
537
  const final = started.jobId ? await client.status(started.jobId) : null;
545
- const status = final?.state ?? started.status;
546
- const out = {
547
- runId: started.runId,
548
- runDir: final?.runDir ?? started.runDir,
549
- status: status,
550
- jobId: started.jobId,
551
- error: final?.error ?? started.error,
552
- };
538
+ const out = mergeDaemonRunOutcome(started, final);
539
+ const status = out.status;
553
540
  const reason = daemonOutcomeSummary({ ...started, status, error: out.error });
554
- const applyEligibility = await fetchApplyEligibility(addr, started.runId);
555
- const outcomeBanner = await fetchOutcomeBanner(addr, started.runId);
541
+ // ONE GET /runs/:id feeds all three terminal projections (INV-120/122).
542
+ const detail = await fetchRunDetail(addr, out.runId);
543
+ const applyEligibility = projectApplyEligibility(detail);
544
+ const outcomeBanner = projectOutcomeBanner(detail);
556
545
  printJsonLine({
557
546
  frame: "run.terminal",
558
547
  runId: out.runId,
@@ -561,11 +550,12 @@ async function daemonRun(args, json, p) {
561
550
  jobId: out.jobId,
562
551
  mode: p.mode,
563
552
  ...(out.error ? { error: out.error } : {}),
553
+ ...daemonOutcomeProblemFields(out),
564
554
  ...(reason ? { summary: reason } : {}),
565
555
  ...(outcomeBanner ? { outcomeBanner } : {}),
566
556
  ...(applyEligibility ? { applyEligibility } : {}),
567
557
  });
568
- return exitCodeForState(status);
558
+ return exitCodeForState(status, projectRunOutcomeFacts(detail));
569
559
  }
570
560
  if (json) {
571
561
  // Pure machine surface: await the terminal outcome and print one JSON object.
@@ -574,9 +564,11 @@ async function daemonRun(args, json, p) {
574
564
  const reason = daemonOutcomeSummary(out);
575
565
  // ADD-ONLY key (bench contract keeps {runId,runDir,status}): the derived
576
566
  // apply-gate verdict, so machine callers act on truth instead of
577
- // re-implying eligibility from status.
578
- const applyEligibility = await fetchApplyEligibility(addr, out.runId);
579
- const outcomeBanner = await fetchOutcomeBanner(addr, out.runId);
567
+ // re-implying eligibility from status. ONE GET /runs/:id feeds all three
568
+ // terminal projections (INV-120/122).
569
+ const detail = await fetchRunDetail(addr, out.runId);
570
+ const applyEligibility = projectApplyEligibility(detail);
571
+ const outcomeBanner = projectOutcomeBanner(detail);
580
572
  printJson({
581
573
  runId: out.runId,
582
574
  runDir: out.runDir,
@@ -584,11 +576,12 @@ async function daemonRun(args, json, p) {
584
576
  jobId: out.jobId,
585
577
  mode: p.mode,
586
578
  ...(out.error ? { error: out.error } : {}),
579
+ ...daemonOutcomeProblemFields(out),
587
580
  ...(reason ? { summary: reason } : {}),
588
581
  ...(outcomeBanner ? { outcomeBanner } : {}),
589
582
  ...(applyEligibility ? { applyEligibility } : {}),
590
583
  });
591
- return exitCodeForState(out.status);
584
+ return exitCodeForState(out.status, projectRunOutcomeFacts(detail));
592
585
  }
593
586
  // Text mode: enqueue, then live-stream the run through the shared follow
594
587
  // pipeline (replay + push + interactive TTY question answering), then print
@@ -652,18 +645,15 @@ async function daemonRun(args, json, p) {
652
645
  print(` inspect with: claudexor inspect ${effectiveRunId}`);
653
646
  }
654
647
  }
655
- return exitCodeForState(status);
648
+ return exitCodeForState(status, await fetchRunOutcomeFacts(addr, effectiveRunId));
656
649
  }
657
650
  catch (err) {
658
- if (json)
659
- printJson({
660
- ok: false,
661
- exitCode: 1,
662
- error: `claudexor: ${err instanceof Error ? err.message : String(err)}`,
663
- });
664
- else
665
- process.stderr.write(`claudexor: ${err instanceof Error ? err.message : String(err)}\n`);
666
- return 1;
651
+ // D-7 projector: a typed control problem (code/retryable/context) survives
652
+ // intact; --json-stream stays valid NDJSON via the compact-line stream option.
653
+ return renderCliFailure(json || jsonStream, err, {
654
+ messagePrefix: "claudexor:",
655
+ stream: jsonStream,
656
+ });
667
657
  }
668
658
  }
669
659
  /**
@@ -692,19 +682,9 @@ async function decisionCommand(args, json) {
692
682
  const text = await res.text();
693
683
  const data = text ? JSON.parse(text) : {};
694
684
  if (!res.ok) {
695
- // A typed decision rejection (e.g. revert refused: tree diverged) carries
696
- // its reason in `message`; transport/gate failures use `error`. Surface
697
- // whichever is present so the concrete reason is never lost behind "HTTP 409".
698
- const msg = typeof data["error"] === "string"
699
- ? data["error"]
700
- : typeof data["message"] === "string"
701
- ? data["message"]
702
- : `decision failed (HTTP ${res.status})`;
703
- if (json)
704
- printJson({ accepted: false, status: "rejected", message: msg });
705
- else
706
- process.stderr.write(`claudexor decision: ${msg}\n`);
707
- return 1;
685
+ // A typed decision rejection (revert_refused: tree diverged) rides through
686
+ // the projector with its code/retryable/bounded git-stderr context intact.
687
+ return renderCliFailure(json, controlProblemError(res.status, data, `decision failed (HTTP ${res.status})`), { messagePrefix: "claudexor decision:" });
708
688
  }
709
689
  if (json) {
710
690
  printJson(data);
@@ -720,8 +700,7 @@ async function decisionCommand(args, json) {
720
700
  return data["accepted"] === true ? 0 : 1;
721
701
  }
722
702
  catch (err) {
723
- process.stderr.write(`claudexor decision: ${err instanceof Error ? err.message : String(err)}\n`);
724
- return 1;
703
+ return renderCliFailure(json, err, { messagePrefix: "claudexor decision:" });
725
704
  }
726
705
  }
727
706
  /**
@@ -818,19 +797,12 @@ function listCliArtifacts(root) {
818
797
  // KNOWN_FLAGS / VALUE_FLAGS (imported above) and the per-command scope check
819
798
  // are projections of the command registry. Unknown flags FAIL LOUDLY: `--harnes
820
799
  // codex` must never silently run all harnesses.
821
- async function main() {
822
- const args = parseArgs(process.argv.slice(2));
823
- // --version / --help are standard CLI affordances, not unknown flags.
824
- if (flagBool(args, "version")) {
825
- process.stdout.write(`${CLI_VERSION}\n`);
826
- return 0;
827
- }
828
- if (flagBool(args, "help")) {
829
- process.stdout.write(HELP);
830
- return 0;
831
- }
832
- const json = flagBool(args, "json");
800
+ async function dispatch(args, json) {
833
801
  const cmd = args._[0] ?? "help";
802
+ // `--help` resolves the COMMAND first (QA-057): scoped usage for a known verb,
803
+ // global help for a bare/`help` verb, a usage error (exit 2) for a typo.
804
+ if (flagBool(args, "help"))
805
+ return handleHelpRequest(cmd, args._.length, json, CLI_VERSION);
834
806
  const unknownFlags = Object.keys(args.flags).filter((f) => !KNOWN_FLAGS.has(f));
835
807
  if (unknownFlags.length > 0) {
836
808
  const error = `claudexor: unknown flag(s): ${unknownFlags.map((f) => `--${f}`).join(", ")} (see \`claudexor help\`)`;
@@ -921,9 +893,7 @@ async function main() {
921
893
  return printUsageError(json, "usage: claudexor mcp serve");
922
894
  }
923
895
  case "acp": {
924
- if (args._[1] === "serve")
925
- return serveAcpBridge();
926
- return printUsageError(json, "usage: claudexor acp serve");
896
+ return dispatchAcpCommand(args, json);
927
897
  }
928
898
  case "follow": {
929
899
  const runId = args._[1];
@@ -947,11 +917,8 @@ async function main() {
947
917
  // store, or a daemon-tracked run that started in another project.
948
918
  const resolved = await resolveRunStore(runId);
949
919
  if (!resolved) {
950
- if (json)
951
- printJson({ runId, error: `no such run ${runId}` });
952
- else
953
- print(`no such run ${runId}`);
954
- return 1;
920
+ // D-7 projector: canonical envelope, run id as a per-command extra.
921
+ return renderCliFailure(json, new Error(`no such run ${runId}`), { extras: { runId } });
955
922
  }
956
923
  const store = resolved.store;
957
924
  const paths = store.runPaths(runId);
@@ -1090,11 +1057,10 @@ async function main() {
1090
1057
  }
1091
1058
  const rawMode = flagStr(args, "mode") ?? "apply";
1092
1059
  if (!["apply", "commit", "branch", "pr"].includes(rawMode)) {
1093
- if (json)
1094
- printJson({ runId, error: `unsupported apply mode: ${rawMode}` });
1095
- else
1096
- print(`unsupported apply mode: ${rawMode}`);
1097
- return 2;
1060
+ // D-7 projector: a usage failure (exit 2) with the run id as extra.
1061
+ return renderCliFailure(json, usageError(`unsupported apply mode: ${rawMode}`), {
1062
+ extras: { runId },
1063
+ });
1098
1064
  }
1099
1065
  const { addr } = await ensureDaemon();
1100
1066
  const dryRun = flagBool(args, "dry-run");
@@ -1116,9 +1082,14 @@ async function main() {
1116
1082
  else if (!response.ok)
1117
1083
  print(String(result["message"] ?? result["error"] ?? text));
1118
1084
  else if (dryRun)
1119
- print(result["ok"] === true ? "patch applies cleanly" : "patch does not apply");
1085
+ print(result["alreadyApplied"] === true
1086
+ ? "already applied; nothing would change"
1087
+ : result["ok"] === true
1088
+ ? "patch applies cleanly"
1089
+ : "patch does not apply");
1120
1090
  else
1121
1091
  print(`${String(result["mode"] ?? rawMode)}: applied=${String(result["applied"] ?? false)}` +
1092
+ (result["alreadyApplied"] === true ? " (already applied; no files changed)" : "") +
1122
1093
  (typeof result["commit"] === "string"
1123
1094
  ? ` commit=${result["commit"].slice(0, 8)}`
1124
1095
  : "") +
@@ -1214,6 +1185,15 @@ async function main() {
1214
1185
  }
1215
1186
  return 0;
1216
1187
  }
1188
+ case "about":
1189
+ // Product identity (version + author + license + owner links, D-11).
1190
+ // `--json` is a small stable envelope; the Swift About panel and the
1191
+ // packed npm-manifest assertion consume the same facts.
1192
+ if (json)
1193
+ printJson(aboutJson(CLI_VERSION));
1194
+ else
1195
+ print(renderAbout(CLI_VERSION));
1196
+ return 0;
1217
1197
  case "help":
1218
1198
  // `help --json` is the machine-readable command catalog (agents parse
1219
1199
  // it instead of scraping the text help).
@@ -1225,23 +1205,37 @@ async function main() {
1225
1205
  default:
1226
1206
  // Unknown command is an ERROR (exit 2), not a silent help print with
1227
1207
  // exit 0 — scripts must not mistake a typo'd verb for success. --json
1228
- // callers get the machine envelope on stdout (stdout purity contract).
1208
+ // callers get the ONE projector envelope (with message/code shape, no
1209
+ // longer a partial {ok,exitCode,error}); text mode prints the full help.
1229
1210
  if (json) {
1230
- printJson({
1231
- ok: false,
1232
- exitCode: 2,
1233
- error: `claudexor: unknown command '${cmd}' (see \`claudexor help --json\`)`,
1234
- });
1235
- return 2;
1211
+ return renderCliFailure(true, usageError(`claudexor: unknown command '${cmd}' (see \`claudexor help --json\`)`), {});
1236
1212
  }
1237
1213
  process.stderr.write(`claudexor: unknown command '${cmd}'\n\n${HELP}\n`);
1238
1214
  return 2;
1239
1215
  }
1240
1216
  }
1217
+ // The ONE top-level result/error projector (D-7, GH #28): any throw from any
1218
+ // command path is rendered by renderCliFailure (see cli-error.ts) into exactly
1219
+ // one JSON envelope or one stderr line, via the central category->exit-code
1220
+ // table. Commands that already print and return a code are unaffected.
1221
+ async function main() {
1222
+ const args = parseArgs(process.argv.slice(2));
1223
+ if (flagBool(args, "version")) {
1224
+ process.stdout.write(`${CLI_VERSION}\n`);
1225
+ return 0;
1226
+ }
1227
+ const json = flagBool(args, "json");
1228
+ try {
1229
+ return await dispatch(args, json);
1230
+ }
1231
+ catch (err) {
1232
+ return renderCliFailure(json, err);
1233
+ }
1234
+ }
1241
1235
  main()
1242
1236
  .then((code) => process.exit(code))
1243
1237
  .catch((err) => {
1244
- process.stderr.write(`claudexor: ${err instanceof Error ? err.message : String(err)}\n`);
1245
- process.exit(1);
1238
+ // Last-resort projector: still emit ONE envelope if the parse threw before json was known.
1239
+ process.exit(renderCliFailure(process.argv.includes("--json"), err));
1246
1240
  });
1247
1241
  //# sourceMappingURL=cli.js.map