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 +1 -1
- package/src/command.ts +0 -13
- package/src/index.ts +149 -17
- package/src/prompt.ts +0 -1
package/package.json
CHANGED
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
|
-
|
|
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,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", {
|
|
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]",
|