bigpowers 2.91.0 → 2.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.agents/skills/guard-git/SKILL.md +7 -2
  2. package/.cline/skills/guard-git/SKILL.md +7 -2
  3. package/.codebuddy/skills/guard-git/SKILL.md +7 -2
  4. package/.codex/skills/guard-git/SKILL.md +7 -2
  5. package/.copilot/skills/guard-git/SKILL.md +7 -2
  6. package/.cursor/rules/guard-git.mdc +7 -2
  7. package/.gemini/extensions/bigpowers/commands/prompts/guard-git.md +7 -2
  8. package/.gemini/extensions/bigpowers/gemini-extension.json +1 -1
  9. package/.gemini/extensions/bigpowers/skills/guard-git/SKILL.md +7 -2
  10. package/.hermes/skills/guard-git/SKILL.md +7 -2
  11. package/.kilocode/rules/guard-git.md +7 -2
  12. package/.mimocode/skills/guard-git/SKILL.md +7 -2
  13. package/.opencode/skills/guard-git/SKILL.md +7 -2
  14. package/.pi/package.json +1 -1
  15. package/.pi/prompts/guard-git.md +7 -2
  16. package/.pi/skills/guard-git/SKILL.md +7 -2
  17. package/.qwen/skills/guard-git/SKILL.md +7 -2
  18. package/.trae/skills/guard-git/SKILL.md +7 -2
  19. package/.windsurf/rules/guard-git.md +7 -2
  20. package/.zcode/skills/guard-git/SKILL.md +7 -2
  21. package/CHANGELOG.md +15 -0
  22. package/extensions/omp-hooks.ts +197 -58
  23. package/hooks/pre/bash.sh +54 -7
  24. package/hooks/pre-tool-use.sh +49 -11
  25. package/package.json +1 -1
  26. package/scripts/ab-sandbox.sh +3 -3
  27. package/scripts/adapters/agy.sh +1 -1
  28. package/scripts/adapters/cursor.sh +1 -1
  29. package/scripts/adapters/gemini.sh +1 -1
  30. package/scripts/adapters/mimo.sh +1 -1
  31. package/scripts/adapters/pi.sh +1 -1
  32. package/scripts/adapters/zcode.sh +1 -1
  33. package/scripts/generate-context-bundle.sh +2 -1
  34. package/scripts/lib/__pycache__/atomic_write.cpython-310.pyc +0 -0
  35. package/scripts/lib/__pycache__/link_utils.cpython-310.pyc +0 -0
  36. package/scripts/lib/__pycache__/srp_engine_links.cpython-310.pyc +0 -0
  37. package/scripts/lib/__pycache__/srp_engine_okf.cpython-310.pyc +0 -0
  38. package/scripts/lib/ab-eval.py +19 -5
  39. package/scripts/lib/ab_lean.py +116 -0
  40. package/scripts/lib/ab_metrics.py +8 -0
  41. package/scripts/lib/ab_progress.py +3 -1
  42. package/scripts/lib/ab_report.py +15 -4
  43. package/scripts/lib/ab_vsm.py +122 -0
  44. package/scripts/lib/ab_waste.py +173 -0
  45. package/scripts/lib/git-argv.py +168 -0
  46. package/scripts/lib/golden-suite-gates.sh +2 -0
  47. package/scripts/omp-guard-probe.ts +46 -0
  48. package/scripts/test-adapter-render.sh +1 -1
  49. package/scripts/test-guard-git-normalize.sh +189 -0
  50. package/scripts/test-test-lock-hook.sh +7 -7
  51. package/scripts/verify-adversarial.sh +8 -1
  52. package/skills/guard-git/SKILL.md +7 -2
  53. package/skills-lock.json +1 -1
  54. package/tests/test_ab_eval.py +0 -44
  55. package/tests/test_ab_lean.py +81 -0
  56. package/tests/test_ab_score.py +12 -2
  57. package/tests/test_ab_tasks.py +75 -0
  58. package/tests/test_ab_vsm.py +109 -0
  59. package/tests/test_ab_waste.py +106 -0
@@ -76,6 +76,8 @@ GOLDEN_GATES=(
76
76
  "import-boundaries:bash scripts/check-import-boundaries.sh:false"
77
77
  "security-dashboard-render:node tests/security/dashboard-render.cjs:false"
78
78
  "skill-catalog:bash scripts/validate-skill-catalog.sh:false"
79
+ # Guard-git judges git argv, not substrings; shell and TS must agree (#184).
80
+ "guard-git-normalize:bash scripts/test-guard-git-normalize.sh:false"
79
81
  "specs-parse:bash scripts/validate-specs-yaml.sh:false"
80
82
  # The runner itself: failing output is shown, skips are named and reasoned.
81
83
  "golden-gate-runner:bash scripts/test-golden-gate-runner.sh:false"
@@ -0,0 +1,46 @@
1
+ // story: BUG-2026-11-03-guard-git-flag-normalize
2
+ // story: BUG-2026-11-03-guard-normalize-branch-context
3
+ // omp-guard-probe.ts — evaluate shell commands through the extension's guard,
4
+ // so the TypeScript rules can be held to the same fixture table as
5
+ // hooks/pre-tool-use.sh (scripts/test-guard-git-normalize.sh).
6
+ //
7
+ // Usage: echo '[{"command":"git status","branch":"main"}, …]' | node scripts/omp-guard-probe.ts
8
+ // Prints a JSON array of "block" | "allow", one per input row.
9
+ //
10
+ // It calls evaluateBashCall(), the same pure function the tool_call handler
11
+ // calls — destructive argv rules AND the protected-branch/commit rules. Probing
12
+ // only dangerousGitCommand() compared one rule against a whole hook, which is
13
+ // how "git push --force-with-lease origin main" passed here and failed there.
14
+ //
15
+ // The branch context is supplied per row instead of fixed: protected-branch
16
+ // rules answer differently on `main` than on a feature branch, so a hardcoded
17
+ // branch made the table agree in one direction only and the gate went red the
18
+ // moment it ran on `main`. The harness gives the shell hook the same per-row
19
+ // branch (a throwaway repo checked out on it) and strips GIT_BIGPOWERS_LAND, so
20
+ // neither verdict can depend on the machine or the checked-out branch.
21
+
22
+ import { readFileSync } from "node:fs";
23
+ import { evaluateBashCall } from "../extensions/omp-hooks.ts";
24
+
25
+ const source = readFileSync(new URL("../extensions/omp-hooks.ts", import.meta.url), "utf8");
26
+ const handlerStart = source.indexOf('pi.on("tool_call"');
27
+ if (handlerStart < 0) throw new Error("tool_call handler not found in extensions/omp-hooks.ts");
28
+ if (!source.slice(handlerStart).includes("evaluateBashCall(")) {
29
+ throw new Error("tool_call handler does not call evaluateBashCall() — the guard is not wired");
30
+ }
31
+
32
+ const DEFAULT_BRANCH = "fix/guard-git-normalize";
33
+ type Row = { command: string; branch?: string };
34
+
35
+ const input = JSON.parse(readFileSync(0, "utf8")) as Array<Row | string>;
36
+ const rows: Row[] = input.map((row) => (typeof row === "string" ? { command: row } : row));
37
+ const verdicts = rows.map((row) =>
38
+ evaluateBashCall(row.command, {
39
+ landMode: false,
40
+ hasRepo: true,
41
+ branchFor: () => row.branch ?? DEFAULT_BRANCH,
42
+ })
43
+ ? "block"
44
+ : "allow",
45
+ );
46
+ process.stdout.write(`${JSON.stringify(verdicts)}\n`);
@@ -60,7 +60,7 @@ for adapter in scripts/adapters/*.sh; do
60
60
  IR_DESC_ESCAPED="test desc"; IR_MODEL=""
61
61
  declare -f render_skill >/dev/null || exit 1
62
62
  render_skill
63
- ' _ "$PWD/$adapter" "$nm" >/dev/null 2>&1 )
63
+ ' _ "$PWD/$adapter" "$nm" </dev/null >/dev/null 2>&1 )
64
64
  }
65
65
 
66
66
  dest="$TMP_ROOT/$id"
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env bash
2
+ # story: BUG-2026-11-03-guard-git-flag-normalize
3
+ # test-guard-git-normalize.sh — the guard must judge git ARGV, not substrings (#184).
4
+ #
5
+ # Literal patterns miss `git -C repo <destructive>` and `reset -q --hard`: the
6
+ # destructive tokens survive, only their position changes. One fixture table
7
+ # pins four things:
8
+ #
9
+ # (a) the shell hook (hooks/pre-tool-use.sh) reaches the expected verdict
10
+ # (b) the TS extension reaches the same verdict — through evaluateBashCall(),
11
+ # the function the tool_call handler calls, so protected-branch and commit
12
+ # rules are compared too, not only the destructive-argv table
13
+ # (c) the two agree with each other on every row — that is what "consistent
14
+ # across Claude Code, Cursor and Gemini CLI modes" means
15
+ # (d) the rules that share the hook (commits, protected branch, JSON
16
+ # envelopes, the pi/OMP fallback) still behave
17
+ #
18
+ # Both sides run with GIT_BIGPOWERS_LAND stripped: land mode disables the
19
+ # protected-branch block, so an inherited value would let a developer's shell
20
+ # turn a red gate green (BUG-2026-11-03-guard-normalize-parity-vacuous).
21
+ #
22
+ # Both sides also run in a throwaway repo checked out on the row's `branch:`
23
+ # (default: a feature branch). Protected-branch rules answer differently on
24
+ # `main`, so pinning only the probe to a feature branch made the table agree in
25
+ # one direction and go red as soon as CI ran it on `main`
26
+ # (BUG-2026-11-03-guard-normalize-branch-context).
27
+ #
28
+ # The row loop runs inside one Python process: every row x (hook + probe) with a
29
+ # Python start per row cost ~70s on a pyenv host, which is too much for a gate.
30
+ #
31
+ # Usage: bash scripts/test-guard-git-normalize.sh
32
+
33
+ set -euo pipefail
34
+ source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
35
+ resolve_repo_root
36
+ cd "$REPO_ROOT"
37
+ source scripts/lib/python-env.sh
38
+ source scripts/lib/test-assertions.sh
39
+ resolve_python
40
+ TA_PASS=0
41
+ TA_FAIL=0
42
+ FIXTURE="specs/verifications/fixtures/guard-git-normalize.yaml"
43
+
44
+ echo "=== test-guard-git-normalize.sh ==="
45
+
46
+ if [[ ! -f "$FIXTURE" ]]; then
47
+ ta_fail "fixture missing: $FIXTURE"
48
+ echo "$TA_PASS passed, $TA_FAIL failed"
49
+ exit 1
50
+ fi
51
+
52
+ REPORT=$($PYTHON - "$FIXTURE" <<'PY'
53
+ import json, os, subprocess, sys, tempfile, yaml
54
+
55
+ fixture = sys.argv[1]
56
+ # Ambient LAND mode disables the protected-branch block. A gate that depends on
57
+ # the developer's shell is not a gate (this masked a CI failure once).
58
+ ENV = {k: v for k, v in os.environ.items() if k != "GIT_BIGPOWERS_LAND"}
59
+ DEFAULT_BRANCH = "fix/guard-git-normalize"
60
+ cases = yaml.safe_load(open(fixture, encoding="utf-8"))["cases"]
61
+ rows = [{"command": c["command"], "branch": c.get("branch", DEFAULT_BRANCH)} for c in cases]
62
+
63
+ # (b) one node start for every row.
64
+ probe = subprocess.run(
65
+ ["node", "scripts/omp-guard-probe.ts"],
66
+ input=json.dumps(rows), capture_output=True, text=True,
67
+ )
68
+ if probe.returncode != 0:
69
+ print(f"PROBE_ERROR {probe.stderr.strip().splitlines()[-1][:300] if probe.stderr.strip() else 'no stderr'}")
70
+ sys.exit(0)
71
+ ts = json.loads(probe.stdout.strip().splitlines()[-1])
72
+
73
+
74
+ def make_repo(root, branch):
75
+ """A throwaway repo on `branch`, so the hook resolves that branch as HEAD."""
76
+ path = os.path.join(root, f"repo-{len(os.listdir(root))}")
77
+ def git(*args):
78
+ subprocess.run(["git", "-C", path, *args], check=True, capture_output=True)
79
+ subprocess.run(["git", "init", "-q", "-b", "main", path], check=True, capture_output=True)
80
+ git("config", "user.email", "fixture@example.com")
81
+ git("config", "user.name", "fixture")
82
+ git("commit", "-q", "--allow-empty", "-m", "init")
83
+ if branch != "main":
84
+ git("checkout", "-q", "-b", branch)
85
+ return path
86
+
87
+
88
+ hook = os.path.abspath("hooks/pre-tool-use.sh")
89
+ shell_fails = ts_fails = disagree = 0
90
+ with tempfile.TemporaryDirectory() as tmp:
91
+ repos = {}
92
+ for case, row, ts_verdict in zip(cases, rows, ts):
93
+ branch = row["branch"]
94
+ if branch not in repos:
95
+ repos[branch] = make_repo(tmp, branch)
96
+ payload = json.dumps({"command": row["command"]})
97
+ run = subprocess.run(
98
+ ["bash", hook], input=payload, capture_output=True, text=True,
99
+ env=ENV, cwd=repos[branch],
100
+ )
101
+ shell_verdict = "block" if run.returncode == 2 else "allow"
102
+ expected = case["verdict"]
103
+ if shell_verdict != expected:
104
+ shell_fails += 1
105
+ print(f"SHELL_FAIL expected={expected} got={shell_verdict} :: {row['command']}")
106
+ if ts_verdict != expected:
107
+ ts_fails += 1
108
+ print(f"TS_FAIL expected={expected} got={ts_verdict} :: {row['command']}")
109
+ if shell_verdict != ts_verdict:
110
+ disagree += 1
111
+ print(f"PARITY_FAIL shell={shell_verdict} ts={ts_verdict} :: {row['command']}")
112
+
113
+ print(f"RESULT rows={len(cases)} shell_fails={shell_fails} ts_fails={ts_fails} disagree={disagree}")
114
+ PY
115
+ )
116
+
117
+ if [[ "$REPORT" == PROBE_ERROR* ]]; then
118
+ ta_fail "TS probe failed: ${REPORT#PROBE_ERROR }"
119
+ else
120
+ # sed, not grep: grep exits 1 when there is nothing to show, and pipefail
121
+ # would turn a perfectly green run into a failure.
122
+ printf '%s\n' "$REPORT" | sed -n 's/^\(SHELL_FAIL\|TS_FAIL\|PARITY_FAIL\)/ \1/p'
123
+ read -r _ ROWS SHELL_FAILS TS_FAILS DISAGREE <<< "$(grep '^RESULT' <<< "$REPORT")"
124
+ ROWS=${ROWS#rows=}; SHELL_FAILS=${SHELL_FAILS#shell_fails=}
125
+ TS_FAILS=${TS_FAILS#ts_fails=}; DISAGREE=${DISAGREE#disagree=}
126
+
127
+ [ "${SHELL_FAILS:-1}" -eq 0 ] \
128
+ && ta_pass "(a) shell hook matches all $ROWS fixture rows" \
129
+ || ta_fail "(a) shell hook missed $SHELL_FAILS of $ROWS rows (see above)"
130
+ [ "${TS_FAILS:-1}" -eq 0 ] \
131
+ && ta_pass "(b) TS extension matches all $ROWS fixture rows" \
132
+ || ta_fail "(b) TS extension missed $TS_FAILS of $ROWS rows (see above)"
133
+ [ "${DISAGREE:-1}" -eq 0 ] \
134
+ && ta_pass "(c) shell and TS agree on all $ROWS rows" \
135
+ || ta_fail "(c) shell and TS disagree on $DISAGREE of $ROWS rows (see above)"
136
+ fi
137
+
138
+ # ── (d) the rules that share this hook must keep working ────────────────────
139
+ # The normalizer replaced the pattern loop in the same handler; a regression in
140
+ # the neighbouring checks (protected branches, Conventional Commits, the gemini
141
+ # JSON envelope, the pi/OMP fallback) would be invisible to the rows above.
142
+ #
143
+ # These run in a throwaway repo on a feature branch, like the rows: "a commit is
144
+ # allowed" is only true off `main`, and the gate itself runs on `main` after a
145
+ # merge. Duplicating the repo setup here keeps each block self-contained.
146
+ json_cmd() { $PYTHON -c 'import json,sys;print(json.dumps({"command": sys.argv[1]}))' "$1"; }
147
+ FEATURE_REPO="$($PYTHON - "$(mktemp -d)" <<'PY'
148
+ import subprocess, sys
149
+ path = sys.argv[1]
150
+ def git(*args):
151
+ subprocess.run(["git", "-C", path, *args], check=True, capture_output=True)
152
+ subprocess.run(["git", "init", "-q", "-b", "main", path], check=True, capture_output=True)
153
+ git("config", "user.email", "fixture@example.com")
154
+ git("config", "user.name", "fixture")
155
+ git("commit", "-q", "--allow-empty", "-m", "init")
156
+ git("checkout", "-q", "-b", "fix/guard-git-normalize")
157
+ print(path)
158
+ PY
159
+ )"
160
+ adjacent() { # name want_rc command [hook]
161
+ local name="$1" want="$2" command="$3" hook="${4:-}" rc
162
+ hook="${hook:-$REPO_ROOT/hooks/pre-tool-use.sh}"
163
+ cd "$FEATURE_REPO" || ta_fail "(d) $name: cannot enter fixture repo"
164
+ json_cmd "$command" | env -u GIT_BIGPOWERS_LAND bash "$hook" >/dev/null 2>&1 && rc=0 || rc=$?
165
+ cd "$REPO_ROOT" || true
166
+ [ "$rc" -eq "$want" ] || ta_fail "(d) $name: rc=$rc (want $want) — $command"
167
+ }
168
+ adjacent "plain status allowed" 0 "git status"
169
+ adjacent "conventional commit allowed" 0 'git commit -m "fix(scope): subject"'
170
+ adjacent "feature push allowed" 0 "git push origin feature/x"
171
+ adjacent "non-conventional commit blocked" 2 'git commit -m "bad message"'
172
+ rm -rf "$FEATURE_REPO"
173
+
174
+ DESTRUCTIVE="git reset --hard"
175
+ GEMINI_OUT=$(json_cmd "$DESTRUCTIVE" | env -u GIT_BIGPOWERS_LAND GIT_GUARDRAILS_MODE=gemini bash hooks/pre-tool-use.sh 2>&1)
176
+ echo "$GEMINI_OUT" | grep -q '"decision": *"deny"' \
177
+ || ta_fail "(d) gemini mode must emit deny JSON, got: $(echo "$GEMINI_OUT" | head -c 120)"
178
+
179
+ PI_OUT=$(json_cmd "$DESTRUCTIVE" | env -u GIT_BIGPOWERS_LAND bash hooks/pre/bash.sh 2>&1)
180
+ echo "$PI_OUT" | grep -q '"decision":"block"' \
181
+ || ta_fail "(d) pi/OMP fallback must emit block JSON, got: $(echo "$PI_OUT" | head -c 120)"
182
+
183
+ bash -n hooks/pre/bash.sh || ta_fail "(d) hooks/pre/bash.sh must parse"
184
+ grep -q "classify_git_argv" hooks/pre/bash.sh \
185
+ || ta_fail "(d) pi/OMP fallback must use the normalized classifier"
186
+ ta_pass "(d) adjacent rules: commits, pushes, gemini JSON, pi fallback"
187
+
188
+ echo "$TA_PASS passed, $TA_FAIL failed"
189
+ [[ "$TA_FAIL" -eq 0 ]]
@@ -10,7 +10,7 @@ HOOK="$REPO_ROOT/hooks/pre-tool-use.sh"
10
10
  pass_count=0
11
11
  fail_count=0
12
12
 
13
- assert_denied() {
13
+ lock_assert_denied() {
14
14
  local label="$1"
15
15
  local input="$2"
16
16
  local expected_substr="${3:-}"
@@ -30,7 +30,7 @@ assert_denied() {
30
30
  fi
31
31
  }
32
32
 
33
- assert_allowed() {
33
+ lock_assert_allowed() {
34
34
  local label="$1"
35
35
  local input="$2"
36
36
  local output exit_code=0
@@ -64,7 +64,7 @@ git add .
64
64
  git commit -q -m "feat(app): initial commit"
65
65
 
66
66
  # 1. Without a RED commit yet during fix_bug, test edits should be allowed (to author the RED test)
67
- assert_allowed "Test file edit allowed when authoring RED test" \
67
+ lock_assert_allowed "Test file edit allowed when authoring RED test" \
68
68
  '{"tool": "Edit", "file_path": "test/app.test.js"}'
69
69
 
70
70
  # 2. Commit a RED test
@@ -73,23 +73,23 @@ git add test/app.test.js
73
73
  git commit -q -m "test(app): red test reproducing defect"
74
74
 
75
75
  # 3. Now that a RED commit exists and active_flow is fix_bug, test edits must be LOCKED
76
- assert_denied "Test file edit denied after RED commit in fix_bug" \
76
+ lock_assert_denied "Test file edit denied after RED commit in fix_bug" \
77
77
  '{"tool": "Edit", "file_path": "test/app.test.js"}' \
78
78
  "locked"
79
79
 
80
- assert_denied "Test file write denied after RED commit in fix_bug" \
80
+ lock_assert_denied "Test file write denied after RED commit in fix_bug" \
81
81
  '{"tool": "Write", "file_path": "tests/test_other.py"}' \
82
82
  "locked"
83
83
 
84
84
  # 4. Source file edits must remain allowed
85
- assert_allowed "Source file edit allowed after RED commit in fix_bug" \
85
+ lock_assert_allowed "Source file edit allowed after RED commit in fix_bug" \
86
86
  '{"tool": "Edit", "file_path": "src/app.js"}'
87
87
 
88
88
  # 5. When active_flow is not fix_bug, test edits are allowed
89
89
  cat > specs/state.yaml <<'YAML'
90
90
  active_flow: build_epic
91
91
  YAML
92
- assert_allowed "Test file edit allowed when active_flow is not fix_bug" \
92
+ lock_assert_allowed "Test file edit allowed when active_flow is not fix_bug" \
93
93
  '{"tool": "Edit", "file_path": "test/app.test.js"}'
94
94
 
95
95
  echo ""
@@ -109,9 +109,16 @@ fi
109
109
  DEFECTS_FOUND=0
110
110
  mkdir -p specs/verifications
111
111
 
112
+ # A JS/TS source file that exists and is not a test: the files the boundary scan inspects.
113
+ is_scannable_source() {
114
+ local file="$1"
115
+ [[ "$file" =~ \.(js|ts|mjs)$ && -f "$file" ]] || return 1
116
+ [[ ! "$file" =~ (^|/)test/ && ! "$file" =~ \.test\. ]]
117
+ }
118
+
112
119
  # 2. Check for common unsafe property access patterns on JS/TS files
113
120
  for file in $CHANGED_FILES; do
114
- if [[ "$file" =~ \.(js|ts|mjs)$ ]] && [[ -f "$file" ]] && [[ ! "$file" =~ (^|/)test/ ]] && [[ ! "$file" =~ \.test\. ]]; then
121
+ if is_scannable_source "$file"; then
115
122
  # Look for unsafe chained property access like item.brand.toLowerCase() or item.prop.subprop
116
123
  if grep -nE '\.[a-zA-Z0-9_]+\.(toLowerCase|toUpperCase|trim|length|slice)\(' "$file" | grep -v '\?\.' >/dev/null 2>&1; then
117
124
  echo "WARNING [boundary-mutation]: Potential unhandled undefined access without optional chaining in $file:"
@@ -13,7 +13,8 @@ Installs a shared hook that blocks destructive git operations and enforces workf
13
13
 
14
14
  ## What gets blocked/enforced
15
15
 
16
- - **Safety**: `git push --force`, `git reset --hard`, `git clean -f`, `git branch -D`, `git checkout .`, `git restore .`.
16
+ - **Safety**: force push (`-f`, `--force`, `+refspec`), `reset` with `--hard`, `clean` with any `-f*`, `branch -D` / `--delete --force`, `checkout`/`restore` that discard work (`-f`, `.` pathspec).
17
+ - **Argv, not substrings**: the hook tokenizes each command, strips git's global options (`-C <path>`, `-c k=v`, `--no-pager`, `--git-dir=…`) and judges the `(subcommand, arguments)` pair — so `git -C repo reset -q --hard` is blocked, while `git push --force-with-lease` is allowed. Rules live in `scripts/lib/git-argv.py`, mirrored in `extensions/omp-hooks.ts`; `scripts/test-guard-git-normalize.sh` fails if the two disagree.
17
18
  - **Discipline**: Blocks direct commits or pushes to protected branches (`main`, `master`) unless `GIT_BIGPOWERS_LAND=1` (set only by `scripts/land-branch.sh`).
18
19
  - **Allows**: `git push origin <feature-branch>` for backup/CI; solo land push to `main` only inside `land-branch.sh`.
19
20
  - **Standardization**: Enforces [Conventional Commits](https://www.conventionalcommits.org/) for all `git commit` commands.
@@ -38,7 +39,11 @@ Installs a shared hook that blocks destructive git operations and enforces workf
38
39
 
39
40
  ## Customization
40
41
 
41
- To add or remove patterns or protected branches, edit `pre-tool-use.sh`.
42
+ The rule table lives in `scripts/lib/git-argv.py` (shell hooks) and is mirrored in `extensions/omp-hooks.ts`. Add a rule to both and a row to `specs/verifications/fixtures/guard-git-normalize.yaml`.
43
+
44
+ The TS side is judged through `evaluateBashCall()` — the single pure function the `tool_call` handler calls — so the fixture covers **every** rule (destructive argv, protected-branch push, Conventional Commits), not just the argv table. The gate (`bash scripts/test-guard-git-normalize.sh`) runs both implementations with `GIT_BIGPOWERS_LAND` unset and fails if they disagree, so a verdict can never depend on the developer's shell.
45
+
46
+ Protected-branch rules answer differently on `main` than on a feature branch, so a row that touches commit/push semantics states its `branch:` and both sides are run in it (the shell hook in a throwaway repo checked out on that branch). A row without `branch:` is judged on a feature branch. Do not pin the branch inside either implementation — that is what made the gate pass on PRs and fail on `main`.
42
47
 
43
48
  ## Advanced
44
49
 
package/skills-lock.json CHANGED
@@ -158,7 +158,7 @@
158
158
  },
159
159
  "guard-git": {
160
160
  "description": "Block dangerous git commands (push, force push, reset --hard, clean, branch -D, checkout/restore .) and enforce Conventional Commits & Branch Protection before an AI agent runs them. Installs hook scripts for Claude Code, Cursor, Cursor CLI, and Gemini CLI; documents Google Antigravity Terminal deny lists. Use when the user wants git safety hooks, to block git push or destructive git in agents, or to mirror the same policy across AI coding tools.",
161
- "sha256": "e7083cba6d95a15f",
161
+ "sha256": "08b4b14a1134032e",
162
162
  "path": "skills/guard-git/SKILL.md"
163
163
  },
164
164
  "harden-vps": {
@@ -9,7 +9,6 @@ import importlib.util
9
9
  import io
10
10
  import json
11
11
  import shutil
12
- import subprocess
13
12
  import sys
14
13
  import tempfile
15
14
  import unittest
@@ -249,49 +248,6 @@ class TestCli(Sandbox):
249
248
  self.assertIsNone(report["delta"])
250
249
 
251
250
 
252
- @unittest.skipUnless(shutil.which("node"), "node is needed for the starter tasks")
253
- class TestStarterTasks(unittest.TestCase):
254
- """Each shipped task must fail as given and pass with its reference solution."""
255
-
256
- TASKS = REPO_ROOT / "specs" / "benchmarks" / "ab" / "tasks"
257
-
258
- def grade_copy(self, task_dir: Path, with_solution: bool) -> bool:
259
- task = yaml.safe_load((task_dir / "task.yaml").read_text())
260
- work = Path(tempfile.mkdtemp(prefix="ab-starter-"))
261
- try:
262
- shutil.copytree(task_dir / "fixture", work, dirs_exist_ok=True)
263
- if with_solution:
264
- shutil.copytree(task_dir / "solution", work, dirs_exist_ok=True)
265
- shutil.copytree(task_dir / "hidden", work, dirs_exist_ok=True)
266
- res = subprocess.run(["bash", "-c", task["grade"]], cwd=work, capture_output=True)
267
- return res.returncode == 0
268
- finally:
269
- shutil.rmtree(work, ignore_errors=True)
270
-
271
- def test_eight_tasks_ship(self):
272
- self.assertGreaterEqual(len(list(self.TASKS.glob("*/task.yaml"))), 8)
273
-
274
- def test_fixture_fails_and_solution_passes(self):
275
- for task_yaml in sorted(self.TASKS.glob("*/task.yaml")):
276
- with self.subTest(task=task_yaml.parent.name):
277
- self.assertFalse(self.grade_copy(task_yaml.parent, with_solution=False))
278
- self.assertTrue(self.grade_copy(task_yaml.parent, with_solution=True))
279
-
280
- def test_prompts_never_name_bigpowers(self):
281
- for task_yaml in self.TASKS.glob("*/task.yaml"):
282
- self.assertNotIn("bigpowers", yaml.safe_load(task_yaml.read_text())["prompt"].lower())
283
-
284
- def test_grade_commands_never_pass_a_bare_directory(self):
285
- """`node --test test/` exits 1 on node 24 (it resolves the directory as a module path), so
286
- such a task can never pass however good the solution is, while a hidden-only `score`
287
- command still looks healthy (BUG found in e58s07: ab-11 and ab-13 read pass=0 forever)."""
288
- for task_yaml in self.TASKS.glob("*/task.yaml"):
289
- with self.subTest(task=task_yaml.parent.name):
290
- for token in yaml.safe_load(task_yaml.read_text())["grade"].split():
291
- self.assertNotIn(token.rstrip("/"), ("test", "test-hidden"),
292
- f"{task_yaml.parent.name} grades a directory, not a glob")
293
-
294
-
295
251
  class TestRunBenchmarkHonesty(unittest.TestCase):
296
252
  def test_without_skill_arm_is_unmeasured_not_zero(self):
297
253
  sc = {"id": "s1", "grader": {"type": "code", "command": "true"}}
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ # story: e58s10
3
+ """tests/test_ab_lean.py — lean views: per-arm VSM and waste summary, deltas, bars, redundant tests.
4
+
5
+ Run: python3 tests/test_ab_lean.py
6
+ """
7
+ import sys
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
12
+ from test_ab_eval import load_module, rep # noqa: E402
13
+ from test_ab_waste import Stream, waste # noqa: E402
14
+
15
+ lean = load_module("ab_lean", "ab_lean.py")
16
+
17
+
18
+ def run(arm, lead, build, test, passed=True, cost=0.01, test_runs=3, rework=0):
19
+ stages = {"plan": {"s": lead * 0.1}, "design": {"s": lead * 0.2}, "build": {"s": build}, "test": {"s": test}}
20
+ vsm = {"lead_time_s": lead, "time_to_first_build_s": lead * 0.3, "switches": 4, "stages": stages}
21
+ w = {"test_runs": test_runs, "redundant_test_runs": 1, "rework_loops": rework, "reedits": 0,
22
+ "rereads": 0, "repeated_commands": 0, "review_load": 50, "diff_ratio": 1.2, "ftt": rework == 0,
23
+ "pce": round((build + test) / lead, 3), "leftovers": [], "extra_files": []}
24
+ return {"task": "t", "arm": arm, "pass": passed, "score": None, "tampered": False, "agent_error": False,
25
+ "cost_usd": cost, "input_tokens": None, "output_tokens": None, "turns": 5, "duration_s": lead,
26
+ "diff_lines": 5, "vsm": vsm, "waste": w}
27
+
28
+
29
+ def records():
30
+ return ([run("vanilla", 70, 28, 21) for _ in range(5)]
31
+ + [run("bigpowers", 140, 20, 70, cost=0.02, test_runs=8, rework=1) for _ in range(5)])
32
+
33
+
34
+ class TestLeanSummary(unittest.TestCase):
35
+ def test_per_arm_summary_and_deltas(self):
36
+ summary = lean.lean_summary(records(), ["vanilla", "bigpowers"])
37
+ va, bp = summary["arms"]["vanilla"], summary["arms"]["bigpowers"]
38
+ self.assertEqual(va["lead_time_s"], 70)
39
+ self.assertEqual(round(va["stage_share"]["build"], 2), 0.4)
40
+ self.assertEqual(bp["waste"]["test_runs"], 8)
41
+ self.assertEqual(bp["ftt_rate"], 0.0)
42
+ self.assertEqual(va["cost_per_pass"], 0.01)
43
+ d = summary["deltas"]["bigpowers"]
44
+ self.assertEqual(d["lead_time_s"]["delta"], 70)
45
+ self.assertEqual(d["test_runs"]["delta"], 5)
46
+ self.assertEqual(len(d["build_share"]["ci95"]), 2)
47
+
48
+ def test_old_records_give_none(self):
49
+ old = [{k: v for k, v in r.items() if k not in ("vsm", "waste")} for r in records()]
50
+ self.assertIsNone(lean.lean_summary(old, ["vanilla", "bigpowers"]))
51
+ self.assertEqual(lean.lean_lines(None), [])
52
+
53
+ def test_report_carries_lean(self):
54
+ result = rep.aggregate(records(), ["vanilla", "bigpowers"], ["t"])
55
+ self.assertIn("bigpowers", result["lean"]["deltas"])
56
+
57
+
58
+ class TestViews(unittest.TestCase):
59
+ def test_bars_scale_with_lead_time(self):
60
+ lines = lean.lean_lines(lean.lean_summary(records(), ["vanilla", "bigpowers"]))
61
+ bars = {line.split()[0]: line for line in lines if line.split() and line.split()[0] in ("vanilla", "bigpowers")}
62
+ va_len = bars["vanilla"].count("|") and len(bars["vanilla"].split("|")[1].rstrip())
63
+ bp_len = len(bars["bigpowers"].split("|")[1].rstrip())
64
+ self.assertAlmostEqual(bp_len / va_len, 2, delta=0.2)
65
+ self.assertIn("T", bars["bigpowers"])
66
+ self.assertTrue(any("rework" in line for line in lines))
67
+
68
+
69
+ class TestRedundantTestRuns(unittest.TestCase):
70
+ def test_repeated_green_run_without_edit_is_redundant(self):
71
+ stream = (Stream()
72
+ .call("edit", {"path": "/work/src/a.js"})
73
+ .call("bash", {"command": "npm test"}, "# pass 3\n# fail 0\n")
74
+ .call("bash", {"command": "npm test 2>&1 | tail -3"}, "# pass 3\n# fail 0\n")
75
+ .call("edit", {"path": "/work/src/a.js"})
76
+ .call("bash", {"command": "npm test"}, "# pass 3\n# fail 0\n"))
77
+ self.assertEqual(waste.transcript_waste(stream.text())["redundant_test_runs"], 1)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ unittest.main(verbosity=1)
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- # story: e58s03
2
+ # story: e58s03 e58s11
3
3
  """tests/test_ab_score.py — partial-credit scoring and the hard tasks (ab-11, ab-13).
4
4
 
5
5
  Run: python3 tests/test_ab_score.py
@@ -18,7 +18,17 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
18
  from test_ab_eval import REPO_ROOT, Sandbox, met, rep # noqa: E402
19
19
 
20
20
  TASKS = REPO_ROOT / "specs" / "benchmarks" / "ab" / "tasks"
21
- HARD_TASKS = ["ab-11-ledger-rounding", "ab-13-orders-split"]
21
+ HARD_TASKS = [
22
+ "ab-09-inventory-reservations",
23
+ "ab-11-ledger-rounding",
24
+ "ab-12-todo-due-dates",
25
+ "ab-13-orders-split",
26
+ "ab-15-config-precedence",
27
+ "ab-16-site-tags-rss",
28
+ "ab-17-payment-alias",
29
+ "ab-19-catalog-segregation",
30
+ "ab-20-brand-discount",
31
+ ]
22
32
 
23
33
 
24
34
  class TestTapScore(unittest.TestCase):
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ # story: e58s01 e58s02 e58s07 e58s09
3
+ """tests/test_ab_tasks.py — every shipped A/B task is calibrated: its fixture fails, its reference
4
+ solution passes, its prompt never names bigpowers, and its grade command uses file globs.
5
+
6
+ Run: python3 tests/test_ab_tasks.py
7
+ Split from test_ab_eval.py (300-line cap).
8
+ """
9
+ import json
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ import unittest
15
+ from pathlib import Path
16
+
17
+ import yaml
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
20
+ from test_ab_eval import REPO_ROOT # noqa: E402
21
+
22
+
23
+ @unittest.skipUnless(shutil.which("node"), "node is needed for the starter tasks")
24
+ class TestStarterTasks(unittest.TestCase):
25
+ """Each shipped task must fail as given and pass with its reference solution."""
26
+
27
+ TASKS = REPO_ROOT / "specs" / "benchmarks" / "ab" / "tasks"
28
+
29
+ def grade_copy(self, task_dir: Path, with_solution: bool) -> bool:
30
+ task = yaml.safe_load((task_dir / "task.yaml").read_text())
31
+ work = Path(tempfile.mkdtemp(prefix="ab-starter-"))
32
+ try:
33
+ shutil.copytree(task_dir / "fixture", work, dirs_exist_ok=True)
34
+ if with_solution:
35
+ shutil.copytree(task_dir / "solution", work, dirs_exist_ok=True)
36
+ shutil.copytree(task_dir / "hidden", work, dirs_exist_ok=True)
37
+ res = subprocess.run(["bash", "-c", task["grade"]], cwd=work, capture_output=True)
38
+ return res.returncode == 0
39
+ finally:
40
+ shutil.rmtree(work, ignore_errors=True)
41
+
42
+ def test_eight_tasks_ship(self):
43
+ self.assertGreaterEqual(len(list(self.TASKS.glob("*/task.yaml"))), 8)
44
+
45
+ def test_fixture_fails_and_solution_passes(self):
46
+ for task_yaml in sorted(self.TASKS.glob("*/task.yaml")):
47
+ with self.subTest(task=task_yaml.parent.name):
48
+ self.assertFalse(self.grade_copy(task_yaml.parent, with_solution=False))
49
+ self.assertTrue(self.grade_copy(task_yaml.parent, with_solution=True))
50
+
51
+ def test_prompts_never_name_bigpowers(self):
52
+ for task_yaml in self.TASKS.glob("*/task.yaml"):
53
+ self.assertNotIn("bigpowers", yaml.safe_load(task_yaml.read_text())["prompt"].lower())
54
+
55
+ def test_fixture_test_scripts_never_pass_a_bare_directory(self):
56
+ """`npm test` must work in every fixture: a `node --test test/` script fails on node 24, and
57
+ agents then spend the run repairing the harness (found by e58s09 waste metrics)."""
58
+ for package in self.TASKS.glob("*/fixture/package.json"):
59
+ with self.subTest(task=package.parent.parent.name):
60
+ script = json.loads(package.read_text()).get("scripts", {}).get("test", "")
61
+ self.assertNotRegex(script, r"--test\s+\S*/(\s|$)", script)
62
+
63
+ def test_grade_commands_never_pass_a_bare_directory(self):
64
+ """`node --test test/` exits 1 on node 24 (it resolves the directory as a module path), so
65
+ such a task can never pass however good the solution is, while a hidden-only `score`
66
+ command still looks healthy (BUG found in e58s07: ab-11 and ab-13 read pass=0 forever)."""
67
+ for task_yaml in self.TASKS.glob("*/task.yaml"):
68
+ with self.subTest(task=task_yaml.parent.name):
69
+ for token in yaml.safe_load(task_yaml.read_text())["grade"].split():
70
+ self.assertNotIn(token.rstrip("/"), ("test", "test-hidden"),
71
+ f"{task_yaml.parent.name} grades a directory, not a glob")
72
+
73
+
74
+ if __name__ == "__main__":
75
+ unittest.main(verbosity=1)