@pantheon.ai/agents 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
  Create/update `.env`:
14
14
 
15
15
  ```bash
16
+ # Use mysql://... for TiDB, or postgresql://... for db9.
16
17
  DATABASE_URL=mysql://root@127.0.0.1:4000/pantheon_agents
17
18
  PANTHEON_API_KEY=<your_pantheon_api_key>
18
19
  DEFAULT_PANTHEON_PROJECT_ID=<optional_default_project_id>
@@ -48,6 +49,28 @@ To avoid running it twice, apply the rest of the file to `pantheon_agents`:
48
49
  sed '1d' src/db/schema/tidb.sql | mysql --host 127.0.0.1 --port 4000 -u root pantheon_agents
49
50
  ```
50
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
+
64
+ Apply manually:
65
+
66
+ ```bash
67
+ # TiDB/MySQL
68
+ mysql --host 127.0.0.1 --port 4000 -u root pantheon_agents < src/db/migrations/tidb/20260304_0001_add_task_retry_columns.sql
69
+
70
+ # db9/PostgreSQL
71
+ psql "$DATABASE_URL" -f src/db/migrations/db9/20260304_0001_add_task_retry_columns.sql
72
+ ```
73
+
51
74
  ## Developer Local Setup
52
75
 
53
76
  This section is only for developing this package locally.
@@ -135,6 +158,9 @@ pantheon-agents show-config <agent-name>
135
158
  pantheon-agents show-tasks --all
136
159
  pantheon-agents get-task <agent-name> <task-id>
137
160
  pantheon-agents cancel-task <agent-name> <task-id> [reason] --yes
161
+ pantheon-agents retry-task <agent-name> <task-id> [reason] --yes
162
+ pantheon-agents skill.sh
163
+ pantheon-agents gen-migration-sql --provider tidb --from 0.3.0
138
164
  pantheon-agents delete-task <agent-name> <task-id>
139
165
  ```
140
166
 
@@ -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,14 +2,17 @@
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
- import { Kysely, MysqlDialect } from "kysely";
9
- import { createPool } from "mysql2";
9
+ import { Kysely, MysqlDialect, PostgresDialect } from "kysely";
10
+ import { Pool } from "pg";
10
11
  import z$1, { z } from "zod";
12
+ import { createPool } from "mysql2";
11
13
  import readline from "node:readline/promises";
12
14
  import expandTilde from "expand-tilde";
15
+ import { fileURLToPath } from "node:url";
13
16
  import blessed from "reblessed";
14
17
  import { inspect } from "node:util";
15
18
  import { parse } from "shell-quote";
@@ -84,7 +87,7 @@ var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
84
87
  //#endregion
85
88
  //#region ../../node_modules/dotenv/lib/main.js
86
89
  var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
87
- const fs$1 = __require("fs");
90
+ const fs$2 = __require("fs");
88
91
  const path$1 = __require("path");
89
92
  const os = __require("os");
90
93
  const crypto = __require("crypto");
@@ -221,10 +224,10 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
221
224
  function _vaultPath(options) {
222
225
  let possibleVaultPath = null;
223
226
  if (options && options.path && options.path.length > 0) if (Array.isArray(options.path)) {
224
- 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`;
225
228
  } else possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
226
229
  else possibleVaultPath = path$1.resolve(process.cwd(), ".env.vault");
227
- if (fs$1.existsSync(possibleVaultPath)) return possibleVaultPath;
230
+ if (fs$2.existsSync(possibleVaultPath)) return possibleVaultPath;
228
231
  return null;
229
232
  }
230
233
  function _resolveHome(envPath) {
@@ -258,7 +261,7 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
258
261
  let lastError;
259
262
  const parsedAll = {};
260
263
  for (const path of optionPaths) try {
261
- const parsed = DotenvModule.parse(fs$1.readFileSync(path, { encoding }));
264
+ const parsed = DotenvModule.parse(fs$2.readFileSync(path, { encoding }));
262
265
  DotenvModule.populate(parsedAll, parsed, options);
263
266
  } catch (e) {
264
267
  if (debug) _debug(`Failed to load ${path} ${e.message}`);
@@ -396,21 +399,19 @@ var require_cli_options = /* @__PURE__ */ __commonJSMin(((exports, module) => {
396
399
 
397
400
  //#endregion
398
401
  //#region package.json
399
- var version = "0.2.2";
402
+ var version = "0.3.0";
400
403
 
401
404
  //#endregion
402
- //#region src/db/tidb.ts
403
- function createTidbPoolOptions(databaseUrl) {
405
+ //#region src/db/db9.ts
406
+ function createDb9PoolOptions(databaseUrl) {
404
407
  return {
405
- uri: databaseUrl,
406
- supportBigNumbers: true,
407
- bigNumberStrings: true,
408
- timezone: "Z"
408
+ connectionString: databaseUrl,
409
+ ssl: process.env.DB9_INSECURE === "true" ? { rejectUnauthorized: false } : true
409
410
  };
410
411
  }
411
- function createTidbDb(databaseUrl = process.env.DATABASE_URL) {
412
+ function createDb9Db(databaseUrl = process.env.DATABASE_URL) {
412
413
  if (!databaseUrl) throw new Error("DATABASE_URL environment variable is not set.");
413
- return new Kysely({ dialect: new MysqlDialect({ pool: createPool(createTidbPoolOptions(databaseUrl)) }) });
414
+ return new Kysely({ dialect: new PostgresDialect({ pool: new Pool(createDb9PoolOptions(databaseUrl)) }) });
414
415
  }
415
416
 
416
417
  //#endregion
@@ -428,8 +429,10 @@ const taskCommonSchema = z.object({
428
429
  base_branch_id: z.uuid(),
429
430
  parent_task_id: z.string().nullable().optional(),
430
431
  config_version: z.number().int().positive().nullable().default(null),
432
+ attempt_count: z.coerce.number().int().positive().optional(),
431
433
  cancel_reason: z.string().nullable().default(null),
432
- queued_at: z.coerce.date()
434
+ queued_at: z.coerce.date(),
435
+ error: z.string().nullable().optional()
433
436
  });
434
437
  const taskItemSchema = z.discriminatedUnion("status", [
435
438
  taskCommonSchema.extend({ status: z.literal("pending") }),
@@ -511,6 +514,7 @@ var TaskListProvider = class {
511
514
  base_branch_id: params.base_branch_id,
512
515
  parent_task_id: parentTaskId,
513
516
  config_version: params.type === "reconfig" || params.type === "bootstrap" ? params.configVersion : null,
517
+ attempt_count: 1,
514
518
  cancel_reason: null,
515
519
  queued_at: /* @__PURE__ */ new Date()
516
520
  });
@@ -602,6 +606,154 @@ var TaskListProvider = class {
602
606
  }
603
607
  };
604
608
 
609
+ //#endregion
610
+ //#region src/providers/task-list-db9-provider.ts
611
+ var TaskListDb9Provider = class extends TaskListProvider {
612
+ db;
613
+ ownsDb;
614
+ constructor(agentName, logger, options = {}) {
615
+ super(agentName, logger);
616
+ if (options.db) {
617
+ this.db = options.db;
618
+ this.ownsDb = false;
619
+ } else {
620
+ this.db = createDb9Db();
621
+ this.ownsDb = true;
622
+ }
623
+ }
624
+ async close() {
625
+ if (this.ownsDb) await this.db.destroy();
626
+ }
627
+ selectTask() {
628
+ return this.db.selectFrom("task").selectAll().where("agent", "=", this.agentName);
629
+ }
630
+ async getAgentConfig(projectId) {
631
+ const config = await this.db.selectFrom("agent_project_config").innerJoin("task", "task.id", "agent_project_config.config_task_id").select([
632
+ "agent_project_config.agent",
633
+ "agent_project_config.project_id",
634
+ "agent_project_config.base_branch_id",
635
+ "agent_project_config.config_version",
636
+ "agent_project_config.config_task_id",
637
+ "agent_project_config.concurrency",
638
+ "agent_project_config.max_retry_attempts",
639
+ "agent_project_config.retry_backoff_seconds",
640
+ "agent_project_config.role",
641
+ "agent_project_config.skills",
642
+ "agent_project_config.prototype_url",
643
+ "agent_project_config.execute_agent"
644
+ ]).select((eb) => eb.ref("task.status").$castTo().as("config_task_status")).where("agent_project_config.project_id", "=", projectId).where("agent_project_config.agent", "=", this.agentName).executeTakeFirst();
645
+ if (config == null) return null;
646
+ return config;
647
+ }
648
+ async getAgentConfigs() {
649
+ return await this.db.selectFrom("agent_project_config").innerJoin("task", "task.id", "agent_project_config.config_task_id").select([
650
+ "agent_project_config.agent",
651
+ "agent_project_config.project_id",
652
+ "agent_project_config.base_branch_id",
653
+ "agent_project_config.config_version",
654
+ "agent_project_config.config_task_id",
655
+ "agent_project_config.concurrency",
656
+ "agent_project_config.max_retry_attempts",
657
+ "agent_project_config.retry_backoff_seconds",
658
+ "agent_project_config.role",
659
+ "agent_project_config.skills",
660
+ "agent_project_config.prototype_url",
661
+ "agent_project_config.execute_agent"
662
+ ]).select((eb) => eb.ref("task.status").$castTo().as("config_task_status")).where("agent_project_config.agent", "=", this.agentName).execute();
663
+ }
664
+ async setAgentConfig({ skills, ...config }) {
665
+ const maxRetryAttempts = config.max_retry_attempts ?? 3;
666
+ const retryBackoffSeconds = config.retry_backoff_seconds ?? 30;
667
+ await this.db.insertInto("agent_project_config").values({
668
+ agent: this.agentName,
669
+ skills: JSON.stringify(skills),
670
+ ...config,
671
+ max_retry_attempts: maxRetryAttempts,
672
+ retry_backoff_seconds: retryBackoffSeconds
673
+ }).execute();
674
+ }
675
+ async updateAgentConfig({ skills, ...config }) {
676
+ const maxRetryAttempts = config.max_retry_attempts;
677
+ const retryBackoffSeconds = config.retry_backoff_seconds;
678
+ const result = await this.db.updateTable("agent_project_config").set({
679
+ skills: JSON.stringify(skills),
680
+ ...config,
681
+ ...maxRetryAttempts == null ? {} : { max_retry_attempts: maxRetryAttempts },
682
+ ...retryBackoffSeconds == null ? {} : { retry_backoff_seconds: retryBackoffSeconds }
683
+ }).where("agent", "=", this.agentName).where("project_id", "=", config.project_id).executeTakeFirst();
684
+ if (Number(result.numUpdatedRows ?? 0) === 0) throw new Error(`No config found to update for agent ${this.agentName} and project ${config.project_id}.`);
685
+ }
686
+ async getTask(taskId) {
687
+ const taskItem = await this.selectTask().where("id", "=", taskId).executeTakeFirst();
688
+ if (taskItem == null) return null;
689
+ return taskItemSchema.parse(taskItem);
690
+ }
691
+ async getTasks({ status, order_by, order_direction = "asc", from, limit, type, min_config_version, max_config_version, parent_task_id, project_id } = {}) {
692
+ let builder = this.selectTask();
693
+ if (status && status.length > 0) builder = builder.where("status", "in", status);
694
+ if (from) builder = builder.where("queued_at", ">=", from);
695
+ if (type) builder = builder.where("type", "=", type);
696
+ if (min_config_version != null) builder = builder.where("config_version", ">=", min_config_version);
697
+ if (max_config_version != null) builder = builder.where("config_version", "<=", max_config_version);
698
+ if (parent_task_id === null) builder = builder.where("parent_task_id", "is", null);
699
+ else if (parent_task_id) builder = builder.where("parent_task_id", "=", parent_task_id);
700
+ if (project_id != null) builder = builder.where("project_id", "=", project_id);
701
+ if (order_by) builder = builder.orderBy(order_by, order_direction);
702
+ if (limit != null) builder = builder.limit(limit);
703
+ return (await builder.execute()).map((item) => taskItemSchema.parse(item));
704
+ }
705
+ async listAgentNames() {
706
+ const [taskAgents, configAgents] = await Promise.all([this.db.selectFrom("task").select("agent").distinct().execute(), this.db.selectFrom("agent_project_config").select("agent").distinct().execute()]);
707
+ const agents = /* @__PURE__ */ new Set();
708
+ for (const item of taskAgents) agents.add(item.agent);
709
+ for (const item of configAgents) agents.add(item.agent);
710
+ return Array.from(agents).sort();
711
+ }
712
+ async updateTask(taskItem) {
713
+ const { id, started_at, ended_at, queued_at, cancelled_at, ...rest } = taskItem;
714
+ await this.db.updateTable("task").set({
715
+ started_at,
716
+ ended_at,
717
+ queued_at,
718
+ cancelled_at,
719
+ ...rest
720
+ }).where("id", "=", id).where("agent", "=", this.agentName).execute();
721
+ }
722
+ async insertTask(taskItem) {
723
+ const { started_at, ended_at, queued_at, cancelled_at, ...rest } = taskItem;
724
+ const attemptCount = taskItem.attempt_count ?? 1;
725
+ const inserted = await this.db.insertInto("task").values({
726
+ agent: this.agentName,
727
+ started_at,
728
+ ended_at,
729
+ queued_at,
730
+ cancelled_at,
731
+ ...rest,
732
+ attempt_count: attemptCount
733
+ }).returningAll().executeTakeFirstOrThrow();
734
+ return taskItemSchema.parse(inserted);
735
+ }
736
+ async deleteTask(taskId) {
737
+ const result = await this.db.deleteFrom("task").where("id", "=", taskId).where("agent", "=", this.agentName).executeTakeFirst();
738
+ return Number(result.numDeletedRows ?? 0) > 0;
739
+ }
740
+ };
741
+
742
+ //#endregion
743
+ //#region src/db/tidb.ts
744
+ function createTidbPoolOptions(databaseUrl) {
745
+ return {
746
+ uri: databaseUrl,
747
+ supportBigNumbers: true,
748
+ bigNumberStrings: true,
749
+ timezone: "Z"
750
+ };
751
+ }
752
+ function createTidbDb(databaseUrl = process.env.DATABASE_URL) {
753
+ if (!databaseUrl) throw new Error("DATABASE_URL environment variable is not set.");
754
+ return new Kysely({ dialect: new MysqlDialect({ pool: createPool(createTidbPoolOptions(databaseUrl)) }) });
755
+ }
756
+
605
757
  //#endregion
606
758
  //#region src/providers/task-list-tidb-provider.ts
607
759
  var TaskListTidbProvider = class extends TaskListProvider {
@@ -632,16 +784,24 @@ var TaskListTidbProvider = class extends TaskListProvider {
632
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();
633
785
  }
634
786
  async setAgentConfig({ skills, ...config }) {
787
+ const maxRetryAttempts = config.max_retry_attempts ?? 3;
788
+ const retryBackoffSeconds = config.retry_backoff_seconds ?? 30;
635
789
  await this.db.insertInto("agent_project_config").values({
636
790
  agent: this.agentName,
637
791
  skills: JSON.stringify(skills),
638
- ...config
792
+ ...config,
793
+ max_retry_attempts: maxRetryAttempts,
794
+ retry_backoff_seconds: retryBackoffSeconds
639
795
  }).execute();
640
796
  }
641
797
  async updateAgentConfig({ skills, ...config }) {
798
+ const maxRetryAttempts = config.max_retry_attempts;
799
+ const retryBackoffSeconds = config.retry_backoff_seconds;
642
800
  const result = await this.db.updateTable("agent_project_config").set({
643
801
  skills: JSON.stringify(skills),
644
- ...config
802
+ ...config,
803
+ ...maxRetryAttempts == null ? {} : { max_retry_attempts: maxRetryAttempts },
804
+ ...retryBackoffSeconds == null ? {} : { retry_backoff_seconds: retryBackoffSeconds }
645
805
  }).where("agent", "=", this.agentName).where("project_id", "=", config.project_id).executeTakeFirst();
646
806
  if (Number(result.numUpdatedRows ?? 0) === 0) throw new Error(`No config found to update for agent ${this.agentName} and project ${config.project_id}.`);
647
807
  }
@@ -683,13 +843,15 @@ var TaskListTidbProvider = class extends TaskListProvider {
683
843
  }
684
844
  async insertTask(taskItem) {
685
845
  const { started_at, ended_at, queued_at, cancelled_at, ...rest } = taskItem;
846
+ const attemptCount = taskItem.attempt_count ?? 1;
686
847
  const { insertId } = await this.db.insertInto("task").values({
687
848
  agent: this.agentName,
688
849
  started_at,
689
850
  ended_at,
690
851
  queued_at,
691
852
  cancelled_at,
692
- ...rest
853
+ ...rest,
854
+ attempt_count: attemptCount
693
855
  }).executeTakeFirstOrThrow();
694
856
  return {
695
857
  ...taskItem,
@@ -702,6 +864,28 @@ var TaskListTidbProvider = class extends TaskListProvider {
702
864
  }
703
865
  };
704
866
 
867
+ //#endregion
868
+ //#region src/providers/task-list-provider-factory.ts
869
+ function getDatabaseProtocol(databaseUrl) {
870
+ try {
871
+ return new URL(databaseUrl).protocol.toLowerCase();
872
+ } catch {
873
+ throw new Error("Invalid DATABASE_URL. Expected a URL starting with mysql:// or postgresql://.");
874
+ }
875
+ }
876
+ function resolveTaskListProviderKind(databaseUrl) {
877
+ const protocol = getDatabaseProtocol(databaseUrl);
878
+ if (protocol === "mysql:") return "tidb";
879
+ if (protocol === "postgresql:") return "db9";
880
+ throw new Error(`Unsupported DATABASE_URL protocol '${protocol}'. Use mysql:// for TiDB or postgresql:// for db9.`);
881
+ }
882
+ function createTaskListProvider(agentName, logger) {
883
+ const databaseUrl = process.env.DATABASE_URL?.trim();
884
+ if (!databaseUrl) throw new Error("DATABASE_URL environment variable is not set.");
885
+ if (resolveTaskListProviderKind(databaseUrl) === "db9") return new TaskListDb9Provider(agentName, logger);
886
+ return new TaskListTidbProvider(agentName, logger);
887
+ }
888
+
705
889
  //#endregion
706
890
  //#region src/core/agent-config-utils.ts
707
891
  function normalizeRepoUrl(value) {
@@ -1016,7 +1200,7 @@ const branchExecutionResultSchema = z.object({
1016
1200
  status: z.string(),
1017
1201
  status_text: z.string(),
1018
1202
  branch_id: z.string(),
1019
- snap_id: z.string(),
1203
+ snap_id: z.string().nullable(),
1020
1204
  background_task_id: z.string().nullable(),
1021
1205
  started_at: zodJsonDate,
1022
1206
  last_polled_at: zodJsonDate,
@@ -1093,7 +1277,7 @@ const explorationResultSchema = z.object({
1093
1277
  status: z.string(),
1094
1278
  status_text: z.string().nullable(),
1095
1279
  branch_id: z.string(),
1096
- snap_id: z.string(),
1280
+ snap_id: z.string().nullable(),
1097
1281
  background_task_id: z.string().nullable(),
1098
1282
  started_at: zodJsonDate,
1099
1283
  last_polled_at: zodJsonDate,
@@ -1902,7 +2086,65 @@ async function executeOnPantheon({ projectId, branchId, prompt, agent }) {
1902
2086
  //#endregion
1903
2087
  //#region src/core/task-list.ts
1904
2088
  const DEFAULT_CONCURRENCY = 1;
2089
+ const DEFAULT_MAX_RETRY_ATTEMPTS$1 = 3;
2090
+ const DEFAULT_RETRY_BACKOFF_SECONDS$1 = 30;
1905
2091
  const AUTO_RECONFIG_PROMPT = "Config outdated. Reconfigure this branch against the latest agent config.";
2092
+ function getTaskAttemptCount(task) {
2093
+ if (Number.isInteger(task.attempt_count) && task.attempt_count > 0) return task.attempt_count;
2094
+ return 1;
2095
+ }
2096
+ function getMaxRetryAttempts(config, logger) {
2097
+ const configured = config.max_retry_attempts;
2098
+ if (configured == null) return DEFAULT_MAX_RETRY_ATTEMPTS$1;
2099
+ if (Number.isInteger(configured) && configured >= 0) return configured;
2100
+ logger.warn("Invalid max_retry_attempts=%o for project %s; fallback to %d.", configured, config.project_id, DEFAULT_MAX_RETRY_ATTEMPTS$1);
2101
+ return DEFAULT_MAX_RETRY_ATTEMPTS$1;
2102
+ }
2103
+ function getRetryBackoffSeconds(config, logger) {
2104
+ const configured = config.retry_backoff_seconds;
2105
+ if (configured == null) return DEFAULT_RETRY_BACKOFF_SECONDS$1;
2106
+ if (Number.isInteger(configured) && configured > 0) return configured;
2107
+ logger.warn("Invalid retry_backoff_seconds=%o for project %s; fallback to %d.", configured, config.project_id, DEFAULT_RETRY_BACKOFF_SECONDS$1);
2108
+ return DEFAULT_RETRY_BACKOFF_SECONDS$1;
2109
+ }
2110
+ function getBackoffDelaySeconds(backoffBaseSeconds, nextAttemptCount) {
2111
+ return backoffBaseSeconds * 2 ** (Math.max(1, nextAttemptCount - 1) - 1);
2112
+ }
2113
+ async function retryFailedTaskInPlace(provider, task, logger, options = {}) {
2114
+ const config = await provider.getAgentConfig(task.project_id);
2115
+ if (!config) {
2116
+ logger.warn("Skip retry for task %s because project config %s is missing.", task.id, task.project_id);
2117
+ return null;
2118
+ }
2119
+ const currentAttemptCount = getTaskAttemptCount(task);
2120
+ const retriesUsed = Math.max(0, currentAttemptCount - 1);
2121
+ const maxRetryAttempts = getMaxRetryAttempts(config, logger);
2122
+ if (!options.ignoreRetryLimit && retriesUsed >= maxRetryAttempts) {
2123
+ logger.info("Task %s reached max retries (%d). Keep failed status.", task.id, maxRetryAttempts);
2124
+ return null;
2125
+ }
2126
+ const nextAttemptCount = currentAttemptCount + 1;
2127
+ const now = Date.now();
2128
+ const delaySeconds = options.immediate ? 0 : getBackoffDelaySeconds(getRetryBackoffSeconds(config, logger), nextAttemptCount);
2129
+ const queuedAt = new Date(now + delaySeconds * 1e3);
2130
+ const retriedTask = {
2131
+ status: "pending",
2132
+ id: task.id,
2133
+ task: task.task,
2134
+ type: task.type,
2135
+ project_id: task.project_id,
2136
+ base_branch_id: task.base_branch_id,
2137
+ parent_task_id: task.parent_task_id ?? null,
2138
+ config_version: task.config_version ?? null,
2139
+ attempt_count: nextAttemptCount,
2140
+ cancel_reason: task.cancel_reason ?? null,
2141
+ queued_at: queuedAt,
2142
+ error: task.error
2143
+ };
2144
+ await provider.updateTask(retriedTask);
2145
+ logger.info("Requeued failed task %s for attempt %d at %s%s.", task.id, nextAttemptCount, queuedAt.toISOString(), options.reason ? ` (reason: ${options.reason})` : "");
2146
+ return retriedTask;
2147
+ }
1906
2148
  function buildTaskPromptSequence(config, taskPrompt, taskType = "default") {
1907
2149
  if (taskType === "bootstrap" && taskPrompt === "$setup") return [buildConfigSetupStep({
1908
2150
  prototypeRepoUrl: config.prototype_url,
@@ -1929,7 +2171,7 @@ async function startTaskListLoop(agentName, { loopInterval = 5 }, logger) {
1929
2171
  abortController.abort(signal);
1930
2172
  process.exit(1);
1931
2173
  });
1932
- const provider = new TaskListTidbProvider(agentName, logger);
2174
+ const provider = createTaskListProvider(agentName, logger);
1933
2175
  while (!abortController.signal.aborted) {
1934
2176
  const loopIndex = i++;
1935
2177
  await loopOnce(provider, logger.child({ name: `loop:${loopIndex}` }));
@@ -1989,6 +2231,8 @@ async function pollRunningTaskState(provider, state, logger) {
1989
2231
  } else if (newStatus.state === "failed") {
1990
2232
  logger.info(`Task failed on Branch[id = ${newStatus.branch.id},snap_id=${newStatus.branch.latest_snap_id}] with error: %s`, newStatus.error);
1991
2233
  await provider.failTask(state, newStatus.error);
2234
+ const failedTask = await provider.getTask(state.id);
2235
+ if (failedTask?.status === "failed") await retryFailedTaskInPlace(provider, failedTask, logger.child({ name: `task:${state.id}:auto-retry` }));
1992
2236
  }
1993
2237
  }
1994
2238
  function isTaskOutdated(task, config) {
@@ -2113,6 +2357,7 @@ async function startPendingTasksUpToConcurrency(provider, pendingTasks, runningT
2113
2357
  return config;
2114
2358
  };
2115
2359
  for (const task of pendingTasks) {
2360
+ if (task.queued_at.getTime() > Date.now()) continue;
2116
2361
  const config = await getProjectConfig(task.project_id);
2117
2362
  if (!config) {
2118
2363
  if (!missingConfigProjects.has(task.project_id)) {
@@ -2129,6 +2374,8 @@ async function startPendingTasksUpToConcurrency(provider, pendingTasks, runningT
2129
2374
  runningCountByProject.set(task.project_id, runningCount + 1);
2130
2375
  } catch (e) {
2131
2376
  logger.error(`Failed to start task ${task.id}: ${getErrorMessage(e)}`);
2377
+ const failedTask = await provider.getTask(task.id);
2378
+ if (failedTask?.status === "failed") await retryFailedTaskInPlace(provider, failedTask, logger.child({ name: `task:${task.id}:auto-retry` }));
2132
2379
  }
2133
2380
  }
2134
2381
  }
@@ -2558,10 +2805,12 @@ var WatchStepAggregator = class {
2558
2805
 
2559
2806
  //#endregion
2560
2807
  //#region src/core/index.ts
2808
+ const DEFAULT_MAX_RETRY_ATTEMPTS = 3;
2809
+ const DEFAULT_RETRY_BACKOFF_SECONDS = 30;
2561
2810
  async function runAgent(name, options, logger) {
2562
2811
  const agentDir = path.join(options.dataDir, "agents", name);
2563
2812
  const pidFile = path.join(agentDir, "pid");
2564
- await fs.promises.mkdir(agentDir, { recursive: true });
2813
+ await fs$1.promises.mkdir(agentDir, { recursive: true });
2565
2814
  await assertsSingleton(logger, pidFile);
2566
2815
  await startTaskListLoop(name, { loopInterval: options.loopInterval }, logger);
2567
2816
  }
@@ -2575,7 +2824,7 @@ function resolveConfigRole(options) {
2575
2824
  return { error: "--role is required for first-time config in a project." };
2576
2825
  }
2577
2826
  async function configAgent(name, options) {
2578
- const provider = new TaskListTidbProvider(name, pino());
2827
+ const provider = createTaskListProvider(name, pino());
2579
2828
  try {
2580
2829
  const previousConfig = await provider.getAgentConfig(options.projectId);
2581
2830
  const resolvedRoleResult = resolveConfigRole({
@@ -2594,6 +2843,18 @@ async function configAgent(name, options) {
2594
2843
  process.exitCode = 1;
2595
2844
  return;
2596
2845
  }
2846
+ const maxRetryAttempts = options.maxRetryAttempts ?? previousConfig?.max_retry_attempts ?? DEFAULT_MAX_RETRY_ATTEMPTS;
2847
+ if (!Number.isInteger(maxRetryAttempts) || maxRetryAttempts < 0) {
2848
+ console.error("--max-retry-attempts must be a non-negative integer.");
2849
+ process.exitCode = 1;
2850
+ return;
2851
+ }
2852
+ const retryBackoffSeconds = options.retryBackoffSeconds ?? previousConfig?.retry_backoff_seconds ?? DEFAULT_RETRY_BACKOFF_SECONDS;
2853
+ if (!Number.isInteger(retryBackoffSeconds) || retryBackoffSeconds <= 0) {
2854
+ console.error("--retry-backoff-seconds must be a positive integer.");
2855
+ process.exitCode = 1;
2856
+ return;
2857
+ }
2597
2858
  const resolvedSkills = options.skills ?? normalizeSkills(previousConfig?.skills);
2598
2859
  const resolvedExecuteAgent = options.executeAgent.trim() || previousConfig?.execute_agent || "codex";
2599
2860
  const resolvedPrototypeUrl = options.prototypeUrl.trim() || previousConfig?.prototype_url || "https://github.com/pingcap-inc/pantheon-agents";
@@ -2624,6 +2885,8 @@ async function configAgent(name, options) {
2624
2885
  config_task_id: configTaskId,
2625
2886
  config_version: previousConfig.config_version + (options.prompt ? 2 : 1),
2626
2887
  concurrency,
2888
+ max_retry_attempts: maxRetryAttempts,
2889
+ retry_backoff_seconds: retryBackoffSeconds,
2627
2890
  execute_agent: resolvedExecuteAgent,
2628
2891
  role: resolvedRole ?? previousConfig.role,
2629
2892
  skills: resolvedSkills,
@@ -2691,6 +2954,8 @@ async function configAgent(name, options) {
2691
2954
  config_task_id: configTaskId,
2692
2955
  config_version: configVersion,
2693
2956
  concurrency,
2957
+ max_retry_attempts: maxRetryAttempts,
2958
+ retry_backoff_seconds: retryBackoffSeconds,
2694
2959
  execute_agent: resolvedExecuteAgent,
2695
2960
  role: resolvedRole,
2696
2961
  skills: resolvedSkills,
@@ -2702,6 +2967,8 @@ async function configAgent(name, options) {
2702
2967
  config_task_id: configTaskId,
2703
2968
  config_version: configVersion,
2704
2969
  concurrency,
2970
+ max_retry_attempts: maxRetryAttempts,
2971
+ retry_backoff_seconds: retryBackoffSeconds,
2705
2972
  execute_agent: resolvedExecuteAgent,
2706
2973
  role: resolvedRole ?? previousConfig.role,
2707
2974
  skills: resolvedSkills,
@@ -2735,7 +3002,7 @@ async function resolveInitialBaseBranchId(projectId, rootBranchId) {
2735
3002
  return project.root_branch_id;
2736
3003
  }
2737
3004
  async function addTask(name, options) {
2738
- const provider = new TaskListTidbProvider(name, pino());
3005
+ const provider = createTaskListProvider(name, pino());
2739
3006
  try {
2740
3007
  const config = await provider.getAgentConfig(options.projectId);
2741
3008
  if (!config) throw new Error(`Agent ${name} not configured for project ${options.projectId}`);
@@ -2751,7 +3018,7 @@ async function addTask(name, options) {
2751
3018
  }
2752
3019
  }
2753
3020
  async function deleteTask(agentName, taskId) {
2754
- const provider = new TaskListTidbProvider(agentName, pino());
3021
+ const provider = createTaskListProvider(agentName, pino());
2755
3022
  try {
2756
3023
  const task = await provider.getTask(taskId);
2757
3024
  if (!task) return null;
@@ -2762,7 +3029,7 @@ async function deleteTask(agentName, taskId) {
2762
3029
  }
2763
3030
  }
2764
3031
  async function getTask(agentName, taskId) {
2765
- const provider = new TaskListTidbProvider(agentName, pino());
3032
+ const provider = createTaskListProvider(agentName, pino());
2766
3033
  try {
2767
3034
  return await provider.getTask(taskId);
2768
3035
  } finally {
@@ -2770,7 +3037,7 @@ async function getTask(agentName, taskId) {
2770
3037
  }
2771
3038
  }
2772
3039
  async function cancelTask(agentName, taskId, reason) {
2773
- const provider = new TaskListTidbProvider(agentName, pino());
3040
+ const provider = createTaskListProvider(agentName, pino());
2774
3041
  try {
2775
3042
  const task = await provider.getTask(taskId);
2776
3043
  if (!task) return null;
@@ -2781,8 +3048,24 @@ async function cancelTask(agentName, taskId, reason) {
2781
3048
  await provider.close();
2782
3049
  }
2783
3050
  }
3051
+ async function retryTask(agentName, taskId, reason) {
3052
+ const provider = createTaskListProvider(agentName, pino());
3053
+ try {
3054
+ const task = await provider.getTask(taskId);
3055
+ if (!task) return null;
3056
+ if (task.status !== "failed") throw new Error(`Task ${taskId} is ${task.status}; only failed tasks can be retried.`);
3057
+ if (!await retryFailedTaskInPlace(provider, task, pino(), {
3058
+ immediate: true,
3059
+ reason,
3060
+ ignoreRetryLimit: true
3061
+ })) throw new Error(`Task ${taskId} retry was not scheduled.`);
3062
+ return await provider.getTask(taskId);
3063
+ } finally {
3064
+ await provider.close();
3065
+ }
3066
+ }
2784
3067
  async function showAgentConfig(agentName, projectId) {
2785
- const provider = new TaskListTidbProvider(agentName, pino());
3068
+ const provider = createTaskListProvider(agentName, pino());
2786
3069
  try {
2787
3070
  return await provider.getAgentConfig(projectId);
2788
3071
  } finally {
@@ -2790,7 +3073,7 @@ async function showAgentConfig(agentName, projectId) {
2790
3073
  }
2791
3074
  }
2792
3075
  async function showAgentConfigs(agentName) {
2793
- const provider = new TaskListTidbProvider(agentName, pino());
3076
+ const provider = createTaskListProvider(agentName, pino());
2794
3077
  try {
2795
3078
  return await provider.getAgentConfigs();
2796
3079
  } finally {
@@ -2798,7 +3081,7 @@ async function showAgentConfigs(agentName) {
2798
3081
  }
2799
3082
  }
2800
3083
  async function showTasks(name, options) {
2801
- const provider = new TaskListTidbProvider(name, pino());
3084
+ const provider = createTaskListProvider(name, pino());
2802
3085
  try {
2803
3086
  return await provider.getTasks({
2804
3087
  status: options.status,
@@ -2811,7 +3094,7 @@ async function showTasks(name, options) {
2811
3094
  }
2812
3095
  }
2813
3096
  async function listAgentNames() {
2814
- const provider = new TaskListTidbProvider("list-agents", pino());
3097
+ const provider = createTaskListProvider("list-agents", pino());
2815
3098
  try {
2816
3099
  return await provider.listAgentNames();
2817
3100
  } finally {
@@ -2883,24 +3166,26 @@ function formatConciseTaskLine(options) {
2883
3166
  const statusText = formatStatus(task.status, useColor);
2884
3167
  const timeText = formatRelativeTime(timestamp);
2885
3168
  const taskText = truncateText(task.task, maxTaskLength);
3169
+ const attempt = Number.isInteger(task.attempt_count) && task.attempt_count > 0 ? task.attempt_count : 1;
2886
3170
  return [
2887
3171
  agent,
2888
3172
  statusText,
2889
3173
  task.id,
3174
+ `attempt:${attempt}`,
2890
3175
  timeText,
2891
3176
  taskText
2892
3177
  ].filter((part) => part !== "").join(" ");
2893
3178
  }
2894
3179
  async function assertsSingleton(logger, pidFile) {
2895
3180
  try {
2896
- const pid = await fs.promises.readFile(pidFile, "utf-8");
3181
+ const pid = await fs$1.promises.readFile(pidFile, "utf-8");
2897
3182
  process.kill(parseInt(pid), 0);
2898
3183
  console.error("Failed to assert singleton agent process:");
2899
3184
  process.exit(1);
2900
3185
  } catch (e) {
2901
- await fs.promises.writeFile(pidFile, process.pid.toString());
3186
+ await fs$1.promises.writeFile(pidFile, process.pid.toString());
2902
3187
  process.on("exit", () => {
2903
- fs.promises.rm(pidFile);
3188
+ fs$1.promises.rm(pidFile);
2904
3189
  });
2905
3190
  }
2906
3191
  }
@@ -3069,8 +3354,18 @@ function parseConcurrency(value) {
3069
3354
  if (!Number.isInteger(parsed) || parsed < 1) throw new InvalidArgumentError("concurrency must be a positive integer.");
3070
3355
  return parsed;
3071
3356
  }
3357
+ function parseNonNegativeInteger(value, fieldName) {
3358
+ const parsed = Number(value);
3359
+ if (!Number.isInteger(parsed) || parsed < 0) throw new InvalidArgumentError(`${fieldName} must be a non-negative integer.`);
3360
+ return parsed;
3361
+ }
3362
+ function parsePositiveInteger(value, fieldName) {
3363
+ const parsed = Number(value);
3364
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new InvalidArgumentError(`${fieldName} must be a positive integer.`);
3365
+ return parsed;
3366
+ }
3072
3367
  function createConfigAgentCommand(version, deps = {}) {
3073
- 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() {
3368
+ 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() {
3074
3369
  const [name, prompt] = this.args;
3075
3370
  const options = this.opts();
3076
3371
  const resolvedProjectId = resolvePantheonProjectId(options.projectId);
@@ -3088,6 +3383,8 @@ function createConfigAgentCommand(version, deps = {}) {
3088
3383
  role: options.role,
3089
3384
  executeAgent: options.executeAgent,
3090
3385
  concurrency: options.concurrency,
3386
+ maxRetryAttempts: options.maxRetryAttempts,
3387
+ retryBackoffSeconds: options.retryBackoffSeconds,
3091
3388
  skills: options.skills,
3092
3389
  prototypeUrl: options.prototypeUrl,
3093
3390
  rootBranchId: resolvedRootBranchId
@@ -3142,9 +3439,11 @@ function formatTaskAsSkillTask(agent, task) {
3142
3439
  const endedAt = "ended_at" in task ? task.ended_at ?? null : null;
3143
3440
  const canceledAt = task.status === "cancelled" ? task.cancelled_at : null;
3144
3441
  const finishedAt = endedAt ?? canceledAt ?? null;
3442
+ const attempt = Number.isInteger(task.attempt_count) && task.attempt_count > 0 ? task.attempt_count : 1;
3145
3443
  return {
3146
3444
  agent,
3147
3445
  task: task.task,
3446
+ attempt,
3148
3447
  parent_task_id: task.parent_task_id ?? null,
3149
3448
  status: toSkillTaskStatus(task.status),
3150
3449
  queued_at: task.queued_at.toISOString(),
@@ -3154,7 +3453,7 @@ function formatTaskAsSkillTask(agent, task) {
3154
3453
  canceled_at: toIsoOrNull(canceledAt),
3155
3454
  cancel_reason: task.cancel_reason ?? null,
3156
3455
  output: task.status === "completed" ? task.output : null,
3157
- error: task.status === "failed" ? task.error : null
3456
+ error: task.error ?? null
3158
3457
  };
3159
3458
  }
3160
3459
  function createGetTaskCommand(version, deps = {}) {
@@ -3235,6 +3534,8 @@ function createShowConfigCommand(version) {
3235
3534
  config_version: config.config_version,
3236
3535
  config_task_id: config.config_task_id,
3237
3536
  concurrency: config.concurrency,
3537
+ max_retry_attempts: config.max_retry_attempts ?? 3,
3538
+ retry_backoff_seconds: config.retry_backoff_seconds ?? 30,
3238
3539
  role: config.role,
3239
3540
  execute_agent: config.execute_agent,
3240
3541
  prototype_url: config.prototype_url,
@@ -3259,6 +3560,8 @@ function createShowConfigCommand(version) {
3259
3560
  config_version: config.config_version,
3260
3561
  config_task_id: config.config_task_id,
3261
3562
  concurrency: config.concurrency,
3563
+ max_retry_attempts: config.max_retry_attempts ?? 3,
3564
+ retry_backoff_seconds: config.retry_backoff_seconds ?? 30,
3262
3565
  role: config.role,
3263
3566
  execute_agent: config.execute_agent,
3264
3567
  prototype_url: config.prototype_url,
@@ -3382,6 +3685,135 @@ function createShowTasksCommand(version, deps = {}) {
3382
3685
  });
3383
3686
  }
3384
3687
 
3688
+ //#endregion
3689
+ //#region src/cli/commands/retry-task.ts
3690
+ function resolveTaskAttempt(value) {
3691
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
3692
+ return 1;
3693
+ }
3694
+ function createRetryTaskCommand(version, deps = {}) {
3695
+ 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() {
3696
+ const [name, taskId, reason] = this.args;
3697
+ const options = this.opts();
3698
+ if (!ensureEnv(["DATABASE_URL"])) return;
3699
+ const rl = options.yes || options.force ? null : readline.createInterface({
3700
+ input: process$1.stdin,
3701
+ output: process$1.stdout
3702
+ });
3703
+ try {
3704
+ if (rl) {
3705
+ if ((await rl.question(`Type the task id (${taskId}) to confirm retry: `)).trim() !== taskId) {
3706
+ console.error("Confirmation failed. Task id did not match.");
3707
+ process$1.exitCode = 1;
3708
+ return;
3709
+ }
3710
+ if ((await rl.question("Type RETRY to requeue this failed task: ")).trim() !== "RETRY") {
3711
+ console.error("Confirmation failed. Aborting retry.");
3712
+ process$1.exitCode = 1;
3713
+ return;
3714
+ }
3715
+ }
3716
+ const updatedTask = await (deps.retryTask ?? retryTask)(name, taskId, reason);
3717
+ if (!updatedTask) {
3718
+ console.error(`Task ${taskId} not found for agent ${name}.`);
3719
+ process$1.exitCode = 1;
3720
+ return;
3721
+ }
3722
+ if (updatedTask.status !== "pending") {
3723
+ console.error(`Unexpected result: task ${taskId} status is ${updatedTask.status} after retry.`);
3724
+ process$1.exitCode = 1;
3725
+ return;
3726
+ }
3727
+ const attempt = resolveTaskAttempt(updatedTask.attempt_count);
3728
+ console.log(`Requeued task ${taskId} for agent ${name} as attempt ${attempt}, next run at ${updatedTask.queued_at.toISOString()}.${reason ? ` Reason: ${reason}` : ""}`);
3729
+ } catch (error) {
3730
+ console.error(error instanceof Error ? error.message : String(error));
3731
+ process$1.exitCode = 1;
3732
+ } finally {
3733
+ rl?.close();
3734
+ }
3735
+ });
3736
+ }
3737
+
3738
+ //#endregion
3739
+ //#region src/cli/commands/skill-sh.ts
3740
+ 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";
3741
+ function createSkillShCommand(version) {
3742
+ return createCommand("skill.sh").version(version).description("Print embedded pantheon-agents skill markdown").action(() => {
3743
+ process$1.stdout.write(EMBEDDED_SKILL_MARKDOWN);
3744
+ });
3745
+ }
3746
+
3747
+ //#endregion
3748
+ //#region src/cli/commands/gen-migration-sql.ts
3749
+ const introducedInPattern = /^--\s*introduced_in:\s*([0-9]+\.[0-9]+\.[0-9]+)\s*$/m;
3750
+ function parseProvider(value) {
3751
+ if (value === "tidb" || value === "db9") return value;
3752
+ throw new InvalidArgumentError("provider must be one of: tidb, db9");
3753
+ }
3754
+ function parseSemver(value) {
3755
+ const matched = value.trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
3756
+ if (!matched) return null;
3757
+ return [
3758
+ Number(matched[1]),
3759
+ Number(matched[2]),
3760
+ Number(matched[3])
3761
+ ];
3762
+ }
3763
+ function parseSemverOption(value) {
3764
+ if (parseSemver(value) == null) throw new InvalidArgumentError("from must be a semver like 0.3.0");
3765
+ return value;
3766
+ }
3767
+ function compareSemver(a, b) {
3768
+ const pa = parseSemver(a);
3769
+ const pb = parseSemver(b);
3770
+ if (!pa || !pb) throw new Error(`Invalid semver comparison: '${a}' vs '${b}'`);
3771
+ for (let i = 0; i < 3; i++) {
3772
+ if (pa[i] > pb[i]) return 1;
3773
+ if (pa[i] < pb[i]) return -1;
3774
+ }
3775
+ return 0;
3776
+ }
3777
+ function resolveMigrationDir(provider, cwd) {
3778
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
3779
+ const candidates = [
3780
+ path.resolve(cwd, "src/db/migrations", provider),
3781
+ path.resolve(cwd, "packages/agents/src/db/migrations", provider),
3782
+ path.resolve(moduleDir, "db/migrations", provider),
3783
+ path.resolve(moduleDir, "../db/migrations", provider),
3784
+ path.resolve(moduleDir, "../../db/migrations", provider)
3785
+ ];
3786
+ for (const candidate of candidates) if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate;
3787
+ throw new Error(`Migration directory not found for provider '${provider}'. Checked: ${candidates.join(", ")}`);
3788
+ }
3789
+ function loadProviderMigrations(provider, options = {}) {
3790
+ const migrationDir = resolveMigrationDir(provider, options.cwd ?? process$1.cwd());
3791
+ return fs.readdirSync(migrationDir).filter((name) => name.endsWith(".sql")).sort((a, b) => a.localeCompare(b)).map((fileName) => {
3792
+ const filePath = path.join(migrationDir, fileName);
3793
+ const sql = fs.readFileSync(filePath, "utf8");
3794
+ const introduced = sql.match(introducedInPattern)?.[1];
3795
+ if (!introduced) throw new Error(`Migration file '${fileName}' missing required header '-- introduced_in: x.y.z'.`);
3796
+ return {
3797
+ fileName,
3798
+ introducedIn: introduced,
3799
+ sql
3800
+ };
3801
+ });
3802
+ }
3803
+ function getMigrationSql(provider, fromVersion, options = {}) {
3804
+ if (!parseSemver(fromVersion)) throw new Error(`Invalid from version '${fromVersion}'. Expected x.y.z`);
3805
+ const migrations = loadProviderMigrations(provider, options).filter((migration) => compareSemver(migration.introducedIn, fromVersion) > 0);
3806
+ if (migrations.length === 0) return `-- No migrations for provider '${provider}' since ${fromVersion}.\n`;
3807
+ return migrations.map((migration) => `-- migration: ${migration.fileName}\n${migration.sql.trimEnd()}\n`).join("\n");
3808
+ }
3809
+ function createGenMigrationSqlCommand(version, deps = {}) {
3810
+ 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() {
3811
+ const options = this.opts();
3812
+ const output = (deps.getMigrationSql ?? getMigrationSql)(options.provider, options.from);
3813
+ process$1.stdout.write(output);
3814
+ });
3815
+ }
3816
+
3385
3817
  //#endregion
3386
3818
  //#region ../agent-stream-parser/src/utils.ts
3387
3819
  function isRecord(value) {
@@ -5068,6 +5500,11 @@ async function resolveWatchTargets(options) {
5068
5500
  warnings
5069
5501
  };
5070
5502
  }
5503
+ function createWatchDb() {
5504
+ const databaseUrl = process.env.DATABASE_URL?.trim();
5505
+ if (!databaseUrl) throw new Error("DATABASE_URL environment variable is not set.");
5506
+ return resolveTaskListProviderKind(databaseUrl) === "db9" ? createDb9Db() : createTidbDb();
5507
+ }
5071
5508
  const ANSI = {
5072
5509
  reset: "\x1B[0m",
5073
5510
  bold: "\x1B[1m",
@@ -5153,7 +5590,7 @@ function createWatchCommand(version) {
5153
5590
  console.error("Use either --tasks or --agents, not both.");
5154
5591
  process.exit(1);
5155
5592
  }
5156
- const db = createTidbDb();
5593
+ const db = createWatchDb();
5157
5594
  try {
5158
5595
  const { targets, warnings } = await resolveWatchTargets({
5159
5596
  db,
@@ -5802,7 +6239,7 @@ function createWatchStreamCommand(version) {
5802
6239
 
5803
6240
  //#endregion
5804
6241
  //#region src/cli/index.ts
5805
- 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));
6242
+ 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(createRetryTaskCommand(version)).addCommand(createSkillShCommand(version)).addCommand(createGenMigrationSqlCommand(version)).addCommand(createShowConfigCommand(version)).addCommand(createShowTasksCommand(version)).addCommand(createWatchCommand(version)).addCommand(createWatchStreamCommand(version));
5806
6243
  async function main() {
5807
6244
  if (process$1.argv.length <= 2) {
5808
6245
  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.2.2",
4
+ "version": "0.3.1",
5
5
  "bin": {
6
6
  "pantheon-agents": "dist/index.js"
7
7
  },
@@ -12,20 +12,22 @@
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",
19
19
  "dev:db:start": "tiup playground --without-monitor --tag pantheon-agents",
20
- "dev:db:gen": "kysely-codegen "
20
+ "dev:db:gen": "kysely-codegen --no-domains"
21
21
  },
22
22
  "dependencies": {
23
+ "@types/pg": "^8.18.0",
23
24
  "@types/shell-quote": "^1.7.5",
24
25
  "commander": "^14.0.3",
25
26
  "dotenv": "^17.2.4",
26
27
  "expand-tilde": "^2.0.2",
27
28
  "kysely": "^0.28.11",
28
29
  "mysql2": "^3.16.3",
30
+ "pg": "^8.19.0",
29
31
  "pino": "^10.3.0",
30
32
  "pino-pretty": "^13.1.3",
31
33
  "pino-roll": "^4.0.0",