opencode-planpilot 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-planpilot",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Planpilot plugin for OpenCode",
5
5
  "type": "module",
6
6
  "repository": {
package/src/command.ts CHANGED
@@ -125,8 +125,6 @@ async function handlePlan(
125
125
  context: { cwd: string | undefined },
126
126
  ) {
127
127
  switch (subcommand) {
128
- case "add":
129
- return { planIds: handlePlanAdd(app, args), shouldSync: true }
130
128
  case "add-tree":
131
129
  return { planIds: handlePlanAddTree(app, args), shouldSync: true }
132
130
  case "list":
@@ -212,17 +210,6 @@ async function handleGoal(app: PlanpilotApp, subcommand: string | undefined, arg
212
210
  }
213
211
  }
214
212
 
215
- function handlePlanAdd(app: PlanpilotApp, args: string[]): number[] {
216
- const [title, content] = args
217
- if (!title || content === undefined) {
218
- throw invalidInput("plan add requires <title> <content>")
219
- }
220
- ensureNonEmpty("plan content", content)
221
- const plan = app.addPlan({ title, content })
222
- log(`Created plan ID: ${plan.id}: ${plan.title}`)
223
- return [plan.id]
224
- }
225
-
226
213
  function handlePlanAddTree(app: PlanpilotApp, args: string[]): number[] {
227
214
  const [title, content, ...rest] = args
228
215
  if (!title || content === undefined) {
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
12
12
  const skipNextAuto = new Set<string>()
13
13
  const lastIdleAt = new Map<string, number>()
14
14
  const waitTimers = new Map<string, ReturnType<typeof setTimeout>>()
15
+ const runSeq = new Map<string, number>()
15
16
 
16
17
  const clearWaitTimer = (sessionID: string) => {
17
18
  const existing = waitTimers.get(sessionID)
@@ -36,6 +37,16 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
36
37
  }
37
38
  }
38
39
 
40
+ const logDebug = async (message: string, extra?: Record<string, any>) => {
41
+ await log("debug", message, extra)
42
+ }
43
+
44
+ const nextRun = (sessionID: string) => {
45
+ const next = (runSeq.get(sessionID) ?? 0) + 1
46
+ runSeq.set(sessionID, next)
47
+ return next
48
+ }
49
+
39
50
  type SessionMessage = {
40
51
  info?: {
41
52
  role?: string
@@ -113,32 +124,76 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
113
124
  aborted,
114
125
  ready,
115
126
  missingUser: false,
127
+ assistantFinish: finish,
128
+ assistantErrorName: typeof error === "object" && error ? (error as any).name : undefined,
116
129
  }
117
130
  }
118
131
 
119
- const handleSessionIdle = async (sessionID: string) => {
120
- if (inFlight.has(sessionID)) return
121
- if (skipNextAuto.has(sessionID)) {
122
- skipNextAuto.delete(sessionID)
132
+ const handleSessionIdle = async (sessionID: string, source: string) => {
133
+ const now = Date.now()
134
+ const run = nextRun(sessionID)
135
+
136
+ if (inFlight.has(sessionID)) {
137
+ await logDebug("auto-continue skipped: already in-flight", { sessionID, source, run })
123
138
  return
124
139
  }
125
- const lastIdle = lastIdleAt.get(sessionID)
126
- const now = Date.now()
127
- if (lastIdle && now - lastIdle < 1000) return
128
- lastIdleAt.set(sessionID, now)
129
140
  inFlight.add(sessionID)
130
141
  try {
142
+ if (skipNextAuto.has(sessionID)) {
143
+ skipNextAuto.delete(sessionID)
144
+ await logDebug("auto-continue skipped: skipNextAuto", { sessionID, source, run })
145
+ return
146
+ }
147
+
148
+ const lastIdle = lastIdleAt.get(sessionID)
149
+ if (lastIdle && now - lastIdle < 1000) {
150
+ await logDebug("auto-continue skipped: idle debounce", {
151
+ sessionID,
152
+ source,
153
+ run,
154
+ lastIdle,
155
+ now,
156
+ deltaMs: now - lastIdle,
157
+ })
158
+ return
159
+ }
160
+
161
+ lastIdleAt.set(sessionID, now)
162
+
131
163
  const app = new PlanpilotApp(openDatabase(), sessionID)
132
164
  const active = app.getActivePlan()
133
- if (!active) return
165
+ if (!active) {
166
+ clearWaitTimer(sessionID)
167
+ await logDebug("auto-continue skipped: no active plan", { sessionID, source, run })
168
+ return
169
+ }
134
170
  const next = app.nextStep(active.plan_id)
135
- if (!next) return
136
- if (next.executor !== "ai") return
171
+ if (!next) {
172
+ clearWaitTimer(sessionID)
173
+ await logDebug("auto-continue skipped: no pending step", { sessionID, source, run, planId: active.plan_id })
174
+ return
175
+ }
176
+ if (next.executor !== "ai") {
177
+ clearWaitTimer(sessionID)
178
+ await logDebug("auto-continue skipped: next executor is not ai", {
179
+ sessionID,
180
+ source,
181
+ run,
182
+ planId: active.plan_id,
183
+ stepId: next.id,
184
+ executor: next.executor,
185
+ })
186
+ return
187
+ }
188
+
137
189
  const wait = parseWaitFromComment(next.comment)
138
190
  if (wait && wait.until > now) {
139
191
  clearWaitTimer(sessionID)
140
192
  await log("info", "auto-continue delayed by step wait", {
141
193
  sessionID,
194
+ source,
195
+ run,
196
+ planId: active.plan_id,
142
197
  stepId: next.id,
143
198
  until: wait.until,
144
199
  reason: wait.reason,
@@ -146,7 +201,7 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
146
201
  const msUntil = Math.max(0, wait.until - now)
147
202
  const timer = setTimeout(() => {
148
203
  waitTimers.delete(sessionID)
149
- handleSessionIdle(sessionID).catch((err) => {
204
+ handleSessionIdle(sessionID, "wait_timer").catch((err) => {
150
205
  void log("warn", "auto-continue retry failed", {
151
206
  sessionID,
152
207
  error: err instanceof Error ? err.message : String(err),
@@ -162,14 +217,55 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
162
217
 
163
218
  const goals = app.goalsForStep(next.id)
164
219
  const detail = formatStepDetail(next, goals)
165
- if (!detail.trim()) return
220
+ if (!detail.trim()) {
221
+ await log("warn", "auto-continue stopped: empty step detail", {
222
+ sessionID,
223
+ source,
224
+ run,
225
+ planId: active.plan_id,
226
+ stepId: next.id,
227
+ })
228
+ return
229
+ }
166
230
 
167
231
  const autoContext = await resolveAutoContext(sessionID)
168
232
  if (autoContext?.missingUser) {
169
233
  await log("warn", "auto-continue stopped: missing user context", { sessionID })
170
234
  return
171
235
  }
172
- if (autoContext?.aborted || autoContext?.ready === false) return
236
+ if (!autoContext) {
237
+ await logDebug("auto-continue stopped: missing autoContext (no recent messages?)", {
238
+ sessionID,
239
+ source,
240
+ run,
241
+ planId: active.plan_id,
242
+ stepId: next.id,
243
+ })
244
+ return
245
+ }
246
+ if (autoContext.aborted) {
247
+ await logDebug("auto-continue skipped: last assistant aborted", {
248
+ sessionID,
249
+ source,
250
+ run,
251
+ planId: active.plan_id,
252
+ stepId: next.id,
253
+ assistantErrorName: autoContext.assistantErrorName,
254
+ assistantFinish: autoContext.assistantFinish,
255
+ })
256
+ return
257
+ }
258
+ if (autoContext.ready === false) {
259
+ await logDebug("auto-continue skipped: last assistant not ready", {
260
+ sessionID,
261
+ source,
262
+ run,
263
+ planId: active.plan_id,
264
+ stepId: next.id,
265
+ assistantFinish: autoContext.assistantFinish,
266
+ })
267
+ return
268
+ }
173
269
 
174
270
  const timestamp = new Date().toISOString()
175
271
  const message = formatPlanpilotAutoContinueMessage({
@@ -186,17 +282,51 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
186
282
  promptBody.variant = autoContext.variant
187
283
  }
188
284
 
285
+ await logDebug("auto-continue sending prompt_async", {
286
+ sessionID,
287
+ source,
288
+ run,
289
+ planId: active.plan_id,
290
+ stepId: next.id,
291
+ agent: promptBody.agent,
292
+ model: promptBody.model,
293
+ variant: promptBody.variant,
294
+ messageChars: message.length,
295
+ })
296
+
189
297
  await ctx.client.session.promptAsync({
190
298
  path: { id: sessionID },
191
299
  body: promptBody,
300
+ // OpenCode server routes requests to the correct instance (project) using this header.
301
+ // Without it, the server falls back to process.cwd(), which breaks when OpenCode is
302
+ // managed by opencode-studio (cwd != active project directory).
303
+ headers: ctx.directory ? { "x-opencode-directory": ctx.directory } : undefined,
304
+ })
305
+
306
+ await log("info", "auto-continue prompt_async accepted", {
307
+ sessionID,
308
+ source,
309
+ run,
310
+ planId: active.plan_id,
311
+ stepId: next.id,
192
312
  })
193
313
  } catch (err) {
194
- await log("warn", "failed to auto-continue plan", { error: err instanceof Error ? err.message : String(err) })
314
+ await log("warn", "failed to auto-continue plan", {
315
+ sessionID,
316
+ source,
317
+ error: err instanceof Error ? err.message : String(err),
318
+ stack: err instanceof Error ? err.stack : undefined,
319
+ })
195
320
  } finally {
196
321
  inFlight.delete(sessionID)
197
322
  }
198
323
  }
199
324
 
325
+ await log("info", "planpilot plugin initialized", {
326
+ directory: ctx.directory,
327
+ worktree: ctx.worktree,
328
+ })
329
+
200
330
  return {
201
331
  "experimental.chat.system.transform": async (_input, output) => {
202
332
  output.system.push(PLANPILOT_SYSTEM_INJECTION)
@@ -241,16 +371,18 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
241
371
  skipNextAuto.add(sessionID)
242
372
  lastIdleAt.set(sessionID, Date.now())
243
373
 
374
+ await logDebug("compaction hook: skip next auto-continue", { sessionID })
375
+
244
376
  // Compaction runs with tools disabled; inject Planpilot guidance into the continuation summary.
245
377
  output.context.push(PLANPILOT_TOOL_DESCRIPTION)
246
378
  },
247
379
  event: async ({ event }) => {
248
380
  if (event.type === "session.idle") {
249
- await handleSessionIdle(event.properties.sessionID)
381
+ await handleSessionIdle(event.properties.sessionID, "session.idle")
250
382
  return
251
383
  }
252
384
  if (event.type === "session.status" && event.properties.status.type === "idle") {
253
- await handleSessionIdle(event.properties.sessionID)
385
+ await handleSessionIdle(event.properties.sessionID, "session.status")
254
386
  }
255
387
  },
256
388
  }
package/src/prompt.ts CHANGED
@@ -42,7 +42,6 @@ export const PLANPILOT_HELP_TEXT = [
42
42
  "- help",
43
43
  "",
44
44
  "Plan:",
45
- "- plan add <title> <content>",
46
45
  "- plan add-tree <title> <content> --step <content> [--executor ai|human] [--goal <content>]... [--step ...]...",
47
46
  "- plan list [--scope project|all] [--status todo|done|all] [--limit N] [--page N] [--order id|title|created|updated] [--desc]",
48
47
  "- plan count [--scope project|all] [--status todo|done|all]",