opencode-lisa 0.3.3 → 0.4.1
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/assets/skills/lisa/SKILL.md +2105 -0
- package/bin/cli.js +36 -1
- package/dist/index.js +22 -9
- package/package.json +1 -1
|
@@ -0,0 +1,2105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lisa
|
|
3
|
+
description: Lisa - intelligent epic workflow with spec, research, plan, and execute phases. Smarter than Ralph.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lisa - Intelligent Epic Workflow
|
|
7
|
+
|
|
8
|
+
A structured approach to implementing large features by breaking them into phases: spec, research, plan, and execute.
|
|
9
|
+
|
|
10
|
+
Like the Ralph Wiggum pattern, but smarter. Lisa plans before she acts.
|
|
11
|
+
|
|
12
|
+
## Working Directory
|
|
13
|
+
|
|
14
|
+
Epics are stored in \`.lisa/epics/\` relative to **where you run \`opencode\`**.
|
|
15
|
+
|
|
16
|
+
Run opencode from your project root and epics will be at \`your-project/.lisa/epics/\`.
|
|
17
|
+
|
|
18
|
+
**Example structure:**
|
|
19
|
+
\`\`\`
|
|
20
|
+
my-project/ <- run \`opencode\` from here
|
|
21
|
+
├── .lisa/
|
|
22
|
+
│ ├── config.jsonc
|
|
23
|
+
│ ├── .gitignore
|
|
24
|
+
│ └── epics/
|
|
25
|
+
│ └── my-feature/
|
|
26
|
+
│ ├── .state
|
|
27
|
+
│ ├── spec.md
|
|
28
|
+
│ └── tasks/
|
|
29
|
+
├── src/
|
|
30
|
+
└── package.json
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Parse Arguments
|
|
36
|
+
|
|
37
|
+
The input format is: \`<epic-name> [mode]\`
|
|
38
|
+
|
|
39
|
+
### If no arguments or \`help\`:
|
|
40
|
+
|
|
41
|
+
If the user runs \`/lisa\` with no arguments, or \`/lisa help\`, IMMEDIATELY output EXACTLY this text (verbatim, no modifications, no tool calls):
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
**Lisa - Intelligent Epic Workflow**
|
|
46
|
+
|
|
47
|
+
**Available Commands:**
|
|
48
|
+
|
|
49
|
+
\`/lisa list\` - List all epics and their status
|
|
50
|
+
\`/lisa <name>\` - Continue or create an epic (interactive)
|
|
51
|
+
\`/lisa <name> spec\` - Create/view the spec only
|
|
52
|
+
\`/lisa <name> status\` - Show detailed epic status
|
|
53
|
+
\`/lisa <name> yolo\` - Auto-execute mode (no confirmations)
|
|
54
|
+
\`/lisa config view\` - View current configuration
|
|
55
|
+
\`/lisa config init\` - Initialize config with defaults
|
|
56
|
+
\`/lisa config reset\` - Reset config to defaults
|
|
57
|
+
|
|
58
|
+
**Examples:**
|
|
59
|
+
- \`/lisa list\` - See all your epics
|
|
60
|
+
- \`/lisa auth-system\` - Start or continue the auth-system epic
|
|
61
|
+
- \`/lisa auth-system yolo\` - Run auth-system in full auto mode
|
|
62
|
+
|
|
63
|
+
**Get started:** \`/lisa <epic-name>\`
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
**CRITICAL: Output the above help text EXACTLY as shown. Do not add explanations, do not call tools, do not be creative. Just show the menu and stop.**
|
|
68
|
+
|
|
69
|
+
### Otherwise, parse the arguments with SMART PARSING:
|
|
70
|
+
|
|
71
|
+
**CRITICAL PARSING RULES:**
|
|
72
|
+
|
|
73
|
+
The known modes are: \`list\`, \`config\`, \`spec\`, \`yolo\`, \`status\`
|
|
74
|
+
|
|
75
|
+
**Parse from RIGHT to LEFT:**
|
|
76
|
+
1. Check if the LAST argument is a known mode (spec/yolo/status)
|
|
77
|
+
2. If yes: everything BEFORE it = epic name (joined with hyphens), last arg = mode
|
|
78
|
+
3. If no: check if FIRST argument is "list" or "config" (special modes)
|
|
79
|
+
4. Otherwise: ALL arguments = epic name (joined with hyphens), mode = null (default)
|
|
80
|
+
|
|
81
|
+
**Parsing Examples:**
|
|
82
|
+
- \`initial setup\` → name: "initial-setup", mode: null
|
|
83
|
+
- \`initial setup yolo\` → name: "initial-setup", mode: "yolo"
|
|
84
|
+
- \`my complex feature spec\` → name: "my-complex-feature", mode: "spec"
|
|
85
|
+
- \`auth system status\` → name: "auth-system", mode: "status"
|
|
86
|
+
- \`list\` → mode: "list" (special, no epic name)
|
|
87
|
+
- \`config view\` → mode: "config view" (special, no epic name)
|
|
88
|
+
- \`my-feature\` → name: "my-feature", mode: null
|
|
89
|
+
|
|
90
|
+
**IMPORTANT:** Epic names are stored as hyphenated (e.g., \`initial-setup\`) but display with the user's original spacing in messages.
|
|
91
|
+
|
|
92
|
+
**Modes:**
|
|
93
|
+
- \`list\` → List all epics
|
|
94
|
+
- \`config <action>\` → Config management (view/init/reset)
|
|
95
|
+
- \`<name>\` (no mode) → Default mode with checkpoints
|
|
96
|
+
- \`<name> spec\` → Just create/view spec
|
|
97
|
+
- \`<name> yolo\` → Full auto, no checkpoints
|
|
98
|
+
- \`<name> status\` → Show status
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Mode: config
|
|
103
|
+
|
|
104
|
+
Handle config subcommands using the \`lisa_config\` tool:
|
|
105
|
+
|
|
106
|
+
- \`config view\` → Call \`lisa_config(action: "view")\` and display the result
|
|
107
|
+
- \`config init\` → Call \`lisa_config(action: "init")\` and confirm creation
|
|
108
|
+
- \`config reset\` → Call \`lisa_config(action: "reset")\` and confirm reset
|
|
109
|
+
|
|
110
|
+
After the tool returns, display the result in a user-friendly format.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Mode: list
|
|
115
|
+
|
|
116
|
+
**Use the \`list_epics\` tool** to quickly get all epics and their status.
|
|
117
|
+
|
|
118
|
+
Display the results in a formatted list showing:
|
|
119
|
+
- Epic name
|
|
120
|
+
- Current phase (spec/research/plan/execute/complete)
|
|
121
|
+
- Task progress (X/Y done) if in execute phase
|
|
122
|
+
- Whether yolo mode is active
|
|
123
|
+
|
|
124
|
+
**If no epics found:**
|
|
125
|
+
> "No epics found. Start one with \`/lisa <name>\`"
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Mode: status
|
|
130
|
+
|
|
131
|
+
**Use the \`get_epic_status\` tool** to quickly get detailed status.
|
|
132
|
+
|
|
133
|
+
Display the results showing:
|
|
134
|
+
- Current phase
|
|
135
|
+
- Which artifacts exist (spec.md, research.md, plan.md)
|
|
136
|
+
- Task breakdown: done, in-progress, pending, blocked
|
|
137
|
+
- Yolo mode status (if active)
|
|
138
|
+
- Suggested next action
|
|
139
|
+
|
|
140
|
+
**If epic doesn't exist:**
|
|
141
|
+
> "Epic '<name>' not found. Start it with \`/lisa <name>\`"
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Mode: spec
|
|
146
|
+
|
|
147
|
+
Interactive spec creation only. Does NOT continue to research/plan/execute.
|
|
148
|
+
|
|
149
|
+
### If spec already exists:
|
|
150
|
+
|
|
151
|
+
Read and display the existing spec, then:
|
|
152
|
+
|
|
153
|
+
> "Spec already exists at \`.lisa/epics/<name>/spec.md\`. You can:
|
|
154
|
+
> - Edit it directly in your editor
|
|
155
|
+
> - Delete it and run \`/lisa <name> spec\` again to start over
|
|
156
|
+
> - Run \`/lisa <name>\` to continue with research and planning"
|
|
157
|
+
|
|
158
|
+
### If no spec exists:
|
|
159
|
+
|
|
160
|
+
Have an interactive conversation to define the spec. Cover:
|
|
161
|
+
|
|
162
|
+
1. **Goal** - What are we trying to achieve? Why?
|
|
163
|
+
2. **Scope** - What's included? What's explicitly out of scope?
|
|
164
|
+
3. **Acceptance Criteria** - How do we know when it's done?
|
|
165
|
+
4. **Technical Constraints** - Any specific technologies, patterns, or limitations?
|
|
166
|
+
|
|
167
|
+
Be conversational. Ask clarifying questions. Push back if scope is too large or vague.
|
|
168
|
+
|
|
169
|
+
**Keep it concise** - aim for 20-50 lines. Focus on "what" and "why", not "how".
|
|
170
|
+
|
|
171
|
+
### When conversation is complete:
|
|
172
|
+
|
|
173
|
+
Summarize the spec and ask:
|
|
174
|
+
|
|
175
|
+
> "Here's the spec:
|
|
176
|
+
>
|
|
177
|
+
> [formatted spec]
|
|
178
|
+
>
|
|
179
|
+
> Ready to save to \`.lisa/epics/<name>/spec.md\`?"
|
|
180
|
+
|
|
181
|
+
On confirmation, create the directory and save:
|
|
182
|
+
|
|
183
|
+
\`\`\`
|
|
184
|
+
.lisa/epics/<name>/
|
|
185
|
+
spec.md
|
|
186
|
+
.state
|
|
187
|
+
\`\`\`
|
|
188
|
+
|
|
189
|
+
**spec.md format:**
|
|
190
|
+
\`\`\`markdown
|
|
191
|
+
# Epic: <name>
|
|
192
|
+
|
|
193
|
+
## Goal
|
|
194
|
+
[What we're building and why - 1-2 sentences]
|
|
195
|
+
|
|
196
|
+
## Scope
|
|
197
|
+
- [What's included]
|
|
198
|
+
- [What's included]
|
|
199
|
+
|
|
200
|
+
### Out of Scope
|
|
201
|
+
- [What we're NOT doing]
|
|
202
|
+
|
|
203
|
+
## Acceptance Criteria
|
|
204
|
+
- [ ] [Measurable criterion]
|
|
205
|
+
- [ ] [Measurable criterion]
|
|
206
|
+
|
|
207
|
+
## Technical Constraints
|
|
208
|
+
- [Any constraints, or "None"]
|
|
209
|
+
\`\`\`
|
|
210
|
+
|
|
211
|
+
**.state format (JSON):**
|
|
212
|
+
\`\`\`json
|
|
213
|
+
{
|
|
214
|
+
"name": "<name>",
|
|
215
|
+
"currentPhase": "spec",
|
|
216
|
+
"specComplete": true,
|
|
217
|
+
"researchComplete": false,
|
|
218
|
+
"planComplete": false,
|
|
219
|
+
"executeComplete": false,
|
|
220
|
+
"lastUpdated": "<timestamp>"
|
|
221
|
+
}
|
|
222
|
+
\`\`\`
|
|
223
|
+
|
|
224
|
+
After saving:
|
|
225
|
+
> "Spec saved to \`.lisa/epics/<name>/spec.md\`
|
|
226
|
+
>
|
|
227
|
+
> Next steps:
|
|
228
|
+
> - Run \`/lisa <name>\` to continue with research and planning
|
|
229
|
+
> - Run \`/lisa <name> yolo\` for full auto execution"
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Mode: default (with checkpoints)
|
|
234
|
+
|
|
235
|
+
This is the main interactive mode. It guides you through each phase with approval checkpoints.
|
|
236
|
+
|
|
237
|
+
### Step 1: Ensure spec exists
|
|
238
|
+
|
|
239
|
+
**If no spec:**
|
|
240
|
+
Run the spec conversation (same as spec mode). After saving, continue to step 2.
|
|
241
|
+
|
|
242
|
+
**If spec exists:**
|
|
243
|
+
Read and briefly summarize it, then continue to step 2.
|
|
244
|
+
|
|
245
|
+
### Step 2: Research phase
|
|
246
|
+
|
|
247
|
+
**If research.md already exists:**
|
|
248
|
+
> "Research already complete. Proceeding to planning..."
|
|
249
|
+
Skip to step 3.
|
|
250
|
+
|
|
251
|
+
**If research not done:**
|
|
252
|
+
> "Ready to start research? I'll explore the codebase to understand what's needed for this epic."
|
|
253
|
+
|
|
254
|
+
Wait for confirmation. On "yes" or similar:
|
|
255
|
+
|
|
256
|
+
1. Read spec.md
|
|
257
|
+
2. Explore the codebase using available tools (LSP, grep, glob, file reads)
|
|
258
|
+
3. Document findings
|
|
259
|
+
4. Save to \`.lisa/epics/<name>/research.md\`
|
|
260
|
+
5. Update .state
|
|
261
|
+
|
|
262
|
+
**research.md format:**
|
|
263
|
+
\`\`\`markdown
|
|
264
|
+
# Research: <name>
|
|
265
|
+
|
|
266
|
+
## Overview
|
|
267
|
+
[1-2 sentence summary of findings]
|
|
268
|
+
|
|
269
|
+
## Relevant Files
|
|
270
|
+
- \`path/to/file.ts\` - [why it's relevant]
|
|
271
|
+
- \`path/to/file.ts\` - [why it's relevant]
|
|
272
|
+
|
|
273
|
+
## Existing Patterns
|
|
274
|
+
[How similar things are done in this codebase]
|
|
275
|
+
|
|
276
|
+
## Dependencies
|
|
277
|
+
[External packages or internal modules needed]
|
|
278
|
+
|
|
279
|
+
## Technical Findings
|
|
280
|
+
[Key discoveries that affect implementation]
|
|
281
|
+
|
|
282
|
+
## Recommendations
|
|
283
|
+
[Suggested approach based on findings]
|
|
284
|
+
\`\`\`
|
|
285
|
+
|
|
286
|
+
After saving:
|
|
287
|
+
> "Research complete and saved. Found X relevant files. Key insight: [one line summary]"
|
|
288
|
+
|
|
289
|
+
### Step 3: Plan phase
|
|
290
|
+
|
|
291
|
+
**If plan.md already exists:**
|
|
292
|
+
> "Plan already complete with X tasks. Proceeding to execution..."
|
|
293
|
+
Skip to step 4.
|
|
294
|
+
|
|
295
|
+
**If plan not done:**
|
|
296
|
+
> "Ready to create the implementation plan?"
|
|
297
|
+
|
|
298
|
+
Wait for confirmation. On "yes" or similar:
|
|
299
|
+
|
|
300
|
+
1. Read spec.md and research.md
|
|
301
|
+
2. Break down into discrete tasks (aim for 1-5 files per task, ~30 min of work each)
|
|
302
|
+
3. Define dependencies between tasks
|
|
303
|
+
4. Save plan.md and individual task files
|
|
304
|
+
5. Update .state
|
|
305
|
+
|
|
306
|
+
**plan.md format:**
|
|
307
|
+
\`\`\`markdown
|
|
308
|
+
# Plan: <name>
|
|
309
|
+
|
|
310
|
+
## Overview
|
|
311
|
+
[1-2 sentence summary of approach]
|
|
312
|
+
|
|
313
|
+
## Tasks
|
|
314
|
+
|
|
315
|
+
1. [Task name] - tasks/01-[slug].md
|
|
316
|
+
2. [Task name] - tasks/02-[slug].md
|
|
317
|
+
3. [Task name] - tasks/03-[slug].md
|
|
318
|
+
|
|
319
|
+
## Dependencies
|
|
320
|
+
|
|
321
|
+
- 01: []
|
|
322
|
+
- 02: [01]
|
|
323
|
+
- 03: [01]
|
|
324
|
+
- 04: [02, 03]
|
|
325
|
+
|
|
326
|
+
## Risks
|
|
327
|
+
- [Risk and mitigation, or "None identified"]
|
|
328
|
+
\`\`\`
|
|
329
|
+
|
|
330
|
+
**Task file format (tasks/XX-slug.md):**
|
|
331
|
+
\`\`\`markdown
|
|
332
|
+
# Task X: [Name]
|
|
333
|
+
|
|
334
|
+
## Status: pending
|
|
335
|
+
|
|
336
|
+
## Goal
|
|
337
|
+
[What this task accomplishes - 1-2 sentences]
|
|
338
|
+
|
|
339
|
+
## Files
|
|
340
|
+
- path/to/file1.ts
|
|
341
|
+
- path/to/file2.ts
|
|
342
|
+
|
|
343
|
+
## Steps
|
|
344
|
+
1. [Concrete step]
|
|
345
|
+
2. [Concrete step]
|
|
346
|
+
3. [Concrete step]
|
|
347
|
+
|
|
348
|
+
## Done When
|
|
349
|
+
- [ ] [Testable criterion]
|
|
350
|
+
- [ ] [Testable criterion]
|
|
351
|
+
\`\`\`
|
|
352
|
+
|
|
353
|
+
After saving:
|
|
354
|
+
> "Plan created with X tasks:
|
|
355
|
+
> 1. [task 1 name]
|
|
356
|
+
> 2. [task 2 name]
|
|
357
|
+
> ...
|
|
358
|
+
>
|
|
359
|
+
> Saved to \`.lisa/epics/<name>/plan.md\`"
|
|
360
|
+
|
|
361
|
+
### Step 4: Execute phase
|
|
362
|
+
|
|
363
|
+
**Use \`get_available_tasks\` tool** to quickly see what's ready to run.
|
|
364
|
+
|
|
365
|
+
**If all tasks done (available and blocked both empty):**
|
|
366
|
+
> "All tasks complete! Epic finished."
|
|
367
|
+
Stop.
|
|
368
|
+
|
|
369
|
+
**If tasks remain:**
|
|
370
|
+
Show task summary from the tool output and ask:
|
|
371
|
+
> "Ready to execute? X tasks remaining:
|
|
372
|
+
> - Available now: [from available list]
|
|
373
|
+
> - Blocked by dependencies: [from blocked list]"
|
|
374
|
+
|
|
375
|
+
Wait for confirmation. On "yes" or similar:
|
|
376
|
+
|
|
377
|
+
**Execute tasks using \`build_task_context\` + Task tool:**
|
|
378
|
+
|
|
379
|
+
Tasks with satisfied dependencies can be executed in **parallel** (the \`available\` list from \`get_available_tasks\` shows all tasks that are ready). Tasks whose dependencies aren't met yet are in the \`blocked\` list and must wait.
|
|
380
|
+
|
|
381
|
+
For each task in the \`available\` list:
|
|
382
|
+
1. Call \`build_task_context(epicName, taskId)\` to get the prompt
|
|
383
|
+
2. Call the Task tool with the prompt to spawn a sub-agent
|
|
384
|
+
3. After sub-agent(s) complete, call \`get_available_tasks\` again to refresh the list
|
|
385
|
+
4. If a task isn't done, retry up to 3 times, then mark blocked
|
|
386
|
+
5. Repeat until all tasks done
|
|
387
|
+
|
|
388
|
+
**Note:** If executing in parallel, each sub-agent gets the same context snapshot. Their reports will be available for subsequent tasks.
|
|
389
|
+
|
|
390
|
+
**On task failure (after 3 attempts):**
|
|
391
|
+
- Mark task as \`blocked\` in the task file
|
|
392
|
+
- Add \`## Blocked Reason: [why]\`
|
|
393
|
+
- Continue with other available tasks
|
|
394
|
+
|
|
395
|
+
**On all tasks complete:**
|
|
396
|
+
> "Epic complete! All X tasks finished.
|
|
397
|
+
>
|
|
398
|
+
> Summary of changes:
|
|
399
|
+
> - [file]: [what changed]
|
|
400
|
+
> - [file]: [what changed]"
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Mode: yolo (full auto)
|
|
405
|
+
|
|
406
|
+
Full automatic execution with no checkpoints. Requires spec to exist.
|
|
407
|
+
|
|
408
|
+
**IMPORTANT:** In yolo mode, the Lisa plugin monitors for session idle events and automatically continues execution until all tasks are complete. You don't need to worry about session limits - just keep working and the plugin handles continuation.
|
|
409
|
+
|
|
410
|
+
### YOLO MODE RULES - READ CAREFULLY
|
|
411
|
+
|
|
412
|
+
When in yolo mode, you MUST follow these rules strictly:
|
|
413
|
+
|
|
414
|
+
1. **NEVER stop to summarize progress** - Don't say "I've completed X, Y tasks remain". Just keep working.
|
|
415
|
+
|
|
416
|
+
2. **NEVER ask for confirmation** - Don't say "Ready to continue?" or "Should I proceed?". Just proceed.
|
|
417
|
+
|
|
418
|
+
3. **NEVER explain what you're about to do** - Don't narrate. Execute.
|
|
419
|
+
|
|
420
|
+
4. **ALWAYS execute the next task immediately** - After one task completes, immediately call \`get_available_tasks\` and start the next one.
|
|
421
|
+
|
|
422
|
+
5. **ONLY stop when truly done** - You stop ONLY when:
|
|
423
|
+
- All tasks have \`## Status: done\`, OR
|
|
424
|
+
- All remaining tasks are \`## Status: blocked\`
|
|
425
|
+
|
|
426
|
+
6. **Treat each response as a work session** - Your goal is to make maximum progress before your response ends. Execute as many tasks as possible.
|
|
427
|
+
|
|
428
|
+
**Why these rules matter:** Yolo mode is for autonomous, unattended execution. The user has walked away. Every time you stop to summarize or ask a question, you break the automation and waste the user's time.
|
|
429
|
+
|
|
430
|
+
**If you're unsure, keep working.** It's better to complete an extra task than to stop and ask.
|
|
431
|
+
|
|
432
|
+
### If no spec exists:
|
|
433
|
+
|
|
434
|
+
> "No spec found at \`.lisa/epics/<name>/spec.md\`.
|
|
435
|
+
>
|
|
436
|
+
> Create one first:
|
|
437
|
+
> - Interactively: \`/lisa <name> spec\`
|
|
438
|
+
> - Manually: Create \`.lisa/epics/<name>/spec.md\`"
|
|
439
|
+
|
|
440
|
+
Stop. Do not proceed.
|
|
441
|
+
|
|
442
|
+
### If spec exists:
|
|
443
|
+
|
|
444
|
+
**Step 1: Activate yolo mode in .state**
|
|
445
|
+
|
|
446
|
+
Read the current \`.lisa/epics/<name>/.state\` file and add the \`yolo\` configuration:
|
|
447
|
+
|
|
448
|
+
\`\`\`json
|
|
449
|
+
{
|
|
450
|
+
"name": "<name>",
|
|
451
|
+
"currentPhase": "...",
|
|
452
|
+
"specComplete": true,
|
|
453
|
+
"researchComplete": false,
|
|
454
|
+
"planComplete": false,
|
|
455
|
+
"executeComplete": false,
|
|
456
|
+
"lastUpdated": "<timestamp>",
|
|
457
|
+
"yolo": {
|
|
458
|
+
"active": true,
|
|
459
|
+
"iteration": 1,
|
|
460
|
+
"maxIterations": 100,
|
|
461
|
+
"startedAt": "<current ISO timestamp>"
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
\`\`\`
|
|
465
|
+
|
|
466
|
+
This tells the Lisa plugin to automatically continue the session when you finish responding.
|
|
467
|
+
|
|
468
|
+
**Step 2: Run all phases without asking for confirmation:**
|
|
469
|
+
|
|
470
|
+
1. **Research** (if not done) - explore codebase, save research.md
|
|
471
|
+
2. **Plan** (if not done) - create plan.md and task files
|
|
472
|
+
3. **Execute** - use \`get_available_tasks\` + \`build_task_context\` + Task tool
|
|
473
|
+
|
|
474
|
+
**Execute tasks using \`build_task_context\` + Task tool:**
|
|
475
|
+
|
|
476
|
+
Tasks with satisfied dependencies can be executed in **parallel** if desired.
|
|
477
|
+
|
|
478
|
+
1. Call \`get_available_tasks(epicName)\` to get the list of ready tasks
|
|
479
|
+
2. For each task in the \`available\` list (can parallelize):
|
|
480
|
+
- Call \`build_task_context(epicName, taskId)\` to get the prompt
|
|
481
|
+
- Call the Task tool with the prompt to spawn a sub-agent
|
|
482
|
+
3. After sub-agent(s) complete, call \`get_available_tasks\` again to refresh
|
|
483
|
+
4. If a task isn't done, retry up to 3 times, then mark blocked
|
|
484
|
+
5. Repeat until all tasks done or all blocked
|
|
485
|
+
|
|
486
|
+
The plugin will automatically continue the session if context fills up.
|
|
487
|
+
|
|
488
|
+
**REMEMBER THE YOLO RULES:** Don't stop to summarize. Don't ask questions. Just keep executing tasks until they're all done or blocked.
|
|
489
|
+
|
|
490
|
+
**On all tasks complete:**
|
|
491
|
+
- Update .state: set \`executeComplete: true\` and \`yolo.active: false\`
|
|
492
|
+
> "Epic complete! All X tasks finished."
|
|
493
|
+
|
|
494
|
+
**On task blocked (after 3 attempts):**
|
|
495
|
+
- Mark as blocked in the task file, continue with others
|
|
496
|
+
- If all remaining tasks blocked:
|
|
497
|
+
- Update .state: set \`yolo.active: false\`
|
|
498
|
+
- Report which tasks are blocked and why
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Shared: Task Execution Logic
|
|
503
|
+
|
|
504
|
+
**IMPORTANT: Use the \`build_task_context\` tool + Task tool for each task.**
|
|
505
|
+
|
|
506
|
+
This pattern ensures each task runs with fresh context in a sub-agent:
|
|
507
|
+
- Fresh context for each task (no accumulated cruft)
|
|
508
|
+
- Proper handoff between tasks via reports
|
|
509
|
+
- Consistent execution pattern
|
|
510
|
+
|
|
511
|
+
### Execution Flow (Orchestrator)
|
|
512
|
+
|
|
513
|
+
As the orchestrator, you manage the overall flow:
|
|
514
|
+
|
|
515
|
+
1. **Read plan.md** to understand task order and dependencies
|
|
516
|
+
2. **For each available task** (dependencies satisfied, not blocked):
|
|
517
|
+
|
|
518
|
+
**Step A: Build context**
|
|
519
|
+
\`\`\`
|
|
520
|
+
Call build_task_context with:
|
|
521
|
+
- epicName: the epic name
|
|
522
|
+
- taskId: the task number (e.g., "01", "02")
|
|
523
|
+
\`\`\`
|
|
524
|
+
This returns a \`prompt\` field with the full context.
|
|
525
|
+
|
|
526
|
+
**Step B: Execute with sub-agent**
|
|
527
|
+
\`\`\`
|
|
528
|
+
Call the Task tool with:
|
|
529
|
+
- description: "Execute task {taskId} of epic {epicName}"
|
|
530
|
+
- prompt: [the prompt returned from build_task_context]
|
|
531
|
+
\`\`\`
|
|
532
|
+
|
|
533
|
+
3. **After sub-agent completes**, check the task file:
|
|
534
|
+
- If \`## Status: done\` → Move to next task
|
|
535
|
+
- If not done → Retry (up to 3 times) or mark blocked
|
|
536
|
+
4. **Repeat** until all tasks done or all remaining tasks blocked
|
|
537
|
+
|
|
538
|
+
### What the Sub-Agent Does
|
|
539
|
+
|
|
540
|
+
The sub-agent (spawned via Task tool) receives full context and:
|
|
541
|
+
|
|
542
|
+
1. **Reads the context**: spec, research, plan, all previous task files with reports
|
|
543
|
+
2. **Executes the task steps**
|
|
544
|
+
3. **Updates the task file**:
|
|
545
|
+
- Changes \`## Status: pending\` to \`## Status: done\`
|
|
546
|
+
- Adds a \`## Report\` section (see format below)
|
|
547
|
+
4. **May update future tasks** if the plan needs changes
|
|
548
|
+
5. **Confirms completion** when done
|
|
549
|
+
|
|
550
|
+
### Task File Format (with Report)
|
|
551
|
+
|
|
552
|
+
After completion, a task file should look like:
|
|
553
|
+
|
|
554
|
+
\`\`\`markdown
|
|
555
|
+
# Task 01: [Name]
|
|
556
|
+
|
|
557
|
+
## Status: done
|
|
558
|
+
|
|
559
|
+
## Goal
|
|
560
|
+
[What this task accomplishes]
|
|
561
|
+
|
|
562
|
+
## Files
|
|
563
|
+
- path/to/file1.ts
|
|
564
|
+
- path/to/file2.ts
|
|
565
|
+
|
|
566
|
+
## Steps
|
|
567
|
+
1. [Concrete step]
|
|
568
|
+
2. [Concrete step]
|
|
569
|
+
|
|
570
|
+
## Done When
|
|
571
|
+
- [x] [Criterion - now checked]
|
|
572
|
+
- [x] [Criterion - now checked]
|
|
573
|
+
|
|
574
|
+
## Report
|
|
575
|
+
|
|
576
|
+
### What Was Done
|
|
577
|
+
- Created X component
|
|
578
|
+
- Added Y functionality
|
|
579
|
+
- Configured Z
|
|
580
|
+
|
|
581
|
+
### Decisions Made
|
|
582
|
+
- Chose approach A over B because [reason]
|
|
583
|
+
- Used library X for [reason]
|
|
584
|
+
|
|
585
|
+
### Issues / Notes for Next Task
|
|
586
|
+
- The API returns data in format X, next task should handle this
|
|
587
|
+
- Found that Y needs to be done differently than planned
|
|
588
|
+
|
|
589
|
+
### Files Changed
|
|
590
|
+
- src/components/Foo.tsx (new)
|
|
591
|
+
- src/hooks/useBar.ts (modified)
|
|
592
|
+
- package.json (added dependency)
|
|
593
|
+
\`\`\`
|
|
594
|
+
|
|
595
|
+
### Handling Failures
|
|
596
|
+
|
|
597
|
+
When \`execute_epic_task\` returns \`status: "failed"\`:
|
|
598
|
+
|
|
599
|
+
1. **Check the summary** for what went wrong
|
|
600
|
+
2. **Decide**:
|
|
601
|
+
- Retry (up to 3 times) if it seems like a transient issue
|
|
602
|
+
- Mark as blocked if fundamentally broken
|
|
603
|
+
- Revise the plan if the approach is wrong
|
|
604
|
+
|
|
605
|
+
To mark as blocked:
|
|
606
|
+
\`\`\`markdown
|
|
607
|
+
## Status: blocked
|
|
608
|
+
|
|
609
|
+
## Blocked Reason
|
|
610
|
+
[Explanation of why this task cannot proceed]
|
|
611
|
+
\`\`\`
|
|
612
|
+
|
|
613
|
+
### On discovering the plan needs changes:
|
|
614
|
+
|
|
615
|
+
If during execution you realize:
|
|
616
|
+
- A task's approach is fundamentally wrong (not just a bug to fix)
|
|
617
|
+
- Tasks are missing that should have been included
|
|
618
|
+
- Dependencies are incorrect
|
|
619
|
+
- The order should change
|
|
620
|
+
- New information invalidates earlier assumptions
|
|
621
|
+
|
|
622
|
+
**You may update the plan. The plan is a living document, not a rigid contract.**
|
|
623
|
+
|
|
624
|
+
1. **Update the affected task file(s)** in \`tasks/\`:
|
|
625
|
+
- Revise steps if the approach needs changing
|
|
626
|
+
- Update "Files" if different files are involved
|
|
627
|
+
- Update "Done When" if criteria need adjusting
|
|
628
|
+
|
|
629
|
+
2. **Update \`plan.md\`** if:
|
|
630
|
+
- Adding new tasks (create new task files too)
|
|
631
|
+
- Removing tasks (mark as \`## Status: cancelled\` with reason)
|
|
632
|
+
- Changing dependencies
|
|
633
|
+
|
|
634
|
+
3. **Document the change** in the task file:
|
|
635
|
+
\`\`\`markdown
|
|
636
|
+
## Plan Revision
|
|
637
|
+
- Changed: [what changed]
|
|
638
|
+
- Reason: [why the original approach didn't work]
|
|
639
|
+
- Timestamp: [now]
|
|
640
|
+
\`\`\`
|
|
641
|
+
|
|
642
|
+
4. **Continue execution** with the revised plan
|
|
643
|
+
|
|
644
|
+
**Key principle:** Do NOT keep retrying a broken approach. If something fundamentally doesn't work, adapt the plan. It's better to revise and succeed than to stubbornly fail.
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
## Shared: Parsing Dependencies
|
|
649
|
+
|
|
650
|
+
The plan.md Dependencies section looks like:
|
|
651
|
+
\`\`\`markdown
|
|
652
|
+
## Dependencies
|
|
653
|
+
- 01: []
|
|
654
|
+
- 02: [01]
|
|
655
|
+
- 03: [01, 02]
|
|
656
|
+
\`\`\`
|
|
657
|
+
|
|
658
|
+
A task is **available** when:
|
|
659
|
+
1. Status is \`pending\` (or \`in-progress\` with progress notes)
|
|
660
|
+
2. All tasks in its dependency list have status \`done\`
|
|
661
|
+
|
|
662
|
+
A task is **blocked** when:
|
|
663
|
+
1. Status is \`blocked\`, OR
|
|
664
|
+
2. Any dependency is not \`done\` and not expected to complete
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## State File (.state)
|
|
669
|
+
|
|
670
|
+
Track epic progress in \`.lisa/epics/<name>/.state\`:
|
|
671
|
+
|
|
672
|
+
\`\`\`json
|
|
673
|
+
{
|
|
674
|
+
"name": "<name>",
|
|
675
|
+
"currentPhase": "execute",
|
|
676
|
+
"specComplete": true,
|
|
677
|
+
"researchComplete": true,
|
|
678
|
+
"planComplete": true,
|
|
679
|
+
"executeComplete": false,
|
|
680
|
+
"lastUpdated": "2026-01-16T10:00:00Z"
|
|
681
|
+
}
|
|
682
|
+
\`\`\`
|
|
683
|
+
|
|
684
|
+
**With yolo mode active:**
|
|
685
|
+
\`\`\`json
|
|
686
|
+
{
|
|
687
|
+
"name": "<name>",
|
|
688
|
+
"currentPhase": "execute",
|
|
689
|
+
"specComplete": true,
|
|
690
|
+
"researchComplete": true,
|
|
691
|
+
"planComplete": true,
|
|
692
|
+
"executeComplete": false,
|
|
693
|
+
"lastUpdated": "2026-01-16T10:00:00Z",
|
|
694
|
+
"yolo": {
|
|
695
|
+
"active": true,
|
|
696
|
+
"iteration": 1,
|
|
697
|
+
"maxIterations": 100,
|
|
698
|
+
"startedAt": "2026-01-16T10:00:00Z"
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
\`\`\`
|
|
702
|
+
|
|
703
|
+
**Yolo fields:**
|
|
704
|
+
- \`active\`: Set to \`true\` when yolo mode starts, \`false\` when complete or stopped
|
|
705
|
+
- \`iteration\`: Current iteration count (plugin increments this on each continuation)
|
|
706
|
+
- \`maxIterations\`: Safety limit. Use the value from config (\`yolo.defaultMaxIterations\`). Set to 0 for unlimited.
|
|
707
|
+
- \`startedAt\`: ISO timestamp when yolo mode was activated
|
|
708
|
+
|
|
709
|
+
Update this file after each phase completes. The Lisa plugin reads this file to determine whether to auto-continue.
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
## Configuration
|
|
714
|
+
|
|
715
|
+
Lisa settings are stored in \`.lisa/config.jsonc\`. The config is automatically created with safe defaults when you first create an epic.
|
|
716
|
+
|
|
717
|
+
**Config locations (merged in order):**
|
|
718
|
+
1. \`~/.config/lisa/config.jsonc\` - Global user defaults
|
|
719
|
+
2. \`.lisa/config.jsonc\` - Project settings (commit this)
|
|
720
|
+
3. \`.lisa/config.local.jsonc\` - Personal overrides (gitignored)
|
|
721
|
+
|
|
722
|
+
**Use the \`get_lisa_config\` tool** to read current config settings.
|
|
723
|
+
|
|
724
|
+
**Use the \`lisa_config\` tool** to view or manage config:
|
|
725
|
+
- \`lisa_config(action: "view")\` - Show current config and sources
|
|
726
|
+
- \`lisa_config(action: "init")\` - Create config if it doesn't exist
|
|
727
|
+
- \`lisa_config(action: "reset")\` - Reset config to defaults
|
|
728
|
+
|
|
729
|
+
### Config Schema
|
|
730
|
+
|
|
731
|
+
\`\`\`jsonc
|
|
732
|
+
{
|
|
733
|
+
"execution": {
|
|
734
|
+
"maxRetries": 3 // Retries for failed tasks before marking blocked
|
|
735
|
+
},
|
|
736
|
+
"git": {
|
|
737
|
+
"completionMode": "none", // "pr" | "commit" | "none"
|
|
738
|
+
"branchPrefix": "epic/", // Branch naming prefix
|
|
739
|
+
"autoPush": true // Auto-push when completionMode is "pr"
|
|
740
|
+
},
|
|
741
|
+
"yolo": {
|
|
742
|
+
"defaultMaxIterations": 100 // Default max iterations (0 = unlimited)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
\`\`\`
|
|
746
|
+
|
|
747
|
+
### Completion Modes
|
|
748
|
+
|
|
749
|
+
The \`git.completionMode\` setting controls what happens when an epic completes:
|
|
750
|
+
|
|
751
|
+
- **\`"none"\`** (default, safest): No git operations. You manage git entirely.
|
|
752
|
+
- **\`"commit"\`**: Create a branch and commits, but don't push. You handle push/PR.
|
|
753
|
+
- **\`"pr"\`**: Create branch, commits, push, and open a PR via \`gh\` CLI.
|
|
754
|
+
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
## Epic Completion
|
|
758
|
+
|
|
759
|
+
When all tasks are done and the epic is complete, follow this completion flow based on the config:
|
|
760
|
+
|
|
761
|
+
### Step 1: Check config
|
|
762
|
+
|
|
763
|
+
Call \`get_lisa_config()\` to read the current \`git.completionMode\`.
|
|
764
|
+
|
|
765
|
+
### Step 2: Execute completion based on mode
|
|
766
|
+
|
|
767
|
+
**If \`git.completionMode\` is \`"none"\`:**
|
|
768
|
+
- Update \`.state\` with \`executeComplete: true\`
|
|
769
|
+
- Report completion to user:
|
|
770
|
+
> "Epic complete! All X tasks finished.
|
|
771
|
+
>
|
|
772
|
+
> Changes have been made but not committed. You can review and commit them manually."
|
|
773
|
+
|
|
774
|
+
**If \`git.completionMode\` is \`"commit"\`:**
|
|
775
|
+
1. Create a new branch if not already on one:
|
|
776
|
+
\`\`\`bash
|
|
777
|
+
git checkout -b {branchPrefix}{epicName}
|
|
778
|
+
\`\`\`
|
|
779
|
+
2. Stage and commit all changes:
|
|
780
|
+
\`\`\`bash
|
|
781
|
+
git add -A
|
|
782
|
+
git commit -m "feat: {epic goal summary}"
|
|
783
|
+
\`\`\`
|
|
784
|
+
3. Update \`.state\` with \`executeComplete: true\`
|
|
785
|
+
4. Report completion:
|
|
786
|
+
> "Epic complete! All X tasks finished.
|
|
787
|
+
>
|
|
788
|
+
> Changes committed to branch \`{branchPrefix}{epicName}\`.
|
|
789
|
+
> Push and create a PR when ready:
|
|
790
|
+
> \`\`\`
|
|
791
|
+
> git push -u origin {branchPrefix}{epicName}
|
|
792
|
+
> gh pr create
|
|
793
|
+
> \`\`\`"
|
|
794
|
+
|
|
795
|
+
**If \`git.completionMode\` is \`"pr"\`:**
|
|
796
|
+
1. Create a new branch if not already on one:
|
|
797
|
+
\`\`\`bash
|
|
798
|
+
git checkout -b {branchPrefix}{epicName}
|
|
799
|
+
\`\`\`
|
|
800
|
+
2. Stage and commit all changes:
|
|
801
|
+
\`\`\`bash
|
|
802
|
+
git add -A
|
|
803
|
+
git commit -m "feat: {epic goal summary}"
|
|
804
|
+
\`\`\`
|
|
805
|
+
3. Check if \`gh\` CLI is available:
|
|
806
|
+
\`\`\`bash
|
|
807
|
+
which gh
|
|
808
|
+
\`\`\`
|
|
809
|
+
4. **If \`gh\` is available and \`autoPush\` is true:**
|
|
810
|
+
\`\`\`bash
|
|
811
|
+
git push -u origin {branchPrefix}{epicName}
|
|
812
|
+
gh pr create --title "{epic goal}" --body "## Summary\\n\\n{epic description}\\n\\n## Tasks Completed\\n\\n{task list}"
|
|
813
|
+
\`\`\`
|
|
814
|
+
Report:
|
|
815
|
+
> "Epic complete! All X tasks finished.
|
|
816
|
+
>
|
|
817
|
+
> PR created: {PR URL}"
|
|
818
|
+
|
|
819
|
+
5. **If \`gh\` is NOT available:**
|
|
820
|
+
Report:
|
|
821
|
+
> "Epic complete! All X tasks finished.
|
|
822
|
+
>
|
|
823
|
+
> Changes committed to branch \`{branchPrefix}{epicName}\`.
|
|
824
|
+
>
|
|
825
|
+
> Note: GitHub CLI (\`gh\`) not found. Install it to enable automatic PR creation:
|
|
826
|
+
> - macOS: \`brew install gh\`
|
|
827
|
+
> - Then: \`gh auth login\`
|
|
828
|
+
>
|
|
829
|
+
> To create a PR manually:
|
|
830
|
+
> \`\`\`
|
|
831
|
+
> git push -u origin {branchPrefix}{epicName}
|
|
832
|
+
> gh pr create
|
|
833
|
+
> \`\`\`"
|
|
834
|
+
|
|
835
|
+
### Commit Message Format
|
|
836
|
+
|
|
837
|
+
Use conventional commits format for the commit message:
|
|
838
|
+
- \`feat: {epic goal}\` for new features
|
|
839
|
+
- \`fix: {epic goal}\` for bug fixes
|
|
840
|
+
- \`refactor: {epic goal}\` for refactoring
|
|
841
|
+
|
|
842
|
+
Include a brief body with the tasks completed if helpful.
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
## First Epic Setup
|
|
847
|
+
|
|
848
|
+
When creating the first epic in a project (when \`.lisa/\` doesn't exist):
|
|
849
|
+
|
|
850
|
+
1. Create \`.lisa/\` directory
|
|
851
|
+
2. Create \`.lisa/config.jsonc\` with default settings
|
|
852
|
+
3. Create \`.lisa/.gitignore\` containing \`config.local.jsonc\`
|
|
853
|
+
4. Create \`.lisa/epics/\` directory
|
|
854
|
+
5. Create the epic directory \`.lisa/epics/{epicName}/\`
|
|
855
|
+
|
|
856
|
+
This ensures config is always present with safe defaults.
|
|
857
|
+
`
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Lisa - Intelligent Epic Workflow Plugin for OpenCode
|
|
861
|
+
*
|
|
862
|
+
* Like the Ralph Wiggum pattern, but smarter. Lisa plans before she acts.
|
|
863
|
+
*
|
|
864
|
+
* Provides:
|
|
865
|
+
* 1. `build_task_context` tool - Builds context for a task (to be used with Task tool)
|
|
866
|
+
* 2. Yolo mode auto-continue - Keeps the session running until all tasks are done
|
|
867
|
+
*
|
|
868
|
+
* Works with the lisa skill (.opencode/skill/lisa/SKILL.md) which manages the epic state.
|
|
869
|
+
*/
|
|
870
|
+
|
|
871
|
+
// ============================================================================
|
|
872
|
+
// Types
|
|
873
|
+
// ============================================================================
|
|
874
|
+
|
|
875
|
+
interface YoloState {
|
|
876
|
+
active: boolean
|
|
877
|
+
iteration: number
|
|
878
|
+
maxIterations: number
|
|
879
|
+
startedAt: string
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
interface EpicState {
|
|
883
|
+
name: string
|
|
884
|
+
currentPhase: string
|
|
885
|
+
specComplete: boolean
|
|
886
|
+
researchComplete: boolean
|
|
887
|
+
planComplete: boolean
|
|
888
|
+
executeComplete: boolean
|
|
889
|
+
lastUpdated: string
|
|
890
|
+
yolo?: YoloState
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ----------------------------------------------------------------------------
|
|
894
|
+
// Lisa Configuration Types
|
|
895
|
+
// ----------------------------------------------------------------------------
|
|
896
|
+
|
|
897
|
+
type GitCompletionMode = "pr" | "commit" | "none"
|
|
898
|
+
|
|
899
|
+
interface LisaConfigExecution {
|
|
900
|
+
maxRetries: number
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
interface LisaConfigGit {
|
|
904
|
+
completionMode: GitCompletionMode
|
|
905
|
+
branchPrefix: string
|
|
906
|
+
autoPush: boolean
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
interface LisaConfigYolo {
|
|
910
|
+
defaultMaxIterations: number
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
interface LisaConfig {
|
|
914
|
+
execution: LisaConfigExecution
|
|
915
|
+
git: LisaConfigGit
|
|
916
|
+
yolo: LisaConfigYolo
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Default configuration (most cautious)
|
|
920
|
+
const DEFAULT_CONFIG: LisaConfig = {
|
|
921
|
+
execution: {
|
|
922
|
+
maxRetries: 3,
|
|
923
|
+
},
|
|
924
|
+
git: {
|
|
925
|
+
completionMode: "none",
|
|
926
|
+
branchPrefix: "epic/",
|
|
927
|
+
autoPush: true,
|
|
928
|
+
},
|
|
929
|
+
yolo: {
|
|
930
|
+
defaultMaxIterations: 100,
|
|
931
|
+
},
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Default config file content with comments
|
|
935
|
+
const DEFAULT_CONFIG_CONTENT = `{
|
|
936
|
+
// Lisa Configuration
|
|
937
|
+
//
|
|
938
|
+
// Merge order: ~/.config/lisa/config.jsonc -> .lisa/config.jsonc -> .lisa/config.local.jsonc
|
|
939
|
+
// Override locally (gitignored) with: .lisa/config.local.jsonc
|
|
940
|
+
|
|
941
|
+
"execution": {
|
|
942
|
+
// Number of retries for failed tasks before stopping
|
|
943
|
+
"maxRetries": 3
|
|
944
|
+
},
|
|
945
|
+
|
|
946
|
+
"git": {
|
|
947
|
+
// How the epic completes when all tasks are done:
|
|
948
|
+
// "pr" - Create branch, commit, push, and open PR (requires \`gh\` CLI)
|
|
949
|
+
// "commit" - Create commits only, you handle push/PR manually
|
|
950
|
+
// "none" - No git operations, you manage everything
|
|
951
|
+
"completionMode": "none",
|
|
952
|
+
|
|
953
|
+
// Branch naming prefix (e.g., "epic/my-feature")
|
|
954
|
+
"branchPrefix": "epic/",
|
|
955
|
+
|
|
956
|
+
// When completionMode is "pr": automatically push and create PR
|
|
957
|
+
// Set false to review commits before pushing
|
|
958
|
+
"autoPush": true
|
|
959
|
+
},
|
|
960
|
+
|
|
961
|
+
"yolo": {
|
|
962
|
+
// Maximum iterations in yolo mode before pausing (0 = unlimited)
|
|
963
|
+
"defaultMaxIterations": 100
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
`
|
|
967
|
+
|
|
968
|
+
// .gitignore content for .lisa directory
|
|
969
|
+
const LISA_GITIGNORE_CONTENT = `# Local config overrides (not committed)
|
|
970
|
+
config.local.jsonc
|
|
971
|
+
`
|
|
972
|
+
|
|
973
|
+
// ============================================================================
|
|
974
|
+
// Helper Functions
|
|
975
|
+
// ============================================================================
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Read a file if it exists, return empty string otherwise
|
|
979
|
+
*/
|
|
980
|
+
async function readFileIfExists(path: string): Promise<string> {
|
|
981
|
+
if (!existsSync(path)) return ""
|
|
982
|
+
try {
|
|
983
|
+
return await readFile(path, "utf-8")
|
|
984
|
+
} catch {
|
|
985
|
+
return ""
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Strip JSON comments (single-line // and multi-line block comments) from a string
|
|
991
|
+
* Simple state-machine approach - handles most common cases
|
|
992
|
+
*/
|
|
993
|
+
function stripJsonComments(jsonc: string): string {
|
|
994
|
+
// Remove single-line comments (// ...)
|
|
995
|
+
// Be careful not to match // inside strings
|
|
996
|
+
let result = ""
|
|
997
|
+
let inString = false
|
|
998
|
+
let inSingleLineComment = false
|
|
999
|
+
let inMultiLineComment = false
|
|
1000
|
+
let i = 0
|
|
1001
|
+
|
|
1002
|
+
while (i < jsonc.length) {
|
|
1003
|
+
const char = jsonc[i]
|
|
1004
|
+
const nextChar = jsonc[i + 1]
|
|
1005
|
+
|
|
1006
|
+
// Handle string boundaries
|
|
1007
|
+
if (!inSingleLineComment && !inMultiLineComment && char === '"' && jsonc[i - 1] !== "\\") {
|
|
1008
|
+
inString = !inString
|
|
1009
|
+
result += char
|
|
1010
|
+
i++
|
|
1011
|
+
continue
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Skip content inside strings
|
|
1015
|
+
if (inString) {
|
|
1016
|
+
result += char
|
|
1017
|
+
i++
|
|
1018
|
+
continue
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Check for comment start
|
|
1022
|
+
if (!inSingleLineComment && !inMultiLineComment && char === "/" && nextChar === "/") {
|
|
1023
|
+
inSingleLineComment = true
|
|
1024
|
+
i += 2
|
|
1025
|
+
continue
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (!inSingleLineComment && !inMultiLineComment && char === "/" && nextChar === "*") {
|
|
1029
|
+
inMultiLineComment = true
|
|
1030
|
+
i += 2
|
|
1031
|
+
continue
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Check for comment end
|
|
1035
|
+
if (inSingleLineComment && (char === "\n" || char === "\r")) {
|
|
1036
|
+
inSingleLineComment = false
|
|
1037
|
+
result += char
|
|
1038
|
+
i++
|
|
1039
|
+
continue
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (inMultiLineComment && char === "*" && nextChar === "/") {
|
|
1043
|
+
inMultiLineComment = false
|
|
1044
|
+
i += 2
|
|
1045
|
+
continue
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Skip comment content
|
|
1049
|
+
if (inSingleLineComment || inMultiLineComment) {
|
|
1050
|
+
i++
|
|
1051
|
+
continue
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
result += char
|
|
1055
|
+
i++
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return result
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Deep merge two objects, with source overwriting target for matching keys
|
|
1063
|
+
*/
|
|
1064
|
+
function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
|
|
1065
|
+
const result = { ...target }
|
|
1066
|
+
|
|
1067
|
+
for (const key of Object.keys(source) as Array<keyof T>) {
|
|
1068
|
+
const sourceValue = source[key]
|
|
1069
|
+
const targetValue = target[key]
|
|
1070
|
+
|
|
1071
|
+
if (
|
|
1072
|
+
sourceValue !== undefined &&
|
|
1073
|
+
typeof sourceValue === "object" &&
|
|
1074
|
+
sourceValue !== null &&
|
|
1075
|
+
!Array.isArray(sourceValue) &&
|
|
1076
|
+
typeof targetValue === "object" &&
|
|
1077
|
+
targetValue !== null &&
|
|
1078
|
+
!Array.isArray(targetValue)
|
|
1079
|
+
) {
|
|
1080
|
+
result[key] = deepMerge(targetValue, sourceValue as any)
|
|
1081
|
+
} else if (sourceValue !== undefined) {
|
|
1082
|
+
result[key] = sourceValue as T[keyof T]
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return result
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Validate and sanitize config, logging warnings for invalid values
|
|
1091
|
+
*/
|
|
1092
|
+
function validateConfig(config: Partial<LisaConfig>, logWarning: (msg: string) => void): LisaConfig {
|
|
1093
|
+
const result = deepMerge(DEFAULT_CONFIG, config)
|
|
1094
|
+
|
|
1095
|
+
// Validate execution.maxRetries
|
|
1096
|
+
if (typeof result.execution.maxRetries !== "number" || result.execution.maxRetries < 0) {
|
|
1097
|
+
logWarning(`Invalid execution.maxRetries: ${result.execution.maxRetries}. Using default: ${DEFAULT_CONFIG.execution.maxRetries}`)
|
|
1098
|
+
result.execution.maxRetries = DEFAULT_CONFIG.execution.maxRetries
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Validate git.completionMode
|
|
1102
|
+
const validModes: GitCompletionMode[] = ["pr", "commit", "none"]
|
|
1103
|
+
if (!validModes.includes(result.git.completionMode)) {
|
|
1104
|
+
logWarning(`Invalid git.completionMode: "${result.git.completionMode}". Using default: "${DEFAULT_CONFIG.git.completionMode}"`)
|
|
1105
|
+
result.git.completionMode = DEFAULT_CONFIG.git.completionMode
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Validate git.branchPrefix
|
|
1109
|
+
if (typeof result.git.branchPrefix !== "string" || result.git.branchPrefix.length === 0) {
|
|
1110
|
+
logWarning(`Invalid git.branchPrefix: "${result.git.branchPrefix}". Using default: "${DEFAULT_CONFIG.git.branchPrefix}"`)
|
|
1111
|
+
result.git.branchPrefix = DEFAULT_CONFIG.git.branchPrefix
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Validate git.autoPush
|
|
1115
|
+
if (typeof result.git.autoPush !== "boolean") {
|
|
1116
|
+
logWarning(`Invalid git.autoPush: ${result.git.autoPush}. Using default: ${DEFAULT_CONFIG.git.autoPush}`)
|
|
1117
|
+
result.git.autoPush = DEFAULT_CONFIG.git.autoPush
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Validate yolo.defaultMaxIterations
|
|
1121
|
+
if (typeof result.yolo.defaultMaxIterations !== "number" || result.yolo.defaultMaxIterations < 0) {
|
|
1122
|
+
logWarning(`Invalid yolo.defaultMaxIterations: ${result.yolo.defaultMaxIterations}. Using default: ${DEFAULT_CONFIG.yolo.defaultMaxIterations}`)
|
|
1123
|
+
result.yolo.defaultMaxIterations = DEFAULT_CONFIG.yolo.defaultMaxIterations
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return result
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Load config from a JSONC file
|
|
1131
|
+
*/
|
|
1132
|
+
async function loadConfigFile(path: string): Promise<Partial<LisaConfig> | null> {
|
|
1133
|
+
if (!existsSync(path)) return null
|
|
1134
|
+
|
|
1135
|
+
try {
|
|
1136
|
+
const content = await readFile(path, "utf-8")
|
|
1137
|
+
const stripped = stripJsonComments(content)
|
|
1138
|
+
return JSON.parse(stripped) as Partial<LisaConfig>
|
|
1139
|
+
} catch {
|
|
1140
|
+
return null
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Load and merge config from all sources
|
|
1146
|
+
* Order: global -> project -> project-local
|
|
1147
|
+
*/
|
|
1148
|
+
async function loadConfig(directory: string, logWarning: (msg: string) => void): Promise<LisaConfig> {
|
|
1149
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || ""
|
|
1150
|
+
|
|
1151
|
+
// Config file paths
|
|
1152
|
+
const globalConfigPath = join(homeDir, ".config", "lisa", "config.jsonc")
|
|
1153
|
+
const projectConfigPath = join(directory, ".lisa", "config.jsonc")
|
|
1154
|
+
const localConfigPath = join(directory, ".lisa", "config.local.jsonc")
|
|
1155
|
+
|
|
1156
|
+
// Load configs in order
|
|
1157
|
+
const globalConfig = await loadConfigFile(globalConfigPath)
|
|
1158
|
+
const projectConfig = await loadConfigFile(projectConfigPath)
|
|
1159
|
+
const localConfig = await loadConfigFile(localConfigPath)
|
|
1160
|
+
|
|
1161
|
+
// Merge configs
|
|
1162
|
+
let merged: Partial<LisaConfig> = {}
|
|
1163
|
+
|
|
1164
|
+
if (globalConfig) {
|
|
1165
|
+
merged = deepMerge(merged as LisaConfig, globalConfig)
|
|
1166
|
+
}
|
|
1167
|
+
if (projectConfig) {
|
|
1168
|
+
merged = deepMerge(merged as LisaConfig, projectConfig)
|
|
1169
|
+
}
|
|
1170
|
+
if (localConfig) {
|
|
1171
|
+
merged = deepMerge(merged as LisaConfig, localConfig)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Validate and return
|
|
1175
|
+
return validateConfig(merged, logWarning)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Ensure .lisa directory exists with config files
|
|
1180
|
+
*/
|
|
1181
|
+
async function ensureLisaDirectory(directory: string): Promise<{ created: boolean; configCreated: boolean }> {
|
|
1182
|
+
const lisaDir = join(directory, ".lisa")
|
|
1183
|
+
const configPath = join(lisaDir, "config.jsonc")
|
|
1184
|
+
const gitignorePath = join(lisaDir, ".gitignore")
|
|
1185
|
+
|
|
1186
|
+
let created = false
|
|
1187
|
+
let configCreated = false
|
|
1188
|
+
|
|
1189
|
+
// Create .lisa directory if needed
|
|
1190
|
+
if (!existsSync(lisaDir)) {
|
|
1191
|
+
const { mkdir } = await import("fs/promises")
|
|
1192
|
+
await mkdir(lisaDir, { recursive: true })
|
|
1193
|
+
created = true
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Create config.jsonc if it doesn't exist
|
|
1197
|
+
if (!existsSync(configPath)) {
|
|
1198
|
+
await writeFile(configPath, DEFAULT_CONFIG_CONTENT, "utf-8")
|
|
1199
|
+
configCreated = true
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Create .gitignore if it doesn't exist
|
|
1203
|
+
if (!existsSync(gitignorePath)) {
|
|
1204
|
+
await writeFile(gitignorePath, LISA_GITIGNORE_CONTENT, "utf-8")
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
return { created, configCreated }
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Get all task files for an epic, sorted by task number
|
|
1212
|
+
*/
|
|
1213
|
+
async function getTaskFiles(directory: string, epicName: string): Promise<string[]> {
|
|
1214
|
+
const tasksDir = join(directory, ".lisa", "epics", epicName, "tasks")
|
|
1215
|
+
|
|
1216
|
+
if (!existsSync(tasksDir)) return []
|
|
1217
|
+
|
|
1218
|
+
try {
|
|
1219
|
+
const files = await readdir(tasksDir)
|
|
1220
|
+
return files
|
|
1221
|
+
.filter((f) => f.endsWith(".md"))
|
|
1222
|
+
.sort((a, b) => {
|
|
1223
|
+
const numA = parseInt(a.match(/^(\d+)/)?.[1] || "0", 10)
|
|
1224
|
+
const numB = parseInt(b.match(/^(\d+)/)?.[1] || "0", 10)
|
|
1225
|
+
return numA - numB
|
|
1226
|
+
})
|
|
1227
|
+
} catch {
|
|
1228
|
+
return []
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Find the active epic with yolo mode enabled
|
|
1234
|
+
*/
|
|
1235
|
+
async function findActiveYoloEpic(
|
|
1236
|
+
directory: string
|
|
1237
|
+
): Promise<{ name: string; state: EpicState } | null> {
|
|
1238
|
+
const epicsDir = join(directory, ".lisa", "epics")
|
|
1239
|
+
|
|
1240
|
+
if (!existsSync(epicsDir)) return null
|
|
1241
|
+
|
|
1242
|
+
try {
|
|
1243
|
+
const entries = await readdir(epicsDir, { withFileTypes: true })
|
|
1244
|
+
|
|
1245
|
+
for (const entry of entries) {
|
|
1246
|
+
if (!entry.isDirectory()) continue
|
|
1247
|
+
|
|
1248
|
+
const statePath = join(epicsDir, entry.name, ".state")
|
|
1249
|
+
if (!existsSync(statePath)) continue
|
|
1250
|
+
|
|
1251
|
+
try {
|
|
1252
|
+
const content = await readFile(statePath, "utf-8")
|
|
1253
|
+
const state = JSON.parse(content) as EpicState
|
|
1254
|
+
|
|
1255
|
+
if (state.yolo?.active) {
|
|
1256
|
+
return { name: entry.name, state }
|
|
1257
|
+
}
|
|
1258
|
+
} catch {
|
|
1259
|
+
continue
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
} catch {
|
|
1263
|
+
return null
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
return null
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Count remaining tasks for an epic (pending or in-progress)
|
|
1271
|
+
*/
|
|
1272
|
+
async function countRemainingTasks(directory: string, epicName: string): Promise<number> {
|
|
1273
|
+
const tasksDir = join(directory, ".lisa", "epics", epicName, "tasks")
|
|
1274
|
+
|
|
1275
|
+
if (!existsSync(tasksDir)) return 0
|
|
1276
|
+
|
|
1277
|
+
try {
|
|
1278
|
+
const files = await readdir(tasksDir)
|
|
1279
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"))
|
|
1280
|
+
|
|
1281
|
+
let remaining = 0
|
|
1282
|
+
for (const file of mdFiles) {
|
|
1283
|
+
const content = await readFile(join(tasksDir, file), "utf-8")
|
|
1284
|
+
if (!content.includes("## Status: done") && !content.includes("## Status: blocked")) {
|
|
1285
|
+
remaining++
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return remaining
|
|
1289
|
+
} catch {
|
|
1290
|
+
return 0
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Update the epic's .state file
|
|
1296
|
+
*/
|
|
1297
|
+
async function updateEpicState(
|
|
1298
|
+
directory: string,
|
|
1299
|
+
epicName: string,
|
|
1300
|
+
updates: Partial<EpicState>
|
|
1301
|
+
): Promise<void> {
|
|
1302
|
+
const statePath = join(directory, ".lisa", "epics", epicName, ".state")
|
|
1303
|
+
|
|
1304
|
+
try {
|
|
1305
|
+
const content = await readFile(statePath, "utf-8")
|
|
1306
|
+
const state = JSON.parse(content) as EpicState
|
|
1307
|
+
|
|
1308
|
+
const newState = { ...state, ...updates, lastUpdated: new Date().toISOString() }
|
|
1309
|
+
|
|
1310
|
+
// Handle nested yolo updates
|
|
1311
|
+
if (updates.yolo && state.yolo) {
|
|
1312
|
+
newState.yolo = { ...state.yolo, ...updates.yolo }
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
await writeFile(statePath, JSON.stringify(newState, null, 2), "utf-8")
|
|
1316
|
+
} catch {
|
|
1317
|
+
// Ignore errors
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Send a desktop notification (cross-platform)
|
|
1323
|
+
* Fails silently if notifications aren't available
|
|
1324
|
+
*/
|
|
1325
|
+
async function notify($: any, title: string, message: string): Promise<void> {
|
|
1326
|
+
try {
|
|
1327
|
+
// macOS
|
|
1328
|
+
await $`osascript -e 'display notification "${message}" with title "${title}"'`.quiet()
|
|
1329
|
+
} catch {
|
|
1330
|
+
try {
|
|
1331
|
+
// Linux
|
|
1332
|
+
await $`notify-send "${title}" "${message}"`.quiet()
|
|
1333
|
+
} catch {
|
|
1334
|
+
// Silently fail - don't pollute the UI with console.log
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Get task statistics for an epic
|
|
1341
|
+
*/
|
|
1342
|
+
async function getTaskStats(
|
|
1343
|
+
directory: string,
|
|
1344
|
+
epicName: string
|
|
1345
|
+
): Promise<{ total: number; done: number; inProgress: number; pending: number; blocked: number }> {
|
|
1346
|
+
const tasksDir = join(directory, ".lisa", "epics", epicName, "tasks")
|
|
1347
|
+
|
|
1348
|
+
if (!existsSync(tasksDir)) {
|
|
1349
|
+
return { total: 0, done: 0, inProgress: 0, pending: 0, blocked: 0 }
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
try {
|
|
1353
|
+
const files = await readdir(tasksDir)
|
|
1354
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"))
|
|
1355
|
+
|
|
1356
|
+
let done = 0
|
|
1357
|
+
let inProgress = 0
|
|
1358
|
+
let pending = 0
|
|
1359
|
+
let blocked = 0
|
|
1360
|
+
|
|
1361
|
+
for (const file of mdFiles) {
|
|
1362
|
+
const content = await readFile(join(tasksDir, file), "utf-8")
|
|
1363
|
+
if (content.includes("## Status: done")) {
|
|
1364
|
+
done++
|
|
1365
|
+
} else if (content.includes("## Status: in-progress")) {
|
|
1366
|
+
inProgress++
|
|
1367
|
+
} else if (content.includes("## Status: blocked")) {
|
|
1368
|
+
blocked++
|
|
1369
|
+
} else {
|
|
1370
|
+
pending++
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
return { total: mdFiles.length, done, inProgress, pending, blocked }
|
|
1375
|
+
} catch {
|
|
1376
|
+
return { total: 0, done: 0, inProgress: 0, pending: 0, blocked: 0 }
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Parse dependencies from plan.md
|
|
1382
|
+
*/
|
|
1383
|
+
async function parseDependencies(
|
|
1384
|
+
directory: string,
|
|
1385
|
+
epicName: string
|
|
1386
|
+
): Promise<Map<string, string[]>> {
|
|
1387
|
+
const planPath = join(directory, ".lisa", "epics", epicName, "plan.md")
|
|
1388
|
+
const deps = new Map<string, string[]>()
|
|
1389
|
+
|
|
1390
|
+
if (!existsSync(planPath)) return deps
|
|
1391
|
+
|
|
1392
|
+
try {
|
|
1393
|
+
const content = await readFile(planPath, "utf-8")
|
|
1394
|
+
const depsMatch = content.match(/## Dependencies\n([\s\S]*?)(?=\n##|$)/)
|
|
1395
|
+
if (!depsMatch) return deps
|
|
1396
|
+
|
|
1397
|
+
const lines = depsMatch[1].trim().split("\n")
|
|
1398
|
+
for (const line of lines) {
|
|
1399
|
+
const match = line.match(/^-\s*(\d+):\s*\[(.*)\]/)
|
|
1400
|
+
if (match) {
|
|
1401
|
+
const taskId = match[1]
|
|
1402
|
+
const depList = match[2]
|
|
1403
|
+
.split(",")
|
|
1404
|
+
.map((d) => d.trim())
|
|
1405
|
+
.filter((d) => d.length > 0)
|
|
1406
|
+
deps.set(taskId, depList)
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
} catch {
|
|
1410
|
+
// Ignore errors
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
return deps
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// ============================================================================
|
|
1417
|
+
// Plugin
|
|
1418
|
+
// ============================================================================
|
|
1419
|
+
|
|
1420
|
+
export const LisaPlugin: Plugin = async ({ directory, client, $ }) => {
|
|
1421
|
+
// Register /lisa command programmatically
|
|
1422
|
+
// This replaces the external .opencode/command/lisa.md file
|
|
1423
|
+
if (client.registerCommand) {
|
|
1424
|
+
client.registerCommand({
|
|
1425
|
+
name: 'lisa',
|
|
1426
|
+
description: 'Lisa - intelligent epic workflow (/lisa help for commands)',
|
|
1427
|
+
handler: async (args: string[]) => {
|
|
1428
|
+
// Parse arguments like the command file would
|
|
1429
|
+
const input = args.join(' ').trim()
|
|
1430
|
+
|
|
1431
|
+
// If no args or help, return the help menu from SKILL.md
|
|
1432
|
+
if (!input || input === 'help') {
|
|
1433
|
+
return LISA_SKILL_CONTENT
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Otherwise, this would normally invoke the skill
|
|
1437
|
+
// For now, return a message directing to use the skill
|
|
1438
|
+
return `Lisa command received: "${input}". Use the lisa skill for full functionality.`
|
|
1439
|
+
}
|
|
1440
|
+
})
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return {
|
|
1444
|
+
// ========================================================================
|
|
1445
|
+
// Custom Tools
|
|
1446
|
+
// ========================================================================
|
|
1447
|
+
tool: {
|
|
1448
|
+
// ----------------------------------------------------------------------
|
|
1449
|
+
// list_epics - Fast listing of all epics
|
|
1450
|
+
// ----------------------------------------------------------------------
|
|
1451
|
+
list_epics: tool({
|
|
1452
|
+
description: `List all epics and their current status.
|
|
1453
|
+
|
|
1454
|
+
Returns a list of all epics in .lisa/epics/ with their phase and task progress.
|
|
1455
|
+
Much faster than manually reading files.`,
|
|
1456
|
+
args: {},
|
|
1457
|
+
async execute() {
|
|
1458
|
+
const epicsDir = join(directory, ".lisa", "epics")
|
|
1459
|
+
|
|
1460
|
+
if (!existsSync(epicsDir)) {
|
|
1461
|
+
return JSON.stringify({
|
|
1462
|
+
epics: [],
|
|
1463
|
+
message: "No epics found. Start one with `/lisa <name>`",
|
|
1464
|
+
}, null, 2)
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
try {
|
|
1468
|
+
const entries = await readdir(epicsDir, { withFileTypes: true })
|
|
1469
|
+
const epics: Array<{
|
|
1470
|
+
name: string
|
|
1471
|
+
phase: string
|
|
1472
|
+
tasks: { done: number; total: number } | null
|
|
1473
|
+
yoloActive: boolean
|
|
1474
|
+
}> = []
|
|
1475
|
+
|
|
1476
|
+
for (const entry of entries) {
|
|
1477
|
+
if (!entry.isDirectory()) continue
|
|
1478
|
+
|
|
1479
|
+
const statePath = join(epicsDir, entry.name, ".state")
|
|
1480
|
+
let phase = "unknown"
|
|
1481
|
+
let yoloActive = false
|
|
1482
|
+
|
|
1483
|
+
if (existsSync(statePath)) {
|
|
1484
|
+
try {
|
|
1485
|
+
const content = await readFile(statePath, "utf-8")
|
|
1486
|
+
const state = JSON.parse(content) as EpicState
|
|
1487
|
+
phase = state.currentPhase || "unknown"
|
|
1488
|
+
yoloActive = state.yolo?.active || false
|
|
1489
|
+
} catch {
|
|
1490
|
+
phase = "unknown"
|
|
1491
|
+
}
|
|
1492
|
+
} else {
|
|
1493
|
+
// No state file - check what exists
|
|
1494
|
+
const hasSpec = existsSync(join(epicsDir, entry.name, "spec.md"))
|
|
1495
|
+
const hasResearch = existsSync(join(epicsDir, entry.name, "research.md"))
|
|
1496
|
+
const hasPlan = existsSync(join(epicsDir, entry.name, "plan.md"))
|
|
1497
|
+
const hasTasks = existsSync(join(epicsDir, entry.name, "tasks"))
|
|
1498
|
+
|
|
1499
|
+
if (hasTasks) phase = "execute"
|
|
1500
|
+
else if (hasPlan) phase = "plan"
|
|
1501
|
+
else if (hasResearch) phase = "research"
|
|
1502
|
+
else if (hasSpec) phase = "spec"
|
|
1503
|
+
else phase = "new"
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Get task stats if in execute phase
|
|
1507
|
+
let tasks: { done: number; total: number } | null = null
|
|
1508
|
+
if (phase === "execute") {
|
|
1509
|
+
const stats = await getTaskStats(directory, entry.name)
|
|
1510
|
+
tasks = { done: stats.done, total: stats.total }
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
epics.push({ name: entry.name, phase, tasks, yoloActive })
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
return JSON.stringify({ epics }, null, 2)
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
return JSON.stringify({ epics: [], error: String(error) }, null, 2)
|
|
1519
|
+
}
|
|
1520
|
+
},
|
|
1521
|
+
}),
|
|
1522
|
+
|
|
1523
|
+
// ----------------------------------------------------------------------
|
|
1524
|
+
// get_epic_status - Detailed status for one epic
|
|
1525
|
+
// ----------------------------------------------------------------------
|
|
1526
|
+
get_epic_status: tool({
|
|
1527
|
+
description: `Get detailed status for a specific epic.
|
|
1528
|
+
|
|
1529
|
+
Returns phase, artifacts, task breakdown, and available actions.
|
|
1530
|
+
Much faster than manually reading multiple files.`,
|
|
1531
|
+
args: {
|
|
1532
|
+
epicName: tool.schema.string().describe("Name of the epic"),
|
|
1533
|
+
},
|
|
1534
|
+
async execute(args) {
|
|
1535
|
+
const { epicName } = args
|
|
1536
|
+
const epicDir = join(directory, ".lisa", "epics", epicName)
|
|
1537
|
+
|
|
1538
|
+
if (!existsSync(epicDir)) {
|
|
1539
|
+
return JSON.stringify({
|
|
1540
|
+
found: false,
|
|
1541
|
+
error: `Epic "${epicName}" not found. Start it with \`/lisa ${epicName}\``,
|
|
1542
|
+
}, null, 2)
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// Check which artifacts exist
|
|
1546
|
+
const artifacts = {
|
|
1547
|
+
spec: existsSync(join(epicDir, "spec.md")),
|
|
1548
|
+
research: existsSync(join(epicDir, "research.md")),
|
|
1549
|
+
plan: existsSync(join(epicDir, "plan.md")),
|
|
1550
|
+
tasks: existsSync(join(epicDir, "tasks")),
|
|
1551
|
+
state: existsSync(join(epicDir, ".state")),
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Read state
|
|
1555
|
+
let state: EpicState | null = null
|
|
1556
|
+
if (artifacts.state) {
|
|
1557
|
+
try {
|
|
1558
|
+
const content = await readFile(join(epicDir, ".state"), "utf-8")
|
|
1559
|
+
state = JSON.parse(content)
|
|
1560
|
+
} catch {
|
|
1561
|
+
state = null
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Get task stats
|
|
1566
|
+
const taskStats = await getTaskStats(directory, epicName)
|
|
1567
|
+
|
|
1568
|
+
// Determine current phase
|
|
1569
|
+
let currentPhase = state?.currentPhase || "unknown"
|
|
1570
|
+
if (currentPhase === "unknown") {
|
|
1571
|
+
if (artifacts.tasks) currentPhase = "execute"
|
|
1572
|
+
else if (artifacts.plan) currentPhase = "plan"
|
|
1573
|
+
else if (artifacts.research) currentPhase = "research"
|
|
1574
|
+
else if (artifacts.spec) currentPhase = "spec"
|
|
1575
|
+
else currentPhase = "new"
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// Determine next action
|
|
1579
|
+
let nextAction = ""
|
|
1580
|
+
if (!artifacts.spec) {
|
|
1581
|
+
nextAction = `Create spec with \`/lisa ${epicName} spec\``
|
|
1582
|
+
} else if (!artifacts.research) {
|
|
1583
|
+
nextAction = `Run \`/lisa ${epicName}\` to start research`
|
|
1584
|
+
} else if (!artifacts.plan) {
|
|
1585
|
+
nextAction = `Run \`/lisa ${epicName}\` to create plan`
|
|
1586
|
+
} else if (taskStats.pending > 0 || taskStats.inProgress > 0) {
|
|
1587
|
+
nextAction = `Run \`/lisa ${epicName}\` to continue execution or \`/lisa ${epicName} yolo\` for auto mode`
|
|
1588
|
+
} else if (taskStats.blocked > 0) {
|
|
1589
|
+
nextAction = `${taskStats.blocked} task(s) blocked - review and unblock`
|
|
1590
|
+
} else {
|
|
1591
|
+
nextAction = "Epic complete!"
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
return JSON.stringify({
|
|
1595
|
+
found: true,
|
|
1596
|
+
name: epicName,
|
|
1597
|
+
currentPhase,
|
|
1598
|
+
artifacts,
|
|
1599
|
+
tasks: taskStats,
|
|
1600
|
+
yolo: state?.yolo || null,
|
|
1601
|
+
lastUpdated: state?.lastUpdated || null,
|
|
1602
|
+
nextAction,
|
|
1603
|
+
}, null, 2)
|
|
1604
|
+
},
|
|
1605
|
+
}),
|
|
1606
|
+
|
|
1607
|
+
// ----------------------------------------------------------------------
|
|
1608
|
+
// get_available_tasks - Tasks ready to execute
|
|
1609
|
+
// ----------------------------------------------------------------------
|
|
1610
|
+
get_available_tasks: tool({
|
|
1611
|
+
description: `Get tasks that are available to execute (dependencies satisfied).
|
|
1612
|
+
|
|
1613
|
+
Returns tasks that are pending/in-progress and have all dependencies completed.`,
|
|
1614
|
+
args: {
|
|
1615
|
+
epicName: tool.schema.string().describe("Name of the epic"),
|
|
1616
|
+
},
|
|
1617
|
+
async execute(args) {
|
|
1618
|
+
const { epicName } = args
|
|
1619
|
+
const epicDir = join(directory, ".lisa", "epics", epicName)
|
|
1620
|
+
const tasksDir = join(epicDir, "tasks")
|
|
1621
|
+
|
|
1622
|
+
if (!existsSync(tasksDir)) {
|
|
1623
|
+
return JSON.stringify({
|
|
1624
|
+
available: [],
|
|
1625
|
+
blocked: [],
|
|
1626
|
+
message: "No tasks directory found",
|
|
1627
|
+
}, null, 2)
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// Get all task files
|
|
1631
|
+
const taskFiles = await getTaskFiles(directory, epicName)
|
|
1632
|
+
if (taskFiles.length === 0) {
|
|
1633
|
+
return JSON.stringify({
|
|
1634
|
+
available: [],
|
|
1635
|
+
blocked: [],
|
|
1636
|
+
message: "No task files found",
|
|
1637
|
+
}, null, 2)
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// Parse dependencies
|
|
1641
|
+
const dependencies = await parseDependencies(directory, epicName)
|
|
1642
|
+
|
|
1643
|
+
// Read task statuses
|
|
1644
|
+
const taskStatuses = new Map<string, string>()
|
|
1645
|
+
for (const file of taskFiles) {
|
|
1646
|
+
const taskId = file.match(/^(\d+)/)?.[1] || ""
|
|
1647
|
+
const content = await readFile(join(tasksDir, file), "utf-8")
|
|
1648
|
+
|
|
1649
|
+
if (content.includes("## Status: done")) {
|
|
1650
|
+
taskStatuses.set(taskId, "done")
|
|
1651
|
+
} else if (content.includes("## Status: in-progress")) {
|
|
1652
|
+
taskStatuses.set(taskId, "in-progress")
|
|
1653
|
+
} else if (content.includes("## Status: blocked")) {
|
|
1654
|
+
taskStatuses.set(taskId, "blocked")
|
|
1655
|
+
} else {
|
|
1656
|
+
taskStatuses.set(taskId, "pending")
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Determine which tasks are available
|
|
1661
|
+
const available: Array<{ taskId: string; file: string; status: string }> = []
|
|
1662
|
+
const blocked: Array<{ taskId: string; file: string; blockedBy: string[] }> = []
|
|
1663
|
+
|
|
1664
|
+
for (const file of taskFiles) {
|
|
1665
|
+
const taskId = file.match(/^(\d+)/)?.[1] || ""
|
|
1666
|
+
const status = taskStatuses.get(taskId) || "pending"
|
|
1667
|
+
|
|
1668
|
+
// Skip done or blocked tasks
|
|
1669
|
+
if (status === "done" || status === "blocked") continue
|
|
1670
|
+
|
|
1671
|
+
// Check dependencies
|
|
1672
|
+
const deps = dependencies.get(taskId) || []
|
|
1673
|
+
const unmetDeps = deps.filter((depId) => taskStatuses.get(depId) !== "done")
|
|
1674
|
+
|
|
1675
|
+
if (unmetDeps.length === 0) {
|
|
1676
|
+
available.push({ taskId, file, status })
|
|
1677
|
+
} else {
|
|
1678
|
+
blocked.push({ taskId, file, blockedBy: unmetDeps })
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
return JSON.stringify({ available, blocked }, null, 2)
|
|
1683
|
+
},
|
|
1684
|
+
}),
|
|
1685
|
+
|
|
1686
|
+
// ----------------------------------------------------------------------
|
|
1687
|
+
// build_task_context - Build context for task execution
|
|
1688
|
+
// ----------------------------------------------------------------------
|
|
1689
|
+
build_task_context: tool({
|
|
1690
|
+
description: `Build the full context for executing an epic task.
|
|
1691
|
+
|
|
1692
|
+
This tool reads the epic's spec, research, plan, and all previous completed tasks,
|
|
1693
|
+
then returns a complete prompt that should be passed to the Task tool to execute
|
|
1694
|
+
the task with a fresh sub-agent.
|
|
1695
|
+
|
|
1696
|
+
Use this before calling the Task tool for each task execution.`,
|
|
1697
|
+
args: {
|
|
1698
|
+
epicName: tool.schema.string().describe("Name of the epic (the folder name under .lisa/epics/)"),
|
|
1699
|
+
taskId: tool.schema
|
|
1700
|
+
.string()
|
|
1701
|
+
.describe("Task ID - the number prefix like '01', '02', etc."),
|
|
1702
|
+
},
|
|
1703
|
+
async execute(args) {
|
|
1704
|
+
const { epicName, taskId } = args
|
|
1705
|
+
const epicDir = join(directory, ".lisa", "epics", epicName)
|
|
1706
|
+
const tasksDir = join(epicDir, "tasks")
|
|
1707
|
+
|
|
1708
|
+
// Verify epic exists
|
|
1709
|
+
if (!existsSync(epicDir)) {
|
|
1710
|
+
return JSON.stringify({
|
|
1711
|
+
success: false,
|
|
1712
|
+
error: `Epic "${epicName}" not found at ${epicDir}`,
|
|
1713
|
+
}, null, 2)
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// Read context files
|
|
1717
|
+
const spec = await readFileIfExists(join(epicDir, "spec.md"))
|
|
1718
|
+
const research = await readFileIfExists(join(epicDir, "research.md"))
|
|
1719
|
+
const plan = await readFileIfExists(join(epicDir, "plan.md"))
|
|
1720
|
+
|
|
1721
|
+
if (!spec) {
|
|
1722
|
+
return JSON.stringify({
|
|
1723
|
+
success: false,
|
|
1724
|
+
error: `No spec.md found for epic "${epicName}"`,
|
|
1725
|
+
}, null, 2)
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// Find the task file
|
|
1729
|
+
const taskFiles = await getTaskFiles(directory, epicName)
|
|
1730
|
+
const taskFile = taskFiles.find((f) => f.startsWith(taskId))
|
|
1731
|
+
|
|
1732
|
+
if (!taskFile) {
|
|
1733
|
+
return JSON.stringify({
|
|
1734
|
+
success: false,
|
|
1735
|
+
error: `Task "${taskId}" not found in ${tasksDir}`,
|
|
1736
|
+
}, null, 2)
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
const taskPath = join(tasksDir, taskFile)
|
|
1740
|
+
const taskContent = await readFile(taskPath, "utf-8")
|
|
1741
|
+
|
|
1742
|
+
// Check if task is already done
|
|
1743
|
+
if (taskContent.includes("## Status: done")) {
|
|
1744
|
+
return JSON.stringify({
|
|
1745
|
+
success: true,
|
|
1746
|
+
alreadyDone: true,
|
|
1747
|
+
message: `Task ${taskId} is already complete`,
|
|
1748
|
+
}, null, 2)
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// Read all previous task files (for context)
|
|
1752
|
+
const previousTasks: string[] = []
|
|
1753
|
+
for (const file of taskFiles) {
|
|
1754
|
+
const fileTaskId = file.match(/^(\d+)/)?.[1] || ""
|
|
1755
|
+
if (fileTaskId >= taskId) break // Stop at current task
|
|
1756
|
+
|
|
1757
|
+
const content = await readFile(join(tasksDir, file), "utf-8")
|
|
1758
|
+
previousTasks.push(`### ${file}\n\n${content}`)
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Build the sub-agent prompt
|
|
1762
|
+
const prompt = `# Execute Epic Task
|
|
1763
|
+
|
|
1764
|
+
You are executing task ${taskId} of epic "${epicName}".
|
|
1765
|
+
|
|
1766
|
+
## Your Mission
|
|
1767
|
+
|
|
1768
|
+
Execute the task described below. When complete:
|
|
1769
|
+
1. Update the task file's status from "pending" or "in-progress" to "done"
|
|
1770
|
+
2. Add a "## Report" section at the end of the task file with:
|
|
1771
|
+
- **What Was Done**: List the changes you made
|
|
1772
|
+
- **Decisions Made**: Any choices you made and why
|
|
1773
|
+
- **Issues / Notes for Next Task**: Anything the next task should know
|
|
1774
|
+
- **Files Changed**: List of files created/modified
|
|
1775
|
+
|
|
1776
|
+
If you discover the task approach is wrong or future tasks need changes, you may update them.
|
|
1777
|
+
The plan is a living document.
|
|
1778
|
+
|
|
1779
|
+
---
|
|
1780
|
+
|
|
1781
|
+
## Epic Spec
|
|
1782
|
+
|
|
1783
|
+
${spec}
|
|
1784
|
+
|
|
1785
|
+
---
|
|
1786
|
+
|
|
1787
|
+
## Research
|
|
1788
|
+
|
|
1789
|
+
${research || "(No research conducted yet)"}
|
|
1790
|
+
|
|
1791
|
+
---
|
|
1792
|
+
|
|
1793
|
+
## Plan
|
|
1794
|
+
|
|
1795
|
+
${plan || "(No plan created yet)"}
|
|
1796
|
+
|
|
1797
|
+
---
|
|
1798
|
+
|
|
1799
|
+
## Previous Completed Tasks
|
|
1800
|
+
|
|
1801
|
+
${previousTasks.length > 0 ? previousTasks.join("\n\n---\n\n") : "(This is the first task)"}
|
|
1802
|
+
|
|
1803
|
+
---
|
|
1804
|
+
|
|
1805
|
+
## Current Task to Execute
|
|
1806
|
+
|
|
1807
|
+
**File: .lisa/epics/${epicName}/tasks/${taskFile}**
|
|
1808
|
+
|
|
1809
|
+
${taskContent}
|
|
1810
|
+
|
|
1811
|
+
---
|
|
1812
|
+
|
|
1813
|
+
## Instructions
|
|
1814
|
+
|
|
1815
|
+
1. Read and understand the task
|
|
1816
|
+
2. Execute the steps described
|
|
1817
|
+
3. Verify the "Done When" criteria are met
|
|
1818
|
+
4. Update the task file:
|
|
1819
|
+
- Change \`## Status: pending\` or \`## Status: in-progress\` to \`## Status: done\`
|
|
1820
|
+
- Add a \`## Report\` section at the end
|
|
1821
|
+
5. If you need to modify future tasks or the plan, do so
|
|
1822
|
+
6. When complete, confirm what was done
|
|
1823
|
+
`
|
|
1824
|
+
|
|
1825
|
+
await client.app.log({
|
|
1826
|
+
service: "lisa-plugin",
|
|
1827
|
+
level: "info",
|
|
1828
|
+
message: `Built context for task ${taskId} of epic "${epicName}" (${previousTasks.length} previous tasks)`,
|
|
1829
|
+
})
|
|
1830
|
+
|
|
1831
|
+
return JSON.stringify({
|
|
1832
|
+
success: true,
|
|
1833
|
+
taskFile,
|
|
1834
|
+
taskPath,
|
|
1835
|
+
prompt,
|
|
1836
|
+
message: `Context built for task ${taskId}. Pass the 'prompt' field to the Task tool to execute with a sub-agent.`,
|
|
1837
|
+
}, null, 2)
|
|
1838
|
+
},
|
|
1839
|
+
}),
|
|
1840
|
+
|
|
1841
|
+
// ----------------------------------------------------------------------
|
|
1842
|
+
// lisa_config - View and manage Lisa configuration
|
|
1843
|
+
// ----------------------------------------------------------------------
|
|
1844
|
+
lisa_config: tool({
|
|
1845
|
+
description: `View or reset Lisa configuration.
|
|
1846
|
+
|
|
1847
|
+
Actions:
|
|
1848
|
+
- "view": Show current merged configuration and where values come from
|
|
1849
|
+
- "reset": Reset project config to defaults (creates .lisa/config.jsonc)
|
|
1850
|
+
- "init": Initialize config if it doesn't exist (non-destructive)`,
|
|
1851
|
+
args: {
|
|
1852
|
+
action: tool.schema.enum(["view", "reset", "init"]).describe("Action to perform"),
|
|
1853
|
+
},
|
|
1854
|
+
async execute(args) {
|
|
1855
|
+
const { action } = args
|
|
1856
|
+
const lisaDir = join(directory, ".lisa")
|
|
1857
|
+
const configPath = join(lisaDir, "config.jsonc")
|
|
1858
|
+
const localConfigPath = join(lisaDir, "config.local.jsonc")
|
|
1859
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || ""
|
|
1860
|
+
const globalConfigPath = join(homeDir, ".config", "lisa", "config.jsonc")
|
|
1861
|
+
|
|
1862
|
+
const logWarning = (msg: string) => {
|
|
1863
|
+
client.app.log({
|
|
1864
|
+
service: "lisa-plugin",
|
|
1865
|
+
level: "warn",
|
|
1866
|
+
message: msg,
|
|
1867
|
+
})
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
if (action === "view") {
|
|
1871
|
+
// Load config and show sources
|
|
1872
|
+
const config = await loadConfig(directory, logWarning)
|
|
1873
|
+
|
|
1874
|
+
const sources: string[] = []
|
|
1875
|
+
if (existsSync(globalConfigPath)) sources.push(`Global: ${globalConfigPath}`)
|
|
1876
|
+
if (existsSync(configPath)) sources.push(`Project: ${configPath}`)
|
|
1877
|
+
if (existsSync(localConfigPath)) sources.push(`Local: ${localConfigPath}`)
|
|
1878
|
+
if (sources.length === 0) sources.push("(Using defaults - no config files found)")
|
|
1879
|
+
|
|
1880
|
+
return JSON.stringify({
|
|
1881
|
+
config,
|
|
1882
|
+
sources,
|
|
1883
|
+
paths: {
|
|
1884
|
+
global: globalConfigPath,
|
|
1885
|
+
project: configPath,
|
|
1886
|
+
local: localConfigPath,
|
|
1887
|
+
},
|
|
1888
|
+
}, null, 2)
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
if (action === "reset") {
|
|
1892
|
+
// Ensure directory exists and reset config
|
|
1893
|
+
const { mkdir } = await import("fs/promises")
|
|
1894
|
+
if (!existsSync(lisaDir)) {
|
|
1895
|
+
await mkdir(lisaDir, { recursive: true })
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
await writeFile(configPath, DEFAULT_CONFIG_CONTENT, "utf-8")
|
|
1899
|
+
|
|
1900
|
+
// Also ensure .gitignore exists
|
|
1901
|
+
const gitignorePath = join(lisaDir, ".gitignore")
|
|
1902
|
+
if (!existsSync(gitignorePath)) {
|
|
1903
|
+
await writeFile(gitignorePath, LISA_GITIGNORE_CONTENT, "utf-8")
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
return JSON.stringify({
|
|
1907
|
+
success: true,
|
|
1908
|
+
message: "Config reset to defaults",
|
|
1909
|
+
path: configPath,
|
|
1910
|
+
tip: "Edit .lisa/config.jsonc to customize settings. Create .lisa/config.local.jsonc for personal overrides (gitignored).",
|
|
1911
|
+
}, null, 2)
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
if (action === "init") {
|
|
1915
|
+
const result = await ensureLisaDirectory(directory)
|
|
1916
|
+
|
|
1917
|
+
if (result.configCreated) {
|
|
1918
|
+
return JSON.stringify({
|
|
1919
|
+
success: true,
|
|
1920
|
+
message: "Config initialized with defaults",
|
|
1921
|
+
path: configPath,
|
|
1922
|
+
tip: "Edit .lisa/config.jsonc to customize settings. Create .lisa/config.local.jsonc for personal overrides (gitignored).",
|
|
1923
|
+
}, null, 2)
|
|
1924
|
+
} else {
|
|
1925
|
+
return JSON.stringify({
|
|
1926
|
+
success: true,
|
|
1927
|
+
message: "Config already exists",
|
|
1928
|
+
path: configPath,
|
|
1929
|
+
tip: "Use action 'reset' to overwrite with defaults, or 'view' to see current config.",
|
|
1930
|
+
}, null, 2)
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
return JSON.stringify({ success: false, error: `Unknown action: ${action}` }, null, 2)
|
|
1935
|
+
},
|
|
1936
|
+
}),
|
|
1937
|
+
|
|
1938
|
+
// ----------------------------------------------------------------------
|
|
1939
|
+
// get_lisa_config - Get current config for use by other tools/skills
|
|
1940
|
+
// ----------------------------------------------------------------------
|
|
1941
|
+
get_lisa_config: tool({
|
|
1942
|
+
description: `Get the current Lisa configuration.
|
|
1943
|
+
|
|
1944
|
+
Returns the merged configuration from all sources (global, project, local).
|
|
1945
|
+
Use this to check settings like git.completionMode before performing actions.`,
|
|
1946
|
+
args: {},
|
|
1947
|
+
async execute() {
|
|
1948
|
+
const logWarning = (msg: string) => {
|
|
1949
|
+
client.app.log({
|
|
1950
|
+
service: "lisa-plugin",
|
|
1951
|
+
level: "warn",
|
|
1952
|
+
message: msg,
|
|
1953
|
+
})
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
const config = await loadConfig(directory, logWarning)
|
|
1957
|
+
return JSON.stringify({ config }, null, 2)
|
|
1958
|
+
},
|
|
1959
|
+
}),
|
|
1960
|
+
},
|
|
1961
|
+
|
|
1962
|
+
// ========================================================================
|
|
1963
|
+
// Event Handler: Yolo Mode Auto-Continue
|
|
1964
|
+
// ========================================================================
|
|
1965
|
+
event: async ({ event }) => {
|
|
1966
|
+
if (event.type !== "session.idle") return
|
|
1967
|
+
|
|
1968
|
+
const sessionId = (event as any).properties?.sessionID
|
|
1969
|
+
|
|
1970
|
+
// Debug: log the event
|
|
1971
|
+
await client.app.log({
|
|
1972
|
+
service: "lisa-plugin",
|
|
1973
|
+
level: "info",
|
|
1974
|
+
message: `session.idle event received. sessionId: ${sessionId || "UNDEFINED"}`,
|
|
1975
|
+
})
|
|
1976
|
+
|
|
1977
|
+
// Find active yolo epic
|
|
1978
|
+
const activeEpic = await findActiveYoloEpic(directory)
|
|
1979
|
+
if (!activeEpic) {
|
|
1980
|
+
await client.app.log({
|
|
1981
|
+
service: "lisa-plugin",
|
|
1982
|
+
level: "info",
|
|
1983
|
+
message: "No active yolo epic found",
|
|
1984
|
+
})
|
|
1985
|
+
return
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
const { name: epicName, state } = activeEpic
|
|
1989
|
+
const yolo = state.yolo!
|
|
1990
|
+
|
|
1991
|
+
// Check remaining tasks
|
|
1992
|
+
const remaining = await countRemainingTasks(directory, epicName)
|
|
1993
|
+
|
|
1994
|
+
// Log progress
|
|
1995
|
+
await client.app.log({
|
|
1996
|
+
service: "lisa-plugin",
|
|
1997
|
+
level: "info",
|
|
1998
|
+
message: `Epic "${epicName}" yolo check: ${remaining} tasks remaining, iteration ${yolo.iteration}/${yolo.maxIterations || "unlimited"}`,
|
|
1999
|
+
})
|
|
2000
|
+
|
|
2001
|
+
// Check if complete
|
|
2002
|
+
if (remaining === 0) {
|
|
2003
|
+
await updateEpicState(directory, epicName, {
|
|
2004
|
+
executeComplete: true,
|
|
2005
|
+
yolo: { ...yolo, active: false },
|
|
2006
|
+
})
|
|
2007
|
+
|
|
2008
|
+
await notify($, "Lisa Complete", `Epic "${epicName}" finished successfully!`)
|
|
2009
|
+
|
|
2010
|
+
await client.app.log({
|
|
2011
|
+
service: "lisa-plugin",
|
|
2012
|
+
level: "info",
|
|
2013
|
+
message: `Epic "${epicName}" completed! All tasks done.`,
|
|
2014
|
+
})
|
|
2015
|
+
|
|
2016
|
+
return
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Check max iterations
|
|
2020
|
+
if (yolo.maxIterations > 0 && yolo.iteration >= yolo.maxIterations) {
|
|
2021
|
+
await updateEpicState(directory, epicName, {
|
|
2022
|
+
yolo: { ...yolo, active: false },
|
|
2023
|
+
})
|
|
2024
|
+
|
|
2025
|
+
await notify(
|
|
2026
|
+
$,
|
|
2027
|
+
"Lisa Stopped",
|
|
2028
|
+
`Epic "${epicName}" hit max iterations (${yolo.maxIterations})`
|
|
2029
|
+
)
|
|
2030
|
+
|
|
2031
|
+
await client.app.log({
|
|
2032
|
+
service: "lisa-plugin",
|
|
2033
|
+
level: "warn",
|
|
2034
|
+
message: `Epic "${epicName}" stopped: max iterations (${yolo.maxIterations}) reached with ${remaining} tasks remaining`,
|
|
2035
|
+
})
|
|
2036
|
+
|
|
2037
|
+
return
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// Continue the epic
|
|
2041
|
+
const nextIteration = yolo.iteration + 1
|
|
2042
|
+
await updateEpicState(directory, epicName, {
|
|
2043
|
+
yolo: { ...yolo, iteration: nextIteration },
|
|
2044
|
+
})
|
|
2045
|
+
|
|
2046
|
+
// Send continuation prompt
|
|
2047
|
+
if (sessionId) {
|
|
2048
|
+
await client.app.log({
|
|
2049
|
+
service: "lisa-plugin",
|
|
2050
|
+
level: "info",
|
|
2051
|
+
message: `Sending continuation prompt for "${epicName}" to session ${sessionId}`,
|
|
2052
|
+
})
|
|
2053
|
+
|
|
2054
|
+
// Build a forceful continuation prompt that leaves no ambiguity
|
|
2055
|
+
const continuationPrompt = `[LISA YOLO MODE - AUTO-CONTINUE]
|
|
2056
|
+
|
|
2057
|
+
Epic: ${epicName}
|
|
2058
|
+
Tasks remaining: ${remaining}
|
|
2059
|
+
Iteration: ${nextIteration}${yolo.maxIterations > 0 ? ` of ${yolo.maxIterations}` : ""}
|
|
2060
|
+
|
|
2061
|
+
MANDATORY ACTIONS:
|
|
2062
|
+
1. Load the lisa skill
|
|
2063
|
+
2. Call get_available_tasks("${epicName}") to see ready tasks
|
|
2064
|
+
3. For each available task, call build_task_context then Task tool
|
|
2065
|
+
4. Continue until ALL tasks are done
|
|
2066
|
+
|
|
2067
|
+
RULES:
|
|
2068
|
+
- Do NOT stop to summarize
|
|
2069
|
+
- Do NOT ask for confirmation
|
|
2070
|
+
- Do NOT explain what you're about to do
|
|
2071
|
+
- Just execute the next task immediately
|
|
2072
|
+
|
|
2073
|
+
This is automated execution. Keep working.`
|
|
2074
|
+
|
|
2075
|
+
try {
|
|
2076
|
+
await client.session.send({
|
|
2077
|
+
id: sessionId,
|
|
2078
|
+
text: continuationPrompt,
|
|
2079
|
+
})
|
|
2080
|
+
|
|
2081
|
+
await client.app.log({
|
|
2082
|
+
service: "lisa-plugin",
|
|
2083
|
+
level: "info",
|
|
2084
|
+
message: `Epic "${epicName}" continuing: iteration ${nextIteration}, ${remaining} tasks remaining`,
|
|
2085
|
+
})
|
|
2086
|
+
} catch (err) {
|
|
2087
|
+
await client.app.log({
|
|
2088
|
+
service: "lisa-plugin",
|
|
2089
|
+
level: "error",
|
|
2090
|
+
message: `Failed to send continuation: ${err}`,
|
|
2091
|
+
})
|
|
2092
|
+
}
|
|
2093
|
+
} else {
|
|
2094
|
+
await client.app.log({
|
|
2095
|
+
service: "lisa-plugin",
|
|
2096
|
+
level: "warn",
|
|
2097
|
+
message: `No sessionId available - cannot continue epic "${epicName}"`,
|
|
2098
|
+
})
|
|
2099
|
+
}
|
|
2100
|
+
},
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// Default export for OpenCode plugin loading
|
|
2105
|
+
export default LisaPlugin
|