jules-orchestrator-kit 0.29.1 → 0.31.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.
@@ -0,0 +1,167 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { git, changedFiles, diffText } from "../git.mjs";
4
+ import { resolveRoot } from "../config.mjs";
5
+
6
+ export class CheckpointError extends Error {
7
+ constructor(message, opts = {}) {
8
+ super(message);
9
+ this.name = "CheckpointError";
10
+ this.code = opts.code || 1;
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Ensures checkpoint storage directory exists.
16
+ */
17
+ export function getCheckpointDir(root = resolveRoot()) {
18
+ const dir = join(root, ".agent", "state", "checkpoints");
19
+ try {
20
+ mkdirSync(dir, { recursive: true });
21
+ } catch (_) {}
22
+ return dir;
23
+ }
24
+
25
+ /**
26
+ * Creates an atomic pre-flight snapshot of the repository state before an agent task runs.
27
+ * @param {string} sessionId
28
+ * @param {object} options
29
+ * @returns {object} Checkpoint metadata
30
+ */
31
+ export function createCheckpoint(sessionId = `session-${Date.now()}`, options = {}) {
32
+ const root = options.root || resolveRoot();
33
+ const dir = getCheckpointDir(root);
34
+
35
+ let headSha = "";
36
+ try {
37
+ headSha = git(["rev-parse", "HEAD"], { cwd: root, ignoreError: true }).trim();
38
+ } catch (_) {}
39
+
40
+ let branch = "";
41
+ try {
42
+ branch = git(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root, ignoreError: true }).trim();
43
+ } catch (_) {}
44
+
45
+ const uncommittedFiles = changedFiles(root, branch || "main", "working-tree");
46
+ const diffContent = diffText(root, branch || "main", "working-tree");
47
+
48
+ const snapshot = {
49
+ version: 1,
50
+ id: sessionId,
51
+ timestamp: new Date().toISOString(),
52
+ headSha,
53
+ branch,
54
+ uncommittedFiles,
55
+ diffContent,
56
+ };
57
+
58
+ const snapshotPath = join(dir, `${sessionId}.json`);
59
+ writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2), "utf-8");
60
+
61
+ // Keep last 10 checkpoints
62
+ pruneCheckpoints(root, 10);
63
+
64
+ return snapshot;
65
+ }
66
+
67
+ /**
68
+ * Restores working tree and git state to a previously saved checkpoint.
69
+ * @param {string} sessionId or '--latest'
70
+ * @param {object} options
71
+ * @returns {object} Restore result summary
72
+ */
73
+ export function restoreCheckpoint(sessionId = "--latest", options = {}) {
74
+ const root = options.root || resolveRoot();
75
+ const dir = getCheckpointDir(root);
76
+
77
+ let targetId = sessionId;
78
+ if (!targetId || targetId === "--latest" || targetId === "latest") {
79
+ const list = listCheckpoints(root);
80
+ if (list.length === 0) {
81
+ throw new CheckpointError("No checkpoints found to restore.");
82
+ }
83
+ targetId = list[0].id;
84
+ }
85
+
86
+ const snapshotFile = join(dir, `${targetId}.json`);
87
+ if (!existsSync(snapshotFile)) {
88
+ throw new CheckpointError(`Checkpoint snapshot file not found for session '${targetId}'.`);
89
+ }
90
+
91
+ let snapshot;
92
+ try {
93
+ snapshot = JSON.parse(readFileSync(snapshotFile, "utf-8"));
94
+ } catch (err) {
95
+ throw new CheckpointError(`Corrupted checkpoint file '${targetId}.json': ${err.message}`);
96
+ }
97
+
98
+ if (snapshot.headSha) {
99
+ try {
100
+ git(["reset", "--hard", snapshot.headSha], { cwd: root, ignoreError: true });
101
+ } catch (err) {
102
+ throw new CheckpointError(`Failed git reset --hard ${snapshot.headSha}: ${err.message}`);
103
+ }
104
+ }
105
+
106
+ // Clean untracked files generated during session
107
+ try {
108
+ git(["clean", "-fd"], { cwd: root, ignoreError: true });
109
+ } catch (_) {}
110
+
111
+ return {
112
+ ok: true,
113
+ id: snapshot.id,
114
+ headSha: snapshot.headSha,
115
+ restoredAt: new Date().toISOString(),
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Lists all active checkpoints ordered by timestamp descending.
121
+ */
122
+ export function listCheckpoints(root = resolveRoot()) {
123
+ const dir = getCheckpointDir(root);
124
+ if (!existsSync(dir)) return [];
125
+
126
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
127
+ const items = [];
128
+
129
+ for (const file of files) {
130
+ const filePath = join(dir, file);
131
+ try {
132
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
133
+ items.push({
134
+ id: data.id || file.replace(/\.json$/, ""),
135
+ timestamp: data.timestamp || new Date(statSync(filePath).mtimeMs).toISOString(),
136
+ headSha: data.headSha || "",
137
+ branch: data.branch || "",
138
+ filePath,
139
+ });
140
+ } catch (_) {}
141
+ }
142
+
143
+ items.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
144
+ return items;
145
+ }
146
+
147
+ /**
148
+ * Prunes checkpoints keeping only the most recent N sessions.
149
+ */
150
+ export function pruneCheckpoints(root = resolveRoot(), maxRetention = 10) {
151
+ const list = listCheckpoints(root);
152
+ if (list.length <= maxRetention) return 0;
153
+
154
+ const toRemove = list.slice(maxRetention);
155
+ let prunedCount = 0;
156
+
157
+ for (const item of toRemove) {
158
+ try {
159
+ if (existsSync(item.filePath)) {
160
+ rmSync(item.filePath, { force: true });
161
+ prunedCount++;
162
+ }
163
+ } catch (_) {}
164
+ }
165
+
166
+ return prunedCount;
167
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * @typedef {"low" | "moderate" | "high" | "destructive"} RiskLevel
3
+ * @typedef {"Create" | "Inspect" | "Operate" | "Repair" | "Configure"} CommandCategory
4
+ *
5
+ * @typedef {Object} CommandFlag
6
+ * @property {string} name
7
+ * @property {"boolean" | "string"} type
8
+ * @property {string} description
9
+ * @property {any} [default]
10
+ *
11
+ * @typedef {Object} CommandDescriptor
12
+ * @property {string} id
13
+ * @property {string[]} path
14
+ * @property {string} title
15
+ * @property {string} description
16
+ * @property {CommandCategory} category
17
+ * @property {boolean} mutates
18
+ * @property {RiskLevel} risk
19
+ * @property {"never" | "optional" | "authoring-auto"} interactive
20
+ * @property {boolean} requiresRepository
21
+ * @property {string[]} shortcuts
22
+ * @property {string[]} examples
23
+ * @property {CommandFlag[]} flags
24
+ */
25
+
26
+ /** @type {CommandDescriptor[]} */
27
+ export const COMMAND_REGISTRY = [
28
+ {
29
+ id: "doctor",
30
+ path: ["doctor"],
31
+ title: "doctor",
32
+ description: "Run repository diagnostics and guided fixes",
33
+ category: "Repair",
34
+ mutates: false,
35
+ risk: "low",
36
+ interactive: "optional",
37
+ requiresRepository: true,
38
+ shortcuts: ["d", "doc"],
39
+ examples: [
40
+ "agentctl doctor",
41
+ "agentctl doctor --interactive",
42
+ "agentctl doctor --fix safe --yes",
43
+ "agentctl doctor --json",
44
+ ],
45
+ flags: [
46
+ { name: "interactive", type: "boolean", description: "Open full-screen diagnostic matrix" },
47
+ { name: "fix", type: "string", description: "Apply fix classes (e.g. 'safe' or check IDs)" },
48
+ { name: "probe", type: "boolean", description: "Enable active network/execution probes" },
49
+ { name: "json", type: "boolean", description: "Output structured JSON doctor report" },
50
+ { name: "yes", type: "boolean", description: "Bypass interactive confirmation for safe fixes" },
51
+ ],
52
+ },
53
+ {
54
+ id: "queue",
55
+ path: ["queue"],
56
+ title: "queue",
57
+ description: "Browse and manage canonical task queue",
58
+ category: "Operate",
59
+ mutates: false,
60
+ risk: "low",
61
+ interactive: "optional",
62
+ requiresRepository: true,
63
+ shortcuts: ["q"],
64
+ examples: [
65
+ "agentctl queue",
66
+ "agentctl queue --interactive",
67
+ "agentctl queue --json",
68
+ ],
69
+ flags: [
70
+ { name: "interactive", type: "boolean", description: "Open full-screen queue dashboard" },
71
+ { name: "json", type: "boolean", description: "Output structured JSON queue snapshot" },
72
+ { name: "limit", type: "string", description: "Maximum tasks to include in snapshot" },
73
+ ],
74
+ },
75
+ {
76
+ id: "swarm",
77
+ path: ["swarm"],
78
+ title: "swarm",
79
+ description: "Inspect active worker slots and concurrency scheduler",
80
+ category: "Operate",
81
+ mutates: false,
82
+ risk: "low",
83
+ interactive: "optional",
84
+ requiresRepository: true,
85
+ shortcuts: ["s"],
86
+ examples: [
87
+ "agentctl swarm",
88
+ "agentctl swarm --interactive",
89
+ "agentctl swarm --json",
90
+ ],
91
+ flags: [
92
+ { name: "interactive", type: "boolean", description: "Open full-screen swarm dashboard" },
93
+ { name: "json", type: "boolean", description: "Output structured JSON swarm snapshot" },
94
+ ],
95
+ },
96
+ {
97
+ id: "task-create",
98
+ path: ["task", "create"],
99
+ title: "task create",
100
+ description: "Author a scoped, falsifiable task",
101
+ category: "Create",
102
+ mutates: true,
103
+ risk: "moderate",
104
+ interactive: "authoring-auto",
105
+ requiresRepository: true,
106
+ shortcuts: ["tc", "create"],
107
+ examples: [
108
+ 'agentctl task create --title "Fix webhook" --prompt "Add retry handling" --verify-cmd "npm test"',
109
+ "agentctl task create --interactive",
110
+ ],
111
+ flags: [
112
+ { name: "title", type: "string", description: "Short title for task" },
113
+ { name: "prompt", type: "string", description: "Detailed task instructions" },
114
+ { name: "verify-cmd", type: "string", description: "Verification command" },
115
+ { name: "interactive", type: "boolean", description: "Launch interactive task wizard" },
116
+ ],
117
+ },
118
+ {
119
+ id: "task-optimize",
120
+ path: ["task", "optimize"],
121
+ title: "task optimize",
122
+ description: "Score task prompt falsifiability and static path resolution",
123
+ category: "Inspect",
124
+ mutates: false,
125
+ risk: "low",
126
+ interactive: "never",
127
+ requiresRepository: true,
128
+ shortcuts: ["to", "optimize"],
129
+ examples: [
130
+ 'agentctl task optimize "Fix JWT token expiry in src/auth.js"',
131
+ 'agentctl task optimize "Refactor auth" --fix',
132
+ 'agentctl task optimize --file prompt.txt --json',
133
+ ],
134
+ flags: [
135
+ { name: "fix", type: "boolean", description: "Synthesize optimized markdown task envelope" },
136
+ { name: "json", type: "boolean", description: "Output structured JSON prompt evaluation" },
137
+ { name: "file", type: "string", description: "Path to text file containing task prompt" },
138
+ { name: "dir", type: "string", description: "Target repository directory root" },
139
+ ],
140
+ },
141
+ {
142
+ id: "init",
143
+ path: ["init"],
144
+ title: "init",
145
+ description: "Configure Stack Oracle and Jules provider manifests",
146
+ category: "Configure",
147
+ mutates: true,
148
+ risk: "moderate",
149
+ interactive: "optional",
150
+ requiresRepository: true,
151
+ shortcuts: ["i"],
152
+ examples: [
153
+ "agentctl init",
154
+ "agentctl init --interactive",
155
+ "agentctl init --tier pro --yes",
156
+ ],
157
+ flags: [
158
+ { name: "tier", type: "string", description: "Target configuration tier (free, pro, enterprise)" },
159
+ { name: "interactive", type: "boolean", description: "Launch interactive onboarding wizard" },
160
+ { name: "yes", type: "boolean", description: "Accept auto-detected Stack Oracle defaults" },
161
+ ],
162
+ },
163
+ {
164
+ id: "dashboard",
165
+ path: ["dashboard"],
166
+ title: "dashboard",
167
+ description: "Start local web dashboard server",
168
+ category: "Inspect",
169
+ mutates: false,
170
+ risk: "low",
171
+ interactive: "never",
172
+ requiresRepository: true,
173
+ shortcuts: ["dash"],
174
+ examples: [
175
+ "agentctl dashboard",
176
+ "agentctl dashboard --port 3000",
177
+ ],
178
+ flags: [
179
+ { name: "port", type: "string", description: "HTTP server port (default 3000)" },
180
+ { name: "host", type: "string", description: "Bind host address (default 127.0.0.1)" },
181
+ ],
182
+ },
183
+ {
184
+ id: "status",
185
+ path: ["status"],
186
+ title: "status",
187
+ description: "Show operating status and health summary",
188
+ category: "Inspect",
189
+ mutates: false,
190
+ risk: "low",
191
+ interactive: "never",
192
+ requiresRepository: false,
193
+ shortcuts: ["st"],
194
+ examples: [
195
+ "agentctl status",
196
+ "agentctl status --json",
197
+ ],
198
+ flags: [
199
+ { name: "json", type: "boolean", description: "Output status in JSON format" },
200
+ ],
201
+ },
202
+ ];
203
+
204
+ /**
205
+ * Lookup command descriptor by ID, command path, or shortcut alias.
206
+ * @param {string} idOrAlias
207
+ * @returns {CommandDescriptor | null}
208
+ */
209
+ export function getCommandDescriptor(idOrAlias) {
210
+ if (!idOrAlias) return null;
211
+ const target = String(idOrAlias).trim().toLowerCase();
212
+
213
+ return (
214
+ COMMAND_REGISTRY.find((cmd) => {
215
+ if (cmd.id.toLowerCase() === target) return true;
216
+ if (cmd.path.join(" ").toLowerCase() === target) return true;
217
+ if (cmd.shortcuts.some((s) => s.toLowerCase() === target)) return true;
218
+ return false;
219
+ }) || null
220
+ );
221
+ }
222
+
223
+ /**
224
+ * List commands filtered by category.
225
+ * @param {CommandCategory} [category]
226
+ * @returns {CommandDescriptor[]}
227
+ */
228
+ export function listCommandsByCategory(category) {
229
+ if (!category) return COMMAND_REGISTRY;
230
+ return COMMAND_REGISTRY.filter((cmd) => cmd.category === category);
231
+ }
232
+
233
+ /**
234
+ * Format clean CLI --help text for a command descriptor.
235
+ * @param {CommandDescriptor} descriptor
236
+ * @returns {string}
237
+ */
238
+ export function formatCommandHelp(descriptor) {
239
+ const lines = [];
240
+ lines.push(`Usage: agentctl ${descriptor.path.join(" ")} [flags]`);
241
+ lines.push("");
242
+ lines.push(`Description: ${descriptor.description}`);
243
+ lines.push(`Category: ${descriptor.category}`);
244
+ lines.push(`Risk Tier: ${descriptor.risk.toUpperCase()}`);
245
+ lines.push("");
246
+
247
+ if (descriptor.flags && descriptor.flags.length > 0) {
248
+ lines.push("Flags:");
249
+ for (const flag of descriptor.flags) {
250
+ lines.push(` --${flag.name.padEnd(16)} ${flag.description}`);
251
+ }
252
+ lines.push("");
253
+ }
254
+
255
+ if (descriptor.examples && descriptor.examples.length > 0) {
256
+ lines.push("Examples:");
257
+ for (const ex of descriptor.examples) {
258
+ lines.push(` $ ${ex}`);
259
+ }
260
+ }
261
+
262
+ return lines.join("\n");
263
+ }
264
+
265
+ /**
266
+ * Format JSON representation of full command registry.
267
+ * @returns {{ ok: true, commands: CommandDescriptor[] }}
268
+ */
269
+ export function formatRegistryJson() {
270
+ return {
271
+ ok: true,
272
+ commands: COMMAND_REGISTRY,
273
+ };
274
+ }
@@ -0,0 +1,214 @@
1
+ import { join, resolve } from "node:path";
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+
5
+ /**
6
+ * @typedef {import("../ux/capabilities.mjs").ActionPlan} ActionPlan
7
+ * @typedef {import("./doctor-registry.mjs").DoctorReport} DoctorReport
8
+ *
9
+ * @typedef {Object} FixPlanningContext
10
+ * @property {string} root
11
+ * @property {DoctorReport} report
12
+ * @property {string[]} selectedFixIds
13
+ * @property {Record<string, unknown>} [answers]
14
+ */
15
+
16
+ /**
17
+ * Generate unified diff preview string for file creation/replacement.
18
+ * @param {string} path
19
+ * @param {string} oldContent
20
+ * @param {string} newContent
21
+ * @returns {string}
22
+ */
23
+ function createDiffPreview(path, oldContent, newContent) {
24
+ const oldLines = oldContent ? oldContent.split("\n") : [];
25
+ const newLines = newContent ? newContent.split("\n") : [];
26
+ const lines = [];
27
+
28
+ lines.push(`--- a/${path}`);
29
+ lines.push(`+++ b/${path}`);
30
+ lines.push(`@@ -1,${oldLines.length || 0} +1,${newLines.length || 0} @@`);
31
+
32
+ for (const line of oldLines) {
33
+ lines.push(`-${line}`);
34
+ }
35
+ for (const line of newLines) {
36
+ lines.push(`+${line}`);
37
+ }
38
+
39
+ return lines.join("\n");
40
+ }
41
+
42
+ function deepFreeze(value) {
43
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
44
+ for (const child of Object.values(value)) deepFreeze(child);
45
+ return Object.freeze(value);
46
+ }
47
+
48
+ function finalizePlan(plan) {
49
+ const { planHash: _oldHash, ...unsigned } = plan;
50
+ const planHash = "sha256:" + createHash("sha256").update(JSON.stringify(unsigned)).digest("hex");
51
+ return deepFreeze({ ...unsigned, planHash });
52
+ }
53
+
54
+ /**
55
+ * Plan diagnostic fixes into immutable ActionPlan objects.
56
+ * @param {FixPlanningContext} context
57
+ * @returns {Promise<ActionPlan[]>}
58
+ */
59
+ export async function planDiagnosticFixes(context = {}) {
60
+ const root = resolve(context.root || process.cwd());
61
+ const report = context.report || { results: [] };
62
+ const selectedFixIds = Array.isArray(context.selectedFixIds) ? context.selectedFixIds : ["safe"];
63
+
64
+ const wantSafeOnly = selectedFixIds.includes("safe");
65
+ /** @type {ActionPlan[]} */
66
+ const plans = [];
67
+
68
+ for (const result of report.results || []) {
69
+ for (const fix of result.fixes || []) {
70
+ const isSelected = selectedFixIds.includes(fix.id) || (wantSafeOnly && fix.automatic);
71
+ if (!isSelected) continue;
72
+
73
+ if (fix.id === "config.create-default") {
74
+ const configPath = ".agent/config.yml";
75
+ const newContent = `# jules-orchestrator-kit configuration manifest
76
+ version: 1
77
+ tier: pro
78
+ verify:
79
+ test: "npm test"
80
+ build: "npm run build"
81
+ `;
82
+ const diff = createDiffPreview(configPath, "", newContent);
83
+ const planId = `PLAN-${Date.now()}-${randomUUID().slice(0, 8)}`;
84
+
85
+ plans.push({
86
+ schema: "agentctl/action-plan-v1",
87
+ id: planId,
88
+ kind: "config.create-default",
89
+ title: "Create default .agent/config.yml",
90
+ summary: "Generate initial configuration manifest for jules-orchestrator-kit",
91
+ risk: "low",
92
+ repository: root,
93
+ createdAt: new Date().toISOString(),
94
+ preconditions: [],
95
+ fileMutations: [
96
+ {
97
+ operation: "create",
98
+ path: configPath,
99
+ newContent,
100
+ },
101
+ ],
102
+ commandEffects: [],
103
+ stateTransitions: [],
104
+ preview: {
105
+ unifiedDiff: diff,
106
+ warnings: [],
107
+ estimatedImpact: ["Creates .agent/config.yml"],
108
+ },
109
+ confirmation: {
110
+ mode: "none",
111
+ prompt: "Apply default configuration?",
112
+ },
113
+ planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
114
+ });
115
+ } else if (fix.id === "state.rebuild-head") {
116
+ const dateStr = new Date().toISOString().split("T")[0];
117
+ const stateDir = join(root, ".agent", "state");
118
+ const headPath = `.agent/state/telemetry-${dateStr}.head`;
119
+ let tailHash = "0".repeat(64);
120
+ let segment = 0;
121
+ try {
122
+ const telemetryFiles = readdirSync(stateDir)
123
+ .filter((name) => name.startsWith(`telemetry-${dateStr}`) && name.endsWith(".jsonl"))
124
+ .sort((a, b) => {
125
+ const index = (name) => Number(name.match(/-(\d+)\.jsonl$/)?.[1] || 0);
126
+ return index(a) - index(b);
127
+ });
128
+ const lastFile = telemetryFiles.at(-1);
129
+ if (lastFile) {
130
+ const lines = readFileSync(join(stateDir, lastFile), "utf-8").split("\n").filter(Boolean);
131
+ const lastEntry = JSON.parse(lines.at(-1));
132
+ if (typeof lastEntry?.hash === "string") tailHash = lastEntry.hash;
133
+ const segmentMatch = lastFile.match(/-(\d+)\.jsonl$/);
134
+ segment = segmentMatch ? Number(segmentMatch[1]) : 0;
135
+ }
136
+ } catch (_) {}
137
+ const newContent = JSON.stringify({ v: 1, hash: tailHash, segment, ts: new Date().toISOString() });
138
+ const planId = `PLAN-${Date.now()}-${randomUUID().slice(0, 8)}`;
139
+
140
+ plans.push({
141
+ schema: "agentctl/action-plan-v1",
142
+ id: planId,
143
+ kind: "state.rebuild-head",
144
+ title: "Rebuild telemetry spine head pointer",
145
+ summary: "Re-initialize .head pointer for telemetry spine ledger",
146
+ risk: "low",
147
+ repository: root,
148
+ createdAt: new Date().toISOString(),
149
+ preconditions: [],
150
+ fileMutations: [
151
+ {
152
+ operation: "create",
153
+ path: headPath,
154
+ newContent,
155
+ },
156
+ ],
157
+ commandEffects: [],
158
+ stateTransitions: [],
159
+ preview: {
160
+ unifiedDiff: createDiffPreview(headPath, "", newContent),
161
+ warnings: [],
162
+ estimatedImpact: [`Creates ${headPath} pointer`],
163
+ },
164
+ confirmation: {
165
+ mode: "none",
166
+ prompt: "Rebuild telemetry head pointer?",
167
+ },
168
+ planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
169
+ });
170
+ } else if (fix.id === "locks.prune-stale") {
171
+ const locksDir = join(root, ".agent", "state", "locks");
172
+ if (existsSync(locksDir)) {
173
+ const lockFiles = readdirSync(locksDir).filter((f) => f.endsWith(".json"));
174
+ for (const lockFile of lockFiles) {
175
+ const lockRelPath = `.agent/state/locks/${lockFile}`;
176
+ const planId = `PLAN-${Date.now()}-${randomUUID().slice(0, 8)}`;
177
+
178
+ plans.push({
179
+ schema: "agentctl/action-plan-v1",
180
+ id: planId,
181
+ kind: "locks.prune-stale",
182
+ title: `Remove stale lock ${lockFile}`,
183
+ summary: `Prune inactive VFS lock file ${lockRelPath}`,
184
+ risk: "low",
185
+ repository: root,
186
+ createdAt: new Date().toISOString(),
187
+ preconditions: [],
188
+ fileMutations: [
189
+ {
190
+ operation: "delete",
191
+ path: lockRelPath,
192
+ },
193
+ ],
194
+ commandEffects: [],
195
+ stateTransitions: [],
196
+ preview: {
197
+ unifiedDiff: `--- a/${lockRelPath}\n+++ /dev/null\n`,
198
+ warnings: [],
199
+ estimatedImpact: [`Removes ${lockRelPath}`],
200
+ },
201
+ confirmation: {
202
+ mode: "none",
203
+ prompt: `Remove lock ${lockFile}?`,
204
+ },
205
+ planHash: "sha256:" + createHash("sha256").update(planId).digest("hex"),
206
+ });
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ return plans.map(finalizePlan);
214
+ }