feat(task-loop): new skill for in-session board draining v0.1.0
Interactive claim -> work -> close -> repeat loop in the current session; no daemon, no spawned claude, no busy-poll. Coordinates with using-tasks (.tasks/.lock, session_break gate, 10-min claim TTL -> tasks_heartbeat) and project-discipline (push Rule 4, sensitive artifacts). TDD (writing-skills RED-GREEN-REFACTOR): - RED: 2 clean-context subagents revealed gaps A-E (claim scope, missed session_break + .lock, consult-gate boundary, paused-vs-blocked). - GREEN: SKILL.md addresses all five; compliance subagent B clean. - REFACTOR: closed CronCreate loophole in long-watch (separate session = daemon); mandate ScheduleWakeup on this session. Re-test passed. Semver: 0.1.0 (initial). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
117
skills/task-loop/SKILL.md
Normal file
117
skills/task-loop/SKILL.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: task-loop
|
||||
version: 0.1.0
|
||||
description: >
|
||||
Use when the user asks you to work the task board yourself, in this
|
||||
session, one task after another — «поработай очередь», «прогони доску»,
|
||||
«бери задачи по очереди», «работай пока не скажу стоп», «work the queue»,
|
||||
«drain the board», «keep working tasks until I say stop». Does NOT apply
|
||||
to delegating work to another agent/project (→ delegate-task), to one
|
||||
named task you already know (→ using-tasks), or to configuring the
|
||||
background poller.
|
||||
---
|
||||
|
||||
# task-loop
|
||||
|
||||
Work the board **in this session**: claim the next ready task, do it, close it, claim the next — until the queue is empty or the user says stop.
|
||||
|
||||
**Core principle:** an interactive loop, not a daemon. You stay in the chair. Empty queue → **stop and report**, never spin a wait-timer. No subprocess, no `CronCreate` (that schedules a *separate* session — exactly the daemon you're replacing), no short `ScheduleWakeup` poll — those are the unattended poller's job, not yours here.
|
||||
|
||||
**REQUIRED SUB-SKILL:** `using-tasks` owns the board, the `.tasks/.lock` session lock, the `session_break` gate, and the pre-close coverage check. This skill drives the loop *through* those rules — it does not replace them.
|
||||
**REQUIRED SUB-SKILL:** `project-discipline` — commit/push gate (Rule 4) and sensitive-artifact handling apply to every task you touch.
|
||||
|
||||
## When to use
|
||||
|
||||
**Activates:** «поработай очередь», «прогони доску», «бери задачи по очереди», «работай пока не скажу стоп», «work the queue», «drain the board», «keep working tasks until I say stop».
|
||||
|
||||
**Does NOT apply:**
|
||||
- Delegating work to another agent/project → `delegate-task`.
|
||||
- One specific task you already named → `using-tasks` (switch/resume that task).
|
||||
- Setting up / debugging the background poller or agent-runner → that is infra, not this loop.
|
||||
|
||||
## The loop
|
||||
|
||||
Run this cycle. One task at a time.
|
||||
|
||||
```dot
|
||||
digraph task_loop {
|
||||
rankdir=TB;
|
||||
claim [shape=box, label="tasks_claim_next\n(current project, confirm=true)"];
|
||||
empty [shape=diamond,label="task returned?"];
|
||||
stop [shape=box, label="STOP — report board drained"];
|
||||
work [shape=box, label="do the work this session"];
|
||||
done [shape=diamond,label="completed?"];
|
||||
park [shape=box, label="park: blocked (external) | paused (resumable)"];
|
||||
gate [shape=diamond,label="consult_policy = human-only/strict-human?"];
|
||||
consult [shape=box, label="STOP before close/commit — consult user"];
|
||||
close [shape=box, label="pre-close coverage check → tasks_close"];
|
||||
brk [shape=diamond,label="session_break marker on closed task?"];
|
||||
boundary[shape=box, label="STOP — print SESSION BOUNDARY"];
|
||||
|
||||
claim -> empty;
|
||||
empty -> stop [label="no ready tasks"];
|
||||
empty -> work [label="yes"];
|
||||
work -> done;
|
||||
done -> park [label="no"];
|
||||
done -> gate [label="yes"];
|
||||
gate -> consult [label="yes"];
|
||||
gate -> close [label="auto"];
|
||||
close -> brk;
|
||||
brk -> boundary [label="yes"];
|
||||
brk -> claim [label="no"];
|
||||
park -> claim;
|
||||
}
|
||||
```
|
||||
|
||||
1. **Claim** the next ready task with `tasks_claim_next(claimer_identity, filter, confirm=true)`.
|
||||
- `claimer_identity` = `<machine>:<runtime>:<session>` (e.g. `DESKTOP-NSEF0UK:claude-opus:<session>`).
|
||||
- `filter.project` = **the current project** (qualified `<owner>/<repo>`) by default. Only widen to other projects when the user explicitly asks ("прогони все доски" / passes a project list).
|
||||
- The server already excludes `weight: needs-human` and anti-self-review tasks — you will never claim those.
|
||||
2. **No task returned** → the queue is drained. **STOP** and report (see *Empty queue*). Do not poll.
|
||||
3. **Do the work this session.** All your tools are available. Read the task description and per-task `<slug>.md`. Set the task `active` if it isn't already.
|
||||
4. **Honor the gate before the irreversible step.** Use the `consult_policy` returned by the claim:
|
||||
- `auto` → full autopilot through close.
|
||||
- `human-only` / `strict-human` → do the work, then **STOP before `tasks_close` / commit** and consult the user. Don't barrel through.
|
||||
- **Push is never automatic** regardless of policy — `project-discipline` Rule 4 (commit freely, push only on an explicit per-session grant).
|
||||
5. **Close** with the `using-tasks` pre-close coverage check, then `tasks_close(target_project, slug, confirm=true, note=…)`.
|
||||
6. **session_break gate.** After the close, **before claiming the next task**, honor the `using-tasks` `session_break` check: if the closed task carries the marker → print the `🔚 SESSION BOUNDARY` line and **STOP** (do not claim next). Otherwise → back to step 1.
|
||||
|
||||
## When a task can't be finished
|
||||
|
||||
Never leave a claimed task hanging (its claim expires in 10 min and it returns as a zombie), and never `tasks_close` unfinished work (that lies to the board).
|
||||
|
||||
- **External / unresolvable blocker** discovered mid-task (missing upstream, needs a human decision, scope change) → `tasks_update(slug, status="blocked", blocker="<concrete fact + what's needed>")`. Roll back partial work that would break the build. Then continue the loop (the blocker is isolated; the next claim won't return this task).
|
||||
- **Interrupted or resumable by you** (you ran out of budget, the user stops you mid-task) → `tasks_update(slug, status="paused", where_stopped=…, next_action=…)`.
|
||||
|
||||
A single failing task does not stop the loop — park it and move to the next.
|
||||
|
||||
## Empty queue & stopping
|
||||
|
||||
The loop ends on the **first** of:
|
||||
- **Empty queue** — `tasks_claim_next` returns no ready task → stop, report what you closed/parked, and wait for the user. Do **not** `ScheduleWakeup`, `CronCreate`, or sleep-poll for new tasks.
|
||||
- **Explicit user signal** — «стоп», «хватит», «отбой». Park any in-flight claimed task (paused) before stopping.
|
||||
- **Budget** — `budget.remaining()` near zero → park the current task (paused) and report.
|
||||
|
||||
**Long-running watch (opt-in only).** If the user explicitly says «работай пока не скажу стоп» *and* wants you to keep checking for newly-arrived tasks, use **`ScheduleWakeup`** — it re-invokes *this* session — with a **long** interval (≥1200 s). **Never `CronCreate`** even here: it starts a *separate* scheduled session, i.e. the daemon this skill exists to avoid. And never a short poll. Default is still stop-on-empty; only arm a wakeup on an explicit standing request.
|
||||
|
||||
## Heartbeat
|
||||
|
||||
A claim lives 10 minutes. If a single task will take longer than ~8 minutes, call `tasks_heartbeat(slug, claim_token)` periodically to keep the claim alive. Short tasks need no heartbeat. (The in-session `.tasks/.lock` is a separate 2-hour lock owned by `using-tasks` session start/end — don't manage it from the loop.)
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- **No daemon, no `CronCreate`, no spawned claude, no subprocess** to "run the queue" — `CronCreate` starts a separate scheduled session; the whole point is you do it in *this* session.
|
||||
- **No busy-poll on empty** — empty queue is a natural stop, not a wait-loop. A short `ScheduleWakeup` loop burns tokens for nothing. The only exception is the explicit long-watch opt-in above (a single ≥1200 s `ScheduleWakeup`, never `CronCreate`).
|
||||
- **Don't widen scope silently** — default to the current project; claim other boards only when the user asks.
|
||||
- **Don't `tasks_close` unfinished work** and **don't leave a task claimed** when blocked — park it (blocked/paused).
|
||||
- **Don't skip the `session_break` gate** between tasks — a milestone/domain-switch marker means stop, even if more tasks are ready.
|
||||
- **Don't autopilot through a `human-only`/`strict-human` task's close/commit**, and **never auto-push** — consult first.
|
||||
- **Don't blind-retry** a task that failed for an external reason — diagnose once, record the blocker, move on.
|
||||
|
||||
## Red flags — STOP
|
||||
|
||||
- "I'll set a timer to check for new tasks" → no. Stop on empty; report.
|
||||
- "I'll spawn a background worker to drain faster" → no. One task at a time, this session.
|
||||
- "User said work-until-stop, I'll `CronCreate` a recurring run" → no. `CronCreate` is a separate scheduled session = the daemon. Long-watch uses a single long `ScheduleWakeup` on *this* session.
|
||||
- "It's sensitive but consult_policy says auto, I'll just commit" → push still needs a grant; sensitive close still respects the gate.
|
||||
- "The task isn't done but I'll close it and note it" → never close unfinished. Park it.
|
||||
Reference in New Issue
Block a user