@pantheon.ai/agents 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,15 +60,19 @@ Example migration added in this version:
60
60
 
61
61
  - `src/db/migrations/tidb/20260304_0001_add_task_retry_columns.sql`
62
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`
63
65
 
64
66
  Apply manually:
65
67
 
66
68
  ```bash
67
69
  # TiDB/MySQL
68
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
69
72
 
70
73
  # db9/PostgreSQL
71
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
72
76
  ```
73
77
 
74
78
  ## Developer Local Setup
@@ -112,6 +116,9 @@ pantheon-agents config reviewer "Download some files" --project-id 019c0495-f77a
112
116
 
113
117
  # update role on later config request
114
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"}'
115
122
  ```
116
123
 
117
124
  ### 2) Add a task
@@ -159,6 +166,7 @@ pantheon-agents show-tasks --all
159
166
  pantheon-agents get-task <agent-name> <task-id>
160
167
  pantheon-agents cancel-task <agent-name> <task-id> [reason] --yes
161
168
  pantheon-agents retry-task <agent-name> <task-id> [reason] --yes
169
+ pantheon-agents kill <agent-name> <task-id> [reason] --yes
162
170
  pantheon-agents skill.sh
163
171
  pantheon-agents gen-migration-sql --provider tidb --from 0.3.0
164
172
  pantheon-agents delete-task <agent-name> <task-id>
@@ -0,0 +1,5 @@
1
+ -- introduced_in: 0.3.2
2
+ -- Add optional env overrides for per-project agent config (db9/PostgreSQL).
3
+
4
+ ALTER TABLE agent_project_config
5
+ ADD COLUMN IF NOT EXISTS envs JSONB;
@@ -0,0 +1,5 @@
1
+ -- introduced_in: 0.3.2
2
+ -- Add optional env overrides for per-project agent config (TiDB/MySQL).
3
+
4
+ ALTER TABLE agent_project_config
5
+ ADD COLUMN IF NOT EXISTS envs JSON NULL;
package/dist/index.js CHANGED
@@ -399,7 +399,7 @@ var require_cli_options = /* @__PURE__ */ __commonJSMin(((exports, module) => {
399
399
 
400
400
  //#endregion
401
401
  //#region package.json
402
- var version = "0.3.0";
402
+ var version = "0.3.3";
403
403
 
404
404
  //#endregion
405
405
  //#region src/db/db9.ts
@@ -639,6 +639,7 @@ var TaskListDb9Provider = class extends TaskListProvider {
639
639
  "agent_project_config.retry_backoff_seconds",
640
640
  "agent_project_config.role",
641
641
  "agent_project_config.skills",
642
+ "agent_project_config.envs",
642
643
  "agent_project_config.prototype_url",
643
644
  "agent_project_config.execute_agent"
644
645
  ]).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();
@@ -657,26 +658,30 @@ var TaskListDb9Provider = class extends TaskListProvider {
657
658
  "agent_project_config.retry_backoff_seconds",
658
659
  "agent_project_config.role",
659
660
  "agent_project_config.skills",
661
+ "agent_project_config.envs",
660
662
  "agent_project_config.prototype_url",
661
663
  "agent_project_config.execute_agent"
662
664
  ]).select((eb) => eb.ref("task.status").$castTo().as("config_task_status")).where("agent_project_config.agent", "=", this.agentName).execute();
663
665
  }
664
- async setAgentConfig({ skills, ...config }) {
666
+ async setAgentConfig({ skills, envs, ...config }) {
665
667
  const maxRetryAttempts = config.max_retry_attempts ?? 3;
666
668
  const retryBackoffSeconds = config.retry_backoff_seconds ?? 30;
667
669
  await this.db.insertInto("agent_project_config").values({
668
670
  agent: this.agentName,
669
671
  skills: JSON.stringify(skills),
672
+ envs: JSON.stringify(envs ?? {}),
670
673
  ...config,
671
674
  max_retry_attempts: maxRetryAttempts,
672
675
  retry_backoff_seconds: retryBackoffSeconds
673
676
  }).execute();
674
677
  }
675
- async updateAgentConfig({ skills, ...config }) {
678
+ async updateAgentConfig({ skills, envs, ...config }) {
676
679
  const maxRetryAttempts = config.max_retry_attempts;
677
680
  const retryBackoffSeconds = config.retry_backoff_seconds;
681
+ const resolvedEnvs = envs == null ? void 0 : JSON.stringify(envs);
678
682
  const result = await this.db.updateTable("agent_project_config").set({
679
683
  skills: JSON.stringify(skills),
684
+ ...resolvedEnvs == null ? {} : { envs: resolvedEnvs },
680
685
  ...config,
681
686
  ...maxRetryAttempts == null ? {} : { max_retry_attempts: maxRetryAttempts },
682
687
  ...retryBackoffSeconds == null ? {} : { retry_backoff_seconds: retryBackoffSeconds }
@@ -783,22 +788,25 @@ var TaskListTidbProvider = class extends TaskListProvider {
783
788
  async getAgentConfigs() {
784
789
  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();
785
790
  }
786
- async setAgentConfig({ skills, ...config }) {
791
+ async setAgentConfig({ skills, envs, ...config }) {
787
792
  const maxRetryAttempts = config.max_retry_attempts ?? 3;
788
793
  const retryBackoffSeconds = config.retry_backoff_seconds ?? 30;
789
794
  await this.db.insertInto("agent_project_config").values({
790
795
  agent: this.agentName,
791
796
  skills: JSON.stringify(skills),
797
+ envs: JSON.stringify(envs ?? {}),
792
798
  ...config,
793
799
  max_retry_attempts: maxRetryAttempts,
794
800
  retry_backoff_seconds: retryBackoffSeconds
795
801
  }).execute();
796
802
  }
797
- async updateAgentConfig({ skills, ...config }) {
803
+ async updateAgentConfig({ skills, envs, ...config }) {
798
804
  const maxRetryAttempts = config.max_retry_attempts;
799
805
  const retryBackoffSeconds = config.retry_backoff_seconds;
806
+ const resolvedEnvs = envs == null ? void 0 : JSON.stringify(envs);
800
807
  const result = await this.db.updateTable("agent_project_config").set({
801
808
  skills: JSON.stringify(skills),
809
+ ...resolvedEnvs == null ? {} : { envs: resolvedEnvs },
802
810
  ...config,
803
811
  ...maxRetryAttempts == null ? {} : { max_retry_attempts: maxRetryAttempts },
804
812
  ...retryBackoffSeconds == null ? {} : { retry_backoff_seconds: retryBackoffSeconds }
@@ -902,6 +910,20 @@ function normalizeSkills(value) {
902
910
  }
903
911
  return [];
904
912
  }
913
+ function normalizeEnvs(value) {
914
+ const parseRecord = (input) => {
915
+ if (!input || typeof input !== "object" || Array.isArray(input)) return {};
916
+ const result = {};
917
+ for (const [key, item] of Object.entries(input)) if (typeof item === "string") result[key] = item;
918
+ return result;
919
+ };
920
+ if (typeof value === "string") try {
921
+ return parseRecord(JSON.parse(value));
922
+ } catch {
923
+ return {};
924
+ }
925
+ return parseRecord(value);
926
+ }
905
927
  function buildConfigSetupStep(options) {
906
928
  const prototypeRepoUrl = normalizeRepoUrl(options.prototypeRepoUrl);
907
929
  const skillsArg = JSON.stringify(options.skills.join(","));
@@ -1664,7 +1686,9 @@ const projectSchema = z.object({
1664
1686
  created_at: zodJsonDate,
1665
1687
  updated_at: zodJsonDate,
1666
1688
  branches_total: z.number().int().nullable().optional(),
1667
- background_tasks_total: z.number().int().nullable().optional()
1689
+ background_tasks_total: z.number().int().nullable().optional(),
1690
+ env_id: z.string().uuid().nullable().optional(),
1691
+ env_display_name: z.string().nullable().optional()
1668
1692
  });
1669
1693
  const projectMessageSchema = z.object({
1670
1694
  id: z.string(),
@@ -1785,6 +1809,7 @@ const listProjectBackgroundTasks = defineApi(get`/api/v1/projects/${"projectId"}
1785
1809
  const createProjectExploration = defineApi(post`/api/v1/projects/${"projectId"}/explorations`, typeOf(), createExplorationResultSchema);
1786
1810
  const listProjectExplorations = defineApi(get`/api/v1/projects/${"projectId"}/explorations`, null, explorationSchema.array());
1787
1811
  const getProjectExplorationResult = defineApi(get`/api/v1/projects/${"projectId"}/explorations/${"explorationId"}/result`, null, explorationResultSchema);
1812
+ const killProjectBranch = defineApi(post`/api/v1/projects/${"projectId"}/branches/${"branchId"}/kill`, typeOf(), snapSchema.nullable());
1788
1813
  const getProjectBranchFs = defineApi(get`/api/v1/projects/${"projectId"}/branches/${"branchId"}/fs/${"path*"}`, null, FSResponseValidator);
1789
1814
  const getProjectBranchFsPreview = defineApi(get`/api/v1/projects/${"projectId"}/branches/${"branchId"}/preview/${"path*"}`, null, FSResponseValidator);
1790
1815
 
@@ -2073,15 +2098,33 @@ async function getPantheonBranchInfo({ projectId, branchId }) {
2073
2098
  branchId
2074
2099
  }, null);
2075
2100
  }
2076
- async function executeOnPantheon({ projectId, branchId, prompt, agent }) {
2101
+ async function executeOnPantheon({ projectId, branchId, prompt, agent, envs }) {
2077
2102
  const promptSequence = Array.isArray(prompt) ? prompt : [prompt];
2078
2103
  return (await executor.execute(createProjectExploration, { projectId }, {
2079
2104
  shared_prompt_sequence: promptSequence,
2080
2105
  num_branches: 1,
2081
2106
  agent,
2082
- parent_branch_id: branchId
2107
+ parent_branch_id: branchId,
2108
+ envs
2083
2109
  })).branches[0];
2084
2110
  }
2111
+ function isNotFoundError(error) {
2112
+ if (!(error instanceof Error)) return false;
2113
+ const message = error.message.toLowerCase();
2114
+ return message.includes("404") || message.includes("not found");
2115
+ }
2116
+ async function killPantheonBranchExecution({ projectId, branchId }) {
2117
+ try {
2118
+ await executor.execute(killProjectBranch, {
2119
+ projectId,
2120
+ branchId
2121
+ }, {});
2122
+ return [branchId];
2123
+ } catch (error) {
2124
+ if (isNotFoundError(error)) return [];
2125
+ throw error;
2126
+ }
2127
+ }
2085
2128
 
2086
2129
  //#endregion
2087
2130
  //#region src/core/task-list.ts
@@ -2226,6 +2269,7 @@ async function pollRunningTaskState(provider, state, logger) {
2226
2269
  execute_agent: config.execute_agent,
2227
2270
  role: config.role,
2228
2271
  skills: normalizeSkills(config.skills),
2272
+ envs: normalizeEnvs(config.envs),
2229
2273
  prototype_url: config.prototype_url
2230
2274
  });
2231
2275
  } else if (newStatus.state === "failed") {
@@ -2330,7 +2374,8 @@ async function startPendingTask(provider, task, config, logger) {
2330
2374
  projectId: taskToStart.project_id,
2331
2375
  branchId: taskToStart.base_branch_id,
2332
2376
  prompt: promptSequence,
2333
- agent: config.execute_agent
2377
+ agent: config.execute_agent,
2378
+ envs: normalizeEnvs(config.envs)
2334
2379
  });
2335
2380
  });
2336
2381
  logger.info(`Task ${taskToStart.id} started successfully on branch [id = ${taskBranch.branch_id}, snap_id = ${taskBranch.latest_snap_id}]`);
@@ -2807,6 +2852,7 @@ var WatchStepAggregator = class {
2807
2852
  //#region src/core/index.ts
2808
2853
  const DEFAULT_MAX_RETRY_ATTEMPTS = 3;
2809
2854
  const DEFAULT_RETRY_BACKOFF_SECONDS = 30;
2855
+ const DEFAULT_KILL_REASON = "Killed by user";
2810
2856
  async function runAgent(name, options, logger) {
2811
2857
  const agentDir = path.join(options.dataDir, "agents", name);
2812
2858
  const pidFile = path.join(agentDir, "pid");
@@ -2856,6 +2902,7 @@ async function configAgent(name, options) {
2856
2902
  return;
2857
2903
  }
2858
2904
  const resolvedSkills = options.skills ?? normalizeSkills(previousConfig?.skills);
2905
+ const resolvedEnvs = options.envs ?? normalizeEnvs(previousConfig?.envs);
2859
2906
  const resolvedExecuteAgent = options.executeAgent.trim() || previousConfig?.execute_agent || "codex";
2860
2907
  const resolvedPrototypeUrl = options.prototypeUrl.trim() || previousConfig?.prototype_url || "https://github.com/pingcap-inc/pantheon-agents";
2861
2908
  const baseBranchId = previousConfig ? previousConfig.base_branch_id : await resolveInitialBaseBranchId(options.projectId, options.rootBranchId);
@@ -2890,6 +2937,7 @@ async function configAgent(name, options) {
2890
2937
  execute_agent: resolvedExecuteAgent,
2891
2938
  role: resolvedRole ?? previousConfig.role,
2892
2939
  skills: resolvedSkills,
2940
+ envs: resolvedEnvs,
2893
2941
  prototype_url: resolvedPrototypeUrl
2894
2942
  });
2895
2943
  if (!options.prompt) await provider.updateTask({
@@ -2959,6 +3007,7 @@ async function configAgent(name, options) {
2959
3007
  execute_agent: resolvedExecuteAgent,
2960
3008
  role: resolvedRole,
2961
3009
  skills: resolvedSkills,
3010
+ envs: resolvedEnvs,
2962
3011
  prototype_url: resolvedPrototypeUrl
2963
3012
  });
2964
3013
  else await provider.updateAgentConfig({
@@ -2972,6 +3021,7 @@ async function configAgent(name, options) {
2972
3021
  execute_agent: resolvedExecuteAgent,
2973
3022
  role: resolvedRole ?? previousConfig.role,
2974
3023
  skills: resolvedSkills,
3024
+ envs: resolvedEnvs,
2975
3025
  prototype_url: resolvedPrototypeUrl
2976
3026
  });
2977
3027
  for (const pendingTask of pendingTasks) {
@@ -3064,6 +3114,170 @@ async function retryTask(agentName, taskId, reason) {
3064
3114
  await provider.close();
3065
3115
  }
3066
3116
  }
3117
+ function resolveTaskAttemptCount(value) {
3118
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
3119
+ return 1;
3120
+ }
3121
+ function hasTaskBranchId(task) {
3122
+ return "branch_id" in task && typeof task.branch_id === "string";
3123
+ }
3124
+ function collectKillBranchTargets(tasks, options = {}) {
3125
+ const childrenByParentId = /* @__PURE__ */ new Map();
3126
+ const taskById = /* @__PURE__ */ new Map();
3127
+ for (const task of tasks) {
3128
+ taskById.set(task.id, task);
3129
+ if (!task.parent_task_id) continue;
3130
+ const siblings = childrenByParentId.get(task.parent_task_id);
3131
+ if (siblings) siblings.push(task);
3132
+ else childrenByParentId.set(task.parent_task_id, [task]);
3133
+ }
3134
+ 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");
3135
+ if (roots.length === 0) return [];
3136
+ const maxDepthByTaskId = /* @__PURE__ */ new Map();
3137
+ const stack = roots.map((task) => ({
3138
+ taskId: task.id,
3139
+ depth: 0
3140
+ }));
3141
+ while (stack.length > 0) {
3142
+ const item = stack.pop();
3143
+ if (!item) continue;
3144
+ const previousDepth = maxDepthByTaskId.get(item.taskId);
3145
+ if (previousDepth != null && previousDepth >= item.depth) continue;
3146
+ maxDepthByTaskId.set(item.taskId, item.depth);
3147
+ const children = childrenByParentId.get(item.taskId) ?? [];
3148
+ for (const child of children) stack.push({
3149
+ taskId: child.id,
3150
+ depth: item.depth + 1
3151
+ });
3152
+ }
3153
+ const scoredTargets = [];
3154
+ for (const [taskId, depth] of maxDepthByTaskId) {
3155
+ const task = taskById.get(taskId);
3156
+ if (!task || !hasTaskBranchId(task)) continue;
3157
+ scoredTargets.push({
3158
+ task_id: task.id,
3159
+ project_id: task.project_id,
3160
+ branch_id: task.branch_id,
3161
+ depth,
3162
+ queued_at_ms: task.queued_at.getTime()
3163
+ });
3164
+ }
3165
+ scoredTargets.sort((a, b) => {
3166
+ if (a.depth !== b.depth) return b.depth - a.depth;
3167
+ return b.queued_at_ms - a.queued_at_ms;
3168
+ });
3169
+ const seen = /* @__PURE__ */ new Set();
3170
+ const targets = [];
3171
+ for (const target of scoredTargets) {
3172
+ const key = `${target.project_id}:${target.branch_id}`;
3173
+ if (seen.has(key)) continue;
3174
+ seen.add(key);
3175
+ targets.push({
3176
+ task_id: target.task_id,
3177
+ project_id: target.project_id,
3178
+ branch_id: target.branch_id
3179
+ });
3180
+ }
3181
+ return targets;
3182
+ }
3183
+ function collectTaskSubtree(tasks, rootTaskId) {
3184
+ const childrenByParentId = /* @__PURE__ */ new Map();
3185
+ const taskById = /* @__PURE__ */ new Map();
3186
+ for (const task of tasks) {
3187
+ taskById.set(task.id, task);
3188
+ if (!task.parent_task_id) continue;
3189
+ const siblings = childrenByParentId.get(task.parent_task_id);
3190
+ if (siblings) siblings.push(task);
3191
+ else childrenByParentId.set(task.parent_task_id, [task]);
3192
+ }
3193
+ if (!taskById.has(rootTaskId)) return [];
3194
+ const subtreeIds = /* @__PURE__ */ new Set();
3195
+ const stack = [rootTaskId];
3196
+ while (stack.length > 0) {
3197
+ const taskId = stack.pop();
3198
+ if (!taskId || subtreeIds.has(taskId)) continue;
3199
+ subtreeIds.add(taskId);
3200
+ const children = childrenByParentId.get(taskId) ?? [];
3201
+ for (const child of children) stack.push(child.id);
3202
+ }
3203
+ return tasks.filter((task) => subtreeIds.has(task.id));
3204
+ }
3205
+ function toCancelledTask(task, reason, cancelledAt) {
3206
+ return {
3207
+ status: "cancelled",
3208
+ id: task.id,
3209
+ task: task.task,
3210
+ type: task.type,
3211
+ project_id: task.project_id,
3212
+ base_branch_id: task.base_branch_id,
3213
+ parent_task_id: task.parent_task_id ?? null,
3214
+ config_version: task.config_version ?? null,
3215
+ attempt_count: resolveTaskAttemptCount(task.attempt_count),
3216
+ cancel_reason: reason,
3217
+ queued_at: task.queued_at,
3218
+ cancelled_at: cancelledAt
3219
+ };
3220
+ }
3221
+ async function killAgentTasks(agentName, taskId, reason = DEFAULT_KILL_REASON, deps = {}) {
3222
+ const provider = createTaskListProvider(agentName, pino());
3223
+ try {
3224
+ const tasks = await provider.getTasks({
3225
+ order_by: "queued_at",
3226
+ order_direction: "asc"
3227
+ });
3228
+ const subtreeTasks = collectTaskSubtree(tasks, taskId);
3229
+ if (subtreeTasks.length === 0) return null;
3230
+ const pendingTasks = subtreeTasks.filter((task) => task.status === "pending");
3231
+ const runningTasks = subtreeTasks.filter((task) => task.status === "running");
3232
+ for (const pendingTask of pendingTasks) await provider.cancelTask(pendingTask, reason);
3233
+ const killTargets = collectKillBranchTargets(tasks, { rootTaskIds: [taskId] });
3234
+ const killBranch = deps.killBranch ?? killPantheonBranchExecution;
3235
+ const killedBranches = [];
3236
+ const seenKilledBranchKeys = /* @__PURE__ */ new Set();
3237
+ const failedBranchKills = [];
3238
+ for (const target of killTargets) try {
3239
+ const deletedBranchIds = await killBranch({
3240
+ projectId: target.project_id,
3241
+ branchId: target.branch_id
3242
+ });
3243
+ for (const deletedBranchId of deletedBranchIds) {
3244
+ const branchKey = `${target.project_id}:${deletedBranchId}`;
3245
+ if (seenKilledBranchKeys.has(branchKey)) continue;
3246
+ seenKilledBranchKeys.add(branchKey);
3247
+ killedBranches.push({
3248
+ project_id: target.project_id,
3249
+ branch_id: deletedBranchId
3250
+ });
3251
+ }
3252
+ } catch (error) {
3253
+ const message = error instanceof Error ? error.message : String(error);
3254
+ failedBranchKills.push({
3255
+ task_id: target.task_id,
3256
+ project_id: target.project_id,
3257
+ branch_id: target.branch_id,
3258
+ error: message
3259
+ });
3260
+ }
3261
+ const now = /* @__PURE__ */ new Date();
3262
+ const killedBranchKeys = new Set(killedBranches.map((item) => `${item.project_id}:${item.branch_id}`));
3263
+ let runningCancelledCount = 0;
3264
+ for (const runningTask of runningTasks) {
3265
+ const branchKey = `${runningTask.project_id}:${runningTask.branch_id}`;
3266
+ if (!killedBranchKeys.has(branchKey)) continue;
3267
+ await provider.updateTask(toCancelledTask(runningTask, reason, now));
3268
+ runningCancelledCount++;
3269
+ }
3270
+ return {
3271
+ task_id: taskId,
3272
+ pending_cancelled_count: pendingTasks.length,
3273
+ running_cancelled_count: runningCancelledCount,
3274
+ killed_branches: killedBranches,
3275
+ failed_branch_kills: failedBranchKills
3276
+ };
3277
+ } finally {
3278
+ await provider.close();
3279
+ }
3280
+ }
3067
3281
  async function showAgentConfig(agentName, projectId) {
3068
3282
  const provider = createTaskListProvider(agentName, pino());
3069
3283
  try {
@@ -3364,8 +3578,25 @@ function parsePositiveInteger(value, fieldName) {
3364
3578
  if (!Number.isInteger(parsed) || parsed <= 0) throw new InvalidArgumentError(`${fieldName} must be a positive integer.`);
3365
3579
  return parsed;
3366
3580
  }
3581
+ function parseEnvs(value) {
3582
+ const trimmed = value.trim();
3583
+ if (!trimmed) throw new InvalidArgumentError("envs must be a JSON object of string values.");
3584
+ let parsed;
3585
+ try {
3586
+ parsed = JSON.parse(trimmed);
3587
+ } catch {
3588
+ throw new InvalidArgumentError("envs must be valid JSON.");
3589
+ }
3590
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new InvalidArgumentError("envs must be a JSON object.");
3591
+ const out = {};
3592
+ for (const [key, item] of Object.entries(parsed)) {
3593
+ if (typeof item !== "string") throw new InvalidArgumentError(`envs.${key} must be a string value.`);
3594
+ out[key] = item;
3595
+ }
3596
+ return out;
3597
+ }
3367
3598
  function createConfigAgentCommand(version, deps = {}) {
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() {
3599
+ 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").option("--envs <json>", "Pantheon branch envs in JSON object format, e.g. '{\"ANTHROPIC_MODEL\":\"claude-opus-4-5\"}'.", parseEnvs).action(async function() {
3369
3600
  const [name, prompt] = this.args;
3370
3601
  const options = this.opts();
3371
3602
  const resolvedProjectId = resolvePantheonProjectId(options.projectId);
@@ -3386,6 +3617,7 @@ function createConfigAgentCommand(version, deps = {}) {
3386
3617
  maxRetryAttempts: options.maxRetryAttempts,
3387
3618
  retryBackoffSeconds: options.retryBackoffSeconds,
3388
3619
  skills: options.skills,
3620
+ envs: options.envs,
3389
3621
  prototypeUrl: options.prototypeUrl,
3390
3622
  rootBranchId: resolvedRootBranchId
3391
3623
  });
@@ -3477,6 +3709,56 @@ function createGetTaskCommand(version, deps = {}) {
3477
3709
  });
3478
3710
  }
3479
3711
 
3712
+ //#endregion
3713
+ //#region src/cli/commands/kill.ts
3714
+ function createKillCommand(version, deps = {}) {
3715
+ 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() {
3716
+ const [name, taskId, reason] = this.args;
3717
+ const options = this.opts();
3718
+ if (!ensureEnv(["DATABASE_URL"])) return;
3719
+ const rl = options.yes || options.force ? null : readline.createInterface({
3720
+ input: process$1.stdin,
3721
+ output: process$1.stdout
3722
+ });
3723
+ try {
3724
+ if (rl) {
3725
+ if ((await rl.question(`Type the agent name (${name}) to confirm kill: `)).trim() !== name) {
3726
+ console.error("Confirmation failed. Agent name did not match.");
3727
+ process$1.exitCode = 1;
3728
+ return;
3729
+ }
3730
+ if ((await rl.question(`Type KILL ${taskId} to stop this task subtree and related branches: `)).trim() !== `KILL ${taskId}`) {
3731
+ console.error("Confirmation failed. Aborting kill.");
3732
+ process$1.exitCode = 1;
3733
+ return;
3734
+ }
3735
+ }
3736
+ const result = await (deps.killAgentTasks ?? killAgentTasks)(name, taskId, reason);
3737
+ if (!result) {
3738
+ console.error(`Task ${taskId} not found for agent ${name}.`);
3739
+ process$1.exitCode = 1;
3740
+ return;
3741
+ }
3742
+ const attemptedBranchKills = result.killed_branches.length + result.failed_branch_kills.length;
3743
+ if (result.pending_cancelled_count === 0 && attemptedBranchKills === 0) {
3744
+ console.log(`No actionable work found in task subtree ${taskId}.`);
3745
+ return;
3746
+ }
3747
+ 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).`);
3748
+ if (result.killed_branches.length > 0) console.log(`Killed ${result.killed_branches.length} branch(es).`);
3749
+ if (result.failed_branch_kills.length > 0) {
3750
+ for (const failed of result.failed_branch_kills) console.error(`Failed to kill branch ${failed.branch_id} in project ${failed.project_id}: ${failed.error}`);
3751
+ process$1.exitCode = 1;
3752
+ }
3753
+ } catch (error) {
3754
+ console.error(error instanceof Error ? error.message : String(error));
3755
+ process$1.exitCode = 1;
3756
+ } finally {
3757
+ rl?.close();
3758
+ }
3759
+ });
3760
+ }
3761
+
3480
3762
  //#endregion
3481
3763
  //#region src/cli/commands/run.ts
3482
3764
  function createRunAgentCommand(version) {
@@ -3539,7 +3821,8 @@ function createShowConfigCommand(version) {
3539
3821
  role: config.role,
3540
3822
  execute_agent: config.execute_agent,
3541
3823
  prototype_url: config.prototype_url,
3542
- skills: Array.isArray(config.skills) ? config.skills.join(", ") : String(config.skills)
3824
+ skills: Array.isArray(config.skills) ? config.skills.join(", ") : String(config.skills),
3825
+ envs: config.envs && typeof config.envs === "object" ? JSON.stringify(config.envs) : String(config.envs ?? "{}")
3543
3826
  }]);
3544
3827
  return;
3545
3828
  }
@@ -3565,7 +3848,8 @@ function createShowConfigCommand(version) {
3565
3848
  role: config.role,
3566
3849
  execute_agent: config.execute_agent,
3567
3850
  prototype_url: config.prototype_url,
3568
- skills: Array.isArray(config.skills) ? config.skills.join(", ") : String(config.skills)
3851
+ skills: Array.isArray(config.skills) ? config.skills.join(", ") : String(config.skills),
3852
+ envs: config.envs && typeof config.envs === "object" ? JSON.stringify(config.envs) : String(config.envs ?? "{}")
3569
3853
  })));
3570
3854
  });
3571
3855
  }
@@ -6239,7 +6523,7 @@ function createWatchStreamCommand(version) {
6239
6523
 
6240
6524
  //#endregion
6241
6525
  //#region src/cli/index.ts
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));
6526
+ 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));
6243
6527
  async function main() {
6244
6528
  if (process$1.argv.length <= 2) {
6245
6529
  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.1",
4
+ "version": "0.3.3",
5
5
  "bin": {
6
6
  "pantheon-agents": "dist/index.js"
7
7
  },