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.
- package/package.json +1 -1
- package/src/index.ts +145 -17
package/package.json
CHANGED
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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)
|
|
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)
|
|
136
|
-
|
|
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())
|
|
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
|
|
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", {
|
|
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
|
}
|