@soleur/opencode 1.0.0 → 3.354.3
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/commands/go.md +6 -5
- package/commands/sync.md +2 -2
- package/install.js +6 -85
- package/lib/harness.ts +1 -1
- package/lib/heartbeat-manifest.ts +19 -0
- package/lib/pr-merge-poll.ts +2 -2
- package/lib/workflow-fidelity.ts +2 -2
- package/opencode/INSTRUCTIONS.md +19 -0
- package/opencode/plugin.ts +343 -18
- package/package.json +1 -1
- package/scripts/admin-merge-ready.sh +1 -1
- package/scripts/admin-merge-ready.test.sh +0 -0
- package/scripts/affected-tests.sh +0 -0
- package/scripts/alpha-metrics.sh +0 -0
- package/scripts/alpha-metrics.test.sh +0 -0
- package/scripts/check-red-on-main.sh +0 -0
- package/scripts/ci-head-verdict.sh +54 -6
- package/scripts/cloud-detect.sh +0 -0
- package/scripts/delegation-verify.sh +0 -0
- package/scripts/delegation-verify.test.sh +0 -0
- package/scripts/deploy-arm.sh +0 -0
- package/scripts/domain-model-drift.sh +0 -0
- package/scripts/emit-decision.sh +0 -0
- package/scripts/emit-decision.test.sh +0 -0
- package/scripts/ext-filter-divergence.sh +0 -0
- package/scripts/grok-fidelity-gate.sh +0 -0
- package/scripts/grok-pre-push-gate.sh +0 -0
- package/scripts/install-opencode-adapter.ts +118 -129
- package/scripts/lib/affected-tests/common.sh +0 -0
- package/scripts/lib/affected-tests/go.sh +0 -0
- package/scripts/lib/affected-tests/js.sh +0 -0
- package/scripts/lib/affected-tests/nx-turbo.sh +0 -0
- package/scripts/lib/affected-tests/python.sh +0 -0
- package/scripts/lib/session-state.sh +0 -0
- package/scripts/lib/tmp-classify.sh +0 -0
- package/scripts/monitor-pr-checks.sh +1 -1
- package/scripts/pipeline-tally.sh +0 -0
- package/scripts/plugin-version.sh +0 -0
- package/scripts/precommit-guard.sh +3 -1
- package/scripts/precommit-guard.test.sh +0 -0
- package/scripts/render-c4-model.sh +0 -0
- package/scripts/resolve-regenerable-conflicts.sh +0 -0
- package/scripts/run-in-pid-namespace.sh +0 -0
- package/scripts/setup-ralph-loop.sh +0 -0
- package/scripts/sync-npm-package.ts +74 -87
- package/scripts/sync-opencode-adapter.ts +19 -0
- package/scripts/sync-pr-behind.sh +183 -70
- package/scripts/taste-profile-update.sh +0 -0
- package/scripts/taste-profile-update.test.sh +0 -0
- package/scripts/write-budget-marker.sh +0 -0
- package/skills/deepen-plan/SKILL.md +1 -1
- package/skills/deploy/references/hetzner-setup.md +5 -3
- package/skills/drain-prs/SKILL.md +3 -2
- package/skills/fix-issue/SKILL.md +8 -8
- package/skills/fix-issue/references/agent-authored-exclusion.md +4 -2
- package/skills/fix-issue/references/exclude-label-jq-snippet.md +4 -3
- package/skills/merge-pr/SKILL.md +5 -3
- package/skills/model-launch-review/SKILL.md +4 -4
- package/skills/one-shot/SKILL.md +1 -1
- package/skills/plan/SKILL.md +1 -1
- package/skills/plan/references/plan-sharp-edges.md +7 -1
- package/skills/postmerge/SKILL.md +2 -2
- package/skills/postmerge/references/deploy-status-debugging.md +3 -0
- package/skills/review/SKILL.md +14 -15
- package/skills/review/references/review-evidence-comment.md +36 -0
- package/skills/review/references/risk-tier-and-fix-rounds.md +2 -0
- package/skills/ship/SKILL.md +14 -13
- package/skills/ship/references/cleanup-merged-archival.md +39 -0
- package/skills/ship/references/delegated-act-authoring-form.md +7 -0
- package/skills/ship/references/epic-close-sweep.md +62 -0
- package/skills/ship/references/merge-queue-dequeue.md +1 -1
- package/skills/ship/references/operator-holds.md +30 -0
- package/skills/ship/references/queue-mode.md +1 -1
- package/skills/ship/references/ready-run-wait.md +3 -3
- package/skills/test-fix-loop/SKILL.md +1 -1
- package/skills/verification-scaffold/SKILL.md +73 -0
- package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
- package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
- package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
- package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
- package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
- package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
- package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
- package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
- package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
- package/skills/verification-scaffold/references/guards.tsv +21 -0
- package/skills/verification-scaffold/references/verification-readme.template +55 -0
- package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
- package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
- package/skills/verification-scaffold/test/generator.test.sh +319 -0
- package/skills/verification-scaffold/test/manifest.test.sh +144 -0
- package/skills/work/SKILL.md +2 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# precommit-guard.sh — refuse `git commit` on main/master, without hook execution.
|
|
3
|
+
#
|
|
4
|
+
# Canonical source for the commit-on-main check (Soleur Cloud Mode, FR5).
|
|
5
|
+
# Plugin command hooks are documented cloud-capable for most events, but a
|
|
6
|
+
# skill cannot rely on hook execution in a not-local session — and user repos
|
|
7
|
+
# consuming via requiredPlugins have no .claude/hooks at all. work/ship/one-shot
|
|
8
|
+
# invoke this script directly so the refusal is structural in both worlds.
|
|
9
|
+
#
|
|
10
|
+
# Scope: commit-on-main ONLY. The DONE-marker stop-gate is not extractable —
|
|
11
|
+
# it reads hook-stdin transcript data a standalone script cannot see; it stays
|
|
12
|
+
# prose + the plugin Stop hook.
|
|
13
|
+
#
|
|
14
|
+
# Self-contained by design: no jq, no .claude/hooks/lib vendoring (those paths
|
|
15
|
+
# resolve relative to the repo, which is wrong when this script runs from the
|
|
16
|
+
# plugin install cache), no emit_incident — callers log their own incidents.
|
|
17
|
+
# ONE sibling dependency: scripts/lib/wrapper-boundary.sh ships beside this
|
|
18
|
+
# script in the plugin cache, so $(dirname)/lib/… resolves in both contexts
|
|
19
|
+
# (#10248 — the canonical wrapper-prefix grammar the COMMIT_RE below consumes).
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# precommit-guard.sh "COMMAND" check a shell command string
|
|
23
|
+
# precommit-guard.sh --cwd DIR also check DIR's branch (a `cd DIR &&`
|
|
24
|
+
# or `git -C DIR` inside COMMAND wins)
|
|
25
|
+
#
|
|
26
|
+
# Exit 0 = allowed (not a commit, or not on main/master).
|
|
27
|
+
# Exit 1 = refused — reason on stderr.
|
|
28
|
+
# Exit 2 = usage error.
|
|
29
|
+
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
|
|
32
|
+
COMMAND=""
|
|
33
|
+
CWD=""
|
|
34
|
+
while [[ $# -gt 0 ]]; do
|
|
35
|
+
case "$1" in
|
|
36
|
+
--cwd) CWD="${2:-}"; shift 2 ;;
|
|
37
|
+
-*) echo "precommit-guard.sh: unknown flag: $1" >&2; exit 2 ;;
|
|
38
|
+
*) COMMAND="$1"; shift ;;
|
|
39
|
+
esac
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
# Detection is per pipeline segment: a `git commit` can hide anywhere in a
|
|
43
|
+
# chain — `git add && git commit`, `cd repo; git commit`, `printf msg |
|
|
44
|
+
# git commit -F -`, `LEFTHOOK=0 git commit`. Split COMMAND on the chain
|
|
45
|
+
# operators (&&, ||, ;, |, newline), then judge each segment.
|
|
46
|
+
#
|
|
47
|
+
# Known lexical limits (documented, not silently missed): subshell/eval forms
|
|
48
|
+
# like `bash -c 'git commit'`, `$(git commit)`, `( git commit )`, quoted
|
|
49
|
+
# `-C`/`cd` paths containing spaces, `git -c <cfg> commit` side effects beyond
|
|
50
|
+
# detection, and commits created without `git commit` (merge/pull/cherry-pick/
|
|
51
|
+
# rebase/continue) are out of this guard's declared scope — commit-on-main ONLY.
|
|
52
|
+
# Opaque string payloads (`rtk run -c '…'`, `rtk summary --shell '…'`) join that
|
|
53
|
+
# same declared class — the boundary fragment deliberately excludes the
|
|
54
|
+
# -c/--command/--shell atoms so they cannot match here.
|
|
55
|
+
_PC_GUARD_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
56
|
+
unset VERIFICATION_ENV_ASN_RE VERIFICATION_LAUNCHER_RE VERIFICATION_RTK_RE VERIFICATION_BOUNDARY_PFX VERIFICATION_BOUNDARY_PFX_LOADED
|
|
57
|
+
if [[ -f "$_PC_GUARD_DIR/lib/wrapper-boundary.sh" ]]; then
|
|
58
|
+
# shellcheck source=/dev/null
|
|
59
|
+
source "$_PC_GUARD_DIR/lib/wrapper-boundary.sh" 2>/dev/null || true
|
|
60
|
+
fi
|
|
61
|
+
if [[ "${VERIFICATION_BOUNDARY_PFX_LOADED:-0}" != 1 ]]; then
|
|
62
|
+
unset VERIFICATION_ENV_ASN_RE VERIFICATION_LAUNCHER_RE VERIFICATION_RTK_RE VERIFICATION_BOUNDARY_PFX
|
|
63
|
+
echo "precommit-guard.sh: wrapper-boundary.sh unavailable at $_PC_GUARD_DIR/lib — commit gate runs pre-#10248 matching" >&2
|
|
64
|
+
fi
|
|
65
|
+
unset _PC_GUARD_DIR
|
|
66
|
+
# #10248: the shared fragment replaces the inline env-asn/launcher alternation —
|
|
67
|
+
# one canonical atom set for every boundary-anchored gate. The :- fallback is
|
|
68
|
+
# the PRE-FIX atom set (env + launcher) — a superset of this file's prior
|
|
69
|
+
# env*/launcher?/env* sandwich, which is the safe direction for a deny gate.
|
|
70
|
+
COMMIT_RE='^[[:space:]]*('"${VERIFICATION_BOUNDARY_PFX:-([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]+|sudo|command|nice|env|xargs)}"'[[:space:]]+)*git([[:space:]]+(-C[[:space:]]+[^[:space:]]+|-c[[:space:]]+[^[:space:]]+|--git-dir=[^[:space:]]+|--git-dir[[:space:]]+[^[:space:]]+|-[A-Za-z]))*[[:space:]]+commit([[:space:]]|$)'
|
|
71
|
+
|
|
72
|
+
last_cd=""
|
|
73
|
+
while IFS= read -r seg; do
|
|
74
|
+
# Track the dir the chain has cd'd into — it applies to later segments.
|
|
75
|
+
cd_hit="$(grep -oE '(^|[[:space:]])cd[[:space:]]+[^[:space:]]+' <<<"$seg" | tail -n 1 | sed -E 's/.*cd[[:space:]]+//' || true)"
|
|
76
|
+
[[ -n "$cd_hit" ]] && last_cd="$cd_hit"
|
|
77
|
+
|
|
78
|
+
grep -qE "$COMMIT_RE" <<<"$seg" || continue
|
|
79
|
+
|
|
80
|
+
# Resolve the repo THIS segment commits into. Priority: the -C / --git-dir
|
|
81
|
+
# attached to the commit's own git invocation, then a GIT_DIR env-assignment
|
|
82
|
+
# prefix (LEFTHOOK=0-style prefixes are the reason env-assignments are scanned
|
|
83
|
+
# at all — dropping GIT_DIR would resolve the wrong repo); then the chain's
|
|
84
|
+
# most recent cd; then --cwd; then ambient $PWD.
|
|
85
|
+
target="" git_dir=""
|
|
86
|
+
if grep -qoE -- '--git-dir[=[:space:]][^[:space:]]+' <<<"$seg"; then
|
|
87
|
+
git_dir="$(grep -oE -- '--git-dir[=[:space:]][^[:space:]]+' <<<"$seg" | tail -n 1 | sed -E 's/^--git-dir[=[:space:]]+//' || true)"
|
|
88
|
+
elif grep -qoE -- '(^|[[:space:]])GIT_DIR=[^[:space:]]+' <<<"$seg"; then
|
|
89
|
+
git_dir="$(grep -oE -- '(^|[[:space:]])GIT_DIR=[^[:space:]]+' <<<"$seg" | tail -n 1 | sed -E 's/.*GIT_DIR=//' || true)"
|
|
90
|
+
else
|
|
91
|
+
git_c="$(grep -oE -- '-C[[:space:]]+[^[:space:]]+' <<<"$seg" | tail -n 1 | sed -E 's/^-C[[:space:]]+//' || true)"
|
|
92
|
+
[[ -n "$git_c" ]] && target="$git_c"
|
|
93
|
+
fi
|
|
94
|
+
if [[ -z "$target" && -z "$git_dir" ]]; then
|
|
95
|
+
for cand in "$last_cd" "$CWD" "$PWD"; do
|
|
96
|
+
[[ -n "$cand" && -d "$cand" ]] && target="$cand" && break
|
|
97
|
+
done
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
if [[ -n "$git_dir" ]]; then
|
|
101
|
+
BRANCH="$(git --git-dir="$git_dir" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
102
|
+
elif [[ -n "$target" && -d "$target" ]]; then
|
|
103
|
+
BRANCH="$(git -C "$target" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
104
|
+
else
|
|
105
|
+
continue # nowhere to resolve — nothing to refuse on
|
|
106
|
+
fi
|
|
107
|
+
# The alternation is the stamped default-branch set — soleur:verification-scaffold
|
|
108
|
+
# emits this file with `@DEFAULT_BRANCHES@` substituted for the target's resolved default.
|
|
109
|
+
if [[ "$BRANCH" =~ ^(@DEFAULT_BRANCHES@)$ ]]; then
|
|
110
|
+
printf 'BLOCKED: Committing directly to %s is not allowed. Create a feature branch first.\n' "$BRANCH" >&2
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
done < <(printf '%s\n' "$COMMAND" | sed -E 's/&&|\|\||[;|]/\n/g')
|
|
114
|
+
|
|
115
|
+
exit 0
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# secret-scan.sh — PreToolUse hook emitted by soleur:verification-scaffold.
|
|
3
|
+
# Scans the staged index for secret-shaped strings (PEM bodies, vendor tokens,
|
|
4
|
+
# cloud keys) via gitleaks and reports before a `git commit` tool call lands.
|
|
5
|
+
#
|
|
6
|
+
# MODE=@MODE@ — stamped at emit time:
|
|
7
|
+
# advisory (the installed default): real findings surface as `ask`; tool
|
|
8
|
+
# errors (missing/unrunnable gitleaks, absent ruleset, empty report) warn
|
|
9
|
+
# and allow — loudly, never silently.
|
|
10
|
+
# enforce: findings deny; tool errors deny with the install remediation.
|
|
11
|
+
# Promote by changing MODE to "enforce" — a one-line flip. The predicates are
|
|
12
|
+
# identical in both modes; only the terminal decision changes.
|
|
13
|
+
#
|
|
14
|
+
# Hook stdin: JSON payload with tool_input. Hook stdout: the PreToolUse JSON
|
|
15
|
+
# decision envelope. Exit code is always 0 — the envelope carries the verdict.
|
|
16
|
+
#
|
|
17
|
+
# Portability floor: bash + POSIX tools only (stock macOS included — the
|
|
18
|
+
# timeout->gtimeout->bare-fallback below is deliberate). No network egress.
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
MODE="@MODE@"
|
|
22
|
+
|
|
23
|
+
# --- decision envelopes -------------------------------------------------------
|
|
24
|
+
# _vs_decide <allow|ask|deny> [reason]: one JSON envelope on stdout, exit 0.
|
|
25
|
+
# Reasons are escaped (backslash, quote, newline) before embedding.
|
|
26
|
+
_vs_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' '; }
|
|
27
|
+
_vs_decide() {
|
|
28
|
+
local d="$1" r="${2:-}"
|
|
29
|
+
if [ -n "$r" ]; then
|
|
30
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"%s","permissionDecisionReason":"%s"}}\n' \
|
|
31
|
+
"$d" "$(_vs_escape "$r")"
|
|
32
|
+
else
|
|
33
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"%s"}}\n' "$d"
|
|
34
|
+
fi
|
|
35
|
+
exit 0
|
|
36
|
+
}
|
|
37
|
+
_vs_allow() { _vs_decide allow; }
|
|
38
|
+
# _vs_tool_error <msg>: a scanner/tool problem, never a finding. Advisory warns
|
|
39
|
+
# and allows; enforce denies with remediation. `ask` can never fire on this path.
|
|
40
|
+
_vs_tool_error() {
|
|
41
|
+
local msg="secret-scan: $1"
|
|
42
|
+
printf '[secret-scan] WARN: %s\n' "$msg" >&2
|
|
43
|
+
if [ "$MODE" = "enforce" ]; then
|
|
44
|
+
_vs_decide deny "BLOCKED: $msg"
|
|
45
|
+
else
|
|
46
|
+
_vs_decide allow
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
# _vs_finding <msg>: a REAL finding — the only path that may `ask`.
|
|
50
|
+
_vs_finding() {
|
|
51
|
+
if [ "$MODE" = "enforce" ]; then
|
|
52
|
+
_vs_decide deny "BLOCKED: $1"
|
|
53
|
+
else
|
|
54
|
+
_vs_decide ask "$1"
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# --- stdin command extraction (jq -> node -> sed fallback) --------------------
|
|
59
|
+
_vs_payload="$(cat)"
|
|
60
|
+
_vs_cmd=""
|
|
61
|
+
if command -v jq >/dev/null 2>&1; then
|
|
62
|
+
_vs_cmd="$(printf '%s' "$_vs_payload" | jq -r '.tool_input.command // ""' 2>/dev/null || true)"
|
|
63
|
+
elif command -v node >/dev/null 2>&1; then
|
|
64
|
+
_vs_cmd="$(printf '%s' "$_vs_payload" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const c=JSON.parse(s).tool_input.command;process.stdout.write(typeof c==="string"?c:"")}catch(e){}})' 2>/dev/null || true)"
|
|
65
|
+
else
|
|
66
|
+
# Last-resort extraction: handles the common single-quoted-string shape.
|
|
67
|
+
# Documented limit: escaped quotes inside the command truncate the value,
|
|
68
|
+
# which only ever DE-escalates this hook (the CI job still scans the push).
|
|
69
|
+
_vs_cmd="$(printf '%s' "$_vs_payload" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"
|
|
70
|
+
fi
|
|
71
|
+
[ -n "$_vs_cmd" ] || _vs_allow
|
|
72
|
+
|
|
73
|
+
# `git commit` at a command boundary: start, or after &&, ||, ;, |, $(.
|
|
74
|
+
# Trailing space-or-end rejects commit-tree/commit-graph.
|
|
75
|
+
if ! grep -qE '(^|[[:space:]]|&&|\|\||;|\$\()[[:space:]]*git[[:space:]]+commit([[:space:]]|$)' <<<"$_vs_cmd"; then
|
|
76
|
+
_vs_allow
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# --- environment guards -------------------------------------------------------
|
|
80
|
+
GL_REMEDIATION="install gitleaks 8.24.x (e.g. 'mise use gitleaks@8.24.2' or 'brew install gitleaks') — the CI job scans every PR regardless."
|
|
81
|
+
|
|
82
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
83
|
+
_vs_allow
|
|
84
|
+
fi
|
|
85
|
+
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
86
|
+
if [ -z "$repo_root" ] || [ ! -f "$repo_root/.gitleaks.toml" ]; then
|
|
87
|
+
_vs_tool_error ".gitleaks.toml not found at the repo root — the staged scan was skipped. $GL_REMEDIATION"
|
|
88
|
+
fi
|
|
89
|
+
if ! command -v gitleaks >/dev/null 2>&1; then
|
|
90
|
+
_vs_tool_error "gitleaks is not on PATH — the staged scan was skipped. $GL_REMEDIATION"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Resolvable is not runnable: probe the binary, bounded when a bound exists
|
|
94
|
+
# (stock macOS has neither timeout nor gtimeout — a missing bound must never
|
|
95
|
+
# read as a broken scanner).
|
|
96
|
+
gl_to=()
|
|
97
|
+
if command -v timeout >/dev/null 2>&1; then gl_to=(timeout 10)
|
|
98
|
+
elif command -v gtimeout >/dev/null 2>&1; then gl_to=(gtimeout 10); fi
|
|
99
|
+
gl_rc=0
|
|
100
|
+
{ ${gl_to[@]+"${gl_to[@]}"} gitleaks version >/dev/null; } 2>/dev/null || gl_rc=$?
|
|
101
|
+
if [ "$gl_rc" -ne 0 ]; then
|
|
102
|
+
_vs_tool_error "gitleaks resolves on PATH but cannot run (rc=$gl_rc — an unpinned version-manager shim?). $GL_REMEDIATION"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# --- staged scan ---------------------------------------------------------------
|
|
106
|
+
# Colour is pinned off on BOTH config surfaces: gitleaks runs git itself, and a
|
|
107
|
+
# user-side color.ui/color.diff=always wraps patch lines in ANSI, which makes
|
|
108
|
+
# the scan report ZERO findings on a staged key (measured). GIT_CONFIG_COUNT
|
|
109
|
+
# appends so a caller's own -c settings survive; GIT_CONFIG_PARAMETERS is a
|
|
110
|
+
# separate source read AFTER the count list, so it is pinned too.
|
|
111
|
+
report_file="$(mktemp -t verification-scan-XXXXXX.json)"
|
|
112
|
+
nodiff_dir="$(mktemp -d -t verification-nodiff-XXXXXX)"
|
|
113
|
+
nodiff_report="$(mktemp -t verification-nodiff-XXXXXX.json)"
|
|
114
|
+
trap 'rm -f "$report_file" "$nodiff_report"; rm -rf "$nodiff_dir"' EXIT INT TERM
|
|
115
|
+
|
|
116
|
+
gl_cfg_n="${GIT_CONFIG_COUNT:-0}"
|
|
117
|
+
case "$gl_cfg_n" in ''|*[!0-9]*) gl_cfg_n=0 ;; esac
|
|
118
|
+
scan_rc=0
|
|
119
|
+
(
|
|
120
|
+
cd "$repo_root" || exit 1
|
|
121
|
+
env "GIT_CONFIG_COUNT=$((gl_cfg_n + 2))" \
|
|
122
|
+
"GIT_CONFIG_KEY_${gl_cfg_n}=color.ui" "GIT_CONFIG_VALUE_${gl_cfg_n}=never" \
|
|
123
|
+
"GIT_CONFIG_KEY_$((gl_cfg_n + 1))=color.diff" "GIT_CONFIG_VALUE_$((gl_cfg_n + 1))=never" \
|
|
124
|
+
"GIT_CONFIG_PARAMETERS=${GIT_CONFIG_PARAMETERS:+${GIT_CONFIG_PARAMETERS} }'color.ui'='never' 'color.diff'='never'" \
|
|
125
|
+
gitleaks git --pre-commit --staged --redact --no-banner --exit-code 1 \
|
|
126
|
+
--report-format json --report-path "$report_file" >/dev/null 2>&1
|
|
127
|
+
) || scan_rc=$?
|
|
128
|
+
|
|
129
|
+
# -diff gitattribute bypass: a path marked -diff renders "Binary files …
|
|
130
|
+
# differ" with no `+` lines, so the staged scan sees nothing in it. Re-scan
|
|
131
|
+
# those paths from their STAGED blobs with `detect --no-git`, which never
|
|
132
|
+
# consults git's diff machinery.
|
|
133
|
+
gl_nodiff=()
|
|
134
|
+
while IFS= read -r -d '' _p; do
|
|
135
|
+
[ -n "$_p" ] || continue
|
|
136
|
+
if [ "$(git -C "$repo_root" check-attr diff -- "$_p" 2>/dev/null | sed 's/.*: //')" = "unset" ]; then
|
|
137
|
+
gl_nodiff+=("$_p")
|
|
138
|
+
fi
|
|
139
|
+
done < <(git -C "$repo_root" diff --cached --name-only -z --diff-filter=ACMR 2>/dev/null)
|
|
140
|
+
|
|
141
|
+
if (( ${#gl_nodiff[@]} > 0 )); then
|
|
142
|
+
for _p in "${gl_nodiff[@]}"; do
|
|
143
|
+
mkdir -p "$nodiff_dir/$(dirname "$_p")" 2>/dev/null || true
|
|
144
|
+
git -C "$repo_root" show ":$_p" > "$nodiff_dir/$_p" 2>/dev/null || true
|
|
145
|
+
done
|
|
146
|
+
nodiff_rc=0
|
|
147
|
+
gitleaks detect --no-git --source "$nodiff_dir" --redact --no-banner --exit-code 1 \
|
|
148
|
+
--report-format json --report-path "$nodiff_report" >/dev/null 2>&1 || nodiff_rc=$?
|
|
149
|
+
if [ "$nodiff_rc" -eq 1 ] && [ -s "$nodiff_report" ]; then
|
|
150
|
+
nd_count="$(grep -c '"RuleID"' "$nodiff_report" 2>/dev/null || echo 0)"
|
|
151
|
+
nd_files="$(printf '%s, ' "${gl_nodiff[@]}")"
|
|
152
|
+
_vs_finding "gitleaks found ${nd_count} secret-shaped string(s) in staged file(s) carrying a \`-diff\` gitattribute (${nd_files%, }), which hides them from the staged scan — they were re-scanned from their staged blobs. Scrub the secret bytes; if the attribute is not deliberate, remove it."
|
|
153
|
+
elif [ "$nodiff_rc" -ne 0 ] && [ "$nodiff_rc" -ne 1 ]; then
|
|
154
|
+
_vs_tool_error "the -diff re-scan exited $nodiff_rc without a findings report; those paths were NOT verified."
|
|
155
|
+
fi
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
if [ "$scan_rc" -eq 0 ]; then
|
|
159
|
+
_vs_allow
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Non-zero scan rc with an empty/absent report is a scanner failure, not a
|
|
163
|
+
# finding — it takes the tool-error path in both modes.
|
|
164
|
+
if [ ! -s "$report_file" ] || ! grep -q '"RuleID"' "$report_file" 2>/dev/null; then
|
|
165
|
+
_vs_tool_error "gitleaks exited $scan_rc but produced no findings report; the staged content was NOT scanned. Run 'gitleaks git --pre-commit --staged' locally to diagnose. $GL_REMEDIATION"
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
count="$(grep -c '"RuleID"' "$report_file" 2>/dev/null || echo 0)"
|
|
169
|
+
if command -v jq >/dev/null 2>&1; then
|
|
170
|
+
summary="$(jq -r '[.[] | "\(.File):\(.StartLine) (\(.RuleID))"] | .[0:5] | join("; ")' "$report_file" 2>/dev/null || echo "")"
|
|
171
|
+
else
|
|
172
|
+
summary="$(grep -o '"File": *"[^"]*"' "$report_file" 2>/dev/null | head -n 5 | sed 's/"File": *"//; s/"$//' | tr '\n' '; ' || true)"
|
|
173
|
+
fi
|
|
174
|
+
reason="gitleaks found ${count} secret-shaped string(s) in the staged index. Locations: ${summary:-<redacted>}. Scrub the secrets — never commit-then-rotate. Unstage with 'git restore --staged <path>', redact in place, re-stage and retry. To mark a known-safe line, annotate it '# gitleaks:allow issue:#NNN <reason>'." # waiver-doc string (issue:#10404 teaches trailer form)
|
|
175
|
+
|
|
176
|
+
_vs_finding "$reason"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# verification-gates.yml — emitted by soleur:verification-scaffold at the
|
|
2
|
+
# REPOSITORY ROOT (.github/workflows only runs from the root — a nested copy is
|
|
3
|
+
# dormant). Jobs below run advisory-first: `continue-on-error` is the chokepoint
|
|
4
|
+
# the promotion step flips (see VERIFICATION.md).
|
|
5
|
+
#
|
|
6
|
+
# Marker convention for the emitter — a job block between
|
|
7
|
+
# `# verification-scaffold:begin:<guard>` and `# verification-scaffold:end:<guard>`
|
|
8
|
+
# is kept only when that guard is selected. Do not remove the markers.
|
|
9
|
+
name: verification-gates
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
pull_request:
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
# verification-scaffold:begin:secret-scan
|
|
20
|
+
secret-scan:
|
|
21
|
+
name: secret-scan (gitleaks)
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
continue-on-error: @CI_CONTINUE_ON_ERROR@
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
26
|
+
- name: gitleaks staged+tree scan
|
|
27
|
+
# Pinned by commit SHA (supply-chain posture — never a mutable tag).
|
|
28
|
+
# Resolves gitleaks-action v2.3.9.
|
|
29
|
+
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7
|
|
30
|
+
env:
|
|
31
|
+
GITLEAKS_ENABLE_COMMENTS: "false"
|
|
32
|
+
GITLEAKS_ENABLE_SUMMARY: "true"
|
|
33
|
+
# Honour the emitted ruleset when the repo carries one.
|
|
34
|
+
GITLEAKS_CONFIG: ".gitleaks.toml"
|
|
35
|
+
# verification-scaffold:end:secret-scan
|
|
36
|
+
# verification-scaffold:begin:orphan-test
|
|
37
|
+
orphan-test:
|
|
38
|
+
name: orphan-test registration check
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
continue-on-error: @CI_CONTINUE_ON_ERROR@
|
|
41
|
+
env:
|
|
42
|
+
# Declare the files that RUN your suites; the check searches them for each
|
|
43
|
+
# tracked *.test.sh basename. Glob runner files are fine.
|
|
44
|
+
RUNNER_GLOBS: "scripts/test-all.sh scripts/run-tests* Makefile makefile package.json"
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
47
|
+
- name: orphan-test check
|
|
48
|
+
run: bash scripts/verification/orphan-test-check.sh
|
|
49
|
+
# verification-scaffold:end:orphan-test
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# wrapper-boundary.sh — canonical inter-boundary prefix grammar (#10248).
|
|
3
|
+
#
|
|
4
|
+
# Boundary-anchored gates grep for a gated command anchored at a shell chain
|
|
5
|
+
# boundary (`(^|&&|\|\||;|\s--\s)` and siblings). That anchor is blind to
|
|
6
|
+
# command wrappers: `rtk gh pr merge`, `sudo git commit`, `X=1 gh pr merge`
|
|
7
|
+
# all sit between the boundary and the gated command. ONE shared fragment —
|
|
8
|
+
# interpolated by every site — gives ~20 boundary-anchored gates a common
|
|
9
|
+
# wrapper-prefix grammar (only ~4 sites had any prefix list before; the rest
|
|
10
|
+
# had none — that absence was the defect) so the wrapper set cannot silently
|
|
11
|
+
# drift (#10248; same argv-prefix class as PR #3689's `with_lock` bypass, learnings
|
|
12
|
+
# 2026-05-12-cross-session-lock-lease-bash-primitives.md SE1).
|
|
13
|
+
#
|
|
14
|
+
# Consumption idiom — insert between the boundary alternation and the gated
|
|
15
|
+
# command:
|
|
16
|
+
# (<boundary-alternation>)[[:space:]]*(${VERIFICATION_BOUNDARY_PFX:-}[[:space:]]+)*<gated-cmd>
|
|
17
|
+
# The `:-` default is load-bearing: several hooks run `set -u`, and an unset
|
|
18
|
+
# fragment degrades to pre-fix matching (status quo), never a hard error and
|
|
19
|
+
# never a new hole. The required trailing `[[:space:]]+` after each atom is the
|
|
20
|
+
# right-edge word boundary: `rtk-foo`, `artk`, `sudox`, `FOO` (no `=`) can never
|
|
21
|
+
# partially consume — the wrapper word must be a whole word between the boundary
|
|
22
|
+
# and the gated command.
|
|
23
|
+
#
|
|
24
|
+
# Restatement pin: the rtk atom below restates exec-subcommand and global-flag
|
|
25
|
+
# rows of lib/ext-filter-grammar.tsv (the argv[0]-unwrap class's canonical
|
|
26
|
+
# grammar, #10230); test/rtk-grammar-parity.test.sh asserts set equality by
|
|
27
|
+
# extraction — edit the TSV and this atom together. The atom admits ONLY inert
|
|
28
|
+
# grammar words (counted-verbose globals, --skip-env, the six exec subcommands);
|
|
29
|
+
# a string-payload flag (-c|--command|--shell) is NOT an atom, so
|
|
30
|
+
# `rtk run -c 'gh pr merge'` stops the prefix at `run` and cannot match — the
|
|
31
|
+
# opaque-payload class joins the pre-existing `bash -c '…'` lexical limit every
|
|
32
|
+
# boundary gate already declares (#10248 CTO ruling, decision-challenges row 2).
|
|
33
|
+
|
|
34
|
+
VERIFICATION_ENV_ASN_RE='[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]+'
|
|
35
|
+
VERIFICATION_LAUNCHER_RE='(sudo|command|nice|env|xargs)'
|
|
36
|
+
# Globals: -v/-vv/-vvv/--verbose (counted) and --skip-env (takes an optional
|
|
37
|
+
# =value — the argv[0]-unwrap SUT accepts both spellings). Exec subcommands:
|
|
38
|
+
# run|err|test|proxy|bypass|summary (canonical TSV, class=exec). `--` is the
|
|
39
|
+
# clap end-of-options marker — inert anywhere in the wrapper span, so
|
|
40
|
+
# `rtk -- run gh` and `rtk run -- gh` both execute `gh` upstream and both
|
|
41
|
+
# must reach the gated literal here (canonical TSV flag-global row).
|
|
42
|
+
VERIFICATION_RTK_RE='rtk([[:space:]]+(-v|-vv|-vvv|--verbose|--skip-env(=[^[:space:]]+)?|--|run|err|test|proxy|bypass|summary))*'
|
|
43
|
+
VERIFICATION_BOUNDARY_PFX="(${VERIFICATION_ENV_ASN_RE}|${VERIFICATION_LAUNCHER_RE}|${VERIFICATION_RTK_RE})"
|
|
44
|
+
|
|
45
|
+
# Load sentinel — suites assert this, never `[[ -n "$VERIFICATION_BOUNDARY_PFX" ]]`,
|
|
46
|
+
# which an inherited environment value would satisfy even after a failed source.
|
|
47
|
+
VERIFICATION_BOUNDARY_PFX_LOADED=1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# verification-scaffold guard manifest (#10404) — the machine form of guardrail-audit.md.
|
|
2
|
+
# One row per installable guard. Columns:
|
|
3
|
+
# name guard id the CLI accepts (--guards/--uninstall/--list)
|
|
4
|
+
# verdict audit verdict (references/guardrail-audit.md) — every v1 row is
|
|
5
|
+
# generic-after-extraction by construction
|
|
6
|
+
# surfaces hook | ci | hook+ci — which emitted surfaces the guard owns
|
|
7
|
+
# templates `;`-separated template paths under references/ this row emits
|
|
8
|
+
# default_mode advisory — every v1 guard installs advisory-first (flip MODE= / drop
|
|
9
|
+
# continue-on-error to promote; emitted VERIFICATION.md names the step)
|
|
10
|
+
# deps external deps the emitted guard needs at the target (NOT satisfied by
|
|
11
|
+
# the emit; checked and named, never silently assumed)
|
|
12
|
+
# source_files `;`-separated repo sources the templates were extracted from (provenance;
|
|
13
|
+
# the near-verbatim pair is byte-pinned by test/manifest.test.sh)
|
|
14
|
+
# emits `;`-separated target-relative artifact paths this guard emits
|
|
15
|
+
# tier v1 | v2 — v2 rows are candidates only; the generator refuses to emit them
|
|
16
|
+
name verdict surfaces templates default_mode deps source_files emits tier
|
|
17
|
+
secret-scan generic-after-extraction hook+ci references/guards/secret-scan-hook.sh.template;references/gitleaks.toml.template;references/guards/verification-gates.yml.template advisory gitleaks 8.24.x on PATH (hook) or gitleaks-action (CI);jq or node for the settings merge .claude/hooks/git-commit-secret-scan.sh .claude/hooks/secret-scan.sh;.gitleaks.toml;.github/workflows/verification-gates.yml v1
|
|
18
|
+
commit-on-main generic-after-extraction hook references/guards/commit-on-main-hook.sh.template;references/guards/precommit-guard.sh.template;references/guards/wrapper-boundary.sh.template advisory git plugins/soleur/scripts/precommit-guard.sh;plugins/soleur/scripts/lib/wrapper-boundary.sh;.claude/hooks/guardrails.sh .claude/hooks/commit-on-main.sh;scripts/verification/precommit-guard.sh;scripts/verification/lib/wrapper-boundary.sh v1
|
|
19
|
+
block-stash generic-after-extraction hook references/guards/block-stash-hook.sh.template advisory git .claude/hooks/guardrails.sh .claude/hooks/block-stash.sh v1
|
|
20
|
+
orphan-test generic-after-extraction ci references/guards/orphan-test-check.sh.template;references/guards/verification-gates.yml.template advisory git scripts/lint-orphan-test-suites.sh scripts/verification/orphan-test-check.sh;.github/workflows/verification-gates.yml;.github/verification-known-orphans.txt v1
|
|
21
|
+
block-rm-rf generic-after-extraction hook - advisory git .claude/hooks/guardrails.sh .claude/hooks/block-rm-rf.sh v2
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!-- verification-scaffold:readme:start -->
|
|
2
|
+
## Verification gates (installed by soleur:verification-scaffold)
|
|
3
|
+
|
|
4
|
+
Installed mode: **@MODE@** · installed guards: **@GUARD_LIST@** · generated @DATE@.
|
|
5
|
+
|
|
6
|
+
| Guard | Artifacts | Decides |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
@GUARD_TABLE@
|
|
9
|
+
|
|
10
|
+
### What advisory means
|
|
11
|
+
|
|
12
|
+
Every guard was installed **advisory-first**: hooks warn (or ask, on a real
|
|
13
|
+
secret-scan finding) instead of denying, and each CI job carries
|
|
14
|
+
`continue-on-error: true`, so nothing blocks a commit or a PR yet. Advisory is
|
|
15
|
+
a decision-verb difference only — the detection predicates are identical to
|
|
16
|
+
enforcement mode.
|
|
17
|
+
|
|
18
|
+
### Promoting a guard to enforcement (one line per surface)
|
|
19
|
+
|
|
20
|
+
Promote only after **at least one observed clean advisory run** — a gate that
|
|
21
|
+
has never run green is a wall, not a floor.
|
|
22
|
+
|
|
23
|
+
- Hooks (`.claude/hooks/<guard>.sh`): change `MODE="advisory"` to `MODE="enforce"`.
|
|
24
|
+
- CI (`.github/workflows/verification-gates.yml`): set the job's
|
|
25
|
+
`continue-on-error` to `false` (or remove the line), then add the check to
|
|
26
|
+
the branch's required status checks. Note `commit-on-main`/`block-stash` are
|
|
27
|
+
hook-only — branch protection on the remote is the enforcement floor for the
|
|
28
|
+
*default branch itself*.
|
|
29
|
+
- Re-running the scaffold NEVER overwrites your edits: re-install refuses on
|
|
30
|
+
any existing artifact (there is no `--force`).
|
|
31
|
+
|
|
32
|
+
### Uninstall
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bash <plugin>/skills/verification-scaffold/scripts/verification-scaffold.sh --target . --uninstall <guard>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
removes only that guard's emitted files plus its `.claude/settings.json` hook
|
|
39
|
+
entry; shared artifacts (the workflow, `.gitleaks.toml` when another guard
|
|
40
|
+
still needs it) stay — the workflow is only edited when this guard's job block
|
|
41
|
+
is actually present. `--uninstall` on a never-installed guard is a no-op that
|
|
42
|
+
lists what is installed, and once no guard artifact remains this marker block
|
|
43
|
+
is pruned (a file that held only this block is removed).
|
|
44
|
+
|
|
45
|
+
### Requirements
|
|
46
|
+
|
|
47
|
+
- bash + POSIX tools (stock macOS included). Emitted hooks resolve `gitleaks`
|
|
48
|
+
via `command -v` at exec time; the CI job uses the SHA-pinned
|
|
49
|
+
`gitleaks/gitleaks-action`.
|
|
50
|
+
- The secret-scan hook needs `gitleaks` on PATH to run locally (8.24.x
|
|
51
|
+
recommended); without it the hook warns and allows (advisory) — the CI job
|
|
52
|
+
still scans every PR.
|
|
53
|
+
- `.claude/settings.json` hook entries serve Claude-Code-compatible agent
|
|
54
|
+
sessions; the workflow is the floor for everyone else.
|
|
55
|
+
<!-- verification-scaffold:readme:end -->
|