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.
@@ -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