@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$
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|