@reddb-io/red-skills 4.4.0 → 4.5.0

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.
Files changed (77) hide show
  1. package/bin/build-info.mjs +87 -0
  2. package/bin/cli-frontdoor.mjs +14 -0
  3. package/bin/red-skills-brain.mjs +17 -22
  4. package/bin/red-skills-content.mjs +17 -0
  5. package/bin/red-skills-hook.mjs +45 -0
  6. package/bin/red-skills-memory.mjs +17 -22
  7. package/bin/red-skills-redskilled-mcp.mjs +2 -0
  8. package/bin/red-skills-resource.mjs +21 -0
  9. package/bin/runtime-compatibility.mjs +17 -0
  10. package/bin/runtime-paths.mjs +13 -0
  11. package/dist/code-nav.bundle.min.mjs +1 -1
  12. package/dist/opencode-host.bundle.min.mjs +1 -1
  13. package/dist/redskilled-mcp.bundle.min.mjs +36 -36
  14. package/dist/redskilled.bundle.min.mjs +234 -230
  15. package/dist/release.bundle.min.mjs +1 -1
  16. package/dist/rsp-core.bundle.min.mjs +1 -1
  17. package/dist/rsp.bundle.min.mjs +1 -1
  18. package/dist/statusline.bundle.min.mjs +1 -1
  19. package/package.json +9 -4
  20. package/runtime/hook-routes.json +125 -0
  21. package/runtime/plugins/brain/scripts/bootstrap.mjs +656 -0
  22. package/runtime/plugins/brain/scripts/structural-smoke.sh +7 -0
  23. package/runtime/plugins/dev/hooks/branch-lock-codex.sh +105 -0
  24. package/runtime/plugins/dev/hooks/code-nav-mcp.sh +63 -0
  25. package/runtime/plugins/dev/hooks/command-guard.sh +893 -0
  26. package/runtime/plugins/dev/hooks/ensure-codex-statusline.mjs +144 -0
  27. package/runtime/plugins/dev/hooks/red-fetch.mjs +38 -0
  28. package/runtime/plugins/dev/hooks/redskilled-mcp.sh +64 -0
  29. package/runtime/plugins/dev/hooks/rsp-hook.sh +343 -0
  30. package/runtime/plugins/dev/hooks/tests/command-guard.test.sh +378 -0
  31. package/runtime/plugins/dev/hooks/tests/rsp-hook.test.sh +229 -0
  32. package/runtime/plugins/dev/scripts/memory-bridge.sh +230 -0
  33. package/runtime/plugins/dev/scripts/structural-smoke.sh +7 -0
  34. package/runtime/plugins/dev/scripts/tests/memory-bridge.test.sh +555 -0
  35. package/runtime/plugins/dev/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
  36. package/runtime/plugins/dev/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
  37. package/runtime/plugins/dev/skills/engineering/afk/defaults/gradle-pre-worktree.sh +45 -0
  38. package/runtime/plugins/dev/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
  39. package/runtime/plugins/dev/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
  40. package/runtime/plugins/dev/skills/engineering/afk/detectors/cargo.sh +27 -0
  41. package/runtime/plugins/dev/skills/engineering/afk/detectors/gradle.sh +38 -0
  42. package/runtime/plugins/dev/skills/engineering/afk/examples/only-mine.sh +32 -0
  43. package/runtime/plugins/dev/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
  44. package/runtime/plugins/dev/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
  45. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-cargo +38 -0
  46. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-envelope +50 -0
  47. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-gradle +42 -0
  48. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-heartbeat +46 -0
  49. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-validation +79 -0
  50. package/runtime/plugins/dev/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
  51. package/runtime/plugins/dev/skills/engineering/red-setup/scripts/install-runtime-shim.sh +181 -0
  52. package/runtime/plugins/dev/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
  53. package/runtime/plugins/dev/skills/engineering/wizard/template.sh +211 -0
  54. package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
  55. package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
  56. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
  57. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
  58. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
  59. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
  60. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
  61. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
  62. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
  63. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
  64. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
  65. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +230 -0
  66. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
  67. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
  68. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
  69. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
  70. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
  71. package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
  72. package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
  73. package/runtime/plugins/internal/scripts/structural-smoke.sh +7 -0
  74. package/runtime/plugins/internal/skills/maintainer/create-plugin/scripts/create-plugin.sh +308 -0
  75. package/runtime/plugins/memory/scripts/bootstrap.mjs +659 -0
  76. package/runtime/plugins/memory/scripts/structural-smoke.sh +7 -0
  77. package/scripts/check-skills-boundary.mjs +32 -0
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ # gradle-pre-worktree.sh — built-in pre_worktree default for Gradle projects.
3
+ #
4
+ # Contract (PRD #207, issue #211):
5
+ # stdin — JSON object: pre_worktree context (issue, target, branch, env).
6
+ # stdout — empty (context unchanged) when no build.gradle* is present at
7
+ # $PROJECT_ROOT, OR when RED_AFK_GRADLE_USER_HOME_BASE is unset
8
+ # (opt-in by env var — we do not claim a path on the user's
9
+ # filesystem without their consent). Otherwise a JSON object
10
+ # that merges {env: {GRADLE_USER_HOME: <slot dir>}} into ctx.
11
+ # exit — 0 on either branch. Never aborts the chain.
12
+ #
13
+ # Side effect: mkdir -p the slot Gradle home so daemons / caches are isolated
14
+ # between worker slots.
15
+ #
16
+ # Disable with `afk.hooks.defaults.gradle: false` in `.red/config.yaml`.
17
+
18
+ set -u
19
+
20
+ project_root="${PROJECT_ROOT:-$(pwd)}"
21
+ ctx="$(timeout "${RED_SKILLS_HOOK_STDIN_TIMEOUT_S:-5s}" cat 2>/dev/null || true)"
22
+ [ -n "$ctx" ] || ctx='{}'
23
+
24
+ has_gradle_build=false
25
+ for candidate in "$project_root"/build.gradle*; do
26
+ [ -f "$candidate" ] || continue
27
+ has_gradle_build=true
28
+ break
29
+ done
30
+ if [ "$has_gradle_build" = false ]; then
31
+ exit 0
32
+ fi
33
+
34
+ if [ -z "${RED_AFK_GRADLE_USER_HOME_BASE:-}" ]; then
35
+ exit 0
36
+ fi
37
+
38
+ slot="${RED_AFK_SLOT:-0}"
39
+ home_dir="${RED_AFK_GRADLE_USER_HOME_BASE}/slot-${slot}"
40
+
41
+ mkdir -p "$home_dir" 2>/dev/null || true
42
+
43
+ printf '%s' "$ctx" | jq -c \
44
+ --arg h "$home_dir" \
45
+ '. + {env: ((.env // {}) + {GRADLE_USER_HOME: $h})}'
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ # heartbeat-post-attempt.sh — built-in post_attempt default (PRD #207, issue
3
+ # #212; renamed from post_worker in #226).
4
+ #
5
+ # Contract:
6
+ # stdin — JSON object: post_attempt context (issue, workspace, result).
7
+ # stdout — empty (context unchanged). The default is a pure side-effect:
8
+ # it stops the orchestrator's per-minute heartbeat side-channel
9
+ # and writes the "iteration stopped" boundary marker into the
10
+ # iteration log, migrating the inline `heartbeat_stop` that
11
+ # used to fire from afk.sh right after `run_inner` returned.
12
+ # exit — 0. Never aborts the post_attempt chain (the dispatcher already
13
+ # routes post_attempt rc through `continue` policy, but the
14
+ # default also belt-and-braces this with a 0 exit).
15
+ #
16
+ # Env contract (set by afk.sh before dispatch):
17
+ # RED_AFK_HEARTBEAT_PID — pid of the orchestrator heartbeat sub-shell;
18
+ # empty when no heartbeat was started for this
19
+ # iteration (smoke paths). Killing a stale pid
20
+ # is a no-op via `kill -0` guard.
21
+ # RED_AFK_ITER_LOG — path to the iteration log; the boundary line
22
+ # is appended only when the file exists.
23
+ #
24
+ # Disable with `afk.hooks.defaults.heartbeat: false` in `.red/config.yaml`.
25
+
26
+ set -uo pipefail
27
+
28
+ # Drain stdin (context flows through unchanged — emit nothing).
29
+ timeout "${RED_SKILLS_HOOK_STDIN_TIMEOUT_S:-5s}" cat >/dev/null 2>&1 || true
30
+
31
+ pid="${RED_AFK_HEARTBEAT_PID:-}"
32
+ if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
33
+ kill "$pid" 2>/dev/null || true
34
+ # Best-effort reap; the sub-shell may already be exiting.
35
+ wait "$pid" 2>/dev/null || true
36
+ fi
37
+
38
+ iterlog="${RED_AFK_ITER_LOG:-}"
39
+ if [[ -n "$iterlog" && -f "$iterlog" ]]; then
40
+ printf '[heartbeat] iteration stopped at %s\n' "$(date -Is)" >> "$iterlog"
41
+ fi
42
+
43
+ exit 0
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ # validation-post-merge.sh — built-in post_merge default (PRD #207, issue #213).
3
+ #
4
+ # Runs CI/smoke validation against the merged primary checkout so user
5
+ # post_merge hooks see the validation status already reconciled into the
6
+ # context. Migrated from the inline `feedback`-style call: the pre-merge
7
+ # `feedback()` in afk.sh stays as the mechanism-owned safety gate (ADR 0008
8
+ # — anything that must abort the merge loudly is mechanism, not a hook), and
9
+ # this default exposes a post-merge re-run hook authors can disable, observe,
10
+ # or replace.
11
+ #
12
+ # Contract:
13
+ # stdin — JSON object: post_merge context (issue, workspace, merge_commit).
14
+ # stdout — JSON object: the same context with
15
+ # `.result = {validation_status: "passed"|"failed"|"skipped",
16
+ # validation_summary: <one-line string>}` merged into
17
+ # it. Non-JSON stdin → pass-through with no validation attempt.
18
+ # exit — 0. Post_merge policy is `continue`; even a failing validation
19
+ # only logs (it cannot roll back the merge — that ship has sailed
20
+ # at this lifecycle point).
21
+ #
22
+ # Env contract (set by afk.sh before dispatch):
23
+ # RED_AFK_WORKSPACE — primary checkout (post-merge tip). When unset or
24
+ # not a directory, the default is a no-op.
25
+ # RED_AFK_MERGE_COMMIT — full sha of the merge commit on `target`. Not
26
+ # consumed by this default but documented so user
27
+ # hooks reading env directly see a consistent view.
28
+ #
29
+ # Disable with `afk.hooks.defaults.validation: false` in `.red/config.yaml`.
30
+
31
+ set -uo pipefail
32
+
33
+ ctx="$(timeout "${RED_SKILLS_HOOK_STDIN_TIMEOUT_S:-5s}" cat 2>/dev/null || true)"
34
+ ws="${RED_AFK_WORKSPACE:-}"
35
+
36
+ emit_ctx() {
37
+ local status="$1" summary="$2"
38
+ if [[ -z "$ctx" ]]; then
39
+ return 0
40
+ fi
41
+ printf '%s' "$ctx" \
42
+ | jq --arg s "$status" --arg sum "$summary" \
43
+ '.result = ((.result // {}) + {validation_status: $s, validation_summary: $sum})' \
44
+ 2>/dev/null \
45
+ || printf '%s' "$ctx"
46
+ }
47
+
48
+ if [[ -z "$ws" || ! -d "$ws" ]]; then
49
+ emit_ctx "skipped" "no workspace"
50
+ exit 0
51
+ fi
52
+
53
+ if ! [[ -f "$ws/package.json" ]]; then
54
+ emit_ctx "skipped" "no package.json"
55
+ exit 0
56
+ fi
57
+
58
+ # Run pnpm test/typecheck/lint/build at the workspace root if the script
59
+ # exists. This mirrors the mechanism-owned `feedback()` step shape but
60
+ # operates on the merged tip rather than the worker's worktree.
61
+ overall="passed"
62
+ parts=""
63
+ for script in test typecheck lint build; do
64
+ if jq -e --arg s "$script" '.scripts[$s] // empty' "$ws/package.json" >/dev/null 2>&1; then
65
+ if ( cd "$ws" && pnpm -s "$script" >/dev/null 2>&1 ); then
66
+ parts+="${script}:✓ "
67
+ else
68
+ parts+="${script}:✗ "
69
+ overall="failed"
70
+ fi
71
+ else
72
+ parts+="${script}:skip "
73
+ fi
74
+ done
75
+
76
+ emit_ctx "$overall" "${parts% }"
77
+ exit 0
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ # cargo.sh — afk pre-spawn detector for Rust projects.
3
+ #
4
+ # Applies when a Cargo.toml exists at PROJECT_ROOT. Exports
5
+ # CARGO_TARGET_DIR=${RED_AFK_CARGO_TARGET_BASE:-/opt/cargo-target}/slot-${RED_AFK_SLOT}
6
+ # so each worker slot gets its own target directory and Cargo
7
+ # never serializes on .cargo-lock between workers.
8
+ #
9
+ # Exit codes:
10
+ # 1 — not a Rust project (no Cargo.toml). The orchestrator treats
11
+ # this as "skip silently".
12
+ # 0 — applies; KEY=value line(s) written to $RED_AFK_HOOK_ENV_FILE.
13
+
14
+ set -u
15
+
16
+ project_root="${PROJECT_ROOT:-$(pwd)}"
17
+ [ -f "$project_root/Cargo.toml" ] || exit 1
18
+
19
+ slot="${RED_AFK_SLOT:-0}"
20
+ base="${RED_AFK_CARGO_TARGET_BASE:-/opt/cargo-target}"
21
+ target_dir="${base}/slot-${slot}"
22
+
23
+ mkdir -p "$target_dir"
24
+
25
+ : "${RED_AFK_HOOK_ENV_FILE:?RED_AFK_HOOK_ENV_FILE not set}"
26
+ printf 'CARGO_TARGET_DIR=%s\n' "$target_dir" >> "$RED_AFK_HOOK_ENV_FILE"
27
+ exit 0
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # gradle.sh — afk pre-spawn detector for Gradle projects.
3
+ #
4
+ # Applies when a build.gradle* file exists at PROJECT_ROOT AND the
5
+ # operator has opted in by setting RED_AFK_GRADLE_USER_HOME_BASE. Exports
6
+ # GRADLE_USER_HOME=${RED_AFK_GRADLE_USER_HOME_BASE}/slot-${RED_AFK_SLOT} so each
7
+ # worker slot gets its own Gradle home (caches, daemons, lockfiles).
8
+ #
9
+ # Opt-in by env var is deliberate: we will not claim a path on the
10
+ # user's filesystem without their consent. Without
11
+ # RED_AFK_GRADLE_USER_HOME_BASE the detector is a no-op (exit 1).
12
+ #
13
+ # Exit codes:
14
+ # 1 — not a Gradle project, or RED_AFK_GRADLE_USER_HOME_BASE unset.
15
+ # 0 — applies; KEY=value line written to $RED_AFK_HOOK_ENV_FILE.
16
+
17
+ set -u
18
+
19
+ project_root="${PROJECT_ROOT:-$(pwd)}"
20
+
21
+ has_gradle_build=false
22
+ for candidate in "$project_root"/build.gradle*; do
23
+ [ -f "$candidate" ] || continue
24
+ has_gradle_build=true
25
+ break
26
+ done
27
+ [ "$has_gradle_build" = true ] || exit 1
28
+
29
+ [ -n "${RED_AFK_GRADLE_USER_HOME_BASE:-}" ] || exit 1
30
+
31
+ slot="${RED_AFK_SLOT:-0}"
32
+ home_dir="${RED_AFK_GRADLE_USER_HOME_BASE}/slot-${slot}"
33
+
34
+ mkdir -p "$home_dir"
35
+
36
+ : "${RED_AFK_HOOK_ENV_FILE:?RED_AFK_HOOK_ENV_FILE not set}"
37
+ printf 'GRADLE_USER_HOME=%s\n' "$home_dir" >> "$RED_AFK_HOOK_ENV_FILE"
38
+ exit 0
@@ -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 }}
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # red-cargo — built-in pre_worktree library hook for Rust projects.
3
+ #
4
+ # Contract (ADR 0026, PRD #207, issue #211):
5
+ # stdin — JSON object: pre_worktree context (issue, target, branch, env).
6
+ # applicability — decided by the Node resolver before this leaf runs.
7
+ # stdout — a JSON object that merges
8
+ # {env: {CARGO_TARGET_DIR: <slot dir>}} into the context.
9
+ # exit — 0. Never aborts the chain.
10
+ #
11
+ # Side effect: mkdir -p the slot target dir so cargo can use it immediately.
12
+ # CARGO_TARGET_DIR=${RED_AFK_CARGO_TARGET_BASE:-/opt/cargo-target}/slot-${RED_AFK_SLOT:-0}
13
+ # so each worker slot gets its own target directory and Cargo never
14
+ # serializes on .cargo-lock between workers.
15
+ #
16
+ # Env contract (RED_AFK_* exported by the orchestrator):
17
+ # RED_AFK_SLOT — worker slot number (0-based); defaults to 0.
18
+ # RED_AFK_CARGO_TARGET_BASE — base dir for slot target dirs; defaults to
19
+ # /opt/cargo-target. Set in .red/config.yaml
20
+ # via afk.cargo_target_base or by environment.
21
+ #
22
+ # Shadowing: place a same-named file in .red/hooks/red-cargo to replace this
23
+ # default with a project-local variant. An empty (exit-0) file disables it.
24
+
25
+ set -u
26
+
27
+ ctx="$(timeout "${RED_SKILLS_HOOK_STDIN_TIMEOUT_S:-5s}" cat 2>/dev/null || true)"
28
+ [ -n "$ctx" ] || ctx='{}'
29
+
30
+ slot="${RED_AFK_SLOT:-0}"
31
+ base="${RED_AFK_CARGO_TARGET_BASE:-/opt/cargo-target}"
32
+ target_dir="${base}/slot-${slot}"
33
+
34
+ mkdir -p "$target_dir" 2>/dev/null || true
35
+
36
+ printf '%s' "$ctx" | jq -c \
37
+ --arg t "$target_dir" \
38
+ '. + {env: ((.env // {}) + {CARGO_TARGET_DIR: $t})}'
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # red-envelope — built-in post_attempt library hook (ADR 0026, PRD #207,
3
+ # issue #212; renamed from post_worker in #226).
4
+ #
5
+ # Reconciles the iteration's intermediate envelope state on the AFK state
6
+ # file as soon as the runner returns — before any user post_attempt hook
7
+ # runs, so a user notifier/pager integration that reads state already sees
8
+ # the worker's terminal result classified.
9
+ #
10
+ # Contract:
11
+ # stdin — JSON object: post_attempt context with .result.status of
12
+ # "success" or "fail".
13
+ # stdout — empty (context unchanged). Pure side-effect on the state file.
14
+ # exit — 0. Never aborts the chain.
15
+ #
16
+ # Env contract (RED_AFK_* exported by the orchestrator):
17
+ # RED_AFK_STATE_FILE — absolute path to the iteration's afk.state.json;
18
+ # when unset/missing, the default is a no-op.
19
+ # RED_AFK_RESULT_STATUS — redundant fallback ("success"|"fail") for
20
+ # runners/tests that cannot shape a context JSON;
21
+ # if ctx.result.status is absent this is used
22
+ # instead.
23
+ #
24
+ # Shadowing: place a same-named file in .red/hooks/red-envelope to replace
25
+ # this default with a project-local variant. An empty (exit-0) file disables
26
+ # it.
27
+
28
+ set -uo pipefail
29
+
30
+ ctx="$(timeout "${RED_SKILLS_HOOK_STDIN_TIMEOUT_S:-5s}" cat 2>/dev/null || true)"
31
+
32
+ state_file="${RED_AFK_STATE_FILE:-}"
33
+ [[ -z "$state_file" || ! -f "$state_file" ]] && exit 0
34
+
35
+ status=""
36
+ if [[ -n "$ctx" ]]; then
37
+ status="$(printf '%s' "$ctx" | jq -r '.result.status // empty' 2>/dev/null || true)"
38
+ fi
39
+ [[ -z "$status" ]] && status="${RED_AFK_RESULT_STATUS:-}"
40
+ [[ -z "$status" ]] && exit 0
41
+
42
+ tmp="${state_file}.tmp.$$"
43
+ if jq --arg s "$status" '.current = ((.current // {}) + {result_status: $s})' \
44
+ "$state_file" > "$tmp" 2>/dev/null; then
45
+ mv "$tmp" "$state_file"
46
+ else
47
+ rm -f "$tmp"
48
+ fi
49
+
50
+ exit 0
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # red-gradle — built-in pre_worktree library hook for Gradle projects.
3
+ #
4
+ # Contract (ADR 0026, PRD #207, issue #211):
5
+ # stdin — JSON object: pre_worktree context (issue, target, branch, env).
6
+ # applicability — decided by the Node resolver before this leaf runs.
7
+ # stdout — empty (context unchanged) when RED_AFK_GRADLE_USER_HOME_BASE is
8
+ # unset (opt-in by env var — we do not claim a path on the user's
9
+ # filesystem without their consent). Otherwise a JSON object
10
+ # that merges {env: {GRADLE_USER_HOME: <slot dir>}} into ctx.
11
+ # exit — 0 on either branch. Never aborts the chain.
12
+ #
13
+ # Side effect: mkdir -p the slot Gradle home so daemons / caches are isolated
14
+ # between worker slots.
15
+ #
16
+ # Env contract (RED_AFK_* exported by the orchestrator):
17
+ # RED_AFK_SLOT — worker slot number (0-based); defaults to 0.
18
+ # RED_AFK_GRADLE_USER_HOME_BASE — base dir for per-slot Gradle homes;
19
+ # UNSET by default (opt-in). Set in
20
+ # .red/config.yaml or by environment to
21
+ # activate per-slot Gradle isolation.
22
+ #
23
+ # Shadowing: place a same-named file in .red/hooks/red-gradle to replace this
24
+ # default with a project-local variant. An empty (exit-0) file disables it.
25
+
26
+ set -u
27
+
28
+ ctx="$(timeout "${RED_SKILLS_HOOK_STDIN_TIMEOUT_S:-5s}" cat 2>/dev/null || true)"
29
+ [ -n "$ctx" ] || ctx='{}'
30
+
31
+ if [ -z "${RED_AFK_GRADLE_USER_HOME_BASE:-}" ]; then
32
+ exit 0
33
+ fi
34
+
35
+ slot="${RED_AFK_SLOT:-0}"
36
+ home_dir="${RED_AFK_GRADLE_USER_HOME_BASE}/slot-${slot}"
37
+
38
+ mkdir -p "$home_dir" 2>/dev/null || true
39
+
40
+ printf '%s' "$ctx" | jq -c \
41
+ --arg h "$home_dir" \
42
+ '. + {env: ((.env // {}) + {GRADLE_USER_HOME: $h})}'
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ # red-heartbeat — built-in post_attempt library hook (ADR 0026, PRD #207,
3
+ # issue #212; renamed from post_worker in #226).
4
+ #
5
+ # Stops the orchestrator's per-minute heartbeat side-channel (when a PID is
6
+ # available) and writes the "iteration stopped" boundary marker into the
7
+ # iteration log, migrating the inline heartbeat_stop that used to fire from
8
+ # afk.sh right after run_inner returned.
9
+ #
10
+ # Contract:
11
+ # stdin — JSON object: post_attempt context (issue, workspace, result).
12
+ # stdout — empty (context unchanged). Pure side-effect.
13
+ # exit — 0. Never aborts the post_attempt chain.
14
+ #
15
+ # Env contract (RED_AFK_* exported by the orchestrator):
16
+ # RED_AFK_HEARTBEAT_PID — PID of the orchestrator heartbeat subprocess;
17
+ # empty/unset when no external heartbeat runs
18
+ # (e.g. the native TS runtime manages heartbeat
19
+ # internally). Killing a stale pid is guarded via
20
+ # kill -0.
21
+ # RED_AFK_ITER_LOG — absolute path to the Worker's canonical
22
+ # worker.log.toonl; the boundary payload is appended only when the file
23
+ # exists. Unset/empty → skip the marker.
24
+ #
25
+ # Shadowing: place a same-named file in .red/hooks/red-heartbeat to replace
26
+ # this default. An empty (exit-0) file disables it; the orchestrator EXIT
27
+ # trap remains the backstop for heartbeat teardown.
28
+
29
+ set -uo pipefail
30
+
31
+ # Drain stdin (context flows through unchanged — emit nothing).
32
+ timeout "${RED_SKILLS_HOOK_STDIN_TIMEOUT_S:-5s}" cat >/dev/null 2>&1 || true
33
+
34
+ pid="${RED_AFK_HEARTBEAT_PID:-}"
35
+ if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
36
+ kill "$pid" 2>/dev/null || true
37
+ # Best-effort reap; the sub-shell may already be exiting.
38
+ wait "$pid" 2>/dev/null || true
39
+ fi
40
+
41
+ iterlog="${RED_AFK_ITER_LOG:-}"
42
+ if [[ -n "$iterlog" && -f "$iterlog" ]]; then
43
+ printf '[heartbeat] iteration stopped at %s\n' "$(date -Is)" >> "$iterlog"
44
+ fi
45
+
46
+ exit 0
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # red-validation — built-in post_merge library hook (ADR 0026, PRD #207,
3
+ # issue #213).
4
+ #
5
+ # Runs CI/smoke validation against the merged primary checkout so user
6
+ # post_merge hooks see the validation status already reconciled into the
7
+ # context. The pre-merge feedback() step in the mechanism layer stays as the
8
+ # safety gate (ADR 0008 — anything that must abort the merge loudly is
9
+ # mechanism, not a hook); this default exposes a post-merge re-run hook
10
+ # authors can disable, observe, or replace.
11
+ #
12
+ # Contract:
13
+ # stdin — JSON object: post_merge context (issue, workspace, merge_commit).
14
+ # stdout — JSON object: the same context with
15
+ # .result = {validation_status: "passed"|"failed"|"skipped",
16
+ # validation_summary: <one-line string>} merged into
17
+ # it. Non-JSON stdin → pass-through with no validation attempt.
18
+ # exit — 0. Post_merge policy is continue; even a failing validation
19
+ # only logs (it cannot roll back the merge — that ship has sailed
20
+ # at this lifecycle point).
21
+ #
22
+ # Env contract (RED_AFK_* exported by the orchestrator):
23
+ # RED_AFK_WORKSPACE — primary checkout (post-merge tip). When unset or
24
+ # not a directory, the default is a no-op.
25
+ # RED_AFK_MERGE_COMMIT — full sha of the merge commit on target. Not
26
+ # consumed by this default but documented so user
27
+ # hooks reading env directly see a consistent view.
28
+ #
29
+ # Shadowing: place a same-named file in .red/hooks/red-validation to replace
30
+ # this default with a project-local variant. An empty (exit-0) file disables
31
+ # it.
32
+
33
+ set -uo pipefail
34
+
35
+ ctx="$(timeout "${RED_SKILLS_HOOK_STDIN_TIMEOUT_S:-5s}" cat 2>/dev/null || true)"
36
+ ws="${RED_AFK_WORKSPACE:-}"
37
+
38
+ emit_ctx() {
39
+ local status="$1" summary="$2"
40
+ if [[ -z "$ctx" ]]; then
41
+ return 0
42
+ fi
43
+ printf '%s' "$ctx" \
44
+ | jq --arg s "$status" --arg sum "$summary" \
45
+ '.result = ((.result // {}) + {validation_status: $s, validation_summary: $sum})' \
46
+ 2>/dev/null \
47
+ || printf '%s' "$ctx"
48
+ }
49
+
50
+ if [[ -z "$ws" || ! -d "$ws" ]]; then
51
+ emit_ctx "skipped" "no workspace"
52
+ exit 0
53
+ fi
54
+
55
+ if ! [[ -f "$ws/package.json" ]]; then
56
+ emit_ctx "skipped" "no package.json"
57
+ exit 0
58
+ fi
59
+
60
+ # Run pnpm test/typecheck/lint/build at the workspace root if the script
61
+ # exists. This mirrors the mechanism-owned feedback() step shape but
62
+ # operates on the merged tip rather than the worker's worktree.
63
+ overall="passed"
64
+ parts=""
65
+ for script in test typecheck lint build; do
66
+ if jq -e --arg s "$script" '.scripts[$s] // empty' "$ws/package.json" >/dev/null 2>&1; then
67
+ if ( cd "$ws" && pnpm -s "$script" >/dev/null 2>&1 ); then
68
+ parts+="${script}:✓ "
69
+ else
70
+ parts+="${script}:✗ "
71
+ overall="failed"
72
+ fi
73
+ else
74
+ parts+="${script}:skip "
75
+ fi
76
+ done
77
+
78
+ emit_ctx "$overall" "${parts% }"
79
+ exit 0