@cursor/july 0.1.6 → 0.1.7

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 (83) hide show
  1. package/dist/bin/agent-serve.js +42 -17
  2. package/dist/channels/slack/post-update-delivery.d.ts +85 -0
  3. package/dist/channels/slack/post-update-delivery.d.ts.map +1 -0
  4. package/dist/docs/404.html +1 -1
  5. package/dist/docs/ab.html +2 -2
  6. package/dist/docs/assets/{app.DMboGKUD.js → app.COTN7wgo.js} +1 -1
  7. package/dist/docs/assets/chunks/@localSearchIndexroot.B7UcKvIn.js +1 -0
  8. package/dist/docs/assets/chunks/{VPLocalSearchBox.DkEqEnJU.js → VPLocalSearchBox.BW3TBdT0.js} +1 -1
  9. package/dist/docs/assets/chunks/{theme.ASugdMNm.js → theme.BEJW0vE7.js} +2 -2
  10. package/dist/docs/building-with-agents.html +2 -2
  11. package/dist/docs/concepts.html +2 -2
  12. package/dist/docs/deployment.html +2 -2
  13. package/dist/docs/evals.html +2 -2
  14. package/dist/docs/example-agents/approval-buddy.html +2 -2
  15. package/dist/docs/example-agents/benny.html +2 -2
  16. package/dist/docs/example-agents/bugbot.html +2 -2
  17. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  18. package/dist/docs/example-agents/codeowners-review.html +2 -2
  19. package/dist/docs/example-agents/concierge.html +2 -2
  20. package/dist/docs/example-agents/fsd.html +2 -2
  21. package/dist/docs/example-agents/index.html +2 -2
  22. package/dist/docs/example-agents/knowledge-base.html +2 -2
  23. package/dist/docs/example-agents/security-reviewer.html +2 -2
  24. package/dist/docs/example-agents/slack-agent.html +2 -2
  25. package/dist/docs/example-agents/weather-agent.html +2 -2
  26. package/dist/docs/guides/agent-to-agent.html +2 -2
  27. package/dist/docs/guides/cloud-runtime.html +2 -2
  28. package/dist/docs/guides/github.html +2 -2
  29. package/dist/docs/guides/human-in-the-loop.html +2 -2
  30. package/dist/docs/guides/slack.html +2 -2
  31. package/dist/docs/guides/webhooks.html +2 -2
  32. package/dist/docs/hillclimbing.html +2 -2
  33. package/dist/docs/index.html +2 -2
  34. package/dist/docs/quickstart.html +2 -2
  35. package/dist/docs/reference/agent-config.html +2 -2
  36. package/dist/docs/reference/channels.html +2 -2
  37. package/dist/docs/reference/cli.html +2 -2
  38. package/dist/docs/reference/connections.html +2 -2
  39. package/dist/docs/reference/hooks.html +2 -2
  40. package/dist/docs/reference/http-api.html +2 -2
  41. package/dist/docs/reference/instructions.html +2 -2
  42. package/dist/docs/reference/playground.html +2 -2
  43. package/dist/docs/reference/project-layout.html +2 -2
  44. package/dist/docs/reference/schedules.html +2 -2
  45. package/dist/docs/reference/sessions.html +2 -2
  46. package/dist/docs/reference/skills.html +2 -2
  47. package/dist/docs/reference/subagents.html +2 -2
  48. package/dist/docs/reference/tools.html +2 -2
  49. package/dist/docs/scaffolding-agents.html +2 -2
  50. package/dist/docs/storage.html +2 -2
  51. package/dist/docs/troubleshooting.html +2 -2
  52. package/dist/evals.d.ts +2 -2
  53. package/dist/internal/cli-ax.d.ts +23 -10
  54. package/dist/internal/cli-ax.d.ts.map +1 -1
  55. package/dist/internal/cli-ax.js +358 -55
  56. package/dist/internal/eval-run-store.d.ts +2 -0
  57. package/dist/internal/eval-run-store.d.ts.map +1 -1
  58. package/dist/internal/eval-run-store.js +4 -4
  59. package/dist/internal/evals-client.d.ts +96 -0
  60. package/dist/internal/evals-client.d.ts.map +1 -0
  61. package/dist/internal/evals-client.js +262 -0
  62. package/dist/internal/init-project.js +3 -3
  63. package/dist/internal/json-dir-store.d.ts +32 -0
  64. package/dist/internal/json-dir-store.d.ts.map +1 -0
  65. package/dist/internal/persistence-coordinator.d.ts +127 -0
  66. package/dist/internal/persistence-coordinator.d.ts.map +1 -0
  67. package/dist/internal/server.js +11 -2
  68. package/dist/persistence.d.ts +184 -0
  69. package/dist/persistence.d.ts.map +1 -0
  70. package/dist/playground/assets/index-72vCOBWO.js +86 -0
  71. package/dist/playground/assets/index-BjnMwYoR.css +1 -0
  72. package/dist/playground/index.html +2 -2
  73. package/package.json +1 -1
  74. package/src/bin/agent-serve.ts +47 -16
  75. package/src/evals.ts +2 -2
  76. package/src/internal/cli-ax.ts +466 -75
  77. package/src/internal/eval-run-store.ts +5 -0
  78. package/src/internal/evals-client.ts +431 -0
  79. package/src/internal/init-project.ts +3 -3
  80. package/src/internal/server.ts +12 -0
  81. package/dist/docs/assets/chunks/@localSearchIndexroot.O6eP7QZr.js +0 -1
  82. package/dist/playground/assets/index-7GJIUFQj.css +0 -1
  83. package/dist/playground/assets/index-mux9aj5P.js +0 -319
@@ -10,6 +10,7 @@
10
10
  import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
11
11
  import { tmpdir } from "node:os";
12
12
  import { join, resolve } from "node:path";
13
+ import type { EvalRunSnapshot } from "../evals.js";
13
14
  import { serve } from "../index.js";
14
15
  import type { AgentServeHandle } from "../types.js";
15
16
  import { lookupSessionContinuation, runChat } from "./chat-client.js";
@@ -23,6 +24,14 @@ import {
23
24
  filterDiscoveredEvals,
24
25
  runDiscoveredEvals,
25
26
  } from "./eval-runner.js";
27
+ import {
28
+ getRemoteEvalRun,
29
+ listRemoteEvalRuns,
30
+ listRemoteEvals,
31
+ RemoteEvalError,
32
+ runRemoteEvals,
33
+ startRemoteEvalRun,
34
+ } from "./evals-client.js";
26
35
  import {
27
36
  formatInitNextSteps,
28
37
  formatInitScaffoldSummary,
@@ -95,17 +104,18 @@ export interface AxCliOptions {
95
104
  url?: string;
96
105
  /**
97
106
  * Route chat/resume/run/call/eval/logs/sessions at the team's hosted
98
- * deployment
99
- * instead of a local ephemeral server. Mutually exclusive with `--url`.
100
- * `eval` targets the deployment by default, so there `--prod` is just
101
- * being explicit.
107
+ * deployment instead of a local ephemeral server. Mutually exclusive with
108
+ * `--url`. For `eval`, this runs a server-side batch via
109
+ * `/v1/dev/evals/runs` (persisted when `defineStorage` has an `evals` table);
110
+ * without `--prod`, eval defaults to an ephemeral local harness.
102
111
  */
103
112
  prod?: boolean;
104
113
  /**
105
- * `eval`: run against an ephemeral local server instead of the hosted
106
- * deployment (the eval default). Mutually exclusive with `--url`/`--prod`.
114
+ * `eval` (`--prod` / `--url` only): start the batch via
115
+ * `POST /v1/dev/evals/runs` and return the `runId` without polling.
116
+ * Mutually exclusive with `--list`.
107
117
  */
108
- local?: boolean;
118
+ noWait?: boolean;
109
119
  /** Cursor team id for `--prod` (defaults to the account's team). */
110
120
  team?: string;
111
121
  /** Cursor API key override for `--prod` management calls. */
@@ -667,28 +677,27 @@ export async function cmdTrajectory(options: AxCliOptions): Promise<number> {
667
677
  }
668
678
 
669
679
  /**
670
- * Which surface `eval` should target. Deployment is the default; `--local`
671
- * opts into an ephemeral local server, `--url` into a running one.
680
+ * Which surface `eval` should target. Local (ephemeral) is the default;
681
+ * `--prod` uses the hosted deployment, `--url` a running server.
672
682
  * Exported for tests.
673
683
  */
674
684
  export function resolveEvalTargetMode(
675
- options: Pick<AxCliOptions, "local" | "prod" | "url">
685
+ options: Pick<AxCliOptions, "prod" | "url">
676
686
  ): "local" | "url" | "deployment" | { error: string } {
677
687
  const flags = [
678
- options.local === true ? "--local" : undefined,
679
688
  options.prod === true ? "--prod" : undefined,
680
689
  options.url !== undefined ? "--url" : undefined,
681
690
  ].filter((flag) => flag !== undefined);
682
691
  if (flags.length > 1) {
683
692
  return { error: `${flags.join(" and ")} are mutually exclusive` };
684
693
  }
685
- if (options.local === true) {
686
- return "local";
687
- }
688
694
  if (options.url !== undefined) {
689
695
  return "url";
690
696
  }
691
- return "deployment";
697
+ if (options.prod === true) {
698
+ return "deployment";
699
+ }
700
+ return "local";
692
701
  }
693
702
 
694
703
  /** Resolve the eval target; prints a CLI-ready error and returns an exit code on failure. */
@@ -723,9 +732,8 @@ async function resolveEvalTarget(
723
732
  // genuinely missing deployment.
724
733
  process.stderr.write(
725
734
  `${sanitizeCustomerError(error.message)}\n` +
726
- `Evals run on the hosted deployment by default.\n` +
727
735
  ` deploy it first: ${CLI_COMMAND_NAME} deploy --slug ${error.slug}\n` +
728
- ` or run locally: ${CLI_COMMAND_NAME} eval --local --dir ${options.dir}\n`
736
+ ` or omit --prod: ${CLI_COMMAND_NAME} eval --dir ${options.dir}\n`
729
737
  );
730
738
  return 1;
731
739
  }
@@ -733,7 +741,7 @@ async function resolveEvalTarget(
733
741
  `${sanitizeCustomerError(
734
742
  error instanceof Error ? error.message : String(error)
735
743
  )}\n` +
736
- `Evals run on the hosted deployment by default; pass --local to run on an ephemeral local server.\n`
744
+ `Pass --prod for the hosted deployment, or omit it to run on an ephemeral local server.\n`
737
745
  );
738
746
  return 1;
739
747
  }
@@ -741,43 +749,76 @@ async function resolveEvalTarget(
741
749
 
742
750
  export async function cmdEval(options: AxCliOptions): Promise<number> {
743
751
  const projectRoot = resolve(options.cwd ?? process.cwd(), options.dir);
752
+ const mode = resolveEvalTargetMode(options);
753
+ if (typeof mode === "object") {
754
+ process.stderr.write(`${mode.error}\n`);
755
+ return 2;
756
+ }
757
+ if (options.noWait === true) {
758
+ if (options.list === true) {
759
+ process.stderr.write("--no-wait and --list are mutually exclusive\n");
760
+ return 2;
761
+ }
762
+ if (mode === "local") {
763
+ process.stderr.write(
764
+ "--no-wait requires --prod or --url (not the default local target)\n"
765
+ );
766
+ return 2;
767
+ }
768
+ }
769
+
770
+ const filterIds =
771
+ options.evalIds === undefined || options.evalIds.length === 0
772
+ ? undefined
773
+ : options.evalIds;
774
+ const tags =
775
+ options.tags === undefined || options.tags.length === 0
776
+ ? undefined
777
+ : options.tags;
744
778
 
745
779
  if (options.list) {
746
- const { evals } = await discoverEvals(projectRoot);
747
- const listed = filterDiscoveredEvals(evals, {
748
- filterIds: options.evalIds,
749
- tags: options.tags,
750
- });
751
- if (options.json) {
752
- console.log(
753
- JSON.stringify(
754
- listed.map((e) => ({
755
- id: e.id,
756
- fileId: e.fileId,
757
- path: e.path,
758
- description: e.definition.description,
759
- tags: e.definition.tags ?? [],
760
- })),
761
- null,
762
- 2
763
- )
780
+ // List the same surface a run will execute: local filesystem by default,
781
+ // GET /v1/dev/evals on --prod / --url.
782
+ if (mode === "local") {
783
+ return listLocalEvals(projectRoot, options, filterIds, tags);
784
+ }
785
+ const target = await resolveEvalTarget(options);
786
+ if (typeof target === "number") {
787
+ return target;
788
+ }
789
+ try {
790
+ const listed = await listRemoteEvals({
791
+ baseUrl: target.agentUrl,
792
+ auth: {
793
+ bearerToken: options.bearerToken,
794
+ headers: target.headers,
795
+ },
796
+ fetchImpl: options.fetchImpl,
797
+ filterIds,
798
+ tags,
799
+ });
800
+ return printListedEvals(
801
+ listed.map((e) => ({
802
+ id: e.id,
803
+ fileId: e.fileId,
804
+ description: e.description,
805
+ tags: e.tags ?? [],
806
+ })),
807
+ options,
808
+ `No evals on ${target.agentUrl}`
764
809
  );
765
- } else if (listed.length === 0) {
766
- process.stdout.write(`No evals under ${projectRoot}/evals\n`);
767
- } else {
768
- for (const e of listed) {
769
- const tags =
770
- e.definition.tags && e.definition.tags.length > 0
771
- ? ` [${e.definition.tags.join(", ")}]`
772
- : "";
773
- const desc =
774
- e.definition.description !== undefined
775
- ? ` — ${e.definition.description}`
776
- : "";
777
- process.stdout.write(`${e.id}${tags}${desc}\n`);
810
+ } catch (error) {
811
+ process.stderr.write(
812
+ `${sanitizeCustomerError(
813
+ error instanceof Error ? error.message : String(error)
814
+ )}\n`
815
+ );
816
+ return 1;
817
+ } finally {
818
+ if (target.close !== undefined) {
819
+ await target.close();
778
820
  }
779
821
  }
780
- return 0;
781
822
  }
782
823
 
783
824
  const target = await resolveEvalTarget(options);
@@ -785,52 +826,402 @@ export async function cmdEval(options: AxCliOptions): Promise<number> {
785
826
  return target;
786
827
  }
787
828
  const stream = shouldStreamProgress(options);
788
- const progress = stream ? createStreamProgress() : undefined;
829
+
789
830
  try {
790
831
  if (options.verbose || stream) {
791
832
  process.stderr.write(`eval target: ${target.agentUrl}\n`);
792
833
  }
793
- const results = await runDiscoveredEvals({
794
- projectRoot,
834
+
835
+ // Default local: ephemeral in-process harness against local eval sources.
836
+ // --prod / --url: run on the serve process via /v1/dev/evals/runs so the
837
+ // batch lands in playground history and defineStorage evals (when set).
838
+ if (mode === "local") {
839
+ const progress = stream ? createStreamProgress() : undefined;
840
+ try {
841
+ const results = await runDiscoveredEvals({
842
+ projectRoot,
843
+ baseUrl: target.agentUrl,
844
+ filterIds,
845
+ tags,
846
+ timeoutMs: options.timeoutMs,
847
+ bearerToken: options.bearerToken,
848
+ headers: target.headers,
849
+ verbose: Boolean(options.verbose) || stream,
850
+ onEvent:
851
+ progress === undefined ? undefined : (e) => progress.onEvent(e),
852
+ });
853
+ if (results.length === 0) {
854
+ process.stderr.write("No matching evals found.\n");
855
+ return 2;
856
+ }
857
+ return printEvalResults(results, options);
858
+ } finally {
859
+ progress?.close();
860
+ }
861
+ }
862
+
863
+ try {
864
+ if (options.noWait === true) {
865
+ const started = await startRemoteEvalRun({
866
+ baseUrl: target.agentUrl,
867
+ filterIds,
868
+ tags,
869
+ timeoutMs: options.timeoutMs,
870
+ verbose: Boolean(options.verbose),
871
+ auth: {
872
+ bearerToken: options.bearerToken,
873
+ headers: target.headers,
874
+ },
875
+ fetchImpl: options.fetchImpl,
876
+ });
877
+ return printEvalAccepted(started, options, target.agentUrl);
878
+ }
879
+
880
+ const progress = createRemoteEvalProgress(
881
+ options.verbose === true || stream
882
+ );
883
+ const { snapshot, results } = await runRemoteEvals({
884
+ baseUrl: target.agentUrl,
885
+ filterIds,
886
+ tags,
887
+ timeoutMs: options.timeoutMs,
888
+ verbose: Boolean(options.verbose) || stream,
889
+ bearerToken: options.bearerToken,
890
+ headers: target.headers,
891
+ fetchImpl: options.fetchImpl,
892
+ onSnapshot: progress === undefined ? undefined : progress.onSnapshot,
893
+ });
894
+ if (results.length === 0 && snapshot.status !== "failed") {
895
+ process.stderr.write("No matching evals found.\n");
896
+ return 2;
897
+ }
898
+ return printEvalResults(results, options, {
899
+ runId: snapshot.runId,
900
+ status: snapshot.status,
901
+ error: snapshot.error,
902
+ });
903
+ } catch (error) {
904
+ if (
905
+ error instanceof RemoteEvalError &&
906
+ error.code === "no_matching_evals"
907
+ ) {
908
+ process.stderr.write(`${error.message}\n`);
909
+ return 2;
910
+ }
911
+ process.stderr.write(
912
+ `${sanitizeCustomerError(
913
+ error instanceof Error ? error.message : String(error)
914
+ )}\n`
915
+ );
916
+ return 1;
917
+ }
918
+ } finally {
919
+ if (target.close !== undefined) {
920
+ await target.close();
921
+ }
922
+ }
923
+ }
924
+
925
+ /**
926
+ * Flags to re-target the same surface from an `eval --no-wait` hint.
927
+ * Exported for tests.
928
+ */
929
+ export function formatEvalStatusFlags(
930
+ options: Pick<AxCliOptions, "prod" | "url" | "slug" | "team">
931
+ ): string {
932
+ const parts: string[] = [];
933
+ if (options.url !== undefined) {
934
+ parts.push(`--url ${options.url.replace(/\/$/, "")}`);
935
+ } else {
936
+ parts.push("--prod");
937
+ if (options.slug !== undefined && options.slug !== "") {
938
+ parts.push(`--slug ${options.slug}`);
939
+ }
940
+ }
941
+ if (options.team !== undefined && options.team !== "") {
942
+ parts.push(`--team ${options.team}`);
943
+ }
944
+ return parts.length === 0 ? "" : ` ${parts.join(" ")}`;
945
+ }
946
+
947
+ /** `--no-wait`: print the accepted batch id (and how to poll) then exit. */
948
+ function printEvalAccepted(
949
+ started: EvalRunSnapshot,
950
+ options: AxCliOptions,
951
+ agentUrl: string
952
+ ): number {
953
+ const pollPath = `${agentUrl.replace(/\/$/, "")}/v1/dev/evals/runs/${started.runId}`;
954
+ const statusFlags = formatEvalStatusFlags(options);
955
+ if (options.json) {
956
+ console.log(
957
+ JSON.stringify(
958
+ {
959
+ ok: true,
960
+ accepted: true,
961
+ runId: started.runId,
962
+ status: started.status,
963
+ summary: started.summary,
964
+ pollUrl: pollPath,
965
+ durableRuns: started.config.durableRuns === true,
966
+ },
967
+ null,
968
+ 2
969
+ )
970
+ );
971
+ } else {
972
+ const durable = started.config.durableRuns === true ? " (persisted)" : "";
973
+ process.stdout.write(
974
+ `Accepted eval run ${started.runId}${durable} — ${started.summary.total} case(s)\n`
975
+ );
976
+ process.stdout.write(
977
+ `Status: ${CLI_COMMAND_NAME} eval status ${started.runId}${statusFlags}\n`
978
+ );
979
+ process.stdout.write(`Poll: ${pollPath}\n`);
980
+ }
981
+ return 0;
982
+ }
983
+
984
+ /**
985
+ * `eval status [runId]` — fetch one batch (or list recent) on the deployment /
986
+ * `--url` target. Exit `0` when the batch is completed with all cases passing,
987
+ * `1` when failed / any case failed / unknown, `2` for usage errors, `3` when
988
+ * the batch is still running (for poll loops after `--no-wait`).
989
+ */
990
+ export async function cmdEvalStatus(
991
+ runId: string | undefined,
992
+ options: AxCliOptions
993
+ ): Promise<number> {
994
+ const mode = resolveEvalTargetMode(options);
995
+ if (typeof mode === "object") {
996
+ process.stderr.write(`${mode.error}\n`);
997
+ return 2;
998
+ }
999
+ if (mode === "local") {
1000
+ process.stderr.write(
1001
+ "eval status requires --prod or --url (not the default local target)\n"
1002
+ );
1003
+ return 2;
1004
+ }
1005
+ if (options.list === true || options.noWait === true) {
1006
+ process.stderr.write("eval status does not accept --list or --no-wait\n");
1007
+ return 2;
1008
+ }
1009
+
1010
+ const target = await resolveEvalTarget(options);
1011
+ if (typeof target === "number") {
1012
+ return target;
1013
+ }
1014
+ const auth = {
1015
+ bearerToken: options.bearerToken,
1016
+ headers: target.headers,
1017
+ };
1018
+
1019
+ try {
1020
+ if (runId === undefined || runId === "") {
1021
+ const { runs, activeRunId } = await listRemoteEvalRuns({
1022
+ baseUrl: target.agentUrl,
1023
+ auth,
1024
+ fetchImpl: options.fetchImpl,
1025
+ });
1026
+ if (options.json) {
1027
+ console.log(JSON.stringify({ runs, activeRunId }, null, 2));
1028
+ } else if (runs.length === 0) {
1029
+ process.stdout.write("No eval runs on this agent.\n");
1030
+ } else {
1031
+ if (activeRunId !== undefined) {
1032
+ process.stdout.write(`active: ${activeRunId}\n`);
1033
+ }
1034
+ for (const run of runs) {
1035
+ process.stdout.write(`${formatEvalRunSummaryLine(run)}\n`);
1036
+ }
1037
+ }
1038
+ return 0;
1039
+ }
1040
+
1041
+ const snap = await getRemoteEvalRun({
795
1042
  baseUrl: target.agentUrl,
796
- filterIds:
797
- options.evalIds === undefined || options.evalIds.length === 0
798
- ? undefined
799
- : options.evalIds,
800
- tags:
801
- options.tags === undefined || options.tags.length === 0
802
- ? undefined
803
- : options.tags,
804
- timeoutMs: options.timeoutMs,
805
- bearerToken: options.bearerToken,
806
- headers: target.headers,
807
- verbose: Boolean(options.verbose) || stream,
808
- onEvent: progress === undefined ? undefined : (e) => progress.onEvent(e),
1043
+ runId,
1044
+ auth,
1045
+ fetchImpl: options.fetchImpl,
809
1046
  });
810
-
811
- if (results.length === 0) {
812
- process.stderr.write("No matching evals found.\n");
813
- return 2;
1047
+ if (options.json) {
1048
+ console.log(JSON.stringify(snap, null, 2));
1049
+ } else {
1050
+ process.stdout.write(`${formatEvalRunSummaryLine(snap)}\n`);
1051
+ if (snap.error !== undefined && snap.error !== "") {
1052
+ process.stdout.write(`error: ${snap.error}\n`);
1053
+ }
1054
+ for (const c of snap.cases) {
1055
+ const mark =
1056
+ c.status !== "done" ? c.status : c.ok === true ? "PASS" : "FAIL";
1057
+ const dur =
1058
+ c.durationMs === undefined
1059
+ ? ""
1060
+ : ` (${(c.durationMs / 1000).toFixed(1)}s)`;
1061
+ process.stdout.write(` ${mark} ${c.id}${dur}\n`);
1062
+ if (c.error !== undefined && c.error !== "") {
1063
+ process.stdout.write(` error: ${c.error}\n`);
1064
+ }
1065
+ }
1066
+ }
1067
+ if (snap.status === "running") {
1068
+ return 3;
814
1069
  }
815
- return printEvalResults(results, options);
1070
+ const failed =
1071
+ snap.status === "failed" || snap.cases.some((c) => c.ok !== true);
1072
+ return failed ? 1 : 0;
1073
+ } catch (error) {
1074
+ process.stderr.write(
1075
+ `${sanitizeCustomerError(
1076
+ error instanceof Error ? error.message : String(error)
1077
+ )}\n`
1078
+ );
1079
+ return 1;
816
1080
  } finally {
817
- progress?.close();
818
1081
  if (target.close !== undefined) {
819
1082
  await target.close();
820
1083
  }
821
1084
  }
822
1085
  }
823
1086
 
1087
+ function formatEvalRunSummaryLine(run: EvalRunSnapshot): string {
1088
+ const { passed, failed, total, done } = run.summary;
1089
+ const durable = run.config.durableRuns === true ? " persisted" : "";
1090
+ return `${run.runId} ${run.status} ${done}/${total} done (${passed} passed, ${failed} failed)${durable}`;
1091
+ }
1092
+
1093
+ async function listLocalEvals(
1094
+ projectRoot: string,
1095
+ options: AxCliOptions,
1096
+ filterIds: string[] | undefined,
1097
+ tags: string[] | undefined
1098
+ ): Promise<number> {
1099
+ const { evals } = await discoverEvals(projectRoot);
1100
+ const listed = filterDiscoveredEvals(evals, { filterIds, tags });
1101
+ return printListedEvals(
1102
+ listed.map((e) => ({
1103
+ id: e.id,
1104
+ fileId: e.fileId,
1105
+ path: e.path,
1106
+ description: e.definition.description,
1107
+ tags: e.definition.tags ?? [],
1108
+ })),
1109
+ options,
1110
+ `No evals under ${projectRoot}/evals`
1111
+ );
1112
+ }
1113
+
1114
+ function printListedEvals(
1115
+ listed: Array<{
1116
+ id: string;
1117
+ fileId: string;
1118
+ path?: string;
1119
+ description?: string;
1120
+ tags: string[];
1121
+ }>,
1122
+ options: AxCliOptions,
1123
+ emptyMessage: string
1124
+ ): number {
1125
+ if (options.json) {
1126
+ console.log(JSON.stringify(listed, null, 2));
1127
+ } else if (listed.length === 0) {
1128
+ process.stdout.write(`${emptyMessage}\n`);
1129
+ } else {
1130
+ for (const e of listed) {
1131
+ const tagSuffix = e.tags.length > 0 ? ` [${e.tags.join(", ")}]` : "";
1132
+ const desc = e.description !== undefined ? ` — ${e.description}` : "";
1133
+ process.stdout.write(`${e.id}${tagSuffix}${desc}\n`);
1134
+ }
1135
+ }
1136
+ return 0;
1137
+ }
1138
+
1139
+ /**
1140
+ * Remote batches don't stream turn events to the CLI; print run/case progress
1141
+ * (and case logs once they appear on the snapshot) instead.
1142
+ */
1143
+ function createRemoteEvalProgress(
1144
+ enabled: boolean
1145
+ ): { onSnapshot: (snap: EvalRunSnapshot) => void } | undefined {
1146
+ if (!enabled) {
1147
+ return undefined;
1148
+ }
1149
+ let announced = false;
1150
+ let lastSummaryKey = "";
1151
+ const seenCaseKeys = new Set<string>();
1152
+ return {
1153
+ onSnapshot(snap) {
1154
+ if (!announced) {
1155
+ announced = true;
1156
+ const durable = snap.config.durableRuns === true ? " (persisted)" : "";
1157
+ process.stderr.write(`eval run: ${snap.runId}${durable}\n`);
1158
+ }
1159
+ const summaryKey = `${snap.status}:${snap.summary.done}:${snap.summary.passed}:${snap.summary.failed}`;
1160
+ if (summaryKey !== lastSummaryKey) {
1161
+ lastSummaryKey = summaryKey;
1162
+ const { done, total, passed, failed } = snap.summary;
1163
+ process.stderr.write(
1164
+ `eval progress: ${done}/${total} done (${passed} passed, ${failed} failed) [${snap.status}]\n`
1165
+ );
1166
+ }
1167
+ for (const c of snap.cases) {
1168
+ if (c.status !== "done") {
1169
+ continue;
1170
+ }
1171
+ const caseKey = `${c.id}:${c.ok === true ? "pass" : "fail"}:${c.durationMs ?? 0}`;
1172
+ if (seenCaseKeys.has(caseKey)) {
1173
+ continue;
1174
+ }
1175
+ seenCaseKeys.add(caseKey);
1176
+ process.stderr.write(
1177
+ ` eval ${c.id}: ${c.ok === true ? "PASS" : "FAIL"}${
1178
+ c.error !== undefined ? ` — ${c.error}` : ""
1179
+ }\n`
1180
+ );
1181
+ for (const line of c.logs ?? []) {
1182
+ process.stderr.write(` ${line}\n`);
1183
+ }
1184
+ }
1185
+ },
1186
+ };
1187
+ }
1188
+
824
1189
  function printEvalResults(
825
1190
  results: EvalRunResult[],
826
- options: AxCliOptions
1191
+ options: AxCliOptions,
1192
+ batch?: {
1193
+ runId?: string;
1194
+ status?: EvalRunSnapshot["status"];
1195
+ error?: string;
1196
+ }
827
1197
  ): number {
828
1198
  const passed = results.filter((r) => r.ok).length;
829
1199
  const failed = results.length - passed;
1200
+ const batchFailed = batch?.status === "failed";
1201
+ const ok = failed === 0 && !batchFailed;
1202
+
1203
+ if (batch?.error !== undefined && batch.error !== "" && !options.json) {
1204
+ // Always surface harness-level failures (not only --verbose).
1205
+ process.stderr.write(`eval run error: ${batch.error}\n`);
1206
+ }
830
1207
 
831
1208
  if (options.json) {
832
1209
  console.log(
833
- JSON.stringify({ ok: failed === 0, passed, failed, results }, null, 2)
1210
+ JSON.stringify(
1211
+ {
1212
+ ok,
1213
+ passed,
1214
+ failed,
1215
+ results,
1216
+ ...(batch?.runId === undefined ? {} : { runId: batch.runId }),
1217
+ ...(batch?.status === undefined ? {} : { status: batch.status }),
1218
+ ...(batch?.error === undefined || batch.error === ""
1219
+ ? {}
1220
+ : { error: batch.error }),
1221
+ },
1222
+ null,
1223
+ 2
1224
+ )
834
1225
  );
835
1226
  } else {
836
1227
  for (const r of results) {
@@ -861,7 +1252,7 @@ function printEvalResults(
861
1252
  `\n${passed} passed, ${failed} failed, ${results.length} total\n`
862
1253
  );
863
1254
  }
864
- return failed === 0 ? 0 : 1;
1255
+ return ok ? 0 : 1;
865
1256
  }
866
1257
 
867
1258
  /**
@@ -47,6 +47,8 @@ export interface StartEvalRunInput {
47
47
  tags?: string[];
48
48
  bearerToken?: string;
49
49
  verbose?: boolean;
50
+ /** Per-case timeout override (same semantics as CLI `--timeout-ms`). */
51
+ timeoutMs?: number;
50
52
  }
51
53
 
52
54
  export type EvalListConfig = {
@@ -190,6 +192,7 @@ export class EvalRunStore {
190
192
  ? {}
191
193
  : { bearerToken: input.bearerToken }),
192
194
  ...(input.verbose === undefined ? {} : { verbose: input.verbose }),
195
+ ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
193
196
  });
194
197
 
195
198
  return snapshot;
@@ -316,6 +319,7 @@ export class EvalRunStore {
316
319
  config: Partial<EvalConfigFile>;
317
320
  bearerToken?: string;
318
321
  verbose?: boolean;
322
+ timeoutMs?: number;
319
323
  }
320
324
  ): Promise<void> {
321
325
  if (this.runs.get(runId) === undefined) {
@@ -330,6 +334,7 @@ export class EvalRunStore {
330
334
  ? {}
331
335
  : { bearerToken: opts.bearerToken }),
332
336
  ...(opts.verbose === undefined ? {} : { verbose: opts.verbose }),
337
+ ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
333
338
  onCaseStart: (id) => {
334
339
  const run = this.runs.get(runId);
335
340
  if (run === undefined) {