opencode-planpilot 0.2.1 → 0.2.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +145 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-planpilot",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Planpilot plugin for OpenCode",
5
5
  "type": "module",
6
6
  "repository": {
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,47 @@ 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,
192
300
  })
301
+
302
+ await log("info", "auto-continue prompt_async accepted", {
303
+ sessionID,
304
+ source,
305
+ run,
306
+ planId: active.plan_id,
307
+ stepId: next.id,
308
+ })
193
309
  } catch (err) {
194
- await log("warn", "failed to auto-continue plan", { error: err instanceof Error ? err.message : String(err) })
310
+ await log("warn", "failed to auto-continue plan", {
311
+ sessionID,
312
+ source,
313
+ error: err instanceof Error ? err.message : String(err),
314
+ stack: err instanceof Error ? err.stack : undefined,
315
+ })
195
316
  } finally {
196
317
  inFlight.delete(sessionID)
197
318
  }
198
319
  }
199
320
 
321
+ await log("info", "planpilot plugin initialized", {
322
+ directory: ctx.directory,
323
+ worktree: ctx.worktree,
324
+ })
325
+
200
326
  return {
201
327
  "experimental.chat.system.transform": async (_input, output) => {
202
328
  output.system.push(PLANPILOT_SYSTEM_INJECTION)
@@ -241,16 +367,18 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
241
367
  skipNextAuto.add(sessionID)
242
368
  lastIdleAt.set(sessionID, Date.now())
243
369
 
370
+ await logDebug("compaction hook: skip next auto-continue", { sessionID })
371
+
244
372
  // Compaction runs with tools disabled; inject Planpilot guidance into the continuation summary.
245
373
  output.context.push(PLANPILOT_TOOL_DESCRIPTION)
246
374
  },
247
375
  event: async ({ event }) => {
248
376
  if (event.type === "session.idle") {
249
- await handleSessionIdle(event.properties.sessionID)
377
+ await handleSessionIdle(event.properties.sessionID, "session.idle")
250
378
  return
251
379
  }
252
380
  if (event.type === "session.status" && event.properties.status.type === "idle") {
253
- await handleSessionIdle(event.properties.sessionID)
381
+ await handleSessionIdle(event.properties.sessionID, "session.status")
254
382
  }
255
383
  },
256
384
  }