@pantheon.ai/agents 0.3.0 → 0.3.2

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
@@ -49,6 +49,32 @@ To avoid running it twice, apply the rest of the file to `pantheon_agents`:
49
49
  sed '1d' src/db/schema/tidb.sql | mysql --host 127.0.0.1 --port 4000 -u root pantheon_agents
50
50
  ```
51
51
 
52
+ ## Versioned Migrations
53
+
54
+ When schema changes across versions, add versioned migration SQL files under:
55
+
56
+ - `src/db/migrations/tidb`
57
+ - `src/db/migrations/db9`
58
+
59
+ Example migration added in this version:
60
+
61
+ - `src/db/migrations/tidb/20260304_0001_add_task_retry_columns.sql`
62
+ - `src/db/migrations/db9/20260304_0001_add_task_retry_columns.sql`
63
+ - `src/db/migrations/tidb/20260305_0002_add_agent_config_envs.sql`
64
+ - `src/db/migrations/db9/20260305_0002_add_agent_config_envs.sql`
65
+
66
+ Apply manually:
67
+
68
+ ```bash
69
+ # TiDB/MySQL
70
+ mysql --host 127.0.0.1 --port 4000 -u root pantheon_agents < src/db/migrations/tidb/20260304_0001_add_task_retry_columns.sql
71
+ mysql --host 127.0.0.1 --port 4000 -u root pantheon_agents < src/db/migrations/tidb/20260305_0002_add_agent_config_envs.sql
72
+
73
+ # db9/PostgreSQL
74
+ psql "$DATABASE_URL" -f src/db/migrations/db9/20260304_0001_add_task_retry_columns.sql
75
+ psql "$DATABASE_URL" -f src/db/migrations/db9/20260305_0002_add_agent_config_envs.sql
76
+ ```
77
+
52
78
  ## Developer Local Setup
53
79
 
54
80
  This section is only for developing this package locally.
@@ -90,6 +116,9 @@ pantheon-agents config reviewer "Download some files" --project-id 019c0495-f77a
90
116
 
91
117
  # update role on later config request
92
118
  pantheon-agents config reviewer "Switch to review workflow" --project-id 019c0495-f77a-7b6c-ade0-6b59c6654617 --role reviewer
119
+
120
+ # set per-project branch envs (passed on every task start)
121
+ pantheon-agents config reviewer "Use Anthropic model" --project-id 019c0495-f77a-7b6c-ade0-6b59c6654617 --envs '{"ANTHROPIC_MODEL":"claude-opus-4-5"}'
93
122
  ```
94
123
 
95
124
  ### 2) Add a task
@@ -136,6 +165,10 @@ pantheon-agents show-config <agent-name>
136
165
  pantheon-agents show-tasks --all
137
166
  pantheon-agents get-task <agent-name> <task-id>
138
167
  pantheon-agents cancel-task <agent-name> <task-id> [reason] --yes
168
+ pantheon-agents retry-task <agent-name> <task-id> [reason] --yes
169
+ pantheon-agents kill <agent-name> <task-id> [reason] --yes
170
+ pantheon-agents skill.sh
171
+ pantheon-agents gen-migration-sql --provider tidb --from 0.3.0
139
172
  pantheon-agents delete-task <agent-name> <task-id>
140
173
  ```
141
174
 
@@ -0,0 +1,11 @@
1
+ -- introduced_in: 0.3.1
2
+ -- Add retry metadata and policy columns for db9/PostgreSQL.
3
+
4
+ ALTER TABLE task
5
+ ADD COLUMN IF NOT EXISTS attempt_count INT NOT NULL DEFAULT 1;
6
+
7
+ ALTER TABLE agent_project_config
8
+ ADD COLUMN IF NOT EXISTS max_retry_attempts INT NOT NULL DEFAULT 3;
9
+
10
+ ALTER TABLE agent_project_config
11
+ ADD COLUMN IF NOT EXISTS retry_backoff_seconds INT NOT NULL DEFAULT 30;
@@ -0,0 +1,9 @@
1
+ -- introduced_in: 0.3.1
2
+ -- Add retry metadata and policy columns for TiDB/MySQL.
3
+
4
+ ALTER TABLE task
5
+ ADD COLUMN IF NOT EXISTS attempt_count INT NOT NULL DEFAULT 1;
6
+
7
+ ALTER TABLE agent_project_config
8
+ ADD COLUMN IF NOT EXISTS max_retry_attempts INT NOT NULL DEFAULT 3,
9
+ ADD COLUMN IF NOT EXISTS retry_backoff_seconds INT NOT NULL DEFAULT 30;
package/dist/index.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import { createRequire } from "node:module";
3
3
  import { Command, InvalidArgumentError, createCommand } from "commander";
4
4
  import process$1 from "node:process";
5
- import * as fs from "node:fs";
5
+ import * as fs$1 from "node:fs";
6
+ import fs from "node:fs";
6
7
  import path from "node:path";
7
8
  import { multistream, pino, transport } from "pino";
8
9
  import { Kysely, MysqlDialect, PostgresDialect } from "kysely";
@@ -11,6 +12,7 @@ import z$1, { z } from "zod";
11
12
  import { createPool } from "mysql2";
12
13
  import readline from "node:readline/promises";
13
14
  import expandTilde from "expand-tilde";
15
+ import { fileURLToPath } from "node:url";
14
16
  import blessed from "reblessed";
15
17
  import { inspect } from "node:util";
16
18
  import { parse } from "shell-quote";
@@ -85,7 +87,7 @@ var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
85
87
  //#endregion
86
88
  //#region ../../node_modules/dotenv/lib/main.js
87
89
  var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
88
- const fs$1 = __require("fs");
90
+ const fs$2 = __require("fs");
89
91
  const path$1 = __require("path");
90
92
  const os = __require("os");
91
93
  const crypto = __require("crypto");
@@ -222,10 +224,10 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
222
224
  function _vaultPath(options) {
223
225
  let possibleVaultPath = null;
224
226
  if (options && options.path && options.path.length > 0) if (Array.isArray(options.path)) {
225
- for (const filepath of options.path) if (fs$1.existsSync(filepath)) possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
227
+ for (const filepath of options.path) if (fs$2.existsSync(filepath)) possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
226
228
  } else possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
227
229
  else possibleVaultPath = path$1.resolve(process.cwd(), ".env.vault");
228
- if (fs$1.existsSync(possibleVaultPath)) return possibleVaultPath;
230
+ if (fs$2.existsSync(possibleVaultPath)) return possibleVaultPath;
229
231
  return null;
230
232
  }
231
233
  function _resolveHome(envPath) {
@@ -259,7 +261,7 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
259
261
  let lastError;
260
262
  const parsedAll = {};
261
263
  for (const path of optionPaths) try {
262
- const parsed = DotenvModule.parse(fs$1.readFileSync(path, { encoding }));
264
+ const parsed = DotenvModule.parse(fs$2.readFileSync(path, { encoding }));
263
265
  DotenvModule.populate(parsedAll, parsed, options);
264
266
  } catch (e) {
265
267
  if (debug) _debug(`Failed to load ${path} ${e.message}`);
@@ -397,7 +399,7 @@ var require_cli_options = /* @__PURE__ */ __commonJSMin(((exports, module) => {
397
399
 
398
400
  //#endregion
399
401
  //#region package.json
400
- var version = "0.2.2";
402
+ var version = "0.3.1";
401
403
 
402
404
  //#endregion
403
405
  //#region src/db/db9.ts
@@ -427,8 +429,10 @@ const taskCommonSchema = z.object({
427
429
  base_branch_id: z.uuid(),
428
430
  parent_task_id: z.string().nullable().optional(),
429
431
  config_version: z.number().int().positive().nullable().default(null),
432
+ attempt_count: z.coerce.number().int().positive().optional(),
430
433
  cancel_reason: z.string().nullable().default(null),
431
- queued_at: z.coerce.date()
434
+ queued_at: z.coerce.date(),
435
+ error: z.string().nullable().optional()
432
436
  });
433
437
  const taskItemSchema = z.discriminatedUnion("status", [
434
438
  taskCommonSchema.extend({ status: z.literal("pending") }),
@@ -510,6 +514,7 @@ var TaskListProvider = class {
510
514
  base_branch_id: params.base_branch_id,
511
515
  parent_task_id: parentTaskId,
512
516
  config_version: params.type === "reconfig" || params.type === "bootstrap" ? params.configVersion : null,
517
+ attempt_count: 1,
513
518
  cancel_reason: null,
514
519
  queued_at: /* @__PURE__ */ new Date()
515
520
  });
@@ -630,6 +635,8 @@ var TaskListDb9Provider = class extends TaskListProvider {
630
635
  "agent_project_config.config_version",
631
636
  "agent_project_config.config_task_id",
632
637
  "agent_project_config.concurrency",
638
+ "agent_project_config.max_retry_attempts",
639
+ "agent_project_config.retry_backoff_seconds",
633
640
  "agent_project_config.role",
634
641
  "agent_project_config.skills",
635
642
  "agent_project_config.prototype_url",
@@ -646,6 +653,8 @@ var TaskListDb9Provider = class extends TaskListProvider {
646
653
  "agent_project_config.config_version",
647
654
  "agent_project_config.config_task_id",
648
655
  "agent_project_config.concurrency",
656
+ "agent_project_config.max_retry_attempts",
657
+ "agent_project_config.retry_backoff_seconds",
649
658
  "agent_project_config.role",
650
659
  "agent_project_config.skills",
651
660
  "agent_project_config.prototype_url",
@@ -653,16 +662,24 @@ var TaskListDb9Provider = class extends TaskListProvider {
653
662
  ]).select((eb) => eb.ref("task.status").$castTo().as("config_task_status")).where("agent_project_config.agent", "=", this.agentName).execute();
654
663
  }
655
664
  async setAgentConfig({ skills, ...config }) {
665
+ const maxRetryAttempts = config.max_retry_attempts ?? 3;
666
+ const retryBackoffSeconds = config.retry_backoff_seconds ?? 30;
656
667
  await this.db.insertInto("agent_project_config").values({
657
668
  agent: this.agentName,
658
669
  skills: JSON.stringify(skills),
659
- ...config
670
+ ...config,
671
+ max_retry_attempts: maxRetryAttempts,
672
+ retry_backoff_seconds: retryBackoffSeconds
660
673
  }).execute();
661
674
  }
662
675
  async updateAgentConfig({ skills, ...config }) {
676
+ const maxRetryAttempts = config.max_retry_attempts;
677
+ const retryBackoffSeconds = config.retry_backoff_seconds;
663
678
  const result = await this.db.updateTable("agent_project_config").set({
664
679
  skills: JSON.stringify(skills),
665
- ...config
680
+ ...config,
681
+ ...maxRetryAttempts == null ? {} : { max_retry_attempts: maxRetryAttempts },
682
+ ...retryBackoffSeconds == null ? {} : { retry_backoff_seconds: retryBackoffSeconds }
666
683
  }).where("agent", "=", this.agentName).where("project_id", "=", config.project_id).executeTakeFirst();
667
684
  if (Number(result.numUpdatedRows ?? 0) === 0) throw new Error(`No config found to update for agent ${this.agentName} and project ${config.project_id}.`);
668
685
  }
@@ -704,13 +721,15 @@ var TaskListDb9Provider = class extends TaskListProvider {
704
721
  }
705
722
  async insertTask(taskItem) {
706
723
  const { started_at, ended_at, queued_at, cancelled_at, ...rest } = taskItem;
724
+ const attemptCount = taskItem.attempt_count ?? 1;
707
725
  const inserted = await this.db.insertInto("task").values({
708
726
  agent: this.agentName,
709
727
  started_at,
710
728
  ended_at,
711
729
  queued_at,
712
730
  cancelled_at,
713
- ...rest
731
+ ...rest,
732
+ attempt_count: attemptCount
714
733
  }).returningAll().executeTakeFirstOrThrow();
715
734
  return taskItemSchema.parse(inserted);
716
735
  }
@@ -765,16 +784,24 @@ var TaskListTidbProvider = class extends TaskListProvider {
765
784
  return await this.db.selectFrom("agent_project_config").innerJoin("task", "task.id", "agent_project_config.config_task_id").selectAll("agent_project_config").select((eb) => eb.ref("task.status").$castTo().as("config_task_status")).where("agent_project_config.agent", "=", this.agentName).execute();
766
785
  }
767
786
  async setAgentConfig({ skills, ...config }) {
787
+ const maxRetryAttempts = config.max_retry_attempts ?? 3;
788
+ const retryBackoffSeconds = config.retry_backoff_seconds ?? 30;
768
789
  await this.db.insertInto("agent_project_config").values({
769
790
  agent: this.agentName,
770
791
  skills: JSON.stringify(skills),
771
- ...config
792
+ ...config,
793
+ max_retry_attempts: maxRetryAttempts,
794
+ retry_backoff_seconds: retryBackoffSeconds
772
795
  }).execute();
773
796
  }
774
797
  async updateAgentConfig({ skills, ...config }) {
798
+ const maxRetryAttempts = config.max_retry_attempts;
799
+ const retryBackoffSeconds = config.retry_backoff_seconds;
775
800
  const result = await this.db.updateTable("agent_project_config").set({
776
801
  skills: JSON.stringify(skills),
777
- ...config
802
+ ...config,
803
+ ...maxRetryAttempts == null ? {} : { max_retry_attempts: maxRetryAttempts },
804
+ ...retryBackoffSeconds == null ? {} : { retry_backoff_seconds: retryBackoffSeconds }
778
805
  }).where("agent", "=", this.agentName).where("project_id", "=", config.project_id).executeTakeFirst();
779
806
  if (Number(result.numUpdatedRows ?? 0) === 0) throw new Error(`No config found to update for agent ${this.agentName} and project ${config.project_id}.`);
780
807
  }
@@ -816,13 +843,15 @@ var TaskListTidbProvider = class extends TaskListProvider {
816
843
  }
817
844
  async insertTask(taskItem) {
818
845
  const { started_at, ended_at, queued_at, cancelled_at, ...rest } = taskItem;
846
+ const attemptCount = taskItem.attempt_count ?? 1;
819
847
  const { insertId } = await this.db.insertInto("task").values({
820
848
  agent: this.agentName,
821
849
  started_at,
822
850
  ended_at,
823
851
  queued_at,
824
852
  cancelled_at,
825
- ...rest
853
+ ...rest,
854
+ attempt_count: attemptCount
826
855
  }).executeTakeFirstOrThrow();
827
856
  return {
828
857
  ...taskItem,
@@ -1171,7 +1200,7 @@ const branchExecutionResultSchema = z.object({
1171
1200
  status: z.string(),
1172
1201
  status_text: z.string(),
1173
1202
  branch_id: z.string(),
1174
- snap_id: z.string(),
1203
+ snap_id: z.string().nullable(),
1175
1204
  background_task_id: z.string().nullable(),
1176
1205
  started_at: zodJsonDate,
1177
1206
  last_polled_at: zodJsonDate,
@@ -1248,7 +1277,7 @@ const explorationResultSchema = z.object({
1248
1277
  status: z.string(),
1249
1278
  status_text: z.string().nullable(),
1250
1279
  branch_id: z.string(),
1251
- snap_id: z.string(),
1280
+ snap_id: z.string().nullable(),
1252
1281
  background_task_id: z.string().nullable(),
1253
1282
  started_at: zodJsonDate,
1254
1283
  last_polled_at: zodJsonDate,
@@ -1635,7 +1664,9 @@ const projectSchema = z.object({
1635
1664
  created_at: zodJsonDate,
1636
1665
  updated_at: zodJsonDate,
1637
1666
  branches_total: z.number().int().nullable().optional(),
1638
- background_tasks_total: z.number().int().nullable().optional()
1667
+ background_tasks_total: z.number().int().nullable().optional(),
1668
+ env_id: z.string().uuid().nullable().optional(),
1669
+ env_display_name: z.string().nullable().optional()
1639
1670
  });
1640
1671
  const projectMessageSchema = z.object({
1641
1672
  id: z.string(),
@@ -1996,6 +2027,7 @@ const executor = createApiExecutor({
1996
2027
  headers: { Authorization: `Bearer ${process.env.PANTHEON_API_KEY}` },
1997
2028
  validateResponse
1998
2029
  });
2030
+ const killProjectBranchExecutionApi = defineApi(post`/api/v1/projects/${"projectId"}/branches/${"branchId"}/kill`, typeOf(), "raw");
1999
2031
  async function getPantheonProjectInfo({ projectId }) {
2000
2032
  return await executor.execute(getProject, { projectId }, null);
2001
2033
  }
@@ -2053,11 +2085,86 @@ async function executeOnPantheon({ projectId, branchId, prompt, agent }) {
2053
2085
  parent_branch_id: branchId
2054
2086
  })).branches[0];
2055
2087
  }
2088
+ function isNotFoundError(error) {
2089
+ if (!(error instanceof Error)) return false;
2090
+ const message = error.message.toLowerCase();
2091
+ return message.includes("404") || message.includes("not found");
2092
+ }
2093
+ async function killPantheonBranchExecution({ projectId, branchId }) {
2094
+ try {
2095
+ await executor.execute(killProjectBranchExecutionApi, {
2096
+ projectId,
2097
+ branchId
2098
+ }, {});
2099
+ return [branchId];
2100
+ } catch (error) {
2101
+ if (isNotFoundError(error)) return [];
2102
+ throw error;
2103
+ }
2104
+ }
2056
2105
 
2057
2106
  //#endregion
2058
2107
  //#region src/core/task-list.ts
2059
2108
  const DEFAULT_CONCURRENCY = 1;
2109
+ const DEFAULT_MAX_RETRY_ATTEMPTS$1 = 3;
2110
+ const DEFAULT_RETRY_BACKOFF_SECONDS$1 = 30;
2060
2111
  const AUTO_RECONFIG_PROMPT = "Config outdated. Reconfigure this branch against the latest agent config.";
2112
+ function getTaskAttemptCount(task) {
2113
+ if (Number.isInteger(task.attempt_count) && task.attempt_count > 0) return task.attempt_count;
2114
+ return 1;
2115
+ }
2116
+ function getMaxRetryAttempts(config, logger) {
2117
+ const configured = config.max_retry_attempts;
2118
+ if (configured == null) return DEFAULT_MAX_RETRY_ATTEMPTS$1;
2119
+ if (Number.isInteger(configured) && configured >= 0) return configured;
2120
+ logger.warn("Invalid max_retry_attempts=%o for project %s; fallback to %d.", configured, config.project_id, DEFAULT_MAX_RETRY_ATTEMPTS$1);
2121
+ return DEFAULT_MAX_RETRY_ATTEMPTS$1;
2122
+ }
2123
+ function getRetryBackoffSeconds(config, logger) {
2124
+ const configured = config.retry_backoff_seconds;
2125
+ if (configured == null) return DEFAULT_RETRY_BACKOFF_SECONDS$1;
2126
+ if (Number.isInteger(configured) && configured > 0) return configured;
2127
+ logger.warn("Invalid retry_backoff_seconds=%o for project %s; fallback to %d.", configured, config.project_id, DEFAULT_RETRY_BACKOFF_SECONDS$1);
2128
+ return DEFAULT_RETRY_BACKOFF_SECONDS$1;
2129
+ }
2130
+ function getBackoffDelaySeconds(backoffBaseSeconds, nextAttemptCount) {
2131
+ return backoffBaseSeconds * 2 ** (Math.max(1, nextAttemptCount - 1) - 1);
2132
+ }
2133
+ async function retryFailedTaskInPlace(provider, task, logger, options = {}) {
2134
+ const config = await provider.getAgentConfig(task.project_id);
2135
+ if (!config) {
2136
+ logger.warn("Skip retry for task %s because project config %s is missing.", task.id, task.project_id);
2137
+ return null;
2138
+ }
2139
+ const currentAttemptCount = getTaskAttemptCount(task);
2140
+ const retriesUsed = Math.max(0, currentAttemptCount - 1);
2141
+ const maxRetryAttempts = getMaxRetryAttempts(config, logger);
2142
+ if (!options.ignoreRetryLimit && retriesUsed >= maxRetryAttempts) {
2143
+ logger.info("Task %s reached max retries (%d). Keep failed status.", task.id, maxRetryAttempts);
2144
+ return null;
2145
+ }
2146
+ const nextAttemptCount = currentAttemptCount + 1;
2147
+ const now = Date.now();
2148
+ const delaySeconds = options.immediate ? 0 : getBackoffDelaySeconds(getRetryBackoffSeconds(config, logger), nextAttemptCount);
2149
+ const queuedAt = new Date(now + delaySeconds * 1e3);
2150
+ const retriedTask = {
2151
+ status: "pending",
2152
+ id: task.id,
2153
+ task: task.task,
2154
+ type: task.type,
2155
+ project_id: task.project_id,
2156
+ base_branch_id: task.base_branch_id,
2157
+ parent_task_id: task.parent_task_id ?? null,
2158
+ config_version: task.config_version ?? null,
2159
+ attempt_count: nextAttemptCount,
2160
+ cancel_reason: task.cancel_reason ?? null,
2161
+ queued_at: queuedAt,
2162
+ error: task.error
2163
+ };
2164
+ await provider.updateTask(retriedTask);
2165
+ logger.info("Requeued failed task %s for attempt %d at %s%s.", task.id, nextAttemptCount, queuedAt.toISOString(), options.reason ? ` (reason: ${options.reason})` : "");
2166
+ return retriedTask;
2167
+ }
2061
2168
  function buildTaskPromptSequence(config, taskPrompt, taskType = "default") {
2062
2169
  if (taskType === "bootstrap" && taskPrompt === "$setup") return [buildConfigSetupStep({
2063
2170
  prototypeRepoUrl: config.prototype_url,
@@ -2144,6 +2251,8 @@ async function pollRunningTaskState(provider, state, logger) {
2144
2251
  } else if (newStatus.state === "failed") {
2145
2252
  logger.info(`Task failed on Branch[id = ${newStatus.branch.id},snap_id=${newStatus.branch.latest_snap_id}] with error: %s`, newStatus.error);
2146
2253
  await provider.failTask(state, newStatus.error);
2254
+ const failedTask = await provider.getTask(state.id);
2255
+ if (failedTask?.status === "failed") await retryFailedTaskInPlace(provider, failedTask, logger.child({ name: `task:${state.id}:auto-retry` }));
2147
2256
  }
2148
2257
  }
2149
2258
  function isTaskOutdated(task, config) {
@@ -2268,6 +2377,7 @@ async function startPendingTasksUpToConcurrency(provider, pendingTasks, runningT
2268
2377
  return config;
2269
2378
  };
2270
2379
  for (const task of pendingTasks) {
2380
+ if (task.queued_at.getTime() > Date.now()) continue;
2271
2381
  const config = await getProjectConfig(task.project_id);
2272
2382
  if (!config) {
2273
2383
  if (!missingConfigProjects.has(task.project_id)) {
@@ -2284,6 +2394,8 @@ async function startPendingTasksUpToConcurrency(provider, pendingTasks, runningT
2284
2394
  runningCountByProject.set(task.project_id, runningCount + 1);
2285
2395
  } catch (e) {
2286
2396
  logger.error(`Failed to start task ${task.id}: ${getErrorMessage(e)}`);
2397
+ const failedTask = await provider.getTask(task.id);
2398
+ if (failedTask?.status === "failed") await retryFailedTaskInPlace(provider, failedTask, logger.child({ name: `task:${task.id}:auto-retry` }));
2287
2399
  }
2288
2400
  }
2289
2401
  }
@@ -2713,10 +2825,13 @@ var WatchStepAggregator = class {
2713
2825
 
2714
2826
  //#endregion
2715
2827
  //#region src/core/index.ts
2828
+ const DEFAULT_MAX_RETRY_ATTEMPTS = 3;
2829
+ const DEFAULT_RETRY_BACKOFF_SECONDS = 30;
2830
+ const DEFAULT_KILL_REASON = "Killed by user";
2716
2831
  async function runAgent(name, options, logger) {
2717
2832
  const agentDir = path.join(options.dataDir, "agents", name);
2718
2833
  const pidFile = path.join(agentDir, "pid");
2719
- await fs.promises.mkdir(agentDir, { recursive: true });
2834
+ await fs$1.promises.mkdir(agentDir, { recursive: true });
2720
2835
  await assertsSingleton(logger, pidFile);
2721
2836
  await startTaskListLoop(name, { loopInterval: options.loopInterval }, logger);
2722
2837
  }
@@ -2749,6 +2864,18 @@ async function configAgent(name, options) {
2749
2864
  process.exitCode = 1;
2750
2865
  return;
2751
2866
  }
2867
+ const maxRetryAttempts = options.maxRetryAttempts ?? previousConfig?.max_retry_attempts ?? DEFAULT_MAX_RETRY_ATTEMPTS;
2868
+ if (!Number.isInteger(maxRetryAttempts) || maxRetryAttempts < 0) {
2869
+ console.error("--max-retry-attempts must be a non-negative integer.");
2870
+ process.exitCode = 1;
2871
+ return;
2872
+ }
2873
+ const retryBackoffSeconds = options.retryBackoffSeconds ?? previousConfig?.retry_backoff_seconds ?? DEFAULT_RETRY_BACKOFF_SECONDS;
2874
+ if (!Number.isInteger(retryBackoffSeconds) || retryBackoffSeconds <= 0) {
2875
+ console.error("--retry-backoff-seconds must be a positive integer.");
2876
+ process.exitCode = 1;
2877
+ return;
2878
+ }
2752
2879
  const resolvedSkills = options.skills ?? normalizeSkills(previousConfig?.skills);
2753
2880
  const resolvedExecuteAgent = options.executeAgent.trim() || previousConfig?.execute_agent || "codex";
2754
2881
  const resolvedPrototypeUrl = options.prototypeUrl.trim() || previousConfig?.prototype_url || "https://github.com/pingcap-inc/pantheon-agents";
@@ -2779,6 +2906,8 @@ async function configAgent(name, options) {
2779
2906
  config_task_id: configTaskId,
2780
2907
  config_version: previousConfig.config_version + (options.prompt ? 2 : 1),
2781
2908
  concurrency,
2909
+ max_retry_attempts: maxRetryAttempts,
2910
+ retry_backoff_seconds: retryBackoffSeconds,
2782
2911
  execute_agent: resolvedExecuteAgent,
2783
2912
  role: resolvedRole ?? previousConfig.role,
2784
2913
  skills: resolvedSkills,
@@ -2846,6 +2975,8 @@ async function configAgent(name, options) {
2846
2975
  config_task_id: configTaskId,
2847
2976
  config_version: configVersion,
2848
2977
  concurrency,
2978
+ max_retry_attempts: maxRetryAttempts,
2979
+ retry_backoff_seconds: retryBackoffSeconds,
2849
2980
  execute_agent: resolvedExecuteAgent,
2850
2981
  role: resolvedRole,
2851
2982
  skills: resolvedSkills,
@@ -2857,6 +2988,8 @@ async function configAgent(name, options) {
2857
2988
  config_task_id: configTaskId,
2858
2989
  config_version: configVersion,
2859
2990
  concurrency,
2991
+ max_retry_attempts: maxRetryAttempts,
2992
+ retry_backoff_seconds: retryBackoffSeconds,
2860
2993
  execute_agent: resolvedExecuteAgent,
2861
2994
  role: resolvedRole ?? previousConfig.role,
2862
2995
  skills: resolvedSkills,
@@ -2936,6 +3069,186 @@ async function cancelTask(agentName, taskId, reason) {
2936
3069
  await provider.close();
2937
3070
  }
2938
3071
  }
3072
+ async function retryTask(agentName, taskId, reason) {
3073
+ const provider = createTaskListProvider(agentName, pino());
3074
+ try {
3075
+ const task = await provider.getTask(taskId);
3076
+ if (!task) return null;
3077
+ if (task.status !== "failed") throw new Error(`Task ${taskId} is ${task.status}; only failed tasks can be retried.`);
3078
+ if (!await retryFailedTaskInPlace(provider, task, pino(), {
3079
+ immediate: true,
3080
+ reason,
3081
+ ignoreRetryLimit: true
3082
+ })) throw new Error(`Task ${taskId} retry was not scheduled.`);
3083
+ return await provider.getTask(taskId);
3084
+ } finally {
3085
+ await provider.close();
3086
+ }
3087
+ }
3088
+ function resolveTaskAttemptCount(value) {
3089
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
3090
+ return 1;
3091
+ }
3092
+ function hasTaskBranchId(task) {
3093
+ return "branch_id" in task && typeof task.branch_id === "string";
3094
+ }
3095
+ function collectKillBranchTargets(tasks, options = {}) {
3096
+ const childrenByParentId = /* @__PURE__ */ new Map();
3097
+ const taskById = /* @__PURE__ */ new Map();
3098
+ for (const task of tasks) {
3099
+ taskById.set(task.id, task);
3100
+ if (!task.parent_task_id) continue;
3101
+ const siblings = childrenByParentId.get(task.parent_task_id);
3102
+ if (siblings) siblings.push(task);
3103
+ else childrenByParentId.set(task.parent_task_id, [task]);
3104
+ }
3105
+ const roots = options.rootTaskIds && options.rootTaskIds.length > 0 ? options.rootTaskIds.map((taskId) => taskById.get(taskId)).filter((task) => task != null) : tasks.filter((task) => task.status === "pending" || task.status === "running");
3106
+ if (roots.length === 0) return [];
3107
+ const maxDepthByTaskId = /* @__PURE__ */ new Map();
3108
+ const stack = roots.map((task) => ({
3109
+ taskId: task.id,
3110
+ depth: 0
3111
+ }));
3112
+ while (stack.length > 0) {
3113
+ const item = stack.pop();
3114
+ if (!item) continue;
3115
+ const previousDepth = maxDepthByTaskId.get(item.taskId);
3116
+ if (previousDepth != null && previousDepth >= item.depth) continue;
3117
+ maxDepthByTaskId.set(item.taskId, item.depth);
3118
+ const children = childrenByParentId.get(item.taskId) ?? [];
3119
+ for (const child of children) stack.push({
3120
+ taskId: child.id,
3121
+ depth: item.depth + 1
3122
+ });
3123
+ }
3124
+ const scoredTargets = [];
3125
+ for (const [taskId, depth] of maxDepthByTaskId) {
3126
+ const task = taskById.get(taskId);
3127
+ if (!task || !hasTaskBranchId(task)) continue;
3128
+ scoredTargets.push({
3129
+ task_id: task.id,
3130
+ project_id: task.project_id,
3131
+ branch_id: task.branch_id,
3132
+ depth,
3133
+ queued_at_ms: task.queued_at.getTime()
3134
+ });
3135
+ }
3136
+ scoredTargets.sort((a, b) => {
3137
+ if (a.depth !== b.depth) return b.depth - a.depth;
3138
+ return b.queued_at_ms - a.queued_at_ms;
3139
+ });
3140
+ const seen = /* @__PURE__ */ new Set();
3141
+ const targets = [];
3142
+ for (const target of scoredTargets) {
3143
+ const key = `${target.project_id}:${target.branch_id}`;
3144
+ if (seen.has(key)) continue;
3145
+ seen.add(key);
3146
+ targets.push({
3147
+ task_id: target.task_id,
3148
+ project_id: target.project_id,
3149
+ branch_id: target.branch_id
3150
+ });
3151
+ }
3152
+ return targets;
3153
+ }
3154
+ function collectTaskSubtree(tasks, rootTaskId) {
3155
+ const childrenByParentId = /* @__PURE__ */ new Map();
3156
+ const taskById = /* @__PURE__ */ new Map();
3157
+ for (const task of tasks) {
3158
+ taskById.set(task.id, task);
3159
+ if (!task.parent_task_id) continue;
3160
+ const siblings = childrenByParentId.get(task.parent_task_id);
3161
+ if (siblings) siblings.push(task);
3162
+ else childrenByParentId.set(task.parent_task_id, [task]);
3163
+ }
3164
+ if (!taskById.has(rootTaskId)) return [];
3165
+ const subtreeIds = /* @__PURE__ */ new Set();
3166
+ const stack = [rootTaskId];
3167
+ while (stack.length > 0) {
3168
+ const taskId = stack.pop();
3169
+ if (!taskId || subtreeIds.has(taskId)) continue;
3170
+ subtreeIds.add(taskId);
3171
+ const children = childrenByParentId.get(taskId) ?? [];
3172
+ for (const child of children) stack.push(child.id);
3173
+ }
3174
+ return tasks.filter((task) => subtreeIds.has(task.id));
3175
+ }
3176
+ function toCancelledTask(task, reason, cancelledAt) {
3177
+ return {
3178
+ status: "cancelled",
3179
+ id: task.id,
3180
+ task: task.task,
3181
+ type: task.type,
3182
+ project_id: task.project_id,
3183
+ base_branch_id: task.base_branch_id,
3184
+ parent_task_id: task.parent_task_id ?? null,
3185
+ config_version: task.config_version ?? null,
3186
+ attempt_count: resolveTaskAttemptCount(task.attempt_count),
3187
+ cancel_reason: reason,
3188
+ queued_at: task.queued_at,
3189
+ cancelled_at: cancelledAt
3190
+ };
3191
+ }
3192
+ async function killAgentTasks(agentName, taskId, reason = DEFAULT_KILL_REASON, deps = {}) {
3193
+ const provider = createTaskListProvider(agentName, pino());
3194
+ try {
3195
+ const tasks = await provider.getTasks({
3196
+ order_by: "queued_at",
3197
+ order_direction: "asc"
3198
+ });
3199
+ const subtreeTasks = collectTaskSubtree(tasks, taskId);
3200
+ if (subtreeTasks.length === 0) return null;
3201
+ const pendingTasks = subtreeTasks.filter((task) => task.status === "pending");
3202
+ const runningTasks = subtreeTasks.filter((task) => task.status === "running");
3203
+ for (const pendingTask of pendingTasks) await provider.cancelTask(pendingTask, reason);
3204
+ const killTargets = collectKillBranchTargets(tasks, { rootTaskIds: [taskId] });
3205
+ const killBranch = deps.killBranch ?? killPantheonBranchExecution;
3206
+ const killedBranches = [];
3207
+ const seenKilledBranchKeys = /* @__PURE__ */ new Set();
3208
+ const failedBranchKills = [];
3209
+ for (const target of killTargets) try {
3210
+ const deletedBranchIds = await killBranch({
3211
+ projectId: target.project_id,
3212
+ branchId: target.branch_id
3213
+ });
3214
+ for (const deletedBranchId of deletedBranchIds) {
3215
+ const branchKey = `${target.project_id}:${deletedBranchId}`;
3216
+ if (seenKilledBranchKeys.has(branchKey)) continue;
3217
+ seenKilledBranchKeys.add(branchKey);
3218
+ killedBranches.push({
3219
+ project_id: target.project_id,
3220
+ branch_id: deletedBranchId
3221
+ });
3222
+ }
3223
+ } catch (error) {
3224
+ const message = error instanceof Error ? error.message : String(error);
3225
+ failedBranchKills.push({
3226
+ task_id: target.task_id,
3227
+ project_id: target.project_id,
3228
+ branch_id: target.branch_id,
3229
+ error: message
3230
+ });
3231
+ }
3232
+ const now = /* @__PURE__ */ new Date();
3233
+ const killedBranchKeys = new Set(killedBranches.map((item) => `${item.project_id}:${item.branch_id}`));
3234
+ let runningCancelledCount = 0;
3235
+ for (const runningTask of runningTasks) {
3236
+ const branchKey = `${runningTask.project_id}:${runningTask.branch_id}`;
3237
+ if (!killedBranchKeys.has(branchKey)) continue;
3238
+ await provider.updateTask(toCancelledTask(runningTask, reason, now));
3239
+ runningCancelledCount++;
3240
+ }
3241
+ return {
3242
+ task_id: taskId,
3243
+ pending_cancelled_count: pendingTasks.length,
3244
+ running_cancelled_count: runningCancelledCount,
3245
+ killed_branches: killedBranches,
3246
+ failed_branch_kills: failedBranchKills
3247
+ };
3248
+ } finally {
3249
+ await provider.close();
3250
+ }
3251
+ }
2939
3252
  async function showAgentConfig(agentName, projectId) {
2940
3253
  const provider = createTaskListProvider(agentName, pino());
2941
3254
  try {
@@ -3038,24 +3351,26 @@ function formatConciseTaskLine(options) {
3038
3351
  const statusText = formatStatus(task.status, useColor);
3039
3352
  const timeText = formatRelativeTime(timestamp);
3040
3353
  const taskText = truncateText(task.task, maxTaskLength);
3354
+ const attempt = Number.isInteger(task.attempt_count) && task.attempt_count > 0 ? task.attempt_count : 1;
3041
3355
  return [
3042
3356
  agent,
3043
3357
  statusText,
3044
3358
  task.id,
3359
+ `attempt:${attempt}`,
3045
3360
  timeText,
3046
3361
  taskText
3047
3362
  ].filter((part) => part !== "").join(" ");
3048
3363
  }
3049
3364
  async function assertsSingleton(logger, pidFile) {
3050
3365
  try {
3051
- const pid = await fs.promises.readFile(pidFile, "utf-8");
3366
+ const pid = await fs$1.promises.readFile(pidFile, "utf-8");
3052
3367
  process.kill(parseInt(pid), 0);
3053
3368
  console.error("Failed to assert singleton agent process:");
3054
3369
  process.exit(1);
3055
3370
  } catch (e) {
3056
- await fs.promises.writeFile(pidFile, process.pid.toString());
3371
+ await fs$1.promises.writeFile(pidFile, process.pid.toString());
3057
3372
  process.on("exit", () => {
3058
- fs.promises.rm(pidFile);
3373
+ fs$1.promises.rm(pidFile);
3059
3374
  });
3060
3375
  }
3061
3376
  }
@@ -3224,8 +3539,18 @@ function parseConcurrency(value) {
3224
3539
  if (!Number.isInteger(parsed) || parsed < 1) throw new InvalidArgumentError("concurrency must be a positive integer.");
3225
3540
  return parsed;
3226
3541
  }
3542
+ function parseNonNegativeInteger(value, fieldName) {
3543
+ const parsed = Number(value);
3544
+ if (!Number.isInteger(parsed) || parsed < 0) throw new InvalidArgumentError(`${fieldName} must be a non-negative integer.`);
3545
+ return parsed;
3546
+ }
3547
+ function parsePositiveInteger(value, fieldName) {
3548
+ const parsed = Number(value);
3549
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new InvalidArgumentError(`${fieldName} must be a positive integer.`);
3550
+ return parsed;
3551
+ }
3227
3552
  function createConfigAgentCommand(version, deps = {}) {
3228
- return createCommand("config").version(version).description("Queue a configuration task for an agent/project").argument("<name>", "The name of the agent.").argument("[prompt]", "The configuration task prompt.").option("--project-id <project-id>", "The project id of the agent. Defaults to DEFAULT_PANTHEON_PROJECT_ID.").option("--task-id <task-id>", "Optional parent task id.").option("--role <role>", "Role metadata. Required for first-time project config; optional override later.").option("--skills <skills>", "The skills of the agent. Multiple values are separated by comma.", parseUniqueCommaList).option("--execute-agent <agent>", "The execute agent of the agent.", "codex").option("--concurrency <number>", "Max number of parallel running tasks for this project.", parseConcurrency).option("--root-branch-id <branchId>", "The root branch id of the agent. Defaults to DEFAULT_PANTHEON_ROOT_BRANCH_ID, then project root branch id.").option("--prototype-url <url>", "Role and skill definitions repo.", "https://github.com/pingcap-inc/pantheon-agents").action(async function() {
3553
+ return createCommand("config").version(version).description("Queue a configuration task for an agent/project").argument("<name>", "The name of the agent.").argument("[prompt]", "The configuration task prompt.").option("--project-id <project-id>", "The project id of the agent. Defaults to DEFAULT_PANTHEON_PROJECT_ID.").option("--task-id <task-id>", "Optional parent task id.").option("--role <role>", "Role metadata. Required for first-time project config; optional override later.").option("--skills <skills>", "The skills of the agent. Multiple values are separated by comma.", parseUniqueCommaList).option("--execute-agent <agent>", "The execute agent of the agent.", "codex").option("--concurrency <number>", "Max number of parallel running tasks for this project.", parseConcurrency).option("--max-retry-attempts <number>", "Max automatic retry attempts per task after failures.", (value) => parseNonNegativeInteger(value, "max-retry-attempts")).option("--retry-backoff-seconds <seconds>", "Base delay (seconds) for exponential retry backoff.", (value) => parsePositiveInteger(value, "retry-backoff-seconds")).option("--root-branch-id <branchId>", "The root branch id of the agent. Defaults to DEFAULT_PANTHEON_ROOT_BRANCH_ID, then project root branch id.").option("--prototype-url <url>", "Role and skill definitions repo.", "https://github.com/pingcap-inc/pantheon-agents").action(async function() {
3229
3554
  const [name, prompt] = this.args;
3230
3555
  const options = this.opts();
3231
3556
  const resolvedProjectId = resolvePantheonProjectId(options.projectId);
@@ -3243,6 +3568,8 @@ function createConfigAgentCommand(version, deps = {}) {
3243
3568
  role: options.role,
3244
3569
  executeAgent: options.executeAgent,
3245
3570
  concurrency: options.concurrency,
3571
+ maxRetryAttempts: options.maxRetryAttempts,
3572
+ retryBackoffSeconds: options.retryBackoffSeconds,
3246
3573
  skills: options.skills,
3247
3574
  prototypeUrl: options.prototypeUrl,
3248
3575
  rootBranchId: resolvedRootBranchId
@@ -3297,9 +3624,11 @@ function formatTaskAsSkillTask(agent, task) {
3297
3624
  const endedAt = "ended_at" in task ? task.ended_at ?? null : null;
3298
3625
  const canceledAt = task.status === "cancelled" ? task.cancelled_at : null;
3299
3626
  const finishedAt = endedAt ?? canceledAt ?? null;
3627
+ const attempt = Number.isInteger(task.attempt_count) && task.attempt_count > 0 ? task.attempt_count : 1;
3300
3628
  return {
3301
3629
  agent,
3302
3630
  task: task.task,
3631
+ attempt,
3303
3632
  parent_task_id: task.parent_task_id ?? null,
3304
3633
  status: toSkillTaskStatus(task.status),
3305
3634
  queued_at: task.queued_at.toISOString(),
@@ -3309,7 +3638,7 @@ function formatTaskAsSkillTask(agent, task) {
3309
3638
  canceled_at: toIsoOrNull(canceledAt),
3310
3639
  cancel_reason: task.cancel_reason ?? null,
3311
3640
  output: task.status === "completed" ? task.output : null,
3312
- error: task.status === "failed" ? task.error : null
3641
+ error: task.error ?? null
3313
3642
  };
3314
3643
  }
3315
3644
  function createGetTaskCommand(version, deps = {}) {
@@ -3333,6 +3662,56 @@ function createGetTaskCommand(version, deps = {}) {
3333
3662
  });
3334
3663
  }
3335
3664
 
3665
+ //#endregion
3666
+ //#region src/cli/commands/kill.ts
3667
+ function createKillCommand(version, deps = {}) {
3668
+ return createCommand("kill").version(version).description("Kill a task and its descendants recursively for an agent").argument("<name>", "The name of the agent.").argument("<task-id>", "Root task id to kill.").argument("[reason]", "Optional kill reason.").option("-y, --yes", "Skip confirmation prompt.").option("-f, --force", "Alias for --yes.").action(async function() {
3669
+ const [name, taskId, reason] = this.args;
3670
+ const options = this.opts();
3671
+ if (!ensureEnv(["DATABASE_URL"])) return;
3672
+ const rl = options.yes || options.force ? null : readline.createInterface({
3673
+ input: process$1.stdin,
3674
+ output: process$1.stdout
3675
+ });
3676
+ try {
3677
+ if (rl) {
3678
+ if ((await rl.question(`Type the agent name (${name}) to confirm kill: `)).trim() !== name) {
3679
+ console.error("Confirmation failed. Agent name did not match.");
3680
+ process$1.exitCode = 1;
3681
+ return;
3682
+ }
3683
+ if ((await rl.question(`Type KILL ${taskId} to stop this task subtree and related branches: `)).trim() !== `KILL ${taskId}`) {
3684
+ console.error("Confirmation failed. Aborting kill.");
3685
+ process$1.exitCode = 1;
3686
+ return;
3687
+ }
3688
+ }
3689
+ const result = await (deps.killAgentTasks ?? killAgentTasks)(name, taskId, reason);
3690
+ if (!result) {
3691
+ console.error(`Task ${taskId} not found for agent ${name}.`);
3692
+ process$1.exitCode = 1;
3693
+ return;
3694
+ }
3695
+ const attemptedBranchKills = result.killed_branches.length + result.failed_branch_kills.length;
3696
+ if (result.pending_cancelled_count === 0 && attemptedBranchKills === 0) {
3697
+ console.log(`No actionable work found in task subtree ${taskId}.`);
3698
+ return;
3699
+ }
3700
+ console.log(`Kill result for agent ${name} task ${taskId}: cancelled ${result.pending_cancelled_count} pending task(s), cancelled ${result.running_cancelled_count} running task(s).`);
3701
+ if (result.killed_branches.length > 0) console.log(`Killed ${result.killed_branches.length} branch(es).`);
3702
+ if (result.failed_branch_kills.length > 0) {
3703
+ for (const failed of result.failed_branch_kills) console.error(`Failed to kill branch ${failed.branch_id} in project ${failed.project_id}: ${failed.error}`);
3704
+ process$1.exitCode = 1;
3705
+ }
3706
+ } catch (error) {
3707
+ console.error(error instanceof Error ? error.message : String(error));
3708
+ process$1.exitCode = 1;
3709
+ } finally {
3710
+ rl?.close();
3711
+ }
3712
+ });
3713
+ }
3714
+
3336
3715
  //#endregion
3337
3716
  //#region src/cli/commands/run.ts
3338
3717
  function createRunAgentCommand(version) {
@@ -3390,6 +3769,8 @@ function createShowConfigCommand(version) {
3390
3769
  config_version: config.config_version,
3391
3770
  config_task_id: config.config_task_id,
3392
3771
  concurrency: config.concurrency,
3772
+ max_retry_attempts: config.max_retry_attempts ?? 3,
3773
+ retry_backoff_seconds: config.retry_backoff_seconds ?? 30,
3393
3774
  role: config.role,
3394
3775
  execute_agent: config.execute_agent,
3395
3776
  prototype_url: config.prototype_url,
@@ -3414,6 +3795,8 @@ function createShowConfigCommand(version) {
3414
3795
  config_version: config.config_version,
3415
3796
  config_task_id: config.config_task_id,
3416
3797
  concurrency: config.concurrency,
3798
+ max_retry_attempts: config.max_retry_attempts ?? 3,
3799
+ retry_backoff_seconds: config.retry_backoff_seconds ?? 30,
3417
3800
  role: config.role,
3418
3801
  execute_agent: config.execute_agent,
3419
3802
  prototype_url: config.prototype_url,
@@ -3537,6 +3920,135 @@ function createShowTasksCommand(version, deps = {}) {
3537
3920
  });
3538
3921
  }
3539
3922
 
3923
+ //#endregion
3924
+ //#region src/cli/commands/retry-task.ts
3925
+ function resolveTaskAttempt(value) {
3926
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
3927
+ return 1;
3928
+ }
3929
+ function createRetryTaskCommand(version, deps = {}) {
3930
+ return createCommand("retry-task").version(version).description("Retry a failed task for an agent").argument("<name>", "The name of the agent.").argument("<task-id>", "The id of the failed task.").argument("[reason]", "Optional retry reason.").option("-y, --yes", "Skip confirmation prompt.").option("-f, --force", "Alias for --yes.").action(async function() {
3931
+ const [name, taskId, reason] = this.args;
3932
+ const options = this.opts();
3933
+ if (!ensureEnv(["DATABASE_URL"])) return;
3934
+ const rl = options.yes || options.force ? null : readline.createInterface({
3935
+ input: process$1.stdin,
3936
+ output: process$1.stdout
3937
+ });
3938
+ try {
3939
+ if (rl) {
3940
+ if ((await rl.question(`Type the task id (${taskId}) to confirm retry: `)).trim() !== taskId) {
3941
+ console.error("Confirmation failed. Task id did not match.");
3942
+ process$1.exitCode = 1;
3943
+ return;
3944
+ }
3945
+ if ((await rl.question("Type RETRY to requeue this failed task: ")).trim() !== "RETRY") {
3946
+ console.error("Confirmation failed. Aborting retry.");
3947
+ process$1.exitCode = 1;
3948
+ return;
3949
+ }
3950
+ }
3951
+ const updatedTask = await (deps.retryTask ?? retryTask)(name, taskId, reason);
3952
+ if (!updatedTask) {
3953
+ console.error(`Task ${taskId} not found for agent ${name}.`);
3954
+ process$1.exitCode = 1;
3955
+ return;
3956
+ }
3957
+ if (updatedTask.status !== "pending") {
3958
+ console.error(`Unexpected result: task ${taskId} status is ${updatedTask.status} after retry.`);
3959
+ process$1.exitCode = 1;
3960
+ return;
3961
+ }
3962
+ const attempt = resolveTaskAttempt(updatedTask.attempt_count);
3963
+ console.log(`Requeued task ${taskId} for agent ${name} as attempt ${attempt}, next run at ${updatedTask.queued_at.toISOString()}.${reason ? ` Reason: ${reason}` : ""}`);
3964
+ } catch (error) {
3965
+ console.error(error instanceof Error ? error.message : String(error));
3966
+ process$1.exitCode = 1;
3967
+ } finally {
3968
+ rl?.close();
3969
+ }
3970
+ });
3971
+ }
3972
+
3973
+ //#endregion
3974
+ //#region src/cli/commands/skill-sh.ts
3975
+ const EMBEDDED_SKILL_MARKDOWN = "---\nname: pantheon-agents\ndescription: \"Pantheon Agents CLI Usage\"\n---\n\n> Pantheon is the project name of an internal VM environment management project. Do not guess any usage from the name.\n\n\n## Pantheon Agents CLI Usage\n\n### Concepts\n\n- agent: Each agent has a role (like `developer`) and several skills to use.\n- environment: The context of tools, codes or anything else for agents to run tasks. You don't need to know the details.\n- tasks: tasks for each agent to run, each task will create a new environment.\n\n#### Task structure\n\n- agent: The agent to executing the task\n- task (text): The task content\n- attempt: Attempt number (starts from 1 and increases on retries)\n- parent_task_id: The parent task id\n- status: `pending`, `running`, `completed`, `failed`, `canceled`\n- queued_at\n- started_at\n- ended_at\n- finished_at\n- canceled_at\n- cancel_reason\n- output: The output of the completed task.\n- error: The last known error message (failed task error is kept across retries).\n\n#### Other internal implementation details you don't need to know or use\n\n- project\n- branches\n\n### CLI Commands\n\n**You can only use the CLI commands listed below.**\n\n#### Setup a base environment for an agent:\n\n```shell\n# Use default setup script at first time configuration\npantheon-agents config 'Kris' \\\n --role 'developer' \\\n --skills pantheon-issue-resolve,pantheon-solution-design \\\n --max-retry-attempts 3 \\\n --retry-backoff-seconds 30\n\n# Add some custom prompts\npantheon-agents config 'Kris' \\\n --role 'developer' \\\n --skills pantheon-issue-resolve,pantheon-solution-design \\\n 'Update tools to the latest version.'\n```\n\n#### Update agent's environment\n\n##### Configure agent environment by prompt\n\n```shell\npantheon-agents config 'Kris' 'Install some tools'\n```\n\n##### Configure agent to a task result environment\n\npantheon-agents config 'Kris' --task-id 42\n\n#### Enqueue tasks\n\n```shell\npantheon-agents add-task 'Kris' 'Some awesome task'\n ```\n\nWhen adding tasks, cli will output the task id. You can add tasks with `--parent-task-id` to create a task hierarchy.\n\n```shell\npantheon-agents add-task 'Kris' 'Some awesome task' --parent-task-id 42\n```\n\n\n#### List tasks\n\n```shell\npantheon-agents show-tasks --json 'Kris'\n```\n\n\n#### Show single task info\n\n```shell\npantheon-agents get-task 'Kris' 42\n```\n\nReturns task info using this task structure (without internal project/branch fields):\n\n```json\n{\n \"agent\": \"Kris\",\n \"task\": \"Some awesome task\",\n \"attempt\": 1,\n \"parent_task_id\": \"41\",\n \"status\": \"completed\",\n \"queued_at\": \"2026-02-12T00:00:00.000Z\",\n \"started_at\": \"2026-02-12T00:00:05.000Z\",\n \"ended_at\": \"2026-02-12T00:05:00.000Z\",\n \"finished_at\": \"2026-02-12T00:05:00.000Z\",\n \"canceled_at\": null,\n \"cancel_reason\": null,\n \"output\": \"Task output\",\n \"error\": null\n}\n```\n\n#### Cancel task\n\n```shell\npantheon-agents cancel-task 'Kris' 42 'Reason'\n```\n\n#### Retry failed task\n\n```shell\n# Manual retry (immediate requeue and increases attempt)\npantheon-agents retry-task 'Kris' 42 'Reason'\n```\n\n#### Print embedded skill markdown\n\n```shell\npantheon-agents skill.sh\n```\n";
3976
+ function createSkillShCommand(version) {
3977
+ return createCommand("skill.sh").version(version).description("Print embedded pantheon-agents skill markdown").action(() => {
3978
+ process$1.stdout.write(EMBEDDED_SKILL_MARKDOWN);
3979
+ });
3980
+ }
3981
+
3982
+ //#endregion
3983
+ //#region src/cli/commands/gen-migration-sql.ts
3984
+ const introducedInPattern = /^--\s*introduced_in:\s*([0-9]+\.[0-9]+\.[0-9]+)\s*$/m;
3985
+ function parseProvider(value) {
3986
+ if (value === "tidb" || value === "db9") return value;
3987
+ throw new InvalidArgumentError("provider must be one of: tidb, db9");
3988
+ }
3989
+ function parseSemver(value) {
3990
+ const matched = value.trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
3991
+ if (!matched) return null;
3992
+ return [
3993
+ Number(matched[1]),
3994
+ Number(matched[2]),
3995
+ Number(matched[3])
3996
+ ];
3997
+ }
3998
+ function parseSemverOption(value) {
3999
+ if (parseSemver(value) == null) throw new InvalidArgumentError("from must be a semver like 0.3.0");
4000
+ return value;
4001
+ }
4002
+ function compareSemver(a, b) {
4003
+ const pa = parseSemver(a);
4004
+ const pb = parseSemver(b);
4005
+ if (!pa || !pb) throw new Error(`Invalid semver comparison: '${a}' vs '${b}'`);
4006
+ for (let i = 0; i < 3; i++) {
4007
+ if (pa[i] > pb[i]) return 1;
4008
+ if (pa[i] < pb[i]) return -1;
4009
+ }
4010
+ return 0;
4011
+ }
4012
+ function resolveMigrationDir(provider, cwd) {
4013
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
4014
+ const candidates = [
4015
+ path.resolve(cwd, "src/db/migrations", provider),
4016
+ path.resolve(cwd, "packages/agents/src/db/migrations", provider),
4017
+ path.resolve(moduleDir, "db/migrations", provider),
4018
+ path.resolve(moduleDir, "../db/migrations", provider),
4019
+ path.resolve(moduleDir, "../../db/migrations", provider)
4020
+ ];
4021
+ for (const candidate of candidates) if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate;
4022
+ throw new Error(`Migration directory not found for provider '${provider}'. Checked: ${candidates.join(", ")}`);
4023
+ }
4024
+ function loadProviderMigrations(provider, options = {}) {
4025
+ const migrationDir = resolveMigrationDir(provider, options.cwd ?? process$1.cwd());
4026
+ return fs.readdirSync(migrationDir).filter((name) => name.endsWith(".sql")).sort((a, b) => a.localeCompare(b)).map((fileName) => {
4027
+ const filePath = path.join(migrationDir, fileName);
4028
+ const sql = fs.readFileSync(filePath, "utf8");
4029
+ const introduced = sql.match(introducedInPattern)?.[1];
4030
+ if (!introduced) throw new Error(`Migration file '${fileName}' missing required header '-- introduced_in: x.y.z'.`);
4031
+ return {
4032
+ fileName,
4033
+ introducedIn: introduced,
4034
+ sql
4035
+ };
4036
+ });
4037
+ }
4038
+ function getMigrationSql(provider, fromVersion, options = {}) {
4039
+ if (!parseSemver(fromVersion)) throw new Error(`Invalid from version '${fromVersion}'. Expected x.y.z`);
4040
+ const migrations = loadProviderMigrations(provider, options).filter((migration) => compareSemver(migration.introducedIn, fromVersion) > 0);
4041
+ if (migrations.length === 0) return `-- No migrations for provider '${provider}' since ${fromVersion}.\n`;
4042
+ return migrations.map((migration) => `-- migration: ${migration.fileName}\n${migration.sql.trimEnd()}\n`).join("\n");
4043
+ }
4044
+ function createGenMigrationSqlCommand(version, deps = {}) {
4045
+ return createCommand("gen-migration-sql").version(version).description("Print SQL migrations since a given package version").requiredOption("--provider <provider>", "Database provider: tidb or db9.", parseProvider).requiredOption("--from <version>", "Print migrations introduced after this version.", parseSemverOption).action(async function() {
4046
+ const options = this.opts();
4047
+ const output = (deps.getMigrationSql ?? getMigrationSql)(options.provider, options.from);
4048
+ process$1.stdout.write(output);
4049
+ });
4050
+ }
4051
+
3540
4052
  //#endregion
3541
4053
  //#region ../agent-stream-parser/src/utils.ts
3542
4054
  function isRecord(value) {
@@ -5962,7 +6474,7 @@ function createWatchStreamCommand(version) {
5962
6474
 
5963
6475
  //#endregion
5964
6476
  //#region src/cli/index.ts
5965
- const program = new Command().name("pantheon-agents").description("Pantheon agents CLI").version(version).showHelpAfterError().showSuggestionAfterError().addHelpCommand().addCommand(createAddTaskCommand(version)).addCommand(createConfigAgentCommand(version)).addCommand(createDeleteTaskCommand(version)).addCommand(createCancelTaskCommand(version)).addCommand(createGetTaskCommand(version)).addCommand(createRunAgentCommand(version)).addCommand(createShowConfigCommand(version)).addCommand(createShowTasksCommand(version)).addCommand(createWatchCommand(version)).addCommand(createWatchStreamCommand(version));
6477
+ const program = new Command().name("pantheon-agents").description("Pantheon agents CLI").version(version).showHelpAfterError().showSuggestionAfterError().addHelpCommand().addCommand(createAddTaskCommand(version)).addCommand(createConfigAgentCommand(version)).addCommand(createDeleteTaskCommand(version)).addCommand(createCancelTaskCommand(version)).addCommand(createGetTaskCommand(version)).addCommand(createKillCommand(version)).addCommand(createRunAgentCommand(version)).addCommand(createRetryTaskCommand(version)).addCommand(createSkillShCommand(version)).addCommand(createGenMigrationSqlCommand(version)).addCommand(createShowConfigCommand(version)).addCommand(createShowTasksCommand(version)).addCommand(createWatchCommand(version)).addCommand(createWatchStreamCommand(version));
5966
6478
  async function main() {
5967
6479
  if (process$1.argv.length <= 2) {
5968
6480
  program.outputHelp();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pantheon.ai/agents",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "bin": {
6
6
  "pantheon-agents": "dist/index.js"
7
7
  },
@@ -12,7 +12,7 @@
12
12
  "access": "public"
13
13
  },
14
14
  "scripts": {
15
- "build": "rolldown -c rolldown.config.ts && chmod +x dist/index.js",
15
+ "build": "rolldown -c rolldown.config.ts && mkdir -p dist/db && cp -R src/db/migrations dist/db/ && chmod +x dist/index.js",
16
16
  "typecheck": "tsc --noEmit --skipLibCheck --allowImportingTsExtensions --lib ESNext,DOM,DOM.Iterable",
17
17
  "test": "bun test",
18
18
  "test:coverage": "bun test --coverage",