kradle 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -138,14 +138,41 @@ Uses file watching with debouncing (300ms) and hash comparison to minimize unnec
138
138
 
139
139
  ### Run Challenge
140
140
 
141
- Run a challenge in production or studio environment:
141
+ Run a challenge and wait for completion:
142
142
 
143
143
  ```bash
144
144
  kradle challenge run <challenge-name>
145
- kradle challenge run <challenge-name> --studio # Run in local studio environment
145
+ kradle challenge run <challenge-name> --studio # Run in local studio environment
146
146
  kradle challenge run <team-name>:<challenge-name> # Run a public challenge from another team
147
+ kradle challenge run <challenge-name> --no-open # Don't open browser
148
+ kradle challenge run <challenge-name> --no-wait # Fire and forget (don't wait for completion)
147
149
  ```
148
150
 
151
+ By default, the command opens the run URL in your browser and polls until the run completes, then displays the outcome.
152
+
153
+ ### List Runs
154
+
155
+ List your recent runs:
156
+
157
+ ```bash
158
+ kradle challenge runs list # List 10 most recent runs
159
+ kradle challenge runs list --limit 20 # List 20 most recent runs
160
+ ```
161
+
162
+ ### Get Run Details
163
+
164
+ Get details and logs for a specific run:
165
+
166
+ ```bash
167
+ kradle challenge runs get <run-id>
168
+ kradle challenge runs get <run-id> --no-logs # Skip fetching logs
169
+ ```
170
+
171
+ This displays:
172
+ - Run metadata (status, duration, end state)
173
+ - Participant results (agent, winner status, score)
174
+ - Log entries with timestamps and levels (unless `--no-logs` is used)
175
+
149
176
  ## Experiment Commands
150
177
 
151
178
  Experiments allow you to run batches of challenge runs with different agents and configurations, then analyze the results. This is useful for benchmarking agents, testing challenge difficulty, or gathering statistics across many runs.
@@ -424,6 +451,7 @@ kradle-cli/
424
451
  │ │ ├── agent/ # Agent commands
425
452
  │ │ ├── ai-docs/ # AI documentation commands
426
453
  │ │ ├── challenge/ # Challenge management commands
454
+ │ │ │ └── runs/ # Run listing and logs commands
427
455
  │ │ ├── experiment/ # Experiment commands
428
456
  │ │ └── world/ # World management commands
429
457
  │ └── lib/ # Core libraries
@@ -12,7 +12,11 @@ export default class Run extends Command {
12
12
  "studio-api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
13
  "studio-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
14
  studio: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
- open: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ "no-open": import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ "no-wait": import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ "no-summary": import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
18
  };
19
+ private pollForCompletion;
20
+ private displayRunResult;
17
21
  run(): Promise<void>;
18
22
  }
@@ -3,13 +3,18 @@ import pc from "picocolors";
3
3
  import { ApiClient } from "../../lib/api-client.js";
4
4
  import { getChallengeSlugArgument } from "../../lib/arguments.js";
5
5
  import { getConfigFlags } from "../../lib/flags.js";
6
- import { loadTemplateRun, openInBrowser } from "../../lib/utils.js";
6
+ import { formatDuration, getRunStatusDisplay, loadTemplateRun, openInBrowser } from "../../lib/utils.js";
7
+ const POLL_INTERVAL_MS = 2000;
8
+ const MAX_POLL_TIME_MS = 30 * 60 * 1000; // 30 minutes
9
+ const TERMINAL_STATUSES = ["finished", "game_over", "error", "completed", "cancelled", "timeout", "failed"];
7
10
  export default class Run extends Command {
8
11
  static description = "Run a challenge";
9
12
  static examples = [
10
13
  "<%= config.bin %> <%= command.id %> my-challenge",
11
14
  "<%= config.bin %> <%= command.id %> my-challenge --studio",
12
15
  "<%= config.bin %> <%= command.id %> team-name:my-challenge",
16
+ "<%= config.bin %> <%= command.id %> my-challenge --no-open",
17
+ "<%= config.bin %> <%= command.id %> my-challenge --no-wait",
13
18
  ];
14
19
  static args = {
15
20
  challengeSlug: getChallengeSlugArgument({
@@ -19,9 +24,83 @@ export default class Run extends Command {
19
24
  };
20
25
  static flags = {
21
26
  studio: Flags.boolean({ char: "s", description: "Run in studio environment", default: false }),
22
- open: Flags.boolean({ char: "o", description: "Open the run URL in the browser", default: false }),
27
+ "no-open": Flags.boolean({
28
+ description: "Don't open the run URL in the browser",
29
+ default: false,
30
+ }),
31
+ "no-wait": Flags.boolean({
32
+ description: "Don't wait for the run to complete (fire and forget)",
33
+ default: false,
34
+ }),
35
+ "no-summary": Flags.boolean({
36
+ description: "Don't wait for the AI-generated summary",
37
+ default: false,
38
+ }),
23
39
  ...getConfigFlags("api-key", "api-url", "web-url", "studio-url", "studio-api-url"),
24
40
  };
41
+ async pollForCompletion(api, runId, waitForSummary) {
42
+ let lastStatus = "";
43
+ let reachedTerminal = false;
44
+ let waitingForSummary = false;
45
+ const startTime = Date.now();
46
+ while (true) {
47
+ // Check for timeout
48
+ const elapsed = Date.now() - startTime;
49
+ if (elapsed > MAX_POLL_TIME_MS) {
50
+ this.log(pc.yellow("\nTimed out waiting for run completion"));
51
+ return;
52
+ }
53
+ const result = await api.getRunResult(runId);
54
+ const currentStatus = result.status;
55
+ // Show status changes
56
+ if (currentStatus !== lastStatus) {
57
+ const elapsedSecs = (elapsed / 1000).toFixed(0);
58
+ this.log(pc.dim(`[${elapsedSecs}s] Status: ${getRunStatusDisplay(currentStatus)}`));
59
+ lastStatus = currentStatus;
60
+ }
61
+ // Check for terminal state
62
+ if (TERMINAL_STATUSES.includes(currentStatus)) {
63
+ if (!reachedTerminal) {
64
+ reachedTerminal = true;
65
+ }
66
+ // If we need to wait for summary and it's not available yet, keep polling
67
+ if (waitForSummary && !result.summary) {
68
+ if (!waitingForSummary) {
69
+ waitingForSummary = true;
70
+ this.log(pc.dim(`Waiting for summary...`));
71
+ }
72
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
73
+ continue;
74
+ }
75
+ this.log("");
76
+ this.displayRunResult(result);
77
+ return;
78
+ }
79
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
80
+ }
81
+ }
82
+ displayRunResult(result) {
83
+ this.log(pc.bold("=== Run Complete ===\n"));
84
+ this.log(`${pc.dim("Status:")} ${getRunStatusDisplay(result.status)}`);
85
+ this.log(`${pc.dim("End State:")} ${result.endState || "-"}`);
86
+ this.log(`${pc.dim("Duration:")} ${formatDuration(result.totalTime)}`);
87
+ if (result.aggregatedResults) {
88
+ const agg = result.aggregatedResults;
89
+ this.log(`${pc.dim("Results:")} ${pc.green(String(agg.successfulParticipantCount))} successful / ${agg.participantCount} participants`);
90
+ }
91
+ if (result.participantResults && Object.keys(result.participantResults).length > 0) {
92
+ this.log(pc.dim("\nParticipants:"));
93
+ for (const [participantId, pr] of Object.entries(result.participantResults)) {
94
+ const winnerIcon = pr.winner ? pc.green("\u2713") : pc.red("\u2717");
95
+ const score = pr.score !== undefined ? ` (score: ${pr.score})` : "";
96
+ this.log(` ${winnerIcon} ${participantId}: ${pr.agent}${score}`);
97
+ }
98
+ }
99
+ if (result.summary) {
100
+ this.log(pc.bold("\n=== Summary ===\n"));
101
+ this.log(result.summary);
102
+ }
103
+ }
25
104
  async run() {
26
105
  const { args, flags } = await this.parse(Run);
27
106
  const apiUrl = flags.studio ? flags["studio-api-url"] : flags["api-url"];
@@ -29,23 +108,29 @@ export default class Run extends Command {
29
108
  const challengeSlug = args.challengeSlug;
30
109
  try {
31
110
  const { participants } = (await loadTemplateRun());
32
- const template = {
111
+ this.log(pc.blue(`>> Running challenge: ${challengeSlug}${flags.studio ? " (studio)" : ""}...`));
112
+ const response = await studioApi.runChallenge({
33
113
  challenge: challengeSlug,
34
114
  participants,
35
- };
36
- this.log(pc.blue(`>> Running challenge: ${challengeSlug}${flags.studio ? " (studio)" : ""}...`));
37
- const response = await studioApi.runChallenge(template);
115
+ jobType: "foreground",
116
+ });
38
117
  if (response.runIds && response.runIds.length > 0) {
118
+ const runId = response.runIds[0];
39
119
  const baseUrl = flags.studio ? flags["studio-url"] : flags["web-url"];
40
- const runUrl = `${baseUrl}/runs/${response.runIds[0]}`;
41
- this.log(pc.green("\n Challenge started!"));
120
+ const runUrl = `${baseUrl}/runs/${runId}`;
121
+ this.log(pc.green("\n\u2713 Challenge started!"));
122
+ this.log(pc.dim(`Run ID: ${runId}`));
42
123
  this.log(pc.dim(`Run URL: ${runUrl}`));
43
- if (flags.open) {
44
- await openInBrowser(runUrl);
124
+ if (!flags["no-open"]) {
125
+ openInBrowser(runUrl);
126
+ }
127
+ if (!flags["no-wait"]) {
128
+ this.log(pc.blue("\n>> Waiting for run to complete...\n"));
129
+ await this.pollForCompletion(studioApi, runId, !flags["no-summary"]);
45
130
  }
46
131
  }
47
132
  else {
48
- this.log(pc.yellow(" Challenge started but no run ID returned"));
133
+ this.log(pc.yellow("\u26a0 Challenge started but no run ID returned"));
49
134
  }
50
135
  }
51
136
  catch (error) {
@@ -0,0 +1,14 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class GetRun extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ runId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ "no-logs": import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,115 @@
1
+ import { Args, Command, Flags } from "@oclif/core";
2
+ import pc from "picocolors";
3
+ import { ApiClient } from "../../../lib/api-client.js";
4
+ import { getConfigFlags } from "../../../lib/flags.js";
5
+ import { formatDuration, formatTime } from "../../../lib/utils.js";
6
+ function getLogLevelColor(level) {
7
+ switch (level.toLowerCase()) {
8
+ case "error":
9
+ return pc.red;
10
+ case "warn":
11
+ case "warning":
12
+ return pc.yellow;
13
+ case "info":
14
+ return pc.blue;
15
+ case "debug":
16
+ return pc.dim;
17
+ default:
18
+ return (text) => text;
19
+ }
20
+ }
21
+ function formatLogMessage(log) {
22
+ if ("parsedMessage" in log) {
23
+ return JSON.stringify(log.parsedMessage, null, 2);
24
+ }
25
+ return log.message;
26
+ }
27
+ export default class GetRun extends Command {
28
+ static description = "Get details and logs for a specific run";
29
+ static examples = [
30
+ "<%= config.bin %> <%= command.id %> abc123",
31
+ "<%= config.bin %> <%= command.id %> abc123 --no-logs",
32
+ ];
33
+ static args = {
34
+ runId: Args.string({
35
+ description: "Run ID to get details for",
36
+ required: true,
37
+ }),
38
+ };
39
+ static flags = {
40
+ "no-logs": Flags.boolean({
41
+ description: "Skip fetching and displaying logs",
42
+ default: false,
43
+ }),
44
+ ...getConfigFlags("api-key", "api-url"),
45
+ };
46
+ async run() {
47
+ const { args, flags } = await this.parse(GetRun);
48
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
49
+ const showLogs = !flags["no-logs"];
50
+ this.log(pc.blue(`>> Loading run ${args.runId}...`));
51
+ try {
52
+ const [runResult, logs] = await Promise.all([
53
+ api.getRunResult(args.runId),
54
+ showLogs ? api.getRunLogs(args.runId) : Promise.resolve([]),
55
+ ]);
56
+ // Run metadata
57
+ this.log(pc.bold("\n=== Run Result ===\n"));
58
+ this.log(`${pc.dim("ID:")} ${runResult.id || args.runId}`);
59
+ this.log(`${pc.dim("Challenge:")} ${runResult.challenge || "-"}`);
60
+ this.log(`${pc.dim("Status:")} ${runResult.status}`);
61
+ this.log(`${pc.dim("End State:")} ${runResult.endState || "-"}`);
62
+ this.log(`${pc.dim("Finished:")} ${runResult.finishedStatus || "-"}`);
63
+ this.log(`${pc.dim("Duration:")} ${formatDuration(runResult.totalTime)}`);
64
+ this.log(`${pc.dim("End Time:")} ${formatTime(runResult.endTime)}`);
65
+ // Aggregated results
66
+ if (runResult.aggregatedResults) {
67
+ this.log(pc.bold("\n=== Aggregated Results ===\n"));
68
+ const agg = runResult.aggregatedResults;
69
+ this.log(`${pc.dim("Participants:")} ${agg.participantCount}`);
70
+ this.log(`${pc.dim("Successful:")} ${agg.successfulParticipantCount}`);
71
+ this.log(`${pc.dim("Unsuccessful:")} ${agg.unsuccessfulParticipantCount}`);
72
+ this.log(`${pc.dim("Total Time:")} ${formatDuration(agg.totalTime)}`);
73
+ }
74
+ // Participant results
75
+ if (runResult.participantResults && Object.keys(runResult.participantResults).length > 0) {
76
+ this.log(pc.bold("\n=== Participant Results ===\n"));
77
+ const headers = ["Participant", "Agent", "Winner", "Score", "Time to Success"];
78
+ const widths = [15, 35, 10, 10, 18];
79
+ this.log(headers.map((h, i) => h.padEnd(widths[i])).join(" "));
80
+ this.log("-".repeat(widths.reduce((a, b) => a + b + 1, 0)));
81
+ for (const [participantId, result] of Object.entries(runResult.participantResults)) {
82
+ const agentPadded = result.agent.padEnd(widths[1]);
83
+ const winnerText = result.winner ? "Yes" : "No";
84
+ const winnerPadded = winnerText.padEnd(widths[2]);
85
+ const winner = result.winner ? pc.green(winnerPadded) : pc.red(winnerPadded);
86
+ const score = result.score !== undefined ? String(result.score) : "-";
87
+ const timeToSuccess = result.timeToSuccess !== undefined ? formatDuration(result.timeToSuccess) : "-";
88
+ this.log(`${participantId.padEnd(widths[0])} ${agentPadded} ${winner} ${score.padEnd(widths[3])} ${timeToSuccess}`);
89
+ }
90
+ }
91
+ // Logs
92
+ if (showLogs) {
93
+ if (logs.length > 0) {
94
+ this.log(pc.bold("\n=== Logs ===\n"));
95
+ for (const log of logs) {
96
+ const time = formatTime(log.creationTime);
97
+ const levelColor = getLogLevelColor(log.level);
98
+ const levelText = `[${log.level.toUpperCase()}]`.padEnd(9);
99
+ const level = levelColor(levelText);
100
+ const participantText = `[${log.participantId}]`.padEnd(15);
101
+ const participant = pc.cyan(participantText);
102
+ const message = formatLogMessage(log);
103
+ this.log(`${pc.dim(time)} ${level} ${participant} ${message}`);
104
+ }
105
+ }
106
+ else {
107
+ this.log(pc.dim("\nNo logs found."));
108
+ }
109
+ }
110
+ }
111
+ catch (error) {
112
+ this.error(pc.red(`Failed to get run: ${error instanceof Error ? error.message : String(error)}`));
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class ListRuns extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ "api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ "api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,42 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import pc from "picocolors";
3
+ import { ApiClient } from "../../../lib/api-client.js";
4
+ import { getConfigFlags } from "../../../lib/flags.js";
5
+ import { formatDuration, formatTime, getRunStatusDisplay } from "../../../lib/utils.js";
6
+ export default class ListRuns extends Command {
7
+ static description = "List recent runs";
8
+ static examples = ["<%= config.bin %> <%= command.id %>", "<%= config.bin %> <%= command.id %> --limit 20"];
9
+ static flags = {
10
+ limit: Flags.integer({
11
+ char: "n",
12
+ description: "Number of runs to display",
13
+ default: 10,
14
+ }),
15
+ ...getConfigFlags("api-key", "api-url"),
16
+ };
17
+ async run() {
18
+ const { flags } = await this.parse(ListRuns);
19
+ const api = new ApiClient(flags["api-url"], flags["api-key"]);
20
+ this.log(pc.blue(">> Loading runs..."));
21
+ const { runs } = await api.listRuns(flags.limit);
22
+ if (runs.length === 0) {
23
+ this.log(pc.yellow("\nNo runs found."));
24
+ return;
25
+ }
26
+ this.log(pc.bold("\nRuns:\n"));
27
+ const headers = ["ID", "Challenge", "Status", "End State", "Duration", "Created"];
28
+ const widths = [38, 30, 15, 15, 12, 20];
29
+ this.log(headers.map((h, i) => h.padEnd(widths[i])).join(" "));
30
+ this.log("-".repeat(widths.reduce((a, b) => a + b + 1, 0)));
31
+ for (const run of runs) {
32
+ const challenge = run.challenge.length > 28 ? `${run.challenge.slice(0, 25)}...` : run.challenge;
33
+ const statusColored = getRunStatusDisplay(run.status);
34
+ const statusPadding = " ".repeat(Math.max(0, widths[2] - run.status.length));
35
+ const endState = run.endState || "-";
36
+ const duration = formatDuration(run.totalTime);
37
+ const created = formatTime(run.creationTime);
38
+ this.log(`${run.id.padEnd(widths[0])} ${challenge.padEnd(widths[1])} ${statusColored}${statusPadding} ${endState.padEnd(widths[3])} ${duration.padEnd(widths[4])} ${created}`);
39
+ }
40
+ this.log(pc.dim(`\nShowing ${runs.length} runs`));
41
+ }
42
+ }
@@ -1,5 +1,5 @@
1
1
  import type z from "zod";
2
- import { type AgentSchemaType, type ChallengeConfigSchemaType, type ChallengeSchemaType, type DashboardUrlResponse, type DownloadUrlResponse, HumanSchema, type ParsedLogEntry, type RecordingMetadata, type RunResultResponse, type RunStatusSchemaType, type WorldConfigSchemaType, type WorldSchemaType } from "./schemas.js";
2
+ import { type AgentSchemaType, type ChallengeConfigSchemaType, type ChallengeSchemaType, type DashboardUrlResponse, type DownloadUrlResponse, HumanSchema, type ParsedLogEntry, type RecordingMetadata, type Run, type RunResultResponse, type RunStatusSchemaType, type WorldConfigSchemaType, type WorldSchemaType } from "./schemas.js";
3
3
  export declare class ApiClient {
4
4
  private apiUrl;
5
5
  private kradleApiKey;
@@ -75,7 +75,8 @@ export declare class ApiClient {
75
75
  runChallenge(runData: {
76
76
  challenge: string;
77
77
  participants: unknown[];
78
- }, isBackground?: boolean): Promise<{
78
+ jobType: "background" | "foreground";
79
+ }): Promise<{
79
80
  runIds?: string[] | undefined;
80
81
  participants?: Record<string, {
81
82
  agent: string;
@@ -125,6 +126,15 @@ export declare class ApiClient {
125
126
  * @returns Run result with status, end_state, and participant results.
126
127
  */
127
128
  getRunResult(runId: string): Promise<RunResultResponse>;
129
+ /**
130
+ * List runs with pagination.
131
+ * @param limit - Maximum number of runs to return.
132
+ * @returns Object with runs array and optional nextPageToken.
133
+ */
134
+ listRuns(limit?: number): Promise<{
135
+ runs: Run[];
136
+ nextPageToken?: string;
137
+ }>;
128
138
  listWorlds(): Promise<WorldSchemaType[]>;
129
139
  listKradleWorlds(): Promise<WorldSchemaType[]>;
130
140
  getWorld(slug: string): Promise<WorldSchemaType>;
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import fs from "node:fs/promises";
3
- import { AgentsResponseSchema, ChallengeSchema, ChallengesResponseSchema, DashboardUrlResponseSchema, DownloadUrlResponseSchema, HumanSchema, JobResponseSchema, RecordingsListResponseSchema, RunLogsResponseSchema, RunResultResponseSchema, RunStatusSchema, UploadUrlResponseSchema, WorldSchema, WorldsResponseSchema, } from "./schemas.js";
3
+ import { AgentsResponseSchema, ChallengeSchema, ChallengesResponseSchema, DashboardUrlResponseSchema, DownloadUrlResponseSchema, HumanSchema, JobResponseSchema, ListRunsResponseSchema, RecordingsListResponseSchema, RunLogsResponseSchema, RunResultResponseSchema, RunStatusSchema, UploadUrlResponseSchema, WorldSchema, WorldsResponseSchema, } from "./schemas.js";
4
4
  const DEFAULT_PAGE_SIZE = 30;
5
5
  const DEFAULT_CHALLENGE_SCHEMA = {
6
6
  slug: "",
@@ -221,11 +221,10 @@ export class ApiClient {
221
221
  async getChallengeDownloadUrl(slug) {
222
222
  return this.get(`challenges/${slug}/datapackDownloadUrl`, {}, DownloadUrlResponseSchema);
223
223
  }
224
- async runChallenge(runData, isBackground = true) {
224
+ async runChallenge(runData) {
225
225
  const url = "jobs";
226
- const payload = isBackground ? { ...runData, jobType: "background" } : runData;
227
226
  return this.post(url, {
228
- body: JSON.stringify(payload),
227
+ body: JSON.stringify(runData),
229
228
  }, JobResponseSchema);
230
229
  }
231
230
  async deleteChallenge(challengeId) {
@@ -309,6 +308,32 @@ export class ApiClient {
309
308
  const url = `runs/${runId}`;
310
309
  return this.get(url, {}, RunResultResponseSchema);
311
310
  }
311
+ /**
312
+ * List runs with pagination.
313
+ * @param limit - Maximum number of runs to return.
314
+ * @returns Object with runs array and optional nextPageToken.
315
+ */
316
+ async listRuns(limit = 10) {
317
+ const runs = [];
318
+ let currentToken;
319
+ // Use consistent page size for all requests (page tokens are tied to page size)
320
+ const pageSize = DEFAULT_PAGE_SIZE;
321
+ while (runs.length < limit) {
322
+ const params = new URLSearchParams();
323
+ params.set("page_size", String(pageSize));
324
+ if (currentToken) {
325
+ params.set("page_token", currentToken);
326
+ }
327
+ const response = await this.get(`runs?${params}`, {}, ListRunsResponseSchema);
328
+ runs.push(...response.runs);
329
+ if (!response.nextPageToken || response.runs.length === 0) {
330
+ currentToken = undefined;
331
+ break;
332
+ }
333
+ currentToken = response.nextPageToken;
334
+ }
335
+ return { runs: runs.slice(0, limit), nextPageToken: currentToken };
336
+ }
312
337
  async listWorlds() {
313
338
  return this.listResource("worlds", "worlds", WorldsResponseSchema);
314
339
  }
@@ -161,7 +161,8 @@ export class Runner {
161
161
  const response = await this.api.runChallenge({
162
162
  challenge: state.config.challenge_slug,
163
163
  participants: state.config.participants,
164
- }, true);
164
+ jobType: "background",
165
+ });
165
166
  if (!response.runIds || response.runIds.length === 0) {
166
167
  throw new Error("No run ID returned from API");
167
168
  }
@@ -29,17 +29,17 @@ export type RunStatus = "queued" | "initializing" | "watcher_connected" | "parti
29
29
  export declare const ProgressEntrySchema: z.ZodObject<{
30
30
  index: z.ZodNumber;
31
31
  status: z.ZodEnum<{
32
+ finished: "finished";
33
+ game_over: "game_over";
34
+ completed: "completed";
35
+ started: "started";
36
+ initializing: "initializing";
32
37
  error: "error";
33
38
  queued: "queued";
34
- initializing: "initializing";
35
39
  watcher_connected: "watcher_connected";
36
40
  participants_connected: "participants_connected";
37
- started: "started";
38
41
  running: "running";
39
42
  recovering: "recovering";
40
- completed: "completed";
41
- game_over: "game_over";
42
- finished: "finished";
43
43
  }>;
44
44
  runId: z.ZodOptional<z.ZodString>;
45
45
  participantIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -52,17 +52,17 @@ export declare const ProgressSchema: z.ZodObject<{
52
52
  entries: z.ZodArray<z.ZodObject<{
53
53
  index: z.ZodNumber;
54
54
  status: z.ZodEnum<{
55
+ finished: "finished";
56
+ game_over: "game_over";
57
+ completed: "completed";
58
+ started: "started";
59
+ initializing: "initializing";
55
60
  error: "error";
56
61
  queued: "queued";
57
- initializing: "initializing";
58
62
  watcher_connected: "watcher_connected";
59
63
  participants_connected: "participants_connected";
60
- started: "started";
61
64
  running: "running";
62
65
  recovering: "recovering";
63
- completed: "completed";
64
- game_over: "game_over";
65
- finished: "finished";
66
66
  }>;
67
67
  runId: z.ZodOptional<z.ZodString>;
68
68
  participantIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -240,6 +240,7 @@ export declare const RunResultResponseSchema: z.ZodObject<{
240
240
  score: z.ZodOptional<z.ZodNumber>;
241
241
  timeToSuccess: z.ZodOptional<z.ZodNumber>;
242
242
  }, z.core.$strip>>>;
243
+ summary: z.ZodOptional<z.ZodString>;
243
244
  }, z.core.$strip>;
244
245
  export declare const WorldSchema: z.ZodObject<{
245
246
  id: z.ZodString;
@@ -279,6 +280,73 @@ export declare const WorldsResponseSchema: z.ZodObject<{
279
280
  export declare const DashboardUrlResponseSchema: z.ZodObject<{
280
281
  url: z.ZodString;
281
282
  }, z.core.$strip>;
283
+ export declare const RunSchema: z.ZodObject<{
284
+ id: z.ZodString;
285
+ challenge: z.ZodString;
286
+ status: z.ZodString;
287
+ visibility: z.ZodOptional<z.ZodEnum<{
288
+ private: "private";
289
+ public: "public";
290
+ }>>;
291
+ creator: z.ZodOptional<z.ZodString>;
292
+ creationTime: z.ZodOptional<z.ZodString>;
293
+ updateTime: z.ZodOptional<z.ZodString>;
294
+ startTime: z.ZodOptional<z.ZodString>;
295
+ endTime: z.ZodOptional<z.ZodString>;
296
+ totalTime: z.ZodOptional<z.ZodNumber>;
297
+ endState: z.ZodOptional<z.ZodString>;
298
+ finishedStatus: z.ZodOptional<z.ZodString>;
299
+ aggregatedResults: z.ZodOptional<z.ZodObject<{
300
+ participantCount: z.ZodNumber;
301
+ successfulParticipantCount: z.ZodNumber;
302
+ successfulParticipantIds: z.ZodArray<z.ZodString>;
303
+ unsuccessfulParticipantCount: z.ZodNumber;
304
+ unsuccessfulParticipantIds: z.ZodArray<z.ZodString>;
305
+ totalTime: z.ZodNumber;
306
+ }, z.core.$strip>>;
307
+ participantResults: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
308
+ agent: z.ZodString;
309
+ winner: z.ZodBoolean;
310
+ score: z.ZodOptional<z.ZodNumber>;
311
+ timeToSuccess: z.ZodOptional<z.ZodNumber>;
312
+ }, z.core.$strip>>>;
313
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
314
+ }, z.core.$strip>;
315
+ export declare const ListRunsResponseSchema: z.ZodObject<{
316
+ runs: z.ZodArray<z.ZodObject<{
317
+ id: z.ZodString;
318
+ challenge: z.ZodString;
319
+ status: z.ZodString;
320
+ visibility: z.ZodOptional<z.ZodEnum<{
321
+ private: "private";
322
+ public: "public";
323
+ }>>;
324
+ creator: z.ZodOptional<z.ZodString>;
325
+ creationTime: z.ZodOptional<z.ZodString>;
326
+ updateTime: z.ZodOptional<z.ZodString>;
327
+ startTime: z.ZodOptional<z.ZodString>;
328
+ endTime: z.ZodOptional<z.ZodString>;
329
+ totalTime: z.ZodOptional<z.ZodNumber>;
330
+ endState: z.ZodOptional<z.ZodString>;
331
+ finishedStatus: z.ZodOptional<z.ZodString>;
332
+ aggregatedResults: z.ZodOptional<z.ZodObject<{
333
+ participantCount: z.ZodNumber;
334
+ successfulParticipantCount: z.ZodNumber;
335
+ successfulParticipantIds: z.ZodArray<z.ZodString>;
336
+ unsuccessfulParticipantCount: z.ZodNumber;
337
+ unsuccessfulParticipantIds: z.ZodArray<z.ZodString>;
338
+ totalTime: z.ZodNumber;
339
+ }, z.core.$strip>>;
340
+ participantResults: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
341
+ agent: z.ZodString;
342
+ winner: z.ZodBoolean;
343
+ score: z.ZodOptional<z.ZodNumber>;
344
+ timeToSuccess: z.ZodOptional<z.ZodNumber>;
345
+ }, z.core.$strip>>>;
346
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
347
+ }, z.core.$strip>>;
348
+ nextPageToken: z.ZodOptional<z.ZodString>;
349
+ }, z.core.$strip>;
282
350
  export type ChallengeSchemaType = z.infer<typeof ChallengeSchema>;
283
351
  export type ChallengeConfigSchemaType = z.infer<typeof ChallengeConfigSchema>;
284
352
  export type ChallengesResponseType = z.infer<typeof ChallengesResponseSchema>;
@@ -303,3 +371,5 @@ export type WorldSchemaType = z.infer<typeof WorldSchema>;
303
371
  export type WorldConfigSchemaType = z.infer<typeof WorldConfigSchema>;
304
372
  export type WorldsResponseType = z.infer<typeof WorldsResponseSchema>;
305
373
  export type DashboardUrlResponse = z.infer<typeof DashboardUrlResponseSchema>;
374
+ export type Run = z.infer<typeof RunSchema>;
375
+ export type ListRunsResponse = z.infer<typeof ListRunsResponseSchema>;
@@ -123,6 +123,7 @@ export const RunResultResponseSchema = z.object({
123
123
  endTime: z.string().optional(),
124
124
  aggregatedResults: RunAggregatedResultsSchema.optional(),
125
125
  participantResults: z.record(z.string(), RunParticipantResultSchema).optional(),
126
+ summary: z.string().optional(),
126
127
  });
127
128
  export const WorldSchema = z.object({
128
129
  id: z.string(),
@@ -147,3 +148,24 @@ export const WorldsResponseSchema = z.object({
147
148
  export const DashboardUrlResponseSchema = z.object({
148
149
  url: z.string(),
149
150
  });
151
+ export const RunSchema = z.object({
152
+ id: z.string(),
153
+ challenge: z.string(),
154
+ status: z.string(),
155
+ visibility: z.enum(["private", "public"]).optional(),
156
+ creator: z.string().optional(),
157
+ creationTime: z.string().optional(),
158
+ updateTime: z.string().optional(),
159
+ startTime: z.string().optional(),
160
+ endTime: z.string().optional(),
161
+ totalTime: z.number().optional(),
162
+ endState: z.string().optional(),
163
+ finishedStatus: z.string().optional(),
164
+ aggregatedResults: RunAggregatedResultsSchema.optional(),
165
+ participantResults: z.record(z.string(), RunParticipantResultSchema).optional(),
166
+ tags: z.array(z.string()).optional(),
167
+ });
168
+ export const ListRunsResponseSchema = z.object({
169
+ runs: z.array(RunSchema),
170
+ nextPageToken: z.string().optional(),
171
+ });
@@ -94,3 +94,21 @@ export declare function loadTypescriptExport(filePath: string, exportName: strin
94
94
  * @param url The URL to open.
95
95
  */
96
96
  export declare function openInBrowser(url: string): void;
97
+ /**
98
+ * Format a duration in milliseconds to a human-readable string.
99
+ * @param ms Duration in milliseconds
100
+ * @returns Formatted string like "45.3s" or "2m 30s"
101
+ */
102
+ export declare function formatDuration(ms: number | undefined): string;
103
+ /**
104
+ * Format an ISO timestamp to a locale string.
105
+ * @param isoString ISO 8601 timestamp
106
+ * @returns Formatted locale string or "-" if undefined
107
+ */
108
+ export declare function formatTime(isoString: string | undefined): string;
109
+ /**
110
+ * Get a colored string for a run status.
111
+ * @param status The run status
112
+ * @returns Colored status string
113
+ */
114
+ export declare function getRunStatusDisplay(status: string): string;
package/dist/lib/utils.js CHANGED
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import pc from "picocolors";
6
7
  export async function loadTemplateRun() {
7
8
  const templatePath = path.resolve(process.cwd(), "template-run.json");
8
9
  const content = await fs.readFile(templatePath, "utf-8");
@@ -197,3 +198,54 @@ export function openInBrowser(url) {
197
198
  }
198
199
  exec(command);
199
200
  }
201
+ /**
202
+ * Format a duration in milliseconds to a human-readable string.
203
+ * @param ms Duration in milliseconds
204
+ * @returns Formatted string like "45.3s" or "2m 30s"
205
+ */
206
+ export function formatDuration(ms) {
207
+ if (ms === undefined)
208
+ return "-";
209
+ const seconds = ms / 1000;
210
+ if (seconds < 60)
211
+ return `${seconds.toFixed(1)}s`;
212
+ const minutes = Math.floor(seconds / 60);
213
+ const remainingSeconds = seconds % 60;
214
+ return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
215
+ }
216
+ /**
217
+ * Format an ISO timestamp to a locale string.
218
+ * @param isoString ISO 8601 timestamp
219
+ * @returns Formatted locale string or "-" if undefined
220
+ */
221
+ export function formatTime(isoString) {
222
+ if (!isoString)
223
+ return "-";
224
+ const date = new Date(isoString);
225
+ return date.toLocaleString();
226
+ }
227
+ /**
228
+ * Get a colored string for a run status.
229
+ * @param status The run status
230
+ * @returns Colored status string
231
+ */
232
+ export function getRunStatusDisplay(status) {
233
+ switch (status) {
234
+ case "finished":
235
+ case "game_over":
236
+ case "completed":
237
+ return pc.green(status);
238
+ case "started":
239
+ return pc.blue(status);
240
+ case "initializing":
241
+ case "created":
242
+ return pc.yellow(status);
243
+ case "error":
244
+ case "failed":
245
+ case "cancelled":
246
+ case "timeout":
247
+ return pc.red(status);
248
+ default:
249
+ return status;
250
+ }
251
+ }
@@ -42,6 +42,50 @@
42
42
  "init.js"
43
43
  ]
44
44
  },
45
+ "agent:list": {
46
+ "aliases": [],
47
+ "args": {},
48
+ "description": "List all agents",
49
+ "examples": [
50
+ "<%= config.bin %> <%= command.id %>"
51
+ ],
52
+ "flags": {
53
+ "api-key": {
54
+ "description": "Kradle API key",
55
+ "env": "KRADLE_API_KEY",
56
+ "name": "api-key",
57
+ "required": true,
58
+ "hasDynamicHelp": false,
59
+ "multiple": false,
60
+ "type": "option"
61
+ },
62
+ "api-url": {
63
+ "description": "Kradle Web API URL",
64
+ "env": "KRADLE_API_URL",
65
+ "name": "api-url",
66
+ "required": true,
67
+ "default": "https://api.kradle.ai/v0",
68
+ "hasDynamicHelp": false,
69
+ "multiple": false,
70
+ "type": "option"
71
+ }
72
+ },
73
+ "hasDynamicHelp": false,
74
+ "hiddenAliases": [],
75
+ "id": "agent:list",
76
+ "pluginAlias": "kradle",
77
+ "pluginName": "kradle",
78
+ "pluginType": "core",
79
+ "strict": true,
80
+ "enableJsonFlag": false,
81
+ "isESM": true,
82
+ "relativePath": [
83
+ "dist",
84
+ "commands",
85
+ "agent",
86
+ "list.js"
87
+ ]
88
+ },
45
89
  "ai-docs:challenges-sdk": {
46
90
  "aliases": [],
47
91
  "args": {
@@ -451,7 +495,9 @@
451
495
  "examples": [
452
496
  "<%= config.bin %> <%= command.id %> my-challenge",
453
497
  "<%= config.bin %> <%= command.id %> my-challenge --studio",
454
- "<%= config.bin %> <%= command.id %> team-name:my-challenge"
498
+ "<%= config.bin %> <%= command.id %> team-name:my-challenge",
499
+ "<%= config.bin %> <%= command.id %> my-challenge --no-open",
500
+ "<%= config.bin %> <%= command.id %> my-challenge --no-wait"
455
501
  ],
456
502
  "flags": {
457
503
  "studio": {
@@ -461,10 +507,21 @@
461
507
  "allowNo": false,
462
508
  "type": "boolean"
463
509
  },
464
- "open": {
465
- "char": "o",
466
- "description": "Open the run URL in the browser",
467
- "name": "open",
510
+ "no-open": {
511
+ "description": "Don't open the run URL in the browser",
512
+ "name": "no-open",
513
+ "allowNo": false,
514
+ "type": "boolean"
515
+ },
516
+ "no-wait": {
517
+ "description": "Don't wait for the run to complete (fire and forget)",
518
+ "name": "no-wait",
519
+ "allowNo": false,
520
+ "type": "boolean"
521
+ },
522
+ "no-summary": {
523
+ "description": "Don't wait for the AI-generated summary",
524
+ "name": "no-summary",
468
525
  "allowNo": false,
469
526
  "type": "boolean"
470
527
  },
@@ -1198,14 +1255,82 @@
1198
1255
  "push.js"
1199
1256
  ]
1200
1257
  },
1201
- "agent:list": {
1258
+ "challenge:runs:get": {
1259
+ "aliases": [],
1260
+ "args": {
1261
+ "runId": {
1262
+ "description": "Run ID to get details for",
1263
+ "name": "runId",
1264
+ "required": true
1265
+ }
1266
+ },
1267
+ "description": "Get details and logs for a specific run",
1268
+ "examples": [
1269
+ "<%= config.bin %> <%= command.id %> abc123",
1270
+ "<%= config.bin %> <%= command.id %> abc123 --no-logs"
1271
+ ],
1272
+ "flags": {
1273
+ "no-logs": {
1274
+ "description": "Skip fetching and displaying logs",
1275
+ "name": "no-logs",
1276
+ "allowNo": false,
1277
+ "type": "boolean"
1278
+ },
1279
+ "api-key": {
1280
+ "description": "Kradle API key",
1281
+ "env": "KRADLE_API_KEY",
1282
+ "name": "api-key",
1283
+ "required": true,
1284
+ "hasDynamicHelp": false,
1285
+ "multiple": false,
1286
+ "type": "option"
1287
+ },
1288
+ "api-url": {
1289
+ "description": "Kradle Web API URL",
1290
+ "env": "KRADLE_API_URL",
1291
+ "name": "api-url",
1292
+ "required": true,
1293
+ "default": "https://api.kradle.ai/v0",
1294
+ "hasDynamicHelp": false,
1295
+ "multiple": false,
1296
+ "type": "option"
1297
+ }
1298
+ },
1299
+ "hasDynamicHelp": false,
1300
+ "hiddenAliases": [],
1301
+ "id": "challenge:runs:get",
1302
+ "pluginAlias": "kradle",
1303
+ "pluginName": "kradle",
1304
+ "pluginType": "core",
1305
+ "strict": true,
1306
+ "enableJsonFlag": false,
1307
+ "isESM": true,
1308
+ "relativePath": [
1309
+ "dist",
1310
+ "commands",
1311
+ "challenge",
1312
+ "runs",
1313
+ "get.js"
1314
+ ]
1315
+ },
1316
+ "challenge:runs:list": {
1202
1317
  "aliases": [],
1203
1318
  "args": {},
1204
- "description": "List all agents",
1319
+ "description": "List recent runs",
1205
1320
  "examples": [
1206
- "<%= config.bin %> <%= command.id %>"
1321
+ "<%= config.bin %> <%= command.id %>",
1322
+ "<%= config.bin %> <%= command.id %> --limit 20"
1207
1323
  ],
1208
1324
  "flags": {
1325
+ "limit": {
1326
+ "char": "n",
1327
+ "description": "Number of runs to display",
1328
+ "name": "limit",
1329
+ "default": 10,
1330
+ "hasDynamicHelp": false,
1331
+ "multiple": false,
1332
+ "type": "option"
1333
+ },
1209
1334
  "api-key": {
1210
1335
  "description": "Kradle API key",
1211
1336
  "env": "KRADLE_API_KEY",
@@ -1228,7 +1353,7 @@
1228
1353
  },
1229
1354
  "hasDynamicHelp": false,
1230
1355
  "hiddenAliases": [],
1231
- "id": "agent:list",
1356
+ "id": "challenge:runs:list",
1232
1357
  "pluginAlias": "kradle",
1233
1358
  "pluginName": "kradle",
1234
1359
  "pluginType": "core",
@@ -1238,10 +1363,11 @@
1238
1363
  "relativePath": [
1239
1364
  "dist",
1240
1365
  "commands",
1241
- "agent",
1366
+ "challenge",
1367
+ "runs",
1242
1368
  "list.js"
1243
1369
  ]
1244
1370
  }
1245
1371
  },
1246
- "version": "0.4.1"
1372
+ "version": "0.4.3"
1247
1373
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kradle",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Kradle's CLI. Manage challenges, experiments, agents and more!",
5
5
  "keywords": [
6
6
  "cli"
@@ -310,13 +310,15 @@ kradle challenge watch my-challenge
310
310
 
311
311
  ### `kradle challenge run <name>`
312
312
 
313
- Runs a challenge with configured participants.
313
+ Runs a challenge with configured participants and waits for completion.
314
314
 
315
315
  **Usage:**
316
316
  ```bash
317
317
  kradle challenge run <challenge-name>
318
318
  kradle challenge run <challenge-name> --studio
319
319
  kradle challenge run <team-name>:<challenge-name>
320
+ kradle challenge run <challenge-name> --no-open
321
+ kradle challenge run <challenge-name> --no-wait
320
322
  ```
321
323
 
322
324
  **Arguments:**
@@ -325,10 +327,20 @@ kradle challenge run <team-name>:<challenge-name>
325
327
  | `challenge-name` | Challenge to run. Can be a short slug (e.g., `my-challenge`) or include a team/user namespace (e.g., `team-name:my-challenge`). The namespace is useful for running public challenges owned by other teams. If no namespace is provided, it defaults to the user's own namespace. | Yes |
326
328
 
327
329
  **Flags:**
328
- | Flag | Description | Default |
329
- |------|-------------|---------|
330
- | `--studio` | Run in local studio environment instead of production | false |
331
- | `--open` | Open the run URL in the browser | false |
330
+ | Flag | Short | Description |
331
+ |------|-------|-------------|
332
+ | `--studio` | `-s` | Run in local studio environment instead of production |
333
+ | `--no-open` | | Don't open the run URL in the browser |
334
+ | `--no-wait` | | Don't wait for completion (fire and forget) |
335
+ | `--no-summary` | | Don't wait for the AI-generated summary |
336
+
337
+ **Behavior:**
338
+ 1. Creates a run with the challenge and participants from `template-run.json`
339
+ 2. Opens the run URL in the browser (unless `--no-open`)
340
+ 3. Polls every 2 seconds for status changes until completion (unless `--no-wait`)
341
+ 4. Displays the final outcome including status, duration, and participant results
342
+
343
+ **Terminal states:** The polling stops when the run reaches: `finished`, `game_over`, `error`, or `completed`.
332
344
 
333
345
  **Prerequisites:**
334
346
  - `template-run.json` must exist in project root with participant configuration
@@ -352,7 +364,7 @@ kradle challenge run <team-name>:<challenge-name>
352
364
 
353
365
  **Examples:**
354
366
  ```bash
355
- # Run your own challenge in production
367
+ # Run and wait for completion (default behavior)
356
368
  kradle challenge run my-challenge
357
369
 
358
370
  # Run a public challenge from another team
@@ -360,6 +372,115 @@ kradle challenge run team-kradle:battle-royale
360
372
 
361
373
  # Run in local studio
362
374
  kradle challenge run my-challenge --studio
375
+
376
+ # Run without opening browser
377
+ kradle challenge run my-challenge --no-open
378
+
379
+ # Fire and forget (don't wait for completion)
380
+ kradle challenge run my-challenge --no-wait
381
+
382
+ # Fire and forget without browser
383
+ kradle challenge run my-challenge --no-open --no-wait
384
+ ```
385
+
386
+ ---
387
+
388
+ ### `kradle challenge runs list`
389
+
390
+ Lists recent runs for the authenticated user.
391
+
392
+ **Usage:**
393
+ ```bash
394
+ kradle challenge runs list
395
+ kradle challenge runs list --limit 20
396
+ ```
397
+
398
+ **Flags:**
399
+ | Flag | Short | Description | Default |
400
+ |------|-------|-------------|---------|
401
+ | `--limit` | `-n` | Number of runs to display | 10 |
402
+
403
+ **Output format:**
404
+ ```
405
+ Runs:
406
+
407
+ ID Challenge Status End State Duration Created
408
+ ---------------------------------------------------------------------------------------------------------
409
+ abc123def... username:my-challenge finished game_over 45.3s 1/15/2025, 2:30 PM
410
+ xyz789abc... team-kradle:example error - 12.1s 1/15/2025, 2:15 PM
411
+ ```
412
+
413
+ **Status colors:**
414
+ - Green: `finished`, `game_over`, `completed`
415
+ - Blue: `started`
416
+ - Yellow: `initializing`, `created`
417
+ - Red: `error`
418
+
419
+ **Examples:**
420
+ ```bash
421
+ # List 10 most recent runs (default)
422
+ kradle challenge runs list
423
+
424
+ # List 20 most recent runs
425
+ kradle challenge runs list --limit 20
426
+
427
+ # List 5 runs
428
+ kradle challenge runs list -n 5
429
+ ```
430
+
431
+ ---
432
+
433
+ ### `kradle challenge runs get <run-id>`
434
+
435
+ Gets details and optionally logs for a specific run.
436
+
437
+ **Usage:**
438
+ ```bash
439
+ kradle challenge runs get <run-id>
440
+ kradle challenge runs get <run-id> --no-logs
441
+ ```
442
+
443
+ **Arguments:**
444
+ | Argument | Description | Required |
445
+ |----------|-------------|----------|
446
+ | `run-id` | The ID of the run to get details for | Yes |
447
+
448
+ **Flags:**
449
+ | Flag | Description | Default |
450
+ |------|-------------|---------|
451
+ | `--no-logs` | Skip fetching and displaying logs | false |
452
+
453
+ **Output sections:**
454
+
455
+ 1. **Run Result** - Metadata about the run:
456
+ - ID, Challenge, Status, End State, Finished Status, Duration, End Time
457
+
458
+ 2. **Aggregated Results** - Summary statistics:
459
+ - Total participants, successful count, unsuccessful count, total time
460
+
461
+ 3. **Participant Results** - Per-participant breakdown (table format):
462
+ - Participant ID, Agent name, Winner status, Score, Time to Success
463
+
464
+ 4. **Logs** - Log entries from the run (unless `--no-logs` is used):
465
+ - Timestamp, Level (colored), Participant ID, Message
466
+ - JSON messages are automatically parsed and formatted
467
+
468
+ **Log level colors:**
469
+ - Red: `error`
470
+ - Yellow: `warn`, `warning`
471
+ - Blue: `info`
472
+ - Dim: `debug`
473
+
474
+ **Examples:**
475
+ ```bash
476
+ # Get full details including logs
477
+ kradle challenge runs get abc123def456
478
+
479
+ # Get details without logs (faster)
480
+ kradle challenge runs get abc123def456 --no-logs
481
+
482
+ # Get details for a run (full ID)
483
+ kradle challenge runs get 12345678-1234-1234-1234-123456789012
363
484
  ```
364
485
 
365
486
  ---
@@ -1024,7 +1145,9 @@ kradle challenge build --all --visibility public
1024
1145
  | `kradle challenge list` | List all challenges |
1025
1146
  | `kradle challenge pull [name]` | Pull challenge from cloud |
1026
1147
  | `kradle challenge watch <name>` | Watch and auto-rebuild |
1027
- | `kradle challenge run <name>` | Run challenge |
1148
+ | `kradle challenge run <name>` | Run challenge and wait for completion |
1149
+ | `kradle challenge runs list` | List recent runs |
1150
+ | `kradle challenge runs get <run-id>` | Get details and logs for a run |
1028
1151
  | `kradle experiment create <name>` | Create new experiment |
1029
1152
  | `kradle experiment run <name>` | Run/resume experiment |
1030
1153
  | `kradle experiment recordings <name>` | Download recordings |