jules-orchestrator-kit 0.52.8 → 0.53.0

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.
@@ -1,6 +1,8 @@
1
1
  import { existsSync, readFileSync, writeFileSync, openSync, fsyncSync, closeSync, renameSync, mkdirSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parseYaml, TIER_PRESETS, VENDOR_TIERS, FALLBACK_TIER } from "./config.mjs";
4
+ import { suggestProvider } from "./provider-readiness.mjs";
5
+ import { PROFILE_NAMES } from "./profiles.mjs";
4
6
  import { detectStackOracles, runVerificationProbe } from "./wizard-oracle.mjs";
5
7
  import { select, multiSelect, input, confirm, spinner, isTTY } from "./tui.mjs";
6
8
  import { KIT_VERSION } from "./version.mjs";
@@ -158,18 +160,35 @@ export function planInit(root = process.cwd(), options = {}) {
158
160
 
159
161
  const selectedPresets = options.presets || existingConfig.presets || ["nightly-security-audit", "flaky-test-quarantine"];
160
162
 
163
+ // Scaffolding `provider: jules` into a repository whose operator has no Jules
164
+ // key — and a Claude Code or Codex CLI sitting on PATH — produced a config
165
+ // that could never dispatch, and gave no hint that three other providers were
166
+ // installed. Detection only fills a blank: an explicit option, and an
167
+ // existing config, both still win.
168
+ const provider =
169
+ options.provider || existingConfig.provider || suggestProvider({ env: options.env || process.env });
170
+
171
+ // How hard this repository wants its agents verified, in one word. Expanded
172
+ // into concrete stages at load time by `loadConfig`, never frozen here, so
173
+ // the gate list follows the stack instead of the day the repo was scaffolded.
174
+ const requestedProfile = String(options.profile || existingConfig.verify?.profile || "standard").toLowerCase();
175
+ const profile = PROFILE_NAMES.includes(requestedProfile) ? requestedProfile : "standard";
176
+
161
177
  const limitsBlock = isCustomLimits
162
178
  ? `\nlimits:\n concurrency: ${limits.concurrency}\n daily_tasks: ${limits.daily_tasks}\n stagger_ms: ${limits.stagger_ms}\n diff_kb: ${limits.diff_kb}\n`
163
179
  : "";
164
180
 
165
- const configYaml = `# Google Jules Orchestrator Kit Config (v${KIT_VERSION})
181
+ const configYaml = `# Agent Orchestrator Kit Config (v${KIT_VERSION})
182
+ # provider: jules | claude-code | codex | gemini-flash (agentctl providers)
166
183
  version: 1
167
- provider: ${existingConfig.provider || "jules"}
184
+ provider: ${provider}
168
185
  tier: ${tierName}
169
186
  base_branch: ${options.baseBranch || existingConfig.base_branch || "main"}
170
187
  branch_prefix: ${existingConfig.branch_prefix || "agent/"}
171
188
  ${limitsBlock}
172
189
  verify:
190
+ # minimal | standard | max — see: agentctl profile --list
191
+ profile: ${profile}
173
192
  test: "${verify.test}"
174
193
  build: "${verify.build}"
175
194
  lint: "${verify.lint}"
@@ -179,12 +198,19 @@ presets:
179
198
  ${selectedPresets.map((p) => ` - ${p}`).join("\n")}
180
199
  `;
181
200
 
201
+ // Universal defaults only. This list used to name `**/lock-manager/**` and
202
+ // `scripts/jules-self-audit.mjs` — paths that exist in the kit's own
203
+ // repository and in no other, so every project scaffolded by the wizard
204
+ // inherited two rules that could never match anything it contained, and none
205
+ // for the credentials it actually had.
182
206
  const forbiddenPaths = options.forbiddenPaths || existingJules.forbidden_paths || [
183
207
  ".github/**",
184
208
  "**/secrets/**",
185
209
  "**/*.pem",
186
- "**/lock-manager/**",
187
- "scripts/jules-self-audit.mjs",
210
+ "**/*.key",
211
+ "**/.env",
212
+ "**/.env.*",
213
+ ".agent/config.yml",
188
214
  ".agent/jules.yml",
189
215
  ];
190
216
  const allowPaths = options.allowPaths || existingJules.allow_paths || [];
@@ -201,6 +227,8 @@ allow_paths: ${allowPaths.length > 0 ? "\n" + allowPaths.map((p) => ` - "${p}"`
201
227
  return {
202
228
  stack: oracle.stack,
203
229
  tier: tierName,
230
+ provider,
231
+ profile,
204
232
  verify,
205
233
  limits,
206
234
  presets: selectedPresets,
@@ -1,220 +0,0 @@
1
- import { join, resolve } from "node:path";
2
- import { existsSync, readdirSync, readFileSync } from "node:fs";
3
- import { createHash, randomUUID } from "node:crypto";
4
- import { detectStack } from "../config.mjs";
5
-
6
- /**
7
- * @typedef {import("../ux/capabilities.mjs").ActionPlan} ActionPlan
8
- * @typedef {import("./doctor-registry.mjs").DoctorReport} DoctorReport
9
- *
10
- * @typedef {Object} FixPlanningContext
11
- * @property {string} root
12
- * @property {DoctorReport} report
13
- * @property {string[]} selectedFixIds
14
- * @property {Record<string, unknown>} [answers]
15
- */
16
-
17
- /**
18
- * Generate unified diff preview string for file creation/replacement.
19
- * @param {string} path
20
- * @param {string} oldContent
21
- * @param {string} newContent
22
- * @returns {string}
23
- */
24
- function createDiffPreview(path, oldContent, newContent) {
25
- const oldLines = oldContent ? oldContent.split("\n") : [];
26
- const newLines = newContent ? newContent.split("\n") : [];
27
- const lines = [];
28
-
29
- lines.push(`--- a/${path}`);
30
- lines.push(`+++ b/${path}`);
31
- lines.push(`@@ -1,${oldLines.length || 0} +1,${newLines.length || 0} @@`);
32
-
33
- for (const line of oldLines) {
34
- lines.push(`-${line}`);
35
- }
36
- for (const line of newLines) {
37
- lines.push(`+${line}`);
38
- }
39
-
40
- return lines.join("\n");
41
- }
42
-
43
- function deepFreeze(value) {
44
- if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
45
- for (const child of Object.values(value)) deepFreeze(child);
46
- return Object.freeze(value);
47
- }
48
-
49
- function finalizePlan(plan) {
50
- const { planHash: _oldHash, ...unsigned } = plan;
51
- const planHash = "sha256:" + createHash("sha256").update(JSON.stringify(unsigned)).digest("hex");
52
- return deepFreeze({ ...unsigned, planHash });
53
- }
54
-
55
- /**
56
- * Plan diagnostic fixes into immutable ActionPlan objects.
57
- * @param {FixPlanningContext} context
58
- * @returns {Promise<ActionPlan[]>}
59
- */
60
- export async function planDiagnosticFixes(context = {}) {
61
- const root = resolve(context.root || process.cwd());
62
- const report = context.report || { results: [] };
63
- const selectedFixIds = Array.isArray(context.selectedFixIds) ? context.selectedFixIds : ["safe"];
64
-
65
- const wantSafeOnly = selectedFixIds.includes("safe");
66
- /** @type {ActionPlan[]} */
67
- const plans = [];
68
-
69
- for (const result of report.results || []) {
70
- for (const fix of result.fixes || []) {
71
- const isSelected = selectedFixIds.includes(fix.id) || (wantSafeOnly && fix.automatic);
72
- if (!isSelected) continue;
73
-
74
- if (fix.id === "config.create-default") {
75
- const configPath = ".agent/config.yml";
76
- // Detected, not assumed. Writing `npm test` into a Rust or Python
77
- // repository's config produced a manifest whose first verification run
78
- // could only fail, and the stack detector already has the answer.
79
- const detected = detectStack(root);
80
- const newContent = `# jules-orchestrator-kit configuration manifest
81
- version: 1
82
- # Free-tier limits until you state your plan (free | pro | ultra).
83
- tier: free
84
- verify:
85
- test: ${JSON.stringify(detected.testCmd || "")}
86
- build: ${JSON.stringify(detected.buildCmd || "")}
87
- `;
88
- const diff = createDiffPreview(configPath, "", newContent);
89
- const planId = `PLAN-${Date.now()}-${randomUUID().slice(0, 8)}`;
90
-
91
- plans.push({
92
- schema: "agentctl/action-plan-v1",
93
- id: planId,
94
- kind: "config.create-default",
95
- title: "Create default .agent/config.yml",
96
- summary: "Generate initial configuration manifest for jules-orchestrator-kit",
97
- risk: "low",
98
- repository: root,
99
- createdAt: new Date().toISOString(),
100
- preconditions: [],
101
- fileMutations: [
102
- {
103
- operation: "create",
104
- path: configPath,
105
- newContent,
106
- },
107
- ],
108
- commandEffects: [],
109
- stateTransitions: [],
110
- preview: {
111
- unifiedDiff: diff,
112
- warnings: [],
113
- estimatedImpact: ["Creates .agent/config.yml"],
114
- },
115
- confirmation: {
116
- mode: "none",
117
- prompt: "Apply default configuration?",
118
- },
119
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
120
- });
121
- } else if (fix.id === "state.rebuild-head") {
122
- const dateStr = new Date().toISOString().split("T")[0];
123
- const stateDir = join(root, ".agent", "state");
124
- const headPath = `.agent/state/telemetry-${dateStr}.head`;
125
- let tailHash = "0".repeat(64);
126
- let segment = 0;
127
- try {
128
- const telemetryFiles = readdirSync(stateDir)
129
- .filter((name) => name.startsWith(`telemetry-${dateStr}`) && name.endsWith(".jsonl"))
130
- .sort((a, b) => {
131
- const index = (name) => Number(name.match(/-(\d+)\.jsonl$/)?.[1] || 0);
132
- return index(a) - index(b);
133
- });
134
- const lastFile = telemetryFiles.at(-1);
135
- if (lastFile) {
136
- const lines = readFileSync(join(stateDir, lastFile), "utf-8").split("\n").filter(Boolean);
137
- const lastEntry = JSON.parse(lines.at(-1));
138
- if (typeof lastEntry?.hash === "string") tailHash = lastEntry.hash;
139
- const segmentMatch = lastFile.match(/-(\d+)\.jsonl$/);
140
- segment = segmentMatch ? Number(segmentMatch[1]) : 0;
141
- }
142
- } catch (_) {}
143
- const newContent = JSON.stringify({ v: 1, hash: tailHash, segment, ts: new Date().toISOString() });
144
- const planId = `PLAN-${Date.now()}-${randomUUID().slice(0, 8)}`;
145
-
146
- plans.push({
147
- schema: "agentctl/action-plan-v1",
148
- id: planId,
149
- kind: "state.rebuild-head",
150
- title: "Rebuild telemetry spine head pointer",
151
- summary: "Re-initialize .head pointer for telemetry spine ledger",
152
- risk: "low",
153
- repository: root,
154
- createdAt: new Date().toISOString(),
155
- preconditions: [],
156
- fileMutations: [
157
- {
158
- operation: "create",
159
- path: headPath,
160
- newContent,
161
- },
162
- ],
163
- commandEffects: [],
164
- stateTransitions: [],
165
- preview: {
166
- unifiedDiff: createDiffPreview(headPath, "", newContent),
167
- warnings: [],
168
- estimatedImpact: [`Creates ${headPath} pointer`],
169
- },
170
- confirmation: {
171
- mode: "none",
172
- prompt: "Rebuild telemetry head pointer?",
173
- },
174
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
175
- });
176
- } else if (fix.id === "locks.prune-stale") {
177
- const locksDir = join(root, ".agent", "state", "locks");
178
- if (existsSync(locksDir)) {
179
- const lockFiles = readdirSync(locksDir).filter((f) => f.endsWith(".json"));
180
- for (const lockFile of lockFiles) {
181
- const lockRelPath = `.agent/state/locks/${lockFile}`;
182
- const planId = `PLAN-${Date.now()}-${randomUUID().slice(0, 8)}`;
183
-
184
- plans.push({
185
- schema: "agentctl/action-plan-v1",
186
- id: planId,
187
- kind: "locks.prune-stale",
188
- title: `Remove stale lock ${lockFile}`,
189
- summary: `Prune inactive VFS lock file ${lockRelPath}`,
190
- risk: "low",
191
- repository: root,
192
- createdAt: new Date().toISOString(),
193
- preconditions: [],
194
- fileMutations: [
195
- {
196
- operation: "delete",
197
- path: lockRelPath,
198
- },
199
- ],
200
- commandEffects: [],
201
- stateTransitions: [],
202
- preview: {
203
- unifiedDiff: `--- a/${lockRelPath}\n+++ /dev/null\n`,
204
- warnings: [],
205
- estimatedImpact: [`Removes ${lockRelPath}`],
206
- },
207
- confirmation: {
208
- mode: "none",
209
- prompt: `Remove lock ${lockFile}?`,
210
- },
211
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
212
- });
213
- }
214
- }
215
- }
216
- }
217
- }
218
-
219
- return plans.map(finalizePlan);
220
- }
@@ -1,171 +0,0 @@
1
- import {
2
- readFileSync,
3
- mkdirSync,
4
- existsSync,
5
- openSync,
6
- writeSync,
7
- fsyncSync,
8
- closeSync,
9
- renameSync,
10
- unlinkSync,
11
- } from "node:fs";
12
- import { join, resolve, relative, isAbsolute } from "node:path";
13
- import { createHash, randomUUID } from "node:crypto";
14
- import { redactSecrets } from "../security.mjs";
15
-
16
- /**
17
- * @typedef {Object} OperationReceipt
18
- * @property {"agentctl/operation-receipt-v1"} schema
19
- * @property {string} receiptId
20
- * @property {string} actionPlanId
21
- * @property {string} kind
22
- * @property {string} title
23
- * @property {string} repository
24
- * @property {string} appliedAt
25
- * @property {"success" | "rolled-back" | "failed"} status
26
- * @property {number} durationMs
27
- * @property {Array<{ path: string, operation: string, hashBefore?: string, hashAfter?: string }>} mutationsApplied
28
- * @property {Array<{ executable: string, args: string[], exitCode: number }>} effectsExecuted
29
- * @property {string} receiptHash
30
- */
31
-
32
- /**
33
- * Compute SHA-256 hash of operation receipt metadata.
34
- * @param {Omit<OperationReceipt, "receiptHash">} receipt
35
- * @returns {string}
36
- */
37
- export function computeReceiptHash(receipt) {
38
- const payload = JSON.stringify({
39
- schema: receipt.schema,
40
- receiptId: receipt.receiptId,
41
- actionPlanId: receipt.actionPlanId,
42
- kind: receipt.kind,
43
- title: receipt.title,
44
- repository: receipt.repository,
45
- status: receipt.status,
46
- appliedAt: receipt.appliedAt,
47
- durationMs: receipt.durationMs,
48
- mutationsApplied: receipt.mutationsApplied,
49
- effectsExecuted: receipt.effectsExecuted,
50
- });
51
- return "sha256:" + createHash("sha256").update(payload).digest("hex");
52
- }
53
-
54
- function redactReceiptValue(value) {
55
- if (typeof value === "string") return redactSecrets(value);
56
- if (Array.isArray(value)) return value.map(redactReceiptValue);
57
- if (value && typeof value === "object") {
58
- return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, redactReceiptValue(child)]));
59
- }
60
- return value;
61
- }
62
-
63
- function assertReceiptId(receiptId) {
64
- if (typeof receiptId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(receiptId)) {
65
- throw new Error("Invalid operation receipt id");
66
- }
67
- }
68
-
69
- function writeFileAtomically(filePath, content) {
70
- const tmpPath = `${filePath}.${randomUUID()}.tmp`;
71
- let fd;
72
- try {
73
- fd = openSync(tmpPath, "wx", 0o600);
74
- writeSync(fd, content, "utf-8");
75
- fsyncSync(fd);
76
- closeSync(fd);
77
- fd = undefined;
78
- renameSync(tmpPath, filePath);
79
- } catch (err) {
80
- if (fd !== undefined) {
81
- try { closeSync(fd); } catch (_) {}
82
- }
83
- try { unlinkSync(tmpPath); } catch (_) {}
84
- throw err;
85
- }
86
- }
87
-
88
- /**
89
- * Create and persist an OperationReceipt atomically.
90
- * @param {string} root
91
- * @param {Omit<OperationReceipt, "schema" | "receiptId" | "appliedAt" | "receiptHash"> & { receiptId?: string }} data
92
- * @returns {OperationReceipt}
93
- */
94
- export function createOperationReceipt(root, data) {
95
- const receiptsDir = join(root, ".agent", "receipts");
96
- if (!existsSync(receiptsDir)) {
97
- mkdirSync(receiptsDir, { recursive: true });
98
- }
99
-
100
- const receiptId = data.receiptId || `REC-${Date.now()}-${randomUUID().slice(0, 8)}`;
101
- assertReceiptId(receiptId);
102
- const appliedAt = new Date().toISOString();
103
-
104
- const baseReceipt = redactReceiptValue({
105
- schema: /** @type {const} */ ("agentctl/operation-receipt-v1"),
106
- receiptId,
107
- actionPlanId: data.actionPlanId,
108
- kind: data.kind,
109
- title: data.title,
110
- repository: data.repository || "local",
111
- appliedAt,
112
- status: data.status,
113
- durationMs: data.durationMs || 0,
114
- mutationsApplied: data.mutationsApplied || [],
115
- effectsExecuted: data.effectsExecuted || [],
116
- });
117
-
118
- const receiptHash = computeReceiptHash(baseReceipt);
119
- /** @type {OperationReceipt} */
120
- const fullReceipt = {
121
- ...baseReceipt,
122
- receiptHash,
123
- };
124
-
125
- const targetPath = join(receiptsDir, `${receiptId}.json`);
126
- writeFileAtomically(targetPath, JSON.stringify(fullReceipt, null, 2));
127
-
128
- return fullReceipt;
129
- }
130
-
131
- /**
132
- * Load and validate an OperationReceipt from disk.
133
- * @param {string} root
134
- * @param {string} receiptIdOrPath
135
- * @returns {OperationReceipt}
136
- */
137
- export function loadOperationReceipt(root, receiptIdOrPath) {
138
- const receiptsDir = resolve(root, ".agent", "receipts");
139
- let targetPath;
140
- if (typeof receiptIdOrPath !== "string" || !receiptIdOrPath) {
141
- throw new Error("Operation receipt id or path is required");
142
- }
143
-
144
- if (!receiptIdOrPath.endsWith(".json")) {
145
- assertReceiptId(receiptIdOrPath);
146
- targetPath = join(receiptsDir, `${receiptIdOrPath}.json`);
147
- } else {
148
- targetPath = isAbsolute(receiptIdOrPath) ? resolve(receiptIdOrPath) : resolve(root, receiptIdOrPath);
149
- const rel = relative(receiptsDir, targetPath);
150
- if (rel.startsWith("..") || isAbsolute(rel)) {
151
- throw new Error("Operation receipt path escapes the receipts directory");
152
- }
153
- }
154
-
155
- if (!existsSync(targetPath)) {
156
- throw new Error(`Operation receipt not found: ${targetPath}`);
157
- }
158
-
159
- const content = readFileSync(targetPath, "utf-8");
160
- /** @type {OperationReceipt} */
161
- const receipt = JSON.parse(content);
162
-
163
- if (receipt.schema !== "agentctl/operation-receipt-v1") {
164
- throw new Error(`Invalid receipt schema version: ${receipt.schema}`);
165
- }
166
- if (receipt.receiptHash !== computeReceiptHash(receipt)) {
167
- throw new Error(`Invalid operation receipt hash: ${targetPath}`);
168
- }
169
-
170
- return receipt;
171
- }
@@ -1,156 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
-
3
- /**
4
- * @typedef {import("../ux/capabilities.mjs").ActionPlan} ActionPlan
5
- * @typedef {import("../ux/swarm-model.mjs").SwarmSlot} SwarmSlot
6
- */
7
-
8
- /**
9
- * Plan swarm concurrency action into an immutable ActionPlan.
10
- * @param {{ slots: SwarmSlot[], revision: string }} snapshot
11
- * @param {Object} intent
12
- * @param {string} intent.kind
13
- * @param {string} [intent.slotId]
14
- * @param {string} [intent.taskId]
15
- * @param {Record<string, any>} [intent.answers]
16
- * @returns {Promise<ActionPlan>}
17
- */
18
- export async function planSwarmAction(snapshot, intent) {
19
- const planId = `PLAN-SWARM-${Date.now()}-${randomUUID().slice(0, 8)}`;
20
-
21
- if (intent.kind === "pause-scheduler" || intent.kind === "resume-scheduler") {
22
- const isPause = intent.kind === "pause-scheduler";
23
- const schedulerRelPath = ".agent/state/swarm/scheduler.json";
24
- const newContent = JSON.stringify(
25
- {
26
- schema: "agentctl/swarm-scheduler-v1",
27
- paused: isPause,
28
- updatedAt: new Date().toISOString(),
29
- },
30
- null,
31
- 2
32
- );
33
-
34
- return {
35
- schema: "agentctl/action-plan-v1",
36
- id: planId,
37
- kind: intent.kind,
38
- title: `${isPause ? "Pause" : "Resume"} Swarm Scheduler`,
39
- summary: `${isPause ? "Halt dispatching new tasks to swarm slots" : "Resume task dispatching to available slots"}`,
40
- risk: "moderate",
41
- repository: "local",
42
- createdAt: new Date().toISOString(),
43
- preconditions: [],
44
- fileMutations: [
45
- {
46
- operation: "replace",
47
- path: schedulerRelPath,
48
- newContent,
49
- },
50
- ],
51
- commandEffects: [],
52
- stateTransitions: [],
53
- preview: {
54
- unifiedDiff: `--- a/${schedulerRelPath}\n+++ b/${schedulerRelPath}\n@@ -paused @@\n- paused: ${!isPause}\n+ paused: ${isPause}\n`,
55
- warnings: isPause ? ["Running tasks will complete, but no new tasks will be scheduled."] : [],
56
- estimatedImpact: [`Swarm scheduler state set to ${isPause ? "PAUSED" : "RUNNING"}`],
57
- },
58
- confirmation: {
59
- mode: "keypress",
60
- prompt: `Confirm ${isPause ? "pause" : "resume"} of swarm scheduler?`,
61
- },
62
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
63
- };
64
- }
65
-
66
- if (intent.kind === "cancel-slot-task") {
67
- const slotId = intent.slotId;
68
- const slot = (snapshot.slots || []).find((s) => s.id === slotId || s.taskId === intent.taskId);
69
- if (!slot) {
70
- throw new Error(`Target slot ${slotId || intent.taskId} not found.`);
71
- }
72
-
73
- const slotRelPath = `.agent/state/swarm/${slot.id}.json`;
74
- const newContent = JSON.stringify(
75
- {
76
- id: slot.id,
77
- state: "draining",
78
- taskId: slot.taskId,
79
- cancelledAt: new Date().toISOString(),
80
- },
81
- null,
82
- 2
83
- );
84
-
85
- return {
86
- schema: "agentctl/action-plan-v1",
87
- id: planId,
88
- kind: "swarm.cancel-slot-task",
89
- title: `Cancel active slot task ${slot.taskId || slot.id}`,
90
- summary: `Send cancellation signal to slot ${slot.id} (Task ${slot.taskId || "N/A"})`,
91
- risk: "high",
92
- repository: "local",
93
- createdAt: new Date().toISOString(),
94
- preconditions: [],
95
- fileMutations: [
96
- {
97
- operation: "replace",
98
- path: slotRelPath,
99
- newContent,
100
- },
101
- ],
102
- commandEffects: [],
103
- stateTransitions: [{ taskId: slot.taskId, from: slot.state, to: "cancelled" }],
104
- preview: {
105
- unifiedDiff: `--- a/${slotRelPath}\n+++ b/${slotRelPath}\n@@ -state @@\n- state: ${slot.state}\n+ state: draining\n`,
106
- warnings: ["Cancelling a running worker process will terminate its active attempt."],
107
- estimatedImpact: [`Cancels execution in slot ${slot.id}`],
108
- },
109
- confirmation: {
110
- mode: "typed-task-id",
111
- prompt: `Type '${slot.taskId || slot.id}' to confirm cancellation:`,
112
- expected: slot.taskId || slot.id,
113
- },
114
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
115
- };
116
- }
117
-
118
- if (intent.kind === "prune-stale-slots") {
119
- const staleSlots = (snapshot.slots || []).filter((s) => s.state === "stale");
120
- if (staleSlots.length === 0) {
121
- throw new Error("No stale slots found to prune.");
122
- }
123
-
124
- const fileMutations = staleSlots.map((s) => ({
125
- operation: /** @type {const} */ ("delete"),
126
- path: `.agent/state/swarm/${s.id}.json`,
127
- }));
128
-
129
- return {
130
- schema: "agentctl/action-plan-v1",
131
- id: planId,
132
- kind: "swarm.prune-stale-slots",
133
- title: `Prune ${staleSlots.length} stale swarm slot(s)`,
134
- summary: `Remove stale slot state files for dead worker process PIDs`,
135
- risk: "low",
136
- repository: "local",
137
- createdAt: new Date().toISOString(),
138
- preconditions: [],
139
- fileMutations,
140
- commandEffects: [],
141
- stateTransitions: [],
142
- preview: {
143
- unifiedDiff: staleSlots.map((s) => `--- a/.agent/state/swarm/${s.id}.json\n+++ /dev/null\n`).join("\n"),
144
- warnings: [],
145
- estimatedImpact: [`Prunes ${staleSlots.length} stale slot file(s)`],
146
- },
147
- confirmation: {
148
- mode: "keypress",
149
- prompt: `Prune ${staleSlots.length} stale slot(s)?`,
150
- },
151
- planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
152
- };
153
- }
154
-
155
- throw new Error(`Unsupported swarm action intent: ${intent.kind}`);
156
- }