@reddb-io/red-skills-dev 2.75.2
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/LICENSE +202 -0
- package/README.md +30 -0
- package/package.json +32 -0
- package/skills/engineering/README.md +36 -0
- package/skills/engineering/_report-runtime/WRAPPER.md +47 -0
- package/skills/engineering/afk/AGENT-PROMPT.md +295 -0
- package/skills/engineering/afk/SAFETY.md +103 -0
- package/skills/engineering/afk/SKILL.md +160 -0
- package/skills/engineering/afk/TROUBLESHOOTING.md +260 -0
- package/skills/engineering/afk/actions-lane.md +213 -0
- package/skills/engineering/afk/bin/README.md +29 -0
- package/skills/engineering/afk/bin/afk.mjs +51 -0
- package/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
- package/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
- package/skills/engineering/afk/defaults/gradle-pre-worktree.sh +42 -0
- package/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
- package/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
- package/skills/engineering/afk/detectors/README.md +72 -0
- package/skills/engineering/afk/detectors/cargo.sh +27 -0
- package/skills/engineering/afk/detectors/gradle.sh +35 -0
- package/skills/engineering/afk/docs/BOOT-SWEEPS.md +81 -0
- package/skills/engineering/afk/docs/CONFIG.md +238 -0
- package/skills/engineering/afk/docs/ENVELOPE.md +177 -0
- package/skills/engineering/afk/docs/HANDOFF.md +70 -0
- package/skills/engineering/afk/docs/LIVENESS.md +56 -0
- package/skills/engineering/afk/docs/OPERATIONS.md +409 -0
- package/skills/engineering/afk/examples/only-mine.sh +32 -0
- package/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
- package/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
- package/skills/engineering/afk/fleet.md +127 -0
- package/skills/engineering/afk/hooks/red-cargo +44 -0
- package/skills/engineering/afk/hooks/red-envelope +50 -0
- package/skills/engineering/afk/hooks/red-gradle +50 -0
- package/skills/engineering/afk/hooks/red-heartbeat +46 -0
- package/skills/engineering/afk/hooks/red-validation +79 -0
- package/skills/engineering/afk/monitor.md +170 -0
- package/skills/engineering/afk/runner-claude-minimax.md +71 -0
- package/skills/engineering/afk/runner-claude.md +67 -0
- package/skills/engineering/afk/runner-codex.md +138 -0
- package/skills/engineering/afk/runner-hermes.md +40 -0
- package/skills/engineering/afk/runner-opencode.md +142 -0
- package/skills/engineering/ask-red/SKILL.md +137 -0
- package/skills/engineering/audit-skills/SKILL.md +46 -0
- package/skills/engineering/code-review/SKILL.md +106 -0
- package/skills/engineering/context/SKILL.md +142 -0
- package/skills/engineering/curate/SKILL.md +133 -0
- package/skills/engineering/daily-review/SKILL.md +68 -0
- package/skills/engineering/dashboard/SKILL.md +44 -0
- package/skills/engineering/diagnose/SKILL.md +155 -0
- package/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/skills/engineering/go/SKILL.md +144 -0
- package/skills/engineering/go/TROUBLESHOOTING.md +81 -0
- package/skills/engineering/ground-truth/SKILL.md +84 -0
- package/skills/engineering/hitl/SKILL.md +171 -0
- package/skills/engineering/hitl/TROUBLESHOOTING.md +76 -0
- package/skills/engineering/implement/SKILL.md +69 -0
- package/skills/engineering/improve-codebase-architecture/DEEPENING.md +37 -0
- package/skills/engineering/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/engineering/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/skills/engineering/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/engineering/model-tier-policy/SKILL.md +83 -0
- package/skills/engineering/prototype/LOGIC.md +79 -0
- package/skills/engineering/prototype/SKILL.md +30 -0
- package/skills/engineering/prototype/UI.md +112 -0
- package/skills/engineering/red-doctor/APPLY.md +35 -0
- package/skills/engineering/red-doctor/SKILL.md +172 -0
- package/skills/engineering/red-gains/SKILL.md +49 -0
- package/skills/engineering/red-setup/INTERVIEW.md +266 -0
- package/skills/engineering/red-setup/ISSUE-SWEEP.md +18 -0
- package/skills/engineering/red-setup/REFERENCE.md +35 -0
- package/skills/engineering/red-setup/SKILL.md +58 -0
- package/skills/engineering/red-setup/WORKFLOWS.md +74 -0
- package/skills/engineering/red-setup/WRITE-CONTRACT.md +151 -0
- package/skills/engineering/red-setup/config-template.yaml +174 -0
- package/skills/engineering/red-setup/domain.md +52 -0
- package/skills/engineering/red-setup/issue-tracker-github.md +88 -0
- package/skills/engineering/red-setup/rsp-retirement-runbook.md +35 -0
- package/skills/engineering/red-setup/scripts/install-runtime-shim.sh +189 -0
- package/skills/engineering/red-setup/triage-labels.md +176 -0
- package/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
- package/skills/engineering/red-statusline/HOST-NOTES.md +180 -0
- package/skills/engineering/red-statusline/SKILL.md +58 -0
- package/skills/engineering/report-bug/SKILL.md +155 -0
- package/skills/engineering/resolving-merge-conflicts/SKILL.md +143 -0
- package/skills/engineering/retake/SKILL.md +184 -0
- package/skills/engineering/review-adrs/SKILL.md +125 -0
- package/skills/engineering/start/ADR-FORMAT.md +47 -0
- package/skills/engineering/start/CONTEXT-FORMAT.md +77 -0
- package/skills/engineering/start/DOC-LANDING-FINALIZER.md +9 -0
- package/skills/engineering/start/SKILL.md +179 -0
- package/skills/engineering/tdd/SKILL.md +96 -0
- package/skills/engineering/tdd/deep-modules.md +33 -0
- package/skills/engineering/tdd/interface-design.md +31 -0
- package/skills/engineering/tdd/mocking.md +59 -0
- package/skills/engineering/tdd/tests.md +85 -0
- package/skills/engineering/to-spec/SKILL.md +104 -0
- package/skills/engineering/to-tickets/SKILL.md +158 -0
- package/skills/engineering/triage/AGENT-BRIEF.md +207 -0
- package/skills/engineering/triage/OUT-OF-SCOPE.md +101 -0
- package/skills/engineering/triage/SKILL.md +176 -0
- package/skills/engineering/verify/SKILL.md +136 -0
- package/skills/engineering/wayfinder/SKILL.md +128 -0
- package/skills/engineering/zoom-out/SKILL.md +51 -0
- package/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
- package/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
- package/skills/knowledge/README.md +5 -0
- package/skills/knowledge/research/SKILL.md +76 -0
- package/skills/misc/README.md +8 -0
- package/skills/misc/branch-lock/SKILL.md +187 -0
- package/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
- package/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
- package/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
- package/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
- package/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
- package/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
- package/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
- package/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
- package/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
- package/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +229 -0
- package/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
- package/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
- package/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
- package/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
- package/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
- package/skills/misc/git-guardrails-claude-code/SKILL.md +105 -0
- package/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
- package/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
- package/skills/misc/migrate-to-shoehorn/SKILL.md +96 -0
- package/skills/misc/setup-pre-commit/SKILL.md +94 -0
- package/skills/productivity/README.md +8 -0
- package/skills/productivity/ff/SKILL.md +116 -0
- package/skills/productivity/handoff/SKILL.md +37 -0
- package/skills/productivity/reflect/SKILL.md +18 -0
- package/skills/productivity/write-a-skill/SKILL.md +190 -0
- package/skills/productivity/write-a-skill/WRITING-STYLE.md +62 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: afk
|
|
3
|
+
description: Autonomous loop that drains the `ready-for-agent` queue on the issue tracker. Each iteration claims an issue, runs it in an isolated worktree, executes with claude or codex, merges back to main, and closes the issue. Use when the user wants to run AFK execution, drain a Spec, hammer specific issues, or otherwise let agents grind through the backlog.
|
|
4
|
+
argument-hint: "[--spec N | --issues N,N,N] [--runner claude|codex|opencode] [--alternate] [--fallback-runner] [--request TEXT] [-n N] [--once] [--boot-only] | fleet [N] | fleet stop | monitor | dashboard | daily-review | weekly-review | retake N | reap"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /afk
|
|
8
|
+
|
|
9
|
+
**The default lane for all tracked backlog work.** `/afk` is the modus operandi; `/go` is the ad-hoc-only exception. Run the bundle, don't read the source — this `SKILL.md` is the complete behavioural contract; the `bin/` bundle and the `scripts/` files are build/runtime artifacts, and everything an agent needs to operate `/afk` is in this file.
|
|
10
|
+
|
|
11
|
+
Drain the agent-ready backlog. Single skill that owns issue selection, worktree isolation, inner-agent execution, GitHub state coordination, merge-back, and runner-fallback.
|
|
12
|
+
|
|
13
|
+
## Runtime & Invocation
|
|
14
|
+
|
|
15
|
+
The skill ships a single committed runtime bundle. Invoke it as:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
RED_AFK_RUNNER=<claude|codex|opencode> red-skills-dev <command> [params]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The invoking LLM is responsible for setting `RED_AFK_RUNNER` to its own host runner (`codex` from Codex, `claude` from Claude Code). Do not infer a different runner from binaries on `PATH`; use `--runner` only when the user explicitly pinned one.
|
|
22
|
+
|
|
23
|
+
`afk.mjs` is a **dedicated forwarder** (ADR 0039 entrypoint, build role `run:dev`): every argument is passed straight to the `dev` bundle, whose own command surface (`run`, `monitor`, `fleet`, …) is documented below. So `… afk.mjs run --once`, `… afk.mjs monitor`, and the bare `… afk.mjs --issues 42` all reach the orchestrator. The generic entrypoint verbs (`run <plugin>` / `fetch`) belong to `red-fetch.mjs`, not to this launcher — they do **not** shadow the bundle's commands (#434).
|
|
24
|
+
|
|
25
|
+
Commands and their parameters are documented in *When To Use* below — that section is authoritative for the CLI surface.
|
|
26
|
+
|
|
27
|
+
The bundle is a single self-contained build (one file, one inlined runtime dependency, no `node_modules`, no install step) and is the public entrypoint. Every command — orchestration, supervisor, statusline, and hooks — executes natively in the bundle.
|
|
28
|
+
|
|
29
|
+
## Execution Substrate (ADR 0033)
|
|
30
|
+
|
|
31
|
+
The per-issue **agent run** executes on `@reddb-io/red-castle` — reddb.io's vendored sandcastle fork, committed under `packages/red-castle` and consumed as source (ADR 0061/0101) — not on a hand-rolled `claude -p` / `codex exec` session whose stdout is grepped for stage transitions. The boundary is clean: **the substrate owns execution, AFK owns the issue policy**.
|
|
32
|
+
|
|
33
|
+
- **sandcastle** (one `run()` call per attempt) spawns the inner agent, creates and manages the git worktree, runs the configured sandbox, captures the agent's stream, detects the completion signal, and lands the agent's commits on the worker branch.
|
|
34
|
+
- **AFK** keeps everything around that call: issue selection, the three-layer claim, the handoff file, the package-aware feedback gate, lock-toggled landing (ADR 0030), base resolution (ADR 0031), the terminal-event envelope, close, and the boot/monitor/mirror sweeps.
|
|
35
|
+
|
|
36
|
+
AFK drives the sandcastle Orchestrator through **injected providers** (`SandcastleDeps`: `run`, `agentFor`, `sandboxFor`) so the single adapter module is the only code coupled to the package. The pure mapping (`buildRunOptions` → `RunOptions`, `interpretOutcome` → outcome) is unit-tested with `run` injected; the real providers are wired lazily once, on the first agent run, so a `monitor` / `reap` / empty-queue path never imports sandcastle. AFK's canonical sentinels `<promise>DONE</promise>` and `<promise>BLOCKED</promise>` are registered as sandcastle `completionSignal`s, so the [`AGENT-PROMPT.md`](../AGENT-PROMPT.md) contract is unchanged — the agent still authors its own exit.
|
|
37
|
+
|
|
38
|
+
`run()` returns `{ branch, commits, completionSignal }`; AFK maps `completionSignal` to an outcome (`done` / `blocked` / `no-sentinel`) and proceeds with its own feedback → landing → envelope → close. Execution is a **single `runAgent` call**, not a multi-mode dispatch over named run-modes.
|
|
39
|
+
|
|
40
|
+
## When To Use
|
|
41
|
+
|
|
42
|
+
- `/afk` — every issue currently labelled `ready-for-agent`.
|
|
43
|
+
- `/afk --spec 42` — only issues that reference Spec #42 (by `spec: #42` line in body, parent link, or `spec:42` label).
|
|
44
|
+
- `/afk --issues 356,359,362` — explicit list, in that order.
|
|
45
|
+
- `/afk --runner codex` — pin a backend (disables detection cascade; mutually exclusive with `--alternate`).
|
|
46
|
+
- `/afk --alternate` — opt in to round-robin runner rotation between issues (claude → codex → claude → …).
|
|
47
|
+
- `/afk --fallback-runner` — opt in to swapping runners mid-issue when one returns `RUNNER_EXHAUSTED`. Without this flag, exhaustion routes the issue through bounded `blocked:quota` recovery and stops the outer run with exit 75.
|
|
48
|
+
- `/afk --request "dont run cargo tests for this issue resolution"` or `/afk -r "..."` — add a special user request block to every inner-agent prompt for this run.
|
|
49
|
+
- `/afk -n 5` — cap at five issues. `-n N` caps the run at `N` issues; `-n 0` (and omitting `-n`) drains the whole queue until it is empty (`0` means unlimited, not zero). For a no-agent dry-run use `--boot-only` instead.
|
|
50
|
+
- `/afk --once` — single supervised iteration. Use for debugging the prompt.
|
|
51
|
+
- `/afk --boot-only` — run the boot sweeps then exit without claiming or spawning an agent; a safe dry-run to inspect bootstrap / orphan-cleanup / unblock-sweep / precheck.
|
|
52
|
+
- `/afk monitor` — readonly status board that aggregates every `.red/tmp/workers/*/*/afk.state.json` so you see all live workers from another terminal, and mirrors them onto the host runner's native task surface. Running `afk monitor` → read [`monitor.md`](../monitor.md) for the full protocol (dashboard render plus the binding task mirror).
|
|
53
|
+
- `/afk dashboard [--period 30d] [--json]` — readonly process dashboard: open Specs/issues, global `running` issues, local AFK workers on this machine, issue/PR flow metrics, and DORA proxy metrics.
|
|
54
|
+
- `/afk daily-review [--json]` — readonly daily operational review from yesterday local midnight to now: delivery big numbers, local worker attempts/time, token spend when available, HITL/blocker challenges, and issue/PR cycle times.
|
|
55
|
+
- `/afk weekly-review [--json]` — readonly six-day operational review from six-days-ago local midnight to now, with the same sections as `daily-review`.
|
|
56
|
+
- `/afk retake 123 [--apply] [--json]` — issue resumption report: reads the issue, linked PRs, matching local/remote branches, matching local worktrees, HITL state, and prints the next command to continue, fix, recreate a work worktree, or run `requeue`. With `--apply`, executes only safe local setup `git` operations and still leaves merges/HITL to `requeue` or `/hitl`. The parser accepts `#123` too; quote it when invoking through a shell.
|
|
57
|
+
- `/afk fleet [N]` — launch the supervisor maintaining `N` concurrent workers (default `2`). Running `afk fleet` → read [`fleet.md`](../fleet.md) for the full launch/stop/supervisor protocol.
|
|
58
|
+
- `/afk fleet stop` — gracefully shut down a running fleet supervisor and cancel its auto-monitor cron.
|
|
59
|
+
- `/afk reap` — run branch hygiene without starting a worker: count lines for remote `afk/*` and local `afk/*`, then the same safe live-branch reapers used at boot.
|
|
60
|
+
|
|
61
|
+
### Running `/afk` in an execution environment (GitHub Actions)
|
|
62
|
+
|
|
63
|
+
The same `/afk --issues N --runner opencode --once` command runs unchanged in a
|
|
64
|
+
GitHub Actions runner — one attempt, one issue, one PR per invocation, no fleet,
|
|
65
|
+
no admin-merge. Only the trigger and the secret-injection surface differ.
|
|
66
|
+
|
|
67
|
+
The lane is packaged as three layers (ADR 0059/0062): the reusable workflow
|
|
68
|
+
`.github/workflows/reusable-afk-attempt.yml` (triggers + trust gate) → the composite
|
|
69
|
+
action `.github/actions/afk-attempt` (execution) → the `afk.mjs` launcher +
|
|
70
|
+
Release bundle (runtime). Two adoption paths: **turnkey** (call the reusable) or
|
|
71
|
+
**composable** (`uses: reddb-io/red-skills/.github/actions/afk-attempt@v1` with
|
|
72
|
+
your own triggers/gate). Pin `@v1`/SHA for reproducibility. The composite action
|
|
73
|
+
carries its own red-skills checkout, so the launcher resolves in any adopter repo
|
|
74
|
+
— no workspace build, no submodule.
|
|
75
|
+
|
|
76
|
+
Triggers: `issues: labeled`/`opened` (on `ready-for-agent`), `workflow_dispatch`,
|
|
77
|
+
`workflow_call`. Trust gate (ADR 0085): author + label-actor must both be
|
|
78
|
+
allowlisted. Runner: opencode (API-auth); point it at OpenAI/MiniMax/OpenRouter
|
|
79
|
+
by wiring the matching key + a `<provider>/<model>` slug via the `model` input.
|
|
80
|
+
|
|
81
|
+
**→ Full adopter guide:
|
|
82
|
+
[`actions-lane.md`](../actions-lane.md)** (architecture, both examples, all
|
|
83
|
+
inputs, triggers, trust gate, auth precedence, the MiniMax recipe, permissions).
|
|
84
|
+
|
|
85
|
+
The k8s job manifest + real-environment E2E remain tracked as
|
|
86
|
+
[#631](https://github.com/reddb-io/red-skills/issues/631) (ADR 0059).
|
|
87
|
+
|
|
88
|
+
## Parallelization
|
|
89
|
+
|
|
90
|
+
`/afk` is **trivially parallel** — just open another terminal and run `/afk` again. No flag, no coordination, no slot to manage.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
/afk # terminal A → spawns worker "wZ2R4"
|
|
94
|
+
/afk # terminal B → spawns worker "wK7M2"
|
|
95
|
+
/afk # terminal C → spawns worker "w9RQP"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Each invocation generates its own **worker ID** — literal `w` plus 4 random characters from `[A-Z0-9]` (e.g. `wZ2R4`, ~1.7M possible IDs) — and uses it as the prefix for every per-run file. The leading `w` makes the worker directory `.red/tmp/workers/{id}` an unambiguous live-worker anchor. The ID is printed on the first line of the run so you can tail or kill it later.
|
|
99
|
+
|
|
100
|
+
Per-attempt files live under `.red/tmp/workers/{id}/{N}-a{n}/` in the primary checkout, where `{id}` is the worker ID, `{N}` is the issue number, and `{n}` is the per-issue attempt counter (derived by the attempt-ledger — every retry, even by a different worker, gets a fresh `a{n}` directory). Everything for one (worker, issue, attempt) is in one directory — when the attempt ends successfully the whole directory is removed; when it blocks the whole directory is preserved. The worker also holds a single per-worker liveness anchor at `.red/tmp/workers/{id}/worker.pid` (see the `worker.pid` row below).
|
|
101
|
+
|
|
102
|
+
| Path | Purpose |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `.red/tmp/workers/{id}/worker.pid` | Per-worker liveness anchor: the orchestrator's PID, written **once at bootstrap** and removed on the worker's EXIT trap (along with rmdir of the empty worker dir). The single liveness anchor for the worker; the fleet supervisor's slot matching keys off it. |
|
|
105
|
+
| `.red/tmp/workers/{id}/{N}-a{n}/worktree/` | Git worktree for issue `N` on attempt `n`. Lives inside the gitignored `.red/tmp/` so it never pollutes sibling directories. |
|
|
106
|
+
| `.red/tmp/workers/{id}/{N}-a{n}/afk.log` | Append-only plain log for this attempt (orchestrator output + inner-agent stdout + heartbeat lines). Per-attempt scope — each attempt gets a fresh log. |
|
|
107
|
+
| `.red/tmp/workers/{id}/{N}-a{n}/agent.log.jsonl` | Clean **agent lane** (issue #250) — one `type=agent` TOONL record per assistant turn and nothing synthetic, so it is the true liveness signal and reads as a live transcript: `tail -f … \| tq -p toonl -o json -r .msg`. Single-writer. `tq` is pinned by `/red-setup`; there is no jq fallback for RedSkills-owned TOONL lanes. |
|
|
108
|
+
| `.red/tmp/workers/{id}/{N}-a{n}/log.jsonl` | The **firehose** (issue #250) — every record of the attempt in the uniform JSONL envelope: agent turns, heartbeat vitals, hook dispatches, runner timings, and errors. Flock-serialised (many concurrent writers). |
|
|
109
|
+
| `.red/tmp/supervisors/default/supervisor.log.toonl` | Fleet supervisor firehose — append-only TOONL heartbeat records for the supervisor itself. Legacy `.red/tmp/afk-supervisor.log.jsonl`, `.red/state/afk/`, and `.red/state/castle/` structured firehose copies are boot-migrated into this live supervisor lane. Read with `tail -f .red/tmp/supervisors/default/supervisor.log.toonl \| tq -p toonl -o json -r .msg`. Readers sniff legacy JSONL and mixed files; the lane is never rewritten in place. |
|
|
110
|
+
| `.red/tmp/workers/{id}/{N}-a{n}/afk.state.json` | State snapshot for this attempt. Schema in [`docs/ENVELOPE.md`](./ENVELOPE.md). |
|
|
111
|
+
| `.red/tmp/workers/{id}/{N}-a{n}/handoff.md` | Handoff file the inner agent reads — `<issue-body>` (issue body verbatim, including the `## Agent brief` markdown section), `<previous-attempts>`, `<human-guidance-thread>` (one `<human-guidance>` per extracted directive), `<thread-discussion>` (advisory comments with no directive marker), `<agent-notes>`. Top-level XML wrappers make body/comments/notes unambiguous. Template in [`docs/HANDOFF.md`](./HANDOFF.md). |
|
|
112
|
+
|
|
113
|
+
Two workers cannot claim the same issue thanks to a local `mkdir` lock at `.red/tmp/claims/{N}/` plus a `gh issue view` pre-check before the edit. The gh edit itself is not atomic (see *Issue Lifecycle* below for the full three-layer scheme). The race surface is the brief window between two separate checkouts on the same host — acceptable for the intended scale.
|
|
114
|
+
|
|
115
|
+
## Hard Preconditions
|
|
116
|
+
|
|
117
|
+
Refuse to start if any fail — the user fixes them.
|
|
118
|
+
|
|
119
|
+
- `git remote -v`: SSH only. Reject HTTPS — never auto-rewrite.
|
|
120
|
+
- `gh auth status` succeeds.
|
|
121
|
+
- Repo has `main` branch: `git -C primary log -1 main` works.
|
|
122
|
+
- Label `ready-for-agent` exists; if not, point at `/triage`.
|
|
123
|
+
- `pnpm` is on PATH.
|
|
124
|
+
|
|
125
|
+
## Bootstrap
|
|
126
|
+
|
|
127
|
+
Run before the first iteration:
|
|
128
|
+
|
|
129
|
+
1. Ensure `.red/tmp/` exists (create) and in `.gitignore` (append if missing).
|
|
130
|
+
2. **Generate worker ID:** `w` + 4 random `[A-Z0-9]` chars (e.g. `wZ2R4`). Regenerate on live-directory collision. Print `worker: {id}` first.
|
|
131
|
+
3. **Detect runner** (first wins; log `runner: <r> (detected via <method>)`). Load the matching runner doc. Never probe `command -v`; swap only via `--fallback-runner`.
|
|
132
|
+
- `--runner X` pin (`opencode` valid only here or via env) → `RED_AFK_RUNNER` env → env-var sniff (`CLAUDECODE`/`CLAUDE_CODE_ENTRYPOINT`/`CLAUDE_CODE_SSE_PORT` → `claude`; `CODEX_HOME`/`CODEX_SANDBOX`/`CODEX_SANDBOX_NETWORK_DISABLED`/`CODEX_MANAGED_BY_NPM` → `codex`) → process-tree → path (`~/.claude/` → `claude`; `~/.codex/` → `codex`) → default `claude`.
|
|
133
|
+
4. Read [`SAFETY.md`](../SAFETY.md) — binding for every shell action.
|
|
134
|
+
5. **Write `worker.pid`:** create `.red/tmp/workers/{id}/`, write current PID **once** — the worker's liveness anchor for its whole lifetime.
|
|
135
|
+
6. Install signal handlers (SIGINT/SIGTERM/EXIT): release claim, preserve attempt dir, remove `worker.pid`, rmdir empty worker dir.
|
|
136
|
+
|
|
137
|
+
## Boot-time sweeps
|
|
138
|
+
|
|
139
|
+
At boot the bundle reclaims stale state — orphan attempt dirs (issue-state TTL), the per-issue attempt cap (#257), live `afk/*` branch cleanup, the **Docs Sweep** (origin-visible `.red/` docs before dispatch), and the on-demand `/afk reap` branch reaper (#275). Mechanics: [`docs/BOOT-SWEEPS.md`](./BOOT-SWEEPS.md).
|
|
140
|
+
|
|
141
|
+
## Dependency Unblock — `req:N` edges, close cascade + boot sweep
|
|
142
|
+
|
|
143
|
+
Dependencies are first-class **`req:N` edge labels** (one per blocker), and a dependency-blocked issue holds the **`blocked:dependency`** state — *not* `ready-for-human` (it is healthy, waiting, and never pages). Two mechanisms promote it to `ready-for-agent`:
|
|
144
|
+
|
|
145
|
+
**1. Close cascade (event-driven, the fast path).** Immediately after `/afk` closes an issue #N on the DONE path (after the completion sweep), it re-evaluates every dependent of #N:
|
|
146
|
+
|
|
147
|
+
1. `gh issue list --label req:N --state open --json number,labels`.
|
|
148
|
+
2. For each dependent, read its `req:*` labels and resolve each referenced issue's state (the just-closed #N is known closed; others via a cached lookup).
|
|
149
|
+
3. When **every** `req:*` of a dependent is now closed: `gh issue edit --remove-label blocked:dependency --add-label ready-for-agent` + post `🤖 /afk unblocked: all dependencies closed (#…)`.
|
|
150
|
+
|
|
151
|
+
Best-effort: a `gh` failure here logs a `warn:` and never fails the close — the boot sweep below catches anything the cascade missed.
|
|
152
|
+
|
|
153
|
+
**2. Unblock Sweep (boot-time, the safety net).** After [orphan cleanup and the Docs Sweep](./BOOT-SWEEPS.md) and before *Straggler Check*, `/afk` re-scans dependency-blocked issues by label and promotes any whose deps all closed:
|
|
154
|
+
|
|
155
|
+
1. `gh issue list` for open `blocked:dependency` issues with `number,labels,body`.
|
|
156
|
+
2. Deps come from the `req:*` labels (the source of truth); for pre-`req:N` issues with no such label, fall back to extracting `#N` refs under the literal `## Blocked by` body heading (`- [ ] #N`) only when the issue is still labelled `blocked:dependency`.
|
|
157
|
+
3. Resolve each dep via `gh issue view <N> --json state`; promote only when **every** dep is `CLOSED`.
|
|
158
|
+
4. On promotion: remove the holding label (`blocked:dependency`), add `ready-for-agent`, post the audit comment, and log `unblocked N issue(s): #A #B`.
|
|
159
|
+
|
|
160
|
+
`ready-for-human` is a human gate, not dependency-wait. The boot sweep must not promote it from a legacy `## Blocked by` body parse, because a closed blocker can still encode a failed measurement or a no-go decision. `blocked:dependency` issues do not have that ambiguity: the label *means* dependency-wait, which is the whole point of separating it from `ready-for-human`.
|
|
161
|
+
|
|
162
|
+
## Current Blocker State
|
|
163
|
+
|
|
164
|
+
Human gates are first-class issue-body state, not implicit thread archaeology. Before claiming an issue, `/afk` checks for an active `## Current blocker` block:
|
|
165
|
+
|
|
166
|
+
```md
|
|
167
|
+
## Current blocker
|
|
168
|
+
|
|
169
|
+
<!-- red:blocker-state v1 -->
|
|
170
|
+
status: blocked
|
|
171
|
+
kind: decision
|
|
172
|
+
ref: #856
|
|
173
|
+
summary: Phase 2 measured no columnar read win.
|
|
174
|
+
next: Human must decide whether to stop, redesign, or continue anyway.
|
|
175
|
+
<!-- /red:blocker-state -->
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
If this block is present with `status: blocked`, `/afk` does not create an attempt. It removes `ready-for-agent`, adds `ready-for-human` plus the typed blocker label, leaves the issue open, and waits for `/hitl`.
|
|
179
|
+
|
|
180
|
+
When an attempt escalates to a terminal human page (for example BLOCKED, validation failure, non-recoverable stall/infra, or a recoverable reason after retry-budget exhaustion), the runtime writes or replaces this block so the next `/hitl` turn can start from the current blocker instead of re-reading every old envelope. `/hitl` clears the block to `None`, records it under `## Resolved blockers`, refreshes `## Agent brief`, and moves the issue back to `ready-for-agent` only when the next agent can continue without guessing.
|
|
181
|
+
|
|
182
|
+
Use `## Blocked by` only for mechanical dependencies that should auto-promote on close. Use `## Current blocker` / `## Human decision needed` for gates, measurements, product calls, or any state where "the referenced issue closed" is not enough to prove the work is delegable.
|
|
183
|
+
|
|
184
|
+
## Straggler Check
|
|
185
|
+
|
|
186
|
+
Before issue selection, `/afk` counts open issues in states it cannot consume:
|
|
187
|
+
|
|
188
|
+
- `unlabeled` — never triaged
|
|
189
|
+
- `needs-triage` — triage in progress
|
|
190
|
+
- `needs-info` — waiting on reporter
|
|
191
|
+
|
|
192
|
+
If any of those are non-zero, print a warning and (on a TTY, not in `--once`) prompt to confirm before proceeding. This catches the "lost issue" case where a fresh report never made it through `/triage` and is silently invisible to `/afk`.
|
|
193
|
+
|
|
194
|
+
The systemic fix is the `red-issues-needs-triage.yml` workflow installed by `/red-setup`, which auto-applies `needs-triage` to every fresh issue. The straggler check is the in-loop safety net for repos where the workflow isn't installed yet.
|
|
195
|
+
|
|
196
|
+
## Issue Selection
|
|
197
|
+
|
|
198
|
+
Pull: `gh issue list --label ready-for-agent --state open --json number,title,labels,body --limit 100`. Drop every `type:spec` issue before any filter (log `/to-tickets N` warning for each). Prepend `priority:urgent` issues before any filter, oldest first.
|
|
199
|
+
|
|
200
|
+
Filters:
|
|
201
|
+
1. `--issues N…`: match requested numbers against the full post-Spec `ready-for-agent` set, urgent included; keep those numbers in argument order; error if missing or not `ready-for-agent`; Specs rejected.
|
|
202
|
+
2. `--spec N`: keep non-urgent issues with `spec: #N` in body, parent link, or `spec:N` label; Spec itself excluded.
|
|
203
|
+
3. Default: all remaining non-urgent `ready-for-agent`, `priority:high` first, then ascending by number.
|
|
204
|
+
|
|
205
|
+
Final queue: `[urgent…] + [filtered…]`, deduped. Empty → `<promise>NO MORE TASKS</promise>`, exit 0.
|
|
206
|
+
|
|
207
|
+
**Empty queue + non-empty backlog = flow bug, not a stop (binding on the invoking agent).** "Nothing ready" and "nothing to do" are different claims — never report the second when only the first is true. When the queue is empty but open non-Spec issues exist, print a one-line **gate census** — counts per gate: `blocked:dependency`, `ready-for-human`, `needs-triage`/`needs-info`, `type:spec` — and name the highest-leverage unblock. In particular, audit `blocked:dependency` edges whose `req:*` target no longer really pends: a **delivered-but-open Spec** strands every dependent, because the unblock cascade fires on *close*, and Specs close on manual bookkeeping (on 2026-07-02 two fully-delivered Specs froze 14 slices this way). The mission is maximizing autonomous drainage; humans gate only genuine decisions.
|
|
208
|
+
|
|
209
|
+
## Issue Lifecycle (the `/afk` slice)
|
|
210
|
+
|
|
211
|
+
Canonical state machine lives in [`red-setup/triage-labels.md`](../../red-setup/triage-labels.md). The portion `/afk` touches:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
ready-for-agent
|
|
215
|
+
│
|
|
216
|
+
(1) claim
|
|
217
|
+
remove ready-for-agent
|
|
218
|
+
add running
|
|
219
|
+
post start comment
|
|
220
|
+
│
|
|
221
|
+
▼
|
|
222
|
+
running
|
|
223
|
+
┌───┴───┐
|
|
224
|
+
│ │ inner agent works in worktree → emits DONE | BLOCKED
|
|
225
|
+
│ │ orchestrator runs feedback loops, then merges to main
|
|
226
|
+
│ │
|
|
227
|
+
│ ├──── DONE + green + merged + pushed
|
|
228
|
+
│ │ │
|
|
229
|
+
│ │ (4a) close
|
|
230
|
+
│ │ remove running
|
|
231
|
+
│ │ gh issue close --reason completed
|
|
232
|
+
│ │ │
|
|
233
|
+
│ │ ▼
|
|
234
|
+
│ │ closed
|
|
235
|
+
│ │
|
|
236
|
+
│ └──── terminal failure
|
|
237
|
+
│ │
|
|
238
|
+
│ classify Attempt Outcome
|
|
239
|
+
│ add typed blocked:<reason>
|
|
240
|
+
│ │
|
|
241
|
+
│ ┌────────┴────────┐
|
|
242
|
+
│ │ │
|
|
243
|
+
│ │ recoverable and │ non-recoverable, or
|
|
244
|
+
│ │ attempt < cap │ recoverable at/over cap
|
|
245
|
+
│ │ │
|
|
246
|
+
│ ▼ ▼
|
|
247
|
+
│ remove running remove running
|
|
248
|
+
│ add add
|
|
249
|
+
│ ready-for-agent ready-for-human
|
|
250
|
+
│ post/retry post blocker/budget
|
|
251
|
+
│ audit exhausted comment
|
|
252
|
+
│ │ │
|
|
253
|
+
│ ▼ ▼
|
|
254
|
+
│ ready-for-agent ready-for-human
|
|
255
|
+
│ (fresh attempt) (human gate)
|
|
256
|
+
│
|
|
257
|
+
└──── orchestrator interrupted (SIGINT/SIGTERM)
|
|
258
|
+
│
|
|
259
|
+
(4c) release
|
|
260
|
+
remove running
|
|
261
|
+
restore ready-for-agent
|
|
262
|
+
post interruption comment
|
|
263
|
+
│
|
|
264
|
+
▼
|
|
265
|
+
ready-for-agent (next /afk run can pick it up)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Label transitions are **not** atomic at the gh level — `gh issue edit --remove-label A --add-label B` resolves the new label set client-side and submits the union, so a removed-but-no-longer-present label is a silent no-op and the edit returns 0. To prevent two parallel `/afk` runners from both thinking they claimed the same issue, the per-issue claim uses three layers:
|
|
269
|
+
|
|
270
|
+
1. **Local `mkdir` lock** at `.red/tmp/claims/{N}/` (POSIX-atomic). Workers in the same checkout race here, and the loser skips.
|
|
271
|
+
2. **Pre-check** via `gh issue view --json labels` — if `ready-for-agent` is already gone or `running` is already present, abort before the edit. Cuts the cross-checkout race window to roughly one round-trip.
|
|
272
|
+
3. **Stale-lock sweep** at boot, during orphan cleanup — any `.red/tmp/claims/{N}/` whose recorded pid is dead gets reclaimed automatically.
|
|
273
|
+
|
|
274
|
+
Residual gap: two clones of the same repo on the same host (or different hosts) do not share `.red/tmp/`, so each holds its own mkdir lock and the gh edit race re-opens for the brief window the pre-check leaves uncovered. Acceptable for the intended scale (a few terminals, one checkout). If you need cross-host claim safety, gate `/afk` on a proper coordinator instead of GitHub labels.
|
|
275
|
+
|
|
276
|
+
### Typed Failure Labels And Recovery Caps
|
|
277
|
+
|
|
278
|
+
AFK labels terminal failures with a descriptive `blocked:<reason>` label in addition to the routing label. The typed label is observability: a retry path still adds `ready-for-agent`, and an escalated path still adds `ready-for-human`.
|
|
279
|
+
|
|
280
|
+
| Attempt Outcome | typed label | recovery policy | default cap |
|
|
281
|
+
|---|---|---|---|
|
|
282
|
+
| `exhausted` | `blocked:quota` | `quota` | `RED_AFK_RETRY_QUOTA=3` |
|
|
283
|
+
| `runner-transient` | `blocked:runner-transient` | `runner-transient` | `RED_AFK_RETRY_RUNNER_TRANSIENT=3` |
|
|
284
|
+
| `merge-conflict` | `blocked:merge-conflict` | `merge-conflict` | `RED_AFK_RETRY_MERGE=3` |
|
|
285
|
+
| `ci-failed` | `blocked:ci` | none — escalates to a human/CI-aware finisher (never re-runs the agent) | n/a |
|
|
286
|
+
| `ci-pending` | `blocked:ci` | none — escalates to a human/CI-aware finisher (never re-runs the agent) | n/a |
|
|
287
|
+
| `no-sentinel` | `blocked:crashed` | `crashed` | `RED_AFK_RETRY_CRASH=1` |
|
|
288
|
+
| `hook-aborted` | `blocked:policy` | `policy` | `RED_AFK_RETRY_POLICY=1` |
|
|
289
|
+
| `blocked` | `blocked:spec` | none — escalates immediately | n/a |
|
|
290
|
+
| `feedback-failed` | `blocked:validation` | none — escalates immediately | n/a |
|
|
291
|
+
| `stalled` | `blocked:stalled` | none — escalates immediately in the per-issue path | n/a |
|
|
292
|
+
| `infra` | `blocked:infra` | none — escalates immediately | n/a |
|
|
293
|
+
| `done` / `claim-lost` | none | none | n/a |
|
|
294
|
+
|
|
295
|
+
Recoverable reasons retry while the 1-based attempt number is less than the cap. At the cap and above, the same reason escalates to `ready-for-human`, keeps the typed `blocked:<reason>` label, and posts a retry-budget-exhausted comment. Missing, non-numeric, zero, or negative `RED_AFK_RETRY_*` values fall back to the default cap.
|
|
296
|
+
|
|
297
|
+
**`blocked:ci` never re-runs the agent (#812).** On an `enforce_admins` base, an admin-merge cannot bypass required status checks, so a completed, MERGEABLE PR whose required checks **failed** (`ci-failed`) or are still **pending** past the CI-wait timeout (`ci-pending`) is **not** a merge conflict. These outcomes carry `blocked:ci` and escalate straight to `ready-for-human` with the **PR left open** — they are deliberately NON-recoverable so AFK never re-runs the whole inner agent (re-spending tokens) for work that is already done and only awaiting CI. A human / CI-aware finisher drives the existing PR to merge once CI is green. This is gated by `afk.merge.ci_aware` (see step 8); with it off, the unlocked path admin-merges immediately (correct only on a base with no required checks).
|
|
298
|
+
|
|
299
|
+
## Per-Issue Loop
|
|
300
|
+
|
|
301
|
+
For each issue `N`:
|
|
302
|
+
|
|
303
|
+
1. **Claim.** `gh issue edit N --remove-label ready-for-agent --add-label running`. Then resolve the attempt number `{n}` from the attempt-ledger (per-issue across all workers), create the attempt directory `.red/tmp/workers/{id}/{N}-a{n}/`, open `afk.log` (tee target for orchestrator output), and initialise `afk.state.json` per [`docs/ENVELOPE.md`](./ENVELOPE.md). The orchestrator PID is already recorded once in the per-worker `worker.pid` (written at bootstrap) and is also embedded in `afk.state.json`'s `.pid` field — there is no per-attempt pid file. Comment a start line on the issue: ISO timestamp, runner identity, worktree path. If labelling fails because someone else already claimed it, abandon the attempt directory and skip to the next issue.
|
|
304
|
+
2. **Worktree.** Resolve the **base branch** with precedence **lock > pin > main** (ADR 0031): the primary checkout's branch-lock value (`.red/state/branch-lock.yaml`, with legacy `.red/tmp/branch-lock.yaml` fallback during migration) wins when set; else the **pinned branch** (ADR 0008 — the issue's own `branch:` line, else its parent Spec's); else `main`. (`{pinned}` below denotes this resolved base.) Then `git -C primary fetch origin {pinned}` and `git worktree add .red/tmp/workers/{id}/{N}-a{n}/worktree -b afk/{id}/{N}-{slug} origin/{pinned}` from the primary checkout. The worktree lives inside the gitignored `.red/tmp/` tree so it never appears in `git status` for `main`. Immediately after worktree creation the runtime mirrors the new branch on origin (`git push origin -u HEAD:refs/heads/afk/{id}/{N}-{slug} --force-with-lease`) and installs a per-worktree `post-commit` hook that fire-and-forgets a `git push origin HEAD --force-with-lease` after every inner-agent commit. Both calls are best-effort: a network/auth failure logs a `warn:` line and the iteration continues. Net effect: `afk/{id}/{N}-{slug}` is a **remote-tracked branch throughout the iteration**, so a SIGKILL or terminal failure preserves the diff on origin without manual recovery.
|
|
305
|
+
3. **Handoff file.** Materialise the handoff into `.red/tmp/workers/{id}/{N}-a{n}/handoff.md` using the template below — top-level XML wrappers (`<issue-body>`, `<previous-attempts>`, `<prior-attempt-context>`, `<human-guidance-thread>`, `<agent-notes>`) keep the issue body, orchestrator-authored prior attempts, the restart-informed retry block, human comments, and the inner-agent scratchpad unambiguous. `<issue-body>` carries the issue body verbatim (including the `## Agent brief` section written by `/triage`). The handoff file lives one level above the worktree so the inner agent reads it via `../handoff.md` from inside the worktree, and so it survives a worktree wipe on retry.
|
|
306
|
+
- **Restart-informed retries (PRD #244, issue #255).** On a terminal failure the orchestrator writes `failure.reason` (the envelope summary) into the failing attempt dir. On the **next** attempt — the runtime reads that marker *before* the current attempt dir is created, so it sees the prior attempt's state — the handoff emits a `<prior-attempt-context>` element carrying the previous attempt number and the verbatim `prev-failure-reason`. First attempts skip all of this and are byte-for-byte unchanged. Older retained attempt dirs may still contain `snapshot-branch.ref`; the reader treats that as legacy history only.
|
|
307
|
+
4. **Local heartbeat marker.** Write one `[heartbeat] iteration started for #N` line to `afk.log`. Slice D retired the periodic GitHub-comment heartbeat (`:one: :two: :three: :four:`) — local liveness is now signalled by the inner-agent stdout stream tee'd into `afk.log` plus state-file mtime, both of which already exist.
|
|
308
|
+
5. **Inner agent.** Drive the inner agent via the single sandcastle `runAgent` call (ADR 0033, *Execution Substrate* above): the handoff file is the `promptFile`, the resolved runner/model selects the provider, the resolved sandbox mode selects the isolation backend, and the worker branch is the `branchStrategy` target forked off the base resolved in step 2. The optional `--request/-r` special user request block is materialised into the handoff. sandcastle captures the agent's stream (surfaced through the `onAgentStreamEvent` callback, which AFK fans out to `agent.log.jsonl` + the firehose) and detects the `<promise>DONE|BLOCKED</promise>` completion signal; AFK reads stages off that stream — see [`docs/ENVELOPE.md`](./ENVELOPE.md). The call's termination bounds (`idleTimeoutSeconds`, `maxIterations`, and the commit-anchored attempt guard) are documented under *Attempt Completion & Termination Bounds*.
|
|
309
|
+
6. **Inner result.**
|
|
310
|
+
- Inner committed and emits `<promise>DONE</promise>` → continue to feedback loops.
|
|
311
|
+
- Inner emits `<promise>BLOCKED</promise>` plus notes appended to the handoff file → comment the blocker on the issue, re-label `ready-for-human`, drop the worktree, go to next issue.
|
|
312
|
+
- Inner emits `<promise>NO MORE TASKS</promise>` from inside one iteration → ignored. That sentinel is for the outer loop.
|
|
313
|
+
- Runner-exhausted signal (rate limit / quota error string per runner) → without `--fallback-runner`, terminate this issue as Attempt Outcome `exhausted`; route it through bounded recovery (`blocked:quota`, retry under `RED_AFK_RETRY_QUOTA`, escalate at/over cap). With `--fallback-runner`, keep the same worktree and handoff, swap runner once, and only route `exhausted` if the swapped runner also exhausts.
|
|
314
|
+
- Inner emits `DONE` / no-sentinel while the worktree still has dirty paths (zero commits or a partial commit) → AFK runs the ADR 0050 salvage: commit each dirty path on the worker branch, push it, then continue through the same feedback + landing tail. A salvage that later fails feedback/backpressure still parks as `blocked:validation`; the terminal envelope names the commit state (`zero commits` or `left dirty worktree paths after N commit(s)`), `salvaged N uncommitted file(s)`, and the failing gate so operators know the work was rescued but not mergeable.
|
|
315
|
+
7. **Feedback loops.** In the worktree, derive relevant package scopes from the worker branch diff against the pinned base, then run `test`, `typecheck`, `lint`, and `build` with `pnpm -C <scope>` for each touched package that declares the script. Root-only repos keep using the root package. The validation subprocesses run with the explicit feedback env contract from `buildFeedbackSubprocessEnv`: allow only stable OS/toolchain variables needed to find node/pnpm and caches/config, deny AFK lane/routing variables (`RED_AFK_*`, including `RED_AFK_WORKERS_NAMESPACE`, worker id, iter dir, runner/model controls) and common auth/model secret prefixes. They never inherit the worker shell wholesale, so `/afk`, `/go`, and scout lanes validate the same diff under the same subprocess env. Any missing script is reported as an explicit per-scope skip in the validation section. Any failure blocks the merge and flips the issue to `ready-for-human` with the validation report in the blocker envelope. **The gate command is canonical** — this feedback run plus the operator's `afk.backpressure` commands are the sole validation authority. Workers run those exact commands and never self-impose stricter flags (`--all-targets`), extra lint restrictions, or a harder contract than the gate defines; an error class visible only under such a check is a mirage, to be reconciled against the gate's real command before anyone reports a red `main` (see [`AGENT-PROMPT.md`](../AGENT-PROMPT.md) → *Validation Authority*).
|
|
316
|
+
8. **Merge.** All steps target the **base branch** resolved in step 2 (`{pinned}`, defaults to `main`). The integration prelude is shared; **landing is lock-toggled** by the branch-lock state (ADR 0030).
|
|
317
|
+
- Primary dirty? Auto-stage and commit `chore(afk): pre-merge snapshot for #N` in primary. Never `git stash`. Never `git checkout -- .`.
|
|
318
|
+
- `git -C primary fetch origin {pinned}`. The primary checkout is pinned to `main` by the precheck; when `{pinned}` is not `main`, switch the primary checkout onto it for the merge (creating the local branch from `origin/{pinned}` if needed) and **restore it to `main` on every exit path**.
|
|
319
|
+
- **Integrate the fetched tip into local `{pinned}` before merging**: fast-forward when local is strictly behind, otherwise rebase local commits onto `origin/{pinned}`. Without this the worker branch merges onto the stale boot-time HEAD and the push is rejected non-fast-forward whenever origin moved mid-run. If integration fails (diverged history that won't rebase), abort the merge and route the `merge-conflict` outcome through bounded recovery.
|
|
320
|
+
- Capture the integrated tip (`pre_merge_sha`) for rollback, then land per lock state:
|
|
321
|
+
- **Locked** (`.red/state/branch-lock.yaml` present, or the legacy tmp lock is still present during migration — `{pinned}` *is* the locked branch): `git -C primary merge --no-ff afk/{id}/{N}-{slug} -m "merge: #{N} {title}"` directly into the local locked branch, then `git -C primary push origin {pinned}`. **Nothing reaches `main`** — promoting the locked branch to `main` is the operator's call. Conflict → one-shot self-resolve; still-conflicting → `git merge --abort` → bounded `merge-conflict` recovery. Push rejected → roll back to `pre_merge_sha` → bounded `merge-conflict` recovery.
|
|
322
|
+
- **Unlocked**: land via an **admin-merged PR**. Force-push the attempt branch's final state to origin, open (or reuse) a PR `--base {pinned} --head afk/{id}/{N}-{slug}`, then `gh pr merge --admin --merge`. The **PR is the durable per-attempt history** — it survives the branch deletion in step 11. No completed work reaches `{pinned}` except through this admin-merge. Then fast-forward local `{pinned}` to the PR merge commit so the closing envelope's `merge_sha` is correct.
|
|
323
|
+
- **CI-aware merge (#812, `afk.merge.ci_aware: true`).** On an `enforce_admins` base the admin-merge **cannot** bypass required status checks, so admin-merging a just-opened PR with checks pending is rejected. When `ci_aware` is on, after opening/reusing the PR **poll `gh pr view --json mergeStateStatus,statusCheckRollup`** on a bounded loop (budget `RED_AFK_MERGE_CI_TIMEOUT_S`, default 1800s) until the PR settles, then `gh pr merge --admin --merge` **only** once `mergeStateStatus == CLEAN` (or it is `BLOCKED` solely by a required review, which `--admin` waives). Route the distinct failure modes instead of collapsing all to `merge-conflict`: a real git conflict / `DIRTY` / `BEHIND` → `merge-conflict` (bounded recovery — correct here); a **failed** required check → `ci-failed` (`blocked:ci`); checks still **pending** at the timeout → `ci-pending` (`blocked:ci`). `ci-failed`/`ci-pending` **leave the PR open** and escalate to `ready-for-human` — they never re-run the inner agent for already-complete work (see *Typed Failure Labels And Recovery Caps*). With `ci_aware` off (the default), a push/create/admin-merge failure routes through bounded `merge-conflict` recovery as before.
|
|
324
|
+
9. **Push.** Folded into step 8: the **locked** path pushes the locked branch over SSH (rollback on reject); the **unlocked** path's push *is* the admin-merge of the PR. Either way, do not retry-loop indefinitely.
|
|
325
|
+
10. **Close.** Validation comment on the issue: tests pass/fail, lint, typecheck, build, commits added, files touched. Then `gh issue close N --reason completed`. Remove `running` label. Once the close succeeds, delete the live remote branch (`git push origin --delete afk/{id}/{N}-{slug}`) so the remote graveyard stays tidy — the merge commit on `{pinned}` already carries the diff. Best-effort: a failed delete (branch protection, network) logs a `warn:` line and the close still completes; the orphan `afk/*` branch can be cleaned up later.
|
|
326
|
+
11. **Cleanup (split teardown, issue #256).** Every close path — success **and** failure/blocker — always drops the heavy worktree (`git worktree remove .red/tmp/workers/{id}/{N}-a{n}/worktree`) while **retaining** the cheap artifacts (the JSONL lanes `log.jsonl` / `agent.log.jsonl` and the `handoff.md`) in the attempt directory for post-mortem. On DONE the merged branch is also deleted (`git branch -d afk/{id}/{N}-{slug}`, after the worktree is gone). The retained attempt's state file is marked not-live (`pid: 0`) so monitor / mirror / statusline read it as finished. No worktree survives a close; the attempt dir itself is reclaimed later by the boot-time orphan sweep's TTL **or, on DONE, immediately by the completion sweep below**. The remote `afk/{id}/{N}-{slug}` ref was deleted in step 10 on DONE; failure paths leave the remote ref intact as the durable branch record (see [`docs/ENVELOPE.md`](./ENVELOPE.md)).
|
|
327
|
+
- **Completion sweep (issue #257).** Once an issue is closed and merged, the runtime reclaims **every** attempt dir for that issue across **all** workers via the canonical `.red/tmp/workers/*/{N}-a*` glob — not just the worker that completed it. The split-teardown retention only buys time for the orphan-sweep TTL; a completed issue needs none of it, so its retained dirs (including this worker's just-closed one) go now. A live worker's active attempt — one whose own state file still carries a live `pid` — is always skipped, though the claim lock makes a live duplicate of a just-completed issue unlikely.
|
|
328
|
+
12. **Tick.** Update state file. Recompute ETA from rolling average of last 3 issue durations. Print one summary line: `finished {done}/{total} ({pct}%) — next: #{next}`.
|
|
329
|
+
|
|
330
|
+
## Runner Fallback
|
|
331
|
+
|
|
332
|
+
Default behaviour is **no rotation and no fallback** — the runner resolved by the detection cascade (see *Bootstrap* step 4) is used for every issue in the run. `RUNNER_EXHAUSTED` is first handled as the per-issue Attempt Outcome `exhausted`: the issue gets `blocked:quota`, returns to `ready-for-agent` while under `RED_AFK_RETRY_QUOTA`, and escalates to `ready-for-human` at/over the cap. The outer session then stops the drain and returns exit 75 (`EX_TEMPFAIL`) so a supervisor can retry later instead of treating runner quota as a clean queue drain. Both rotation/fallback behaviours are opt-in:
|
|
333
|
+
|
|
334
|
+
- `--alternate` re-enables round-robin rotation between consecutive issues (claude → codex → claude → …). Mutually exclusive with `--runner`.
|
|
335
|
+
- `--fallback-runner` re-enables mid-issue swap when the active runner returns `RUNNER_EXHAUSTED`. Without it, exhaustion is terminal for the current runner invocation and routes through bounded recovery as `blocked:quota`.
|
|
336
|
+
|
|
337
|
+
Exhaustion detection lives in [`runner-claude.md`](../runner-claude.md), [`runner-codex.md`](../runner-codex.md), and [`runner-opencode.md`](../runner-opencode.md) — they own the per-runner error strings. The orchestrator only sees `RUNNER_EXHAUSTED` as a structured signal. Note `opencode` is an API-auth runner; the auth key rides in `OpenCodeOptions.env` and the model slug's leading segment (`openai/`, `minimax/`, `openrouter/...`) tells OpenCode which endpoint to dispatch to. See `runner-opencode.md` *Auth env precedence* for the env-var order (`OPENAI_API_KEY` > `MINIMAX_API_KEY` > `OPENROUTER_API_KEY`). In an API-key-only lane with no host session, run it without `--fallback-runner` so exhaustion is terminal-through-recovery rather than a swap to a session-auth runner that is not present.
|
|
338
|
+
|
|
339
|
+
When swap happens mid-issue (only with `--fallback-runner`), the same worktree and handoff file are reused; the new runner sees the previous agent's Notes appended.
|
|
340
|
+
|
|
341
|
+
## Attempt Completion & Termination Bounds (`<promise>` is canonical — ADR 0028)
|
|
342
|
+
|
|
343
|
+
The `<promise>…</promise>` sentinel the inner agent emits is the **canonical "attempt is over" signal**. AFK registers `<promise>DONE</promise>` and `<promise>BLOCKED</promise>` as sandcastle `completionSignal`s, so sandcastle stops re-invoking the agent the moment one is observed (line-anchored, so the agent quoting the sentinel in planning prose does not false-positive). sandcastle owns the stream read and signal detection — there is no hand-rolled foreground pipe reader, no recursive SIGTERM/SIGKILL of a `claude | jq | grep | tee` pipeline, and no `RED_AFK_ATTEMPT_GRACE_S` / `RED_AFK_ATTEMPT_KILL_S` / `RED_AFK_WATCHDOG_GRACE_S` tear-down knobs. This is the architecture fix flagged during the #216 bash-hang diagnosis (we need to be more responsive to the promise result): the completion signal is the terminator, and the substrate enforces it.
|
|
344
|
+
|
|
345
|
+
`runAgent` maps the returned `completionSignal` to an outcome: `<promise>DONE</promise>` → `done`, `<promise>BLOCKED</promise>` → `blocked`, no signal → `no-sentinel`. The completion signal is the **real** terminator — a normal issue finishes in 1-3 iterations — but three independent bounds cap a run that never signals so a stuck agent cannot burn cycles forever:
|
|
346
|
+
|
|
347
|
+
- **`idleTimeoutSeconds`** (default **600 s**, env `RED_AFK_IDLE_TIMEOUT_S`) — sandcastle's per-iteration **silence** watchdog: an iteration producing no stream output for this long is aborted. This is the actual termination bound on a quiet hang.
|
|
348
|
+
- **`maxIterations`** (default **12**, env `RED_AFK_MAX_ITERATIONS`) — the sandcastle Orchestrator **re-invocation** ceiling (issue #322). sandcastle's own default is 1, which would cut the agent off after a single agentic invocation before it can emit `DONE`; AFK raises it so the completion signal stays the terminator while bounding repeated no-sentinel failures. A non-numeric / zero / negative value (env or config) is ignored and falls back to the default, so a typo can never disable the cap or pin the agent to 1.
|
|
349
|
+
- **Commit-anchored attempt guard** (fixed **2700 s**, ADR 0044/0045 as amended by ADR 0107) — proof-of-**progress**: a run that stays *busy* (re-exploring, re-running tests) without landing a **new commit** within the cap is aborted, resetting on every commit. This catches the "productive infinite loop" that `idleTimeoutSeconds` misses because the agent is never silent. It maps to a `timeout` outcome → `blocked:stalled` / `ready-for-human`, preserving the worktree/PR. Armed **only under `none` (no-sandbox) isolation**, where the worker branch's commits land in the shared `.git` so HEAD advance is observable; under docker/podman the commits are not host-visible until final sync, so a commit-anchored guard would false-fire and is skipped (idle timeout + maxIterations still apply). The fleet hard **stall reaper** (see [`fleet.md`](../fleet.md)) is separately gated by the active-`vitest`/`tsc`/`cargo`-descendant + flat-cpu predicate, so a worker mid-build/test is never killed for being idle on the agent lane.
|
|
350
|
+
|
|
351
|
+
**No sentinel is `on_attempt_error`.** When sandcastle's run completes with no completion signal, the agent never declared the attempt over (crash, kill, or a daemon that ended without speaking): the outcome is `no-sentinel`, `on_attempt_error` fires (error class `no-sentinel`), and `post_attempt` does **not** fire for that invocation. The issue routes through bounded `blocked:crashed` recovery. With the default `RED_AFK_RETRY_CRASH=1`, the first such failure escalates to `ready-for-human`; a higher cap can requeue it first. **Runner exhaustion** (`RUNNER_EXHAUSTED`, detected by matching the per-runner quota/rate-limit strings against the thrown sandcastle error) stays out of the sentinel channel — it keeps its own `exhausted` outcome and the `--fallback-runner` swap. A **transient runner transport/setup** failure maps to `runner-transient` and is bounded by AFK's retry policy rather than escaping as a crash.
|
|
352
|
+
|
|
353
|
+
The parsed outcome rides into the `post_attempt` mutable context as `result.outcome` and the `RED_AFK_RESULT_OUTCOME` env var, so hooks (and the Memory `attempt.hooks` record, #216) see the agent-authored exit, not just `success`/`fail`.
|
|
354
|
+
|
|
355
|
+
Preventive counterpart lives in [`AGENT-PROMPT.md`](../AGENT-PROMPT.md) under *Background Tasks and Polling* — inner agents are required to cap every polling loop with a deadline. The termination bounds are the safety net; the prompt rule is the design.
|
|
356
|
+
|
|
357
|
+
## Liveness & stall protection
|
|
358
|
+
|
|
359
|
+
Local liveness = the clean `agent.log.jsonl` lane + the firehose + state-file mtime + a per-minute orchestrator heartbeat (the GitHub-thread heartbeat was retired in Slice D). A solo run is guarded by the commit-anchored attempt-progress guard (#400) and the lane-idle reaper (#363), both armed only under no-sandbox isolation. Details: [`docs/LIVENESS.md`](./LIVENESS.md).
|
|
360
|
+
|
|
361
|
+
## Terminal-event envelope, stages & state file
|
|
362
|
+
|
|
363
|
+
Every terminal event posts exactly one structured `<details data-attempt-status=…>` comment (the canonical record). Stages are read off the sandcastle stream; the terminal header redraws every 3s; per-attempt state lives in `afk.state.json`. Schemas + the Attempt-Outcome→status mapping: [`docs/ENVELOPE.md`](./ENVELOPE.md).
|
|
364
|
+
|
|
365
|
+
## Fleet Mode
|
|
366
|
+
|
|
367
|
+
Running `afk fleet` → read [`fleet.md`](../fleet.md) for the runner-portable launch/stop/supervisor protocol (stall detector, hard stall reaper, circuit trip sweep, and per-runner monitor attachment).
|
|
368
|
+
|
|
369
|
+
## Monitor
|
|
370
|
+
|
|
371
|
+
Running `afk monitor` → read [`monitor.md`](../monitor.md) for the readonly dashboard, the binding native-task mirror, its self-cancel teardown, and the Codex monitor agent.
|
|
372
|
+
|
|
373
|
+
## Handoff file template
|
|
374
|
+
|
|
375
|
+
The inner agent reads `../handoff.md` — top-level XML wrappers (`<issue-body>`, `<previous-attempts>`, `<prior-attempt-context>`, `<human-guidance-thread>`, `<thread-discussion>`, `<agent-notes>`) keep body/comments/notes unambiguous. Full template: [`docs/HANDOFF.md`](./HANDOFF.md).
|
|
376
|
+
|
|
377
|
+
## Stop Conditions
|
|
378
|
+
|
|
379
|
+
- Queue drained → `<promise>NO MORE TASKS</promise>` → exit 0. When the backlog still has open non-Spec issues, the invoking agent accompanies this with the gate census (see *Issue Selection*) — a drained queue with a gated backlog is a flow bug to surface, not "nothing to do".
|
|
380
|
+
- `-n N` reached → summary + exit 0.
|
|
381
|
+
- Runner exhaustion / runner transport failure → route the current issue through bounded recovery (`blocked:quota` or `blocked:runner-transient`), then stop the outer run with exit 75.
|
|
382
|
+
- Uncaught error in orchestrator → leave worktree in place, exit 1, print recovery hint. (No heartbeat sub-shell to kill since Slice D.)
|
|
383
|
+
|
|
384
|
+
## Reporting
|
|
385
|
+
|
|
386
|
+
After every issue, print:
|
|
387
|
+
|
|
388
|
+
```
|
|
389
|
+
✓ #142 wire OAuth callback 12m 14s tests:✓ lint:✓ types:✓ build:✓ merged b3f2a91
|
|
390
|
+
finished 4 / 12 (33%) — next: #143
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
After the loop, a final block:
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
/afk done.
|
|
397
|
+
runner : codex (3 issues), claude (1 issue)
|
|
398
|
+
duration : 01:14:22
|
|
399
|
+
processed : 4 closed, 0 blocked, 0 failed
|
|
400
|
+
remaining : 8 still ready-for-agent
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Configuration & lifecycle hooks
|
|
404
|
+
|
|
405
|
+
All `.red/config.yaml` knobs + `RED_AFK_*` env overrides (sandbox, runner, model/effort, timeouts, retry caps, stall thresholds, backpressure) and the lifecycle-hook contract live in [`docs/CONFIG.md`](./CONFIG.md). The runtime supplies the documented base env (`RED_AFK_REPO`, `RED_AFK_ROOT`, `RED_AFK_WORKSPACE`, `RED_AFK_RUNNER`, optional `RED_AFK_SLOT`) and layers each hook event's documented `RED_AFK_*` variables from that event's JSON context. Runner/model resolution policy: [`../model-tier-policy/SKILL.md`](../../model-tier-policy/SKILL.md).
|
|
406
|
+
|
|
407
|
+
## Safety
|
|
408
|
+
|
|
409
|
+
See [`SAFETY.md`](../SAFETY.md). The orchestrator and the inner agent both inherit those rules. Violations abort the loop.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# only-mine.sh — example post_pick hook for /afk.
|
|
3
|
+
#
|
|
4
|
+
# Narrows the queue to issues authored by $RED_AFK_GITHUB_LOGIN. Wire it in
|
|
5
|
+
# .red/config.yaml under `afk.hooks.post_pick`:
|
|
6
|
+
#
|
|
7
|
+
# afk:
|
|
8
|
+
# hooks:
|
|
9
|
+
# post_pick:
|
|
10
|
+
# - "RED_AFK_GITHUB_LOGIN=$(gh api user --jq .login) \
|
|
11
|
+
# plugins/dev/skills/engineering/afk/examples/only-mine.sh"
|
|
12
|
+
#
|
|
13
|
+
# Contract (PRD #207, issue #210):
|
|
14
|
+
# stdin — JSON object: `{ "issues": [ { number, title, labels, body, author, … } ] }`
|
|
15
|
+
# stdout — empty (context unchanged) OR a JSON object whose `.issues`
|
|
16
|
+
# replaces the queue. Extra keys are ignored by AFK.
|
|
17
|
+
# exit — non-zero is logged; AFK falls back to the un-mutated list.
|
|
18
|
+
#
|
|
19
|
+
# When $RED_AFK_GITHUB_LOGIN is unset we exit 0 with empty stdout so the
|
|
20
|
+
# queue is preserved — declaring this hook becomes a no-op rather than a
|
|
21
|
+
# silent queue wipe.
|
|
22
|
+
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
|
|
25
|
+
login="${RED_AFK_GITHUB_LOGIN:-}"
|
|
26
|
+
if [[ -z "$login" ]]; then
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
jq -c --arg login "$login" '
|
|
31
|
+
{issues: ((.issues // []) | map(select((.author.login // "") == $login)))}
|
|
32
|
+
'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Example: direct use of the afk-attempt composite action (the COMPOSABLE path).
|
|
2
|
+
#
|
|
3
|
+
# Use this when you want full control over triggers and gating and don't need the
|
|
4
|
+
# turnkey reusable workflow. The composite action is the execution PRIMITIVE
|
|
5
|
+
# (ADR 0062): it sets up Node, installs the runner CLI, and runs ONE AFK attempt
|
|
6
|
+
# against this repo's checkout. You own the trigger and any trust gate.
|
|
7
|
+
#
|
|
8
|
+
# Trade-off vs. the turnkey caller (`rs-afk-attempt.yml`): the reusable ships
|
|
9
|
+
# the author/label-actor trust gate (ADR 0085); the raw action does not. For
|
|
10
|
+
# public repos prefer the reusable. For private repos an `if:` on the actor (as
|
|
11
|
+
# below) is usually enough.
|
|
12
|
+
#
|
|
13
|
+
# Pin the action to a tag (`@v1`) for reproducible runs — that ref also fixes the
|
|
14
|
+
# `dev` bundle version the launcher fetches. Copy to `.github/workflows/afk.yml`.
|
|
15
|
+
|
|
16
|
+
name: afk
|
|
17
|
+
on:
|
|
18
|
+
issues:
|
|
19
|
+
types: [labeled, opened]
|
|
20
|
+
workflow_dispatch:
|
|
21
|
+
inputs:
|
|
22
|
+
issue:
|
|
23
|
+
description: "Issue number to process."
|
|
24
|
+
required: true
|
|
25
|
+
type: string
|
|
26
|
+
|
|
27
|
+
permissions:
|
|
28
|
+
contents: write
|
|
29
|
+
issues: write
|
|
30
|
+
pull-requests: write
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
attempt:
|
|
34
|
+
# Minimal inline gate: a labeled/opened issue carrying `ready-for-agent`,
|
|
35
|
+
# applied by a trusted actor. EDIT the login list.
|
|
36
|
+
if: >-
|
|
37
|
+
github.event_name == 'workflow_dispatch' ||
|
|
38
|
+
(
|
|
39
|
+
contains(github.event.issue.labels.*.name, 'ready-for-agent') &&
|
|
40
|
+
contains(fromJSON('["filipeforattini"]'), github.event.sender.login)
|
|
41
|
+
)
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
- uses: reddb-io/red-skills/.github/actions/afk-attempt@v1
|
|
46
|
+
with:
|
|
47
|
+
issue: ${{ github.event.issue.number || inputs.issue }}
|
|
48
|
+
runner: opencode
|
|
49
|
+
# Wire whichever key your org provisions (first set wins; see
|
|
50
|
+
# opencode-env.ts precedence). EDIT.
|
|
51
|
+
openrouter-api-key: ${{ secrets.OPENROUTER_API_KEY }}
|
|
52
|
+
minimax-api-key: ${{ secrets.MINIMAX_API_KEY }}
|
|
53
|
+
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Example: thin caller for the reddb-io/red-skills AFK Actions lane
|
|
2
|
+
#
|
|
3
|
+
# Copy this file into your repo at
|
|
4
|
+
# `.github/workflows/rs-afk-attempt.yml` (the `rs-*`
|
|
5
|
+
# installed-name convention — only the filename changes; the `uses:` ref
|
|
6
|
+
# below keeps the `reusable-` source name), then edit:
|
|
7
|
+
# 1. `allowlist_authors` and `allowlist_label_actors` — the GitHub
|
|
8
|
+
# logins whose issues + label-applications you trust to drive an
|
|
9
|
+
# autonomous run. Adopters: keep these short. Public repos: lean
|
|
10
|
+
# toward 0 or 1 entry (you and your automation bot only).
|
|
11
|
+
# 2. `runner` — defaults to `opencode` (the third runner, ADR 0059).
|
|
12
|
+
# 3. The `secrets:` block — wire whichever of the three API-key
|
|
13
|
+
# secrets your org provisions. The env-precedence resolver in
|
|
14
|
+
# `opencode-env.ts` picks the first set; the others are ignored.
|
|
15
|
+
#
|
|
16
|
+
# This file calls the published reusable workflow
|
|
17
|
+
# `reddb-io/red-skills/.github/workflows/reusable-afk-attempt.yml@v1` and
|
|
18
|
+
# is the only piece of YAML your repo needs to enable the AFK Actions
|
|
19
|
+
# lane. The reusable itself also auto-triggers on `issues: labeled`
|
|
20
|
+
# when this caller does NOT route through it (e.g. an external
|
|
21
|
+
# automation applies `ready-for-agent` directly), so the trigger
|
|
22
|
+
# surface is covered from both sides.
|
|
23
|
+
#
|
|
24
|
+
# If you don't want this caller to exist at all (i.e. you only want
|
|
25
|
+
# auto-trigger), you don't need this file: install
|
|
26
|
+
# `reddb-io/red-skills/.github/workflows/reusable-afk-attempt.yml@v1`
|
|
27
|
+
# directly into your `.github/workflows/` and edit the
|
|
28
|
+
# `allowlist_*` inputs there. The `if:` gate on that workflow
|
|
29
|
+
# filters to the `ready-for-agent` label automatically.
|
|
30
|
+
#
|
|
31
|
+
# Trust gate: rigorous by default. The reusable refuses to claim an
|
|
32
|
+
# issue whose author (`issue.user.login`) is NOT in
|
|
33
|
+
# `allowlist_authors` OR whose label-applier (`sender.login` from the
|
|
34
|
+
# labeled event) is NOT in `allowlist_label_actors`. Both checks
|
|
35
|
+
# pass → claim proceeds. Either fails → logged, no claim, no PR.
|
|
36
|
+
# (See #621 for the runtime predicate; the gh-script in the reusable
|
|
37
|
+
# is the source of truth.)
|
|
38
|
+
#
|
|
39
|
+
# Permission scope (recommended minimum): contents: write, issues:
|
|
40
|
+
# write, pull-requests: write. No id-token (no OIDC), no actions:
|
|
41
|
+
# write (the workflow does not publish actions).
|
|
42
|
+
|
|
43
|
+
name: rs-afk-attempt
|
|
44
|
+
|
|
45
|
+
on:
|
|
46
|
+
# Manual trigger from the Actions UI; the reusable's `if:` filter
|
|
47
|
+
# also auto-triggers on `issues: labeled` regardless of whether
|
|
48
|
+
# this caller exists.
|
|
49
|
+
workflow_dispatch:
|
|
50
|
+
inputs:
|
|
51
|
+
issue_number:
|
|
52
|
+
description: "Issue number to process (e.g. 631). Leave EMPTY to auto-pick the oldest open ready-for-agent issue."
|
|
53
|
+
required: false
|
|
54
|
+
type: string
|
|
55
|
+
default: ""
|
|
56
|
+
|
|
57
|
+
permissions:
|
|
58
|
+
contents: write
|
|
59
|
+
issues: write
|
|
60
|
+
pull-requests: write
|
|
61
|
+
|
|
62
|
+
jobs:
|
|
63
|
+
# A reusable workflow is invoked at the JOB level via `uses:` (NOT as a step) —
|
|
64
|
+
# `with:`/`secrets:` are siblings of `uses:`, and the job has no `runs-on`/`steps`.
|
|
65
|
+
attempt:
|
|
66
|
+
uses: reddb-io/red-skills/.github/workflows/reusable-afk-attempt.yml@v1
|
|
67
|
+
with:
|
|
68
|
+
issue_number: ${{ inputs.issue_number }}
|
|
69
|
+
runner: opencode
|
|
70
|
+
enforce_trust_gate: "true"
|
|
71
|
+
allowlist_authors: "filipeforattini" # EDIT — your trusted author logins
|
|
72
|
+
allowlist_label_actors: "filipeforattini" # EDIT — your trusted label-applier logins
|
|
73
|
+
secrets:
|
|
74
|
+
openrouter_api_key: ${{ secrets.OPENROUTER_API_KEY }}
|
|
75
|
+
minimax_api_key: ${{ secrets.MINIMAX_API_KEY }}
|
|
76
|
+
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
|