feat(dev): lab auto-launches AFK runs per project under the global cap #75

Merged
dominik.polakovics merged 1 commit from afk/64 into main 2026-06-02 10:33:36 +02:00

What

Implements the automatic AFK mode (parent #61, governed by ADR-0007). Each Forgejo-hosted project gains a persisted autoEnabled toggle in its ⋯ menu, and a server-side scheduler launches AFK runs on its own while the toggle is on and ready issues remain — serial per project, additive to manual runs, under the global instance cap.

How it maps to the acceptance criteria

  • Persisted autoEnabled — new per-project field in the JSON store, keyed by project name like lastOpenedAt; round-trips across a restart (Store.AutoEnabled/SetAutoEnabled, TestStore_autoEnabledRoundTrips).
  • ⋯-menu toggle — an Auto AFK runs: On/Off stateful form-button (never <input type=checkbox>, whose checked the DOM morph treats as client-owned and never repaints; the label is server text the morph syncs). Posts /afk/auto/<project> to flip + persist + re-render. Forgejo-only; a direct POST off git.cloonar.com is refused, not just hidden.
  • Scheduler goroutinerunAFKScheduler on a ~45s ticker (ADR-0007's 30–60s band), a second long-lived worker distinct from the reaper and the ~4s client poll. No-ops when the AFK seams aren't wired.
  • Shared claim path — the manual handler's select → cap-check → claim → worktree → trust-seed → spawn → rollback core is factored into launchAFKRun (holding afkMu), reused verbatim by the handler and the scheduler so the claim stays single-flighted.
  • Serial per project / additive manual — auto runs carry an afk-auto-<N> session-name marker (vs manual afk-<N>); launchAFKRun re-checks one-auto-run-per-project under afkMu on fresh liveness, so a toggle-on kick is race-safe against the ticker. Manual runs never register as auto, so they're additive (bounded only by the cap).
  • Restart-proof markerparseAFKLabel recovers both kinds and the issue number, so the reaper still reaps auto runs (the documented hazard: a naive afk-auto-<N> would have broken strconv.Atoi and silently stopped reaping). A restart re-adopts runs with the right kind from their names.
  • Drains one at a time / idles / launches at toggle-on / waits at cap — covered by the scheduler sweep + the toggle-on kick; at the cap it launches nothing and does not error.
  • Pure launch predicateshouldLaunchAuto(autoEnabled ∧ under-cap ∧ no-auto-in-flight ∧ ready-issue-exists), table-tested in isolation from tmux/tea/clock (TestShouldLaunchAuto), shaped (a struct + single expression) so #65's && !paused term slots in without restructuring.

Logged-out ticks claim nothing (a doomed session would park its issue in in-progress for nothing), gated on a fresh login check exactly as the manual Start is.

Tests

New: store round-trip, the pure predicate, scheduler launch / serial-per-project / manual-additive / auto-off / no-ready-idle / at-cap / concurrent-sweeps-stay-serial (race) / logged-out, the toggle handler (persist + refuse-non-Forgejo + On/Off render), auto-run-is-reaped (hazard regression), and extended label round-trip + reject tables for the auto marker.

go test ./..., go test -race ./..., go vet ./..., go build ./..., and gofmt -l all clean in the lab module. The repo pre-commit hook is eval-only (nix-instantiate) and does not exercise the Go, so the module was validated locally; the fw dry-build passes.

Known limitation (follow-up for #65, out of scope here)

A run that fails (death/timeout) parks its issue in in-progress and keeps its afk/<N> branch + worktree for inspection (#63) — so the scheduler does not auto-churn on it (it's out of the ready queue). But if a human later requeues that issue to ready-for-agent without removing the leftover afk/<N> branch, the next claim's git worktree add -b afk/<N> fails and rolls the label back, so an auto-enabled project can re-attempt it each tick. This is a pre-existing property of the #62 claim path (faithfully reused here); the clean fix is the #65 reset UX cleaning up the branch (or a self-healing claim that doesn't clobber the kept-for-inspection worktree). Not addressed here to respect this slice's scope (ADR-0007 defers failure handling — counter + three-strikes pause + reset — to #65).

Closes #64

## What Implements the **automatic AFK mode** (parent #61, governed by ADR-0007). Each Forgejo-hosted project gains a persisted `autoEnabled` toggle in its ⋯ menu, and a server-side scheduler launches AFK runs on its own while the toggle is on and ready issues remain — serial per project, additive to manual runs, under the global instance cap. ## How it maps to the acceptance criteria - **Persisted `autoEnabled`** — new per-project field in the JSON store, keyed by project name like `lastOpenedAt`; round-trips across a restart (`Store.AutoEnabled`/`SetAutoEnabled`, `TestStore_autoEnabledRoundTrips`). - **⋯-menu toggle** — an `Auto AFK runs: On/Off` stateful **form-button** (never `<input type=checkbox>`, whose `checked` the DOM morph treats as client-owned and never repaints; the label is server text the morph syncs). Posts `/afk/auto/<project>` to flip + persist + re-render. Forgejo-only; a direct POST off `git.cloonar.com` is refused, not just hidden. - **Scheduler goroutine** — `runAFKScheduler` on a ~45s ticker (ADR-0007's 30–60s band), a second long-lived worker distinct from the reaper and the ~4s client poll. No-ops when the AFK seams aren't wired. - **Shared claim path** — the manual handler's `select → cap-check → claim → worktree → trust-seed → spawn → rollback` core is factored into `launchAFKRun` (holding `afkMu`), reused verbatim by the handler and the scheduler so the claim stays single-flighted. - **Serial per project / additive manual** — auto runs carry an `afk-auto-<N>` session-name marker (vs manual `afk-<N>`); `launchAFKRun` re-checks one-auto-run-per-project under `afkMu` on fresh liveness, so a toggle-on kick is race-safe against the ticker. Manual runs never register as auto, so they're additive (bounded only by the cap). - **Restart-proof marker** — `parseAFKLabel` recovers both kinds *and* the issue number, so the reaper still reaps auto runs (the documented hazard: a naive `afk-auto-<N>` would have broken `strconv.Atoi` and silently stopped reaping). A restart re-adopts runs with the right kind from their names. - **Drains one at a time / idles / launches at toggle-on / waits at cap** — covered by the scheduler sweep + the toggle-on kick; at the cap it launches nothing and does not error. - **Pure launch predicate** — `shouldLaunchAuto(autoEnabled ∧ under-cap ∧ no-auto-in-flight ∧ ready-issue-exists)`, table-tested in isolation from tmux/tea/clock (`TestShouldLaunchAuto`), shaped (a struct + single expression) so #65's `&& !paused` term slots in without restructuring. Logged-out ticks claim nothing (a doomed session would park its issue in `in-progress` for nothing), gated on a fresh login check exactly as the manual Start is. ## Tests New: store round-trip, the pure predicate, scheduler launch / serial-per-project / manual-additive / auto-off / no-ready-idle / at-cap / concurrent-sweeps-stay-serial (race) / logged-out, the toggle handler (persist + refuse-non-Forgejo + On/Off render), auto-run-is-reaped (hazard regression), and extended label round-trip + reject tables for the auto marker. `go test ./...`, `go test -race ./...`, `go vet ./...`, `go build ./...`, and `gofmt -l` all clean in the lab module. The repo pre-commit hook is eval-only (`nix-instantiate`) and does not exercise the Go, so the module was validated locally; the `fw` dry-build passes. ## Known limitation (follow-up for #65, out of scope here) A run that fails (death/timeout) parks its issue in `in-progress` and keeps its `afk/<N>` branch + worktree for inspection (#63) — so the scheduler does **not** auto-churn on it (it's out of the ready queue). But if a human later requeues that issue to `ready-for-agent` *without* removing the leftover `afk/<N>` branch, the next claim's `git worktree add -b afk/<N>` fails and rolls the label back, so an auto-enabled project can re-attempt it each tick. This is a pre-existing property of the #62 claim path (faithfully reused here); the clean fix is the #65 reset UX cleaning up the branch (or a self-healing claim that doesn't clobber the kept-for-inspection worktree). Not addressed here to respect this slice's scope (ADR-0007 defers failure handling — counter + three-strikes pause + reset — to #65). Closes #64
Add the automatic AFK mode: a persisted per-project autoEnabled toggle in
the ⋯ menu plus a server-side scheduler that launches AFK runs on its own —
serial per project, additive to manual runs, under the global instance cap,
draining the ready-for-agent queue one issue at a time and idling when empty.

- store: persist a per-project autoEnabled flag (round-trips across restart).
- ⋯ menu: an "Auto AFK runs: On/Off" stateful form-button — NOT a checkbox,
  whose checked state the DOM morph treats as client-owned and never syncs —
  posting /afk/auto/<project> to flip+persist+re-render. Forgejo-only; a
  direct POST off git.cloonar.com is refused, not just hidden.
- scheduler: a second long-lived worker (~45s), distinct from the reaper and
  the ~4s client poll. Launches via the shared claim path, waits (never
  errors) at the cap, and kicks one sweep on toggle-on so work starts promptly.
- marker: auto runs carry an afk-auto-<N> session-name label (vs manual
  afk-<N>); parseAFKLabel recovers both kinds and the issue number, so the
  reaper still reaps auto runs and a restart re-adopts them with the right
  kind.
- refactor: factor the manual handler's select→claim→worktree→spawn core into
  a shared launchAFKRun (under afkMu) so the handler and scheduler share one
  single-flighted claim path; an auto launch re-checks one-auto-per-project
  there, making the toggle-on kick race-safe against the ticker.
- a pure, table-tested shouldLaunchAuto predicate decides each launch
  (autoEnabled ∧ under-cap ∧ no-auto-in-flight ∧ ready-issue-exists), shaped
  so #65's not-paused term slots in without restructuring.

Verified with go test/vet/build in the lab module (the repo pre-commit hook
is eval-only and does not exercise the Go).
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos!75
No description provided.