opencode-planpilot 0.1.0 → 0.1.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/docs/planpilot.md +1 -0
- package/package.json +2 -2
- package/src/index.ts +128 -3
package/docs/planpilot.md
CHANGED
|
@@ -42,6 +42,7 @@ A CLI binary named `planpilot` is available for manual use. If you use the CLI d
|
|
|
42
42
|
|
|
43
43
|
## AI Workflow Guidelines
|
|
44
44
|
- Use Planpilot for all planning, status, and progress tracking; do not use built-in plan/todo tools or other methods to track plan/step/goal status.
|
|
45
|
+
- Do not read plan files from disk or follow plan file placeholders; use the planpilot tool for plan/step/goal info.
|
|
45
46
|
- Treat tool output as authoritative. Do not invent IDs; only use IDs shown by `list`/`show`.
|
|
46
47
|
- If the tool is missing or unavailable, ask the user to enable/install the plugin.
|
|
47
48
|
- Record implementation details using Planpilot comments (plan/step/goal `--comment` or `comment` commands). Before starting a step or goal, think through the next actions and capture that context in comments so the plan stays actionable.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -9,6 +9,15 @@ import { loadPlanpilotInstructions } from "./lib/instructions"
|
|
|
9
9
|
|
|
10
10
|
export const PlanpilotPlugin: Plugin = async (ctx) => {
|
|
11
11
|
const inFlight = new Set<string>()
|
|
12
|
+
const skipNextAuto = new Set<string>()
|
|
13
|
+
const lastIdleAt = new Map<string, number>()
|
|
14
|
+
|
|
15
|
+
const PLANPILOT_GUIDANCE = [
|
|
16
|
+
"Planpilot guidance:",
|
|
17
|
+
"- Do not read plan files from disk or follow plan file placeholders.",
|
|
18
|
+
"- Use the planpilot tool for plan/step/goal info (plan show-active, step show-next, goal list <step_id>).",
|
|
19
|
+
"- If you cannot continue or need human input, insert a new step with executor `human` before the next pending step using planpilot so auto-continue pauses.",
|
|
20
|
+
].join("\n")
|
|
12
21
|
|
|
13
22
|
const log = async (level: "debug" | "info" | "warn" | "error", message: string, extra?: Record<string, any>) => {
|
|
14
23
|
try {
|
|
@@ -25,8 +34,96 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
|
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
36
|
|
|
37
|
+
type SessionMessage = {
|
|
38
|
+
info?: {
|
|
39
|
+
role?: string
|
|
40
|
+
agent?: string
|
|
41
|
+
model?: {
|
|
42
|
+
providerID: string
|
|
43
|
+
modelID: string
|
|
44
|
+
}
|
|
45
|
+
modelID?: string
|
|
46
|
+
providerID?: string
|
|
47
|
+
variant?: string
|
|
48
|
+
time?: {
|
|
49
|
+
created?: number
|
|
50
|
+
}
|
|
51
|
+
error?: {
|
|
52
|
+
name?: string
|
|
53
|
+
}
|
|
54
|
+
finish?: string
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const loadRecentMessages = async (sessionID: string, limit = 200): Promise<SessionMessage[]> => {
|
|
59
|
+
const response = await ctx.client.session.messages({
|
|
60
|
+
path: { id: sessionID },
|
|
61
|
+
query: { limit },
|
|
62
|
+
})
|
|
63
|
+
const data = (response as { data?: unknown }).data ?? response
|
|
64
|
+
if (!Array.isArray(data)) return []
|
|
65
|
+
return data as SessionMessage[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const findLastMessage = (messages: SessionMessage[], predicate: (message: SessionMessage) => boolean) => {
|
|
69
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
70
|
+
const message = messages[idx]
|
|
71
|
+
if (predicate(message)) return message
|
|
72
|
+
}
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const findLastMessageByRole = (messages: SessionMessage[], role: "user" | "assistant") =>
|
|
77
|
+
findLastMessage(messages, (message) => message?.info?.role === role)
|
|
78
|
+
|
|
79
|
+
const resolveAutoContext = async (sessionID: string) => {
|
|
80
|
+
const messages = await loadRecentMessages(sessionID)
|
|
81
|
+
if (!messages.length) return null
|
|
82
|
+
|
|
83
|
+
const sortedMessages = [...messages].sort((left, right) => {
|
|
84
|
+
const leftTime = left?.info?.time?.created ?? 0
|
|
85
|
+
const rightTime = right?.info?.time?.created ?? 0
|
|
86
|
+
return leftTime - rightTime
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const lastUser = findLastMessage(sortedMessages, (message) => message?.info?.role === "user")
|
|
90
|
+
if (!lastUser) return { missingUser: true }
|
|
91
|
+
|
|
92
|
+
const lastAssistant = findLastMessageByRole(sortedMessages, "assistant")
|
|
93
|
+
const error = lastAssistant?.info?.error
|
|
94
|
+
const aborted = typeof error === "object" && error?.name === "MessageAbortedError"
|
|
95
|
+
const finish = lastAssistant?.info?.finish
|
|
96
|
+
const ready =
|
|
97
|
+
!lastAssistant || (typeof finish === "string" && finish !== "tool-calls" && finish !== "unknown")
|
|
98
|
+
|
|
99
|
+
const model =
|
|
100
|
+
lastUser?.info?.model ??
|
|
101
|
+
(lastUser?.info?.providerID && lastUser?.info?.modelID
|
|
102
|
+
? { providerID: lastUser.info.providerID, modelID: lastUser.info.modelID }
|
|
103
|
+
: lastAssistant?.info?.providerID && lastAssistant?.info?.modelID
|
|
104
|
+
? { providerID: lastAssistant.info.providerID, modelID: lastAssistant.info.modelID }
|
|
105
|
+
: undefined)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
agent: lastUser?.info?.agent ?? lastAssistant?.info?.agent,
|
|
109
|
+
model,
|
|
110
|
+
variant: lastUser?.info?.variant,
|
|
111
|
+
aborted,
|
|
112
|
+
ready,
|
|
113
|
+
missingUser: false,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
28
117
|
const handleSessionIdle = async (sessionID: string) => {
|
|
29
118
|
if (inFlight.has(sessionID)) return
|
|
119
|
+
if (skipNextAuto.has(sessionID)) {
|
|
120
|
+
skipNextAuto.delete(sessionID)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
const lastIdle = lastIdleAt.get(sessionID)
|
|
124
|
+
const now = Date.now()
|
|
125
|
+
if (lastIdle && now - lastIdle < 1000) return
|
|
126
|
+
lastIdleAt.set(sessionID, now)
|
|
30
127
|
inFlight.add(sessionID)
|
|
31
128
|
try {
|
|
32
129
|
const app = new PlanpilotApp(openDatabase(), sessionID)
|
|
@@ -39,16 +136,32 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
|
|
|
39
136
|
const detail = formatStepDetail(next, goals)
|
|
40
137
|
if (!detail.trim()) return
|
|
41
138
|
|
|
139
|
+
const autoContext = await resolveAutoContext(sessionID)
|
|
140
|
+
if (autoContext?.missingUser) {
|
|
141
|
+
await log("warn", "auto-continue stopped: missing user context", { sessionID })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
if (autoContext?.aborted || autoContext?.ready === false) return
|
|
145
|
+
|
|
42
146
|
const message =
|
|
43
147
|
"Planpilot (auto):\n" +
|
|
44
148
|
"Before acting, think through the next step and its goals. Record implementation details using Planpilot comments (plan/step/goal --comment or comment commands). Continue with the next step (executor: ai). Do not ask for confirmation; proceed and report results.\n\n" +
|
|
149
|
+
PLANPILOT_GUIDANCE +
|
|
150
|
+
"\n\n" +
|
|
45
151
|
detail.trimEnd()
|
|
46
152
|
|
|
153
|
+
const promptBody: any = {
|
|
154
|
+
agent: autoContext?.agent ?? undefined,
|
|
155
|
+
model: autoContext?.model ?? undefined,
|
|
156
|
+
parts: [{ type: "text" as const, text: message }],
|
|
157
|
+
}
|
|
158
|
+
if (autoContext?.variant) {
|
|
159
|
+
promptBody.variant = autoContext.variant
|
|
160
|
+
}
|
|
161
|
+
|
|
47
162
|
await ctx.client.session.promptAsync({
|
|
48
163
|
path: { id: sessionID },
|
|
49
|
-
body:
|
|
50
|
-
parts: [{ type: "text", text: message }],
|
|
51
|
-
},
|
|
164
|
+
body: promptBody,
|
|
52
165
|
})
|
|
53
166
|
} catch (err) {
|
|
54
167
|
await log("warn", "failed to auto-continue plan", { error: err instanceof Error ? err.message : String(err) })
|
|
@@ -116,6 +229,18 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
|
|
|
116
229
|
if (instructions && !alreadyInjected) {
|
|
117
230
|
output.system.push(instructions)
|
|
118
231
|
}
|
|
232
|
+
const guidanceInjected = output.system.some((entry) => entry.includes("Planpilot guidance:"))
|
|
233
|
+
if (!guidanceInjected) {
|
|
234
|
+
output.system.push(PLANPILOT_GUIDANCE)
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
"experimental.session.compacting": async ({ sessionID }, output) => {
|
|
238
|
+
const hasGuidance = output.context.some((entry) => entry.includes("Planpilot guidance:"))
|
|
239
|
+
if (!hasGuidance) {
|
|
240
|
+
output.context.push(PLANPILOT_GUIDANCE)
|
|
241
|
+
}
|
|
242
|
+
skipNextAuto.add(sessionID)
|
|
243
|
+
lastIdleAt.set(sessionID, Date.now())
|
|
119
244
|
},
|
|
120
245
|
event: async ({ event }) => {
|
|
121
246
|
if (event.type === "session.idle") {
|