bigpowers 2.86.1 → 2.86.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.
Files changed (59) hide show
  1. package/.agents/skills/security-review/SKILL.md +3 -1
  2. package/.agents/skills/smoke-test/SKILL.md +1 -1
  3. package/.agents/skills/wire-ci/SKILL.md +78 -59
  4. package/.codebuddy/skills/security-review/SKILL.md +3 -1
  5. package/.codebuddy/skills/smoke-test/SKILL.md +1 -1
  6. package/.codebuddy/skills/wire-ci/SKILL.md +78 -59
  7. package/.codex/skills/security-review/SKILL.md +3 -1
  8. package/.codex/skills/smoke-test/SKILL.md +1 -1
  9. package/.codex/skills/wire-ci/SKILL.md +78 -59
  10. package/.hermes/skills/security-review/SKILL.md +3 -1
  11. package/.hermes/skills/smoke-test/SKILL.md +1 -1
  12. package/.hermes/skills/wire-ci/SKILL.md +78 -59
  13. package/.kilocode/rules/security-review.md +3 -1
  14. package/.kilocode/rules/smoke-test.md +1 -1
  15. package/.kilocode/rules/wire-ci.md +78 -59
  16. package/.mimocode/skills/security-review/SKILL.md +3 -1
  17. package/.mimocode/skills/smoke-test/SKILL.md +1 -1
  18. package/.mimocode/skills/wire-ci/SKILL.md +78 -59
  19. package/.pi/package.json +1 -1
  20. package/.pi/prompts/security-review.md +3 -1
  21. package/.pi/prompts/smoke-test.md +1 -1
  22. package/.pi/prompts/wire-ci.md +78 -59
  23. package/.pi/skills/security-review/SKILL.md +3 -1
  24. package/.pi/skills/smoke-test/SKILL.md +1 -1
  25. package/.pi/skills/wire-ci/SKILL.md +78 -59
  26. package/.trae/skills/security-review/SKILL.md +3 -1
  27. package/.trae/skills/smoke-test/SKILL.md +1 -1
  28. package/.trae/skills/wire-ci/SKILL.md +78 -59
  29. package/.windsurf/rules/security-review.md +3 -1
  30. package/.windsurf/rules/smoke-test.md +1 -1
  31. package/.windsurf/rules/wire-ci.md +78 -59
  32. package/.zcode/skills/security-review/SKILL.md +3 -1
  33. package/.zcode/skills/smoke-test/SKILL.md +1 -1
  34. package/.zcode/skills/wire-ci/SKILL.md +78 -59
  35. package/CHANGELOG.md +21 -0
  36. package/SKILL-INDEX.md +1 -1
  37. package/package.json +1 -1
  38. package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
  39. package/scripts/lib/__pycache__/srp_engine_links.cpython-312.pyc +0 -0
  40. package/scripts/lib/context-wire.sh +9 -0
  41. package/scripts/lib/detect-forge.sh +105 -0
  42. package/scripts/lib/extract-story-verify.py +102 -0
  43. package/scripts/lib/fail-open-detect.sh +67 -0
  44. package/scripts/lib/golden-suite-gates.sh +4 -0
  45. package/scripts/lib/plan-consistency-check.sh +26 -2
  46. package/scripts/lib/test-assertions.sh +11 -1
  47. package/scripts/run-skill-verify.sh +5 -46
  48. package/scripts/run-story-verify.sh +208 -0
  49. package/scripts/test-adapters.sh +14 -8
  50. package/scripts/verify-cwe-fixture-sync.sh +88 -0
  51. package/scripts/verify-install.sh +13 -1
  52. package/scripts/wire-ci.sh +159 -0
  53. package/skills/security-review/SKILL.md +3 -1
  54. package/skills/security-review/fixtures/CWE-639-idor-negative.go +48 -0
  55. package/skills/security-review/fixtures/CWE-639-idor-positive.go +29 -0
  56. package/skills/smoke-test/SKILL.md +1 -1
  57. package/skills/wire-ci/REFERENCE.md +15 -4
  58. package/skills/wire-ci/SKILL.md +63 -55
  59. package/skills-lock.json +4 -4
@@ -25,47 +25,11 @@ SKILLS_ROOT="$REPO_ROOT"
25
25
  PASS=0; FAIL=0; SKIP=0
26
26
  TARGET="${1:-}"
27
27
 
28
- # A directive is fail-open when it cannot exit non-zero on a broken repo.
29
- # Two families, both checked syntactically because a pipeline's exit status
30
- # is the status of its LAST command:
31
- # 1. explicit swallow — `|| echo ...`, `|| true`, `|| :`, `; true`, `; exit 0`
32
- # 2. non-asserting tail — pipeline ends in a filter that always exits 0
33
- # (head/wc/cat/tee/sort/tr/sed/awk/echo/true/:). `grep x | wc -l` is the
34
- # canonical offender: wc always succeeds, so the grep result is discarded.
35
- # Use an assertion instead, e.g. [ "$(... | wc -l)" -gt 0 ].
36
- FAIL_OPEN_TAIL_CMDS='head|wc|cat|tee|sort|tr|sed|awk|echo|true|:'
37
-
38
- is_fail_open_directive() {
39
- local cmd="$1"
40
-
41
- # Family 1: explicit exit-status swallowing.
42
- if echo "$cmd" | grep -qE '\|\|[[:space:]]*(echo|true|:)|;[[:space:]]*(true|:|exit[[:space:]]+0)[[:space:]]*$'; then
43
- return 0
44
- fi
45
-
46
- # Family 2: pipeline whose last stage never fails.
47
- # Strip $(...) first — a pipe inside a command substitution feeds an
48
- # assertion, e.g. [ "$(ls x | wc -l)" -gt 0 ], and is NOT fail-open.
49
- local stripped="$cmd" prev=""
50
- while [ "$stripped" != "$prev" ]; do
51
- prev="$stripped"
52
- stripped=$(printf '%s' "$stripped" | sed 's/\$([^()]*)/SUBST/g')
53
- done
54
-
55
- # Only inspect the segment after the final `|` that is not part of `||`.
56
- local tail_seg
57
- tail_seg=$(printf '%s' "$stripped" | sed 's/||/\
58
- /g' | tail -1)
59
- case "$tail_seg" in
60
- *\|*)
61
- tail_seg=${tail_seg##*|}
62
- tail_seg=$(printf '%s' "$tail_seg" | sed 's/^[[:space:]]*//')
63
- echo "$tail_seg" | grep -qE "^($FAIL_OPEN_TAIL_CMDS)([[:space:]]|$)" && return 0
64
- ;;
65
- esac
66
-
67
- return 1
68
- }
28
+ # is_fail_open_directive / is_executable_verify now live in the shared detector,
29
+ # so tier-1 (here) and tier-2 (run-story-verify.sh, #106) reject the same
30
+ # idioms. A new evasion must be closed in one place, not two.
31
+ # shellcheck source=lib/fail-open-detect.sh
32
+ source "$REPO_ROOT/scripts/lib/fail-open-detect.sh"
69
33
 
70
34
  normalize_verify_cmd() {
71
35
  local raw="$1"
@@ -74,11 +38,6 @@ normalize_verify_cmd() {
74
38
  echo "$raw"
75
39
  }
76
40
 
77
- is_executable_verify() {
78
- local cmd="$1"
79
- echo "$cmd" | grep -qE '^[a-zA-Z][a-zA-Z0-9_.-]*\b|^\['
80
- }
81
-
82
41
  run_verify_cmd() {
83
42
  local skill="$1"
84
43
  local cmd="$2"
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env bash
2
+ # story: BUG-2026-07-26-story-verify-never-executed
3
+ # Execute story-level verify: commands declared in specs/epics/*/epic.yaml.
4
+ # Tier 2 of the verify arc (GH #106). Tier 1 is run-skill-verify.sh.
5
+ #
6
+ # Only stories marked `status: done` are gated: a done story asserts its own
7
+ # acceptance criteria hold. Backlog/in-progress stories are reported as SKIP.
8
+ #
9
+ # Usage:
10
+ # bash scripts/run-story-verify.sh # gate all done stories
11
+ # bash scripts/run-story-verify.sh e80s05 # one story
12
+ # bash scripts/run-story-verify.sh --self-test # prove the gate can fail
13
+
14
+ set -uo pipefail
15
+
16
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
17
+ cd "$REPO_ROOT"
18
+
19
+ # shellcheck source=lib/fail-open-detect.sh
20
+ source "$REPO_ROOT/scripts/lib/fail-open-detect.sh"
21
+ # shellcheck source=lib/python-env.sh
22
+ source "$REPO_ROOT/scripts/lib/python-env.sh"
23
+ resolve_python || exit 1
24
+
25
+ VERIFY_TIMEOUT_SECONDS=120
26
+
27
+ # Portable timeout: macOS lacks GNU timeout; use perl alarm as fallback.
28
+ # stdin is closed for every verify command: gate_all reads its record list on
29
+ # fd 0, and a child that reads stdin (any test-*.sh sourcing a read loop) would
30
+ # otherwise swallow the remaining records and silently shrink the gate.
31
+ if command -v timeout >/dev/null 2>&1; then
32
+ run_with_timeout() { timeout "$VERIFY_TIMEOUT_SECONDS" bash -c "$1" </dev/null 2>&1; }
33
+ elif command -v perl >/dev/null 2>&1; then
34
+ run_with_timeout() { perl -e "alarm $VERIFY_TIMEOUT_SECONDS; exec @ARGV" bash -c "$1" </dev/null 2>&1; }
35
+ else
36
+ run_with_timeout() { bash -c "$1" </dev/null 2>&1; }
37
+ fi
38
+
39
+ GATED_STATUS="done"
40
+ PASS=0; FAIL=0; SKIP=0
41
+ TARGET="${1:-}"
42
+
43
+ extract_records() {
44
+ "$PYTHON" "$REPO_ROOT/scripts/lib/extract-story-verify.py" "$REPO_ROOT"
45
+ }
46
+
47
+ # Report a story whose directive can never fail. Kept separate from execution so
48
+ # a fail-open directive is a FAIL even when the command happens to exit 0.
49
+ reject_fail_open() {
50
+ local sid="$1" src="$2" cmd="$3"
51
+ echo "FAIL: $sid [$src] — fail-open directive (cannot exit non-zero): $cmd"
52
+ FAIL=$((FAIL + 1))
53
+ }
54
+
55
+ reject_non_executable() {
56
+ local sid="$1" src="$2" cmd="$3"
57
+ echo "FAIL: $sid [$src] — verify is prose, not a runnable command: $cmd"
58
+ FAIL=$((FAIL + 1))
59
+ }
60
+
61
+ run_story_verify() {
62
+ local sid="$1" src="$2" cmd="$3"
63
+
64
+ if ! is_executable_verify "$cmd"; then
65
+ reject_non_executable "$sid" "$src" "$cmd"
66
+ return 1
67
+ fi
68
+ if is_fail_open_directive "$cmd"; then
69
+ reject_fail_open "$sid" "$src" "$cmd"
70
+ return 1
71
+ fi
72
+
73
+ local output
74
+ if output=$(run_with_timeout "$cmd"); then
75
+ echo "PASS: $sid"
76
+ PASS=$((PASS + 1))
77
+ return 0
78
+ fi
79
+ echo "FAIL: $sid [$src] — $cmd"
80
+ echo " output: $(echo "$output" | tail -3 | tr '\n' ' ')"
81
+ FAIL=$((FAIL + 1))
82
+ return 1
83
+ }
84
+
85
+ gate_all() {
86
+ local sid src status cmd
87
+ while IFS=$'\t' read -r sid src status cmd; do
88
+ [ -z "${sid:-}" ] && continue
89
+ if [ -n "$TARGET" ] && [ "$sid" != "$TARGET" ]; then
90
+ continue
91
+ fi
92
+ if [ -z "$TARGET" ] && [ "$status" != "$GATED_STATUS" ]; then
93
+ echo "SKIP: $sid (status: $status)"
94
+ SKIP=$((SKIP + 1))
95
+ continue
96
+ fi
97
+ run_story_verify "$sid" "$src" "$cmd" || true
98
+ done < <(extract_records)
99
+ }
100
+
101
+ # Anti-vacuity proof: seed a capsule whose done story must fail, and one that
102
+ # must pass, then require the runner to agree. A gate that cannot fail is the
103
+ # defect this script exists to close (#96/#106) — so it must prove otherwise.
104
+ run_self_test() {
105
+ echo "=== run-story-verify self-test ==="
106
+ local tmp_root
107
+ tmp_root=$(mktemp -d)
108
+ trap 'rm -rf "$tmp_root"' EXIT
109
+
110
+ mkdir -p "$tmp_root/specs/epics/e99-selftest"
111
+ cat > "$tmp_root/specs/epics/e99-selftest/epic.yaml" <<'YAML'
112
+ id: e99
113
+ title: self-test capsule
114
+ stories:
115
+ - id: e99s01
116
+ title: passing story
117
+ status: done
118
+ verify: test -d specs
119
+ - id: e99s02
120
+ title: failing story
121
+ status: done
122
+ verify: test -f /nonexistent/path/for/story-verify-self-test
123
+ - id: e99s03
124
+ title: fail-open story
125
+ status: done
126
+ verify: test -f /nonexistent || true
127
+ - id: e99s04
128
+ title: prose story
129
+ status: done
130
+ verify: node bin/bigpowers.js setup (interactive in TTY)
131
+ - id: e99s05
132
+ title: backlog story is not gated
133
+ status: backlog
134
+ verify: test -f /nonexistent/should/not/run
135
+ YAML
136
+
137
+ local records
138
+ records=$("$PYTHON" "$REPO_ROOT/scripts/lib/extract-story-verify.py" "$tmp_root")
139
+
140
+ local checked=0 problems=0
141
+ local sid src status cmd
142
+ while IFS=$'\t' read -r sid src status cmd; do
143
+ [ -z "${sid:-}" ] && continue
144
+ checked=$((checked + 1))
145
+ case "$sid" in
146
+ e99s01)
147
+ if is_fail_open_directive "$cmd" || ! is_executable_verify "$cmd"; then
148
+ echo "FAIL: self-test — genuine assertion rejected: $cmd"; problems=$((problems + 1))
149
+ elif ! run_with_timeout "$cmd" >/dev/null; then
150
+ echo "FAIL: self-test — passing fixture did not pass"; problems=$((problems + 1))
151
+ fi
152
+ ;;
153
+ e99s02)
154
+ if run_with_timeout "$cmd" >/dev/null; then
155
+ echo "FAIL: self-test — failing fixture exited 0 (gate cannot fail)"; problems=$((problems + 1))
156
+ fi
157
+ ;;
158
+ e99s03)
159
+ if ! is_fail_open_directive "$cmd"; then
160
+ echo "FAIL: self-test — fail-open directive not detected: $cmd"; problems=$((problems + 1))
161
+ fi
162
+ ;;
163
+ e99s04)
164
+ if is_executable_verify "$cmd"; then
165
+ echo "FAIL: self-test — prose accepted as a command: $cmd"; problems=$((problems + 1))
166
+ fi
167
+ ;;
168
+ e99s05)
169
+ if [ "$status" = "$GATED_STATUS" ]; then
170
+ echo "FAIL: self-test — backlog story reported as done"; problems=$((problems + 1))
171
+ fi
172
+ ;;
173
+ esac
174
+ done <<EOF
175
+ $records
176
+ EOF
177
+
178
+ rm -rf "$tmp_root"
179
+ trap - EXIT
180
+
181
+ if [ "$checked" -lt 5 ]; then
182
+ echo "FAIL: self-test — extractor found $checked/5 fixture stories"
183
+ return 1
184
+ fi
185
+ if [ "$problems" -gt 0 ]; then
186
+ echo "FAIL: self-test — $problems assertion(s) failed"
187
+ return 1
188
+ fi
189
+ echo "PASS: self-test — gate fails on broken stories, passes on sound ones"
190
+ return 0
191
+ }
192
+
193
+ if [ "$TARGET" = "--self-test" ]; then
194
+ run_self_test
195
+ exit $?
196
+ fi
197
+
198
+ gate_all
199
+
200
+ echo ""
201
+ echo "Results: $PASS PASS, $FAIL FAIL, $SKIP SKIP"
202
+
203
+ if [ -n "$TARGET" ] && [ "$((PASS + FAIL))" -eq 0 ]; then
204
+ echo "ERROR: no story matching '$TARGET' declares a verify: command" >&2
205
+ exit 2
206
+ fi
207
+
208
+ [ "$FAIL" -eq 0 ]
@@ -21,7 +21,6 @@ bash "$SCRIPT_DIR/validate-targets-yaml.sh" >/dev/null
21
21
 
22
22
  PASS=0
23
23
  FAIL=0
24
- TMPDIR=""
25
24
  TA_PASS=0
26
25
  TA_FAIL=0
27
26
  TA_TMPDIR=""
@@ -56,15 +55,22 @@ test_adapter() {
56
55
  if [[ "$has_context" == "true" && "$mode" != "native" ]]; then
57
56
  declare -f wire_context >/dev/null 2>&1 && ta_pass "$id: wire_context defined" || ta_fail "$id: wire_context missing"
58
57
 
59
- TMPDIR=$(mktemp -d)
60
- cp "$SCRIPT_DIR/../docs/templates/AGENTS.md" "$TMPDIR/AGENTS.md" 2>/dev/null || echo "# test" > "$TMPDIR/AGENTS.md"
61
- (
62
- cd "$TMPDIR"
58
+ TA_TMPDIR=$(mktemp -d)
59
+ cp "$SCRIPT_DIR/../docs/templates/AGENTS.md" "$TA_TMPDIR/AGENTS.md" 2>/dev/null || echo "# test" > "$TA_TMPDIR/AGENTS.md"
60
+ # The idempotency probe runs in a subshell (it cd's away), so its ta_pass/
61
+ # ta_fail would increment counters in a child and be lost — the script used
62
+ # to print 5 PASS lines and report 4. Capture the status, tally in-process.
63
+ if (
64
+ cd "$TA_TMPDIR"
63
65
  # shellcheck source=/dev/null
64
66
  source "$adapter_file"
65
- wire_context 2>/dev/null || true
66
- wire_context 2>/dev/null && ta_pass "$id: idempotent wire_context" || ta_fail "$id: wire_context failed"
67
- )
67
+ wire_context >/dev/null 2>&1 || true
68
+ wire_context >/dev/null 2>&1
69
+ ); then
70
+ ta_pass "$id: idempotent wire_context"
71
+ else
72
+ ta_fail "$id: wire_context failed"
73
+ fi
68
74
  fi
69
75
  }
70
76
 
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bash
2
+ # story: e80s05
3
+ # Assert security-review's CWE rule table and its fixtures/ directory agree.
4
+ #
5
+ # e80s05 ("security-review fixture table growth") was marked done while the
6
+ # fixtures directory gained a class the table never listed. Remembering to
7
+ # update both is exactly the kind of discipline that decays, so this makes the
8
+ # drift mechanical instead: every fixture class needs a table row, and every
9
+ # table row needs both fixtures on disk.
10
+ #
11
+ # Usage:
12
+ # bash scripts/verify-cwe-fixture-sync.sh [--self-test]
13
+ set -uo pipefail
14
+
15
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
16
+ SKILL_MD="$REPO_ROOT/skills/security-review/SKILL.md"
17
+ FIXTURES="$REPO_ROOT/skills/security-review/fixtures"
18
+
19
+ fail=0
20
+ note() { echo " $1"; }
21
+
22
+ check_sync() {
23
+ local skill_md="$1" fixtures="$2"
24
+ local rc=0
25
+
26
+ [ -f "$skill_md" ] || { echo "FAIL: missing $skill_md"; return 1; }
27
+ [ -d "$fixtures" ] || { echo "FAIL: missing $fixtures"; return 1; }
28
+
29
+ # Classes present on disk, derived from the -positive fixture filenames.
30
+ local disk_classes
31
+ disk_classes=$(find "$fixtures" -name '*-positive.*' -exec basename {} \; 2>/dev/null \
32
+ | sed 's/-positive\..*$//' | sort -u)
33
+
34
+ # Every disk class must appear in the rule table.
35
+ local c
36
+ for c in $disk_classes; do
37
+ if ! grep -q "$c" "$skill_md"; then
38
+ note "FAIL: fixture class '$c' has no row in the CWE rule table"
39
+ rc=1
40
+ fi
41
+ done
42
+
43
+ # Every fixture referenced by the table must exist, both polarities.
44
+ local ref
45
+ for ref in $(grep -oE 'fixtures/[A-Za-z0-9._-]+' "$skill_md" | sed 's|fixtures/||' | sort -u); do
46
+ if [ ! -f "$fixtures/$ref" ]; then
47
+ note "FAIL: table references fixtures/$ref which does not exist"
48
+ rc=1
49
+ fi
50
+ done
51
+
52
+ for c in $disk_classes; do
53
+ local pos neg
54
+ pos=$(find "$fixtures" -name "$c-positive.*" | head -1)
55
+ neg=$(find "$fixtures" -name "$c-negative.*" | head -1)
56
+ if [ -z "$pos" ] || [ -z "$neg" ]; then
57
+ note "FAIL: class '$c' needs BOTH a -positive and a -negative fixture"
58
+ rc=1
59
+ fi
60
+ done
61
+
62
+ return $rc
63
+ }
64
+
65
+ # --self-test proves this gate can fail: point it at a fixtures dir holding a
66
+ # class the table does not list, and require a non-zero exit.
67
+ if [ "${1:-}" = "--self-test" ]; then
68
+ tmp=$(mktemp -d)
69
+ trap 'rm -rf "$tmp"' EXIT
70
+ mkdir -p "$tmp/fixtures"
71
+ printf '| Rule | CWE | Positive fixture | Negative fixture |\n' > "$tmp/SKILL.md"
72
+ : > "$tmp/fixtures/CWE-999-unlisted-positive.py"
73
+ : > "$tmp/fixtures/CWE-999-unlisted-negative.py"
74
+
75
+ if check_sync "$tmp/SKILL.md" "$tmp/fixtures" >/dev/null 2>&1; then
76
+ echo "FAIL: self-test — gate passed on an unlisted fixture class (it cannot fail)"
77
+ exit 1
78
+ fi
79
+ echo "PASS: self-test — gate rejects a fixture class missing from the table"
80
+ exit 0
81
+ fi
82
+
83
+ if check_sync "$SKILL_MD" "$FIXTURES"; then
84
+ echo "PASS: CWE rule table and fixtures/ are in sync"
85
+ exit 0
86
+ fi
87
+ echo "FAIL: security-review CWE table and fixtures/ have drifted"
88
+ exit 1
@@ -168,7 +168,19 @@ grep -q "'antigravity'" "$REPO_ROOT/bin/setup.js" && grep -q 'SUPPORTED_IDS' "$R
168
168
  grep -q "'agy'" "$REPO_ROOT/bin/setup.js" && ta_pass "setup.js: agy supported" || ta_fail "setup.js: agy not in SUPPORTED_IDS"
169
169
  grep -q "case 'antigravity'" "$REPO_ROOT/scripts/lib/install-helpers.js" && ta_pass "install-helpers: antigravity case" || ta_fail "install-helpers: missing antigravity case"
170
170
  grep -q "case 'agy'" "$REPO_ROOT/scripts/lib/install-helpers.js" && ta_pass "install-helpers: agy case" || ta_fail "install-helpers: missing agy case"
171
- ! install_grep -q '\.gemini/extensions/bigpowers' <<< "$(sed -n '/install_agy/,/^}/p')" && ta_pass "source: agy avoids gemini extension path" || ta_fail "source: agy touches gemini extension path"
171
+ # Scope this to install_agy's OWN body. The previous form piped a herestring into
172
+ # install_grep(), which is `grep "$@" $(install_grep_paths)` — it always greps the
173
+ # install files and ignores stdin, so the assertion actually searched every target.
174
+ # install_gemini/install_claude legitimately use .gemini/extensions/bigpowers
175
+ # (install-targets-a.sh:21,22,70), so the check could never pass. (#106)
176
+ AGY_BODY="$(sed -n '/^install_agy()/,/^}/p' $(install_grep_paths) 2>/dev/null)"
177
+ if [[ -z "$AGY_BODY" ]]; then
178
+ ta_fail "source: install_agy() body not found — assertion would pass vacuously"
179
+ elif printf '%s' "$AGY_BODY" | grep -q '\.gemini/extensions/bigpowers'; then
180
+ ta_fail "source: agy touches gemini extension path"
181
+ else
182
+ ta_pass "source: agy avoids gemini extension path"
183
+ fi
172
184
 
173
185
  install_grep -q 'install_codex()' && ta_pass "source: install_codex()" || ta_fail "source: missing install_codex()"
174
186
  install_grep -q 'uninstall_codex()' && ta_pass "source: uninstall_codex()" || ta_fail "source: missing uninstall_codex()"
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env bash
2
+ # story: GH-104-forge-neutrality
3
+ # wire-ci.sh — resolve forge + stack and place a CI template, or skip honestly.
4
+ #
5
+ # GH #104: wire-ci declared itself a HARD GATE, then generated GitHub Actions
6
+ # config copied from one person's personal repository. On GitLab it could not
7
+ # work at all. This runner makes the template source configurable and turns an
8
+ # unsupported forge into an explicit skip instead of a false guarantee.
9
+ #
10
+ # Usage:
11
+ # bash scripts/wire-ci.sh --detect # report forge + stack, write nothing
12
+ # bash scripts/wire-ci.sh --plan # show what would be written
13
+ # bash scripts/wire-ci.sh --apply # write the workflow
14
+ # bash scripts/wire-ci.sh --self-test # prove skip/apply paths behave
15
+ #
16
+ # Exit codes: 0 applied or cleanly skipped, 1 error, 3 unsupported forge.
17
+
18
+ set -uo pipefail
19
+
20
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
21
+ # shellcheck source=lib/detect-forge.sh
22
+ source "$REPO_ROOT/scripts/lib/detect-forge.sh"
23
+
24
+ EXIT_UNSUPPORTED_FORGE=3
25
+ MODE="${1:---detect}"
26
+ TARGET_DIR="${WIRE_CI_TARGET:-$REPO_ROOT}"
27
+
28
+ detect_stack() {
29
+ local root="$1"
30
+ if [[ -f "$root/Cargo.toml" ]]; then echo "rust"
31
+ elif [[ -f "$root/go.mod" ]]; then echo "go"
32
+ elif [[ -f "$root/pyproject.toml" || -f "$root/setup.py" ]]; then echo "python"
33
+ elif [[ -f "$root/package.json" ]]; then echo "node"
34
+ else echo "unknown"
35
+ fi
36
+ }
37
+
38
+ template_for() {
39
+ local forge="$1" stack="$2"
40
+ printf '%s/%s/test-build-release-%s.yml' "$(forge_template_root)" "$forge" "$stack"
41
+ }
42
+
43
+ # The honest-degradation path. An unsupported forge gets a named reason and a
44
+ # non-zero-but-distinct exit code, never a GitHub workflow it cannot run.
45
+ report_unsupported() {
46
+ local forge="$1"
47
+ echo "wire-ci: SKIP — no CI templates for forge '$forge'"
48
+ echo " Supported: $FORGE_SUPPORTED_LIST"
49
+ echo " wire-ci is NOT a hard gate on this forge; it cannot generate config it does not ship."
50
+ echo " Options:"
51
+ echo " - point at your own templates: BIGPOWERS_CI_TEMPLATES=/path/to/templates"
52
+ echo " - pin the forge explicitly: echo 'forge: github' > specs/forge.yaml"
53
+ echo " - write CI by hand for $(forge_ci_path "$forge" || true)"
54
+ }
55
+
56
+ resolve_and_report() {
57
+ local root="$1"
58
+ FORGE=$(detect_forge "$root")
59
+ STACK=$(detect_stack "$root")
60
+ echo "wire-ci: forge=$FORGE stack=$STACK"
61
+
62
+ if ! forge_is_supported "$FORGE"; then
63
+ report_unsupported "$FORGE"
64
+ return "$EXIT_UNSUPPORTED_FORGE"
65
+ fi
66
+ if [[ "$STACK" == "unknown" ]]; then
67
+ echo "wire-ci: SKIP — no recognized manifest (Cargo.toml, go.mod, pyproject.toml, package.json)"
68
+ return "$EXIT_UNSUPPORTED_FORGE"
69
+ fi
70
+
71
+ TEMPLATE=$(template_for "$FORGE" "$STACK")
72
+ if [[ ! -f "$TEMPLATE" ]]; then
73
+ echo "wire-ci: SKIP — no bundled template at $TEMPLATE"
74
+ return "$EXIT_UNSUPPORTED_FORGE"
75
+ fi
76
+ echo "wire-ci: template=$TEMPLATE"
77
+ return 0
78
+ }
79
+
80
+ apply_template() {
81
+ local root="$1"
82
+ local dest_dir="$root/$(forge_ci_path "$FORGE")"
83
+ mkdir -p "$dest_dir"
84
+ cp "$TEMPLATE" "$dest_dir/test-build-release.yml"
85
+ echo "wire-ci: wrote $dest_dir/test-build-release.yml"
86
+ }
87
+
88
+ run_self_test() {
89
+ echo "=== wire-ci self-test ==="
90
+ local tmp problems=0
91
+ tmp=$(mktemp -d)
92
+ trap 'rm -rf "$tmp"' EXIT
93
+
94
+ # An unsupported forge must skip, not emit GitHub Actions config.
95
+ mkdir -p "$tmp/gitlab-project"
96
+ echo '{}' > "$tmp/gitlab-project/package.json"
97
+ echo 'forge: gitlab' > /dev/null
98
+ mkdir -p "$tmp/gitlab-project/specs"
99
+ printf 'forge: gitlab\n' > "$tmp/gitlab-project/specs/forge.yaml"
100
+ local out rc
101
+ out=$(BIGPOWERS_FORGE="" resolve_and_report "$tmp/gitlab-project" 2>&1); rc=$?
102
+ if [[ "$rc" -ne "$EXIT_UNSUPPORTED_FORGE" ]]; then
103
+ echo "FAIL: self-test — gitlab project did not skip (rc=$rc)"; problems=$((problems + 1))
104
+ fi
105
+ if printf '%s' "$out" | grep -q 'HARD GATE'; then
106
+ echo "FAIL: self-test — skip path still claims HARD GATE"; problems=$((problems + 1))
107
+ fi
108
+ if [[ -d "$tmp/gitlab-project/.github" ]]; then
109
+ echo "FAIL: self-test — wrote .github/ for a non-GitHub forge"; problems=$((problems + 1))
110
+ fi
111
+
112
+ # A supported forge must resolve to a bundled template — proving the runner
113
+ # does not depend on any external repository being reachable.
114
+ mkdir -p "$tmp/gh-project/specs"
115
+ echo '{}' > "$tmp/gh-project/package.json"
116
+ printf 'forge: github\n' > "$tmp/gh-project/specs/forge.yaml"
117
+ if ! out=$(resolve_and_report "$tmp/gh-project" 2>&1); then
118
+ echo "FAIL: self-test — github/node project did not resolve: $out"; problems=$((problems + 1))
119
+ elif ! printf '%s' "$out" | grep -q 'test-build-release-node.yml'; then
120
+ echo "FAIL: self-test — resolved the wrong template: $out"; problems=$((problems + 1))
121
+ fi
122
+
123
+ # The default template root must live inside the package, not in a user's home
124
+ # and not in the project being wired. Guards the #104 regression directly.
125
+ local root
126
+ root=$(forge_template_root)
127
+ case "$root" in
128
+ "$REPO_ROOT"/*) : ;;
129
+ *) echo "FAIL: self-test — default template root escapes the package: $root"; problems=$((problems + 1)) ;;
130
+ esac
131
+ if printf '%s' "$root" | grep -q 'danielvm-git\|~/Developer/.github'; then
132
+ echo "FAIL: self-test — template root still points at a personal repository"; problems=$((problems + 1))
133
+ fi
134
+
135
+ rm -rf "$tmp"
136
+ trap - EXIT
137
+
138
+ if [[ "$problems" -gt 0 ]]; then
139
+ echo "FAIL: wire-ci self-test — $problems problem(s)"
140
+ return 1
141
+ fi
142
+ echo "PASS: wire-ci self-test — unsupported forges skip, supported forges resolve bundled templates"
143
+ return 0
144
+ }
145
+
146
+ case "$MODE" in
147
+ --self-test) run_self_test; exit $? ;;
148
+ --detect|--plan)
149
+ resolve_and_report "$TARGET_DIR"; exit $?
150
+ ;;
151
+ --apply)
152
+ resolve_and_report "$TARGET_DIR" || exit $?
153
+ apply_template "$TARGET_DIR"
154
+ ;;
155
+ *)
156
+ echo "wire-ci: unknown mode '$MODE' (expected --detect, --plan, --apply, --self-test)" >&2
157
+ exit 1
158
+ ;;
159
+ esac
@@ -64,6 +64,8 @@ Every **new detection rule** added to this skill MUST:
64
64
  |------|-----|------------------|------------------|
65
65
  | SQL injection | CWE-89 | `fixtures/CWE-89-sqli-positive.py` | `fixtures/CWE-89-sqli-negative.py` |
66
66
  | XSS (DOM) | CWE-79 | `fixtures/CWE-79-xss-positive.js` | `fixtures/CWE-79-xss-negative.js` |
67
+ | Missing tenant scoping (IDOR) | CWE-639 | `fixtures/CWE-639-idor-positive.go` | `fixtures/CWE-639-idor-negative.go` |
68
+ | Fail-open verify directive | CWE-754 | `fixtures/CWE-fail-open-verify-positive.sh` | `fixtures/CWE-fail-open-verify-negative.sh` |
67
69
 
68
70
  Before merging a new category, run both fixtures through the detection guidance and confirm positive flags / negative passes.
69
71
 
@@ -115,4 +117,4 @@ Each finding: **`File:Line` — Severity — Category**
115
117
 
116
118
  ## Verify
117
119
 
118
- → verify: `test -d specs/security && test -f scripts/lib/parallel-review-worktrees.sh && git rev-parse HEAD >/dev/null 2>&1`
120
+ → verify: `test -d specs/security && test -f scripts/lib/parallel-review-worktrees.sh && bash scripts/verify-cwe-fixture-sync.sh >/dev/null && git rev-parse HEAD >/dev/null 2>&1`
@@ -0,0 +1,48 @@
1
+ // story: e80s05
2
+ // Negative fixture: correctly scoped tenant query (must NOT be flagged).
3
+ //
4
+ // The org comes from the authenticated context, never from the request, and it
5
+ // is part of the WHERE clause. A caller from another organization gets zero
6
+ // rows rather than someone else's site.
7
+ //
8
+ // Note the retrofit trap this deliberately avoids: bigbase's BUG-2026-07-24T184443
9
+ // added `org_id INTEGER NOT NULL DEFAULT 0`, which passed tests on new rows and
10
+ // hid 8 of 9 production sites whose legacy rows defaulted to org 0. Scope on a
11
+ // real, backfilled org_id — never a placeholder default.
12
+ package fixtures
13
+
14
+ import (
15
+ "database/sql"
16
+ "net/http"
17
+ )
18
+
19
+ func orgIDFromContext(r *http.Request) (string, bool) {
20
+ v, ok := r.Context().Value(ctxOrgID).(string)
21
+ return v, ok && v != ""
22
+ }
23
+
24
+ type ctxKey string
25
+
26
+ const ctxOrgID ctxKey = "org_id"
27
+
28
+ func handleGetSiteNegative(w http.ResponseWriter, r *http.Request, db *sql.DB) {
29
+ orgID, ok := orgIDFromContext(r)
30
+ if !ok {
31
+ http.Error(w, "forbidden", http.StatusForbidden)
32
+ return
33
+ }
34
+ siteID := r.URL.Query().Get("site_id")
35
+
36
+ // SAFE: the tenant predicate is not optional and does not come from input.
37
+ row := db.QueryRow(
38
+ `SELECT id, name, domain FROM sites WHERE id = ? AND org_id = ?`,
39
+ siteID, orgID,
40
+ )
41
+
42
+ var id, name, domain string
43
+ if err := row.Scan(&id, &name, &domain); err != nil {
44
+ http.Error(w, "not found", http.StatusNotFound)
45
+ return
46
+ }
47
+ _, _ = w.Write([]byte(name + " " + domain))
48
+ }
@@ -0,0 +1,29 @@
1
+ // story: e80s05
2
+ // Positive fixture: IDOR / missing tenant scoping (must be flagged).
3
+ //
4
+ // Modelled on the bigbase family (BUG-129/130/133/135/136/140/143): the
5
+ // handler authenticates the caller, then trusts an ID from the request and
6
+ // queries by that ID alone. Authentication is proven; authorization is not.
7
+ // Nothing in the type system or a diff-scoped review makes this look wrong —
8
+ // the bug is a predicate that was never written.
9
+ package fixtures
10
+
11
+ import (
12
+ "database/sql"
13
+ "net/http"
14
+ )
15
+
16
+ func handleGetSitePositive(w http.ResponseWriter, r *http.Request, db *sql.DB) {
17
+ siteID := r.URL.Query().Get("site_id")
18
+
19
+ // VULNERABLE: scoped by primary key only. Any authenticated caller from any
20
+ // organization can read any site by guessing or enumerating site_id.
21
+ row := db.QueryRow(`SELECT id, name, domain FROM sites WHERE id = ?`, siteID)
22
+
23
+ var id, name, domain string
24
+ if err := row.Scan(&id, &name, &domain); err != nil {
25
+ http.Error(w, "not found", http.StatusNotFound)
26
+ return
27
+ }
28
+ _, _ = w.Write([]byte(name + " " + domain))
29
+ }