From 0016c458d1d619a976e1dcc92437192e1e6d52d2 Mon Sep 17 00:00:00 2001 From: vitya Date: Thu, 11 Jun 2026 17:33:35 +0300 Subject: [PATCH] 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) --- .tasks/task-loop-skill.md | 45 +++++++++++++++ skills/task-loop/SKILL.md | 117 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 .tasks/task-loop-skill.md create mode 100644 skills/task-loop/SKILL.md diff --git a/.tasks/task-loop-skill.md b/.tasks/task-loop-skill.md new file mode 100644 index 0000000..1f8cb74 --- /dev/null +++ b/.tasks/task-loop-skill.md @@ -0,0 +1,45 @@ +# task-loop-skill + +## Goal +Write a new skill `task-loop` for interactive Claude Code sessions: the agent in an open +session claims tasks from the board and works them one-by-one **in that same session** — +no separate daemon, no spawned claude processes. Empty queue → stop and report (never +busy-poll). The skill must coordinate with `using-tasks` v1.4.0 (session `.tasks/.lock`, +`session_break` gate, 10-min claim TTL → `tasks_heartbeat`) and `project-discipline` +(push-gate Rule 4, sensitive artifacts). + +## Key files +- `skills/task-loop/SKILL.md` — the deliverable (to be created) +- `skills/using-tasks/SKILL.md:120-179` — session-lock guard + session-break + completion gates the loop must honor +- `skills/project-discipline/SKILL.md` — push Rule 4, sensitive-artifact gates +- `skills/delegate-task/SKILL.md` — sibling task-system skill (style reference) +- projects-meta tools: `tasks_claim_next` (returns slug/weight/claim_token/consult_policy), `tasks_close`, `tasks_update`, `tasks_heartbeat` + +## Decisions log +Reverse-chronological. Append-only. +- 2026-06-11: **RED baseline run** (2 clean-context subagents, dry-run, no live tools). Finding: ecosystem already produces mostly-correct behavior (no busy-wait on empty, pointed heartbeat, parks blocked tasks, push only on grant). Real gaps the skill must close: (A) **claim scope diverged** — agent-1 used `filter={}` cross-federation, agent-2 `{project:current}`; (B) **both missed session-break gate** between tasks; (C) **both ignored `.tasks/.lock`**; (D) **autonomy vs sensitive-gate boundary unclear** — agent-1 injected an unasked confirmation stop on a CI task; (E) **paused vs blocked** — spec said paused, agent-2 chose `blocked` for an external blocker (more correct). +- 2026-06-11: Design resolutions (recommend-don't-menu, no user objection to proposal): + - Scope default = **current project** (`filter={project:}`); multi-project only via explicit arg / POLLER_PROJECTS. + - Autonomy gate via **`consult_policy`** from claim: `auto`→full autopilot; `human-only`/`strict-human`→do the work but STOP before the irreversible step (commit/close) to consult. `weight:needs-human` never reaches the loop (server excludes from autonomous claim). Push never automatic (project-discipline Rule 4). + - Failed task: external/unresolvable blocker → `tasks_update status=blocked` + blocker note (frees claim, don't leave hanging, don't `close`, roll back partial work); interrupted/resumable-by-me → `status=paused`. (Refines acceptance #3 literal "paused".) + - Empty queue → STOP + report. `ScheduleWakeup` ONLY on explicit "работай пока не скажу стоп", interval ≥1200s. + - session-break: after each close, BEFORE next claim, honor `using-tasks` session_break marker → STOP. Loop delegates this gate, doesn't reimplement. + - Heartbeat: single task expected >~8 min → `tasks_heartbeat(slug, claim_token)`. + +## Open questions +- [x] Sensitive-task confirmation driven by `consult_policy` (the contract) + project-discipline push-gate for the riskiest step — NO blanket overlay. Resolved: compliance test B confirmed the `human-only` gate stops before close/commit correctly; push stays ask-mode regardless. consult_policy=auto means autopilot through close (push still needs a grant). + +## Completed steps +- [x] Claimed task (meta status=active, commit 9168a14), synced local +- [x] Read mandatory skills: writing-skills, test-driven-development +- [x] Recon: skills/ layout, heartbeat refs, using-tasks session-lock section, claim/close tool schemas +- [x] RED baseline: 2 subagents, gaps A–E documented above +- [x] GREEN: wrote skills/task-loop/SKILL.md v0.1.0 (desc trimmed of workflow summary per CSO rule) +- [x] GREEN compliance: 2 subagents. B (blocked/consult/break) PERFECT — all gaps A–E fixed (scope=current, human-only→stop-before-close, session_break halts drain, external→blocked not close, empty→stop). A (scope/empty/watch) clean EXCEPT chose CronCreate for long-watch → loophole. +- [x] REFACTOR: long-watch carve-out reworded to mandate ScheduleWakeup (same session) and forbid CronCreate (separate session=daemon) always; core/What-NOT/red-flags aligned. Re-test PASSED — agent picks ScheduleWakeup 1800s, rejects CronCreate with correct reasoning. +- [x] Acceptance 1-6 all met (see commit). TDD cycle RED→GREEN→REFACTOR complete. + +## Notes +- `heartbeat-side-channel` skill referenced in acceptance #4 does NOT exist — resolved by documenting `tasks_heartbeat` usage directly. +- Installed `~/.claude/skills/using-tasks` appears older than repo source (no session-lock) — deployment gap, not this task's concern. Write the skill against the repo source (v1.4.0). +- Notify target on close: OpeItcLoc03/workshop. diff --git a/skills/task-loop/SKILL.md b/skills/task-loop/SKILL.md new file mode 100644 index 0000000..ce1f0ab --- /dev/null +++ b/skills/task-loop/SKILL.md @@ -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` = `::` (e.g. `DESKTOP-NSEF0UK:claude-opus:`). + - `filter.project` = **the current project** (qualified `/`) 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 `.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="")`. 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.