opencode-planpilot 0.2.2 → 0.2.4

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/dist/index.js ADDED
@@ -0,0 +1,3815 @@
1
+ // src/index.ts
2
+ import { tool } from "@opencode-ai/plugin";
3
+
4
+ // src/command.ts
5
+ import fs3 from "fs";
6
+
7
+ // src/lib/db.ts
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import os from "os";
11
+ import { Database } from "bun:sqlite";
12
+ import { xdgConfig } from "xdg-basedir";
13
+ var cachedDb = null;
14
+ function resolveConfigRoot() {
15
+ if (process.env.OPENCODE_CONFIG_DIR) {
16
+ return process.env.OPENCODE_CONFIG_DIR;
17
+ }
18
+ const base = xdgConfig ?? path.join(os.homedir(), ".config");
19
+ return path.join(base, "opencode");
20
+ }
21
+ function resolvePlanpilotDir() {
22
+ const override = process.env.OPENCODE_PLANPILOT_DIR || process.env.OPENCODE_PLANPILOT_HOME;
23
+ if (override && override.trim()) return override;
24
+ return path.join(resolveConfigRoot(), ".planpilot");
25
+ }
26
+ function resolveDbPath() {
27
+ return path.join(resolvePlanpilotDir(), "planpilot.db");
28
+ }
29
+ function resolvePlanMarkdownDir() {
30
+ return path.join(resolvePlanpilotDir(), "plans");
31
+ }
32
+ function resolvePlanMarkdownPath(planId) {
33
+ return path.join(resolvePlanMarkdownDir(), `plan_${planId}.md`);
34
+ }
35
+ function ensureParentDir(filePath) {
36
+ const dir = path.dirname(filePath);
37
+ fs.mkdirSync(dir, { recursive: true });
38
+ }
39
+ function openDatabase() {
40
+ if (cachedDb) return cachedDb;
41
+ const dbPath = resolveDbPath();
42
+ ensureParentDir(dbPath);
43
+ const db = new Database(dbPath);
44
+ db.exec("PRAGMA foreign_keys = ON;");
45
+ ensureSchema(db);
46
+ cachedDb = db;
47
+ return db;
48
+ }
49
+ function ensureSchema(db) {
50
+ db.exec(`
51
+ CREATE TABLE IF NOT EXISTS plans (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ title TEXT NOT NULL,
54
+ content TEXT NOT NULL,
55
+ status TEXT NOT NULL,
56
+ comment TEXT,
57
+ last_session_id TEXT,
58
+ last_cwd TEXT,
59
+ created_at INTEGER NOT NULL,
60
+ updated_at INTEGER NOT NULL
61
+ );
62
+ CREATE TABLE IF NOT EXISTS steps (
63
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
64
+ plan_id INTEGER NOT NULL,
65
+ content TEXT NOT NULL,
66
+ status TEXT NOT NULL,
67
+ executor TEXT NOT NULL,
68
+ sort_order INTEGER NOT NULL,
69
+ comment TEXT,
70
+ created_at INTEGER NOT NULL,
71
+ updated_at INTEGER NOT NULL,
72
+ FOREIGN KEY(plan_id) REFERENCES plans(id) ON DELETE CASCADE
73
+ );
74
+ CREATE TABLE IF NOT EXISTS goals (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ step_id INTEGER NOT NULL,
77
+ content TEXT NOT NULL,
78
+ status TEXT NOT NULL,
79
+ comment TEXT,
80
+ created_at INTEGER NOT NULL,
81
+ updated_at INTEGER NOT NULL,
82
+ FOREIGN KEY(step_id) REFERENCES steps(id) ON DELETE CASCADE
83
+ );
84
+ CREATE TABLE IF NOT EXISTS active_plan (
85
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
86
+ session_id TEXT NOT NULL,
87
+ plan_id INTEGER NOT NULL,
88
+ updated_at INTEGER NOT NULL,
89
+ UNIQUE(session_id),
90
+ UNIQUE(plan_id),
91
+ FOREIGN KEY(plan_id) REFERENCES plans(id) ON DELETE CASCADE
92
+ );
93
+ CREATE INDEX IF NOT EXISTS idx_steps_plan_order ON steps(plan_id, sort_order);
94
+ CREATE INDEX IF NOT EXISTS idx_goals_step ON goals(step_id);
95
+ `);
96
+ const columns = db.prepare("PRAGMA table_info(plans)").all().map((row) => row.name);
97
+ if (!columns.includes("last_cwd")) {
98
+ db.exec("ALTER TABLE plans ADD COLUMN last_cwd TEXT");
99
+ }
100
+ }
101
+
102
+ // src/lib/models.ts
103
+ function createEmptyStatusChanges() {
104
+ return { steps: [], plans: [], active_plans_cleared: [] };
105
+ }
106
+ function mergeStatusChanges(target, other) {
107
+ target.steps.push(...other.steps);
108
+ target.plans.push(...other.plans);
109
+ target.active_plans_cleared.push(...other.active_plans_cleared);
110
+ }
111
+ function statusChangesEmpty(changes) {
112
+ return !changes.steps.length && !changes.plans.length && !changes.active_plans_cleared.length;
113
+ }
114
+
115
+ // src/lib/util.ts
116
+ import fs2 from "fs";
117
+ import path2 from "path";
118
+
119
+ // src/lib/errors.ts
120
+ var AppError = class extends Error {
121
+ kind;
122
+ detail;
123
+ constructor(kind, detail) {
124
+ super(detail);
125
+ this.kind = kind;
126
+ this.detail = detail;
127
+ }
128
+ toDisplayString() {
129
+ const label = this.kind === "InvalidInput" ? "Invalid input" : this.kind === "NotFound" ? "Not found" : null;
130
+ if (!label) {
131
+ return this.detail;
132
+ }
133
+ if (this.detail.includes("\n")) {
134
+ return `${label}:
135
+ ${this.detail}`;
136
+ }
137
+ return `${label}: ${this.detail}`;
138
+ }
139
+ };
140
+ function invalidInput(message) {
141
+ return new AppError("InvalidInput", message);
142
+ }
143
+ function notFound(message) {
144
+ return new AppError("NotFound", message);
145
+ }
146
+
147
+ // src/lib/util.ts
148
+ function ensureNonEmpty(label, value) {
149
+ if (value.trim().length === 0) {
150
+ throw invalidInput(`${label} cannot be empty`);
151
+ }
152
+ }
153
+ function formatDateTimeUTC(timestamp) {
154
+ const date = new Date(timestamp);
155
+ const yyyy = date.getUTCFullYear();
156
+ const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
157
+ const dd = String(date.getUTCDate()).padStart(2, "0");
158
+ const hh = String(date.getUTCHours()).padStart(2, "0");
159
+ const min = String(date.getUTCMinutes()).padStart(2, "0");
160
+ return `${yyyy}-${mm}-${dd} ${hh}:${min}`;
161
+ }
162
+ function uniqueIds(ids) {
163
+ const seen = /* @__PURE__ */ new Set();
164
+ const unique = [];
165
+ for (const id of ids) {
166
+ if (!seen.has(id)) {
167
+ seen.add(id);
168
+ unique.push(id);
169
+ }
170
+ }
171
+ return unique;
172
+ }
173
+ function joinIds(ids) {
174
+ return ids.map((id) => String(id)).join(", ");
175
+ }
176
+ function normalizeCommentEntries(entries) {
177
+ const seen = /* @__PURE__ */ new Map();
178
+ const ordered = [];
179
+ for (const [id, comment] of entries) {
180
+ const idx = seen.get(id);
181
+ if (idx !== void 0) {
182
+ ordered[idx][1] = comment;
183
+ } else {
184
+ seen.set(id, ordered.length);
185
+ ordered.push([id, comment]);
186
+ }
187
+ }
188
+ return ordered;
189
+ }
190
+ var WAIT_UNTIL_PREFIX = "@wait-until=";
191
+ var WAIT_REASON_PREFIX = "@wait-reason=";
192
+ function parseWaitFromComment(comment) {
193
+ if (!comment) return null;
194
+ let until = null;
195
+ let reason;
196
+ for (const line of comment.split(/\r?\n/)) {
197
+ const trimmed = line.trim();
198
+ if (trimmed.startsWith(WAIT_UNTIL_PREFIX)) {
199
+ const raw = trimmed.slice(WAIT_UNTIL_PREFIX.length).trim();
200
+ const value = Number(raw);
201
+ if (Number.isFinite(value)) {
202
+ until = value;
203
+ }
204
+ continue;
205
+ }
206
+ if (trimmed.startsWith(WAIT_REASON_PREFIX)) {
207
+ const raw = trimmed.slice(WAIT_REASON_PREFIX.length).trim();
208
+ if (raw) reason = raw;
209
+ }
210
+ }
211
+ if (until === null) return null;
212
+ return { until, reason };
213
+ }
214
+ function isWaitLine(line) {
215
+ const trimmed = line.trim();
216
+ return trimmed.startsWith(WAIT_UNTIL_PREFIX) || trimmed.startsWith(WAIT_REASON_PREFIX);
217
+ }
218
+ function upsertWaitInComment(comment, until, reason) {
219
+ const lines = comment ? comment.split(/\r?\n/) : [];
220
+ const filtered = lines.filter((line) => !isWaitLine(line));
221
+ const waitLines = [`${WAIT_UNTIL_PREFIX}${Math.trunc(until)}`];
222
+ const reasonValue = reason?.trim();
223
+ if (reasonValue) {
224
+ waitLines.push(`${WAIT_REASON_PREFIX}${reasonValue}`);
225
+ }
226
+ return [...waitLines, ...filtered].join("\n").trimEnd();
227
+ }
228
+ function removeWaitFromComment(comment) {
229
+ if (!comment) return null;
230
+ const lines = comment.split(/\r?\n/).filter((line) => !isWaitLine(line));
231
+ const cleaned = lines.join("\n").trimEnd();
232
+ return cleaned.length ? cleaned : null;
233
+ }
234
+ function resolveMaybeRealpath(value) {
235
+ try {
236
+ return fs2.realpathSync.native(value);
237
+ } catch {
238
+ return value;
239
+ }
240
+ }
241
+ function normalizePath(value) {
242
+ let resolved = resolveMaybeRealpath(value);
243
+ resolved = path2.resolve(resolved);
244
+ if (process.platform === "win32") {
245
+ resolved = resolved.toLowerCase();
246
+ }
247
+ return resolved.replace(/[\\/]+$/, "");
248
+ }
249
+ function projectMatchesPath(project, current) {
250
+ const projectNorm = normalizePath(project);
251
+ const currentNorm = normalizePath(current);
252
+ if (projectNorm === currentNorm) return true;
253
+ if (currentNorm.startsWith(projectNorm + path2.sep)) return true;
254
+ if (projectNorm.startsWith(currentNorm + path2.sep)) return true;
255
+ return false;
256
+ }
257
+
258
+ // src/lib/format.ts
259
+ function hasText(value) {
260
+ return value !== void 0 && value !== null && value.trim().length > 0;
261
+ }
262
+ function formatStepDetail(step, goals) {
263
+ let output = "";
264
+ output += `Step ID: ${step.id}
265
+ `;
266
+ output += `Plan ID: ${step.plan_id}
267
+ `;
268
+ output += `Status: ${step.status}
269
+ `;
270
+ output += `Executor: ${step.executor}
271
+ `;
272
+ output += `Content: ${step.content}
273
+ `;
274
+ if (hasText(step.comment)) {
275
+ output += `Comment: ${step.comment ?? ""}
276
+ `;
277
+ }
278
+ output += `Created: ${formatDateTimeUTC(step.created_at)}
279
+ `;
280
+ output += `Updated: ${formatDateTimeUTC(step.updated_at)}
281
+ `;
282
+ output += "\n";
283
+ if (!goals.length) {
284
+ output += "Goals: (none)";
285
+ return output.trimEnd();
286
+ }
287
+ output += "Goals:\n";
288
+ for (const goal of goals) {
289
+ output += `- [${goal.status}] ${goal.content} (goal id ${goal.id})
290
+ `;
291
+ if (hasText(goal.comment)) {
292
+ output += ` Comment: ${goal.comment ?? ""}
293
+ `;
294
+ }
295
+ }
296
+ return output.trimEnd();
297
+ }
298
+ function formatGoalDetail(goal, step) {
299
+ let output = "";
300
+ output += `Goal ID: ${goal.id}
301
+ `;
302
+ output += `Step ID: ${goal.step_id}
303
+ `;
304
+ output += `Plan ID: ${step.plan_id}
305
+ `;
306
+ output += `Status: ${goal.status}
307
+ `;
308
+ output += `Content: ${goal.content}
309
+ `;
310
+ if (hasText(goal.comment)) {
311
+ output += `Comment: ${goal.comment ?? ""}
312
+ `;
313
+ }
314
+ output += `Created: ${formatDateTimeUTC(goal.created_at)}
315
+ `;
316
+ output += `Updated: ${formatDateTimeUTC(goal.updated_at)}
317
+ `;
318
+ output += "\n";
319
+ output += `Step Status: ${step.status}
320
+ `;
321
+ output += `Step Executor: ${step.executor}
322
+ `;
323
+ output += `Step Content: ${step.content}
324
+ `;
325
+ if (hasText(step.comment)) {
326
+ output += `Step Comment: ${step.comment ?? ""}
327
+ `;
328
+ }
329
+ return output.trimEnd();
330
+ }
331
+ function formatPlanDetail(plan, steps, goals) {
332
+ let output = "";
333
+ output += `Plan ID: ${plan.id}
334
+ `;
335
+ output += `Title: ${plan.title}
336
+ `;
337
+ output += `Status: ${plan.status}
338
+ `;
339
+ output += `Content: ${plan.content}
340
+ `;
341
+ if (hasText(plan.comment)) {
342
+ output += `Comment: ${plan.comment ?? ""}
343
+ `;
344
+ }
345
+ output += `Created: ${formatDateTimeUTC(plan.created_at)}
346
+ `;
347
+ output += `Updated: ${formatDateTimeUTC(plan.updated_at)}
348
+ `;
349
+ output += "\n";
350
+ if (!steps.length) {
351
+ output += "Steps: (none)";
352
+ return output.trimEnd();
353
+ }
354
+ output += "Steps:\n";
355
+ for (const step of steps) {
356
+ const stepGoals = goals.get(step.id) ?? [];
357
+ if (stepGoals.length) {
358
+ const done = stepGoals.filter((goal) => goal.status === "done").length;
359
+ output += `- [${step.status}] ${step.content} (step id ${step.id}, exec ${step.executor}, goals ${done}/${stepGoals.length})
360
+ `;
361
+ } else {
362
+ output += `- [${step.status}] ${step.content} (step id ${step.id}, exec ${step.executor})
363
+ `;
364
+ }
365
+ if (hasText(step.comment)) {
366
+ output += ` Comment: ${step.comment ?? ""}
367
+ `;
368
+ }
369
+ if (stepGoals.length) {
370
+ for (const goal of stepGoals) {
371
+ output += ` - [${goal.status}] ${goal.content} (goal id ${goal.id})
372
+ `;
373
+ if (hasText(goal.comment)) {
374
+ output += ` Comment: ${goal.comment ?? ""}
375
+ `;
376
+ }
377
+ }
378
+ }
379
+ }
380
+ return output.trimEnd();
381
+ }
382
+ function formatPlanMarkdown(active, activeUpdated, plan, steps, goals) {
383
+ const lines = [];
384
+ const checkbox = (status) => status === "done" ? "x" : " ";
385
+ const collapseHeading = (text) => {
386
+ const normalized = text.replace(/\r\n/g, "\n");
387
+ const parts = normalized.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
388
+ if (!parts.length) return "(untitled)";
389
+ return parts.join(" / ");
390
+ };
391
+ const splitTaskText = (text) => {
392
+ const normalized = text.replace(/\r\n/g, "\n");
393
+ const rawLines = normalized.split("\n");
394
+ if (!rawLines.length) return ["(empty)", []];
395
+ let firstIdx = -1;
396
+ for (let i = 0; i < rawLines.length; i += 1) {
397
+ if (rawLines[i].trim()) {
398
+ firstIdx = i;
399
+ break;
400
+ }
401
+ }
402
+ if (firstIdx === -1) return ["(empty)", []];
403
+ return [rawLines[firstIdx], rawLines.slice(firstIdx + 1)];
404
+ };
405
+ const pushLine = (indent, text) => {
406
+ lines.push(`${" ".repeat(indent)}${text}`);
407
+ };
408
+ const pushBlank = (indent) => {
409
+ lines.push(indent === 0 ? "" : " ".repeat(indent));
410
+ };
411
+ pushLine(0, "# Plan");
412
+ pushBlank(0);
413
+ pushLine(0, `## Plan: ${collapseHeading(plan.title)}`);
414
+ pushBlank(0);
415
+ pushLine(0, `- **Active:** \`${active ? "true" : "false"}\``);
416
+ pushLine(0, `- **Plan ID:** \`${plan.id}\``);
417
+ pushLine(0, `- **Status:** \`${plan.status}\``);
418
+ if (hasText(plan.comment)) {
419
+ pushLine(0, `- **Comment:** ${plan.comment ?? ""}`);
420
+ }
421
+ if (activeUpdated) {
422
+ pushLine(0, `- **Activated:** ${formatDateTimeUTC(activeUpdated)}`);
423
+ }
424
+ pushLine(0, `- **Created:** ${formatDateTimeUTC(plan.created_at)}`);
425
+ pushLine(0, `- **Updated:** ${formatDateTimeUTC(plan.updated_at)}`);
426
+ const stepsDone = steps.filter((step) => step.status === "done").length;
427
+ pushLine(0, `- **Steps:** ${stepsDone}/${steps.length}`);
428
+ pushBlank(0);
429
+ pushLine(0, "### Plan Content");
430
+ pushBlank(0);
431
+ if (!plan.content.trim()) {
432
+ pushLine(0, "*No content*");
433
+ } else {
434
+ const normalized = plan.content.replace(/\r\n/g, "\n");
435
+ for (const line of normalized.split("\n")) {
436
+ if (!line.length) {
437
+ pushLine(0, ">");
438
+ } else {
439
+ pushLine(0, `> ${line}`);
440
+ }
441
+ }
442
+ }
443
+ pushBlank(0);
444
+ pushLine(0, "### Steps");
445
+ pushBlank(0);
446
+ if (!steps.length) {
447
+ pushLine(0, "*No steps*");
448
+ return lines.join("\n").trimEnd();
449
+ }
450
+ steps.forEach((step, idx) => {
451
+ const [firstLine, restLines] = splitTaskText(step.content);
452
+ pushLine(0, `- [${checkbox(step.status)}] **${firstLine}** *(id: ${step.id}, exec: ${step.executor}, order: ${step.sort_order})*`);
453
+ let hasRest = false;
454
+ for (const line of restLines) {
455
+ if (!line.trim()) continue;
456
+ if (!hasRest) {
457
+ pushBlank(2);
458
+ hasRest = true;
459
+ } else {
460
+ pushBlank(2);
461
+ }
462
+ pushLine(2, line);
463
+ }
464
+ pushBlank(2);
465
+ pushLine(2, `- Created: ${formatDateTimeUTC(step.created_at)}`);
466
+ pushLine(2, `- Updated: ${formatDateTimeUTC(step.updated_at)}`);
467
+ if (hasText(step.comment)) {
468
+ pushLine(2, `- Comment: ${step.comment ?? ""}`);
469
+ }
470
+ const stepGoals = goals.get(step.id);
471
+ if (stepGoals && stepGoals.length) {
472
+ const done = stepGoals.filter((goal) => goal.status === "done").length;
473
+ pushLine(2, `- Goals: ${done}/${stepGoals.length}`);
474
+ for (const goal of stepGoals) {
475
+ const [goalFirst, goalRest] = splitTaskText(goal.content);
476
+ pushBlank(2);
477
+ pushLine(2, `- [${checkbox(goal.status)}] ${goalFirst} *(id: ${goal.id})*`);
478
+ for (const line of goalRest) {
479
+ if (!line.trim()) continue;
480
+ pushBlank(4);
481
+ pushLine(4, line);
482
+ }
483
+ if (hasText(goal.comment)) {
484
+ pushBlank(4);
485
+ pushLine(4, `Comment: ${goal.comment ?? ""}`);
486
+ }
487
+ }
488
+ } else {
489
+ pushLine(2, "- Goals: 0/0");
490
+ pushBlank(2);
491
+ pushLine(2, "- (none)");
492
+ }
493
+ if (idx + 1 < steps.length) {
494
+ pushBlank(0);
495
+ }
496
+ });
497
+ return lines.join("\n").trimEnd();
498
+ }
499
+
500
+ // src/lib/app.ts
501
+ var PlanpilotApp = class {
502
+ db;
503
+ sessionId;
504
+ cwd;
505
+ constructor(db, sessionId, cwd) {
506
+ this.db = db;
507
+ this.sessionId = sessionId;
508
+ this.cwd = cwd;
509
+ }
510
+ addPlan(input) {
511
+ ensureNonEmpty("plan title", input.title);
512
+ ensureNonEmpty("plan content", input.content);
513
+ const now = Date.now();
514
+ const result = this.db.prepare(
515
+ `INSERT INTO plans (title, content, status, comment, last_session_id, last_cwd, created_at, updated_at)
516
+ VALUES (?, ?, ?, NULL, ?, ?, ?, ?)`
517
+ ).run(input.title, input.content, "todo", this.sessionId, this.cwd ?? null, now, now);
518
+ const plan = this.getPlan(result.lastInsertRowid);
519
+ return plan;
520
+ }
521
+ addPlanTree(input, steps) {
522
+ ensureNonEmpty("plan title", input.title);
523
+ ensureNonEmpty("plan content", input.content);
524
+ steps.forEach((step) => {
525
+ ensureNonEmpty("step content", step.content);
526
+ step.goals.forEach((goal) => ensureNonEmpty("goal content", goal));
527
+ });
528
+ const tx = this.db.transaction(() => {
529
+ const now = Date.now();
530
+ const planResult = this.db.prepare(
531
+ `INSERT INTO plans (title, content, status, comment, last_session_id, last_cwd, created_at, updated_at)
532
+ VALUES (?, ?, ?, NULL, ?, ?, ?, ?)`
533
+ ).run(input.title, input.content, "todo", this.sessionId, this.cwd ?? null, now, now);
534
+ const plan = this.getPlan(planResult.lastInsertRowid);
535
+ let stepCount = 0;
536
+ let goalCount = 0;
537
+ steps.forEach((step, idx) => {
538
+ const stepResult = this.db.prepare(
539
+ `INSERT INTO steps (plan_id, content, status, executor, sort_order, comment, created_at, updated_at)
540
+ VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`
541
+ ).run(plan.id, step.content, "todo", step.executor, idx + 1, now, now);
542
+ const stepId = stepResult.lastInsertRowid;
543
+ stepCount += 1;
544
+ step.goals.forEach((goal) => {
545
+ this.db.prepare(
546
+ `INSERT INTO goals (step_id, content, status, comment, created_at, updated_at)
547
+ VALUES (?, ?, ?, NULL, ?, ?)`
548
+ ).run(stepId, goal, "todo", now, now);
549
+ goalCount += 1;
550
+ });
551
+ });
552
+ return { plan, stepCount, goalCount };
553
+ });
554
+ return tx();
555
+ }
556
+ listPlans(order, desc) {
557
+ const orderBy = order ?? "updated";
558
+ const direction = desc ? "DESC" : "ASC";
559
+ const orderColumn = orderBy === "id" ? "id" : orderBy === "title" ? "title" : orderBy === "created" ? "created_at" : "updated_at";
560
+ return this.db.prepare(`SELECT * FROM plans ORDER BY ${orderColumn} ${direction}, id ASC`).all();
561
+ }
562
+ getPlan(id) {
563
+ const row = this.db.prepare("SELECT * FROM plans WHERE id = ?").get(id);
564
+ if (!row) throw notFound(`plan id ${id}`);
565
+ return row;
566
+ }
567
+ getStep(id) {
568
+ const row = this.db.prepare("SELECT * FROM steps WHERE id = ?").get(id);
569
+ if (!row) throw notFound(`step id ${id}`);
570
+ return row;
571
+ }
572
+ getGoal(id) {
573
+ const row = this.db.prepare("SELECT * FROM goals WHERE id = ?").get(id);
574
+ if (!row) throw notFound(`goal id ${id}`);
575
+ return row;
576
+ }
577
+ planWithSteps(id) {
578
+ const plan = this.getPlan(id);
579
+ const steps = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(id);
580
+ return { plan, steps };
581
+ }
582
+ getPlanDetail(id) {
583
+ const plan = this.getPlan(id);
584
+ const steps = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(id);
585
+ const stepIds = steps.map((step) => step.id);
586
+ const goals = this.goalsForSteps(stepIds);
587
+ const goalsMap = /* @__PURE__ */ new Map();
588
+ for (const step of steps) {
589
+ goalsMap.set(step.id, goals.get(step.id) ?? []);
590
+ }
591
+ return { plan, steps, goals: goalsMap };
592
+ }
593
+ getStepDetail(id) {
594
+ const step = this.getStep(id);
595
+ const goals = this.goalsForStep(step.id);
596
+ return { step, goals };
597
+ }
598
+ getGoalDetail(id) {
599
+ const goal = this.getGoal(id);
600
+ const step = this.getStep(goal.step_id);
601
+ return { goal, step };
602
+ }
603
+ getPlanDetails(plans) {
604
+ if (!plans.length) return [];
605
+ const planIds = plans.map((plan) => plan.id);
606
+ const steps = this.db.prepare(`SELECT * FROM steps WHERE plan_id IN (${planIds.map(() => "?").join(",")}) ORDER BY plan_id ASC, sort_order ASC, id ASC`).all(...planIds);
607
+ const stepIds = steps.map((step) => step.id);
608
+ const goalsByStep = this.goalsForSteps(stepIds);
609
+ const stepsByPlan = /* @__PURE__ */ new Map();
610
+ for (const step of steps) {
611
+ const list = stepsByPlan.get(step.plan_id);
612
+ if (list) list.push(step);
613
+ else stepsByPlan.set(step.plan_id, [step]);
614
+ }
615
+ return plans.map((plan) => {
616
+ const planSteps = stepsByPlan.get(plan.id) ?? [];
617
+ const goalsMap = /* @__PURE__ */ new Map();
618
+ for (const step of planSteps) {
619
+ goalsMap.set(step.id, goalsByStep.get(step.id) ?? []);
620
+ }
621
+ return { plan, steps: planSteps, goals: goalsMap };
622
+ });
623
+ }
624
+ getStepsDetail(steps) {
625
+ if (!steps.length) return [];
626
+ const stepIds = steps.map((step) => step.id);
627
+ const goalsMap = this.goalsForSteps(stepIds);
628
+ return steps.map((step) => ({
629
+ step,
630
+ goals: goalsMap.get(step.id) ?? []
631
+ }));
632
+ }
633
+ getActivePlan() {
634
+ const row = this.db.prepare("SELECT * FROM active_plan WHERE session_id = ?").get(this.sessionId);
635
+ return row ?? null;
636
+ }
637
+ setActivePlan(planId, takeover) {
638
+ this.getPlan(planId);
639
+ const tx = this.db.transaction(() => {
640
+ const existing = this.db.prepare("SELECT * FROM active_plan WHERE plan_id = ?").get(planId);
641
+ if (existing && existing.session_id !== this.sessionId && !takeover) {
642
+ throw invalidInput(
643
+ `plan id ${planId} is already active in session ${existing.session_id} (use --force to take over)`
644
+ );
645
+ }
646
+ this.db.prepare("DELETE FROM active_plan WHERE session_id = ?").run(this.sessionId);
647
+ this.db.prepare("DELETE FROM active_plan WHERE plan_id = ?").run(planId);
648
+ const now = Date.now();
649
+ this.db.prepare("INSERT INTO active_plan (session_id, plan_id, updated_at) VALUES (?, ?, ?)").run(this.sessionId, planId, now);
650
+ this.touchPlan(planId);
651
+ const created = this.db.prepare("SELECT * FROM active_plan WHERE session_id = ?").get(this.sessionId);
652
+ if (!created) throw notFound("active plan not found after insert");
653
+ return created;
654
+ });
655
+ return tx();
656
+ }
657
+ clearActivePlan() {
658
+ this.db.prepare("DELETE FROM active_plan WHERE session_id = ?").run(this.sessionId);
659
+ }
660
+ updatePlanWithActiveClear(id, changes) {
661
+ const tx = this.db.transaction(() => {
662
+ const plan = this.updatePlanWithConn(id, changes);
663
+ let cleared = false;
664
+ if (plan.status === "done") {
665
+ cleared = this.clearActivePlansForPlanWithConn(plan.id);
666
+ }
667
+ return { plan, cleared };
668
+ });
669
+ return tx();
670
+ }
671
+ deletePlan(id) {
672
+ const tx = this.db.transaction(() => {
673
+ this.db.prepare("DELETE FROM active_plan WHERE plan_id = ?").run(id);
674
+ const stepIds = this.db.prepare("SELECT id FROM steps WHERE plan_id = ?").all(id).map((row) => row.id);
675
+ if (stepIds.length) {
676
+ this.db.prepare(`DELETE FROM goals WHERE step_id IN (${stepIds.map(() => "?").join(",")})`).run(...stepIds);
677
+ this.db.prepare("DELETE FROM steps WHERE plan_id = ?").run(id);
678
+ }
679
+ const result = this.db.prepare("DELETE FROM plans WHERE id = ?").run(id);
680
+ if (result.changes === 0) {
681
+ throw notFound(`plan id ${id}`);
682
+ }
683
+ });
684
+ tx();
685
+ }
686
+ addStepsBatch(planId, contents, status, executor, at) {
687
+ if (!this.db.prepare("SELECT 1 FROM plans WHERE id = ?").get(planId)) {
688
+ throw notFound(`plan id ${planId}`);
689
+ }
690
+ if (!contents.length) {
691
+ return { steps: [], changes: createEmptyStatusChanges() };
692
+ }
693
+ contents.forEach((content) => ensureNonEmpty("step content", content));
694
+ const tx = this.db.transaction(() => {
695
+ const existing = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(planId);
696
+ this.normalizeStepsInPlace(existing);
697
+ const total = existing.length;
698
+ const insertPos = at !== void 0 && at !== null ? at > 0 ? Math.min(at, total + 1) : 1 : total + 1;
699
+ const now = Date.now();
700
+ const shiftBy = contents.length;
701
+ if (shiftBy > 0) {
702
+ for (let idx = existing.length - 1; idx >= 0; idx -= 1) {
703
+ const step = existing[idx];
704
+ if (step.sort_order >= insertPos) {
705
+ const newOrder = step.sort_order + shiftBy;
706
+ this.db.prepare("UPDATE steps SET sort_order = ?, updated_at = ? WHERE id = ?").run(newOrder, now, step.id);
707
+ step.sort_order = newOrder;
708
+ step.updated_at = now;
709
+ }
710
+ }
711
+ }
712
+ const created = [];
713
+ contents.forEach((content, idx) => {
714
+ const sortOrder = insertPos + idx;
715
+ const result = this.db.prepare(
716
+ `INSERT INTO steps (plan_id, content, status, executor, sort_order, comment, created_at, updated_at)
717
+ VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`
718
+ ).run(planId, content, status, executor, sortOrder, now, now);
719
+ const step = this.getStep(result.lastInsertRowid);
720
+ created.push(step);
721
+ });
722
+ const changes = this.refreshPlanStatus(planId);
723
+ this.touchPlan(planId);
724
+ return { steps: created, changes };
725
+ });
726
+ return tx();
727
+ }
728
+ addStepTree(planId, content, executor, goals) {
729
+ ensureNonEmpty("step content", content);
730
+ goals.forEach((goal) => ensureNonEmpty("goal content", goal));
731
+ const tx = this.db.transaction(() => {
732
+ if (!this.db.prepare("SELECT 1 FROM plans WHERE id = ?").get(planId)) {
733
+ throw notFound(`plan id ${planId}`);
734
+ }
735
+ const existing = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(planId);
736
+ this.normalizeStepsInPlace(existing);
737
+ const sortOrder = existing.length + 1;
738
+ const now = Date.now();
739
+ const stepResult = this.db.prepare(
740
+ `INSERT INTO steps (plan_id, content, status, executor, sort_order, comment, created_at, updated_at)
741
+ VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`
742
+ ).run(planId, content, "todo", executor, sortOrder, now, now);
743
+ const step = this.getStep(stepResult.lastInsertRowid);
744
+ const createdGoals = [];
745
+ for (const goalContent of goals) {
746
+ const goalResult = this.db.prepare(
747
+ `INSERT INTO goals (step_id, content, status, comment, created_at, updated_at)
748
+ VALUES (?, ?, ?, NULL, ?, ?)`
749
+ ).run(step.id, goalContent, "todo", now, now);
750
+ createdGoals.push(this.getGoal(goalResult.lastInsertRowid));
751
+ }
752
+ const changes = this.refreshPlanStatus(planId);
753
+ this.touchPlan(planId);
754
+ return { step, goals: createdGoals, changes };
755
+ });
756
+ return tx();
757
+ }
758
+ listStepsFiltered(planId, query) {
759
+ this.getPlan(planId);
760
+ const conditions = ["plan_id = ?"];
761
+ const params = [planId];
762
+ if (query.status) {
763
+ conditions.push("status = ?");
764
+ params.push(query.status);
765
+ }
766
+ if (query.executor) {
767
+ conditions.push("executor = ?");
768
+ params.push(query.executor);
769
+ }
770
+ const order = query.order ?? "order";
771
+ const direction = query.desc ? "DESC" : "ASC";
772
+ const orderColumn = order === "id" ? "id" : order === "created" ? "created_at" : order === "updated" ? "updated_at" : "sort_order";
773
+ let sql = `SELECT * FROM steps WHERE ${conditions.join(" AND ")} ORDER BY ${orderColumn} ${direction}, id ASC`;
774
+ if (query.limit !== void 0) {
775
+ sql += " LIMIT ?";
776
+ params.push(query.limit);
777
+ }
778
+ if (query.offset !== void 0) {
779
+ sql += " OFFSET ?";
780
+ params.push(query.offset);
781
+ }
782
+ return this.db.prepare(sql).all(...params);
783
+ }
784
+ countSteps(planId, query) {
785
+ this.getPlan(planId);
786
+ const conditions = ["plan_id = ?"];
787
+ const params = [planId];
788
+ if (query.status) {
789
+ conditions.push("status = ?");
790
+ params.push(query.status);
791
+ }
792
+ if (query.executor) {
793
+ conditions.push("executor = ?");
794
+ params.push(query.executor);
795
+ }
796
+ const row = this.db.prepare(`SELECT COUNT(*) as count FROM steps WHERE ${conditions.join(" AND ")}`).get(...params);
797
+ return row.count;
798
+ }
799
+ nextStep(planId) {
800
+ const row = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? AND status = ? ORDER BY sort_order ASC, id ASC LIMIT 1").get(planId, "todo");
801
+ return row ?? null;
802
+ }
803
+ updateStep(id, changes) {
804
+ const tx = this.db.transaction(() => {
805
+ if (changes.content !== void 0) {
806
+ ensureNonEmpty("step content", changes.content);
807
+ }
808
+ if (changes.status === "done") {
809
+ const pending = this.nextGoalForStep(id);
810
+ if (pending) {
811
+ throw invalidInput(`cannot mark step done; next pending goal: ${pending.content} (id ${pending.id})`);
812
+ }
813
+ }
814
+ const existing = this.getStep(id);
815
+ const now = Date.now();
816
+ const updated = {
817
+ content: changes.content ?? existing.content,
818
+ status: changes.status ?? existing.status,
819
+ executor: changes.executor ?? existing.executor,
820
+ comment: changes.comment !== void 0 ? changes.comment : existing.comment
821
+ };
822
+ this.db.prepare(
823
+ `UPDATE steps SET content = ?, status = ?, executor = ?, comment = ?, updated_at = ? WHERE id = ?`
824
+ ).run(updated.content, updated.status, updated.executor, updated.comment, now, id);
825
+ const step = this.getStep(id);
826
+ const statusChanges = createEmptyStatusChanges();
827
+ if (changes.status !== void 0) {
828
+ mergeStatusChanges(statusChanges, this.refreshPlanStatus(step.plan_id));
829
+ }
830
+ this.touchPlan(step.plan_id);
831
+ return { step, changes: statusChanges };
832
+ });
833
+ return tx();
834
+ }
835
+ setStepDoneWithGoals(id, allGoals) {
836
+ const tx = this.db.transaction(() => {
837
+ let changes = createEmptyStatusChanges();
838
+ if (allGoals) {
839
+ const goalChanges = this.setAllGoalsDoneForStep(id);
840
+ mergeStatusChanges(changes, goalChanges);
841
+ } else {
842
+ const pending = this.nextGoalForStep(id);
843
+ if (pending) {
844
+ throw invalidInput(`cannot mark step done; next pending goal: ${pending.content} (id ${pending.id})`);
845
+ }
846
+ }
847
+ const existing = this.getStep(id);
848
+ if (existing.status !== "done") {
849
+ const now = Date.now();
850
+ this.db.prepare("UPDATE steps SET status = ?, updated_at = ? WHERE id = ?").run("done", now, id);
851
+ }
852
+ const step = this.getStep(id);
853
+ mergeStatusChanges(changes, this.refreshPlanStatus(step.plan_id));
854
+ this.touchPlan(step.plan_id);
855
+ return { step, changes };
856
+ });
857
+ return tx();
858
+ }
859
+ moveStep(id, to) {
860
+ const tx = this.db.transaction(() => {
861
+ const target = this.getStep(id);
862
+ const planId = target.plan_id;
863
+ const steps = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(planId);
864
+ const currentIndex = steps.findIndex((step) => step.id === id);
865
+ if (currentIndex === -1) throw notFound(`step id ${id}`);
866
+ let desiredIndex = Math.max(to - 1, 0);
867
+ if (desiredIndex >= steps.length) desiredIndex = steps.length - 1;
868
+ const [moving] = steps.splice(currentIndex, 1);
869
+ if (desiredIndex >= steps.length) steps.push(moving);
870
+ else steps.splice(desiredIndex, 0, moving);
871
+ const now = Date.now();
872
+ steps.forEach((step, idx) => {
873
+ const desiredOrder = idx + 1;
874
+ if (step.sort_order !== desiredOrder) {
875
+ this.db.prepare("UPDATE steps SET sort_order = ?, updated_at = ? WHERE id = ?").run(desiredOrder, now, step.id);
876
+ step.sort_order = desiredOrder;
877
+ step.updated_at = now;
878
+ }
879
+ });
880
+ return steps;
881
+ });
882
+ return tx();
883
+ }
884
+ deleteSteps(ids) {
885
+ const tx = this.db.transaction(() => {
886
+ if (!ids.length) return { deleted: 0, changes: createEmptyStatusChanges() };
887
+ const unique = uniqueIds(ids);
888
+ const steps = this.db.prepare(`SELECT * FROM steps WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
889
+ const existing = new Set(steps.map((step) => step.id));
890
+ const missing = unique.filter((id) => !existing.has(id));
891
+ if (missing.length) {
892
+ throw notFound(`step id(s) not found: ${joinIds(missing)}`);
893
+ }
894
+ const planIds = Array.from(new Set(steps.map((step) => step.plan_id)));
895
+ if (unique.length) {
896
+ this.db.prepare(`DELETE FROM goals WHERE step_id IN (${unique.map(() => "?").join(",")})`).run(...unique);
897
+ }
898
+ const result = this.db.prepare(`DELETE FROM steps WHERE id IN (${unique.map(() => "?").join(",")})`).run(...unique);
899
+ planIds.forEach((planId) => this.normalizeStepsForPlan(planId));
900
+ const changes = createEmptyStatusChanges();
901
+ planIds.forEach((planId) => mergeStatusChanges(changes, this.refreshPlanStatus(planId)));
902
+ if (planIds.length) {
903
+ this.touchPlans(planIds);
904
+ }
905
+ return { deleted: result.changes, changes };
906
+ });
907
+ return tx();
908
+ }
909
+ addGoalsBatch(stepId, contents, status) {
910
+ if (!contents.length) return { goals: [], changes: createEmptyStatusChanges() };
911
+ contents.forEach((content) => ensureNonEmpty("goal content", content));
912
+ const tx = this.db.transaction(() => {
913
+ const step = this.getStep(stepId);
914
+ const now = Date.now();
915
+ const created = [];
916
+ contents.forEach((content) => {
917
+ const result = this.db.prepare(
918
+ `INSERT INTO goals (step_id, content, status, comment, created_at, updated_at)
919
+ VALUES (?, ?, ?, NULL, ?, ?)`
920
+ ).run(stepId, content, status, now, now);
921
+ created.push(this.getGoal(result.lastInsertRowid));
922
+ });
923
+ const changes = this.refreshStepStatus(stepId);
924
+ this.touchPlan(step.plan_id);
925
+ return { goals: created, changes };
926
+ });
927
+ return tx();
928
+ }
929
+ listGoalsFiltered(stepId, query) {
930
+ this.getStep(stepId);
931
+ const conditions = ["step_id = ?"];
932
+ const params = [stepId];
933
+ if (query.status) {
934
+ conditions.push("status = ?");
935
+ params.push(query.status);
936
+ }
937
+ let sql = `SELECT * FROM goals WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, id DESC`;
938
+ if (query.limit !== void 0) {
939
+ sql += " LIMIT ?";
940
+ params.push(query.limit);
941
+ }
942
+ if (query.offset !== void 0) {
943
+ sql += " OFFSET ?";
944
+ params.push(query.offset);
945
+ }
946
+ return this.db.prepare(sql).all(...params);
947
+ }
948
+ countGoals(stepId, query) {
949
+ this.getStep(stepId);
950
+ const conditions = ["step_id = ?"];
951
+ const params = [stepId];
952
+ if (query.status) {
953
+ conditions.push("status = ?");
954
+ params.push(query.status);
955
+ }
956
+ const row = this.db.prepare(`SELECT COUNT(*) as count FROM goals WHERE ${conditions.join(" AND ")}`).get(...params);
957
+ return row.count;
958
+ }
959
+ updateGoal(id, changes) {
960
+ const tx = this.db.transaction(() => {
961
+ if (changes.content !== void 0) {
962
+ ensureNonEmpty("goal content", changes.content);
963
+ }
964
+ const existing = this.getGoal(id);
965
+ const now = Date.now();
966
+ const updated = {
967
+ content: changes.content ?? existing.content,
968
+ status: changes.status ?? existing.status,
969
+ comment: changes.comment !== void 0 ? changes.comment : existing.comment
970
+ };
971
+ this.db.prepare("UPDATE goals SET content = ?, status = ?, comment = ?, updated_at = ? WHERE id = ?").run(updated.content, updated.status, updated.comment, now, id);
972
+ const goal = this.getGoal(id);
973
+ const statusChanges = createEmptyStatusChanges();
974
+ if (changes.status !== void 0) {
975
+ mergeStatusChanges(statusChanges, this.refreshStepStatus(goal.step_id));
976
+ }
977
+ const step = this.getStep(goal.step_id);
978
+ this.touchPlan(step.plan_id);
979
+ return { goal, changes: statusChanges };
980
+ });
981
+ return tx();
982
+ }
983
+ setGoalStatus(id, status) {
984
+ const tx = this.db.transaction(() => {
985
+ this.getGoal(id);
986
+ const now = Date.now();
987
+ this.db.prepare("UPDATE goals SET status = ?, updated_at = ? WHERE id = ?").run(status, now, id);
988
+ const goal = this.getGoal(id);
989
+ const changes = this.refreshStepStatus(goal.step_id);
990
+ const step = this.getStep(goal.step_id);
991
+ this.touchPlan(step.plan_id);
992
+ return { goal, changes };
993
+ });
994
+ return tx();
995
+ }
996
+ setGoalsStatus(ids, status) {
997
+ if (!ids.length) return { updated: 0, changes: createEmptyStatusChanges() };
998
+ const tx = this.db.transaction(() => {
999
+ const unique = uniqueIds(ids);
1000
+ const goals = this.db.prepare(`SELECT * FROM goals WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
1001
+ const existing = new Set(goals.map((goal) => goal.id));
1002
+ const missing = unique.filter((id) => !existing.has(id));
1003
+ if (missing.length) {
1004
+ throw notFound(`goal id(s) not found: ${joinIds(missing)}`);
1005
+ }
1006
+ const now = Date.now();
1007
+ const stepIds = [];
1008
+ const stepSeen = /* @__PURE__ */ new Set();
1009
+ goals.forEach((goal) => {
1010
+ if (!stepSeen.has(goal.step_id)) {
1011
+ stepSeen.add(goal.step_id);
1012
+ stepIds.push(goal.step_id);
1013
+ }
1014
+ this.db.prepare("UPDATE goals SET status = ?, updated_at = ? WHERE id = ?").run(status, now, goal.id);
1015
+ });
1016
+ const changes = createEmptyStatusChanges();
1017
+ stepIds.forEach((stepId) => mergeStatusChanges(changes, this.refreshStepStatus(stepId)));
1018
+ const planIds = [];
1019
+ if (stepIds.length) {
1020
+ const steps = this.db.prepare(`SELECT plan_id FROM steps WHERE id IN (${stepIds.map(() => "?").join(",")})`).all(...stepIds);
1021
+ const seen = /* @__PURE__ */ new Set();
1022
+ steps.forEach((row) => {
1023
+ if (!seen.has(row.plan_id)) {
1024
+ seen.add(row.plan_id);
1025
+ planIds.push(row.plan_id);
1026
+ }
1027
+ });
1028
+ }
1029
+ if (planIds.length) this.touchPlans(planIds);
1030
+ return { updated: unique.length, changes };
1031
+ });
1032
+ return tx();
1033
+ }
1034
+ deleteGoals(ids) {
1035
+ const tx = this.db.transaction(() => {
1036
+ if (!ids.length) return { deleted: 0, changes: createEmptyStatusChanges() };
1037
+ const unique = uniqueIds(ids);
1038
+ const goals = this.db.prepare(`SELECT * FROM goals WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
1039
+ const existing = new Set(goals.map((goal) => goal.id));
1040
+ const missing = unique.filter((id) => !existing.has(id));
1041
+ if (missing.length) {
1042
+ throw notFound(`goal id(s) not found: ${joinIds(missing)}`);
1043
+ }
1044
+ const stepIds = Array.from(new Set(goals.map((goal) => goal.step_id)));
1045
+ const result = this.db.prepare(`DELETE FROM goals WHERE id IN (${unique.map(() => "?").join(",")})`).run(...unique);
1046
+ const changes = createEmptyStatusChanges();
1047
+ stepIds.forEach((stepId) => mergeStatusChanges(changes, this.refreshStepStatus(stepId)));
1048
+ if (stepIds.length) {
1049
+ const planIds = [];
1050
+ const steps = this.db.prepare(`SELECT plan_id FROM steps WHERE id IN (${stepIds.map(() => "?").join(",")})`).all(...stepIds);
1051
+ const seen = /* @__PURE__ */ new Set();
1052
+ steps.forEach((row) => {
1053
+ if (!seen.has(row.plan_id)) {
1054
+ seen.add(row.plan_id);
1055
+ planIds.push(row.plan_id);
1056
+ }
1057
+ });
1058
+ if (planIds.length) this.touchPlans(planIds);
1059
+ }
1060
+ return { deleted: result.changes, changes };
1061
+ });
1062
+ return tx();
1063
+ }
1064
+ commentPlans(entries) {
1065
+ const normalized = normalizeCommentEntries(entries);
1066
+ if (!normalized.length) return [];
1067
+ const ids = normalized.map(([id]) => id);
1068
+ const tx = this.db.transaction(() => {
1069
+ const existing = this.db.prepare(`SELECT id FROM plans WHERE id IN (${ids.map(() => "?").join(",")})`).all(...ids).map((row) => row.id);
1070
+ const existingSet = new Set(existing);
1071
+ const missing = ids.filter((id) => !existingSet.has(id));
1072
+ if (missing.length) {
1073
+ throw notFound(`plan id(s) not found: ${joinIds(missing)}`);
1074
+ }
1075
+ const now = Date.now();
1076
+ normalized.forEach(([planId, comment]) => {
1077
+ if (this.cwd) {
1078
+ this.db.prepare("UPDATE plans SET comment = ?, last_session_id = ?, last_cwd = ?, updated_at = ? WHERE id = ?").run(comment, this.sessionId, this.cwd, now, planId);
1079
+ } else {
1080
+ this.db.prepare("UPDATE plans SET comment = ?, last_session_id = ?, updated_at = ? WHERE id = ?").run(comment, this.sessionId, now, planId);
1081
+ }
1082
+ });
1083
+ return ids;
1084
+ });
1085
+ return tx();
1086
+ }
1087
+ commentSteps(entries) {
1088
+ const normalized = normalizeCommentEntries(entries);
1089
+ if (!normalized.length) return [];
1090
+ const ids = normalized.map(([id]) => id);
1091
+ const tx = this.db.transaction(() => {
1092
+ const steps = this.db.prepare(`SELECT * FROM steps WHERE id IN (${ids.map(() => "?").join(",")})`).all(...ids);
1093
+ const existing = new Set(steps.map((step) => step.id));
1094
+ const missing = ids.filter((id) => !existing.has(id));
1095
+ if (missing.length) {
1096
+ throw notFound(`step id(s) not found: ${joinIds(missing)}`);
1097
+ }
1098
+ const planIds = Array.from(new Set(steps.map((step) => step.plan_id)));
1099
+ const now = Date.now();
1100
+ normalized.forEach(([stepId, comment]) => {
1101
+ this.db.prepare("UPDATE steps SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, stepId);
1102
+ });
1103
+ if (planIds.length) this.touchPlans(planIds);
1104
+ return planIds;
1105
+ });
1106
+ return tx();
1107
+ }
1108
+ setStepWait(stepId, delayMs, reason) {
1109
+ if (!Number.isFinite(delayMs) || delayMs < 0) {
1110
+ throw invalidInput("delay must be a non-negative number");
1111
+ }
1112
+ const tx = this.db.transaction(() => {
1113
+ const step = this.getStep(stepId);
1114
+ const now = Date.now();
1115
+ const until = now + Math.trunc(delayMs);
1116
+ const comment = upsertWaitInComment(step.comment, until, reason);
1117
+ this.db.prepare("UPDATE steps SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, stepId);
1118
+ const updated = this.getStep(stepId);
1119
+ this.touchPlan(updated.plan_id);
1120
+ return { step: updated, until };
1121
+ });
1122
+ return tx();
1123
+ }
1124
+ clearStepWait(stepId) {
1125
+ const tx = this.db.transaction(() => {
1126
+ const step = this.getStep(stepId);
1127
+ const comment = step.comment ? removeWaitFromComment(step.comment) : null;
1128
+ const now = Date.now();
1129
+ this.db.prepare("UPDATE steps SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, stepId);
1130
+ const updated = this.getStep(stepId);
1131
+ this.touchPlan(updated.plan_id);
1132
+ return { step: updated };
1133
+ });
1134
+ return tx();
1135
+ }
1136
+ getStepWait(stepId) {
1137
+ const step = this.getStep(stepId);
1138
+ const wait = parseWaitFromComment(step.comment);
1139
+ return { step, wait };
1140
+ }
1141
+ commentGoals(entries) {
1142
+ const normalized = normalizeCommentEntries(entries);
1143
+ if (!normalized.length) return [];
1144
+ const ids = normalized.map(([id]) => id);
1145
+ const tx = this.db.transaction(() => {
1146
+ const goals = this.db.prepare(`SELECT * FROM goals WHERE id IN (${ids.map(() => "?").join(",")})`).all(...ids);
1147
+ const existing = new Set(goals.map((goal) => goal.id));
1148
+ const missing = ids.filter((id) => !existing.has(id));
1149
+ if (missing.length) {
1150
+ throw notFound(`goal id(s) not found: ${joinIds(missing)}`);
1151
+ }
1152
+ const stepIds = Array.from(new Set(goals.map((goal) => goal.step_id)));
1153
+ const now = Date.now();
1154
+ normalized.forEach(([goalId, comment]) => {
1155
+ this.db.prepare("UPDATE goals SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, goalId);
1156
+ });
1157
+ if (stepIds.length) {
1158
+ const planIds = [];
1159
+ const steps = this.db.prepare(`SELECT plan_id FROM steps WHERE id IN (${stepIds.map(() => "?").join(",")})`).all(...stepIds);
1160
+ const seen = /* @__PURE__ */ new Set();
1161
+ steps.forEach((row) => {
1162
+ if (!seen.has(row.plan_id)) {
1163
+ seen.add(row.plan_id);
1164
+ planIds.push(row.plan_id);
1165
+ }
1166
+ });
1167
+ if (planIds.length) this.touchPlans(planIds);
1168
+ return planIds;
1169
+ }
1170
+ return [];
1171
+ });
1172
+ return tx();
1173
+ }
1174
+ goalsForStep(stepId) {
1175
+ return this.db.prepare("SELECT * FROM goals WHERE step_id = ? ORDER BY id ASC").all(stepId);
1176
+ }
1177
+ goalsForSteps(stepIds) {
1178
+ const grouped = /* @__PURE__ */ new Map();
1179
+ if (!stepIds.length) return grouped;
1180
+ const rows = this.db.prepare(`SELECT * FROM goals WHERE step_id IN (${stepIds.map(() => "?").join(",")}) ORDER BY step_id ASC, id ASC`).all(...stepIds);
1181
+ rows.forEach((goal) => {
1182
+ const list = grouped.get(goal.step_id);
1183
+ if (list) list.push(goal);
1184
+ else grouped.set(goal.step_id, [goal]);
1185
+ });
1186
+ return grouped;
1187
+ }
1188
+ planIdsForSteps(ids) {
1189
+ if (!ids.length) return [];
1190
+ const unique = uniqueIds(ids);
1191
+ const rows = this.db.prepare(`SELECT plan_id FROM steps WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
1192
+ const seen = /* @__PURE__ */ new Set();
1193
+ const planIds = [];
1194
+ rows.forEach((row) => {
1195
+ if (!seen.has(row.plan_id)) {
1196
+ seen.add(row.plan_id);
1197
+ planIds.push(row.plan_id);
1198
+ }
1199
+ });
1200
+ return planIds;
1201
+ }
1202
+ planIdsForGoals(ids) {
1203
+ if (!ids.length) return [];
1204
+ const unique = uniqueIds(ids);
1205
+ const goals = this.db.prepare(`SELECT step_id FROM goals WHERE id IN (${unique.map(() => "?").join(",")})`).all(...unique);
1206
+ const stepIds = Array.from(new Set(goals.map((row) => row.step_id)));
1207
+ if (!stepIds.length) return [];
1208
+ const steps = this.db.prepare(`SELECT plan_id FROM steps WHERE id IN (${stepIds.map(() => "?").join(",")})`).all(...stepIds);
1209
+ const seen = /* @__PURE__ */ new Set();
1210
+ const planIds = [];
1211
+ steps.forEach((row) => {
1212
+ if (!seen.has(row.plan_id)) {
1213
+ seen.add(row.plan_id);
1214
+ planIds.push(row.plan_id);
1215
+ }
1216
+ });
1217
+ return planIds;
1218
+ }
1219
+ updatePlanWithConn(id, changes) {
1220
+ if (changes.title !== void 0) ensureNonEmpty("plan title", changes.title);
1221
+ if (changes.content !== void 0) ensureNonEmpty("plan content", changes.content);
1222
+ if (changes.status === "done") {
1223
+ const total = this.db.prepare("SELECT COUNT(*) as count FROM steps WHERE plan_id = ?").get(id);
1224
+ if (total.count > 0) {
1225
+ const next = this.nextStep(id);
1226
+ if (next) {
1227
+ const goals = this.goalsForStep(next.id);
1228
+ const detail = formatStepDetail(next, goals);
1229
+ throw invalidInput(`cannot mark plan done; next pending step:
1230
+ ${detail}`);
1231
+ }
1232
+ }
1233
+ }
1234
+ const existing = this.getPlan(id);
1235
+ const now = Date.now();
1236
+ const updated = {
1237
+ title: changes.title ?? existing.title,
1238
+ content: changes.content ?? existing.content,
1239
+ status: changes.status ?? existing.status,
1240
+ comment: changes.comment !== void 0 ? changes.comment : existing.comment
1241
+ };
1242
+ if (this.cwd) {
1243
+ this.db.prepare(
1244
+ `UPDATE plans SET title = ?, content = ?, status = ?, comment = ?, last_session_id = ?, last_cwd = ?, updated_at = ? WHERE id = ?`
1245
+ ).run(updated.title, updated.content, updated.status, updated.comment, this.sessionId, this.cwd, now, id);
1246
+ } else {
1247
+ this.db.prepare(
1248
+ `UPDATE plans SET title = ?, content = ?, status = ?, comment = ?, last_session_id = ?, updated_at = ? WHERE id = ?`
1249
+ ).run(updated.title, updated.content, updated.status, updated.comment, this.sessionId, now, id);
1250
+ }
1251
+ return this.getPlan(id);
1252
+ }
1253
+ refreshPlanStatus(planId) {
1254
+ const total = this.db.prepare("SELECT COUNT(*) as count FROM steps WHERE plan_id = ?").get(planId);
1255
+ if (total.count === 0) return createEmptyStatusChanges();
1256
+ const done = this.db.prepare("SELECT COUNT(*) as count FROM steps WHERE plan_id = ? AND status = ?").get(planId, "done");
1257
+ const status = done.count === total.count ? "done" : "todo";
1258
+ const plan = this.getPlan(planId);
1259
+ const changes = createEmptyStatusChanges();
1260
+ if (plan.status !== status) {
1261
+ const now = Date.now();
1262
+ const reason = done.count === total.count ? `all steps are done (${done.count}/${total.count})` : `steps done ${done.count}/${total.count}`;
1263
+ this.db.prepare("UPDATE plans SET status = ?, updated_at = ? WHERE id = ?").run(status, now, planId);
1264
+ changes.plans.push({ plan_id: planId, from: plan.status, to: status, reason });
1265
+ if (status === "done") {
1266
+ const clearedCurrent = this.clearActivePlansForPlanWithConn(planId);
1267
+ if (clearedCurrent) {
1268
+ changes.active_plans_cleared.push({ plan_id: planId, reason: "plan marked done" });
1269
+ }
1270
+ }
1271
+ }
1272
+ return changes;
1273
+ }
1274
+ refreshStepStatus(stepId) {
1275
+ const goals = this.db.prepare("SELECT * FROM goals WHERE step_id = ? ORDER BY id ASC").all(stepId);
1276
+ if (!goals.length) return createEmptyStatusChanges();
1277
+ const doneCount = goals.filter((goal) => goal.status === "done").length;
1278
+ const total = goals.length;
1279
+ const status = doneCount === total ? "done" : "todo";
1280
+ const step = this.getStep(stepId);
1281
+ const changes = createEmptyStatusChanges();
1282
+ if (step.status !== status) {
1283
+ const now = Date.now();
1284
+ const reason = doneCount === total ? `all goals are done (${doneCount}/${total})` : `goals done ${doneCount}/${total}`;
1285
+ this.db.prepare("UPDATE steps SET status = ?, updated_at = ? WHERE id = ?").run(status, now, stepId);
1286
+ changes.steps.push({ step_id: stepId, from: step.status, to: status, reason });
1287
+ }
1288
+ mergeStatusChanges(changes, this.refreshPlanStatus(step.plan_id));
1289
+ return changes;
1290
+ }
1291
+ nextGoalForStep(stepId) {
1292
+ const row = this.db.prepare("SELECT * FROM goals WHERE step_id = ? AND status = ? ORDER BY id ASC LIMIT 1").get(stepId, "todo");
1293
+ return row ?? null;
1294
+ }
1295
+ setAllGoalsDoneForStep(stepId) {
1296
+ this.getStep(stepId);
1297
+ const goals = this.goalsForStep(stepId);
1298
+ if (!goals.length) return createEmptyStatusChanges();
1299
+ const ids = goals.map((goal) => goal.id);
1300
+ return this.setGoalsStatus(ids, "done").changes;
1301
+ }
1302
+ touchPlan(planId) {
1303
+ const now = Date.now();
1304
+ if (this.cwd) {
1305
+ const result = this.db.prepare("UPDATE plans SET last_session_id = ?, last_cwd = ?, updated_at = ? WHERE id = ?").run(this.sessionId, this.cwd, now, planId);
1306
+ if (result.changes === 0) {
1307
+ throw notFound(`plan id ${planId}`);
1308
+ }
1309
+ } else {
1310
+ const result = this.db.prepare("UPDATE plans SET last_session_id = ?, updated_at = ? WHERE id = ?").run(this.sessionId, now, planId);
1311
+ if (result.changes === 0) {
1312
+ throw notFound(`plan id ${planId}`);
1313
+ }
1314
+ }
1315
+ }
1316
+ touchPlans(planIds) {
1317
+ planIds.forEach((planId) => this.touchPlan(planId));
1318
+ }
1319
+ normalizeStepsForPlan(planId) {
1320
+ const steps = this.db.prepare("SELECT * FROM steps WHERE plan_id = ? ORDER BY sort_order ASC, id ASC").all(planId);
1321
+ this.normalizeStepsInPlace(steps);
1322
+ }
1323
+ normalizeStepsInPlace(steps) {
1324
+ const now = Date.now();
1325
+ steps.forEach((step, idx) => {
1326
+ const desired = idx + 1;
1327
+ if (step.sort_order !== desired) {
1328
+ this.db.prepare("UPDATE steps SET sort_order = ?, updated_at = ? WHERE id = ?").run(desired, now, step.id);
1329
+ step.sort_order = desired;
1330
+ step.updated_at = now;
1331
+ }
1332
+ });
1333
+ }
1334
+ clearActivePlansForPlanWithConn(planId) {
1335
+ const existing = this.db.prepare("SELECT * FROM active_plan WHERE plan_id = ?").all(planId);
1336
+ const clearedCurrent = existing.some((row) => row.session_id === this.sessionId);
1337
+ this.db.prepare("DELETE FROM active_plan WHERE plan_id = ?").run(planId);
1338
+ return clearedCurrent;
1339
+ }
1340
+ };
1341
+
1342
+ // src/prompt.ts
1343
+ var PLANPILOT_HELP_TEXT = [
1344
+ "Planpilot - Plan/Step/Goal auto-continue workflow.",
1345
+ "",
1346
+ "Model:",
1347
+ "- plan -> step -> goal",
1348
+ "- step.executor: ai | human",
1349
+ "- status rolls up: goals -> steps -> plan",
1350
+ "",
1351
+ "Rules (important):",
1352
+ "- Prefer assigning steps to ai. Use human steps only for actions that require human approval/credentials",
1353
+ " or elevated permissions / destructive operations (e.g. GitHub web UI, sudo, deleting data/files).",
1354
+ "- One step = one executor. Do NOT mix ai/human work within the same step.",
1355
+ " If a person must do something, create a separate human step.",
1356
+ "- Keep statuses up to date. Mark goals/steps done as soon as they are completed so roll-up and auto-continue stay correct.",
1357
+ "- `step wait` is ONLY for asynchronous, non-blocking external work that is already in progress",
1358
+ " (build/test/job/CI/network/remote service).",
1359
+ " If you can run something now, do it instead of waiting.",
1360
+ " Do NOT use `step wait` to wait for human action.",
1361
+ "- Keep comments short and decision-focused.",
1362
+ "",
1363
+ "Status propagation:",
1364
+ "- Step with goals: done iff ALL goals are done; else todo.",
1365
+ "- Plan with steps: done iff ALL steps are done; else todo.",
1366
+ "- Step with 0 goals: manual status (`step update` / `step done`).",
1367
+ "- Plan with 0 steps: manual status (`plan update` / `plan done`).",
1368
+ "- When a plan becomes done, it is removed from active plan.",
1369
+ "",
1370
+ "Auto-continue:",
1371
+ "- When the session is idle and an active plan exists:",
1372
+ " - if next pending step.executor is ai: Planpilot auto-sends the next step + goals.",
1373
+ " - if next pending step.executor is human: no auto-continue.",
1374
+ "- Pause while waiting on external systems: `step wait`.",
1375
+ "- Stop auto-continue:",
1376
+ " - `plan deactivate`, OR",
1377
+ " - insert a human step BEFORE the next pending ai step (so the next executor becomes human).",
1378
+ "",
1379
+ "Invocation:",
1380
+ "- argv is tokenized: [section, subcommand, ...args]",
1381
+ "- section: help | plan | step | goal",
1382
+ "",
1383
+ "Commands:",
1384
+ "- help",
1385
+ "",
1386
+ "Plan:",
1387
+ "- plan add-tree <title> <content> --step <content> [--executor ai|human] [--goal <content>]... [--step ...]...",
1388
+ "- plan list [--scope project|all] [--status todo|done|all] [--limit N] [--page N] [--order id|title|created|updated] [--desc]",
1389
+ "- plan count [--scope project|all] [--status todo|done|all]",
1390
+ "- plan search --search <term> [--search <term> ...] [--search-mode any|all] [--search-field plan|title|content|comment|steps|goals|all] [--match-case] [--scope project|all] [--status todo|done|all] [--limit N] [--page N] [--order id|title|created|updated] [--desc]",
1391
+ "- plan show <id>",
1392
+ "- plan export <id> <path>",
1393
+ "- plan comment <id> <comment> [<id> <comment> ...]",
1394
+ "- plan update <id> [--title <title>] [--content <content>] [--status todo|done] [--comment <comment>]",
1395
+ "- plan done <id>",
1396
+ "- plan remove <id>",
1397
+ "- plan activate <id> [--force]",
1398
+ "- plan show-active",
1399
+ "- plan deactivate",
1400
+ "",
1401
+ "Step:",
1402
+ "- step add <plan_id> <content...> [--executor ai|human] [--at <pos>]",
1403
+ "- step add-tree <plan_id> <content> [--executor ai|human] [--goal <content> ...]",
1404
+ "- step list <plan_id> [--status todo|done|all] [--executor ai|human] [--limit N] [--page N]",
1405
+ "- step count <plan_id> [--status todo|done|all] [--executor ai|human]",
1406
+ "- step show <id>",
1407
+ "- step show-next",
1408
+ "- step wait <id> --delay <ms> [--reason <text>]",
1409
+ "- step wait <id> --clear",
1410
+ "- step comment <id> <comment> [<id> <comment> ...]",
1411
+ "- step update <id> [--content <content>] [--status todo|done] [--executor ai|human] [--comment <comment>]",
1412
+ "- step done <id> [--all-goals]",
1413
+ "- step move <id> --to <pos>",
1414
+ "- step remove <id...>",
1415
+ "",
1416
+ "Goal:",
1417
+ "- goal add <step_id> <content...>",
1418
+ "- goal list <step_id> [--status todo|done|all] [--limit N] [--page N]",
1419
+ "- goal count <step_id> [--status todo|done|all]",
1420
+ "- goal show <id>",
1421
+ "- goal comment <id> <comment> [<id> <comment> ...]",
1422
+ "- goal update <id> [--content <content>] [--status todo|done] [--comment <comment>]",
1423
+ "- goal done <id...>",
1424
+ "- goal remove <id...>"
1425
+ ].join("\n");
1426
+ var PLANPILOT_TOOL_DESCRIPTION = [
1427
+ "Planpilot planner for auto-continue plan workflows.",
1428
+ "For multi-step and complex tasks, use Planpilot to structure work into plans/steps/goals.",
1429
+ "Run `planpilot help` for full usage + rules."
1430
+ ].join("\n");
1431
+ var PLANPILOT_SYSTEM_INJECTION = "If the task is multi-step or complex, must use the `planpilot` plan tool. For full usage + rules, run: planpilot help.";
1432
+ function formatPlanpilotAutoContinueMessage(input) {
1433
+ const detail = (input.stepDetail ?? "").trimEnd();
1434
+ const trigger = (input.triggerDetail ?? "").trim();
1435
+ return [
1436
+ `Planpilot @ ${input.timestamp}`,
1437
+ "This message was automatically sent by the Planpilot tool because the next pending step executor is ai.",
1438
+ ...trigger ? [`Trigger context: ${trigger}`] : [],
1439
+ "For full usage + rules, run: planpilot help",
1440
+ "Next step details:",
1441
+ detail
1442
+ ].join("\n");
1443
+ }
1444
+
1445
+ // src/command.ts
1446
+ var DEFAULT_PAGE = 1;
1447
+ var DEFAULT_LIMIT = 20;
1448
+ var noopIO = {
1449
+ log: () => {
1450
+ }
1451
+ };
1452
+ var currentIO = noopIO;
1453
+ function log(...args) {
1454
+ currentIO.log(...args);
1455
+ }
1456
+ async function withIO(io, fn) {
1457
+ const prev = currentIO;
1458
+ currentIO = io;
1459
+ try {
1460
+ return await fn();
1461
+ } finally {
1462
+ currentIO = prev;
1463
+ }
1464
+ }
1465
+ function formatCommandError(err) {
1466
+ if (err instanceof AppError) {
1467
+ return `Error: ${err.toDisplayString()}`;
1468
+ }
1469
+ if (err instanceof Error) {
1470
+ return `Error: ${err.message}`;
1471
+ }
1472
+ return `Error: ${String(err)}`;
1473
+ }
1474
+ async function runCommand(argv, context, io = noopIO) {
1475
+ return withIO(io, async () => {
1476
+ if (!argv.length) {
1477
+ throw invalidInput("missing argv");
1478
+ }
1479
+ const [section, subcommand, ...args] = argv;
1480
+ const db = openDatabase();
1481
+ const resolvedCwd = resolveMaybeRealpath(context.cwd);
1482
+ const app = new PlanpilotApp(db, context.sessionId, resolvedCwd);
1483
+ let planIds = [];
1484
+ let shouldSync = false;
1485
+ switch (section) {
1486
+ case "help": {
1487
+ if (subcommand !== void 0 || args.length) {
1488
+ const rest = [subcommand, ...args].filter((x) => x !== void 0);
1489
+ throw invalidInput(`help unexpected argument: ${rest.join(" ")}`);
1490
+ }
1491
+ log(PLANPILOT_HELP_TEXT);
1492
+ return;
1493
+ }
1494
+ case "plan": {
1495
+ const result = await handlePlan(app, subcommand, args, { cwd: context.cwd });
1496
+ planIds = result.planIds;
1497
+ shouldSync = result.shouldSync;
1498
+ break;
1499
+ }
1500
+ case "step": {
1501
+ const result = await handleStep(app, subcommand, args);
1502
+ planIds = result.planIds;
1503
+ shouldSync = result.shouldSync;
1504
+ break;
1505
+ }
1506
+ case "goal": {
1507
+ const result = await handleGoal(app, subcommand, args);
1508
+ planIds = result.planIds;
1509
+ shouldSync = result.shouldSync;
1510
+ break;
1511
+ }
1512
+ default:
1513
+ throw invalidInput(`unknown command: ${section}`);
1514
+ }
1515
+ if (shouldSync) {
1516
+ syncPlanMarkdown(app, planIds);
1517
+ }
1518
+ });
1519
+ }
1520
+ function requireCwd(cwd) {
1521
+ if (!cwd || !cwd.trim()) {
1522
+ throw invalidInput("cwd is required");
1523
+ }
1524
+ return cwd;
1525
+ }
1526
+ async function handlePlan(app, subcommand, args, context) {
1527
+ switch (subcommand) {
1528
+ case "add-tree":
1529
+ return { planIds: handlePlanAddTree(app, args), shouldSync: true };
1530
+ case "list":
1531
+ return { planIds: handlePlanList(app, args, context), shouldSync: false };
1532
+ case "count":
1533
+ return { planIds: handlePlanCount(app, args, context), shouldSync: false };
1534
+ case "search":
1535
+ return { planIds: handlePlanSearch(app, args, context), shouldSync: false };
1536
+ case "show":
1537
+ return { planIds: handlePlanShow(app, args), shouldSync: false };
1538
+ case "export":
1539
+ return { planIds: handlePlanExport(app, args), shouldSync: false };
1540
+ case "comment":
1541
+ return { planIds: handlePlanComment(app, args), shouldSync: true };
1542
+ case "update":
1543
+ return { planIds: handlePlanUpdate(app, args), shouldSync: true };
1544
+ case "done":
1545
+ return { planIds: handlePlanDone(app, args), shouldSync: true };
1546
+ case "remove":
1547
+ return { planIds: handlePlanRemove(app, args), shouldSync: true };
1548
+ case "activate":
1549
+ return { planIds: handlePlanActivate(app, args), shouldSync: true };
1550
+ case "show-active":
1551
+ return { planIds: handlePlanActive(app), shouldSync: false };
1552
+ case "deactivate":
1553
+ return { planIds: handlePlanDeactivate(app), shouldSync: true };
1554
+ default:
1555
+ throw invalidInput(`unknown plan command: ${subcommand ?? ""}`);
1556
+ }
1557
+ }
1558
+ async function handleStep(app, subcommand, args) {
1559
+ switch (subcommand) {
1560
+ case "add":
1561
+ return { planIds: handleStepAdd(app, args), shouldSync: true };
1562
+ case "add-tree":
1563
+ return { planIds: handleStepAddTree(app, args), shouldSync: true };
1564
+ case "list":
1565
+ return { planIds: handleStepList(app, args), shouldSync: false };
1566
+ case "count":
1567
+ return { planIds: handleStepCount(app, args), shouldSync: false };
1568
+ case "show":
1569
+ return { planIds: handleStepShow(app, args), shouldSync: false };
1570
+ case "show-next":
1571
+ return { planIds: handleStepShowNext(app), shouldSync: false };
1572
+ case "wait":
1573
+ return { planIds: handleStepWait(app, args), shouldSync: true };
1574
+ case "comment":
1575
+ return { planIds: handleStepComment(app, args), shouldSync: true };
1576
+ case "update":
1577
+ return { planIds: handleStepUpdate(app, args), shouldSync: true };
1578
+ case "done":
1579
+ return { planIds: handleStepDone(app, args), shouldSync: true };
1580
+ case "move":
1581
+ return { planIds: handleStepMove(app, args), shouldSync: true };
1582
+ case "remove":
1583
+ return { planIds: handleStepRemove(app, args), shouldSync: true };
1584
+ default:
1585
+ throw invalidInput(`unknown step command: ${subcommand ?? ""}`);
1586
+ }
1587
+ }
1588
+ async function handleGoal(app, subcommand, args) {
1589
+ switch (subcommand) {
1590
+ case "add":
1591
+ return { planIds: handleGoalAdd(app, args), shouldSync: true };
1592
+ case "list":
1593
+ return { planIds: handleGoalList(app, args), shouldSync: false };
1594
+ case "count":
1595
+ return { planIds: handleGoalCount(app, args), shouldSync: false };
1596
+ case "show":
1597
+ return { planIds: handleGoalShow(app, args), shouldSync: false };
1598
+ case "comment":
1599
+ return { planIds: handleGoalComment(app, args), shouldSync: true };
1600
+ case "update":
1601
+ return { planIds: handleGoalUpdate(app, args), shouldSync: true };
1602
+ case "done":
1603
+ return { planIds: handleGoalDone(app, args), shouldSync: true };
1604
+ case "remove":
1605
+ return { planIds: handleGoalRemove(app, args), shouldSync: true };
1606
+ default:
1607
+ throw invalidInput(`unknown goal command: ${subcommand ?? ""}`);
1608
+ }
1609
+ }
1610
+ function handlePlanAddTree(app, args) {
1611
+ const [title, content, ...rest] = args;
1612
+ if (!title || content === void 0) {
1613
+ throw invalidInput("plan add-tree requires <title> <content> and at least one --step");
1614
+ }
1615
+ ensureNonEmpty("plan title", title);
1616
+ ensureNonEmpty("plan content", content);
1617
+ const specs = parsePlanAddTreeSteps(rest);
1618
+ if (!specs.length) {
1619
+ throw invalidInput("plan add-tree requires at least one --step");
1620
+ }
1621
+ const steps = specs.map((spec) => ({
1622
+ content: spec.content,
1623
+ executor: spec.executor ?? "ai",
1624
+ goals: spec.goals ?? []
1625
+ }));
1626
+ const result = app.addPlanTree({ title, content }, steps);
1627
+ log(`Created plan ID: ${result.plan.id}: ${result.plan.title} (steps: ${result.stepCount}, goals: ${result.goalCount})`);
1628
+ app.setActivePlan(result.plan.id, false);
1629
+ log(`Active plan set to ${result.plan.id}: ${result.plan.title}`);
1630
+ const detail = app.getPlanDetail(result.plan.id);
1631
+ log("");
1632
+ log(formatPlanDetail(detail.plan, detail.steps, detail.goals));
1633
+ return [result.plan.id];
1634
+ }
1635
+ function handlePlanList(app, args, context) {
1636
+ const { options, positionals } = parseOptions(args);
1637
+ if (positionals.length) {
1638
+ throw invalidInput(`plan list unexpected argument: ${positionals.join(" ")}`);
1639
+ }
1640
+ if (options.search && options.search.length) {
1641
+ throw invalidInput("plan list does not accept --search");
1642
+ }
1643
+ const allowed = /* @__PURE__ */ new Set(["scope", "status", "limit", "page", "order", "desc", "search"]);
1644
+ for (const key of Object.keys(options)) {
1645
+ if (!allowed.has(key)) {
1646
+ throw invalidInput(`plan list does not support --${key}`);
1647
+ }
1648
+ }
1649
+ const desiredStatus = parsePlanStatusFilter(options.status);
1650
+ const cwd = requireCwd(context.cwd);
1651
+ const order = options.order ? parsePlanOrder(options.order) : "updated";
1652
+ const desc = options.desc ?? true;
1653
+ let plans = app.listPlans(order, desc);
1654
+ if (!plans.length) {
1655
+ log("No plans found.");
1656
+ return [];
1657
+ }
1658
+ plans = plans.filter((plan) => desiredStatus ? plan.status === desiredStatus : true);
1659
+ const scope = parseScope(options.scope);
1660
+ if (scope === "project") {
1661
+ const cwdValue = resolveMaybeRealpath(cwd);
1662
+ plans = plans.filter((plan) => plan.last_cwd && projectMatchesPath(plan.last_cwd, cwdValue));
1663
+ }
1664
+ if (!plans.length) {
1665
+ log("No plans found.");
1666
+ return [];
1667
+ }
1668
+ const pagination = resolvePagination(options, { limit: DEFAULT_LIMIT, page: DEFAULT_PAGE });
1669
+ const total = plans.length;
1670
+ if (total === 0) {
1671
+ log("No plans found.");
1672
+ return [];
1673
+ }
1674
+ const totalPages = Math.ceil(total / pagination.limit);
1675
+ if (pagination.page > totalPages) {
1676
+ log(`Page ${pagination.page} exceeds total pages ${totalPages}.`);
1677
+ return [];
1678
+ }
1679
+ const start = pagination.offset;
1680
+ const end = start + pagination.limit;
1681
+ plans = plans.slice(start, end);
1682
+ const details = app.getPlanDetails(plans);
1683
+ printPlanList(details);
1684
+ logPageFooter(pagination.page, pagination.limit);
1685
+ return [];
1686
+ }
1687
+ function handlePlanCount(app, args, context) {
1688
+ const { options, positionals } = parseOptions(args);
1689
+ if (positionals.length) {
1690
+ throw invalidInput(`plan count unexpected argument: ${positionals.join(" ")}`);
1691
+ }
1692
+ if (options.search && options.search.length) {
1693
+ throw invalidInput("plan count does not accept --search");
1694
+ }
1695
+ const allowed = /* @__PURE__ */ new Set(["scope", "status", "search"]);
1696
+ for (const key of Object.keys(options)) {
1697
+ if (!allowed.has(key)) {
1698
+ throw invalidInput(`plan count does not support --${key}`);
1699
+ }
1700
+ }
1701
+ const desiredStatus = parsePlanStatusFilter(options.status);
1702
+ const cwd = requireCwd(context.cwd);
1703
+ let plans = app.listPlans();
1704
+ if (!plans.length) {
1705
+ log("Total: 0");
1706
+ return [];
1707
+ }
1708
+ plans = plans.filter((plan) => desiredStatus ? plan.status === desiredStatus : true);
1709
+ const scope = parseScope(options.scope);
1710
+ if (scope === "project") {
1711
+ const cwdValue = resolveMaybeRealpath(cwd);
1712
+ plans = plans.filter((plan) => plan.last_cwd && projectMatchesPath(plan.last_cwd, cwdValue));
1713
+ }
1714
+ log(`Total: ${plans.length}`);
1715
+ return [];
1716
+ }
1717
+ function handlePlanSearch(app, args, context) {
1718
+ const { options, positionals } = parseOptions(args);
1719
+ if (positionals.length) {
1720
+ throw invalidInput(`plan search unexpected argument: ${positionals.join(" ")}`);
1721
+ }
1722
+ if (options.search && !options.search.length) {
1723
+ throw invalidInput("plan search requires at least one --search");
1724
+ }
1725
+ const desiredStatus = parsePlanStatusFilter(options.status);
1726
+ const cwd = requireCwd(context.cwd);
1727
+ const order = options.order ? parsePlanOrder(options.order) : "updated";
1728
+ const desc = options.desc ?? true;
1729
+ let plans = app.listPlans(order, desc);
1730
+ if (!plans.length) {
1731
+ log("No plans found.");
1732
+ return [];
1733
+ }
1734
+ plans = plans.filter((plan) => desiredStatus ? plan.status === desiredStatus : true);
1735
+ const scope = parseScope(options.scope);
1736
+ if (scope === "project") {
1737
+ const cwdValue = resolveMaybeRealpath(cwd);
1738
+ plans = plans.filter((plan) => plan.last_cwd && projectMatchesPath(plan.last_cwd, cwdValue));
1739
+ }
1740
+ if (!plans.length) {
1741
+ log("No plans found.");
1742
+ return [];
1743
+ }
1744
+ const details = app.getPlanDetails(plans);
1745
+ const query = new PlanSearchQuery(options.search, options.searchMode, options.searchField, options.matchCase);
1746
+ const filtered = details.filter((detail) => planMatchesSearch(detail, query));
1747
+ if (!filtered.length) {
1748
+ log("No plans found.");
1749
+ return [];
1750
+ }
1751
+ const pagination = resolvePagination(options, { limit: DEFAULT_LIMIT, page: DEFAULT_PAGE });
1752
+ const totalPages = Math.ceil(filtered.length / pagination.limit);
1753
+ if (pagination.page > totalPages) {
1754
+ log(`Page ${pagination.page} exceeds total pages ${totalPages}.`);
1755
+ return [];
1756
+ }
1757
+ const start = pagination.offset;
1758
+ const end = start + pagination.limit;
1759
+ const paged = filtered.slice(start, end);
1760
+ printPlanList(paged);
1761
+ logPageFooter(pagination.page, pagination.limit);
1762
+ return [];
1763
+ }
1764
+ function handlePlanShow(app, args) {
1765
+ const id = parseIdArg(args, "plan show");
1766
+ const detail = app.getPlanDetail(id);
1767
+ log(formatPlanDetail(detail.plan, detail.steps, detail.goals));
1768
+ return [];
1769
+ }
1770
+ function handlePlanExport(app, args) {
1771
+ if (args.length < 2) {
1772
+ throw invalidInput("plan export requires <id> <path>");
1773
+ }
1774
+ const id = parseNumber(args[0], "plan id");
1775
+ const filePath = args[1];
1776
+ const detail = app.getPlanDetail(id);
1777
+ const active = app.getActivePlan();
1778
+ const isActive = active?.plan_id === detail.plan.id;
1779
+ const activatedAt = isActive ? active?.updated_at ?? null : null;
1780
+ ensureParentDir(filePath);
1781
+ const markdown = formatPlanMarkdown(isActive, activatedAt ?? null, detail.plan, detail.steps, detail.goals);
1782
+ fs3.writeFileSync(filePath, markdown, "utf8");
1783
+ log(`Exported plan ID: ${detail.plan.id} to ${filePath}`);
1784
+ return [];
1785
+ }
1786
+ function handlePlanComment(app, args) {
1787
+ const entries = parseCommentPairs("plan", args);
1788
+ const planIds = app.commentPlans(entries);
1789
+ if (planIds.length === 1) {
1790
+ log(`Updated plan comment for plan ID: ${planIds[0]}.`);
1791
+ } else {
1792
+ log(`Updated plan comments for ${planIds.length} plans.`);
1793
+ }
1794
+ return planIds;
1795
+ }
1796
+ function handlePlanUpdate(app, args) {
1797
+ if (!args.length) {
1798
+ throw invalidInput("plan update requires <id>");
1799
+ }
1800
+ const id = parseNumber(args[0], "plan id");
1801
+ const { options } = parseOptions(args.slice(1));
1802
+ if (options.content !== void 0) {
1803
+ ensureNonEmpty("plan content", options.content);
1804
+ }
1805
+ const changes = {
1806
+ title: options.title,
1807
+ content: options.content,
1808
+ status: options.status ? parsePlanStatus(options.status) : void 0,
1809
+ comment: options.comment
1810
+ };
1811
+ const result = app.updatePlanWithActiveClear(id, changes);
1812
+ log(`Updated plan ID: ${result.plan.id}: ${result.plan.title}`);
1813
+ if (result.cleared) {
1814
+ log("Active plan deactivated because plan is done.");
1815
+ }
1816
+ if (result.plan.status === "done") {
1817
+ notifyPlanCompleted(result.plan.id);
1818
+ }
1819
+ return [result.plan.id];
1820
+ }
1821
+ function handlePlanDone(app, args) {
1822
+ const id = parseIdArg(args, "plan done");
1823
+ const result = app.updatePlanWithActiveClear(id, { status: "done" });
1824
+ log(`Plan ID: ${result.plan.id} marked done.`);
1825
+ if (result.cleared) {
1826
+ log("Active plan deactivated because plan is done.");
1827
+ }
1828
+ if (result.plan.status === "done") {
1829
+ notifyPlanCompleted(result.plan.id);
1830
+ }
1831
+ return [result.plan.id];
1832
+ }
1833
+ function handlePlanRemove(app, args) {
1834
+ const id = parseIdArg(args, "plan remove");
1835
+ app.deletePlan(id);
1836
+ log(`Plan ID: ${id} removed.`);
1837
+ return [];
1838
+ }
1839
+ function handlePlanActivate(app, args) {
1840
+ if (!args.length) {
1841
+ throw invalidInput("plan activate requires <id>");
1842
+ }
1843
+ const id = parseNumber(args[0], "plan id");
1844
+ const force = args.slice(1).includes("--force");
1845
+ const plan = app.getPlan(id);
1846
+ if (plan.status === "done") {
1847
+ throw invalidInput("cannot activate plan; plan is done");
1848
+ }
1849
+ app.setActivePlan(id, force);
1850
+ log(`Active plan set to ${plan.id}: ${plan.title}`);
1851
+ return [plan.id];
1852
+ }
1853
+ function handlePlanActive(app) {
1854
+ const active = app.getActivePlan();
1855
+ if (!active) {
1856
+ log("No active plan.");
1857
+ return [];
1858
+ }
1859
+ try {
1860
+ const detail = app.getPlanDetail(active.plan_id);
1861
+ log(formatPlanDetail(detail.plan, detail.steps, detail.goals));
1862
+ return [];
1863
+ } catch (err) {
1864
+ if (err instanceof AppError && err.kind === "NotFound") {
1865
+ app.clearActivePlan();
1866
+ log(`Active plan ID: ${active.plan_id} not found.`);
1867
+ return [];
1868
+ }
1869
+ throw err;
1870
+ }
1871
+ }
1872
+ function handlePlanDeactivate(app) {
1873
+ const active = app.getActivePlan();
1874
+ app.clearActivePlan();
1875
+ log("Active plan deactivated.");
1876
+ return active ? [active.plan_id] : [];
1877
+ }
1878
+ function handleStepAdd(app, args) {
1879
+ if (args.length < 2) {
1880
+ throw invalidInput("step add requires <plan_id> <content...>");
1881
+ }
1882
+ const planId = parseNumber(args[0], "plan id");
1883
+ const parsed = parseStepAddArgs(args.slice(1));
1884
+ if (!parsed.contents.length) {
1885
+ throw invalidInput("no contents provided");
1886
+ }
1887
+ if (parsed.at !== void 0 && parsed.at === 0) {
1888
+ throw invalidInput("position starts at 1");
1889
+ }
1890
+ parsed.contents.forEach((content) => ensureNonEmpty("step content", content));
1891
+ const result = app.addStepsBatch(planId, parsed.contents, "todo", parsed.executor ?? "ai", parsed.at);
1892
+ if (result.steps.length === 1) {
1893
+ log(`Created step ID: ${result.steps[0].id} for plan ID: ${result.steps[0].plan_id}`);
1894
+ } else {
1895
+ log(`Created ${result.steps.length} steps for plan ID: ${planId}`);
1896
+ }
1897
+ printStatusChanges(result.changes);
1898
+ return [planId];
1899
+ }
1900
+ function handleStepAddTree(app, args) {
1901
+ if (args.length < 2) {
1902
+ throw invalidInput("step add-tree requires <plan_id> <content>");
1903
+ }
1904
+ const planId = parseNumber(args[0], "plan id");
1905
+ const content = args[1];
1906
+ const parsed = parseStepAddTreeArgs(args.slice(2));
1907
+ ensureNonEmpty("step content", content);
1908
+ parsed.goals.forEach((goal) => ensureNonEmpty("goal content", goal));
1909
+ const executor = parsed.executor ?? "ai";
1910
+ const result = app.addStepTree(planId, content, executor, parsed.goals);
1911
+ log(`Created step ID: ${result.step.id} for plan ID: ${result.step.plan_id} (goals: ${result.goals.length})`);
1912
+ printStatusChanges(result.changes);
1913
+ notifyAfterStepChanges(app, result.changes);
1914
+ notifyPlansCompleted(app, result.changes);
1915
+ return [planId];
1916
+ }
1917
+ function handleStepList(app, args) {
1918
+ if (!args.length) {
1919
+ throw invalidInput("step list requires <plan_id>");
1920
+ }
1921
+ const planId = parseNumber(args[0], "plan id");
1922
+ const { options } = parseOptions(args.slice(1));
1923
+ const allowed = /* @__PURE__ */ new Set(["status", "executor", "limit", "page"]);
1924
+ for (const key of Object.keys(options)) {
1925
+ if (key === "search" && Array.isArray(options.search) && options.search.length === 0) continue;
1926
+ if (!allowed.has(key)) {
1927
+ throw invalidInput(`step list does not support --${key}`);
1928
+ }
1929
+ }
1930
+ const status = parseStepStatusFilter(options.status);
1931
+ const pagination = resolvePagination(options, { limit: DEFAULT_LIMIT, page: DEFAULT_PAGE });
1932
+ const countQuery = {
1933
+ status,
1934
+ executor: options.executor ? parseStepExecutor(options.executor) : void 0,
1935
+ limit: void 0,
1936
+ offset: void 0,
1937
+ order: void 0,
1938
+ desc: void 0
1939
+ };
1940
+ const total = app.countSteps(planId, countQuery);
1941
+ if (total === 0) {
1942
+ log(`No steps found for plan ID: ${planId}.`);
1943
+ return [];
1944
+ }
1945
+ const totalPages = Math.ceil(total / pagination.limit);
1946
+ if (pagination.page > totalPages) {
1947
+ log(`Page ${pagination.page} exceeds total pages ${totalPages} for plan ID: ${planId}.`);
1948
+ return [];
1949
+ }
1950
+ const query = {
1951
+ status,
1952
+ executor: options.executor ? parseStepExecutor(options.executor) : void 0,
1953
+ limit: pagination.limit,
1954
+ offset: pagination.offset,
1955
+ order: "order",
1956
+ desc: false
1957
+ };
1958
+ const steps = app.listStepsFiltered(planId, query);
1959
+ const details = app.getStepsDetail(steps);
1960
+ printStepList(details);
1961
+ logPageFooter(pagination.page, pagination.limit);
1962
+ return [];
1963
+ }
1964
+ function handleStepCount(app, args) {
1965
+ if (!args.length) {
1966
+ throw invalidInput("step count requires <plan_id>");
1967
+ }
1968
+ const planId = parseNumber(args[0], "plan id");
1969
+ const { options } = parseOptions(args.slice(1));
1970
+ const allowed = /* @__PURE__ */ new Set(["status", "executor"]);
1971
+ for (const key of Object.keys(options)) {
1972
+ if (key === "search" && Array.isArray(options.search) && options.search.length === 0) continue;
1973
+ if (!allowed.has(key)) {
1974
+ throw invalidInput(`step count does not support --${key}`);
1975
+ }
1976
+ }
1977
+ const status = parseStepStatusFilter(options.status);
1978
+ const query = {
1979
+ status,
1980
+ executor: options.executor ? parseStepExecutor(options.executor) : void 0,
1981
+ limit: void 0,
1982
+ offset: void 0,
1983
+ order: void 0,
1984
+ desc: void 0
1985
+ };
1986
+ const total = app.countSteps(planId, query);
1987
+ log(`Total: ${total}`);
1988
+ return [];
1989
+ }
1990
+ function handleStepShow(app, args) {
1991
+ const id = parseIdArg(args, "step show");
1992
+ const detail = app.getStepDetail(id);
1993
+ log(formatStepDetail(detail.step, detail.goals));
1994
+ return [];
1995
+ }
1996
+ function handleStepShowNext(app) {
1997
+ const active = app.getActivePlan();
1998
+ if (!active) {
1999
+ log("No active plan.");
2000
+ return [];
2001
+ }
2002
+ const next = app.nextStep(active.plan_id);
2003
+ if (!next) {
2004
+ log("No pending step.");
2005
+ return [];
2006
+ }
2007
+ const goals = app.goalsForStep(next.id);
2008
+ log(formatStepDetail(next, goals));
2009
+ return [];
2010
+ }
2011
+ function handleStepWait(app, args) {
2012
+ if (!args.length) {
2013
+ throw invalidInput("step wait requires <id>");
2014
+ }
2015
+ const stepId = parseNumber(args[0], "step id");
2016
+ const options = parseOptions(args.slice(1)).options;
2017
+ if (options.clear) {
2018
+ const result2 = app.clearStepWait(stepId);
2019
+ log(`Step ID: ${result2.step.id} wait cleared.`);
2020
+ return [result2.step.plan_id];
2021
+ }
2022
+ if (options.delay === void 0) {
2023
+ throw invalidInput("step wait requires --delay <ms> or --clear");
2024
+ }
2025
+ const delayMs = parseNumber(options.delay, "delay");
2026
+ if (delayMs < 0) {
2027
+ throw invalidInput("delay must be >= 0");
2028
+ }
2029
+ const reason = options.reason ? String(options.reason) : void 0;
2030
+ const result = app.setStepWait(stepId, delayMs, reason);
2031
+ log(`Step ID: ${result.step.id} waiting until ${result.until}.`);
2032
+ return [result.step.plan_id];
2033
+ }
2034
+ function handleStepUpdate(app, args) {
2035
+ if (!args.length) {
2036
+ throw invalidInput("step update requires <id>");
2037
+ }
2038
+ const id = parseNumber(args[0], "step id");
2039
+ const { options } = parseOptions(args.slice(1));
2040
+ if (options.content !== void 0) {
2041
+ ensureNonEmpty("step content", options.content);
2042
+ }
2043
+ const status = options.status ? parseStepStatus(options.status) : void 0;
2044
+ const result = app.updateStep(id, {
2045
+ content: options.content,
2046
+ status,
2047
+ executor: options.executor ? parseStepExecutor(options.executor) : void 0,
2048
+ comment: options.comment
2049
+ });
2050
+ log(`Updated step ID: ${result.step.id}.`);
2051
+ printStatusChanges(result.changes);
2052
+ if (status === "done" && result.step.status === "done") {
2053
+ notifyNextStepForPlan(app, result.step.plan_id);
2054
+ }
2055
+ notifyPlansCompleted(app, result.changes);
2056
+ return [result.step.plan_id];
2057
+ }
2058
+ function handleStepComment(app, args) {
2059
+ const entries = parseCommentPairs("step", args);
2060
+ const planIds = app.commentSteps(entries);
2061
+ if (planIds.length === 1) {
2062
+ log(`Updated step comments for plan ID: ${planIds[0]}.`);
2063
+ } else {
2064
+ log(`Updated step comments for ${planIds.length} plans.`);
2065
+ }
2066
+ return planIds;
2067
+ }
2068
+ function handleStepDone(app, args) {
2069
+ if (!args.length) {
2070
+ throw invalidInput("step done requires <id>");
2071
+ }
2072
+ const id = parseNumber(args[0], "step id");
2073
+ const allGoals = args.slice(1).includes("--all-goals");
2074
+ const result = app.setStepDoneWithGoals(id, allGoals);
2075
+ log(`Step ID: ${result.step.id} marked done.`);
2076
+ printStatusChanges(result.changes);
2077
+ notifyNextStepForPlan(app, result.step.plan_id);
2078
+ notifyPlansCompleted(app, result.changes);
2079
+ return [result.step.plan_id];
2080
+ }
2081
+ function handleStepMove(app, args) {
2082
+ if (!args.length) {
2083
+ throw invalidInput("step move requires <id> --to <pos>");
2084
+ }
2085
+ const id = parseNumber(args[0], "step id");
2086
+ const toIndex = args.indexOf("--to");
2087
+ if (toIndex === -1 || toIndex === args.length - 1) {
2088
+ throw invalidInput("step move requires --to <pos>");
2089
+ }
2090
+ const to = parseNumber(args[toIndex + 1], "position");
2091
+ if (to === 0) {
2092
+ throw invalidInput("position starts at 1");
2093
+ }
2094
+ const steps = app.moveStep(id, to);
2095
+ log(`Reordered steps for plan ID: ${steps[0].plan_id}:`);
2096
+ const details = app.getStepsDetail(steps);
2097
+ printStepList(details);
2098
+ return [steps[0].plan_id];
2099
+ }
2100
+ function handleStepRemove(app, args) {
2101
+ if (!args.length) {
2102
+ throw invalidInput("no step ids provided");
2103
+ }
2104
+ const ids = args.map((arg) => parseNumber(arg, "step id"));
2105
+ const planIds = app.planIdsForSteps(ids);
2106
+ const result = app.deleteSteps(ids);
2107
+ if (ids.length === 1) {
2108
+ log(`Step ID: ${ids[0]} removed.`);
2109
+ } else {
2110
+ log(`Removed ${result.deleted} steps.`);
2111
+ }
2112
+ printStatusChanges(result.changes);
2113
+ return planIds;
2114
+ }
2115
+ function handleGoalAdd(app, args) {
2116
+ if (args.length < 2) {
2117
+ throw invalidInput("goal add requires <step_id> <content...>");
2118
+ }
2119
+ const stepId = parseNumber(args[0], "step id");
2120
+ const contents = args.slice(1);
2121
+ if (!contents.length) {
2122
+ throw invalidInput("no contents provided");
2123
+ }
2124
+ contents.forEach((content) => ensureNonEmpty("goal content", content));
2125
+ const result = app.addGoalsBatch(stepId, contents, "todo");
2126
+ if (result.goals.length === 1) {
2127
+ log(`Created goal ID: ${result.goals[0].id} for step ID: ${result.goals[0].step_id}`);
2128
+ } else {
2129
+ log(`Created ${result.goals.length} goals for step ID: ${stepId}`);
2130
+ }
2131
+ printStatusChanges(result.changes);
2132
+ notifyAfterStepChanges(app, result.changes);
2133
+ notifyPlansCompleted(app, result.changes);
2134
+ const step = app.getStep(stepId);
2135
+ return [step.plan_id];
2136
+ }
2137
+ function handleGoalList(app, args) {
2138
+ if (!args.length) {
2139
+ throw invalidInput("goal list requires <step_id>");
2140
+ }
2141
+ const stepId = parseNumber(args[0], "step id");
2142
+ const { options } = parseOptions(args.slice(1));
2143
+ const status = parseGoalStatusFilter(options.status);
2144
+ const pagination = resolvePagination(options, { limit: DEFAULT_LIMIT, page: DEFAULT_PAGE });
2145
+ const countQuery = {
2146
+ status,
2147
+ limit: void 0,
2148
+ offset: void 0
2149
+ };
2150
+ const total = app.countGoals(stepId, countQuery);
2151
+ if (total === 0) {
2152
+ log(`No goals found for step ID: ${stepId}.`);
2153
+ return [];
2154
+ }
2155
+ const totalPages = Math.ceil(total / pagination.limit);
2156
+ if (pagination.page > totalPages) {
2157
+ log(`Page ${pagination.page} exceeds total pages ${totalPages} for step ID: ${stepId}.`);
2158
+ return [];
2159
+ }
2160
+ const query = {
2161
+ status,
2162
+ limit: pagination.limit,
2163
+ offset: pagination.offset
2164
+ };
2165
+ const goals = app.listGoalsFiltered(stepId, query);
2166
+ printGoalList(goals);
2167
+ logPageFooter(pagination.page, pagination.limit);
2168
+ return [];
2169
+ }
2170
+ function handleGoalCount(app, args) {
2171
+ if (!args.length) {
2172
+ throw invalidInput("goal count requires <step_id>");
2173
+ }
2174
+ const stepId = parseNumber(args[0], "step id");
2175
+ const { options } = parseOptions(args.slice(1));
2176
+ const status = parseGoalStatusFilter(options.status);
2177
+ const query = {
2178
+ status,
2179
+ limit: void 0,
2180
+ offset: void 0
2181
+ };
2182
+ const total = app.countGoals(stepId, query);
2183
+ log(`Total: ${total}`);
2184
+ return [];
2185
+ }
2186
+ function handleGoalShow(app, args) {
2187
+ const id = parseIdArg(args, "goal show");
2188
+ const detail = app.getGoalDetail(id);
2189
+ log(formatGoalDetail(detail.goal, detail.step));
2190
+ return [];
2191
+ }
2192
+ function handleGoalComment(app, args) {
2193
+ const entries = parseCommentPairs("goal", args);
2194
+ const planIds = app.commentGoals(entries);
2195
+ if (planIds.length === 1) {
2196
+ log(`Updated goal comments for plan ID: ${planIds[0]}.`);
2197
+ } else {
2198
+ log(`Updated goal comments for ${planIds.length} plans.`);
2199
+ }
2200
+ return planIds;
2201
+ }
2202
+ function handleGoalUpdate(app, args) {
2203
+ if (!args.length) {
2204
+ throw invalidInput("goal update requires <id>");
2205
+ }
2206
+ const id = parseNumber(args[0], "goal id");
2207
+ const { options } = parseOptions(args.slice(1));
2208
+ if (options.content !== void 0) {
2209
+ ensureNonEmpty("goal content", options.content);
2210
+ }
2211
+ const result = app.updateGoal(id, {
2212
+ content: options.content,
2213
+ status: options.status ? parseGoalStatus(options.status) : void 0,
2214
+ comment: options.comment
2215
+ });
2216
+ log(`Updated goal ${result.goal.id}.`);
2217
+ printStatusChanges(result.changes);
2218
+ notifyAfterStepChanges(app, result.changes);
2219
+ notifyPlansCompleted(app, result.changes);
2220
+ const step = app.getStep(result.goal.step_id);
2221
+ return [step.plan_id];
2222
+ }
2223
+ function handleGoalDone(app, args) {
2224
+ if (!args.length) {
2225
+ throw invalidInput("goal done requires <id>");
2226
+ }
2227
+ const ids = args.map((arg) => parseNumber(arg, "goal id"));
2228
+ if (ids.length === 1) {
2229
+ const result2 = app.setGoalStatus(ids[0], "done");
2230
+ log(`Goal ID: ${result2.goal.id} marked done.`);
2231
+ printStatusChanges(result2.changes);
2232
+ notifyAfterStepChanges(app, result2.changes);
2233
+ notifyPlansCompleted(app, result2.changes);
2234
+ const step = app.getStep(result2.goal.step_id);
2235
+ return [step.plan_id];
2236
+ }
2237
+ const planIds = app.planIdsForGoals(ids);
2238
+ const result = app.setGoalsStatus(ids, "done");
2239
+ log(`Goals marked done: ${result.updated}.`);
2240
+ printStatusChanges(result.changes);
2241
+ notifyAfterStepChanges(app, result.changes);
2242
+ notifyPlansCompleted(app, result.changes);
2243
+ return planIds;
2244
+ }
2245
+ function handleGoalRemove(app, args) {
2246
+ if (!args.length) {
2247
+ throw invalidInput("no goal ids provided");
2248
+ }
2249
+ const ids = args.map((arg) => parseNumber(arg, "goal id"));
2250
+ const planIds = app.planIdsForGoals(ids);
2251
+ const result = app.deleteGoals(ids);
2252
+ if (ids.length === 1) {
2253
+ log(`Goal ID: ${ids[0]} removed.`);
2254
+ } else {
2255
+ log(`Removed ${result.deleted} goals.`);
2256
+ }
2257
+ printStatusChanges(result.changes);
2258
+ notifyAfterStepChanges(app, result.changes);
2259
+ notifyPlansCompleted(app, result.changes);
2260
+ return planIds;
2261
+ }
2262
+ function parseIdArg(args, label) {
2263
+ if (!args.length) {
2264
+ throw invalidInput(`${label} requires <id>`);
2265
+ }
2266
+ return parseNumber(args[0], "id");
2267
+ }
2268
+ function parseNumber(value, label) {
2269
+ const num = Number(value);
2270
+ if (!Number.isFinite(num) || !Number.isInteger(num)) {
2271
+ throw invalidInput(`${label} '${value}' is invalid`);
2272
+ }
2273
+ return num;
2274
+ }
2275
+ function parseOptions(args) {
2276
+ const options = { search: [] };
2277
+ const positionals = [];
2278
+ let i = 0;
2279
+ while (i < args.length) {
2280
+ const token = args[i];
2281
+ if (!token.startsWith("--")) {
2282
+ positionals.push(token);
2283
+ i += 1;
2284
+ continue;
2285
+ }
2286
+ switch (token) {
2287
+ case "--scope":
2288
+ options.scope = expectValue(args, i, token);
2289
+ i += 2;
2290
+ break;
2291
+ case "--match-case":
2292
+ options.matchCase = true;
2293
+ i += 1;
2294
+ break;
2295
+ case "--desc":
2296
+ options.desc = true;
2297
+ i += 1;
2298
+ break;
2299
+ case "--all-goals":
2300
+ options.allGoals = true;
2301
+ i += 1;
2302
+ break;
2303
+ case "--force":
2304
+ options.force = true;
2305
+ i += 1;
2306
+ break;
2307
+ case "--search":
2308
+ options.search.push(expectValue(args, i, token));
2309
+ i += 2;
2310
+ break;
2311
+ case "--search-mode":
2312
+ options.searchMode = expectValue(args, i, token);
2313
+ i += 2;
2314
+ break;
2315
+ case "--search-field":
2316
+ options.searchField = expectValue(args, i, token);
2317
+ i += 2;
2318
+ break;
2319
+ case "--title":
2320
+ options.title = expectValue(args, i, token);
2321
+ i += 2;
2322
+ break;
2323
+ case "--content":
2324
+ options.content = expectValue(args, i, token);
2325
+ i += 2;
2326
+ break;
2327
+ case "--status":
2328
+ options.status = expectValue(args, i, token);
2329
+ i += 2;
2330
+ break;
2331
+ case "--comment":
2332
+ options.comment = expectValue(args, i, token);
2333
+ i += 2;
2334
+ break;
2335
+ case "--executor":
2336
+ options.executor = expectValue(args, i, token);
2337
+ i += 2;
2338
+ break;
2339
+ case "--limit":
2340
+ options.limit = expectValue(args, i, token);
2341
+ i += 2;
2342
+ break;
2343
+ case "--page":
2344
+ options.page = expectValue(args, i, token);
2345
+ i += 2;
2346
+ break;
2347
+ case "--order":
2348
+ options.order = expectValue(args, i, token);
2349
+ i += 2;
2350
+ break;
2351
+ case "--to":
2352
+ options.to = expectValue(args, i, token);
2353
+ i += 2;
2354
+ break;
2355
+ case "--delay":
2356
+ options.delay = expectValue(args, i, token);
2357
+ i += 2;
2358
+ break;
2359
+ case "--reason":
2360
+ options.reason = expectValue(args, i, token);
2361
+ i += 2;
2362
+ break;
2363
+ case "--clear":
2364
+ options.clear = true;
2365
+ i += 1;
2366
+ break;
2367
+ case "--goal":
2368
+ if (!options.goals) options.goals = [];
2369
+ options.goals.push(expectValue(args, i, token));
2370
+ i += 2;
2371
+ break;
2372
+ default:
2373
+ throw invalidInput(`unexpected argument: ${token}`);
2374
+ }
2375
+ }
2376
+ return { options, positionals };
2377
+ }
2378
+ function expectValue(args, index, token) {
2379
+ const value = args[index + 1];
2380
+ if (value === void 0) {
2381
+ throw invalidInput(`${token} requires a value`);
2382
+ }
2383
+ return value;
2384
+ }
2385
+ function parsePlanStatus(value) {
2386
+ const normalized = value.trim().toLowerCase();
2387
+ if (normalized === "todo" || normalized === "done") return normalized;
2388
+ throw invalidInput(`invalid status '${value}', expected todo|done`);
2389
+ }
2390
+ function parsePlanStatusFilter(value) {
2391
+ if (!value) return null;
2392
+ const normalized = value.trim().toLowerCase();
2393
+ if (normalized === "all") return null;
2394
+ return parsePlanStatus(normalized);
2395
+ }
2396
+ function parseStepStatus(value) {
2397
+ return parsePlanStatus(value);
2398
+ }
2399
+ function parseGoalStatus(value) {
2400
+ return parsePlanStatus(value);
2401
+ }
2402
+ function parseStepStatusFilter(value) {
2403
+ if (!value) return null;
2404
+ const normalized = value.trim().toLowerCase();
2405
+ if (normalized === "all") return null;
2406
+ return parseStepStatus(normalized);
2407
+ }
2408
+ function parseGoalStatusFilter(value) {
2409
+ if (!value) return null;
2410
+ const normalized = value.trim().toLowerCase();
2411
+ if (normalized === "all") return null;
2412
+ return parseGoalStatus(normalized);
2413
+ }
2414
+ function parseScope(value) {
2415
+ if (!value) return "project";
2416
+ const normalized = value.trim().toLowerCase();
2417
+ if (normalized === "project" || normalized === "all") return normalized;
2418
+ throw invalidInput(`invalid scope '${value}', expected project|all`);
2419
+ }
2420
+ function parsePlanOrder(value) {
2421
+ const normalized = value.trim().toLowerCase();
2422
+ if (normalized === "id" || normalized === "title" || normalized === "created" || normalized === "updated") {
2423
+ return normalized;
2424
+ }
2425
+ throw invalidInput(`invalid order '${value}', expected id|title|created|updated`);
2426
+ }
2427
+ function parseStepExecutor(value) {
2428
+ const normalized = value.trim().toLowerCase();
2429
+ if (normalized === "ai" || normalized === "human") return normalized;
2430
+ throw invalidInput(`invalid executor '${value}', expected ai|human`);
2431
+ }
2432
+ function resolvePagination(options, defaults) {
2433
+ if (defaults.limit <= 0 || defaults.page < 1) {
2434
+ throw invalidInput("invalid default pagination configuration");
2435
+ }
2436
+ const limit = options.limit !== void 0 ? parseNumber(options.limit, "limit") : defaults.limit;
2437
+ if (limit <= 0) {
2438
+ throw invalidInput("limit must be >= 1");
2439
+ }
2440
+ const page = options.page !== void 0 ? parseNumber(options.page, "page") : defaults.page;
2441
+ if (page < 1) {
2442
+ throw invalidInput("page must be >= 1");
2443
+ }
2444
+ return { limit, page, offset: (page - 1) * limit };
2445
+ }
2446
+ var PlanSearchQuery = class {
2447
+ terms;
2448
+ mode;
2449
+ field;
2450
+ matchCase;
2451
+ constructor(rawTerms = [], searchMode, searchField, matchCase) {
2452
+ let terms = rawTerms.map((term) => term.trim()).filter((term) => term.length > 0);
2453
+ const caseSensitive = !!matchCase;
2454
+ if (!caseSensitive) {
2455
+ terms = terms.map((term) => term.toLowerCase());
2456
+ }
2457
+ this.terms = terms;
2458
+ this.mode = parseSearchMode(searchMode);
2459
+ this.field = parseSearchField(searchField);
2460
+ this.matchCase = caseSensitive;
2461
+ }
2462
+ hasTerms() {
2463
+ return this.terms.length > 0;
2464
+ }
2465
+ };
2466
+ function parseSearchMode(value) {
2467
+ if (!value) return "all";
2468
+ const normalized = value.trim().toLowerCase();
2469
+ if (normalized === "any" || normalized === "all") return normalized;
2470
+ throw invalidInput(`invalid search mode '${value}', expected any|all`);
2471
+ }
2472
+ function parseSearchField(value) {
2473
+ if (!value) return "plan";
2474
+ const normalized = value.trim().toLowerCase();
2475
+ if (["plan", "title", "content", "comment", "steps", "goals", "all"].includes(normalized)) {
2476
+ return normalized;
2477
+ }
2478
+ throw invalidInput(
2479
+ `invalid search field '${value}', expected plan|title|content|comment|steps|goals|all`
2480
+ );
2481
+ }
2482
+ function planMatchesSearch(detail, search) {
2483
+ const haystacks = [];
2484
+ const addValue = (value) => {
2485
+ haystacks.push(search.matchCase ? value : value.toLowerCase());
2486
+ };
2487
+ const includePlan = search.field === "plan" || search.field === "all";
2488
+ const includeTitle = search.field === "title" || includePlan || search.field === "all";
2489
+ const includeContent = search.field === "content" || includePlan || search.field === "all";
2490
+ const includeComment = search.field === "comment" || includePlan || search.field === "all";
2491
+ const includeSteps = search.field === "steps" || search.field === "all";
2492
+ const includeGoals = search.field === "goals" || search.field === "all";
2493
+ if (includePlan || includeTitle) addValue(detail.plan.title);
2494
+ if (includePlan || includeContent) addValue(detail.plan.content);
2495
+ if (includePlan || includeComment) {
2496
+ if (detail.plan.comment) addValue(detail.plan.comment);
2497
+ }
2498
+ if (includeSteps) {
2499
+ detail.steps.forEach((step) => addValue(step.content));
2500
+ }
2501
+ if (includeGoals) {
2502
+ detail.goals.forEach((goalList) => goalList.forEach((goal) => addValue(goal.content)));
2503
+ }
2504
+ if (!haystacks.length) return false;
2505
+ if (search.mode === "any") {
2506
+ return search.terms.some((term) => haystacks.some((value) => value.includes(term)));
2507
+ }
2508
+ return search.terms.every((term) => haystacks.some((value) => value.includes(term)));
2509
+ }
2510
+ function parsePlanAddTreeSteps(args) {
2511
+ if (!args.length) {
2512
+ throw invalidInput("plan add-tree requires at least one --step");
2513
+ }
2514
+ const steps = [];
2515
+ let current = null;
2516
+ let i = 0;
2517
+ while (i < args.length) {
2518
+ const token = args[i];
2519
+ if (token === "--") {
2520
+ i += 1;
2521
+ continue;
2522
+ }
2523
+ if (token === "--step") {
2524
+ const value = args[i + 1];
2525
+ if (value === void 0) {
2526
+ throw invalidInput("plan add-tree --step requires a value");
2527
+ }
2528
+ if (!value.trim()) {
2529
+ throw invalidInput("plan add-tree --step cannot be empty");
2530
+ }
2531
+ if (current) {
2532
+ steps.push({ content: current.content, executor: current.executor, goals: current.goals.length ? current.goals : void 0 });
2533
+ }
2534
+ current = { content: value, goals: [] };
2535
+ i += 2;
2536
+ continue;
2537
+ }
2538
+ if (token === "--executor") {
2539
+ const value = args[i + 1];
2540
+ if (value === void 0) {
2541
+ throw invalidInput("plan add-tree --executor requires a value");
2542
+ }
2543
+ if (!current) {
2544
+ throw invalidInput("plan add-tree --executor must follow a --step");
2545
+ }
2546
+ current.executor = parseStepExecutor(value);
2547
+ i += 2;
2548
+ continue;
2549
+ }
2550
+ if (token === "--goal") {
2551
+ const value = args[i + 1];
2552
+ if (value === void 0) {
2553
+ throw invalidInput("plan add-tree --goal requires a value");
2554
+ }
2555
+ if (!current) {
2556
+ throw invalidInput("plan add-tree --goal must follow a --step");
2557
+ }
2558
+ current.goals.push(value);
2559
+ i += 2;
2560
+ continue;
2561
+ }
2562
+ throw invalidInput(`plan add-tree unexpected argument: ${token}`);
2563
+ }
2564
+ if (current) {
2565
+ steps.push({ content: current.content, executor: current.executor, goals: current.goals.length ? current.goals : void 0 });
2566
+ }
2567
+ if (!steps.length) {
2568
+ throw invalidInput("plan add-tree requires at least one --step");
2569
+ }
2570
+ return steps;
2571
+ }
2572
+ function parseCommentPairs(kind, pairs) {
2573
+ if (!pairs.length) {
2574
+ throw invalidInput(`${kind} comment requires <id> <comment> pairs`);
2575
+ }
2576
+ if (pairs.length % 2 !== 0) {
2577
+ throw invalidInput(`${kind} comment expects <id> <comment> pairs`);
2578
+ }
2579
+ const entries = [];
2580
+ for (let i = 0; i < pairs.length; i += 2) {
2581
+ const idValue = pairs[i];
2582
+ const comment = pairs[i + 1];
2583
+ const id = parseNumber(idValue, `${kind} comment id`);
2584
+ ensureNonEmpty("comment", comment);
2585
+ entries.push([id, comment]);
2586
+ }
2587
+ return entries;
2588
+ }
2589
+ function parseStepAddArgs(args) {
2590
+ const contents = [];
2591
+ let executor;
2592
+ let at;
2593
+ let i = 0;
2594
+ while (i < args.length) {
2595
+ const token = args[i];
2596
+ if (token === "--executor") {
2597
+ const value = expectValue(args, i, token);
2598
+ executor = parseStepExecutor(value);
2599
+ i += 2;
2600
+ continue;
2601
+ }
2602
+ if (token === "--at") {
2603
+ const value = expectValue(args, i, token);
2604
+ at = parseNumber(value, "position");
2605
+ i += 2;
2606
+ continue;
2607
+ }
2608
+ if (token.startsWith("--")) {
2609
+ throw invalidInput(`unexpected argument: ${token}`);
2610
+ }
2611
+ contents.push(token);
2612
+ i += 1;
2613
+ }
2614
+ return { contents, executor, at };
2615
+ }
2616
+ function parseStepAddTreeArgs(args) {
2617
+ let executor;
2618
+ const goals = [];
2619
+ let i = 0;
2620
+ while (i < args.length) {
2621
+ const token = args[i];
2622
+ if (token === "--executor") {
2623
+ const value = expectValue(args, i, token);
2624
+ executor = parseStepExecutor(value);
2625
+ i += 2;
2626
+ continue;
2627
+ }
2628
+ if (token === "--goal") {
2629
+ const value = expectValue(args, i, token);
2630
+ goals.push(value);
2631
+ i += 2;
2632
+ continue;
2633
+ }
2634
+ throw invalidInput(`unexpected argument: ${token}`);
2635
+ }
2636
+ return { executor, goals };
2637
+ }
2638
+ function printStatusChanges(changes) {
2639
+ if (statusChangesEmpty(changes)) return;
2640
+ log("Auto status updates:");
2641
+ changes.steps.forEach((change) => {
2642
+ log(`- Step ID: ${change.step_id} status auto-updated from ${change.from} to ${change.to} (${change.reason}).`);
2643
+ });
2644
+ changes.plans.forEach((change) => {
2645
+ log(`- Plan ID: ${change.plan_id} status auto-updated from ${change.from} to ${change.to} (${change.reason}).`);
2646
+ });
2647
+ changes.active_plans_cleared.forEach((change) => {
2648
+ log(`- Active plan deactivated for plan ID: ${change.plan_id} (${change.reason}).`);
2649
+ });
2650
+ }
2651
+ function notifyAfterStepChanges(app, changes) {
2652
+ const planIds = /* @__PURE__ */ new Set();
2653
+ changes.steps.forEach((change) => {
2654
+ if (change.to === "done") {
2655
+ const step = app.getStep(change.step_id);
2656
+ planIds.add(step.plan_id);
2657
+ }
2658
+ });
2659
+ planIds.forEach((planId) => notifyNextStepForPlan(app, planId));
2660
+ }
2661
+ function notifyPlansCompleted(app, changes) {
2662
+ const planIds = /* @__PURE__ */ new Set();
2663
+ changes.plans.forEach((change) => {
2664
+ if (change.to === "done") planIds.add(change.plan_id);
2665
+ });
2666
+ planIds.forEach((planId) => {
2667
+ const plan = app.getPlan(planId);
2668
+ if (plan.status === "done") {
2669
+ notifyPlanCompleted(plan.id);
2670
+ }
2671
+ });
2672
+ }
2673
+ function notifyPlanCompleted(planId) {
2674
+ log(`Plan ID: ${planId} is complete. Summarize the completed results to the user, then end this turn.`);
2675
+ }
2676
+ function notifyNextStepForPlan(app, planId) {
2677
+ const next = app.nextStep(planId);
2678
+ if (!next) return;
2679
+ if (next.executor === "ai") {
2680
+ log(`Next step is assigned to ai (step ID: ${next.id}). Please end this turn so Planpilot can surface it.`);
2681
+ return;
2682
+ }
2683
+ const goals = app.goalsForStep(next.id);
2684
+ log("Next step requires human action:");
2685
+ log(formatStepDetail(next, goals));
2686
+ log(
2687
+ "Tell the user to complete the above step and goals. Confirm each goal when done, then end this turn."
2688
+ );
2689
+ }
2690
+ var COL_ID = 6;
2691
+ var COL_STAT = 6;
2692
+ var COL_STEPS = 9;
2693
+ var COL_ORDER = 5;
2694
+ var COL_EXEC = 6;
2695
+ var COL_GOALS = 9;
2696
+ var COL_TEXT = 30;
2697
+ function printPlanList(details) {
2698
+ log(`${pad("ID", COL_ID)} ${pad("STAT", COL_STAT)} ${pad("STEPS", COL_STEPS)} ${pad("TITLE", COL_TEXT)} COMMENT`);
2699
+ details.forEach((detail) => {
2700
+ const total = detail.steps.length;
2701
+ const done = detail.steps.filter((step) => step.status === "done").length;
2702
+ log(
2703
+ `${pad(String(detail.plan.id), COL_ID)} ${pad(detail.plan.status, COL_STAT)} ${pad(`${done}/${total}`, COL_STEPS)} ${pad(detail.plan.title, COL_TEXT)} ${detail.plan.comment ?? ""}`
2704
+ );
2705
+ });
2706
+ }
2707
+ function printStepList(details) {
2708
+ log(
2709
+ `${pad("ID", COL_ID)} ${pad("STAT", COL_STAT)} ${pad("ORD", COL_ORDER)} ${pad("EXEC", COL_EXEC)} ${pad("GOALS", COL_GOALS)} ${pad("CONTENT", COL_TEXT)} COMMENT`
2710
+ );
2711
+ details.forEach((detail) => {
2712
+ const total = detail.goals.length;
2713
+ const done = detail.goals.filter((goal) => goal.status === "done").length;
2714
+ log(
2715
+ `${pad(String(detail.step.id), COL_ID)} ${pad(detail.step.status, COL_STAT)} ${pad(String(detail.step.sort_order), COL_ORDER)} ${pad(detail.step.executor, COL_EXEC)} ${pad(`${done}/${total}`, COL_GOALS)} ${pad(detail.step.content, COL_TEXT)} ${detail.step.comment ?? ""}`
2716
+ );
2717
+ });
2718
+ }
2719
+ function printGoalList(goals) {
2720
+ log(`${pad("ID", COL_ID)} ${pad("STAT", COL_STAT)} ${pad("CONTENT", COL_TEXT)} COMMENT`);
2721
+ goals.forEach((goal) => {
2722
+ log(`${pad(String(goal.id), COL_ID)} ${pad(goal.status, COL_STAT)} ${pad(goal.content, COL_TEXT)} ${goal.comment ?? ""}`);
2723
+ });
2724
+ }
2725
+ function logPageFooter(page, limit) {
2726
+ log(`Page ${page} / Limit ${limit}`);
2727
+ }
2728
+ function pad(value, width) {
2729
+ if (value.length >= width) return value;
2730
+ return value.padEnd(width, " ");
2731
+ }
2732
+ function syncPlanMarkdown(app, planIds) {
2733
+ if (!planIds.length) return;
2734
+ const unique = Array.from(new Set(planIds));
2735
+ const active = app.getActivePlan();
2736
+ const activeId = active?.plan_id;
2737
+ const activeUpdated = active?.updated_at ?? null;
2738
+ unique.forEach((planId) => {
2739
+ let detail;
2740
+ try {
2741
+ detail = app.getPlanDetail(planId);
2742
+ } catch (err) {
2743
+ if (err instanceof AppError && err.kind === "NotFound") return;
2744
+ throw err;
2745
+ }
2746
+ const isActive = activeId === planId;
2747
+ const activatedAt = isActive ? activeUpdated : null;
2748
+ const mdPath = resolvePlanMarkdownPath(planId);
2749
+ ensureParentDir(mdPath);
2750
+ const markdown = formatPlanMarkdown(isActive, activatedAt, detail.plan, detail.steps, detail.goals);
2751
+ fs3.writeFileSync(mdPath, markdown, "utf8");
2752
+ });
2753
+ }
2754
+
2755
+ // src/lib/config.ts
2756
+ import fs4 from "fs";
2757
+ import path3 from "path";
2758
+ var DEFAULT_KEYWORDS = {
2759
+ any: [],
2760
+ all: [],
2761
+ none: [],
2762
+ matchCase: false
2763
+ };
2764
+ var DEFAULT_EVENT_RULE = {
2765
+ enabled: false,
2766
+ force: false,
2767
+ keywords: DEFAULT_KEYWORDS
2768
+ };
2769
+ var DEFAULT_SESSION_ERROR_RULE = {
2770
+ enabled: false,
2771
+ force: true,
2772
+ keywords: DEFAULT_KEYWORDS,
2773
+ errorNames: [],
2774
+ statusCodes: [],
2775
+ retryableOnly: false
2776
+ };
2777
+ var DEFAULT_SESSION_RETRY_RULE = {
2778
+ enabled: false,
2779
+ force: false,
2780
+ keywords: DEFAULT_KEYWORDS,
2781
+ attemptAtLeast: 1
2782
+ };
2783
+ var DEFAULT_SEND_RETRY = {
2784
+ enabled: true,
2785
+ maxAttempts: 3,
2786
+ delaysMs: [1500, 5e3, 15e3]
2787
+ };
2788
+ var DEFAULT_PLANPILOT_CONFIG = {
2789
+ autoContinue: {
2790
+ sendRetry: DEFAULT_SEND_RETRY,
2791
+ onSessionError: DEFAULT_SESSION_ERROR_RULE,
2792
+ onSessionRetry: DEFAULT_SESSION_RETRY_RULE,
2793
+ onPermissionAsked: DEFAULT_EVENT_RULE,
2794
+ onPermissionRejected: {
2795
+ ...DEFAULT_EVENT_RULE,
2796
+ force: true
2797
+ },
2798
+ onQuestionAsked: DEFAULT_EVENT_RULE,
2799
+ onQuestionRejected: {
2800
+ ...DEFAULT_EVENT_RULE,
2801
+ force: true
2802
+ }
2803
+ },
2804
+ runtime: {
2805
+ paused: false
2806
+ }
2807
+ };
2808
+ function resolvePlanpilotConfigPath() {
2809
+ const override = process.env.OPENCODE_PLANPILOT_CONFIG;
2810
+ if (override && override.trim()) {
2811
+ const value = override.trim();
2812
+ return path3.isAbsolute(value) ? value : path3.resolve(value);
2813
+ }
2814
+ return path3.join(resolvePlanpilotDir(), "config.json");
2815
+ }
2816
+ function cloneDefaultConfig() {
2817
+ return {
2818
+ autoContinue: {
2819
+ sendRetry: {
2820
+ enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.sendRetry.enabled,
2821
+ maxAttempts: DEFAULT_PLANPILOT_CONFIG.autoContinue.sendRetry.maxAttempts,
2822
+ delaysMs: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.sendRetry.delaysMs]
2823
+ },
2824
+ onSessionError: {
2825
+ enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.enabled,
2826
+ force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.force,
2827
+ keywords: {
2828
+ any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.keywords.any],
2829
+ all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.keywords.all],
2830
+ none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.keywords.none],
2831
+ matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.keywords.matchCase
2832
+ },
2833
+ errorNames: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.errorNames],
2834
+ statusCodes: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.statusCodes],
2835
+ retryableOnly: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError.retryableOnly
2836
+ },
2837
+ onSessionRetry: {
2838
+ enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.enabled,
2839
+ force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.force,
2840
+ keywords: {
2841
+ any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.keywords.any],
2842
+ all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.keywords.all],
2843
+ none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.keywords.none],
2844
+ matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.keywords.matchCase
2845
+ },
2846
+ attemptAtLeast: DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry.attemptAtLeast
2847
+ },
2848
+ onPermissionAsked: {
2849
+ enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.enabled,
2850
+ force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.force,
2851
+ keywords: {
2852
+ any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.keywords.any],
2853
+ all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.keywords.all],
2854
+ none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.keywords.none],
2855
+ matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked.keywords.matchCase
2856
+ }
2857
+ },
2858
+ onPermissionRejected: {
2859
+ enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.enabled,
2860
+ force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.force,
2861
+ keywords: {
2862
+ any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.keywords.any],
2863
+ all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.keywords.all],
2864
+ none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.keywords.none],
2865
+ matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected.keywords.matchCase
2866
+ }
2867
+ },
2868
+ onQuestionAsked: {
2869
+ enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.enabled,
2870
+ force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.force,
2871
+ keywords: {
2872
+ any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.keywords.any],
2873
+ all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.keywords.all],
2874
+ none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.keywords.none],
2875
+ matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked.keywords.matchCase
2876
+ }
2877
+ },
2878
+ onQuestionRejected: {
2879
+ enabled: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.enabled,
2880
+ force: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.force,
2881
+ keywords: {
2882
+ any: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.keywords.any],
2883
+ all: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.keywords.all],
2884
+ none: [...DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.keywords.none],
2885
+ matchCase: DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected.keywords.matchCase
2886
+ }
2887
+ }
2888
+ },
2889
+ runtime: {
2890
+ paused: DEFAULT_PLANPILOT_CONFIG.runtime.paused
2891
+ }
2892
+ };
2893
+ }
2894
+ function parseBoolean(value, fallback) {
2895
+ if (typeof value === "boolean") return value;
2896
+ return fallback;
2897
+ }
2898
+ function parseStringArray(value) {
2899
+ if (!Array.isArray(value)) return [];
2900
+ const parsed = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter((item) => item.length > 0);
2901
+ return Array.from(new Set(parsed));
2902
+ }
2903
+ function parseNumberArray(value) {
2904
+ if (!Array.isArray(value)) return [];
2905
+ const parsed = value.map((item) => typeof item === "number" ? item : Number.NaN).filter((item) => Number.isFinite(item)).map((item) => Math.trunc(item));
2906
+ return Array.from(new Set(parsed));
2907
+ }
2908
+ function parsePositiveInt(value, fallback) {
2909
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
2910
+ const parsed = Math.trunc(value);
2911
+ return parsed > 0 ? parsed : fallback;
2912
+ }
2913
+ function parsePositiveNumberArray(value, fallback) {
2914
+ if (!Array.isArray(value)) return fallback;
2915
+ const parsed = value.map((item) => typeof item === "number" ? item : Number.NaN).filter((item) => Number.isFinite(item)).map((item) => Math.trunc(item)).filter((item) => item > 0);
2916
+ if (!parsed.length) return fallback;
2917
+ return Array.from(new Set(parsed));
2918
+ }
2919
+ function parseKeywordRule(value, fallback) {
2920
+ return {
2921
+ any: parseStringArray(value?.any),
2922
+ all: parseStringArray(value?.all),
2923
+ none: parseStringArray(value?.none),
2924
+ matchCase: parseBoolean(value?.matchCase, fallback.matchCase)
2925
+ };
2926
+ }
2927
+ function parseEventRule(value, fallback) {
2928
+ return {
2929
+ enabled: parseBoolean(value?.enabled, fallback.enabled),
2930
+ force: parseBoolean(value?.force, fallback.force),
2931
+ keywords: parseKeywordRule(value?.keywords, fallback.keywords)
2932
+ };
2933
+ }
2934
+ function parseSessionErrorRule(value, fallback) {
2935
+ const base = parseEventRule(value, fallback);
2936
+ return {
2937
+ ...base,
2938
+ errorNames: parseStringArray(value?.errorNames),
2939
+ statusCodes: parseNumberArray(value?.statusCodes),
2940
+ retryableOnly: parseBoolean(value?.retryableOnly, fallback.retryableOnly)
2941
+ };
2942
+ }
2943
+ function parseSessionRetryRule(value, fallback) {
2944
+ const base = parseEventRule(value, fallback);
2945
+ const rawAttempt = typeof value?.attemptAtLeast === "number" ? Math.trunc(value.attemptAtLeast) : fallback.attemptAtLeast;
2946
+ return {
2947
+ ...base,
2948
+ attemptAtLeast: rawAttempt > 0 ? rawAttempt : fallback.attemptAtLeast
2949
+ };
2950
+ }
2951
+ function parseSendRetryConfig(value, fallback) {
2952
+ return {
2953
+ enabled: parseBoolean(value?.enabled, fallback.enabled),
2954
+ maxAttempts: parsePositiveInt(value?.maxAttempts, fallback.maxAttempts),
2955
+ delaysMs: parsePositiveNumberArray(value?.delaysMs, fallback.delaysMs)
2956
+ };
2957
+ }
2958
+ function parseConfig(raw) {
2959
+ return {
2960
+ autoContinue: {
2961
+ sendRetry: parseSendRetryConfig(raw.autoContinue?.sendRetry, DEFAULT_PLANPILOT_CONFIG.autoContinue.sendRetry),
2962
+ onSessionError: parseSessionErrorRule(
2963
+ raw.autoContinue?.onSessionError,
2964
+ DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionError
2965
+ ),
2966
+ onSessionRetry: parseSessionRetryRule(
2967
+ raw.autoContinue?.onSessionRetry,
2968
+ DEFAULT_PLANPILOT_CONFIG.autoContinue.onSessionRetry
2969
+ ),
2970
+ onPermissionAsked: parseEventRule(
2971
+ raw.autoContinue?.onPermissionAsked,
2972
+ DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionAsked
2973
+ ),
2974
+ onPermissionRejected: parseEventRule(
2975
+ raw.autoContinue?.onPermissionRejected,
2976
+ DEFAULT_PLANPILOT_CONFIG.autoContinue.onPermissionRejected
2977
+ ),
2978
+ onQuestionAsked: parseEventRule(
2979
+ raw.autoContinue?.onQuestionAsked,
2980
+ DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionAsked
2981
+ ),
2982
+ onQuestionRejected: parseEventRule(
2983
+ raw.autoContinue?.onQuestionRejected,
2984
+ DEFAULT_PLANPILOT_CONFIG.autoContinue.onQuestionRejected
2985
+ )
2986
+ },
2987
+ runtime: {
2988
+ paused: parseBoolean(raw.runtime?.paused, DEFAULT_PLANPILOT_CONFIG.runtime.paused)
2989
+ }
2990
+ };
2991
+ }
2992
+ function normalizePlanpilotConfig(raw) {
2993
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2994
+ return cloneDefaultConfig();
2995
+ }
2996
+ return parseConfig(raw);
2997
+ }
2998
+ function loadPlanpilotConfig() {
2999
+ const filePath = resolvePlanpilotConfigPath();
3000
+ try {
3001
+ if (!fs4.existsSync(filePath)) {
3002
+ return {
3003
+ path: filePath,
3004
+ loadedFromFile: false,
3005
+ config: cloneDefaultConfig()
3006
+ };
3007
+ }
3008
+ const text = fs4.readFileSync(filePath, "utf8");
3009
+ const parsed = JSON.parse(text);
3010
+ return {
3011
+ path: filePath,
3012
+ loadedFromFile: true,
3013
+ config: normalizePlanpilotConfig(parsed)
3014
+ };
3015
+ } catch (error) {
3016
+ const loadError = error instanceof Error ? error.message : String(error);
3017
+ return {
3018
+ path: filePath,
3019
+ loadedFromFile: false,
3020
+ config: cloneDefaultConfig(),
3021
+ loadError
3022
+ };
3023
+ }
3024
+ }
3025
+ function matchesKeywords(text, rule) {
3026
+ const source = rule.matchCase ? text : text.toLowerCase();
3027
+ const normalize = (value) => rule.matchCase ? value : value.toLowerCase();
3028
+ const any = rule.any.map(normalize);
3029
+ const all = rule.all.map(normalize);
3030
+ const none = rule.none.map(normalize);
3031
+ if (any.length > 0 && !any.some((term) => source.includes(term))) {
3032
+ return false;
3033
+ }
3034
+ if (!all.every((term) => source.includes(term))) {
3035
+ return false;
3036
+ }
3037
+ if (none.some((term) => source.includes(term))) {
3038
+ return false;
3039
+ }
3040
+ return true;
3041
+ }
3042
+
3043
+ // src/index.ts
3044
+ var PlanpilotPlugin = async (ctx) => {
3045
+ const IDLE_DEBOUNCE_MS = 1e3;
3046
+ const RECENT_SEND_DEDUPE_MS = 1500;
3047
+ const TRIGGER_TTL_MS = 10 * 60 * 1e3;
3048
+ const inFlight = /* @__PURE__ */ new Set();
3049
+ const skipNextAuto = /* @__PURE__ */ new Set();
3050
+ const lastIdleAt = /* @__PURE__ */ new Map();
3051
+ const pendingTrigger = /* @__PURE__ */ new Map();
3052
+ const recentSends = /* @__PURE__ */ new Map();
3053
+ const sendRetryTimers = /* @__PURE__ */ new Map();
3054
+ const sendRetryState = /* @__PURE__ */ new Map();
3055
+ const manualStop = /* @__PURE__ */ new Map();
3056
+ const waitTimers = /* @__PURE__ */ new Map();
3057
+ const permissionAsked = /* @__PURE__ */ new Map();
3058
+ const questionAsked = /* @__PURE__ */ new Map();
3059
+ const runSeq = /* @__PURE__ */ new Map();
3060
+ const loadedConfig = loadPlanpilotConfig();
3061
+ const autoConfig = loadedConfig.config.autoContinue;
3062
+ const clearWaitTimer = (sessionID) => {
3063
+ const existing = waitTimers.get(sessionID);
3064
+ if (existing) {
3065
+ clearTimeout(existing);
3066
+ waitTimers.delete(sessionID);
3067
+ }
3068
+ };
3069
+ const log2 = async (level, message, extra) => {
3070
+ try {
3071
+ await ctx.client.app.log({
3072
+ body: {
3073
+ service: "opencode-planpilot",
3074
+ level,
3075
+ message,
3076
+ extra
3077
+ }
3078
+ });
3079
+ } catch {
3080
+ }
3081
+ };
3082
+ const logDebug = async (message, extra) => {
3083
+ await log2("debug", message, extra);
3084
+ };
3085
+ const clearSendRetryTimer = (sessionID) => {
3086
+ const timer = sendRetryTimers.get(sessionID);
3087
+ if (timer) {
3088
+ clearTimeout(timer);
3089
+ sendRetryTimers.delete(sessionID);
3090
+ }
3091
+ };
3092
+ const clearSendRetry = (sessionID) => {
3093
+ clearSendRetryTimer(sessionID);
3094
+ sendRetryState.delete(sessionID);
3095
+ };
3096
+ const clearManualStop = async (sessionID, source) => {
3097
+ if (!manualStop.has(sessionID)) return;
3098
+ manualStop.delete(sessionID);
3099
+ await logDebug("manual-stop guard cleared", { sessionID, source });
3100
+ };
3101
+ const setManualStop = async (sessionID, reason, source) => {
3102
+ manualStop.set(sessionID, {
3103
+ at: Date.now(),
3104
+ reason
3105
+ });
3106
+ pendingTrigger.delete(sessionID);
3107
+ clearSendRetry(sessionID);
3108
+ await log2("info", "manual-stop guard armed", {
3109
+ sessionID,
3110
+ source,
3111
+ reason
3112
+ });
3113
+ };
3114
+ const stringifyError = (error) => {
3115
+ if (error instanceof Error) return error.message;
3116
+ if (typeof error === "string") return error;
3117
+ return String(error);
3118
+ };
3119
+ const isManualStopError = (error) => {
3120
+ const text = stringifyError(error).toLowerCase();
3121
+ return text.includes("aborted") || text.includes("cancel") || text.includes("canceled");
3122
+ };
3123
+ const buildRetryDetail = (baseDetail, attempt, max, message) => toSummary([
3124
+ baseDetail,
3125
+ `send-retry=${attempt}/${max}`,
3126
+ message ? `error=${message}` : void 0
3127
+ ]);
3128
+ const scheduleSendRetry = async (input) => {
3129
+ const cfg = autoConfig.sendRetry;
3130
+ if (!cfg.enabled) return;
3131
+ const blocked = manualStop.get(input.sessionID);
3132
+ if (blocked) {
3133
+ await logDebug("send retry skipped: manual-stop guard active", {
3134
+ sessionID: input.sessionID,
3135
+ source: input.source,
3136
+ reason: blocked.reason
3137
+ });
3138
+ return;
3139
+ }
3140
+ const text = stringifyError(input.error);
3141
+ if (isManualStopError(input.error)) {
3142
+ await logDebug("send retry skipped: manual-stop style error", {
3143
+ sessionID: input.sessionID,
3144
+ source: input.source,
3145
+ error: text
3146
+ });
3147
+ return;
3148
+ }
3149
+ const previous = sendRetryState.get(input.sessionID);
3150
+ const attempt = previous && previous.signature === input.signature ? previous.attempt + 1 : 1;
3151
+ if (attempt > cfg.maxAttempts) {
3152
+ clearSendRetry(input.sessionID);
3153
+ await log2("warn", "send retry exhausted", {
3154
+ sessionID: input.sessionID,
3155
+ source: input.source,
3156
+ signature: input.signature,
3157
+ maxAttempts: cfg.maxAttempts,
3158
+ error: text
3159
+ });
3160
+ return;
3161
+ }
3162
+ sendRetryState.set(input.sessionID, {
3163
+ signature: input.signature,
3164
+ attempt
3165
+ });
3166
+ clearSendRetryTimer(input.sessionID);
3167
+ const index = Math.min(Math.max(attempt - 1, 0), cfg.delaysMs.length - 1);
3168
+ const delayMs = cfg.delaysMs[index];
3169
+ const retryDetail = buildRetryDetail(input.detail, attempt, cfg.maxAttempts, text);
3170
+ const timer = setTimeout(() => {
3171
+ sendRetryTimers.delete(input.sessionID);
3172
+ queueTrigger(input.sessionID, {
3173
+ source: `${input.source}.send_retry`,
3174
+ force: input.force,
3175
+ detail: retryDetail
3176
+ }).catch((err) => {
3177
+ void log2("warn", "send retry trigger failed", {
3178
+ sessionID: input.sessionID,
3179
+ source: input.source,
3180
+ error: stringifyError(err)
3181
+ });
3182
+ });
3183
+ }, delayMs);
3184
+ sendRetryTimers.set(input.sessionID, timer);
3185
+ await log2("info", "send retry scheduled", {
3186
+ sessionID: input.sessionID,
3187
+ source: input.source,
3188
+ signature: input.signature,
3189
+ attempt,
3190
+ maxAttempts: cfg.maxAttempts,
3191
+ delayMs,
3192
+ error: text
3193
+ });
3194
+ };
3195
+ const nextRun = (sessionID) => {
3196
+ const next = (runSeq.get(sessionID) ?? 0) + 1;
3197
+ runSeq.set(sessionID, next);
3198
+ return next;
3199
+ };
3200
+ const loadRecentMessages = async (sessionID, limit = 200) => {
3201
+ const response = await ctx.client.session.messages({
3202
+ path: { id: sessionID },
3203
+ query: { limit }
3204
+ });
3205
+ const data = response.data ?? response;
3206
+ if (!Array.isArray(data)) return [];
3207
+ return data;
3208
+ };
3209
+ const findLastMessage = (messages, predicate) => {
3210
+ for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
3211
+ const message = messages[idx];
3212
+ if (predicate(message)) return message;
3213
+ }
3214
+ return void 0;
3215
+ };
3216
+ const findLastMessageByRole = (messages, role) => findLastMessage(messages, (message) => message?.info?.role === role);
3217
+ const resolveAutoContext = async (sessionID) => {
3218
+ const messages = await loadRecentMessages(sessionID);
3219
+ if (!messages.length) return null;
3220
+ const sortedMessages = [...messages].sort((left, right) => {
3221
+ const leftTime = left?.info?.time?.created ?? 0;
3222
+ const rightTime = right?.info?.time?.created ?? 0;
3223
+ return leftTime - rightTime;
3224
+ });
3225
+ const lastUser = findLastMessage(sortedMessages, (message) => message?.info?.role === "user");
3226
+ if (!lastUser) return { missingUser: true };
3227
+ const lastAssistant = findLastMessageByRole(sortedMessages, "assistant");
3228
+ const error = lastAssistant?.info?.error;
3229
+ const aborted = typeof error === "object" && error?.name === "MessageAbortedError";
3230
+ const finish = lastAssistant?.info?.finish;
3231
+ const ready = !lastAssistant || typeof finish === "string" && finish !== "tool-calls" && finish !== "unknown";
3232
+ const model = lastUser?.info?.model ?? (lastUser?.info?.providerID && lastUser?.info?.modelID ? { providerID: lastUser.info.providerID, modelID: lastUser.info.modelID } : lastAssistant?.info?.providerID && lastAssistant?.info?.modelID ? { providerID: lastAssistant.info.providerID, modelID: lastAssistant.info.modelID } : void 0);
3233
+ return {
3234
+ agent: lastUser?.info?.agent ?? lastAssistant?.info?.agent,
3235
+ model,
3236
+ variant: lastUser?.info?.variant,
3237
+ aborted,
3238
+ ready,
3239
+ missingUser: false,
3240
+ assistantFinish: finish,
3241
+ assistantErrorName: typeof error === "object" && error ? error.name : void 0,
3242
+ assistantErrorMessage: typeof error === "object" && error && typeof error.data?.message === "string" ? error.data.message : void 0,
3243
+ assistantErrorStatusCode: typeof error === "object" && error && typeof error.data?.statusCode === "number" && Number.isFinite(error.data.statusCode) ? Math.trunc(error.data.statusCode) : void 0,
3244
+ assistantErrorRetryable: typeof error === "object" && error && typeof error.data?.isRetryable === "boolean" ? error.data.isRetryable : void 0
3245
+ };
3246
+ };
3247
+ const toSummary = (parts) => parts.filter((part) => typeof part === "string" && part.trim().length > 0).join(" ").trim();
3248
+ const toStringArray = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
3249
+ const summarizePermissionEvent = (properties) => {
3250
+ const permission = typeof properties?.permission === "string" ? properties.permission.trim() : "";
3251
+ const patterns = toStringArray(properties?.patterns);
3252
+ return toSummary([permission, patterns.length ? `patterns=${patterns.join(",")}` : void 0]);
3253
+ };
3254
+ const summarizeQuestionEvent = (properties) => {
3255
+ const questions = Array.isArray(properties?.questions) ? properties.questions : [];
3256
+ const pieces = questions.map(
3257
+ (item) => toSummary([
3258
+ typeof item?.header === "string" ? item.header.trim() : void 0,
3259
+ typeof item?.question === "string" ? item.question.trim() : void 0
3260
+ ])
3261
+ ).filter(Boolean);
3262
+ return pieces.join(" | ");
3263
+ };
3264
+ const shouldTriggerEventRule = (rule, text) => {
3265
+ if (!rule.enabled) return false;
3266
+ return matchesKeywords(text, rule.keywords);
3267
+ };
3268
+ const shouldTriggerSessionError = (rule, error) => {
3269
+ if (!rule.enabled) return { matched: false, detail: "" };
3270
+ if (!error || typeof error !== "object") return { matched: false, detail: "" };
3271
+ const name = typeof error.name === "string" ? error.name : "";
3272
+ const message = typeof error.data?.message === "string" ? error.data.message : "";
3273
+ const statusCode = typeof error.data?.statusCode === "number" && Number.isFinite(error.data.statusCode) ? Math.trunc(error.data.statusCode) : void 0;
3274
+ const retryable = typeof error.data?.isRetryable === "boolean" ? error.data.isRetryable : void 0;
3275
+ if (rule.errorNames.length > 0 && !rule.errorNames.includes(name)) {
3276
+ return { matched: false, detail: "" };
3277
+ }
3278
+ if (rule.statusCodes.length > 0) {
3279
+ if (statusCode === void 0 || !rule.statusCodes.includes(statusCode)) {
3280
+ return { matched: false, detail: "" };
3281
+ }
3282
+ }
3283
+ if (rule.retryableOnly && retryable !== true) {
3284
+ return { matched: false, detail: "" };
3285
+ }
3286
+ const detail = toSummary([
3287
+ name ? `error=${name}` : void 0,
3288
+ statusCode !== void 0 ? `status=${statusCode}` : void 0,
3289
+ retryable !== void 0 ? `retryable=${retryable}` : void 0,
3290
+ message
3291
+ ]);
3292
+ if (!matchesKeywords(detail, rule.keywords)) {
3293
+ return { matched: false, detail };
3294
+ }
3295
+ return { matched: true, detail };
3296
+ };
3297
+ const shouldTriggerSessionRetry = (rule, status) => {
3298
+ if (!rule.enabled) return { matched: false, detail: "" };
3299
+ if (!status || typeof status !== "object") return { matched: false, detail: "" };
3300
+ if (status.type !== "retry") return { matched: false, detail: "" };
3301
+ const attempt = typeof status.attempt === "number" && Number.isFinite(status.attempt) ? Math.trunc(status.attempt) : 0;
3302
+ const message = typeof status.message === "string" ? status.message : "";
3303
+ const next = typeof status.next === "number" && Number.isFinite(status.next) ? Math.trunc(status.next) : void 0;
3304
+ if (attempt < rule.attemptAtLeast) {
3305
+ return { matched: false, detail: "" };
3306
+ }
3307
+ const detail = toSummary([
3308
+ `attempt=${attempt}`,
3309
+ next !== void 0 ? `next=${next}` : void 0,
3310
+ message
3311
+ ]);
3312
+ if (!matchesKeywords(detail, rule.keywords)) {
3313
+ return { matched: false, detail };
3314
+ }
3315
+ return { matched: true, detail };
3316
+ };
3317
+ const readTrigger = (sessionID) => {
3318
+ const current = pendingTrigger.get(sessionID);
3319
+ if (!current) return void 0;
3320
+ if (Date.now() - current.at > TRIGGER_TTL_MS) {
3321
+ pendingTrigger.delete(sessionID);
3322
+ return void 0;
3323
+ }
3324
+ return current;
3325
+ };
3326
+ const queueTrigger = async (sessionID, trigger) => {
3327
+ if (manualStop.has(sessionID)) {
3328
+ await logDebug("auto-continue trigger skipped: manual-stop guard active", {
3329
+ sessionID,
3330
+ source: trigger.source
3331
+ });
3332
+ return;
3333
+ }
3334
+ pendingTrigger.set(sessionID, {
3335
+ ...trigger,
3336
+ at: Date.now()
3337
+ });
3338
+ await logDebug("auto-continue trigger queued", {
3339
+ sessionID,
3340
+ source: trigger.source,
3341
+ force: trigger.force,
3342
+ detail: trigger.detail
3343
+ });
3344
+ await handleSessionIdle(sessionID, trigger.source);
3345
+ };
3346
+ const handleSessionIdle = async (sessionID, source) => {
3347
+ const now = Date.now();
3348
+ const run = nextRun(sessionID);
3349
+ const trigger = readTrigger(sessionID);
3350
+ const force = trigger?.force === true;
3351
+ const idleSource = source === "session.idle" || source === "session.status";
3352
+ if (inFlight.has(sessionID)) {
3353
+ await logDebug("auto-continue skipped: already in-flight", { sessionID, source, run, trigger: trigger?.source });
3354
+ return;
3355
+ }
3356
+ const stopped = manualStop.get(sessionID);
3357
+ if (stopped) {
3358
+ pendingTrigger.delete(sessionID);
3359
+ await logDebug("auto-continue skipped: manual-stop guard active", {
3360
+ sessionID,
3361
+ source,
3362
+ run,
3363
+ stopAt: stopped.at,
3364
+ stopReason: stopped.reason
3365
+ });
3366
+ return;
3367
+ }
3368
+ inFlight.add(sessionID);
3369
+ try {
3370
+ if (skipNextAuto.has(sessionID)) {
3371
+ skipNextAuto.delete(sessionID);
3372
+ pendingTrigger.delete(sessionID);
3373
+ await logDebug("auto-continue skipped: skipNextAuto", { sessionID, source, run, trigger: trigger?.source });
3374
+ return;
3375
+ }
3376
+ if (idleSource && !trigger) {
3377
+ const lastIdle = lastIdleAt.get(sessionID);
3378
+ if (lastIdle && now - lastIdle < IDLE_DEBOUNCE_MS) {
3379
+ await logDebug("auto-continue skipped: idle debounce", {
3380
+ sessionID,
3381
+ source,
3382
+ run,
3383
+ lastIdle,
3384
+ now,
3385
+ deltaMs: now - lastIdle
3386
+ });
3387
+ return;
3388
+ }
3389
+ }
3390
+ if (idleSource) {
3391
+ lastIdleAt.set(sessionID, now);
3392
+ }
3393
+ const app = new PlanpilotApp(openDatabase(), sessionID);
3394
+ const active = app.getActivePlan();
3395
+ if (!active) {
3396
+ clearWaitTimer(sessionID);
3397
+ pendingTrigger.delete(sessionID);
3398
+ await logDebug("auto-continue skipped: no active plan", { sessionID, source, run, trigger: trigger?.source });
3399
+ return;
3400
+ }
3401
+ const next = app.nextStep(active.plan_id);
3402
+ if (!next) {
3403
+ clearWaitTimer(sessionID);
3404
+ pendingTrigger.delete(sessionID);
3405
+ await logDebug("auto-continue skipped: no pending step", {
3406
+ sessionID,
3407
+ source,
3408
+ run,
3409
+ planId: active.plan_id,
3410
+ trigger: trigger?.source
3411
+ });
3412
+ return;
3413
+ }
3414
+ if (next.executor !== "ai") {
3415
+ clearWaitTimer(sessionID);
3416
+ pendingTrigger.delete(sessionID);
3417
+ await logDebug("auto-continue skipped: next executor is not ai", {
3418
+ sessionID,
3419
+ source,
3420
+ run,
3421
+ planId: active.plan_id,
3422
+ stepId: next.id,
3423
+ executor: next.executor,
3424
+ trigger: trigger?.source
3425
+ });
3426
+ return;
3427
+ }
3428
+ const wait = parseWaitFromComment(next.comment);
3429
+ if (wait && wait.until > now) {
3430
+ clearWaitTimer(sessionID);
3431
+ await log2("info", "auto-continue delayed by step wait", {
3432
+ sessionID,
3433
+ source,
3434
+ run,
3435
+ planId: active.plan_id,
3436
+ stepId: next.id,
3437
+ until: wait.until,
3438
+ reason: wait.reason,
3439
+ trigger: trigger?.source
3440
+ });
3441
+ const msUntil = Math.max(0, wait.until - now);
3442
+ const timer = setTimeout(() => {
3443
+ waitTimers.delete(sessionID);
3444
+ handleSessionIdle(sessionID, "wait_timer").catch((err) => {
3445
+ void log2("warn", "auto-continue retry failed", {
3446
+ sessionID,
3447
+ error: err instanceof Error ? err.message : String(err)
3448
+ });
3449
+ });
3450
+ }, msUntil);
3451
+ waitTimers.set(sessionID, timer);
3452
+ return;
3453
+ }
3454
+ if (!wait) {
3455
+ clearWaitTimer(sessionID);
3456
+ }
3457
+ const goals = app.goalsForStep(next.id);
3458
+ const detail = formatStepDetail(next, goals);
3459
+ if (!detail.trim()) {
3460
+ pendingTrigger.delete(sessionID);
3461
+ await log2("warn", "auto-continue stopped: empty step detail", {
3462
+ sessionID,
3463
+ source,
3464
+ run,
3465
+ planId: active.plan_id,
3466
+ stepId: next.id,
3467
+ trigger: trigger?.source
3468
+ });
3469
+ return;
3470
+ }
3471
+ const signature = `${active.plan_id}:${next.id}`;
3472
+ const retryState = sendRetryState.get(sessionID);
3473
+ if (retryState && retryState.signature !== signature) {
3474
+ clearSendRetry(sessionID);
3475
+ }
3476
+ const recent = recentSends.get(sessionID);
3477
+ if (recent && recent.signature === signature && now - recent.at < RECENT_SEND_DEDUPE_MS) {
3478
+ pendingTrigger.delete(sessionID);
3479
+ await logDebug("auto-continue skipped: duplicate send window", {
3480
+ sessionID,
3481
+ source,
3482
+ run,
3483
+ planId: active.plan_id,
3484
+ stepId: next.id,
3485
+ trigger: trigger?.source,
3486
+ deltaMs: now - recent.at
3487
+ });
3488
+ return;
3489
+ }
3490
+ const autoContext = await resolveAutoContext(sessionID);
3491
+ if (autoContext?.missingUser) {
3492
+ pendingTrigger.delete(sessionID);
3493
+ await log2("warn", "auto-continue stopped: missing user context", { sessionID, source, run, trigger: trigger?.source });
3494
+ return;
3495
+ }
3496
+ if (!autoContext) {
3497
+ pendingTrigger.delete(sessionID);
3498
+ await logDebug("auto-continue stopped: missing autoContext (no recent messages?)", {
3499
+ sessionID,
3500
+ source,
3501
+ run,
3502
+ planId: active.plan_id,
3503
+ stepId: next.id,
3504
+ trigger: trigger?.source
3505
+ });
3506
+ return;
3507
+ }
3508
+ if (autoContext.aborted && !force) {
3509
+ pendingTrigger.delete(sessionID);
3510
+ await logDebug("auto-continue skipped: last assistant aborted", {
3511
+ sessionID,
3512
+ source,
3513
+ run,
3514
+ planId: active.plan_id,
3515
+ stepId: next.id,
3516
+ assistantErrorName: autoContext.assistantErrorName,
3517
+ assistantErrorMessage: autoContext.assistantErrorMessage,
3518
+ assistantFinish: autoContext.assistantFinish,
3519
+ trigger: trigger?.source
3520
+ });
3521
+ return;
3522
+ }
3523
+ if (autoContext.ready === false && !force) {
3524
+ pendingTrigger.delete(sessionID);
3525
+ await logDebug("auto-continue skipped: last assistant not ready", {
3526
+ sessionID,
3527
+ source,
3528
+ run,
3529
+ planId: active.plan_id,
3530
+ stepId: next.id,
3531
+ assistantFinish: autoContext.assistantFinish,
3532
+ assistantErrorName: autoContext.assistantErrorName,
3533
+ assistantErrorMessage: autoContext.assistantErrorMessage,
3534
+ trigger: trigger?.source
3535
+ });
3536
+ return;
3537
+ }
3538
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3539
+ const message = formatPlanpilotAutoContinueMessage({
3540
+ timestamp,
3541
+ stepDetail: detail,
3542
+ triggerDetail: trigger?.detail
3543
+ });
3544
+ const promptBody = {
3545
+ agent: autoContext.agent ?? void 0,
3546
+ model: autoContext.model ?? void 0,
3547
+ parts: [{ type: "text", text: message }]
3548
+ };
3549
+ if (autoContext.variant) {
3550
+ promptBody.variant = autoContext.variant;
3551
+ }
3552
+ await logDebug("auto-continue sending prompt_async", {
3553
+ sessionID,
3554
+ source,
3555
+ run,
3556
+ planId: active.plan_id,
3557
+ stepId: next.id,
3558
+ trigger: trigger?.source,
3559
+ force,
3560
+ agent: promptBody.agent,
3561
+ model: promptBody.model,
3562
+ variant: promptBody.variant,
3563
+ messageChars: message.length
3564
+ });
3565
+ await ctx.client.session.promptAsync({
3566
+ path: { id: sessionID },
3567
+ body: promptBody,
3568
+ // OpenCode server routes requests to the correct instance (project) using this header.
3569
+ // Without it, the server falls back to process.cwd(), which breaks when OpenCode is
3570
+ // managed by opencode-studio (cwd != active project directory).
3571
+ headers: ctx.directory ? { "x-opencode-directory": ctx.directory } : void 0
3572
+ }).catch(async (err) => {
3573
+ await scheduleSendRetry({
3574
+ sessionID,
3575
+ signature,
3576
+ source,
3577
+ force,
3578
+ detail: trigger?.detail,
3579
+ error: err
3580
+ });
3581
+ throw err;
3582
+ });
3583
+ recentSends.set(sessionID, {
3584
+ signature,
3585
+ at: Date.now()
3586
+ });
3587
+ clearSendRetry(sessionID);
3588
+ pendingTrigger.delete(sessionID);
3589
+ await log2("info", "auto-continue prompt_async accepted", {
3590
+ sessionID,
3591
+ source,
3592
+ run,
3593
+ planId: active.plan_id,
3594
+ stepId: next.id,
3595
+ trigger: trigger?.source,
3596
+ force
3597
+ });
3598
+ } catch (err) {
3599
+ await log2("warn", "failed to auto-continue plan", {
3600
+ sessionID,
3601
+ source,
3602
+ error: err instanceof Error ? err.message : String(err),
3603
+ stack: err instanceof Error ? err.stack : void 0
3604
+ });
3605
+ } finally {
3606
+ inFlight.delete(sessionID);
3607
+ }
3608
+ };
3609
+ if (loadedConfig.loadError) {
3610
+ await log2("warn", "failed to load planpilot config, falling back to defaults", {
3611
+ path: loadedConfig.path,
3612
+ error: loadedConfig.loadError
3613
+ });
3614
+ }
3615
+ await log2("info", "planpilot plugin initialized", {
3616
+ directory: ctx.directory,
3617
+ worktree: ctx.worktree,
3618
+ configPath: loadedConfig.path,
3619
+ configLoadedFromFile: loadedConfig.loadedFromFile
3620
+ });
3621
+ return {
3622
+ "experimental.chat.system.transform": async (_input, output) => {
3623
+ output.system.push(PLANPILOT_SYSTEM_INJECTION);
3624
+ },
3625
+ tool: {
3626
+ planpilot: tool({
3627
+ description: PLANPILOT_TOOL_DESCRIPTION,
3628
+ args: {
3629
+ argv: tool.schema.array(tool.schema.string()).min(1)
3630
+ },
3631
+ async execute(args, toolCtx) {
3632
+ const argv = Array.isArray(args.argv) ? args.argv : [];
3633
+ if (!argv.length) {
3634
+ return formatCommandError(invalidInput("missing argv"));
3635
+ }
3636
+ if (containsForbiddenFlags(argv)) {
3637
+ return formatCommandError(invalidInput("argv cannot include --cwd or --session-id"));
3638
+ }
3639
+ const cwd = (ctx.directory ?? "").trim();
3640
+ if (!cwd) {
3641
+ return formatCommandError(invalidInput("cwd is required"));
3642
+ }
3643
+ const output = [];
3644
+ const io = {
3645
+ log: (...parts) => output.push(parts.map(String).join(" "))
3646
+ };
3647
+ try {
3648
+ await runCommand(argv, { sessionId: toolCtx.sessionID, cwd }, io);
3649
+ } catch (err) {
3650
+ return formatCommandError(err);
3651
+ }
3652
+ return output.join("\n").trimEnd();
3653
+ }
3654
+ })
3655
+ },
3656
+ "experimental.session.compacting": async ({ sessionID }, output) => {
3657
+ skipNextAuto.add(sessionID);
3658
+ lastIdleAt.set(sessionID, Date.now());
3659
+ await logDebug("compaction hook: skip next auto-continue", { sessionID });
3660
+ output.context.push(PLANPILOT_TOOL_DESCRIPTION);
3661
+ },
3662
+ event: async ({ event }) => {
3663
+ const evt = event;
3664
+ if (evt.type === "message.updated") {
3665
+ const info = evt.properties?.info;
3666
+ const sessionID = typeof info?.sessionID === "string" ? info.sessionID : "";
3667
+ if (!sessionID) return;
3668
+ if (info.role === "user") {
3669
+ pendingTrigger.delete(sessionID);
3670
+ clearSendRetry(sessionID);
3671
+ await clearManualStop(sessionID, "message.updated.user");
3672
+ return;
3673
+ }
3674
+ if (info.role === "assistant" && info.error?.name === "MessageAbortedError") {
3675
+ await setManualStop(sessionID, "assistant message aborted", "message.updated.assistant");
3676
+ }
3677
+ return;
3678
+ }
3679
+ if (evt.type === "session.idle") {
3680
+ await handleSessionIdle(evt.properties.sessionID, "session.idle");
3681
+ return;
3682
+ }
3683
+ if (evt.type === "session.status") {
3684
+ if (evt.properties.status.type === "idle") {
3685
+ await handleSessionIdle(evt.properties.sessionID, "session.status");
3686
+ return;
3687
+ }
3688
+ const retryResult = shouldTriggerSessionRetry(autoConfig.onSessionRetry, evt.properties.status);
3689
+ if (!retryResult.matched) return;
3690
+ await queueTrigger(evt.properties.sessionID, {
3691
+ source: "session.status.retry",
3692
+ force: autoConfig.onSessionRetry.force,
3693
+ detail: retryResult.detail || "session status retry"
3694
+ });
3695
+ return;
3696
+ }
3697
+ if (evt.type === "session.error") {
3698
+ const sessionID = typeof evt.properties?.sessionID === "string" ? evt.properties.sessionID : "";
3699
+ if (!sessionID) return;
3700
+ if (evt.properties?.error?.name === "MessageAbortedError") {
3701
+ await setManualStop(sessionID, "session aborted", "session.error");
3702
+ return;
3703
+ }
3704
+ const errorResult = shouldTriggerSessionError(autoConfig.onSessionError, evt.properties?.error);
3705
+ if (!errorResult.matched) return;
3706
+ await queueTrigger(sessionID, {
3707
+ source: "session.error",
3708
+ force: autoConfig.onSessionError.force,
3709
+ detail: errorResult.detail || "session error"
3710
+ });
3711
+ return;
3712
+ }
3713
+ if (evt.type === "permission.asked") {
3714
+ const sessionID = typeof evt.properties?.sessionID === "string" ? evt.properties.sessionID : "";
3715
+ if (!sessionID) return;
3716
+ const requestID = typeof evt.properties?.id === "string" ? evt.properties.id : "";
3717
+ const summary = summarizePermissionEvent(evt.properties);
3718
+ if (requestID) {
3719
+ permissionAsked.set(requestID, {
3720
+ sessionID,
3721
+ summary
3722
+ });
3723
+ }
3724
+ if (!shouldTriggerEventRule(autoConfig.onPermissionAsked, summary || "permission asked")) return;
3725
+ await queueTrigger(sessionID, {
3726
+ source: "permission.asked",
3727
+ force: autoConfig.onPermissionAsked.force,
3728
+ detail: summary || "permission asked"
3729
+ });
3730
+ return;
3731
+ }
3732
+ if (evt.type === "permission.replied") {
3733
+ const sessionID = typeof evt.properties?.sessionID === "string" ? evt.properties.sessionID : "";
3734
+ if (!sessionID) return;
3735
+ const requestID = typeof evt.properties?.requestID === "string" ? evt.properties.requestID : typeof evt.properties?.permissionID === "string" ? evt.properties.permissionID : "";
3736
+ const reply = typeof evt.properties?.reply === "string" ? evt.properties.reply : typeof evt.properties?.response === "string" ? evt.properties.response : "";
3737
+ const asked = requestID ? permissionAsked.get(requestID) : void 0;
3738
+ if (requestID) {
3739
+ permissionAsked.delete(requestID);
3740
+ }
3741
+ if (reply !== "reject") return;
3742
+ const summary = toSummary([
3743
+ asked?.summary,
3744
+ requestID ? `request=${requestID}` : void 0,
3745
+ "reply=reject"
3746
+ ]);
3747
+ if (!shouldTriggerEventRule(autoConfig.onPermissionRejected, summary || "permission rejected")) return;
3748
+ await queueTrigger(sessionID, {
3749
+ source: "permission.replied.reject",
3750
+ force: autoConfig.onPermissionRejected.force,
3751
+ detail: summary || "permission rejected"
3752
+ });
3753
+ return;
3754
+ }
3755
+ if (evt.type === "question.asked") {
3756
+ const sessionID = typeof evt.properties?.sessionID === "string" ? evt.properties.sessionID : "";
3757
+ if (!sessionID) return;
3758
+ const requestID = typeof evt.properties?.id === "string" ? evt.properties.id : "";
3759
+ const summary = summarizeQuestionEvent(evt.properties);
3760
+ if (requestID) {
3761
+ questionAsked.set(requestID, {
3762
+ sessionID,
3763
+ summary
3764
+ });
3765
+ }
3766
+ if (!shouldTriggerEventRule(autoConfig.onQuestionAsked, summary || "question asked")) return;
3767
+ await queueTrigger(sessionID, {
3768
+ source: "question.asked",
3769
+ force: autoConfig.onQuestionAsked.force,
3770
+ detail: summary || "question asked"
3771
+ });
3772
+ return;
3773
+ }
3774
+ if (evt.type === "question.replied") {
3775
+ const requestID = typeof evt.properties?.requestID === "string" ? evt.properties.requestID : "";
3776
+ if (!requestID) return;
3777
+ questionAsked.delete(requestID);
3778
+ return;
3779
+ }
3780
+ if (evt.type === "question.rejected") {
3781
+ const sessionID = typeof evt.properties?.sessionID === "string" ? evt.properties.sessionID : "";
3782
+ if (!sessionID) return;
3783
+ const requestID = typeof evt.properties?.requestID === "string" ? evt.properties.requestID : "";
3784
+ const asked = requestID ? questionAsked.get(requestID) : void 0;
3785
+ if (requestID) {
3786
+ questionAsked.delete(requestID);
3787
+ }
3788
+ const summary = toSummary([
3789
+ asked?.summary,
3790
+ requestID ? `request=${requestID}` : void 0,
3791
+ "question=rejected"
3792
+ ]);
3793
+ if (!shouldTriggerEventRule(autoConfig.onQuestionRejected, summary || "question rejected")) return;
3794
+ await queueTrigger(sessionID, {
3795
+ source: "question.rejected",
3796
+ force: autoConfig.onQuestionRejected.force,
3797
+ detail: summary || "question rejected"
3798
+ });
3799
+ }
3800
+ }
3801
+ };
3802
+ };
3803
+ var src_default = PlanpilotPlugin;
3804
+ function containsForbiddenFlags(argv) {
3805
+ return argv.some((token) => {
3806
+ if (token === "--cwd" || token === "--session-id") return true;
3807
+ if (token.startsWith("--cwd=") || token.startsWith("--session-id=")) return true;
3808
+ return false;
3809
+ });
3810
+ }
3811
+ export {
3812
+ PlanpilotPlugin,
3813
+ src_default as default
3814
+ };
3815
+ //# sourceMappingURL=index.js.map