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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [2.91.1](https://github.com/danielvm-git/bigpowers/compare/v2.91.0...v2.91.1) (2026-11-03)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **hooks:** give each guard fixture row its own branch context ([#196](https://github.com/danielvm-git/bigpowers/issues/196)) ([5d379df](https://github.com/danielvm-git/bigpowers/commit/5d379df5dc1d9f139636a3f361ee35a1a1ceedd0))
|
|
7
|
+
* **hooks:** judge git argv instead of command substrings in guard-git ([#194](https://github.com/danielvm-git/bigpowers/issues/194)) ([9532372](https://github.com/danielvm-git/bigpowers/commit/95323721c0257964b572d9e0a37834b34955944f)), closes [#184](https://github.com/danielvm-git/bigpowers/issues/184)
|
|
8
|
+
|
|
9
|
+
# [2.91.0](https://github.com/danielvm-git/bigpowers/compare/v2.90.2...v2.91.0) (2026-11-03)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **hooks:** enforce mechanical gates for red-commit, artifacts, and adversarial mutation (e86) ([#191](https://github.com/danielvm-git/bigpowers/issues/191)) ([7a1b090](https://github.com/danielvm-git/bigpowers/commit/7a1b0907aa20962ee71e825c4996aa80c213be3f))
|
|
15
|
+
|
|
1
16
|
## [2.90.2](https://github.com/danielvm-git/bigpowers/compare/v2.90.1...v2.90.2) (2026-11-03)
|
|
2
17
|
|
|
3
18
|
|
package/extensions/omp-hooks.ts
CHANGED
|
@@ -18,15 +18,152 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
// only truly foreign entries (e.g. a stray node_modules symlink) need exclusion.
|
|
19
19
|
const EXCLUDED_DIRS = new Set([".git", "node_modules"]);
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Git argv normalization (BUG-2026-11-03-guard-git-flag-normalize, #184).
|
|
23
|
+
//
|
|
24
|
+
// Literal substring patterns cannot see a global flag in front of the
|
|
25
|
+
// subcommand (`git -C repo reset --hard`), a flag between the subcommand and the
|
|
26
|
+
// destructive option (`git reset -q --hard`), or an alternative spelling of the
|
|
27
|
+
// same act (`git checkout -- .`, `git branch --delete --force`). Tokenize the
|
|
28
|
+
// command, drop the global options, then judge (subcommand, arguments).
|
|
29
|
+
//
|
|
30
|
+
// scripts/lib/git-argv.py is the same rule table in Python for the shell hooks;
|
|
31
|
+
// scripts/test-guard-git-normalize.sh fails if the two disagree on any fixture.
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/** Quote-aware split; a quoting error degrades to whitespace splitting, never to eval. */
|
|
35
|
+
export function tokenizeArgs(command: string): string[] {
|
|
36
|
+
const tokens: string[] = [];
|
|
37
|
+
let current = "";
|
|
38
|
+
let quote: '"' | "'" | null = null;
|
|
39
|
+
let hasToken = false;
|
|
40
|
+
for (let i = 0; i < command.length; i++) {
|
|
41
|
+
const ch = command[i];
|
|
42
|
+
if (quote) {
|
|
43
|
+
if (ch === quote) quote = null;
|
|
44
|
+
else current += ch;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ch === '"' || ch === "'") {
|
|
48
|
+
quote = ch as '"' | "'";
|
|
49
|
+
hasToken = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (/\s/.test(ch)) {
|
|
53
|
+
if (hasToken) {
|
|
54
|
+
tokens.push(current);
|
|
55
|
+
current = "";
|
|
56
|
+
hasToken = false;
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
current += ch;
|
|
61
|
+
hasToken = true;
|
|
62
|
+
}
|
|
63
|
+
if (hasToken) tokens.push(current);
|
|
64
|
+
return tokens;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Global options that consume the next token as their value.
|
|
68
|
+
const GIT_VALUE_FLAGS = new Set([
|
|
69
|
+
"-C", "-c", "--exec-path", "--git-dir", "--work-tree", "--namespace",
|
|
70
|
+
"--super-prefix", "--config-env",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
// Global options that stand alone.
|
|
74
|
+
const GIT_BARE_FLAGS = new Set([
|
|
75
|
+
"-p", "--paginate", "--no-pager", "--bare", "--no-replace-objects",
|
|
76
|
+
"--literal-pathspecs", "--glob-pathspecs", "--icase-pathspecs",
|
|
77
|
+
"--no-optional-locks", "--version", "--help",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const SEPARATORS = new Set([";", "&&", "||", "|", "&", "(", ")", "\n"]);
|
|
81
|
+
|
|
82
|
+
function shortFlagHas(args: string[], letter: string): boolean {
|
|
83
|
+
return args.some((a) => a.startsWith("-") && !a.startsWith("--") && a.slice(1).includes(letter));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Block reason for one normalized git invocation, or null. Mirror git-argv.py. */
|
|
87
|
+
export function classifyGitInvocation(sub: string, args: string[]): string | null {
|
|
88
|
+
if (sub === "reset" && args.includes("--hard")) return "git reset --hard discards uncommitted work";
|
|
89
|
+
if (sub === "clean" && (shortFlagHas(args, "f") || args.includes("--force"))) {
|
|
90
|
+
return "git clean -f deletes untracked files";
|
|
91
|
+
}
|
|
92
|
+
if (sub === "branch") {
|
|
93
|
+
const forced =
|
|
94
|
+
args.includes("-D") ||
|
|
95
|
+
(args.includes("--delete") && (args.includes("--force") || shortFlagHas(args, "f")));
|
|
96
|
+
if (forced) return "git branch -D deletes an unmerged branch";
|
|
97
|
+
}
|
|
98
|
+
if (sub === "checkout") {
|
|
99
|
+
if (args.includes("-f") || args.includes("--force")) return "git checkout -f discards local changes";
|
|
100
|
+
if (args.includes(".")) return "git checkout . discards local changes";
|
|
101
|
+
}
|
|
102
|
+
if (sub === "restore" && (args.includes("--worktree") || args.includes("."))) {
|
|
103
|
+
return "git restore discards local changes";
|
|
104
|
+
}
|
|
105
|
+
if (sub === "push") {
|
|
106
|
+
const leased = args.some((a) => a === "--force-with-lease" || a.startsWith("--force-with-lease="));
|
|
107
|
+
const forced =
|
|
108
|
+
args.includes("-f") ||
|
|
109
|
+
args.includes("--force") ||
|
|
110
|
+
args.some((a) => a.startsWith("+") && !a.startsWith("++"));
|
|
111
|
+
if (forced && !leased) return "force push rewrites remote history";
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Every (subcommand, args) pair in a command, with global options stripped. */
|
|
117
|
+
export function gitInvocations(tokens: string[]): Array<{ sub: string; args: string[] }> {
|
|
118
|
+
const found: Array<{ sub: string; args: string[] }> = [];
|
|
119
|
+
let i = 0;
|
|
120
|
+
while (i < tokens.length) {
|
|
121
|
+
const token = tokens[i];
|
|
122
|
+
if (token.split("/").pop() !== "git") {
|
|
123
|
+
i += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
let j = i + 1;
|
|
127
|
+
while (j < tokens.length) {
|
|
128
|
+
const opt = tokens[j];
|
|
129
|
+
if (SEPARATORS.has(opt)) break;
|
|
130
|
+
const name = opt.split("=")[0];
|
|
131
|
+
if (GIT_VALUE_FLAGS.has(name)) {
|
|
132
|
+
j += opt.includes("=") ? 1 : 2;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (GIT_BARE_FLAGS.has(name) || (opt.startsWith("-") && !opt.startsWith("--") && opt.length > 1)) {
|
|
136
|
+
j += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
if (j >= tokens.length || SEPARATORS.has(tokens[j])) {
|
|
142
|
+
i = j + 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const sub = tokens[j];
|
|
146
|
+
const args: string[] = [];
|
|
147
|
+
let k = j + 1;
|
|
148
|
+
while (k < tokens.length && !SEPARATORS.has(tokens[k])) {
|
|
149
|
+
args.push(tokens[k]);
|
|
150
|
+
k += 1;
|
|
151
|
+
}
|
|
152
|
+
found.push({ sub, args });
|
|
153
|
+
i = k;
|
|
154
|
+
}
|
|
155
|
+
return found;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The normalized verdict for a whole shell command: a reason to block, or null. */
|
|
159
|
+
export function dangerousGitCommand(command: string): string | null {
|
|
160
|
+
for (const { sub, args } of gitInvocations(tokenizeArgs(command))) {
|
|
161
|
+
const reason = classifyGitInvocation(sub, args);
|
|
162
|
+
if (reason) return reason;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
30
167
|
|
|
31
168
|
// Conventional Commits specifies `type(scope)!:`, but many repositories write
|
|
32
169
|
// `type!(scope):`. Accept both; the leading type is what matters.
|
|
@@ -224,6 +361,46 @@ function currentBranch(cwd: string): string {
|
|
|
224
361
|
}
|
|
225
362
|
}
|
|
226
363
|
|
|
364
|
+
/** Everything the tool_call handler decides, as a pure function (testable). */
|
|
365
|
+
export function evaluateBashCall(
|
|
366
|
+
command: string,
|
|
367
|
+
opts: {
|
|
368
|
+
landMode: boolean;
|
|
369
|
+
hasRepo: boolean;
|
|
370
|
+
branchFor: (upTo: number) => string | undefined;
|
|
371
|
+
},
|
|
372
|
+
): string | null {
|
|
373
|
+
const gitReason = dangerousGitCommand(command);
|
|
374
|
+
if (gitReason) return `BLOCKED: ${gitReason}. Use a safer approach or ask the user.`;
|
|
375
|
+
|
|
376
|
+
if (!opts.hasRepo) return null;
|
|
377
|
+
|
|
378
|
+
const pushBranch = opts.branchFor(command.length) ?? "unknown";
|
|
379
|
+
if (/git\s+push/.test(command) && !opts.landMode && protectedPushTarget(command, pushBranch)) {
|
|
380
|
+
return "BLOCKED: Direct push to protected branch. Use kickoff-branch + release-branch.";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (/git\s+commit/.test(command)) {
|
|
384
|
+
const commitBranches = [...command.matchAll(/\bgit\s+commit\b/g)].map(
|
|
385
|
+
(m) => opts.branchFor(m.index ?? 0) ?? "unknown",
|
|
386
|
+
);
|
|
387
|
+
const hit = commitBranches.find((b) => (PROTECTED_BRANCHES as readonly string[]).includes(b));
|
|
388
|
+
if (hit) {
|
|
389
|
+
return `BLOCKED: Direct commits to '${hit}' are forbidden. Use a feature branch.`;
|
|
390
|
+
}
|
|
391
|
+
const msg = extractCommitMessage(command);
|
|
392
|
+
if (msg) {
|
|
393
|
+
if (!CONVENTIONAL_RE.test(msg)) {
|
|
394
|
+
return `BLOCKED: Commit subject '${msg.split("\n")[0]}' must follow Conventional Commits: <type>(<scope>): <subject>.`;
|
|
395
|
+
}
|
|
396
|
+
if (msg.split("\n")[0].length > 72) {
|
|
397
|
+
return "BLOCKED: Commit subject line must be \u226472 characters.";
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
227
404
|
function protectedPushTarget(command: string, branch: string): boolean {
|
|
228
405
|
if (PROTECTED_BRANCH_RE.test(command)) return true;
|
|
229
406
|
const trimmed = command.trim();
|
|
@@ -414,55 +591,17 @@ export default function bigpowers(pi: ExtensionAPI) {
|
|
|
414
591
|
const { command } = event.input as { command?: string };
|
|
415
592
|
if (!command) return;
|
|
416
593
|
|
|
417
|
-
for (const pat of DANGEROUS_PATTERNS) {
|
|
418
|
-
if (pat.test(command)) {
|
|
419
|
-
return {
|
|
420
|
-
block: true,
|
|
421
|
-
reason: `BLOCKED: '${command}' matches dangerous pattern '${pat}'.`,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
594
|
const sessionRoot = process.cwd();
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (/git\s+commit/.test(command)) {
|
|
439
|
-
const commitBranches = [...command.matchAll(/\bgit\s+commit\b/g)].map((m) =>
|
|
440
|
-
currentBranch(commandGitCwd(command, m.index ?? 0, sessionRoot)),
|
|
441
|
-
);
|
|
442
|
-
const hit = commitBranches.find((b) =>
|
|
443
|
-
(PROTECTED_BRANCHES as readonly string[]).includes(b),
|
|
444
|
-
);
|
|
445
|
-
if (hit) {
|
|
446
|
-
return {
|
|
447
|
-
block: true,
|
|
448
|
-
reason: `BLOCKED: Direct commits to '${hit}' are forbidden. Use a feature branch.`,
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
const msg = extractCommitMessage(command);
|
|
452
|
-
if (msg) {
|
|
453
|
-
if (!CONVENTIONAL_RE.test(msg)) {
|
|
454
|
-
return {
|
|
455
|
-
block: true,
|
|
456
|
-
reason: `BLOCKED: Commit subject '${msg.split("\n")[0]}' must follow Conventional Commits: <type>(<scope>): <subject>.`,
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
if (msg.split("\n")[0].length > 72) {
|
|
460
|
-
return {
|
|
461
|
-
block: true,
|
|
462
|
-
reason: "BLOCKED: Commit subject line must be ≤72 characters.",
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
595
|
+
const verdict = evaluateBashCall(command, {
|
|
596
|
+
landMode: process.env.GIT_BIGPOWERS_LAND === "1",
|
|
597
|
+
// `undefined` means "there is no repo here"; the guard then only judges
|
|
598
|
+
// the argv rules, which need no branch.
|
|
599
|
+
branchFor: (upTo: number) =>
|
|
600
|
+
inGitRepo(sessionRoot)
|
|
601
|
+
? currentBranch(commandGitCwd(command, upTo, sessionRoot))
|
|
602
|
+
: undefined,
|
|
603
|
+
hasRepo: inGitRepo(sessionRoot),
|
|
604
|
+
});
|
|
605
|
+
if (verdict) return { block: true, reason: verdict };
|
|
467
606
|
});
|
|
468
607
|
}
|
package/hooks/pre/bash.sh
CHANGED
|
@@ -11,7 +11,8 @@ INPUT=$(cat)
|
|
|
11
11
|
COMMAND=$(echo "$INPUT" | jq -r '.command // .tool_input.command // empty' 2>/dev/null || true)
|
|
12
12
|
[ -z "$COMMAND" ] && exit 0
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# Last-resort literals, used only when the normalizer is unreachable.
|
|
15
|
+
LEGACY_DANGEROUS=(
|
|
15
16
|
"git reset --hard"
|
|
16
17
|
"git clean -f"
|
|
17
18
|
"git branch -D"
|
|
@@ -19,18 +20,64 @@ DANGEROUS_PATTERNS=(
|
|
|
19
20
|
"push -f"
|
|
20
21
|
)
|
|
21
22
|
PROTECTED_BRANCHES=("main" "master")
|
|
23
|
+
COMMIT_MSG_RE='-m[[:space:]]+"([^"]+)"'
|
|
22
24
|
CONVENTIONAL_RE='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?:[[:space:]].+'
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
# --- Normalized git argv (BUG-2026-11-03-guard-git-flag-normalize, #184) ------
|
|
27
|
+
# Literal substring matching cannot see a global flag in front of the subcommand
|
|
28
|
+
# or a flag between the subcommand and the destructive option: the destructive
|
|
29
|
+
# tokens are all still there, only their position changed. The rules live in
|
|
30
|
+
# scripts/lib/git-argv.py and are mirrored in extensions/omp-hooks.ts;
|
|
31
|
+
# scripts/test-guard-git-normalize.sh fails if the two disagree on any fixture.
|
|
32
|
+
SELF_PATH="${BASH_SOURCE[0]}"
|
|
33
|
+
# Only pay for realpath when the hook actually is a symlink (installed copies).
|
|
34
|
+
# This hook runs on every tool call, so a spawn per invocation is real latency.
|
|
35
|
+
if [ -L "$SELF_PATH" ] && command -v python3 >/dev/null 2>&1; then
|
|
36
|
+
SELF_PATH="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "$SELF_PATH" 2>/dev/null || echo "$SELF_PATH")"
|
|
37
|
+
fi
|
|
38
|
+
GUARD_ROOT="$(cd "$(dirname "$SELF_PATH")/.." 2>/dev/null && pwd -P || true)"
|
|
39
|
+
GIT_ARGV="$GUARD_ROOT/scripts/lib/git-argv.py"
|
|
40
|
+
|
|
41
|
+
# classify_git_argv <command>: echoes "block: <reason>" or nothing.
|
|
42
|
+
# Fast path: a command with no "git" token cannot contain a git invocation, so
|
|
43
|
+
# the helper (a python start) is skipped. Falls back to the legacy literal
|
|
44
|
+
# patterns when the helper is unreachable (hook symlinked outside the package).
|
|
45
|
+
classify_git_argv() {
|
|
46
|
+
local cmd="$1" out=""
|
|
47
|
+
case "$cmd" in
|
|
48
|
+
*git*) ;;
|
|
49
|
+
*) return 0 ;;
|
|
50
|
+
esac
|
|
51
|
+
if command -v python3 >/dev/null 2>&1 && [ -f "$GIT_ARGV" ]; then
|
|
52
|
+
out="$(python3 "$GIT_ARGV" --command "$cmd" 2>/dev/null || true)"
|
|
53
|
+
case "$out" in
|
|
54
|
+
block:*) printf '%s' "$out"; return 0 ;;
|
|
55
|
+
*) return 0 ;;
|
|
56
|
+
esac
|
|
28
57
|
fi
|
|
29
|
-
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
GIT_VERDICT="$(classify_git_argv "$COMMAND")"
|
|
62
|
+
if [ -n "$GIT_VERDICT" ]; then
|
|
63
|
+
printf '{"decision":"block","reason":"BLOCKED: %s"}\n' "${GIT_VERDICT#block: }"
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
if [ ! -f "$GIT_ARGV" ]; then
|
|
67
|
+
for p in "${LEGACY_DANGEROUS[@]}"; do
|
|
68
|
+
if echo "$COMMAND" | grep -qE "$p"; then
|
|
69
|
+
printf '{"decision":"block","reason":"BLOCKED: dangerous pattern %q"}\n' "$p"
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
done
|
|
73
|
+
fi
|
|
30
74
|
|
|
31
75
|
if [[ "$COMMAND" =~ git[[:space:]]+commit ]]; then
|
|
32
76
|
MSG=""
|
|
33
|
-
|
|
77
|
+
# Parentheses inside [[ =~ ]] must come from a variable, not the literal
|
|
78
|
+
# pattern: bash parses `([^"]+)` as a syntax error (pre-existing since #118,
|
|
79
|
+
# so this fallback hook never ran; BUG-2026-11-03-guard-git-flag-normalize).
|
|
80
|
+
if [[ "$COMMAND" =~ $COMMIT_MSG_RE ]]; then MSG="${BASH_REMATCH[1]}"; fi
|
|
34
81
|
if [ -n "$MSG" ] && [[ ! "$MSG" =~ $CONVENTIONAL_RE ]]; then
|
|
35
82
|
echo '{"decision":"block","reason":"BLOCKED: Conventional Commits required"}'
|
|
36
83
|
exit 0
|
package/hooks/pre-tool-use.sh
CHANGED
|
@@ -11,16 +11,13 @@ set -euo pipefail
|
|
|
11
11
|
|
|
12
12
|
# Configuration
|
|
13
13
|
PROTECTED_BRANCHES=("main" "master")
|
|
14
|
-
|
|
14
|
+
# Last-resort literals, used only when the normalizer is unreachable.
|
|
15
|
+
LEGACY_DANGEROUS=(
|
|
15
16
|
"git reset --hard"
|
|
16
|
-
"git clean -fd"
|
|
17
17
|
"git clean -f"
|
|
18
18
|
"git branch -D"
|
|
19
|
-
"git checkout \\."
|
|
20
|
-
"git restore \\."
|
|
21
19
|
"push --force"
|
|
22
20
|
"push -f"
|
|
23
|
-
"reset --hard"
|
|
24
21
|
)
|
|
25
22
|
CONVENTIONAL_COMMITS_REGEX='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?:[[:space:]].+'
|
|
26
23
|
LAND_MODE="${GIT_BIGPOWERS_LAND:-}"
|
|
@@ -29,6 +26,41 @@ LAND_MODE="${GIT_BIGPOWERS_LAND:-}"
|
|
|
29
26
|
# GIT_GUARDRAILS_MODE: claude (default) | cursor | gemini
|
|
30
27
|
MODE="${GIT_GUARDRAILS_MODE:-claude}"
|
|
31
28
|
|
|
29
|
+
# --- Normalized git argv (BUG-2026-11-03-guard-git-flag-normalize, #184) ------
|
|
30
|
+
# Literal substring matching cannot see a global flag in front of the subcommand
|
|
31
|
+
# or a flag between the subcommand and the destructive option: the destructive
|
|
32
|
+
# tokens are all still there, only their position changed. The rules live in
|
|
33
|
+
# scripts/lib/git-argv.py and are mirrored in extensions/omp-hooks.ts;
|
|
34
|
+
# scripts/test-guard-git-normalize.sh fails if the two disagree on any fixture.
|
|
35
|
+
SELF_PATH="${BASH_SOURCE[0]}"
|
|
36
|
+
# Only pay for realpath when the hook actually is a symlink (installed copies).
|
|
37
|
+
# This hook runs on every tool call, so a spawn per invocation is real latency.
|
|
38
|
+
if [ -L "$SELF_PATH" ] && command -v python3 >/dev/null 2>&1; then
|
|
39
|
+
SELF_PATH="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "$SELF_PATH" 2>/dev/null || echo "$SELF_PATH")"
|
|
40
|
+
fi
|
|
41
|
+
GUARD_ROOT="$(cd "$(dirname "$SELF_PATH")/.." 2>/dev/null && pwd -P || true)"
|
|
42
|
+
GIT_ARGV="$GUARD_ROOT/scripts/lib/git-argv.py"
|
|
43
|
+
|
|
44
|
+
# classify_git_argv <command>: echoes "block: <reason>" or nothing.
|
|
45
|
+
# Fast path: a command with no "git" token cannot contain a git invocation, so
|
|
46
|
+
# the helper (a python start) is skipped. Falls back to the legacy literal
|
|
47
|
+
# patterns when the helper is unreachable (hook symlinked outside the package).
|
|
48
|
+
classify_git_argv() {
|
|
49
|
+
local cmd="$1" out=""
|
|
50
|
+
case "$cmd" in
|
|
51
|
+
*git*) ;;
|
|
52
|
+
*) return 0 ;;
|
|
53
|
+
esac
|
|
54
|
+
if command -v python3 >/dev/null 2>&1 && [ -f "$GIT_ARGV" ]; then
|
|
55
|
+
out="$(python3 "$GIT_ARGV" --command "$cmd" 2>/dev/null || true)"
|
|
56
|
+
case "$out" in
|
|
57
|
+
block:*) printf '%s' "$out"; return 0 ;;
|
|
58
|
+
*) return 0 ;;
|
|
59
|
+
esac
|
|
60
|
+
fi
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
|
|
32
64
|
deny() {
|
|
33
65
|
local reason="$1"
|
|
34
66
|
if [ "$MODE" = "gemini" ]; then
|
|
@@ -42,20 +74,48 @@ deny() {
|
|
|
42
74
|
# Read input from stdin
|
|
43
75
|
INPUT=$(cat)
|
|
44
76
|
|
|
45
|
-
# Extract command using jq (standard in bigpowers)
|
|
77
|
+
# Extract command and file path using jq (standard in bigpowers)
|
|
46
78
|
COMMAND=$(echo "$INPUT" | jq -r '.command // .tool_input.command // empty')
|
|
79
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.file_path // .tool_input.file_path // .path // .tool_input.path // .target_file // .tool_input.target_file // empty')
|
|
80
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool // .name // .tool_name // empty')
|
|
81
|
+
|
|
82
|
+
# 0. Check file editing rules
|
|
83
|
+
if [ -n "$FILE_PATH" ]; then
|
|
84
|
+
# e86s01: Generated paths are blocked at edit time
|
|
85
|
+
if [[ "$FILE_PATH" =~ ^\.cursor/rules/ ]] || [[ "$FILE_PATH" =~ ^\.gemini/extensions/ ]] || [[ "$FILE_PATH" =~ ^\.windsurf/ ]]; then
|
|
86
|
+
deny "BLOCKED: Generated file '$FILE_PATH' cannot be edited directly. Edit the source SKILL.md instead and run bash scripts/sync-skills.sh."
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# e86s02: Tests are locked during fix_bug flow after RED commit
|
|
90
|
+
if [[ "$FILE_PATH" =~ (^|/)test/ ]] || [[ "$FILE_PATH" =~ (^|/)tests/ ]] || [[ "$FILE_PATH" =~ \.test\.[^/]+$ ]] || [[ "$FILE_PATH" =~ _test\.[^/]+$ ]] || [[ "$FILE_PATH" =~ (^|/)test_[^/]+$ ]]; then
|
|
91
|
+
if [ -f specs/state.yaml ] && grep -qE "active_flow:[[:space:]]*fix_bug" specs/state.yaml 2>/dev/null; then
|
|
92
|
+
if git rev-parse --git-dir >/dev/null 2>&1; then
|
|
93
|
+
recent_log=$(git log -n 5 --format=%s 2>/dev/null || true)
|
|
94
|
+
if echo "$recent_log" | grep -qE '^test(\(.+\))?:'; then
|
|
95
|
+
deny "BLOCKED: Test files ($FILE_PATH) are locked during fix_bug after the RED commit. Write the fix in production code (src/), not in tests."
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
47
101
|
|
|
48
102
|
if [ -z "$COMMAND" ]; then
|
|
49
103
|
[ "$MODE" = "gemini" ] && echo '{"decision":"allow"}'
|
|
50
104
|
exit 0
|
|
51
105
|
fi
|
|
52
106
|
|
|
53
|
-
# 1.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
107
|
+
# 1. Normalized git argv (git push is NOT blanket-blocked)
|
|
108
|
+
GIT_VERDICT="$(classify_git_argv "$COMMAND")"
|
|
109
|
+
if [ -n "$GIT_VERDICT" ]; then
|
|
110
|
+
deny "BLOCKED: ${GIT_VERDICT#block: }. Use a safer approach or ask the user."
|
|
111
|
+
fi
|
|
112
|
+
if [ ! -f "$GIT_ARGV" ]; then
|
|
113
|
+
for p in "${LEGACY_DANGEROUS[@]}"; do
|
|
114
|
+
if echo "$COMMAND" | grep -qE "$p"; then
|
|
115
|
+
deny "BLOCKED: '$COMMAND' matches dangerous pattern '$p'. Use a safer approach or ask the user."
|
|
116
|
+
fi
|
|
117
|
+
done
|
|
118
|
+
fi
|
|
59
119
|
|
|
60
120
|
# 2. Check for git commit / git push
|
|
61
121
|
if [[ "$COMMAND" =~ git[[:space:]]+commit ]] || [[ "$COMMAND" =~ git[[:space:]]+push ]]; then
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bigpowers",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.91.1",
|
|
4
4
|
"description": "73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"zod": "^3.24.2"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
+
"@ctxo/lang-typescript": "^0.11.0",
|
|
71
72
|
"@semantic-release/changelog": "^6.0.3",
|
|
72
73
|
"@semantic-release/exec": "^7.1.0",
|
|
73
74
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# convert-legado.sh — RELEASE-PLAN.md + SCOPE.md → YAML layout (one-time migration helper)
|
|
3
|
+
# bp-manual-utility: legacy MD spec converter (pre-e44 migration)
|
|
3
4
|
set -euo pipefail
|
|
4
5
|
source "$(dirname "${BASH_SOURCE[0]}")/lib/python-env.sh"
|
|
5
6
|
SPECS="$REPO_ROOT/specs"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e86 / GH #182
|
|
3
|
+
# Mechanical audit-code pass gate before commit / release.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
STAGE="${1:-commit}"
|
|
7
|
+
|
|
8
|
+
AUDIT_PASSED=false
|
|
9
|
+
|
|
10
|
+
# Check specs/AUDIT_LATEST.md
|
|
11
|
+
if [[ -f specs/AUDIT_LATEST.md ]]; then
|
|
12
|
+
if grep -qiE "VERDICT:[[:space:]]*PASS|RESULT:[[:space:]]*PASS" specs/AUDIT_LATEST.md; then
|
|
13
|
+
AUDIT_PASSED=true
|
|
14
|
+
fi
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Check specs/execution-status.yaml
|
|
18
|
+
if [[ "$AUDIT_PASSED" == "false" ]] && [[ -f specs/execution-status.yaml ]]; then
|
|
19
|
+
if grep -qE "audit_passed:[[:space:]]*true" specs/execution-status.yaml 2>/dev/null; then
|
|
20
|
+
AUDIT_PASSED=true
|
|
21
|
+
fi
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if [[ "$AUDIT_PASSED" == "false" ]]; then
|
|
25
|
+
echo "ERROR [$STAGE]: audit-code check is missing or did not pass (Issue #182)." >&2
|
|
26
|
+
echo "Remediation: Run audit-code and ensure all checks pass before advancing to $STAGE." >&2
|
|
27
|
+
echo "If audit fails, loop back to develop-tdd to address findings." >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
echo "PASS [$STAGE]: audit-code verified."
|
|
32
|
+
exit 0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e86 / GH #183
|
|
3
|
+
# Mechanical artifact existence assertion for run-planning workflow steps.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
STEP="${1:-}"
|
|
7
|
+
|
|
8
|
+
if [[ -z "$STEP" ]]; then
|
|
9
|
+
echo "Usage: bash scripts/lib/assert-planning-artifacts.sh <step_name>" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
case "$STEP" in
|
|
14
|
+
survey-context)
|
|
15
|
+
if [[ ! -f specs/tech-architecture/tech-stack.md ]] && [[ ! -f specs/state.yaml ]]; then
|
|
16
|
+
echo "ERROR [survey-context]: Neither specs/tech-architecture/tech-stack.md nor specs/state.yaml exists." >&2
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
;;
|
|
20
|
+
|
|
21
|
+
scope-work)
|
|
22
|
+
if [[ ! -f specs/product/SCOPE_LATEST.yaml ]] || [[ ! -s specs/product/SCOPE_LATEST.yaml ]]; then
|
|
23
|
+
echo "ERROR [scope-work]: specs/product/SCOPE_LATEST.yaml is missing or empty." >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
;;
|
|
27
|
+
|
|
28
|
+
research-first)
|
|
29
|
+
# Research can record in specs/product/PRIOR_ART.md or directly in specs/
|
|
30
|
+
if [[ ! -f specs/product/PRIOR_ART.md ]] && [[ ! -f specs/product/SCOPE_LATEST.yaml ]]; then
|
|
31
|
+
echo "ERROR [research-first]: Neither specs/product/PRIOR_ART.md nor specs/product/SCOPE_LATEST.yaml found." >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
;;
|
|
35
|
+
|
|
36
|
+
elaborate-spec)
|
|
37
|
+
if [[ ! -f specs/planning-context.yaml ]] && [[ ! -f specs/product/SCOPE_LATEST.yaml ]]; then
|
|
38
|
+
echo "ERROR [elaborate-spec]: specs/planning-context.yaml is missing." >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
;;
|
|
42
|
+
|
|
43
|
+
plan-release)
|
|
44
|
+
if [[ ! -f specs/release-plan.yaml ]] || [[ ! -s specs/release-plan.yaml ]]; then
|
|
45
|
+
echo "ERROR [plan-release]: specs/release-plan.yaml is missing or empty." >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
;;
|
|
49
|
+
|
|
50
|
+
slice-tasks)
|
|
51
|
+
# Assert at least one task file exists in specs/epics/
|
|
52
|
+
if ! find specs/epics -name "*-tasks.yaml" 2>/dev/null | grep -q .; then
|
|
53
|
+
echo "ERROR [slice-tasks]: No tasks files (*-tasks.yaml) found in specs/epics/." >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
;;
|
|
57
|
+
|
|
58
|
+
*)
|
|
59
|
+
echo "WARNING: Unknown step '$STEP' — skipping artifact assertion" >&2
|
|
60
|
+
exit 0
|
|
61
|
+
;;
|
|
62
|
+
esac
|
|
63
|
+
|
|
64
|
+
echo "PASS: Planning artifacts asserted for step '$STEP'."
|
|
65
|
+
exit 0
|