bigpowers 2.90.2 → 2.91.1
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/.agents/skills/build-epic/SKILL.md +1 -1
- package/.agents/skills/commit-message/SKILL.md +1 -0
- package/.agents/skills/guard-git/SKILL.md +7 -2
- package/.agents/skills/release-branch/SKILL.md +4 -5
- package/.agents/skills/run-planning/SKILL.md +6 -1
- package/.agents/skills/verify-work/SKILL.md +2 -1
- package/.cline/skills/build-epic/SKILL.md +1 -1
- package/.cline/skills/commit-message/SKILL.md +1 -0
- package/.cline/skills/guard-git/SKILL.md +7 -2
- package/.cline/skills/release-branch/SKILL.md +4 -5
- package/.cline/skills/run-planning/SKILL.md +6 -1
- package/.cline/skills/verify-work/SKILL.md +2 -1
- package/.codebuddy/skills/build-epic/SKILL.md +1 -1
- package/.codebuddy/skills/commit-message/SKILL.md +1 -0
- package/.codebuddy/skills/guard-git/SKILL.md +7 -2
- package/.codebuddy/skills/release-branch/SKILL.md +4 -5
- package/.codebuddy/skills/run-planning/SKILL.md +6 -1
- package/.codebuddy/skills/verify-work/SKILL.md +2 -1
- package/.codex/skills/build-epic/SKILL.md +1 -1
- package/.codex/skills/commit-message/SKILL.md +1 -0
- package/.codex/skills/guard-git/SKILL.md +7 -2
- package/.codex/skills/release-branch/SKILL.md +4 -5
- package/.codex/skills/run-planning/SKILL.md +6 -1
- package/.codex/skills/verify-work/SKILL.md +2 -1
- package/.copilot/skills/build-epic/SKILL.md +1 -1
- package/.copilot/skills/commit-message/SKILL.md +1 -0
- package/.copilot/skills/guard-git/SKILL.md +7 -2
- package/.copilot/skills/release-branch/SKILL.md +4 -5
- package/.copilot/skills/run-planning/SKILL.md +6 -1
- package/.copilot/skills/verify-work/SKILL.md +2 -1
- package/.cursor/rules/build-epic.mdc +1 -1
- package/.cursor/rules/commit-message.mdc +1 -0
- package/.cursor/rules/guard-git.mdc +7 -2
- package/.cursor/rules/release-branch.mdc +4 -5
- package/.cursor/rules/run-planning.mdc +6 -1
- package/.cursor/rules/verify-work.mdc +2 -1
- package/.gemini/extensions/bigpowers/commands/prompts/build-epic.md +1 -1
- package/.gemini/extensions/bigpowers/commands/prompts/commit-message.md +1 -0
- package/.gemini/extensions/bigpowers/commands/prompts/guard-git.md +7 -2
- package/.gemini/extensions/bigpowers/commands/prompts/release-branch.md +4 -5
- package/.gemini/extensions/bigpowers/commands/prompts/run-planning.md +6 -1
- package/.gemini/extensions/bigpowers/commands/prompts/verify-work.md +2 -1
- package/.gemini/extensions/bigpowers/gemini-extension.json +1 -1
- package/.gemini/extensions/bigpowers/skills/build-epic/SKILL.md +1 -1
- package/.gemini/extensions/bigpowers/skills/commit-message/SKILL.md +1 -0
- package/.gemini/extensions/bigpowers/skills/guard-git/SKILL.md +7 -2
- package/.gemini/extensions/bigpowers/skills/release-branch/SKILL.md +4 -5
- package/.gemini/extensions/bigpowers/skills/run-planning/SKILL.md +6 -1
- package/.gemini/extensions/bigpowers/skills/verify-work/SKILL.md +2 -1
- package/.hermes/skills/build-epic/SKILL.md +1 -1
- package/.hermes/skills/commit-message/SKILL.md +1 -0
- package/.hermes/skills/guard-git/SKILL.md +7 -2
- package/.hermes/skills/release-branch/SKILL.md +4 -5
- package/.hermes/skills/run-planning/SKILL.md +6 -1
- package/.hermes/skills/verify-work/SKILL.md +2 -1
- package/.kilocode/rules/build-epic.md +1 -1
- package/.kilocode/rules/commit-message.md +1 -0
- package/.kilocode/rules/guard-git.md +7 -2
- package/.kilocode/rules/release-branch.md +4 -5
- package/.kilocode/rules/run-planning.md +6 -1
- package/.kilocode/rules/verify-work.md +2 -1
- package/.mimocode/skills/build-epic/SKILL.md +1 -1
- package/.mimocode/skills/commit-message/SKILL.md +1 -0
- package/.mimocode/skills/guard-git/SKILL.md +7 -2
- package/.mimocode/skills/release-branch/SKILL.md +4 -5
- package/.mimocode/skills/run-planning/SKILL.md +6 -1
- package/.mimocode/skills/verify-work/SKILL.md +2 -1
- package/.opencode/skills/build-epic/SKILL.md +1 -1
- package/.opencode/skills/commit-message/SKILL.md +1 -0
- package/.opencode/skills/guard-git/SKILL.md +7 -2
- package/.opencode/skills/release-branch/SKILL.md +4 -5
- package/.opencode/skills/run-planning/SKILL.md +6 -1
- package/.opencode/skills/verify-work/SKILL.md +2 -1
- package/.pi/package.json +1 -1
- package/.pi/prompts/build-epic.md +1 -1
- package/.pi/prompts/commit-message.md +1 -0
- package/.pi/prompts/guard-git.md +7 -2
- package/.pi/prompts/release-branch.md +4 -5
- package/.pi/prompts/run-planning.md +6 -1
- package/.pi/prompts/verify-work.md +2 -1
- package/.pi/skills/build-epic/SKILL.md +1 -1
- package/.pi/skills/commit-message/SKILL.md +1 -0
- package/.pi/skills/guard-git/SKILL.md +7 -2
- package/.pi/skills/release-branch/SKILL.md +4 -5
- package/.pi/skills/run-planning/SKILL.md +6 -1
- package/.pi/skills/verify-work/SKILL.md +2 -1
- package/.qwen/skills/build-epic/SKILL.md +1 -1
- package/.qwen/skills/commit-message/SKILL.md +1 -0
- package/.qwen/skills/guard-git/SKILL.md +7 -2
- package/.qwen/skills/release-branch/SKILL.md +4 -5
- package/.qwen/skills/run-planning/SKILL.md +6 -1
- package/.qwen/skills/verify-work/SKILL.md +2 -1
- package/.trae/skills/build-epic/SKILL.md +1 -1
- package/.trae/skills/commit-message/SKILL.md +1 -0
- package/.trae/skills/guard-git/SKILL.md +7 -2
- package/.trae/skills/release-branch/SKILL.md +4 -5
- package/.trae/skills/run-planning/SKILL.md +6 -1
- package/.trae/skills/verify-work/SKILL.md +2 -1
- package/.windsurf/rules/build-epic.md +1 -1
- package/.windsurf/rules/commit-message.md +1 -0
- package/.windsurf/rules/guard-git.md +7 -2
- package/.windsurf/rules/release-branch.md +4 -5
- package/.windsurf/rules/run-planning.md +6 -1
- package/.windsurf/rules/verify-work.md +2 -1
- package/.zcode/skills/build-epic/SKILL.md +1 -1
- package/.zcode/skills/commit-message/SKILL.md +1 -0
- package/.zcode/skills/guard-git/SKILL.md +7 -2
- package/.zcode/skills/release-branch/SKILL.md +4 -5
- package/.zcode/skills/run-planning/SKILL.md +6 -1
- package/.zcode/skills/verify-work/SKILL.md +2 -1
- package/CHANGELOG.md +15 -0
- package/extensions/omp-hooks.ts +197 -58
- package/hooks/pre/bash.sh +54 -7
- package/hooks/pre-tool-use.sh +72 -12
- package/package.json +2 -1
- package/scripts/convert-legado.sh +1 -0
- package/scripts/lib/__pycache__/atomic_write.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/link_utils.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_okf.cpython-310.pyc +0 -0
- package/scripts/lib/assert-epic-gate.sh +32 -0
- package/scripts/lib/assert-planning-artifacts.sh +65 -0
- package/scripts/lib/git-argv.py +168 -0
- package/scripts/lib/golden-suite-gates.sh +2 -0
- package/scripts/omp-guard-probe.ts +46 -0
- package/scripts/test-generated-path-hook.sh +57 -0
- package/scripts/test-guard-git-normalize.sh +189 -0
- package/scripts/test-test-lock-hook.sh +100 -0
- package/scripts/validate-doctrine.sh +4 -4
- package/scripts/verify-adversarial.sh +146 -0
- package/scripts/verify-tdd-red-commit.sh +39 -1
- package/skills/build-epic/SKILL.md +1 -1
- package/skills/commit-message/SKILL.md +1 -0
- package/skills/guard-git/SKILL.md +7 -2
- package/skills/release-branch/SKILL.md +4 -5
- package/skills/run-planning/SKILL.md +6 -1
- package/skills/verify-work/SKILL.md +2 -1
- package/skills-lock.json +6 -6
- package/tests/test-artifact-assertions.sh +86 -0
- package/tests/test-verify-adversarial.sh +27 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Normalize git argv before applying the guard-git deny rules (#184).
|
|
3
|
+
|
|
4
|
+
The old guard matched literal substrings, so `git -C repo reset --hard`,
|
|
5
|
+
`git reset -q --hard` and `git checkout -- .` all slipped through: the
|
|
6
|
+
destructive tokens are still there, only their position or a global flag
|
|
7
|
+
changed. Matching on the raw command string cannot see that.
|
|
8
|
+
|
|
9
|
+
This module tokenizes the command (quote-aware), finds every `git` invocation,
|
|
10
|
+
strips the global options that precede the subcommand (`-C <path>`,
|
|
11
|
+
`--no-pager`, `-c k=v`, `--git-dir=…`, …), and then decides on the
|
|
12
|
+
(subcommand, arguments) pair. `extensions/omp-hooks.ts` implements the same
|
|
13
|
+
rules in TypeScript; `scripts/test-guard-git-normalize.sh` fails if the two
|
|
14
|
+
disagree on any fixture row.
|
|
15
|
+
|
|
16
|
+
Rule table (keep in sync with the TS port):
|
|
17
|
+
|
|
18
|
+
reset block when any argument is --hard
|
|
19
|
+
clean block when any short flag carries `f` (-f, -fd, -fdx) or --force
|
|
20
|
+
branch block on -D, or on --delete together with --force/-f
|
|
21
|
+
checkout block on -f/--force, or on a `.` pathspec (with or without --)
|
|
22
|
+
restore block on --worktree, or on a `.` pathspec
|
|
23
|
+
push block on -f/--force/+refspec, unless --force-with-lease is given
|
|
24
|
+
|
|
25
|
+
CLI: git-argv.py --command "<shell command>" → prints `block: <reason>` or `allow`
|
|
26
|
+
Exit: 0 allow, 10 block, 2 usage error.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import shlex
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
# Global options that consume the next token as their value.
|
|
34
|
+
VALUE_FLAGS = {
|
|
35
|
+
"-C",
|
|
36
|
+
"-c",
|
|
37
|
+
"--exec-path",
|
|
38
|
+
"--git-dir",
|
|
39
|
+
"--work-tree",
|
|
40
|
+
"--namespace",
|
|
41
|
+
"--super-prefix",
|
|
42
|
+
"--config-env",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Global options that stand alone.
|
|
46
|
+
BARE_FLAGS = {
|
|
47
|
+
"-p",
|
|
48
|
+
"--paginate",
|
|
49
|
+
"--no-pager",
|
|
50
|
+
"--bare",
|
|
51
|
+
"--no-replace-objects",
|
|
52
|
+
"--literal-pathspecs",
|
|
53
|
+
"--glob-pathspecs",
|
|
54
|
+
"--icase-pathspecs",
|
|
55
|
+
"--no-optional-locks",
|
|
56
|
+
"--version",
|
|
57
|
+
"--help",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Command separators: the guard judges each git invocation on its own.
|
|
61
|
+
SEPARATORS = {";", "&&", "||", "|", "&", "(", ")", "\n"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def tokenize(command: str) -> list[str]:
|
|
65
|
+
"""Quote-aware split. Bad quoting degrades to whitespace splitting, never to eval."""
|
|
66
|
+
try:
|
|
67
|
+
return shlex.split(command, comments=False, posix=True)
|
|
68
|
+
except ValueError:
|
|
69
|
+
return command.split()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def git_invocations(tokens: list[str]) -> list[tuple[str, list[str], int]]:
|
|
73
|
+
"""Yield (subcommand, args, index) for every git call in the token stream."""
|
|
74
|
+
found: list[tuple[str, list[str], int]] = []
|
|
75
|
+
i = 0
|
|
76
|
+
while i < len(tokens):
|
|
77
|
+
token = tokens[i]
|
|
78
|
+
if token.rsplit("/", 1)[-1] != "git":
|
|
79
|
+
i += 1
|
|
80
|
+
continue
|
|
81
|
+
start = i
|
|
82
|
+
j = i + 1
|
|
83
|
+
# Skip global options between `git` and the subcommand.
|
|
84
|
+
while j < len(tokens):
|
|
85
|
+
opt = tokens[j]
|
|
86
|
+
if opt in SEPARATORS:
|
|
87
|
+
break
|
|
88
|
+
name = opt.split("=", 1)[0]
|
|
89
|
+
if name in VALUE_FLAGS:
|
|
90
|
+
j += 2 if "=" not in opt else 1
|
|
91
|
+
continue
|
|
92
|
+
if name in BARE_FLAGS or (opt.startswith("-") and not opt.startswith("--") and len(opt) > 1):
|
|
93
|
+
j += 1
|
|
94
|
+
continue
|
|
95
|
+
break
|
|
96
|
+
if j >= len(tokens) or tokens[j] in SEPARATORS:
|
|
97
|
+
i = j + 1
|
|
98
|
+
continue
|
|
99
|
+
sub = tokens[j]
|
|
100
|
+
args: list[str] = []
|
|
101
|
+
k = j + 1
|
|
102
|
+
while k < len(tokens) and tokens[k] not in SEPARATORS:
|
|
103
|
+
args.append(tokens[k])
|
|
104
|
+
k += 1
|
|
105
|
+
found.append((sub, args, start))
|
|
106
|
+
i = k
|
|
107
|
+
return found
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _short_flag_has(args: list[str], letter: str) -> bool:
|
|
111
|
+
for arg in args:
|
|
112
|
+
if arg.startswith("-") and not arg.startswith("--") and letter in arg[1:]:
|
|
113
|
+
return True
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def classify_git(sub: str, args: list[str]) -> str | None:
|
|
118
|
+
"""Return a block reason, or None to allow."""
|
|
119
|
+
if sub == "reset" and "--hard" in args:
|
|
120
|
+
return "git reset --hard discards uncommitted work"
|
|
121
|
+
if sub == "clean" and (_short_flag_has(args, "f") or "--force" in args):
|
|
122
|
+
return "git clean -f deletes untracked files"
|
|
123
|
+
if sub == "branch":
|
|
124
|
+
forced = "-D" in args or ("--delete" in args and ("--force" in args or _short_flag_has(args, "f")))
|
|
125
|
+
if forced:
|
|
126
|
+
return "git branch -D deletes an unmerged branch"
|
|
127
|
+
if sub == "checkout":
|
|
128
|
+
if "-f" in args or "--force" in args:
|
|
129
|
+
return "git checkout -f discards local changes"
|
|
130
|
+
if "." in args:
|
|
131
|
+
return "git checkout . discards local changes"
|
|
132
|
+
if sub == "restore":
|
|
133
|
+
if "--worktree" in args or "." in args:
|
|
134
|
+
return "git restore discards local changes"
|
|
135
|
+
if sub == "push":
|
|
136
|
+
leased = any(a == "--force-with-lease" or a.startswith("--force-with-lease=") for a in args)
|
|
137
|
+
forced = (
|
|
138
|
+
"-f" in args
|
|
139
|
+
or "--force" in args
|
|
140
|
+
or any(a.startswith("+") and not a.startswith("++") for a in args)
|
|
141
|
+
)
|
|
142
|
+
if forced and not leased:
|
|
143
|
+
return "force push rewrites remote history"
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def classify(command: str) -> str | None:
|
|
148
|
+
for sub, args, _idx in git_invocations(tokenize(command)):
|
|
149
|
+
reason = classify_git(sub, args)
|
|
150
|
+
if reason:
|
|
151
|
+
return reason
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main(argv: list[str]) -> int:
|
|
156
|
+
if len(argv) == 3 and argv[1] == "--command":
|
|
157
|
+
reason = classify(argv[2])
|
|
158
|
+
if reason:
|
|
159
|
+
print(f"block: {reason}")
|
|
160
|
+
return 10
|
|
161
|
+
print("allow")
|
|
162
|
+
return 0
|
|
163
|
+
print(__doc__, file=sys.stderr)
|
|
164
|
+
return 2
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
sys.exit(main(sys.argv))
|
|
@@ -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`);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e86s01
|
|
3
|
+
# Test suite for generated-path edit blocking in pre-tool-use.sh.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
8
|
+
HOOK="$REPO_ROOT/hooks/pre-tool-use.sh"
|
|
9
|
+
|
|
10
|
+
pass_count=0
|
|
11
|
+
fail_count=0
|
|
12
|
+
|
|
13
|
+
assert_denied() {
|
|
14
|
+
local label="$1"
|
|
15
|
+
local input="$2"
|
|
16
|
+
local output exit_code=0
|
|
17
|
+
output=$(echo "$input" | bash "$HOOK" 2>&1) || exit_code=$?
|
|
18
|
+
if [ "$exit_code" -ne 0 ] || echo "$output" | grep -q '"decision"[[:space:]]*:[[:space:]]*"deny"'; then
|
|
19
|
+
echo " PASS $label"
|
|
20
|
+
pass_count=$((pass_count + 1))
|
|
21
|
+
else
|
|
22
|
+
echo " FAIL $label (expected deny, got allow: $output)"
|
|
23
|
+
fail_count=$((fail_count + 1))
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
assert_allowed() {
|
|
28
|
+
local label="$1"
|
|
29
|
+
local input="$2"
|
|
30
|
+
local output exit_code=0
|
|
31
|
+
output=$(echo "$input" | bash "$HOOK" 2>&1) || exit_code=$?
|
|
32
|
+
if [ "$exit_code" -eq 0 ] && ! echo "$output" | grep -q '"decision"[[:space:]]*:[[:space:]]*"deny"'; then
|
|
33
|
+
echo " PASS $label"
|
|
34
|
+
pass_count=$((pass_count + 1))
|
|
35
|
+
else
|
|
36
|
+
echo " FAIL $label (expected allow, got deny: $output)"
|
|
37
|
+
fail_count=$((fail_count + 1))
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
echo "=== Generated Path Hook Tests (e86s01) ==="
|
|
42
|
+
|
|
43
|
+
assert_denied "Deny direct edit to .cursor/rules" \
|
|
44
|
+
'{"tool": "Edit", "file_path": ".cursor/rules/skill.mdc"}'
|
|
45
|
+
|
|
46
|
+
assert_denied "Deny direct edit to .gemini/extensions" \
|
|
47
|
+
'{"tool": "Write", "file_path": ".gemini/extensions/bigpowers/skills/skill.md"}'
|
|
48
|
+
|
|
49
|
+
assert_allowed "Allow edit to source skills/ directory" \
|
|
50
|
+
'{"tool": "Edit", "file_path": "skills/plan-work/SKILL.md"}'
|
|
51
|
+
|
|
52
|
+
echo ""
|
|
53
|
+
echo "=== Summary: $pass_count passed, $fail_count failed ==="
|
|
54
|
+
if [ "$fail_count" -gt 0 ]; then
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
exit 0
|
|
@@ -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 ]]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e86s02
|
|
3
|
+
# Test suite for test-lock hook and RED-commit verification in pre-tool-use.sh.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
8
|
+
HOOK="$REPO_ROOT/hooks/pre-tool-use.sh"
|
|
9
|
+
|
|
10
|
+
pass_count=0
|
|
11
|
+
fail_count=0
|
|
12
|
+
|
|
13
|
+
assert_denied() {
|
|
14
|
+
local label="$1"
|
|
15
|
+
local input="$2"
|
|
16
|
+
local expected_substr="${3:-}"
|
|
17
|
+
local output exit_code=0
|
|
18
|
+
output=$(echo "$input" | bash "$HOOK" 2>&1) || exit_code=$?
|
|
19
|
+
if [ "$exit_code" -ne 0 ] || echo "$output" | grep -q '"decision"[[:space:]]*:[[:space:]]*"deny"'; then
|
|
20
|
+
if [ -n "$expected_substr" ] && ! echo "$output" | grep -qi "$expected_substr"; then
|
|
21
|
+
echo " FAIL $label (expected to contain '$expected_substr', got: $output)"
|
|
22
|
+
fail_count=$((fail_count + 1))
|
|
23
|
+
else
|
|
24
|
+
echo " PASS $label"
|
|
25
|
+
pass_count=$((pass_count + 1))
|
|
26
|
+
fi
|
|
27
|
+
else
|
|
28
|
+
echo " FAIL $label (expected deny, but was allowed: $output)"
|
|
29
|
+
fail_count=$((fail_count + 1))
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
assert_allowed() {
|
|
34
|
+
local label="$1"
|
|
35
|
+
local input="$2"
|
|
36
|
+
local output exit_code=0
|
|
37
|
+
output=$(echo "$input" | bash "$HOOK" 2>&1) || exit_code=$?
|
|
38
|
+
if [ "$exit_code" -eq 0 ] && ! echo "$output" | grep -q '"decision"[[:space:]]*:[[:space:]]*"deny"'; then
|
|
39
|
+
echo " PASS $label"
|
|
40
|
+
pass_count=$((pass_count + 1))
|
|
41
|
+
else
|
|
42
|
+
echo " FAIL $label (expected allow, but was denied: $output)"
|
|
43
|
+
fail_count=$((fail_count + 1))
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
echo "=== Test Locking Hook Tests (e86s02) ==="
|
|
48
|
+
|
|
49
|
+
TMP_DIR=$(mktemp -d)
|
|
50
|
+
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
51
|
+
|
|
52
|
+
cd "$TMP_DIR"
|
|
53
|
+
git init -q
|
|
54
|
+
git config user.email "test@bigpowers.local"
|
|
55
|
+
git config user.name "test"
|
|
56
|
+
mkdir -p specs test src
|
|
57
|
+
|
|
58
|
+
cat > specs/state.yaml <<'YAML'
|
|
59
|
+
active_flow: fix_bug
|
|
60
|
+
YAML
|
|
61
|
+
|
|
62
|
+
echo "initial" > src/app.js
|
|
63
|
+
git add .
|
|
64
|
+
git commit -q -m "feat(app): initial commit"
|
|
65
|
+
|
|
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" \
|
|
68
|
+
'{"tool": "Edit", "file_path": "test/app.test.js"}'
|
|
69
|
+
|
|
70
|
+
# 2. Commit a RED test
|
|
71
|
+
echo "test red" > test/app.test.js
|
|
72
|
+
git add test/app.test.js
|
|
73
|
+
git commit -q -m "test(app): red test reproducing defect"
|
|
74
|
+
|
|
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" \
|
|
77
|
+
'{"tool": "Edit", "file_path": "test/app.test.js"}' \
|
|
78
|
+
"locked"
|
|
79
|
+
|
|
80
|
+
assert_denied "Test file write denied after RED commit in fix_bug" \
|
|
81
|
+
'{"tool": "Write", "file_path": "tests/test_other.py"}' \
|
|
82
|
+
"locked"
|
|
83
|
+
|
|
84
|
+
# 4. Source file edits must remain allowed
|
|
85
|
+
assert_allowed "Source file edit allowed after RED commit in fix_bug" \
|
|
86
|
+
'{"tool": "Edit", "file_path": "src/app.js"}'
|
|
87
|
+
|
|
88
|
+
# 5. When active_flow is not fix_bug, test edits are allowed
|
|
89
|
+
cat > specs/state.yaml <<'YAML'
|
|
90
|
+
active_flow: build_epic
|
|
91
|
+
YAML
|
|
92
|
+
assert_allowed "Test file edit allowed when active_flow is not fix_bug" \
|
|
93
|
+
'{"tool": "Edit", "file_path": "test/app.test.js"}'
|
|
94
|
+
|
|
95
|
+
echo ""
|
|
96
|
+
echo "=== Summary: $pass_count passed, $fail_count failed ==="
|
|
97
|
+
if [ "$fail_count" -gt 0 ]; then
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
exit 0
|
|
@@ -79,7 +79,7 @@ fi
|
|
|
79
79
|
# specs/verifications/reports/ (generated reports), PLAN-evolve-structure.md
|
|
80
80
|
# (migration plan that documents the before→after paths as historical record).
|
|
81
81
|
echo "--- [Epic 2] canonical specs/ subpaths ---"
|
|
82
|
-
LEGACY_SPECS=$(grep -rE "specs/(requirements|plans|audit)\b" \
|
|
82
|
+
LEGACY_SPECS=$(grep -rE --exclude-dir=.claude --exclude-dir=.venv "specs/(requirements|plans|audit)\b" \
|
|
83
83
|
--include="*.md" --include="*.sh" --include="*.yaml" --include="*.yml" --include="*.json" . \
|
|
84
84
|
2>/dev/null \
|
|
85
85
|
| grep -v "specs/archive\|specs/epics/archive\|docs/archive/\|\.git\|node_modules\|\.gemini\|\.cursor" \
|
|
@@ -89,7 +89,7 @@ if [[ "$LEGACY_SPECS" -eq 0 ]]; then
|
|
|
89
89
|
doctrine_pass "no legacy specs/ subpaths (requirements/, plans/, audit/)"
|
|
90
90
|
else
|
|
91
91
|
doctrine_fail "legacy specs/ subpaths found ($LEGACY_SPECS occurrences)"
|
|
92
|
-
grep -rE "specs/(requirements|plans|audit)\b" \
|
|
92
|
+
grep -rE --exclude-dir=.claude --exclude-dir=.venv "specs/(requirements|plans|audit)\b" \
|
|
93
93
|
--include="*.md" --include="*.sh" --include="*.yaml" --include="*.yml" --include="*.json" . \
|
|
94
94
|
2>/dev/null \
|
|
95
95
|
| grep -v "specs/archive\|specs/epics/archive\|docs/archive/\|\.git\|node_modules\|\.gemini\|\.cursor" \
|
|
@@ -99,14 +99,14 @@ fi
|
|
|
99
99
|
|
|
100
100
|
# ── Epic 3: BCP slop guard ────────────────────────────────────────────────────
|
|
101
101
|
echo "--- [Epic 3] BCP terminology ---"
|
|
102
|
-
BCP_SLOP=$(grep -ri "build commit point" . 2>/dev/null \
|
|
102
|
+
BCP_SLOP=$(grep -ri --exclude-dir=.claude --exclude-dir=.venv --exclude-dir=.git --exclude-dir=node_modules "build commit point" . 2>/dev/null \
|
|
103
103
|
| grep -v "specs/archive\|\.git\|node_modules\|\.gemini\|\.cursor\|validate-doctrine\.sh" \
|
|
104
104
|
| wc -l | tr -d ' ') || true
|
|
105
105
|
if [[ "$BCP_SLOP" -eq 0 ]]; then
|
|
106
106
|
doctrine_pass "no 'Build Commit Point' slop — BCP = Business Complexity Points"
|
|
107
107
|
else
|
|
108
108
|
doctrine_fail "'Build Commit Point' found ($BCP_SLOP hits) — canonical term is 'Business Complexity Points'; see docs/references/bcp.md"
|
|
109
|
-
grep -ri "build commit point" . 2>/dev/null \
|
|
109
|
+
grep -ri --exclude-dir=.claude --exclude-dir=.venv --exclude-dir=.git --exclude-dir=node_modules "build commit point" . 2>/dev/null \
|
|
110
110
|
| grep -v "specs/archive\|\.git\|node_modules\|\.gemini\|\.cursor\|validate-doctrine\.sh" | sed -n '1,5p' >&2
|
|
111
111
|
fi
|
|
112
112
|
|