opencode-auto-loop 0.1.5 → 0.1.7
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/commands/auto-loop.md +5 -1
- package/package.json +1 -1
- package/skills/auto-loop/SKILL.md +13 -1
- package/skills/auto-loop-help/SKILL.md +10 -1
- package/skills/cancel-auto-loop/SKILL.md +6 -20
- package/src/index.ts +237 -114
package/commands/auto-loop.md
CHANGED
|
@@ -4,20 +4,24 @@ description: "Start Auto Loop - auto-continues until task completion. Use: /auto
|
|
|
4
4
|
|
|
5
5
|
# Auto Loop
|
|
6
6
|
|
|
7
|
-
Parse `$ARGUMENTS` for the task description and
|
|
7
|
+
Parse `$ARGUMENTS` for the task description and optional flags:
|
|
8
8
|
|
|
9
9
|
- If `$ARGUMENTS` contains `--max <number>`, extract that number as **maxIterations** and remove it from the task string.
|
|
10
10
|
- Otherwise, use **maxIterations**: 25
|
|
11
|
+
- If `$ARGUMENTS` contains `--ralph`, set **forceLoop** to `true` and remove it from the task string. Force mode ignores all completion signals and runs for the full maxIterations.
|
|
11
12
|
|
|
12
13
|
Invoke the `auto-loop` tool with:
|
|
13
14
|
|
|
14
15
|
- **task**: the extracted task description
|
|
15
16
|
- **maxIterations**: the extracted or default value
|
|
17
|
+
- **forceLoop**: `true` if `--ralph` was present, omit otherwise
|
|
16
18
|
|
|
17
19
|
Examples:
|
|
18
20
|
- `/auto-loop Build a REST API` → task="Build a REST API", maxIterations=25
|
|
19
21
|
- `/auto-loop Build a REST API --max 50` → task="Build a REST API", maxIterations=50
|
|
20
22
|
- `/auto-loop --max 10 Fix all lint errors` → task="Fix all lint errors", maxIterations=10
|
|
23
|
+
- `/auto-loop --ralph Fix all lint errors` → task="Fix all lint errors", maxIterations=25, forceLoop=true
|
|
24
|
+
- `/auto-loop --ralph --max 10 Fix all lint errors` → task="Fix all lint errors", maxIterations=10, forceLoop=true
|
|
21
25
|
|
|
22
26
|
After the tool confirms the loop is active, **immediately begin working on the task**. Do not just acknowledge — start doing the work right away.
|
|
23
27
|
|
package/package.json
CHANGED
|
@@ -21,7 +21,9 @@ Your previous work remains accessible through files, git history, and the state
|
|
|
21
21
|
|
|
22
22
|
## Starting the Loop
|
|
23
23
|
|
|
24
|
-
**
|
|
24
|
+
**Use the `auto-loop` tool ONCE to start the loop.** Do NOT create the state file manually. The tool handles state file creation, session tracking, and initialization.
|
|
25
|
+
|
|
26
|
+
**CRITICAL: Do NOT call the `auto-loop` tool more than once per loop.** The plugin handles all subsequent iterations automatically via idle detection. Re-invoking the tool would be rejected to prevent accidental state resets. If you need to restart with different parameters, use `/cancel-auto-loop` first.
|
|
25
27
|
|
|
26
28
|
After the tool confirms the loop is active, **immediately begin working on the task**. Do not just acknowledge — start doing the work.
|
|
27
29
|
|
|
@@ -78,6 +80,16 @@ The loop can only be stopped by:
|
|
|
78
80
|
2. Max iterations reached
|
|
79
81
|
3. User running `/cancel-auto-loop`
|
|
80
82
|
|
|
83
|
+
## Force Mode (--ralph)
|
|
84
|
+
|
|
85
|
+
When started with `--ralph`, the loop ignores ALL completion signals (`<promise>DONE</promise>`, `STATUS: COMPLETE`) and runs for the full iteration count. In force mode:
|
|
86
|
+
- You do NOT need to output `STATUS:` lines or the DONE signal
|
|
87
|
+
- The loop will continue for all iterations regardless
|
|
88
|
+
- Focus on making steady progress each iteration
|
|
89
|
+
- Still output `## Completed` and `## Next Steps` sections so the plugin can track progress
|
|
90
|
+
|
|
91
|
+
Example: `/auto-loop --ralph --max 10 Continue the refactoring task`
|
|
92
|
+
|
|
81
93
|
## Checking Status
|
|
82
94
|
|
|
83
95
|
Check current iteration and progress:
|
|
@@ -19,6 +19,15 @@ Example:
|
|
|
19
19
|
|
|
20
20
|
The AI will work on your task and automatically continue until completion.
|
|
21
21
|
|
|
22
|
+
### `/auto-loop <task> --ralph`
|
|
23
|
+
Force mode: ignore all completion signals and run for the full iteration count. Useful when you got interrupted and want to resume, or when you want the AI to keep iterating without stopping early.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
```
|
|
27
|
+
/auto-loop --ralph Continue the refactoring
|
|
28
|
+
/auto-loop --ralph --max 10 Fix all lint errors
|
|
29
|
+
```
|
|
30
|
+
|
|
22
31
|
### `/cancel-auto-loop`
|
|
23
32
|
Cancel an active Auto Loop before it completes.
|
|
24
33
|
|
|
@@ -35,7 +44,7 @@ Show plugin help and available commands.
|
|
|
35
44
|
1. **Start**: `/auto-loop` creates a state file at `.opencode/auto-loop.local.md`
|
|
36
45
|
2. **Loop**: When the AI goes idle, the plugin checks if `<promise>DONE</promise>` was output
|
|
37
46
|
3. **Continue**: If not found, it injects "Continue from where you left off"
|
|
38
|
-
4. **Stop**: Loop continues until DONE is found or max iterations (
|
|
47
|
+
4. **Stop**: Loop continues until DONE is found or max iterations (default: 25) reached
|
|
39
48
|
5. **Cleanup**: State file is deleted when complete
|
|
40
49
|
6. **Compaction**: Loop context survives session compaction — task and iteration info is preserved
|
|
41
50
|
|
|
@@ -9,33 +9,19 @@ Stop an active Auto Loop before completion.
|
|
|
9
9
|
|
|
10
10
|
## How to Use
|
|
11
11
|
|
|
12
|
-
When you invoke
|
|
12
|
+
When you need to cancel the loop, invoke the `cancel-auto-loop` tool. The tool will:
|
|
13
13
|
|
|
14
|
-
1.
|
|
14
|
+
1. Check if a loop is currently active
|
|
15
|
+
2. Report how many iterations were completed
|
|
16
|
+
3. Clean up the state file
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
test -f .opencode/auto-loop.local.md && echo "Loop is active" || echo "No active loop"
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
2. If active, read the current iteration count:
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
grep '^iteration:' .opencode/auto-loop.local.md
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
3. Delete the state file to stop the loop:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
rm -f .opencode/auto-loop.local.md
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
4. Inform the user of the cancellation and which iteration was reached.
|
|
18
|
+
**That's it.** Just call the `cancel-auto-loop` tool. Do NOT manually delete the state file.
|
|
33
19
|
|
|
34
20
|
## When to Use
|
|
35
21
|
|
|
36
22
|
Use this command when:
|
|
37
23
|
- The task requirements have changed
|
|
38
|
-
- You want to restart with different parameters
|
|
24
|
+
- You want to restart with different parameters (cancel first, then `/auto-loop` again)
|
|
39
25
|
- The loop appears stuck and you want manual control
|
|
40
26
|
- You need to work on something else
|
|
41
27
|
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ interface LoopState {
|
|
|
16
16
|
iteration: number;
|
|
17
17
|
maxIterations: number;
|
|
18
18
|
debounceMs: number;
|
|
19
|
+
forceLoop?: boolean;
|
|
19
20
|
sessionId?: string;
|
|
20
21
|
prompt?: string;
|
|
21
22
|
completed?: string;
|
|
@@ -106,7 +107,7 @@ function getStateFile(directory: string): string {
|
|
|
106
107
|
|
|
107
108
|
// Parse markdown frontmatter state
|
|
108
109
|
function parseState(content: string): LoopState {
|
|
109
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
110
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
110
111
|
if (!match) return { active: false, iteration: 0, maxIterations: DEFAULT_MAX_ITERATIONS, debounceMs: DEFAULT_DEBOUNCE_MS };
|
|
111
112
|
|
|
112
113
|
const frontmatter = match[1];
|
|
@@ -117,13 +118,23 @@ function parseState(content: string): LoopState {
|
|
|
117
118
|
debounceMs: DEFAULT_DEBOUNCE_MS,
|
|
118
119
|
};
|
|
119
120
|
|
|
120
|
-
for (const line of frontmatter.split(
|
|
121
|
+
for (const line of frontmatter.split(/\r?\n/)) {
|
|
121
122
|
const [key, ...valueParts] = line.split(":");
|
|
122
123
|
const value = valueParts.join(":").trim();
|
|
123
124
|
if (key === "active") state.active = value === "true";
|
|
124
|
-
if (key === "iteration")
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
if (key === "iteration") {
|
|
126
|
+
const parsed = parseInt(value, 10);
|
|
127
|
+
state.iteration = Number.isNaN(parsed) ? 0 : Math.max(0, parsed);
|
|
128
|
+
}
|
|
129
|
+
if (key === "maxIterations") {
|
|
130
|
+
const parsed = parseInt(value, 10);
|
|
131
|
+
state.maxIterations = (Number.isFinite(parsed) && parsed > 0) ? parsed : DEFAULT_MAX_ITERATIONS;
|
|
132
|
+
}
|
|
133
|
+
if (key === "debounceMs") {
|
|
134
|
+
const parsed = parseInt(value, 10);
|
|
135
|
+
state.debounceMs = (Number.isFinite(parsed) && parsed >= 0) ? parsed : DEFAULT_DEBOUNCE_MS;
|
|
136
|
+
}
|
|
137
|
+
if (key === "forceLoop") state.forceLoop = value === "true";
|
|
127
138
|
if (key === "sessionId") state.sessionId = value || undefined;
|
|
128
139
|
}
|
|
129
140
|
|
|
@@ -159,6 +170,7 @@ function serializeState(state: LoopState): string {
|
|
|
159
170
|
`maxIterations: ${state.maxIterations}`,
|
|
160
171
|
`debounceMs: ${state.debounceMs}`,
|
|
161
172
|
];
|
|
173
|
+
if (state.forceLoop) lines.push(`forceLoop: ${state.forceLoop}`);
|
|
162
174
|
if (state.sessionId) lines.push(`sessionId: ${state.sessionId}`);
|
|
163
175
|
lines.push("---");
|
|
164
176
|
if (state.prompt) lines.push("", state.prompt);
|
|
@@ -244,9 +256,12 @@ async function getLastAssistantText(
|
|
|
244
256
|
}
|
|
245
257
|
}
|
|
246
258
|
|
|
247
|
-
// Check completion by looking for <promise>DONE</promise>
|
|
259
|
+
// Check completion by looking for <promise>DONE</promise> OR STATUS: COMPLETE
|
|
260
|
+
// in the last assistant text. Either signal indicates the AI believes the task
|
|
261
|
+
// is finished. The validateCompletion() function further validates legitimacy.
|
|
248
262
|
function checkCompletion(text: string): boolean {
|
|
249
|
-
|
|
263
|
+
const cleaned = stripCodeFences(text);
|
|
264
|
+
return COMPLETION_TAG.test(cleaned) || STATUS_COMPLETE_TAG.test(cleaned);
|
|
250
265
|
}
|
|
251
266
|
|
|
252
267
|
// Extract the STATUS signal presence from text.
|
|
@@ -412,16 +427,26 @@ function buildProgressSection(state: LoopState): string {
|
|
|
412
427
|
// Build the loop context reminder for post-compaction injection
|
|
413
428
|
function buildLoopContextReminder(state: LoopState): string {
|
|
414
429
|
const progress = buildProgressSection(state);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
430
|
+
const forceLabel = state.forceLoop ? " [FORCE MODE]" : "";
|
|
431
|
+
const rules = state.forceLoop
|
|
432
|
+
? `IMPORTANT RULES:
|
|
433
|
+
- Do NOT call the auto-loop tool — the plugin handles continuation automatically
|
|
434
|
+
- Before going idle, output ## Completed and ## Next Steps sections
|
|
435
|
+
- FORCE MODE is active — the loop will continue for all ${state.maxIterations} iterations regardless of completion signals
|
|
436
|
+
- Focus on making steady progress each iteration`
|
|
437
|
+
: `IMPORTANT RULES:
|
|
438
|
+
- Do NOT call the auto-loop tool — the plugin handles continuation automatically
|
|
420
439
|
- Before going idle, output ## Completed and ## Next Steps sections
|
|
421
440
|
- You MUST include a STATUS line: either \`STATUS: IN_PROGRESS\` or \`STATUS: COMPLETE\` on its own line
|
|
422
441
|
- Do NOT output <promise>DONE</promise> if there are ANY unchecked items (\`- [ ]\`) in your Next Steps — the plugin WILL reject it
|
|
423
442
|
- Only output \`STATUS: COMPLETE\` and the DONE signal when ALL steps are truly finished and Next Steps is empty
|
|
424
443
|
- Do NOT output false completion promises. If blocked, output \`STATUS: IN_PROGRESS\` and explain the blocker.`;
|
|
444
|
+
|
|
445
|
+
return `[AUTO LOOP${forceLabel} ACTIVE — Iteration ${state.iteration}/${state.maxIterations}]
|
|
446
|
+
|
|
447
|
+
Original task: ${state.prompt || "(no task specified)"}
|
|
448
|
+
${progress}
|
|
449
|
+
${rules}`;
|
|
425
450
|
}
|
|
426
451
|
|
|
427
452
|
// Check if session is currently busy (not idle)
|
|
@@ -481,11 +506,10 @@ export const AutoLoopPlugin: Plugin = async (ctx) => {
|
|
|
481
506
|
|
|
482
507
|
// Debounce tracking for idle events
|
|
483
508
|
let lastContinuation = 0;
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
let continuationInFlight = false;
|
|
509
|
+
// Reentrant guard: prevent concurrent idle event processing.
|
|
510
|
+
// Two idle events can interleave at await points, causing duplicate
|
|
511
|
+
// state reads/writes. This ensures only one handler runs at a time.
|
|
512
|
+
let handlingIdle = false;
|
|
489
513
|
|
|
490
514
|
return {
|
|
491
515
|
tool: {
|
|
@@ -504,33 +528,68 @@ export const AutoLoopPlugin: Plugin = async (ctx) => {
|
|
|
504
528
|
.number()
|
|
505
529
|
.optional()
|
|
506
530
|
.describe("Debounce delay between iterations in ms (default: 2000)"),
|
|
531
|
+
forceLoop: tool.schema
|
|
532
|
+
.boolean()
|
|
533
|
+
.optional()
|
|
534
|
+
.describe("Force mode (--ralph): ignore completion signals and run for all iterations"),
|
|
507
535
|
},
|
|
508
|
-
async execute({ task, maxIterations = DEFAULT_MAX_ITERATIONS, debounceMs = DEFAULT_DEBOUNCE_MS }, context) {
|
|
536
|
+
async execute({ task, maxIterations = DEFAULT_MAX_ITERATIONS, debounceMs = DEFAULT_DEBOUNCE_MS, forceLoop = false }, context) {
|
|
509
537
|
if (context.abort.aborted) return "Auto Loop start was cancelled.";
|
|
510
538
|
|
|
539
|
+
// Coerce parameters to correct types (AI may pass strings)
|
|
540
|
+
const parsedMax = parseInt(String(maxIterations), 10);
|
|
541
|
+
const safeMaxIterations = (typeof maxIterations === "number" && maxIterations > 0)
|
|
542
|
+
? maxIterations
|
|
543
|
+
: (Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : DEFAULT_MAX_ITERATIONS);
|
|
544
|
+
const parsedDebounce = parseInt(String(debounceMs), 10);
|
|
545
|
+
const safeDebounceMs = (typeof debounceMs === "number" && debounceMs >= 0)
|
|
546
|
+
? debounceMs
|
|
547
|
+
: (Number.isFinite(parsedDebounce) && parsedDebounce >= 0 ? parsedDebounce : DEFAULT_DEBOUNCE_MS);
|
|
548
|
+
|
|
549
|
+
// Guard: if a loop is already active for this session, do NOT reset state.
|
|
550
|
+
// This prevents the AI from accidentally re-invoking the tool each iteration
|
|
551
|
+
// (which would reset the iteration counter back to 0 every time).
|
|
552
|
+
const existingState = await readState(directory);
|
|
553
|
+
if (existingState.active && existingState.sessionId === context.sessionID) {
|
|
554
|
+
log("warn", `Tool re-invoked while loop already active (iteration ${existingState.iteration}/${existingState.maxIterations}). Ignoring to prevent state reset.`);
|
|
555
|
+
return `Auto Loop is ALREADY ACTIVE (iteration ${existingState.iteration}/${existingState.maxIterations}). Do NOT call the auto-loop tool again — the plugin handles continuation automatically. Just keep working on the task. Use /cancel-auto-loop first if you need to restart with different parameters.`;
|
|
556
|
+
}
|
|
557
|
+
if (existingState.active && existingState.sessionId && existingState.sessionId !== context.sessionID) {
|
|
558
|
+
log("warn", `Starting new loop in session ${context.sessionID}, overwriting active loop from session ${existingState.sessionId} (was at iteration ${existingState.iteration}/${existingState.maxIterations})`);
|
|
559
|
+
toast(`Auto Loop: replacing active loop from another session`, "warning");
|
|
560
|
+
}
|
|
561
|
+
|
|
511
562
|
const state: LoopState = {
|
|
512
563
|
active: true,
|
|
513
564
|
iteration: 0,
|
|
514
|
-
maxIterations,
|
|
515
|
-
debounceMs,
|
|
565
|
+
maxIterations: safeMaxIterations,
|
|
566
|
+
debounceMs: safeDebounceMs,
|
|
567
|
+
forceLoop: forceLoop ? true : undefined,
|
|
516
568
|
sessionId: context.sessionID,
|
|
517
569
|
prompt: task,
|
|
518
570
|
};
|
|
519
571
|
await writeState(directory, state, log);
|
|
520
572
|
// Reset guards so the first idle event is not blocked
|
|
521
|
-
|
|
573
|
+
handlingIdle = false;
|
|
522
574
|
lastContinuation = 0;
|
|
523
575
|
|
|
524
|
-
|
|
525
|
-
|
|
576
|
+
const modeLabel = forceLoop ? " [FORCE MODE]" : "";
|
|
577
|
+
log("info", `Loop started${modeLabel} for session ${context.sessionID}`);
|
|
578
|
+
toast(`Auto Loop started${modeLabel} (max ${safeMaxIterations} iterations)`, "success");
|
|
579
|
+
|
|
580
|
+
const forceNote = forceLoop
|
|
581
|
+
? `\n\n**FORCE MODE (--ralph):** Completion signals are IGNORED. The loop will run for all ${safeMaxIterations} iterations regardless. Focus on making steady progress each iteration — you do NOT need to output STATUS or DONE signals.`
|
|
582
|
+
: "";
|
|
526
583
|
|
|
527
|
-
return `Auto Loop started (max ${
|
|
584
|
+
return `Auto Loop started (max ${safeMaxIterations} iterations).${forceNote}
|
|
528
585
|
|
|
529
586
|
Task: ${task}
|
|
530
587
|
|
|
531
|
-
**Begin working on the task now.** The loop will auto-continue until you signal completion.
|
|
588
|
+
**Begin working on the task now.** The loop will auto-continue until ${forceLoop ? `all ${safeMaxIterations} iterations are used` : "you signal completion"}.
|
|
532
589
|
|
|
533
|
-
|
|
590
|
+
**CRITICAL: Do NOT call the auto-loop tool again.** The plugin handles auto-continuation automatically via idle detection. Re-invoking the tool would reset your progress. Just work on the task — the plugin will prompt you to continue when you go idle.
|
|
591
|
+
|
|
592
|
+
Before going idle each iteration, output structured progress${forceLoop ? "" : " AND a status line"}:
|
|
534
593
|
|
|
535
594
|
\`\`\`
|
|
536
595
|
## Completed
|
|
@@ -538,10 +597,9 @@ Before going idle each iteration, output structured progress AND a status line:
|
|
|
538
597
|
|
|
539
598
|
## Next Steps
|
|
540
599
|
- [ ] What remains (in priority order)
|
|
541
|
-
|
|
542
|
-
STATUS: IN_PROGRESS
|
|
600
|
+
${forceLoop ? "" : "\nSTATUS: IN_PROGRESS"}
|
|
543
601
|
\`\`\`
|
|
544
|
-
|
|
602
|
+
${forceLoop ? "" : `
|
|
545
603
|
## Completion Rules — READ CAREFULLY
|
|
546
604
|
|
|
547
605
|
1. **If your Next Steps list has ANY unchecked items (\`- [ ]\`), you MUST NOT output the DONE signal.** The plugin will reject it.
|
|
@@ -550,7 +608,7 @@ STATUS: IN_PROGRESS
|
|
|
550
608
|
- \`STATUS: COMPLETE\` on its own line
|
|
551
609
|
- The promise-DONE XML tag on its own line
|
|
552
610
|
4. If you are blocked or stuck, output \`STATUS: IN_PROGRESS\` and explain the blocker. Do NOT output a false DONE.
|
|
553
|
-
|
|
611
|
+
`}
|
|
554
612
|
Use /cancel-auto-loop to stop early.`;
|
|
555
613
|
},
|
|
556
614
|
}),
|
|
@@ -566,7 +624,8 @@ Use /cancel-auto-loop to stop early.`;
|
|
|
566
624
|
}
|
|
567
625
|
const iterations = state.iteration;
|
|
568
626
|
await clearState(directory, log);
|
|
569
|
-
|
|
627
|
+
handlingIdle = false;
|
|
628
|
+
lastContinuation = 0;
|
|
570
629
|
|
|
571
630
|
log("info", `Loop cancelled after ${iterations} iteration(s)`);
|
|
572
631
|
toast(`Auto Loop cancelled after ${iterations} iteration(s)`, "warning");
|
|
@@ -585,6 +644,8 @@ Use /cancel-auto-loop to stop early.`;
|
|
|
585
644
|
|
|
586
645
|
- \`/auto-loop <task>\` - Start an auto-continuation loop (default: 25 iterations)
|
|
587
646
|
- \`/auto-loop <task> --max <n>\` - Start with a custom iteration limit
|
|
647
|
+
- \`/auto-loop <task> --ralph\` - Force mode: ignore completion signals, run all iterations
|
|
648
|
+
- \`/auto-loop <task> --ralph --max <n>\` - Force mode with custom limit
|
|
588
649
|
- \`/cancel-auto-loop\` - Stop an active loop
|
|
589
650
|
- \`/auto-loop-help\` - Show this help
|
|
590
651
|
|
|
@@ -592,6 +653,8 @@ Use /cancel-auto-loop to stop early.`;
|
|
|
592
653
|
|
|
593
654
|
- \`/auto-loop Build a REST API\` — runs up to 25 iterations
|
|
594
655
|
- \`/auto-loop Fix all lint errors --max 10\` — runs up to 10 iterations
|
|
656
|
+
- \`/auto-loop --ralph Continue refactoring\` — force runs all 25 iterations, ignores DONE signals
|
|
657
|
+
- \`/auto-loop --ralph --max 50 Big migration\` — force runs all 50 iterations
|
|
595
658
|
|
|
596
659
|
## How It Works
|
|
597
660
|
|
|
@@ -600,6 +663,13 @@ Use /cancel-auto-loop to stop early.`;
|
|
|
600
663
|
3. Plugin auto-continues if not complete
|
|
601
664
|
4. Loop stops when AI outputs: <promise>DONE</promise>
|
|
602
665
|
|
|
666
|
+
## Force Mode (--ralph)
|
|
667
|
+
|
|
668
|
+
When \`--ralph\` is used, the loop ignores ALL completion signals and runs for the full iteration count. Useful when:
|
|
669
|
+
- You got interrupted and want to continue no matter what
|
|
670
|
+
- You want the AI to keep iterating and improving
|
|
671
|
+
- You don't want the AI to stop early
|
|
672
|
+
|
|
603
673
|
## State File
|
|
604
674
|
|
|
605
675
|
Located at: .opencode/auto-loop.local.md`;
|
|
@@ -613,107 +683,158 @@ Located at: .opencode/auto-loop.local.md`;
|
|
|
613
683
|
if (event.type === "session.idle") {
|
|
614
684
|
const sessionId = event.properties.sessionID;
|
|
615
685
|
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
686
|
+
// Reentrant guard: if we're already processing an idle event,
|
|
687
|
+
// skip this one. Two idle events can interleave at await points,
|
|
688
|
+
// causing duplicate state reads/writes and double continuations.
|
|
689
|
+
if (handlingIdle) {
|
|
690
|
+
log("debug", "Idle handler already running, skipping duplicate event");
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
handlingIdle = true;
|
|
624
694
|
|
|
625
|
-
|
|
626
|
-
|
|
695
|
+
try {
|
|
696
|
+
const state = await readState(directory);
|
|
627
697
|
|
|
628
|
-
|
|
629
|
-
|
|
698
|
+
if (!state.active) return;
|
|
699
|
+
if (!sessionId) return;
|
|
700
|
+
if (state.sessionId && state.sessionId !== sessionId) return;
|
|
701
|
+
|
|
702
|
+
const now = Date.now();
|
|
703
|
+
if (now - lastContinuation < state.debounceMs) return;
|
|
704
|
+
|
|
705
|
+
// Double-check the session is truly idle before sending
|
|
706
|
+
if (await isSessionBusy(client, sessionId, log)) return;
|
|
707
|
+
|
|
708
|
+
// Fetch last assistant message (used for completion check + progress extraction)
|
|
709
|
+
const lastText = await getLastAssistantText(client, sessionId, directory, log);
|
|
710
|
+
|
|
711
|
+
// Skip completion check on iteration 0 (first idle after loop start)
|
|
712
|
+
// to avoid false positives from the tool's initial response text.
|
|
713
|
+
// Also skip entirely when forceLoop is true — force mode ignores
|
|
714
|
+
// all completion signals and runs until max iterations.
|
|
715
|
+
if (!state.forceLoop && state.iteration > 0 && lastText && checkCompletion(lastText)) {
|
|
716
|
+
// Validate the DONE signal — reject if there are unchecked steps
|
|
717
|
+
// or if the STATUS signal contradicts completion
|
|
718
|
+
const validation = validateCompletion(lastText);
|
|
719
|
+
if (validation.valid) {
|
|
720
|
+
await clearState(directory, log);
|
|
721
|
+
log("info", `Loop completed at iteration ${state.iteration}`);
|
|
722
|
+
toast(`Auto Loop completed after ${state.iteration} iteration(s)`, "success");
|
|
723
|
+
return;
|
|
724
|
+
} else {
|
|
725
|
+
log("warn", `Rejected premature DONE signal: ${validation.reason}`);
|
|
726
|
+
toast(`Auto Loop: DONE rejected — ${validation.reason}`, "warning");
|
|
727
|
+
// Fall through to send another continuation prompt
|
|
728
|
+
}
|
|
729
|
+
}
|
|
630
730
|
|
|
631
|
-
|
|
632
|
-
|
|
731
|
+
// Heuristic: if the AI has no remaining work, stop the loop even
|
|
732
|
+
// without an explicit DONE/STATUS signal. This catches cases where
|
|
733
|
+
// the AI says "all done" naturally without the structured tag.
|
|
734
|
+
// Conditions: not force mode, at least 1 iteration done, the last
|
|
735
|
+
// response has no unchecked steps, AND the persisted state also has
|
|
736
|
+
// no unchecked next steps.
|
|
737
|
+
if (!state.forceLoop && state.iteration > 0 && lastText) {
|
|
738
|
+
const responseHasWork = hasIncompleteSteps(lastText);
|
|
739
|
+
const stateHasWork = state.nextSteps
|
|
740
|
+
? /^\s*-\s*\[ \]/m.test(state.nextSteps)
|
|
741
|
+
: false;
|
|
742
|
+
const { hasInProgress } = getStatusSignals(lastText);
|
|
743
|
+
|
|
744
|
+
// No unchecked steps anywhere AND no explicit IN_PROGRESS signal
|
|
745
|
+
if (!responseHasWork && !stateHasWork && !hasInProgress) {
|
|
746
|
+
// Extra guard: only trigger if the response doesn't introduce
|
|
747
|
+
// new next-steps content (even without checkboxes)
|
|
748
|
+
const newSteps = extractNextSteps(lastText);
|
|
749
|
+
const hasNewWork = newSteps && newSteps.trim().length > 0;
|
|
750
|
+
if (!hasNewWork) {
|
|
751
|
+
await clearState(directory, log);
|
|
752
|
+
log("info", `Loop completed at iteration ${state.iteration} (no remaining work detected)`);
|
|
753
|
+
toast(`Auto Loop completed after ${state.iteration} iteration(s)`, "success");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
633
758
|
|
|
634
|
-
|
|
635
|
-
// to avoid false positives from the tool's initial response text
|
|
636
|
-
if (state.iteration > 0 && lastText && checkCompletion(lastText)) {
|
|
637
|
-
// Validate the DONE signal — reject if there are unchecked steps
|
|
638
|
-
// or if the STATUS signal contradicts completion
|
|
639
|
-
const validation = validateCompletion(lastText);
|
|
640
|
-
if (validation.valid) {
|
|
759
|
+
if (state.iteration >= state.maxIterations) {
|
|
641
760
|
await clearState(directory, log);
|
|
642
|
-
log("
|
|
643
|
-
toast(`Auto Loop
|
|
761
|
+
log("warn", `Loop hit max iterations (${state.maxIterations})`);
|
|
762
|
+
toast(`Auto Loop stopped — max iterations (${state.maxIterations}) reached`, "warning");
|
|
644
763
|
return;
|
|
645
|
-
} else {
|
|
646
|
-
log("warn", `Rejected premature DONE signal: ${validation.reason}`);
|
|
647
|
-
toast(`Auto Loop: DONE rejected — ${validation.reason}`, "warning");
|
|
648
|
-
// Fall through to send another continuation prompt
|
|
649
764
|
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (state.iteration >= state.maxIterations) {
|
|
653
|
-
await clearState(directory, log);
|
|
654
|
-
log("warn", `Loop hit max iterations (${state.maxIterations})`);
|
|
655
|
-
toast(`Auto Loop stopped — max iterations (${state.maxIterations}) reached`, "warning");
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
765
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
766
|
+
// Extract progress from last message and merge with existing state
|
|
767
|
+
const newNextSteps = lastText ? extractNextSteps(lastText) : undefined;
|
|
768
|
+
const newCompleted = lastText ? extractCompleted(lastText) : undefined;
|
|
662
769
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
770
|
+
const newState: LoopState = {
|
|
771
|
+
...state,
|
|
772
|
+
iteration: state.iteration + 1,
|
|
773
|
+
sessionId,
|
|
774
|
+
nextSteps: newNextSteps || state.nextSteps,
|
|
775
|
+
completed: mergeCompleted(state.completed, newCompleted),
|
|
776
|
+
};
|
|
777
|
+
await writeState(directory, newState, log);
|
|
778
|
+
lastContinuation = Date.now();
|
|
779
|
+
|
|
780
|
+
// Verify the state write persisted correctly
|
|
781
|
+
const verifyState = await readState(directory);
|
|
782
|
+
if (verifyState.iteration !== newState.iteration) {
|
|
783
|
+
log("error", `State file write verification FAILED: expected iteration ${newState.iteration}, read back ${verifyState.iteration}`);
|
|
784
|
+
toast(`Auto Loop: state write failed! Check .opencode/ permissions.`, "error");
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
675
787
|
|
|
676
|
-
|
|
788
|
+
// Build continuation prompt with progress context
|
|
789
|
+
const progressSection = buildProgressSection(newState);
|
|
677
790
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
IMPORTANT:
|
|
791
|
+
const forceLabel = state.forceLoop ? " [FORCE MODE]" : "";
|
|
792
|
+
const importantRules = state.forceLoop
|
|
793
|
+
? `IMPORTANT:
|
|
794
|
+
- Do NOT call the auto-loop tool — the plugin handles continuation automatically
|
|
795
|
+
- Pick up from the next incomplete step below
|
|
796
|
+
- Before going idle, list your progress using ## Completed and ## Next Steps sections
|
|
797
|
+
- FORCE MODE is active — the loop will continue for all ${newState.maxIterations} iterations regardless of completion signals
|
|
798
|
+
- Focus on making steady progress each iteration`
|
|
799
|
+
: `IMPORTANT:
|
|
800
|
+
- Do NOT call the auto-loop tool — the plugin handles continuation automatically
|
|
681
801
|
- Pick up from the next incomplete step below
|
|
682
802
|
- Before going idle, list your progress using ## Completed and ## Next Steps sections
|
|
683
803
|
- You MUST include a STATUS line: either \`STATUS: IN_PROGRESS\` or \`STATUS: COMPLETE\` on its own line
|
|
684
804
|
- Do NOT output <promise>DONE</promise> if there are ANY unchecked items (\`- [ ]\`) in your Next Steps — the plugin WILL reject it
|
|
685
805
|
- Only output \`STATUS: COMPLETE\` and the DONE signal when ALL steps are truly finished and Next Steps is empty
|
|
686
|
-
- Do not stop until the task is truly done
|
|
806
|
+
- Do not stop until the task is truly done`;
|
|
807
|
+
|
|
808
|
+
const continuationPrompt = `[AUTO LOOP${forceLabel} — ITERATION ${newState.iteration}/${newState.maxIterations}]
|
|
809
|
+
|
|
810
|
+
Continue working on the task. Do NOT repeat work that is already done.
|
|
811
|
+
Do NOT call the auto-loop tool again — the plugin handles auto-continuation.
|
|
812
|
+
${progressSection}
|
|
813
|
+
${importantRules}
|
|
687
814
|
|
|
688
815
|
Original task:
|
|
689
816
|
${state.prompt || "(no task specified)"}`;
|
|
690
817
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// --- session.status: clear in-flight guard when session returns to idle ---
|
|
714
|
-
if (event.type === "session.status") {
|
|
715
|
-
if (event.properties.status?.type === "idle") {
|
|
716
|
-
continuationInFlight = false;
|
|
818
|
+
try {
|
|
819
|
+
// Use promptAsync (fire-and-forget) so the event handler returns
|
|
820
|
+
// immediately. This allows the next session.idle event to fire
|
|
821
|
+
// naturally when the AI finishes, enabling the loop to continue.
|
|
822
|
+
// The synchronous prompt() blocks until the AI response completes,
|
|
823
|
+
// which prevents subsequent idle events from being processed.
|
|
824
|
+
await client.session.promptAsync({
|
|
825
|
+
path: { id: sessionId },
|
|
826
|
+
body: {
|
|
827
|
+
parts: [{ type: "text", text: continuationPrompt }],
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
log("info", `Sent continuation ${newState.iteration}/${newState.maxIterations}`);
|
|
831
|
+
toast(`Auto Loop: iteration ${newState.iteration}/${newState.maxIterations}`);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
log("error", `Failed to send continuation prompt: ${err}`);
|
|
834
|
+
toast(`Auto Loop: failed to send continuation — ${err}`, "error");
|
|
835
|
+
}
|
|
836
|
+
} finally {
|
|
837
|
+
handlingIdle = false;
|
|
717
838
|
}
|
|
718
839
|
}
|
|
719
840
|
|
|
@@ -755,7 +876,8 @@ ${state.prompt || "(no task specified)"}`;
|
|
|
755
876
|
) {
|
|
756
877
|
log("warn", `Session error detected, pausing loop at iteration ${state.iteration}`);
|
|
757
878
|
toast("Auto Loop paused — session error", "error");
|
|
758
|
-
|
|
879
|
+
handlingIdle = false;
|
|
880
|
+
lastContinuation = 0;
|
|
759
881
|
// Mark inactive but keep state so user can inspect/resume
|
|
760
882
|
await writeState(directory, { ...state, active: false }, log);
|
|
761
883
|
}
|
|
@@ -770,7 +892,8 @@ ${state.prompt || "(no task specified)"}`;
|
|
|
770
892
|
if (state.sessionId && deletedSessionId && state.sessionId !== deletedSessionId) return;
|
|
771
893
|
|
|
772
894
|
await clearState(directory, log);
|
|
773
|
-
|
|
895
|
+
handlingIdle = false;
|
|
896
|
+
lastContinuation = 0;
|
|
774
897
|
log("info", "Session deleted, cleaning up loop state");
|
|
775
898
|
}
|
|
776
899
|
},
|