opencode-planpilot 0.1.2 → 0.2.0
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/README.md +3 -10
- package/package.json +2 -10
- package/src/{cli.ts → command.ts} +80 -96
- package/src/index.ts +106 -58
- package/src/lib/app.ts +47 -1
- package/src/lib/util.ts +54 -0
- package/docs/planpilot.md +0 -220
- package/src/lib/argv.ts +0 -58
- package/src/lib/instructions.ts +0 -26
package/README.md
CHANGED
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
# opencode-planpilot
|
|
2
2
|
|
|
3
|
-
Planpilot
|
|
3
|
+
Planpilot for OpenCode. Provides plan/step/goal workflow with auto-continue for AI steps and a native `planpilot` tool.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
|
-
- Plan/step/goal hierarchy with
|
|
6
|
+
- Plan/step/goal hierarchy with status auto-propagation upward (goals -> steps -> plan)
|
|
7
7
|
- SQLite storage with markdown plan snapshots
|
|
8
|
-
- Native OpenCode tool
|
|
8
|
+
- Native OpenCode tool for plan/step/goal operations
|
|
9
9
|
- Auto-continue on `session.idle` when next step is assigned to `ai`
|
|
10
10
|
|
|
11
|
-
## Requirements
|
|
12
|
-
- Bun runtime (uses `bun:sqlite` at runtime)
|
|
13
|
-
|
|
14
11
|
## Install
|
|
15
12
|
|
|
16
13
|
Add to `opencode.json`:
|
|
17
14
|
|
|
18
15
|
```json
|
|
19
16
|
{
|
|
20
|
-
"$schema": "https://opencode.ai/config.json",
|
|
21
17
|
"plugin": ["opencode-planpilot"]
|
|
22
18
|
}
|
|
23
19
|
```
|
|
24
20
|
|
|
25
21
|
OpenCode installs npm plugins automatically at startup.
|
|
26
22
|
|
|
27
|
-
## Details
|
|
28
|
-
Usage and storage info: `docs/planpilot.md`
|
|
29
|
-
|
|
30
23
|
## License
|
|
31
24
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-planpilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Planpilot plugin for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -14,18 +14,10 @@
|
|
|
14
14
|
"packageManager": "bun@1.3.6",
|
|
15
15
|
"main": "./src/index.ts",
|
|
16
16
|
"types": "./src/index.ts",
|
|
17
|
-
"bin": {
|
|
18
|
-
"planpilot": "src/cli.ts",
|
|
19
|
-
"opencode-planpilot": "src/cli.ts"
|
|
20
|
-
},
|
|
21
17
|
"exports": {
|
|
22
18
|
".": {
|
|
23
19
|
"import": "./src/index.ts",
|
|
24
20
|
"types": "./src/index.ts"
|
|
25
|
-
},
|
|
26
|
-
"./cli": {
|
|
27
|
-
"import": "./src/cli.ts",
|
|
28
|
-
"types": "./src/cli.ts"
|
|
29
21
|
}
|
|
30
22
|
},
|
|
31
23
|
"files": [
|
|
@@ -49,7 +41,7 @@
|
|
|
49
41
|
},
|
|
50
42
|
"devDependencies": {
|
|
51
43
|
"@opencode-ai/plugin": "*",
|
|
52
|
-
"@types/node": "^25.0.
|
|
44
|
+
"@types/node": "^25.0.9",
|
|
53
45
|
"bun-types": "^1.3.6",
|
|
54
46
|
"eslint": "^9.39.2",
|
|
55
47
|
"@eslint/js": "^9.39.2",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
1
|
import fs from "fs"
|
|
3
2
|
import { openDatabase, resolvePlanMarkdownPath, ensureParentDir } from "./lib/db"
|
|
4
3
|
import { PlanpilotApp } from "./lib/app"
|
|
@@ -15,36 +14,29 @@ import { AppError, invalidInput } from "./lib/errors"
|
|
|
15
14
|
import { ensureNonEmpty, projectMatchesPath, resolveMaybeRealpath } from "./lib/util"
|
|
16
15
|
import { formatGoalDetail, formatPlanDetail, formatPlanMarkdown, formatStepDetail } from "./lib/format"
|
|
17
16
|
|
|
18
|
-
const CWD_FLAG = "--cwd"
|
|
19
|
-
const SESSION_ID_FLAG = "--session-id"
|
|
20
17
|
const DEFAULT_PAGE = 1
|
|
21
18
|
const DEFAULT_LIMIT = 20
|
|
22
19
|
|
|
23
|
-
export type
|
|
20
|
+
export type CommandIO = {
|
|
24
21
|
log: (...args: any[]) => void
|
|
25
|
-
error: (...args: any[]) => void
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
},
|
|
32
|
-
error: (...args) => {
|
|
33
|
-
console.error(...args)
|
|
34
|
-
},
|
|
24
|
+
export type CommandContext = {
|
|
25
|
+
sessionId: string
|
|
26
|
+
cwd: string
|
|
35
27
|
}
|
|
36
28
|
|
|
37
|
-
|
|
29
|
+
const noopIO: CommandIO = {
|
|
30
|
+
log: () => {},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let currentIO: CommandIO = noopIO
|
|
38
34
|
|
|
39
35
|
function log(...args: any[]) {
|
|
40
36
|
currentIO.log(...args)
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
function
|
|
44
|
-
currentIO.error(...args)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function withIO<T>(io: CliIO, fn: () => Promise<T> | T): Promise<T> {
|
|
39
|
+
async function withIO<T>(io: CommandIO, fn: () => Promise<T> | T): Promise<T> {
|
|
48
40
|
const prev = currentIO
|
|
49
41
|
currentIO = io
|
|
50
42
|
try {
|
|
@@ -54,7 +46,7 @@ async function withIO<T>(io: CliIO, fn: () => Promise<T> | T): Promise<T> {
|
|
|
54
46
|
}
|
|
55
47
|
}
|
|
56
48
|
|
|
57
|
-
export function
|
|
49
|
+
export function formatCommandError(err: unknown): string {
|
|
58
50
|
if (err instanceof AppError) {
|
|
59
51
|
return `Error: ${err.toDisplayString()}`
|
|
60
52
|
}
|
|
@@ -64,28 +56,24 @@ export function formatCliError(err: unknown): string {
|
|
|
64
56
|
return `Error: ${String(err)}`
|
|
65
57
|
}
|
|
66
58
|
|
|
67
|
-
export async function
|
|
59
|
+
export async function runCommand(argv: string[], context: CommandContext, io: CommandIO = noopIO) {
|
|
68
60
|
return withIO(io, async () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (!remaining.length) {
|
|
72
|
-
throw invalidInput("missing command")
|
|
61
|
+
if (!argv.length) {
|
|
62
|
+
throw invalidInput("missing argv")
|
|
73
63
|
}
|
|
74
64
|
|
|
75
|
-
const [section, subcommand, ...args] =
|
|
76
|
-
|
|
77
|
-
const resolvedSessionId = resolveSessionId(sessionId)
|
|
65
|
+
const [section, subcommand, ...args] = argv
|
|
78
66
|
|
|
79
67
|
const db = openDatabase()
|
|
80
|
-
const resolvedCwd =
|
|
81
|
-
const app = new PlanpilotApp(db,
|
|
68
|
+
const resolvedCwd = resolveMaybeRealpath(context.cwd)
|
|
69
|
+
const app = new PlanpilotApp(db, context.sessionId, resolvedCwd)
|
|
82
70
|
|
|
83
71
|
let planIds: number[] = []
|
|
84
72
|
let shouldSync = false
|
|
85
73
|
|
|
86
74
|
switch (section) {
|
|
87
75
|
case "plan": {
|
|
88
|
-
const result = await handlePlan(app, subcommand, args, { cwd
|
|
76
|
+
const result = await handlePlan(app, subcommand, args, { cwd: context.cwd })
|
|
89
77
|
planIds = result.planIds
|
|
90
78
|
shouldSync = result.shouldSync
|
|
91
79
|
break
|
|
@@ -112,57 +100,20 @@ export async function runCLI(argv: string[] = process.argv.slice(2), io: CliIO =
|
|
|
112
100
|
})
|
|
113
101
|
}
|
|
114
102
|
|
|
115
|
-
function
|
|
116
|
-
let cwd: string | undefined
|
|
117
|
-
let sessionId: string | undefined
|
|
118
|
-
let cwdFlagPresent = false
|
|
119
|
-
const remaining: string[] = []
|
|
120
|
-
|
|
121
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
122
|
-
const token = argv[i]
|
|
123
|
-
if (token === CWD_FLAG) {
|
|
124
|
-
cwdFlagPresent = true
|
|
125
|
-
cwd = argv[i + 1]
|
|
126
|
-
i += 1
|
|
127
|
-
continue
|
|
128
|
-
}
|
|
129
|
-
if (token === SESSION_ID_FLAG) {
|
|
130
|
-
sessionId = argv[i + 1]
|
|
131
|
-
i += 1
|
|
132
|
-
continue
|
|
133
|
-
}
|
|
134
|
-
remaining.push(token)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return { cwd, cwdFlagPresent, sessionId, remaining }
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function resolveSessionId(sessionId: string | undefined): string {
|
|
141
|
-
if (!sessionId) {
|
|
142
|
-
throw invalidInput(`${SESSION_ID_FLAG} is required`)
|
|
143
|
-
}
|
|
144
|
-
const trimmed = sessionId.trim()
|
|
145
|
-
if (!trimmed) {
|
|
146
|
-
throw invalidInput(`${SESSION_ID_FLAG} is empty`)
|
|
147
|
-
}
|
|
148
|
-
return trimmed
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function requireCwd(cwdFlagPresent: boolean, cwd: string | undefined): string {
|
|
152
|
-
if (!cwdFlagPresent) {
|
|
153
|
-
throw invalidInput(`${CWD_FLAG} is required`)
|
|
154
|
-
}
|
|
103
|
+
function requireCwd(cwd: string | undefined): string {
|
|
155
104
|
if (!cwd || !cwd.trim()) {
|
|
156
|
-
throw invalidInput(
|
|
105
|
+
throw invalidInput("cwd is required")
|
|
157
106
|
}
|
|
158
107
|
return cwd
|
|
159
108
|
}
|
|
160
109
|
|
|
110
|
+
export type { CommandContext as PlanpilotCommandContext, CommandIO as PlanpilotCommandIO }
|
|
111
|
+
|
|
161
112
|
async function handlePlan(
|
|
162
113
|
app: PlanpilotApp,
|
|
163
114
|
subcommand: string | undefined,
|
|
164
115
|
args: string[],
|
|
165
|
-
context: { cwd: string | undefined
|
|
116
|
+
context: { cwd: string | undefined },
|
|
166
117
|
) {
|
|
167
118
|
switch (subcommand) {
|
|
168
119
|
case "add":
|
|
@@ -212,6 +163,8 @@ async function handleStep(app: PlanpilotApp, subcommand: string | undefined, arg
|
|
|
212
163
|
return { planIds: handleStepShow(app, args), shouldSync: false }
|
|
213
164
|
case "show-next":
|
|
214
165
|
return { planIds: handleStepShowNext(app), shouldSync: false }
|
|
166
|
+
case "wait":
|
|
167
|
+
return { planIds: handleStepWait(app, args), shouldSync: true }
|
|
215
168
|
case "comment":
|
|
216
169
|
return { planIds: handleStepComment(app, args), shouldSync: true }
|
|
217
170
|
case "update":
|
|
@@ -287,7 +240,7 @@ function handlePlanAddTree(app: PlanpilotApp, args: string[]): number[] {
|
|
|
287
240
|
function handlePlanList(
|
|
288
241
|
app: PlanpilotApp,
|
|
289
242
|
args: string[],
|
|
290
|
-
context: { cwd: string | undefined
|
|
243
|
+
context: { cwd: string | undefined },
|
|
291
244
|
): number[] {
|
|
292
245
|
const { options, positionals } = parseOptions(args)
|
|
293
246
|
if (positionals.length) {
|
|
@@ -304,7 +257,7 @@ function handlePlanList(
|
|
|
304
257
|
}
|
|
305
258
|
|
|
306
259
|
const desiredStatus: PlanStatus | null = parsePlanStatusFilter(options.status)
|
|
307
|
-
const cwd = requireCwd(context.
|
|
260
|
+
const cwd = requireCwd(context.cwd)
|
|
308
261
|
|
|
309
262
|
const order = options.order ? parsePlanOrder(options.order) : "updated"
|
|
310
263
|
const desc = options.desc ?? true
|
|
@@ -346,7 +299,7 @@ function handlePlanList(
|
|
|
346
299
|
function handlePlanCount(
|
|
347
300
|
app: PlanpilotApp,
|
|
348
301
|
args: string[],
|
|
349
|
-
context: { cwd: string | undefined
|
|
302
|
+
context: { cwd: string | undefined },
|
|
350
303
|
): number[] {
|
|
351
304
|
const { options, positionals } = parseOptions(args)
|
|
352
305
|
if (positionals.length) {
|
|
@@ -363,7 +316,7 @@ function handlePlanCount(
|
|
|
363
316
|
}
|
|
364
317
|
|
|
365
318
|
const desiredStatus: PlanStatus | null = parsePlanStatusFilter(options.status)
|
|
366
|
-
const cwd = requireCwd(context.
|
|
319
|
+
const cwd = requireCwd(context.cwd)
|
|
367
320
|
|
|
368
321
|
let plans = app.listPlans()
|
|
369
322
|
if (!plans.length) {
|
|
@@ -383,7 +336,7 @@ function handlePlanCount(
|
|
|
383
336
|
function handlePlanSearch(
|
|
384
337
|
app: PlanpilotApp,
|
|
385
338
|
args: string[],
|
|
386
|
-
context: { cwd: string | undefined
|
|
339
|
+
context: { cwd: string | undefined },
|
|
387
340
|
): number[] {
|
|
388
341
|
const { options, positionals } = parseOptions(args)
|
|
389
342
|
if (positionals.length) {
|
|
@@ -393,7 +346,7 @@ function handlePlanSearch(
|
|
|
393
346
|
throw invalidInput("plan search requires at least one --search")
|
|
394
347
|
}
|
|
395
348
|
const desiredStatus: PlanStatus | null = parsePlanStatusFilter(options.status)
|
|
396
|
-
const cwd = requireCwd(context.
|
|
349
|
+
const cwd = requireCwd(context.cwd)
|
|
397
350
|
|
|
398
351
|
const order = options.order ? parsePlanOrder(options.order) : "updated"
|
|
399
352
|
const desc = options.desc ?? true
|
|
@@ -700,6 +653,30 @@ function handleStepShowNext(app: PlanpilotApp): number[] {
|
|
|
700
653
|
return []
|
|
701
654
|
}
|
|
702
655
|
|
|
656
|
+
function handleStepWait(app: PlanpilotApp, args: string[]): number[] {
|
|
657
|
+
if (!args.length) {
|
|
658
|
+
throw invalidInput("step wait requires <id>")
|
|
659
|
+
}
|
|
660
|
+
const stepId = parseNumber(args[0], "step id")
|
|
661
|
+
const options = parseOptions(args.slice(1)).options
|
|
662
|
+
if (options.clear) {
|
|
663
|
+
const result = app.clearStepWait(stepId)
|
|
664
|
+
log(`Step ID: ${result.step.id} wait cleared.`)
|
|
665
|
+
return [result.step.plan_id]
|
|
666
|
+
}
|
|
667
|
+
if (options.delay === undefined) {
|
|
668
|
+
throw invalidInput("step wait requires --delay <ms> or --clear")
|
|
669
|
+
}
|
|
670
|
+
const delayMs = parseNumber(options.delay, "delay")
|
|
671
|
+
if (delayMs < 0) {
|
|
672
|
+
throw invalidInput("delay must be >= 0")
|
|
673
|
+
}
|
|
674
|
+
const reason = options.reason ? String(options.reason) : undefined
|
|
675
|
+
const result = app.setStepWait(stepId, delayMs, reason)
|
|
676
|
+
log(`Step ID: ${result.step.id} waiting until ${result.until}.`)
|
|
677
|
+
return [result.step.plan_id]
|
|
678
|
+
}
|
|
679
|
+
|
|
703
680
|
function handleStepUpdate(app: PlanpilotApp, args: string[]): number[] {
|
|
704
681
|
if (!args.length) {
|
|
705
682
|
throw invalidInput("step update requires <id>")
|
|
@@ -1034,18 +1011,31 @@ function parseOptions(args: string[]) {
|
|
|
1034
1011
|
options.order = expectValue(args, i, token)
|
|
1035
1012
|
i += 2
|
|
1036
1013
|
break
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1014
|
+
case "--to":
|
|
1015
|
+
options.to = expectValue(args, i, token)
|
|
1016
|
+
i += 2
|
|
1017
|
+
break
|
|
1018
|
+
case "--delay":
|
|
1019
|
+
options.delay = expectValue(args, i, token)
|
|
1020
|
+
i += 2
|
|
1021
|
+
break
|
|
1022
|
+
case "--reason":
|
|
1023
|
+
options.reason = expectValue(args, i, token)
|
|
1024
|
+
i += 2
|
|
1025
|
+
break
|
|
1026
|
+
case "--clear":
|
|
1027
|
+
options.clear = true
|
|
1028
|
+
i += 1
|
|
1029
|
+
break
|
|
1030
|
+
case "--goal":
|
|
1031
|
+
if (!options.goals) options.goals = []
|
|
1032
|
+
options.goals.push(expectValue(args, i, token))
|
|
1033
|
+
i += 2
|
|
1034
|
+
break
|
|
1035
|
+
default:
|
|
1036
|
+
throw invalidInput(`unexpected argument: ${token}`)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1049
1039
|
}
|
|
1050
1040
|
return { options, positionals }
|
|
1051
1041
|
}
|
|
@@ -1475,9 +1465,3 @@ function syncPlanMarkdown(app: PlanpilotApp, planIds: number[]) {
|
|
|
1475
1465
|
})
|
|
1476
1466
|
}
|
|
1477
1467
|
|
|
1478
|
-
if (import.meta.main) {
|
|
1479
|
-
runCLI(process.argv.slice(2), defaultIO).catch((err) => {
|
|
1480
|
-
error(formatCliError(err))
|
|
1481
|
-
process.exit(1)
|
|
1482
|
-
})
|
|
1483
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,23 +1,74 @@
|
|
|
1
1
|
import { tool, type Plugin } from "@opencode-ai/plugin"
|
|
2
|
-
import {
|
|
2
|
+
import { runCommand, formatCommandError } from "./command"
|
|
3
3
|
import { PlanpilotApp } from "./lib/app"
|
|
4
|
-
import { parseCommandArgs } from "./lib/argv"
|
|
5
4
|
import { openDatabase } from "./lib/db"
|
|
6
5
|
import { invalidInput } from "./lib/errors"
|
|
7
6
|
import { formatStepDetail } from "./lib/format"
|
|
8
|
-
import {
|
|
7
|
+
import { parseWaitFromComment } from "./lib/util"
|
|
8
|
+
|
|
9
|
+
const PLANPILOT_TOOL_DESCRIPTION = [
|
|
10
|
+
"Planpilot planner for plan workflows.",
|
|
11
|
+
"Hints: 1. Model is plan/step/goal with ai/human executors and status auto-propagation upward (goals -> steps -> plan). 2. Keep comments short and decision-focused. 3. Add human steps only when AI cannot act. 4. Use `step wait` when ending a reply while waiting on external tasks.",
|
|
12
|
+
"",
|
|
13
|
+
"Usage:",
|
|
14
|
+
"- argv is tokenized: [section, subcommand, ...args]",
|
|
15
|
+
"- section: plan | step | goal",
|
|
16
|
+
"",
|
|
17
|
+
"Plan commands:",
|
|
18
|
+
"- plan add <title> <content>",
|
|
19
|
+
"- plan add-tree <title> <content> --step <content> [--executor ai|human] [--goal <content>]... [--step ...]...",
|
|
20
|
+
"- plan list [--scope project|all] [--status todo|done|all] [--limit N] [--page N] [--order id|title|created|updated] [--desc]",
|
|
21
|
+
"- plan count [--scope project|all] [--status todo|done|all]",
|
|
22
|
+
"- plan search --search <term> [--search <term> ...] [--search-mode any|all] [--search-field plan|title|content|comment|steps|goals|all] [--match-case] [--scope project|all] [--status todo|done|all] [--limit N] [--page N] [--order id|title|created|updated] [--desc]",
|
|
23
|
+
"- plan show <id>",
|
|
24
|
+
"- plan export <id> <path>",
|
|
25
|
+
"- plan comment <id> <comment> [<id> <comment> ...]",
|
|
26
|
+
"- plan update <id> [--title <title>] [--content <content>] [--status todo|done] [--comment <comment>]",
|
|
27
|
+
"- plan done <id>",
|
|
28
|
+
"- plan remove <id>",
|
|
29
|
+
"- plan activate <id> [--force]",
|
|
30
|
+
"- plan show-active",
|
|
31
|
+
"- plan deactivate",
|
|
32
|
+
"",
|
|
33
|
+
"Step commands:",
|
|
34
|
+
"- step add <plan_id> <content...> [--executor ai|human] [--at <pos>]",
|
|
35
|
+
"- step add-tree <plan_id> <content> [--executor ai|human] [--goal <content> ...]",
|
|
36
|
+
"- step list <plan_id> [--status todo|done|all] [--executor ai|human] [--limit N] [--page N]",
|
|
37
|
+
"- step count <plan_id> [--status todo|done|all] [--executor ai|human]",
|
|
38
|
+
"- step show <id>",
|
|
39
|
+
"- step show-next",
|
|
40
|
+
"- step wait <id> --delay <ms> [--reason <text>]",
|
|
41
|
+
"- step wait <id> --clear",
|
|
42
|
+
"- step comment <id> <comment> [<id> <comment> ...]",
|
|
43
|
+
"- step update <id> [--content <content>] [--status todo|done] [--executor ai|human] [--comment <comment>]",
|
|
44
|
+
"- step done <id> [--all-goals]",
|
|
45
|
+
"- step move <id> --to <pos>",
|
|
46
|
+
"- step remove <id...>",
|
|
47
|
+
"",
|
|
48
|
+
"Goal commands:",
|
|
49
|
+
"- goal add <step_id> <content...>",
|
|
50
|
+
"- goal list <step_id> [--status todo|done|all] [--limit N] [--page N]",
|
|
51
|
+
"- goal count <step_id> [--status todo|done|all]",
|
|
52
|
+
"- goal show <id>",
|
|
53
|
+
"- goal comment <id> <comment> [<id> <comment> ...]",
|
|
54
|
+
"- goal update <id> [--content <content>] [--status todo|done] [--comment <comment>]",
|
|
55
|
+
"- goal done <id...>",
|
|
56
|
+
"- goal remove <id...>",
|
|
57
|
+
].join("\n")
|
|
9
58
|
|
|
10
59
|
export const PlanpilotPlugin: Plugin = async (ctx) => {
|
|
11
60
|
const inFlight = new Set<string>()
|
|
12
61
|
const skipNextAuto = new Set<string>()
|
|
13
62
|
const lastIdleAt = new Map<string, number>()
|
|
63
|
+
const waitTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
14
64
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
65
|
+
const clearWaitTimer = (sessionID: string) => {
|
|
66
|
+
const existing = waitTimers.get(sessionID)
|
|
67
|
+
if (existing) {
|
|
68
|
+
clearTimeout(existing)
|
|
69
|
+
waitTimers.delete(sessionID)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
21
72
|
|
|
22
73
|
const log = async (level: "debug" | "info" | "warn" | "error", message: string, extra?: Record<string, any>) => {
|
|
23
74
|
try {
|
|
@@ -132,6 +183,32 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
|
|
|
132
183
|
const next = app.nextStep(active.plan_id)
|
|
133
184
|
if (!next) return
|
|
134
185
|
if (next.executor !== "ai") return
|
|
186
|
+
const wait = parseWaitFromComment(next.comment)
|
|
187
|
+
if (wait && wait.until > now) {
|
|
188
|
+
clearWaitTimer(sessionID)
|
|
189
|
+
await log("info", "auto-continue delayed by step wait", {
|
|
190
|
+
sessionID,
|
|
191
|
+
stepId: next.id,
|
|
192
|
+
until: wait.until,
|
|
193
|
+
reason: wait.reason,
|
|
194
|
+
})
|
|
195
|
+
const msUntil = Math.max(0, wait.until - now)
|
|
196
|
+
const timer = setTimeout(() => {
|
|
197
|
+
waitTimers.delete(sessionID)
|
|
198
|
+
handleSessionIdle(sessionID).catch((err) => {
|
|
199
|
+
void log("warn", "auto-continue retry failed", {
|
|
200
|
+
sessionID,
|
|
201
|
+
error: err instanceof Error ? err.message : String(err),
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
}, msUntil)
|
|
205
|
+
waitTimers.set(sessionID, timer)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
if (!wait) {
|
|
209
|
+
clearWaitTimer(sessionID)
|
|
210
|
+
}
|
|
211
|
+
|
|
135
212
|
const goals = app.goalsForStep(next.id)
|
|
136
213
|
const detail = formatStepDetail(next, goals)
|
|
137
214
|
if (!detail.trim()) return
|
|
@@ -143,12 +220,13 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
|
|
|
143
220
|
}
|
|
144
221
|
if (autoContext?.aborted || autoContext?.ready === false) return
|
|
145
222
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
223
|
+
const timestamp = new Date().toISOString()
|
|
224
|
+
const message = `Planpilot plugin auto message @ ${timestamp}
|
|
225
|
+
Hints:
|
|
226
|
+
- If the next step needs human action, insert a human step before it.
|
|
227
|
+
- If you need to wait for something to finish, use the step wait subcommand.
|
|
228
|
+
Next step details:
|
|
229
|
+
${detail.trimEnd()}`
|
|
152
230
|
|
|
153
231
|
const promptBody: any = {
|
|
154
232
|
agent: autoContext?.agent ?? undefined,
|
|
@@ -173,72 +251,41 @@ export const PlanpilotPlugin: Plugin = async (ctx) => {
|
|
|
173
251
|
return {
|
|
174
252
|
tool: {
|
|
175
253
|
planpilot: tool({
|
|
176
|
-
description:
|
|
177
|
-
"Planpilot planner. Use for all plan/step/goal operations. Provide either argv (array) or command (string). " +
|
|
178
|
-
"Do not include --session-id/--cwd; they are injected automatically from the current session.",
|
|
254
|
+
description: PLANPILOT_TOOL_DESCRIPTION,
|
|
179
255
|
args: {
|
|
180
|
-
argv: tool.schema.array(tool.schema.string()).
|
|
181
|
-
command: tool.schema.string().min(1),
|
|
256
|
+
argv: tool.schema.array(tool.schema.string()).min(1),
|
|
182
257
|
},
|
|
183
258
|
async execute(args, toolCtx) {
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
} else if (typeof args.command === "string" && args.command.trim()) {
|
|
188
|
-
argv = parseCommandArgs(args.command)
|
|
189
|
-
} else {
|
|
190
|
-
return formatCliError(invalidInput("missing command"))
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const cwd = (ctx.directory ?? "").trim()
|
|
194
|
-
if (!cwd) {
|
|
195
|
-
return formatCliError(invalidInput(`${"--cwd"} is required`))
|
|
259
|
+
const argv = Array.isArray(args.argv) ? args.argv : []
|
|
260
|
+
if (!argv.length) {
|
|
261
|
+
return formatCommandError(invalidInput("missing argv"))
|
|
196
262
|
}
|
|
197
263
|
|
|
198
264
|
if (containsForbiddenFlags(argv)) {
|
|
199
|
-
return
|
|
265
|
+
return formatCommandError(invalidInput("argv cannot include --cwd or --session-id"))
|
|
200
266
|
}
|
|
201
267
|
|
|
202
|
-
const
|
|
203
|
-
if (!
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
if (!finalArgv.includes("--session-id")) {
|
|
207
|
-
finalArgv.unshift("--session-id", toolCtx.sessionID)
|
|
268
|
+
const cwd = (ctx.directory ?? "").trim()
|
|
269
|
+
if (!cwd) {
|
|
270
|
+
return formatCommandError(invalidInput("cwd is required"))
|
|
208
271
|
}
|
|
209
272
|
|
|
210
273
|
const output: string[] = []
|
|
211
274
|
const io = {
|
|
212
275
|
log: (...parts: any[]) => output.push(parts.map(String).join(" ")),
|
|
213
|
-
error: (...parts: any[]) => output.push(parts.map(String).join(" ")),
|
|
214
276
|
}
|
|
215
277
|
|
|
216
278
|
try {
|
|
217
|
-
await
|
|
279
|
+
await runCommand(argv, { sessionId: toolCtx.sessionID, cwd }, io)
|
|
218
280
|
} catch (err) {
|
|
219
|
-
return
|
|
281
|
+
return formatCommandError(err)
|
|
220
282
|
}
|
|
221
283
|
|
|
222
284
|
return output.join("\n").trimEnd()
|
|
223
285
|
},
|
|
224
286
|
}),
|
|
225
287
|
},
|
|
226
|
-
"experimental.
|
|
227
|
-
const instructions = loadPlanpilotInstructions().trim()
|
|
228
|
-
const alreadyInjected = output.system.some((entry) => entry.includes("Planpilot (OpenCode Tool)"))
|
|
229
|
-
if (instructions && !alreadyInjected) {
|
|
230
|
-
output.system.push(instructions)
|
|
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
|
-
}
|
|
288
|
+
"experimental.session.compacting": async ({ sessionID }) => {
|
|
242
289
|
skipNextAuto.add(sessionID)
|
|
243
290
|
lastIdleAt.set(sessionID, Date.now())
|
|
244
291
|
},
|
|
@@ -263,3 +310,4 @@ function containsForbiddenFlags(argv: string[]): boolean {
|
|
|
263
310
|
return false
|
|
264
311
|
})
|
|
265
312
|
}
|
|
313
|
+
|
package/src/lib/app.ts
CHANGED
|
@@ -22,7 +22,15 @@ import {
|
|
|
22
22
|
type StepRow,
|
|
23
23
|
type StepStatus,
|
|
24
24
|
} from "./models"
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
ensureNonEmpty,
|
|
27
|
+
joinIds,
|
|
28
|
+
normalizeCommentEntries,
|
|
29
|
+
parseWaitFromComment,
|
|
30
|
+
removeWaitFromComment,
|
|
31
|
+
uniqueIds,
|
|
32
|
+
upsertWaitInComment,
|
|
33
|
+
} from "./util"
|
|
26
34
|
import { invalidInput, notFound } from "./errors"
|
|
27
35
|
import { formatStepDetail } from "./format"
|
|
28
36
|
|
|
@@ -820,6 +828,44 @@ export class PlanpilotApp {
|
|
|
820
828
|
return tx()
|
|
821
829
|
}
|
|
822
830
|
|
|
831
|
+
setStepWait(stepId: number, delayMs: number, reason?: string): { step: StepRow; until: number } {
|
|
832
|
+
if (!Number.isFinite(delayMs) || delayMs < 0) {
|
|
833
|
+
throw invalidInput("delay must be a non-negative number")
|
|
834
|
+
}
|
|
835
|
+
const tx = this.db.transaction(() => {
|
|
836
|
+
const step = this.getStep(stepId)
|
|
837
|
+
const now = Date.now()
|
|
838
|
+
const until = now + Math.trunc(delayMs)
|
|
839
|
+
const comment = upsertWaitInComment(step.comment, until, reason)
|
|
840
|
+
this.db.prepare("UPDATE steps SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, stepId)
|
|
841
|
+
const updated = this.getStep(stepId)
|
|
842
|
+
this.touchPlan(updated.plan_id)
|
|
843
|
+
return { step: updated, until }
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
return tx()
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
clearStepWait(stepId: number): { step: StepRow } {
|
|
850
|
+
const tx = this.db.transaction(() => {
|
|
851
|
+
const step = this.getStep(stepId)
|
|
852
|
+
const comment = step.comment ? removeWaitFromComment(step.comment) : null
|
|
853
|
+
const now = Date.now()
|
|
854
|
+
this.db.prepare("UPDATE steps SET comment = ?, updated_at = ? WHERE id = ?").run(comment, now, stepId)
|
|
855
|
+
const updated = this.getStep(stepId)
|
|
856
|
+
this.touchPlan(updated.plan_id)
|
|
857
|
+
return { step: updated }
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
return tx()
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
getStepWait(stepId: number): { step: StepRow; wait: { until: number; reason?: string } | null } {
|
|
864
|
+
const step = this.getStep(stepId)
|
|
865
|
+
const wait = parseWaitFromComment(step.comment)
|
|
866
|
+
return { step, wait }
|
|
867
|
+
}
|
|
868
|
+
|
|
823
869
|
commentGoals(entries: Array<[number, string]>): number[] {
|
|
824
870
|
const normalized = normalizeCommentEntries(entries)
|
|
825
871
|
if (!normalized.length) return []
|
package/src/lib/util.ts
CHANGED
|
@@ -49,6 +49,60 @@ export function normalizeCommentEntries(entries: Array<[number, string]>): Array
|
|
|
49
49
|
return ordered
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
const WAIT_UNTIL_PREFIX = "@wait-until="
|
|
53
|
+
const WAIT_REASON_PREFIX = "@wait-reason="
|
|
54
|
+
|
|
55
|
+
export type WaitComment = {
|
|
56
|
+
until: number
|
|
57
|
+
reason?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseWaitFromComment(comment?: string | null): WaitComment | null {
|
|
61
|
+
if (!comment) return null
|
|
62
|
+
let until: number | null = null
|
|
63
|
+
let reason: string | undefined
|
|
64
|
+
for (const line of comment.split(/\r?\n/)) {
|
|
65
|
+
const trimmed = line.trim()
|
|
66
|
+
if (trimmed.startsWith(WAIT_UNTIL_PREFIX)) {
|
|
67
|
+
const raw = trimmed.slice(WAIT_UNTIL_PREFIX.length).trim()
|
|
68
|
+
const value = Number(raw)
|
|
69
|
+
if (Number.isFinite(value)) {
|
|
70
|
+
until = value
|
|
71
|
+
}
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (trimmed.startsWith(WAIT_REASON_PREFIX)) {
|
|
75
|
+
const raw = trimmed.slice(WAIT_REASON_PREFIX.length).trim()
|
|
76
|
+
if (raw) reason = raw
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (until === null) return null
|
|
80
|
+
return { until, reason }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isWaitLine(line: string): boolean {
|
|
84
|
+
const trimmed = line.trim()
|
|
85
|
+
return trimmed.startsWith(WAIT_UNTIL_PREFIX) || trimmed.startsWith(WAIT_REASON_PREFIX)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function upsertWaitInComment(comment: string | null, until: number, reason?: string): string {
|
|
89
|
+
const lines = comment ? comment.split(/\r?\n/) : []
|
|
90
|
+
const filtered = lines.filter((line) => !isWaitLine(line))
|
|
91
|
+
const waitLines = [`${WAIT_UNTIL_PREFIX}${Math.trunc(until)}`]
|
|
92
|
+
const reasonValue = reason?.trim()
|
|
93
|
+
if (reasonValue) {
|
|
94
|
+
waitLines.push(`${WAIT_REASON_PREFIX}${reasonValue}`)
|
|
95
|
+
}
|
|
96
|
+
return [...waitLines, ...filtered].join("\n").trimEnd()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function removeWaitFromComment(comment?: string | null): string | null {
|
|
100
|
+
if (!comment) return null
|
|
101
|
+
const lines = comment.split(/\r?\n/).filter((line) => !isWaitLine(line))
|
|
102
|
+
const cleaned = lines.join("\n").trimEnd()
|
|
103
|
+
return cleaned.length ? cleaned : null
|
|
104
|
+
}
|
|
105
|
+
|
|
52
106
|
export function resolveMaybeRealpath(value: string): string {
|
|
53
107
|
try {
|
|
54
108
|
return fs.realpathSync.native(value)
|
package/docs/planpilot.md
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
# Planpilot (OpenCode Tool)
|
|
2
|
-
|
|
3
|
-
## Tool Name
|
|
4
|
-
`planpilot`
|
|
5
|
-
|
|
6
|
-
## What it is
|
|
7
|
-
Planpilot is a planner/tracker for multi-step work. It manages plans, steps, and goals, and automatically rolls up status.
|
|
8
|
-
|
|
9
|
-
## OpenCode Tool Usage (Required)
|
|
10
|
-
Use the custom `planpilot` tool. Do **not** call the CLI via `bash` for planning.
|
|
11
|
-
|
|
12
|
-
### Tool schema
|
|
13
|
-
- `argv?: string[]` - preferred; avoids quoting issues
|
|
14
|
-
- `command?: string` - optional; one command string
|
|
15
|
-
|
|
16
|
-
Rules:
|
|
17
|
-
- Provide either `argv` or `command`.
|
|
18
|
-
- Do **not** pass `--session-id` or `--cwd`; the tool injects them automatically from the current session.
|
|
19
|
-
- If both `argv` and `command` are provided, `argv` is used.
|
|
20
|
-
|
|
21
|
-
Example tool args:
|
|
22
|
-
|
|
23
|
-
```json
|
|
24
|
-
{ "argv": ["plan", "add", "Release v1.2", "Plan description"] }
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
```json
|
|
28
|
-
{ "command": "plan add \"Release v1.2\" \"Plan description\"" }
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## CLI (Optional)
|
|
32
|
-
A CLI binary named `planpilot` is available for manual use. If you use the CLI directly, you must pass `--session-id` and `--cwd` yourself.
|
|
33
|
-
|
|
34
|
-
## Storage
|
|
35
|
-
- Database: `~/.config/opencode/.planpilot/planpilot.db`
|
|
36
|
-
- Plan snapshots: `~/.config/opencode/.planpilot/plans/`
|
|
37
|
-
- Override base directory: `OPENCODE_PLANPILOT_DIR` or `OPENCODE_PLANPILOT_HOME`
|
|
38
|
-
|
|
39
|
-
## Hierarchy
|
|
40
|
-
- Plan contains steps; step contains goals.
|
|
41
|
-
- Goals are the smallest units of work; steps group goals; plans group steps.
|
|
42
|
-
|
|
43
|
-
## AI Workflow Guidelines
|
|
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.
|
|
46
|
-
- Treat tool output as authoritative. Do not invent IDs; only use IDs shown by `list`/`show`.
|
|
47
|
-
- If the tool is missing or unavailable, ask the user to enable/install the plugin.
|
|
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.
|
|
49
|
-
- Before starting a new plan, do deep analysis; then create the plan with clear steps and goals.
|
|
50
|
-
- When creating a plan or step, prefer `add-tree` to define all steps/goals upfront.
|
|
51
|
-
- Prefer assigning steps to `ai`; only assign `human` for truly critical/high-risk items or when passwords, `sudo` access, irreversible git history rewrites, or remote git changes are required. If `human` steps are necessary, batch them, make the step content explicit about what the human must do, and only ask for user input when the next step is assigned to `human`.
|
|
52
|
-
- Adjust plans/steps/goals only when necessary; avoid frequent arbitrary changes.
|
|
53
|
-
- Update status promptly as work completes; mark goals done, and let steps/plans auto-refresh unless a step/plan has no children (then use `step done`/`plan done`).
|
|
54
|
-
- In each reply turn, complete at most one step; do not advance multiple steps in a single response.
|
|
55
|
-
|
|
56
|
-
## Status Management
|
|
57
|
-
- Status values: `todo`, `done`.
|
|
58
|
-
- Goals are manual (`goal done`); steps/plans auto-refresh from child status, and use `step done`/`plan done` only when they have no children (`step done --all-goals` marks all goals done and then marks the step done). Auto status changes print as `Auto status updates:` with reasons.
|
|
59
|
-
- Parent status auto-flips to `todo` on incomplete child work and to `done` when all children are done. If a plan has 0 steps or a step has 0 goals, no auto-flip happens; use `plan done` / `step done` as needed.
|
|
60
|
-
- If the user completed a `human` step, verify/mark each goal and clearly list what remains.
|
|
61
|
-
- When a step becomes `done` and there is another pending step, the CLI will print the next-step instruction: for `ai`, end the turn so Planpilot can surface it; for `human`, show the step detail and tell the user to complete the goals, then end the turn. When a plan becomes `done` (automatic or manual), the CLI will prompt you to summarize completed results and end the turn.
|
|
62
|
-
|
|
63
|
-
## Active Plan Management
|
|
64
|
-
- Use `plan activate` / `plan deactivate` to manage, and no active plan means the plan is paused until reactivated. Plans auto-deactivate on `done` (manual or automatic) or removal.
|
|
65
|
-
- Each plan can be active in only one session at a time; use `plan activate --force` to take over. Default to no `--force`, and if activation fails due to another session, ask the user whether to take over.
|
|
66
|
-
- Use `plan show-active` to know which plan is active and get its details.
|
|
67
|
-
|
|
68
|
-
## Auto-Continue Behavior (OpenCode Plugin)
|
|
69
|
-
- The plugin listens to `session.idle` / `session.status` (idle) events.
|
|
70
|
-
- If there is an active plan and the next pending step is assigned to `ai`, it appends a `Planpilot (auto):` message to the prompt and submits it so the model continues the plan.
|
|
71
|
-
- It does nothing when there is no active plan or when the next step is assigned to `human`.
|
|
72
|
-
|
|
73
|
-
## ID Notes
|
|
74
|
-
- Plan/step/goal IDs are database IDs and may be non-contiguous or not start at 1; always use the actual IDs shown by `list`/`show`.
|
|
75
|
-
|
|
76
|
-
## Commands
|
|
77
|
-
|
|
78
|
-
- IMPORTANT: Do NOT pass `--cwd` or `--session-id` when using the tool. These are injected automatically.
|
|
79
|
-
|
|
80
|
-
### plan
|
|
81
|
-
- Plan data is stored under OpenCode config: `~/.config/opencode/.planpilot/` (overridable via `OPENCODE_PLANPILOT_DIR` or `OPENCODE_PLANPILOT_HOME`).
|
|
82
|
-
- `plan add <title> <content>`: create a plan.
|
|
83
|
-
- Output: `Created plan ID: <id>: <title>`.
|
|
84
|
-
- `plan add-tree <title> <content> --step <content> [--executor ai|human] [--goal <goal> ...] [--step <content> ...]`: create a plan with steps/goals in one command.
|
|
85
|
-
- Output: `Created plan ID: <id>: <title> (steps: <n>, goals: <n>)`.
|
|
86
|
-
- Behavior: the newly created plan is automatically activated for the current session.
|
|
87
|
-
- Repeatable groups: you can repeat the `--step ... [--executor ...] [--goal ...]` group multiple times.
|
|
88
|
-
- Each `--executor` / `--goal` applies to the most recent `--step`.
|
|
89
|
-
- Example (tool args):
|
|
90
|
-
```json
|
|
91
|
-
{"argv":["plan","add-tree","Release v1.2","Plan description","--step","Cut release branch","--executor","human","--goal","Create branch","--goal","Tag base","--step","Build artifacts","--executor","ai","--goal","Build packages"]}
|
|
92
|
-
```
|
|
93
|
-
- Another example (3 steps, some without goals/executor):
|
|
94
|
-
```json
|
|
95
|
-
{"argv":["plan","add-tree","Onboarding","Setup plan","--step","Create accounts","--goal","GitHub","--goal","Slack","--step","Install tooling","--executor","ai","--step","Read handbook"]}
|
|
96
|
-
```
|
|
97
|
-
- `plan list [--scope project|all] [--status todo|done|all] [--limit N] [--page N] [--order id|title|created|updated] [--desc]`: list plans. Default: `--scope project`, `--status all`, `--limit 20`, `--page 1`, order by `updated` desc (most recent first).
|
|
98
|
-
- Output: prints a header line, then one line per plan with `ID STAT STEPS TITLE COMMENT` (`STEPS` is `done/total`); use `plan show` for full details.
|
|
99
|
-
- Output (empty): `No plans found.`
|
|
100
|
-
- Order: defaults to `updated` with most recent first. Use `--order` and `--desc` to override.
|
|
101
|
-
- Pagination: If `--page` is provided without `--limit`, the default limit is used.
|
|
102
|
-
- If `--page` exceeds the total pages, a warning is printed and no rows are returned.
|
|
103
|
-
- Footer: prints `Page N / Limit N` after the list.
|
|
104
|
-
- `plan count [--scope project|all] [--status todo|done|all]`: count plans matching the filters. Output: `Total: <n>`.
|
|
105
|
-
- `plan search --search <term> [--search <term> ...] [--search-mode any|all] [--search-field plan|title|content|comment|steps|goals|all] [--match-case] [--scope project|all] [--status todo|done|all] [--limit N] [--page N] [--order id|title|created|updated] [--desc]`: search plans. Default: `--scope project`, `--status all`, `--limit 20`, `--page 1`, order by `updated` desc.
|
|
106
|
-
- Output: same format as `plan list`.
|
|
107
|
-
- Output (empty): `No plans found.`
|
|
108
|
-
- Advanced search:
|
|
109
|
-
- `--search <term>` (repeatable): filter plans by text (required).
|
|
110
|
-
- `--search-mode any|all`: match any term or require all terms (default: `all`).
|
|
111
|
-
- `--search-field plan|title|content|comment|steps|goals|all` (default: `plan`).
|
|
112
|
-
- `--match-case`: make search case-sensitive.
|
|
113
|
-
- Pagination: If `--page` is provided without `--limit`, the default limit is used.
|
|
114
|
-
- If `--page` exceeds the total pages, a warning is printed and no rows are returned.
|
|
115
|
-
- Footer: prints `Page N / Limit N` after the list.
|
|
116
|
-
- `plan show <id>`: prints plan details and nested steps/goals (includes ids for plan/step/goal).
|
|
117
|
-
- Output: plan header includes `Plan ID: <id>`, `Title`, `Status`, `Content`, `Created`, `Updated`, and `Comment` when present.
|
|
118
|
-
- Output: each step line includes step id and executor; progress (`goals done/total`) is shown only when the step has goals. Each goal line includes goal id.
|
|
119
|
-
- `plan export <id> <path>`: export plan details to a markdown file.
|
|
120
|
-
- Output: `Exported plan ID: <id> to <path>`.
|
|
121
|
-
- `plan update <id> [--title <title>] [--content <content>] [--status todo|done] [--comment <comment>]`: update fields; `--status done` is allowed only when all steps are done or the plan has no steps.
|
|
122
|
-
- Output: `Updated plan ID: <id>: <title>`.
|
|
123
|
-
- Errors: multi-line `Error: Invalid input:` with `cannot mark plan done; next pending step:` on the next line, followed by the same step detail output as `step show`.
|
|
124
|
-
- `plan done <id>`: mark plan done (same rule as `plan update --status done`).
|
|
125
|
-
- Output: `Plan ID: <id> marked done.`
|
|
126
|
-
- Output (active plan): `Active plan deactivated because plan is done.`
|
|
127
|
-
- Errors: multi-line `Error: Invalid input:` with `cannot mark plan done; next pending step:` on the next line, followed by the same step detail output as `step show`.
|
|
128
|
-
- `plan comment <id1> <comment1> [<id2> <comment2> ...]`: add or replace comments for one or more plans.
|
|
129
|
-
- Output (single): `Updated plan comment for plan ID: <id>.`
|
|
130
|
-
- Output (batch): `Updated plan comments for <n> plans.`
|
|
131
|
-
- Each plan comment uses an `<id> <comment>` pair; you can provide multiple pairs in one call.
|
|
132
|
-
- Example:
|
|
133
|
-
```json
|
|
134
|
-
{"argv":["plan","comment","12","high priority","15","waiting on input"]}
|
|
135
|
-
```
|
|
136
|
-
- `plan remove <id>`: remove plan (and its steps/goals).
|
|
137
|
-
- Output: `Plan ID: <id> removed.`
|
|
138
|
-
- `plan activate <id> [--force]`: set the active plan.
|
|
139
|
-
- Output: `Active plan set to <id>: <title>`.
|
|
140
|
-
- `--force` takes over a plan already active in another session.
|
|
141
|
-
- Errors: `Error: Invalid input: cannot activate plan; plan is done`.
|
|
142
|
-
- Errors: `Error: Invalid input: plan id <id> is already active in session <session_id> (use --force to take over)`.
|
|
143
|
-
- `plan show-active`: prints the active plan details (same format as `plan show`).
|
|
144
|
-
- Output: the same plan detail format as `plan show`.
|
|
145
|
-
- Output (empty): `No active plan.`
|
|
146
|
-
- Output (missing): `Active plan ID: <id> not found.`
|
|
147
|
-
- `plan deactivate`: unset the active plan (does not delete any plan).
|
|
148
|
-
- Output: `Active plan deactivated.`
|
|
149
|
-
|
|
150
|
-
### step
|
|
151
|
-
- `step add <plan_id> <content1> [<content2> ...] [--at <pos>] [--executor ai|human]`: add steps.
|
|
152
|
-
- Output (single): `Created step ID: <id> for plan ID: <plan_id>`.
|
|
153
|
-
- Output (batch): `Created <n> steps for plan ID: <plan_id>`.
|
|
154
|
-
- `step add-tree <plan_id> <content> [--executor ai|human] [--goal <goal> ...]`: create one step with goals in one command.
|
|
155
|
-
- Output: `Created step ID: <id> for plan ID: <plan_id> (goals: <n>)`.
|
|
156
|
-
- Example:
|
|
157
|
-
```json
|
|
158
|
-
{"argv":["step","add-tree","1","Draft summary","--executor","ai","--goal","Collect inputs","--goal","Write draft"]}
|
|
159
|
-
```
|
|
160
|
-
- `step list <plan_id> [--status todo|done|all] [--executor ai|human] [--limit N] [--page N]`: list steps. Default: `--status all`, `--limit 20`, `--page 1`. Steps are always returned in their plan order.
|
|
161
|
-
- Output: prints a header line, then one line per step with `ID STAT ORD EXEC GOALS CONTENT COMMENT` (`ORD` is the step order within the plan; `GOALS` is `done/total`); use `step show` for full details.
|
|
162
|
-
- Output (empty): `No steps found for plan ID: <plan_id>.`
|
|
163
|
-
- Pagination: If `--page` is provided without `--limit`, the default limit is used.
|
|
164
|
-
- If `--page` exceeds the total pages, a warning is printed and no rows are returned.
|
|
165
|
-
- Footer: prints `Page N / Limit N` after the list.
|
|
166
|
-
- `step count <plan_id> [--status todo|done|all] [--executor ai|human]`: count steps matching the filters. Output: `Total: <n>`.
|
|
167
|
-
- `step show <id>`: prints a single step with full details and its nested goals (includes ids for step/goal).
|
|
168
|
-
- Output: step header includes `Step ID: <id>`, `Plan ID`, `Status`, `Executor`, `Content`, `Created`, `Updated`, and `Comment` when present.
|
|
169
|
-
- Output: lists all goals with `[status]` and goal id.
|
|
170
|
-
- `step show-next`: show the next pending step for the active plan (same format as `step show`).
|
|
171
|
-
- Output (empty): `No active plan.` or `No pending step.`.
|
|
172
|
-
- `step update <id> [--content <content>] [--status todo|done] [--executor ai|human] [--comment <comment>]`: update fields; `--status done` is allowed only when all goals are done or the step has no goals.
|
|
173
|
-
- Output: `Updated step ID: <id>.`.
|
|
174
|
-
- Errors: `Error: Invalid input: cannot mark step done; next pending goal: <content> (id <id>)`.
|
|
175
|
-
- `step comment <id1> <comment1> [<id2> <comment2> ...]`: add or replace comments for one or more steps.
|
|
176
|
-
- Output (single): `Updated step comments for plan ID: <plan_id>.`
|
|
177
|
-
- Output (batch): `Updated step comments for <n> plans.`
|
|
178
|
-
- Each step comment uses an `<id> <comment>` pair; you can provide multiple pairs in one call.
|
|
179
|
-
- Example:
|
|
180
|
-
```json
|
|
181
|
-
{"argv":["step","comment","45","blocked by API","46","ready to start"]}
|
|
182
|
-
```
|
|
183
|
-
- `step done <id> [--all-goals]`: mark step done (same rule as `step update --status done`). Use `--all-goals` to mark all goals in the step done first, then mark the step done.
|
|
184
|
-
- Output: `Step ID: <id> marked done.`
|
|
185
|
-
- Errors: `Error: Invalid input: cannot mark step done; next pending goal: <content> (id <id>)`.
|
|
186
|
-
- `step move <id> --to <pos>`: reorder and print the same one-line list as `step list`.
|
|
187
|
-
- Output: `Reordered steps for plan ID: <plan_id>:` + list.
|
|
188
|
-
- `step remove <id1> [<id2> ...]`: remove step(s).
|
|
189
|
-
- Output (single): `Step ID: <id> removed.`
|
|
190
|
-
- Output (batch): `Removed <n> steps.`
|
|
191
|
-
- Errors: `Error: Not found: step id(s) not found: <id1>[, <id2> ...]`.
|
|
192
|
-
|
|
193
|
-
### goal
|
|
194
|
-
- `goal add <step_id> <content1> [<content2> ...]`: add goals to a step.
|
|
195
|
-
- Output (single): `Created goal ID: <id> for step ID: <step_id>`.
|
|
196
|
-
- Output (batch): `Created <n> goals for step ID: <step_id>`.
|
|
197
|
-
- `goal list <step_id> [--status todo|done|all] [--limit N] [--page N]`: list goals. Default: `--status all`, `--limit 20`, `--page 1`, order by `updated` desc (most recent first).
|
|
198
|
-
- Output: prints a header line, then one line per goal with `ID STAT CONTENT COMMENT`.
|
|
199
|
-
- Output (empty): `No goals found for step ID: <step_id>.`
|
|
200
|
-
- Pagination: If `--page` is provided without `--limit`, the default limit is used.
|
|
201
|
-
- If `--page` exceeds the total pages, a warning is printed and no rows are returned.
|
|
202
|
-
- Footer: prints `Page N / Limit N` after the list.
|
|
203
|
-
- `goal count <step_id> [--status todo|done|all]`: count goals matching the filters. Output: `Total: <n>`.
|
|
204
|
-
- `goal update <id> [--content <content>] [--status todo|done] [--comment <comment>]`: update fields.
|
|
205
|
-
- Output: `Updated goal <id>.`
|
|
206
|
-
- `goal comment <id1> <comment1> [<id2> <comment2> ...]`: add or replace comments for one or more goals.
|
|
207
|
-
- Output (single): `Updated goal comments for plan ID: <plan_id>.`
|
|
208
|
-
- Output (batch): `Updated goal comments for <n> plans.`
|
|
209
|
-
- Each goal comment uses an `<id> <comment>` pair; you can provide multiple pairs in one call.
|
|
210
|
-
- Example:
|
|
211
|
-
```json
|
|
212
|
-
{"argv":["goal","comment","78","done","81","needs review"]}
|
|
213
|
-
```
|
|
214
|
-
- `goal done <id1> [<id2> ...]`: mark one or more goals done.
|
|
215
|
-
- Output (single): `Goal ID: <id> marked done.`
|
|
216
|
-
- Output (batch): `Goals marked done: <n>.`
|
|
217
|
-
- `goal remove <id1> [<id2> ...]`: remove goal(s).
|
|
218
|
-
- Output (single): `Goal ID: <id> removed.`
|
|
219
|
-
- Output (batch): `Removed <n> goals.`
|
|
220
|
-
- Errors: `Error: Not found: goal id(s) not found: <id1>[, <id2> ...]`.
|
package/src/lib/argv.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { invalidInput } from "./errors"
|
|
2
|
-
|
|
3
|
-
export function parseCommandArgs(input: string): string[] {
|
|
4
|
-
const args: string[] = []
|
|
5
|
-
let current = ""
|
|
6
|
-
let inSingle = false
|
|
7
|
-
let inDouble = false
|
|
8
|
-
let escapeNext = false
|
|
9
|
-
|
|
10
|
-
for (let i = 0; i < input.length; i += 1) {
|
|
11
|
-
const ch = input[i]
|
|
12
|
-
|
|
13
|
-
if (escapeNext) {
|
|
14
|
-
current += ch
|
|
15
|
-
escapeNext = false
|
|
16
|
-
continue
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (ch === "\\" && !inSingle) {
|
|
20
|
-
escapeNext = true
|
|
21
|
-
continue
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (ch === "'" && !inDouble) {
|
|
25
|
-
inSingle = !inSingle
|
|
26
|
-
continue
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (ch === '"' && !inSingle) {
|
|
30
|
-
inDouble = !inDouble
|
|
31
|
-
continue
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!inSingle && !inDouble && /\s/.test(ch)) {
|
|
35
|
-
if (current.length) {
|
|
36
|
-
args.push(current)
|
|
37
|
-
current = ""
|
|
38
|
-
}
|
|
39
|
-
continue
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
current += ch
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (escapeNext) {
|
|
46
|
-
current += "\\"
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (inSingle || inDouble) {
|
|
50
|
-
throw invalidInput("unterminated quote in command")
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (current.length) {
|
|
54
|
-
args.push(current)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return args
|
|
58
|
-
}
|
package/src/lib/instructions.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import fs from "fs"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import { fileURLToPath } from "url"
|
|
4
|
-
|
|
5
|
-
let cached: string | null = null
|
|
6
|
-
|
|
7
|
-
export function loadPlanpilotInstructions(): string {
|
|
8
|
-
if (cached !== null) return cached
|
|
9
|
-
try {
|
|
10
|
-
const moduleDir = path.dirname(fileURLToPath(import.meta.url))
|
|
11
|
-
const candidates = [
|
|
12
|
-
path.resolve(moduleDir, "../../docs/planpilot.md"),
|
|
13
|
-
path.resolve(moduleDir, "../../../docs/planpilot.md"),
|
|
14
|
-
]
|
|
15
|
-
for (const candidate of candidates) {
|
|
16
|
-
if (fs.existsSync(candidate)) {
|
|
17
|
-
cached = fs.readFileSync(candidate, "utf8")
|
|
18
|
-
return cached
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
cached = ""
|
|
22
|
-
} catch {
|
|
23
|
-
cached = ""
|
|
24
|
-
}
|
|
25
|
-
return cached
|
|
26
|
-
}
|