@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.
- package/bin/build-info.mjs +87 -0
- package/bin/cli-frontdoor.mjs +14 -0
- package/bin/red-skills-brain.mjs +17 -22
- package/bin/red-skills-content.mjs +17 -0
- package/bin/red-skills-hook.mjs +45 -0
- package/bin/red-skills-memory.mjs +17 -22
- package/bin/red-skills-redskilled-mcp.mjs +2 -0
- package/bin/red-skills-resource.mjs +21 -0
- package/bin/runtime-compatibility.mjs +17 -0
- package/bin/runtime-paths.mjs +13 -0
- package/dist/code-nav.bundle.min.mjs +1 -1
- package/dist/opencode-host.bundle.min.mjs +1 -1
- package/dist/redskilled-mcp.bundle.min.mjs +36 -36
- package/dist/redskilled.bundle.min.mjs +234 -230
- package/dist/release.bundle.min.mjs +1 -1
- package/dist/rsp-core.bundle.min.mjs +1 -1
- package/dist/rsp.bundle.min.mjs +1 -1
- package/dist/statusline.bundle.min.mjs +1 -1
- package/package.json +9 -4
- package/runtime/hook-routes.json +125 -0
- package/runtime/plugins/brain/scripts/bootstrap.mjs +656 -0
- package/runtime/plugins/brain/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/hooks/branch-lock-codex.sh +105 -0
- package/runtime/plugins/dev/hooks/code-nav-mcp.sh +63 -0
- package/runtime/plugins/dev/hooks/command-guard.sh +893 -0
- package/runtime/plugins/dev/hooks/ensure-codex-statusline.mjs +144 -0
- package/runtime/plugins/dev/hooks/red-fetch.mjs +38 -0
- package/runtime/plugins/dev/hooks/redskilled-mcp.sh +64 -0
- package/runtime/plugins/dev/hooks/rsp-hook.sh +343 -0
- package/runtime/plugins/dev/hooks/tests/command-guard.test.sh +378 -0
- package/runtime/plugins/dev/hooks/tests/rsp-hook.test.sh +229 -0
- package/runtime/plugins/dev/scripts/memory-bridge.sh +230 -0
- package/runtime/plugins/dev/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/scripts/tests/memory-bridge.test.sh +555 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/gradle-pre-worktree.sh +45 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/cargo.sh +27 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/gradle.sh +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/only-mine.sh +32 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-cargo +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-envelope +50 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-gradle +42 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-heartbeat +46 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-validation +79 -0
- package/runtime/plugins/dev/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/scripts/install-runtime-shim.sh +181 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
- package/runtime/plugins/dev/skills/engineering/wizard/template.sh +211 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +230 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
- package/runtime/plugins/internal/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/internal/skills/maintainer/create-plugin/scripts/create-plugin.sh +308 -0
- package/runtime/plugins/memory/scripts/bootstrap.mjs +659 -0
- package/runtime/plugins/memory/scripts/structural-smoke.sh +7 -0
- 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
|