loop-test · git:20260525.fd0bda8 · 2026-05-25 · sha256 0525b5d511aaf2e9

loop-test git:20260525.fd0bda8A

Immutable. This exact content is served forever at /api/v1/blob/0525b5d511aaf2e9.

---
name: loop-test
description: Local sandbox for experimenting with Claude Code's /loop command (interval + dynamic/self-paced scheduling) applied to LETS workflows. Use when the user says "loop test", "loop-test", "test the loop skill", "цикл LETS", "ScheduleWakeup experiment", or wants to design a periodic /loop that polls state or babysits a long-running job. Local-only — not part of the lets plugin payload. For convergent autonomous work toward an end state, use sibling skill `goal-test` (which covers /goal).
---

# loop-test (local experiment)

`/loop` schedules **periodic re-invocations of the same prompt**. Two modes:

| Mode | Syntax | Mechanic |
|---|---|---|
| **Interval** | `/loop 5m /foo` · `/loop /standup 1h` · `/loop check the deploy every 20m` | Cron entry in `.claude/scheduled_tasks.json`. Default `10m` when only `[interval]` shorthand. Survives session restart (durable). |
| **Dynamic / self-paced** | `/loop check the deploy` (no interval anywhere) | Model calls `ScheduleWakeup(delaySeconds, prompt, reason)` at the end of each turn. Skip → loop ends silently. |

**`/loop` is the wrong tool for sequential workflow phases.** It repeats the *same prompt* each tick — to chain `plan → review → check`, the prompt itself has to be phase-aware (read state, advance, write back). For state-convergent autonomy, use `/goal` instead (see `goal-test` skill).

**Right tool for `/loop`:**
- Polling external state (CI runs, deploys, PR comments)
- Periodic checks (every 20m: any new bd-ready tasks?)
- Babysitting a long-running job (check progress, surface failures)
- Continuous monitoring during a long focused session

## How `/loop` actually works

Reverse-engineered from `claude` binary v2.1.150.

### Dynamic-mode contract

This is the mode you care about most — interval is just cron.

1. User runs `/loop <prompt>` (no interval).
2. Model does the work.
3. Before yielding, model calls `ScheduleWakeup(delaySeconds, prompt, reason)`:
   - `delaySeconds` clamped to `[60, 3600]`
   - `prompt` must be **exactly the same string** as the original `/loop` input
   - `reason` is one sentence shown to user + sent to telemetry — be specific (`"polling CI run"` beats `"waiting"`)
4. Harness re-invokes the model at the scheduled time. Loop continues.
5. Skip `ScheduleWakeup` → loop ends silently. No error.

### Cache TTL gotcha

Anthropic prompt cache TTL is 5 minutes. Picking `delaySeconds`:
- `< 270s` → cache stays warm, cheap re-invocation
- `>= 1200s` → commit to the miss, one cache-miss amortizes over a long wait
- **`300s` is worst-of-both** — you pay the miss without amortizing. Avoid exactly 300.

For idle ticks with no specific signal to watch, default `1200-1800s` (20-30 min). User can always interrupt if they need you sooner.

### Minute-boundary rounding gotcha (empirical, Experiment 1)

`delaySeconds` is NOT honored exactly. The actual wakeup time is **rounded UP to the next clean minute boundary**. Binary cron expression is `MM HH * * *` (minute-only granularity).

Examples from Experiment 1:
- Requested `30s`, clamped to `60s`, actual scheduled `65s` (we were at HH:MM:55)
- Same request next tick, actual scheduled `101s` (we were at HH:MM:19)

**Implication:** for any tick under ~1min you get 60-119s of real delay. Designing precise polling cadences <2min is futile — pick `60s` (floor) and accept jitter, or accept >120s with predictable bucketing. Also note dispatch latency adds ~10-25s on top.

### Cancellation / collisions

- Re-issuing `/loop` with the **same prompt** cancels the previous pending wakeup for that prompt (`ZP5(_)` in binary). One prompt = one live loop.
- User interrupt cancels **all** pending loop wakeups (`b76()` returns count cancelled).
- Recurring max age: `7 days` (`recurringMaxAgeMs = 604800000`). After that the loop auto-ages-out.

### Sentinels (fully autonomous)

For unattended runs where there's no user prompt sitting in the channel:
- `<<autonomous-loop>>` — cron-mode autonomous (CronCreate-backed)
- `<<autonomous-loop-dynamic>>` — ScheduleWakeup-mode autonomous

Useful for `/lets:schedule`-style routines but probably YAGNI for LETS right now.

### Telemetry

- `tengu_kairos_loop_dynamic` — feature flag for dynamic mode
- `tengu_loop_dynamic_wakeup_scheduled` — fired per ScheduleWakeup
- `tengu_loop_dynamic_wakeup_aged_out` — fired when 7-day cap hits

## Designing a useful `/loop` for LETS

Three rules that fit this project:

1. **Loop only on read-only state polls.** Anything that mutates (commit, push, bd close) needs explicit user approval per AUTO MODE rule — auto-loop violates it. `/lets:status`, `/lets:check`, `bd ready` are safe; `/lets:commit`, `/lets:done` are not.
2. **One sentence of new information per tick, or nothing.** If state hasn't changed, the tick should yield "no change" and reschedule. Don't re-report the same status over and over — the user sees ticks pile up.
3. **Pick the delay for the actual signal, not a round number.** Polling a CI run that takes ~8min? `delaySeconds=480` is better than `delaySeconds=600`. Avoid 300 (cache trap).

## Concrete `/loop` templates for LETS

### Template A — poll for PR feedback

```
/loop check `gh pr view <N> --json comments,reviewDecision` for new activity since last check. If nothing new: report "no change" and reschedule (1200s). If new comments or review state changed: report what's new and stop the loop so user can act.
```

Use case: opened a PR, want to know within 20min when reviewer pings, without keeping the window in focus.

### Template B — backlog watcher

```
/loop run `bd ready --json | jq length`. If count increased since last tick: report which new tasks appeared (bd show each new id, one-line summary). If unchanged: report "no change". Reschedule 1800s.
```

Use case: long deep-work session on a feature, want to surface if a higher-priority bug landed in the backlog without breaking focus.

### Template C — long-running job babysit

```
/loop check the deploy status at <URL or command>. If still running: report "running, T+Xmin" + estimated remaining. If failed: surface the error and stop. If succeeded: confirm and stop. Reschedule 180s while running.
```

Use case: triggered a deploy or migration, want hands-off progress reporting.

## Combining `/loop` with `/goal` (open research)

Interesting question: nest `/loop` inside `/goal`?

```
/goal Repo passes /lets:check three times in a row over the next hour.
/loop 5m /lets:check
```

The loop schedules the *work*; the goal defines the *termination condition*. Untested — see `goal-test` for the /goal half and try.

## Concrete experiment

```
/loop check git status; if no uncommitted files and `git rev-list main..HEAD --count` is 0, report "clean" and stop. Otherwise report the dirty state and reschedule 90s.
```

Watch in findings:
- Does `ScheduleWakeup` fire after each tick?
- Is `delaySeconds=90` honored, or clamped to 60?
- How does the UI display loop ticks between user messages?
- Does the model self-terminate when the stop-condition is met (no ScheduleWakeup call), or does it loop indefinitely?

## Open questions

- How does the cron-mode (`/loop 5m ...`) persist? Just file on disk, or also stored in session state? Does it survive `claude` restart?
- Can a dynamic loop call another dynamic loop? (Probably idempotent on prompt-string — second `/loop` cancels first.)
- Does `/loop` interact with worktrees? Per-worktree state files, or global to the user?
- What's the practical max number of concurrent loops? (Both cron and dynamic.)
- How does `/loop` interact with `/goal`? Does a Stop hook block a `/loop` tick from yielding?

## Findings (fill in during experiments)

Track in sibling `findings.md`. One entry per experiment:

```
### <date> · <prompt>
- Mode:              (interval / dynamic)
- delaySeconds:      requested → actual (clamped?)
- Ticks observed:    N
- Self-terminate?:   yes/no (did it skip ScheduleWakeup when condition met?)
- UI experience:     (intrusive? unobtrusive? useful?)
- Promote to plugin? yes/no + rationale
```

## Promote criteria

Skill graduates to a real `/lets:watch` (or similar) command in `plugins/lets/commands/` only if:

- At least one template (A/B/C) produces useful repeat-value across 3+ different real situations
- The "loop only on read-only polls" boundary is enforceable as a command-level guardrail
- We have a clear answer for: "how does the user stop the loop without losing the context?"

Otherwise: keep notes in `findings.md`, close `lets-53buy` with a "investigated, parking the pattern" comment.

## See also

- **Sibling skill `goal-test`** — for convergent autonomous work (Stop hook condition). `/goal` is the right tool for `plan → review → check → execute → review → check` chaining; `/loop` is not.
- LETS Rules `AUTO MODE` section in `.claude/rules/lets-rules.md` — gating rules every loop must respect.
- Binary symbols (claude v2.1.150): `tengu_kairos_loop_dynamic`, `tengu_loop_dynamic_wakeup_scheduled`, `recurringMaxAgeMs`, `ScheduleWakeup` tool.