bigpowers 2.87.7 → 2.88.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.
- package/.agents/skills/commit-message/SKILL.md +4 -2
- package/.agents/skills/kickoff-branch/SKILL.md +12 -7
- package/.agents/skills/release-branch/SKILL.md +12 -6
- package/.agents/skills/survey-context/SKILL.md +11 -6
- package/.cline/skills/commit-message/SKILL.md +4 -2
- package/.cline/skills/kickoff-branch/SKILL.md +12 -7
- package/.cline/skills/release-branch/SKILL.md +12 -6
- package/.cline/skills/survey-context/SKILL.md +11 -6
- package/.codebuddy/skills/commit-message/SKILL.md +4 -2
- package/.codebuddy/skills/kickoff-branch/SKILL.md +12 -7
- package/.codebuddy/skills/release-branch/SKILL.md +12 -6
- package/.codebuddy/skills/survey-context/SKILL.md +11 -6
- package/.codex/skills/commit-message/SKILL.md +4 -2
- package/.codex/skills/kickoff-branch/SKILL.md +12 -7
- package/.codex/skills/release-branch/SKILL.md +12 -6
- package/.codex/skills/survey-context/SKILL.md +11 -6
- package/.copilot/skills/commit-message/SKILL.md +4 -2
- package/.copilot/skills/kickoff-branch/SKILL.md +12 -7
- package/.copilot/skills/release-branch/SKILL.md +12 -6
- package/.copilot/skills/survey-context/SKILL.md +11 -6
- package/.cursor/rules/commit-message.mdc +4 -2
- package/.cursor/rules/kickoff-branch.mdc +12 -7
- package/.cursor/rules/release-branch.mdc +12 -6
- package/.cursor/rules/survey-context.mdc +11 -6
- package/.gemini/extensions/bigpowers/commands/prompts/commit-message.md +4 -2
- package/.gemini/extensions/bigpowers/commands/prompts/kickoff-branch.md +12 -7
- package/.gemini/extensions/bigpowers/commands/prompts/release-branch.md +12 -6
- package/.gemini/extensions/bigpowers/commands/prompts/survey-context.md +11 -6
- package/.gemini/extensions/bigpowers/gemini-extension.json +1 -1
- package/.gemini/extensions/bigpowers/skills/commit-message/SKILL.md +4 -2
- package/.gemini/extensions/bigpowers/skills/kickoff-branch/SKILL.md +12 -7
- package/.gemini/extensions/bigpowers/skills/release-branch/SKILL.md +12 -6
- package/.gemini/extensions/bigpowers/skills/survey-context/SKILL.md +11 -6
- package/.hermes/skills/commit-message/SKILL.md +4 -2
- package/.hermes/skills/kickoff-branch/SKILL.md +12 -7
- package/.hermes/skills/release-branch/SKILL.md +12 -6
- package/.hermes/skills/survey-context/SKILL.md +11 -6
- package/.kilocode/rules/commit-message.md +4 -2
- package/.kilocode/rules/kickoff-branch.md +12 -7
- package/.kilocode/rules/release-branch.md +12 -6
- package/.kilocode/rules/survey-context.md +11 -6
- package/.mimocode/skills/commit-message/SKILL.md +4 -2
- package/.mimocode/skills/kickoff-branch/SKILL.md +12 -7
- package/.mimocode/skills/release-branch/SKILL.md +12 -6
- package/.mimocode/skills/survey-context/SKILL.md +11 -6
- package/.opencode/skills/commit-message/SKILL.md +4 -2
- package/.opencode/skills/kickoff-branch/SKILL.md +12 -7
- package/.opencode/skills/release-branch/SKILL.md +12 -6
- package/.opencode/skills/survey-context/SKILL.md +11 -6
- package/.pi/package.json +1 -1
- package/.pi/prompts/commit-message.md +4 -2
- package/.pi/prompts/kickoff-branch.md +12 -7
- package/.pi/prompts/release-branch.md +12 -6
- package/.pi/prompts/survey-context.md +11 -6
- package/.pi/skills/commit-message/SKILL.md +4 -2
- package/.pi/skills/kickoff-branch/SKILL.md +12 -7
- package/.pi/skills/release-branch/SKILL.md +12 -6
- package/.pi/skills/survey-context/SKILL.md +11 -6
- package/.qwen/skills/commit-message/SKILL.md +4 -2
- package/.qwen/skills/kickoff-branch/SKILL.md +12 -7
- package/.qwen/skills/release-branch/SKILL.md +12 -6
- package/.qwen/skills/survey-context/SKILL.md +11 -6
- package/.trae/skills/commit-message/SKILL.md +4 -2
- package/.trae/skills/kickoff-branch/SKILL.md +12 -7
- package/.trae/skills/release-branch/SKILL.md +12 -6
- package/.trae/skills/survey-context/SKILL.md +11 -6
- package/.windsurf/rules/commit-message.md +4 -2
- package/.windsurf/rules/kickoff-branch.md +12 -7
- package/.windsurf/rules/release-branch.md +12 -6
- package/.windsurf/rules/survey-context.md +11 -6
- package/.zcode/skills/commit-message/SKILL.md +4 -2
- package/.zcode/skills/kickoff-branch/SKILL.md +12 -7
- package/.zcode/skills/release-branch/SKILL.md +12 -6
- package/.zcode/skills/survey-context/SKILL.md +11 -6
- package/CHANGELOG.md +14 -0
- package/CONTRIBUTORS.md +48 -0
- package/README.md +27 -0
- package/SKILL-INDEX.md +1 -1
- package/bin/bigpowers.js +8 -0
- package/bin/init.js +39 -0
- package/extensions/omp-hooks.ts +406 -0
- package/hooks/pre/bash.sh +47 -0
- package/package.json +6 -1
- package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-312.pyc +0 -0
- package/scripts/lib/golden-suite-gates.sh +2 -0
- package/scripts/lib/install-helpers.js +53 -1
- package/scripts/lib/project-runtime.sh +110 -0
- package/scripts/omp-smoke.ts +82 -0
- package/scripts/test-install-helpers.js +88 -0
- package/scripts/test-project-runtime.sh +91 -0
- package/scripts/validate-omp-extension.sh +103 -0
- package/skills/commit-message/SKILL.md +4 -2
- package/skills/kickoff-branch/SKILL.md +12 -7
- package/skills/release-branch/SKILL.md +12 -6
- package/skills/survey-context/SKILL.md +11 -6
- package/skills-lock.json +5 -5
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// story: e82s03
|
|
2
|
+
// extensions/omp-hooks.ts
|
|
3
|
+
// bigpowers — Single OMP extension entry exposing every source skill as a
|
|
4
|
+
// slash command, a bigpowers_skill LLM tool, and safety policy guards.
|
|
5
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Constants
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
// Skills live at skills/<name>/SKILL.md — scan inside that directory, so
|
|
16
|
+
// only truly foreign entries (e.g. a stray node_modules symlink) need exclusion.
|
|
17
|
+
const EXCLUDED_DIRS = new Set([".git", "node_modules"]);
|
|
18
|
+
|
|
19
|
+
const DANGEROUS_PATTERNS = [
|
|
20
|
+
/git\s+reset\s+--hard/,
|
|
21
|
+
/git\s+clean\s+-f/,
|
|
22
|
+
/git\s+branch\s+-D/,
|
|
23
|
+
/git\s+checkout\s+\./,
|
|
24
|
+
/git\s+restore\s+\./,
|
|
25
|
+
/push\s+--force/,
|
|
26
|
+
/push\s+-f(?:\s|$)/,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const CONVENTIONAL_RE =
|
|
30
|
+
/^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?:\s.+/;
|
|
31
|
+
|
|
32
|
+
const PROTECTED_BRANCH_RE = /(?:^|\s|[:])(?:main|master)(?:\s|$)/;
|
|
33
|
+
const PROTECTED_BRANCHES = ["main", "master"] as const;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Types
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
interface SkillMeta {
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
/** Filesystem path to the skill's SKILL.md. */
|
|
43
|
+
path: string;
|
|
44
|
+
/** Full SKILL.md content (body only, frontmatter stripped). */
|
|
45
|
+
markdown: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// YAML frontmatter
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Conservatively reads the `name` and `description` keys from YAML-style
|
|
54
|
+
* frontmatter delimited by `---`. Only single-line values are handled;
|
|
55
|
+
* a multi-line YAML value silently yields the directory name as fallback.
|
|
56
|
+
*/
|
|
57
|
+
function parseFrontmatter(
|
|
58
|
+
content: string,
|
|
59
|
+
dirName: string,
|
|
60
|
+
): { name: string; description: string } {
|
|
61
|
+
const lines = content.split("\n");
|
|
62
|
+
if (lines[0]?.trim() !== "---") {
|
|
63
|
+
return { name: dirName, description: "" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const endIdx = lines.indexOf("---", 1);
|
|
67
|
+
if (endIdx === -1) {
|
|
68
|
+
return { name: dirName, description: "" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let name = dirName;
|
|
72
|
+
let description = "";
|
|
73
|
+
|
|
74
|
+
for (let i = 1; i < endIdx; i++) {
|
|
75
|
+
const line = lines[i];
|
|
76
|
+
const nameMatch = line.match(/^name:\s*(.+)$/);
|
|
77
|
+
if (nameMatch) {
|
|
78
|
+
name = nameMatch[1].trim();
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const descMatch = line.match(/^description:\s*(.+)$/);
|
|
82
|
+
if (descMatch) {
|
|
83
|
+
description = descMatch[1].trim();
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { name, description };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Returns the SKILL.md body below the closing `---` frontmatter line. */
|
|
92
|
+
function stripFrontmatter(content: string): string {
|
|
93
|
+
const lines = content.split("\n");
|
|
94
|
+
if (lines[0]?.trim() !== "---") return content;
|
|
95
|
+
const endIdx = lines.indexOf("---", 1);
|
|
96
|
+
if (endIdx === -1) return content;
|
|
97
|
+
// Skip both `---` lines and one blank line if present.
|
|
98
|
+
let start = endIdx + 1;
|
|
99
|
+
if (lines[start]?.trim() === "") start++;
|
|
100
|
+
return lines.slice(start).join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Skill discovery
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
function collectSkillMd(dirPath: string): string | null {
|
|
108
|
+
const mdPath = join(dirPath, "SKILL.md");
|
|
109
|
+
try {
|
|
110
|
+
const st = statSync(mdPath);
|
|
111
|
+
if (!st.isFile()) return null;
|
|
112
|
+
return readFileSync(mdPath, "utf-8");
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Scans `skillsRoot` (expected: `<package-root>/skills/`) for immediate
|
|
120
|
+
* subdirectories that contain a SKILL.md file.
|
|
121
|
+
*/
|
|
122
|
+
function discoverSkills(skillsRoot: string): SkillMeta[] {
|
|
123
|
+
const entries = readdirSync(skillsRoot);
|
|
124
|
+
const result: SkillMeta[] = [];
|
|
125
|
+
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (EXCLUDED_DIRS.has(entry)) continue;
|
|
128
|
+
|
|
129
|
+
const absPath = join(skillsRoot, entry);
|
|
130
|
+
let st;
|
|
131
|
+
try {
|
|
132
|
+
st = statSync(absPath);
|
|
133
|
+
} catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!st.isDirectory()) continue;
|
|
137
|
+
|
|
138
|
+
const content = collectSkillMd(absPath);
|
|
139
|
+
if (content === null) continue;
|
|
140
|
+
|
|
141
|
+
const { name, description } = parseFrontmatter(content, entry);
|
|
142
|
+
const markdown = stripFrontmatter(content);
|
|
143
|
+
|
|
144
|
+
result.push({ name, description, path: join(absPath, "SKILL.md"), markdown });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function pluginRoot(): string {
|
|
151
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Git safety helpers
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
function currentBranch(): string {
|
|
159
|
+
try {
|
|
160
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
161
|
+
encoding: "utf-8",
|
|
162
|
+
timeout: 3000,
|
|
163
|
+
}).trim();
|
|
164
|
+
} catch {
|
|
165
|
+
return "unknown";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function protectedPushTarget(command: string, branch: string): boolean {
|
|
170
|
+
if (PROTECTED_BRANCH_RE.test(command)) return true;
|
|
171
|
+
const trimmed = command.trim();
|
|
172
|
+
return (
|
|
173
|
+
branch === "main" &&
|
|
174
|
+
(trimmed === "git push" || trimmed === "git push origin")
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function extractCommitMessage(command: string): string | null {
|
|
179
|
+
const dq = command.match(/-m\s+"([^"]+)"/);
|
|
180
|
+
if (dq) return dq[1];
|
|
181
|
+
const sq = command.match(/-m\s+'([^']+)'/);
|
|
182
|
+
return sq ? sq[1] : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Prompt builder
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
function buildSkillPrompt(skill: SkillMeta, args: string): string {
|
|
190
|
+
const header = [
|
|
191
|
+
`Activate skill: ${skill.name}`,
|
|
192
|
+
`Description: ${skill.description}`,
|
|
193
|
+
"",
|
|
194
|
+
"--- SKILL.md ---",
|
|
195
|
+
].join("\n");
|
|
196
|
+
|
|
197
|
+
const body = skill.markdown;
|
|
198
|
+
|
|
199
|
+
let footer = "";
|
|
200
|
+
if (args.trim()) {
|
|
201
|
+
footer = `\n\n--- USER ARGUMENTS ---\n${args.trim()}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return `${header}\n${body}${footer}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
async function injectSkillPrompt(
|
|
210
|
+
pi: ExtensionAPI,
|
|
211
|
+
prompt: string,
|
|
212
|
+
): Promise<{ injected: boolean; via: "sendUserMessage" | "sendMessage" | "none" }> {
|
|
213
|
+
try {
|
|
214
|
+
await pi.sendUserMessage(prompt);
|
|
215
|
+
return { injected: true, via: "sendUserMessage" };
|
|
216
|
+
} catch {
|
|
217
|
+
try {
|
|
218
|
+
await pi.sendMessage(
|
|
219
|
+
{
|
|
220
|
+
customType: "bigpowers.skill",
|
|
221
|
+
content: prompt,
|
|
222
|
+
display: true,
|
|
223
|
+
attribution: "user",
|
|
224
|
+
},
|
|
225
|
+
{ deliverAs: "nextTurn", triggerTurn: true },
|
|
226
|
+
);
|
|
227
|
+
return { injected: true, via: "sendMessage" };
|
|
228
|
+
} catch {
|
|
229
|
+
return { injected: false, via: "none" };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Extension factory
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
export default function bigpowers(pi: ExtensionAPI) {
|
|
239
|
+
const z = pi.zod;
|
|
240
|
+
|
|
241
|
+
pi.setLabel("bigpowers");
|
|
242
|
+
|
|
243
|
+
// Discover skills from skills/ in the installed plugin package root.
|
|
244
|
+
const skills = discoverSkills(join(pluginRoot(), "skills"));
|
|
245
|
+
|
|
246
|
+
// ----- Slash commands --------------------------------------------------
|
|
247
|
+
|
|
248
|
+
for (const skill of skills) {
|
|
249
|
+
pi.registerCommand(skill.name, {
|
|
250
|
+
description: skill.description,
|
|
251
|
+
handler: async (args, _ctx) => {
|
|
252
|
+
const prompt = buildSkillPrompt(skill, args);
|
|
253
|
+
await injectSkillPrompt(pi, prompt);
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ----- bigpowers_skill tool --------------------------------------------
|
|
259
|
+
|
|
260
|
+
pi.registerTool({
|
|
261
|
+
name: "bigpowers_skill",
|
|
262
|
+
label: "Bigpowers Skill",
|
|
263
|
+
description:
|
|
264
|
+
"Interact with bigpowers source skills. Use 'list' to enumerate available skills, 'get' to retrieve a skill's full markdown, and 'run' to activate a skill with arguments.",
|
|
265
|
+
parameters: z.object({
|
|
266
|
+
action: z
|
|
267
|
+
.enum(["list", "get", "run"])
|
|
268
|
+
.describe("Operation: list all skills, get one skill, or run a skill with arguments"),
|
|
269
|
+
skill: z
|
|
270
|
+
.string()
|
|
271
|
+
.optional()
|
|
272
|
+
.describe("Skill name — required for 'get' and 'run' actions"),
|
|
273
|
+
args: z
|
|
274
|
+
.string()
|
|
275
|
+
.optional()
|
|
276
|
+
.describe("User arguments passed to the skill — only used with 'run'"),
|
|
277
|
+
}),
|
|
278
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
279
|
+
const { action, skill: skillName, args } = params;
|
|
280
|
+
|
|
281
|
+
switch (action) {
|
|
282
|
+
case "list": {
|
|
283
|
+
const listing = skills.map((s) => ({
|
|
284
|
+
name: s.name,
|
|
285
|
+
description: s.description,
|
|
286
|
+
}));
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: "text", text: JSON.stringify(listing, null, 2) }],
|
|
289
|
+
details: { skills: listing },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case "get": {
|
|
294
|
+
if (!skillName) {
|
|
295
|
+
return {
|
|
296
|
+
content: [{ type: "text", text: "Error: 'skill' parameter is required for 'get' action." }],
|
|
297
|
+
details: { error: "missing_skill_param" },
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const found = skills.find((s) => s.name === skillName);
|
|
301
|
+
if (!found) {
|
|
302
|
+
return {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: "text",
|
|
306
|
+
text: `Error: skill '${skillName}' not found. Use 'list' to see available skills.`,
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
details: { error: "skill_not_found", skill: skillName },
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
content: [
|
|
314
|
+
{ type: "text", text: `# ${found.name}\n\n${found.description}\n\n---\n\n${found.markdown}` },
|
|
315
|
+
],
|
|
316
|
+
details: { name: found.name, description: found.description },
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
case "run": {
|
|
321
|
+
if (!skillName) {
|
|
322
|
+
return {
|
|
323
|
+
content: [{ type: "text", text: "Error: 'skill' parameter is required for 'run' action." }],
|
|
324
|
+
details: { error: "missing_skill_param" },
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const found = skills.find((s) => s.name === skillName);
|
|
328
|
+
if (!found) {
|
|
329
|
+
return {
|
|
330
|
+
content: [
|
|
331
|
+
{
|
|
332
|
+
type: "text",
|
|
333
|
+
text: `Error: skill '${skillName}' not found. Use 'list' to see available skills.`,
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
details: { error: "skill_not_found", skill: skillName },
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const prompt = buildSkillPrompt(found, args ?? "");
|
|
340
|
+
const { injected, via } = await injectSkillPrompt(pi, prompt);
|
|
341
|
+
return {
|
|
342
|
+
content: [{ type: "text", text: prompt }],
|
|
343
|
+
details: { skill: found.name, injected, via },
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// ----- Event handlers --------------------------------------------------
|
|
351
|
+
|
|
352
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
353
|
+
const count = skills.length;
|
|
354
|
+
ctx.ui.notify(`bigpowers: ${count} skills loaded`, "info");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
pi.on("tool_call", (event) => {
|
|
358
|
+
if (event.toolName !== "bash") return;
|
|
359
|
+
const { command } = event.input as { command?: string };
|
|
360
|
+
if (!command) return;
|
|
361
|
+
|
|
362
|
+
for (const pat of DANGEROUS_PATTERNS) {
|
|
363
|
+
if (pat.test(command)) {
|
|
364
|
+
return {
|
|
365
|
+
block: true,
|
|
366
|
+
reason: `BLOCKED: '${command}' matches dangerous pattern '${pat}'.`,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const branch = currentBranch();
|
|
372
|
+
|
|
373
|
+
if (/git\s+push/.test(command) && protectedPushTarget(command, branch)) {
|
|
374
|
+
return {
|
|
375
|
+
block: true,
|
|
376
|
+
reason:
|
|
377
|
+
"BLOCKED: Direct push to protected branch. Use kickoff-branch + release-branch.",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (/git\s+commit/.test(command)) {
|
|
382
|
+
if ((PROTECTED_BRANCHES as readonly string[]).includes(branch)) {
|
|
383
|
+
return {
|
|
384
|
+
block: true,
|
|
385
|
+
reason: `BLOCKED: Direct commits to '${branch}' are forbidden. Use a feature branch.`,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const msg = extractCommitMessage(command);
|
|
389
|
+
if (msg) {
|
|
390
|
+
if (!CONVENTIONAL_RE.test(msg)) {
|
|
391
|
+
return {
|
|
392
|
+
block: true,
|
|
393
|
+
reason:
|
|
394
|
+
"BLOCKED: Commit message must follow Conventional Commits: <type>(<scope>): <subject>.",
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (msg.split("\n")[0].length > 72) {
|
|
398
|
+
return {
|
|
399
|
+
block: true,
|
|
400
|
+
reason: "BLOCKED: Commit subject line must be ≤72 characters.",
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e82s03
|
|
3
|
+
# bigpowers — pre-tool-use hook for OMP bash tool
|
|
4
|
+
# Shell fallback for runtimes that invoke hooks as processes rather than
|
|
5
|
+
# calling the TypeScript extension directly. Blocks dangerous git commands
|
|
6
|
+
# and enforces Conventional Commits. Primary implementation lives in
|
|
7
|
+
# extensions/omp-hooks.ts (ExtensionAPI tool_call handler).
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
COMMAND=$(echo "$INPUT" | jq -r '.command // .tool_input.command // empty' 2>/dev/null || true)
|
|
12
|
+
[ -z "$COMMAND" ] && exit 0
|
|
13
|
+
|
|
14
|
+
DANGEROUS_PATTERNS=(
|
|
15
|
+
"git reset --hard"
|
|
16
|
+
"git clean -f"
|
|
17
|
+
"git branch -D"
|
|
18
|
+
"push --force"
|
|
19
|
+
"push -f"
|
|
20
|
+
)
|
|
21
|
+
PROTECTED_BRANCHES=("main" "master")
|
|
22
|
+
CONVENTIONAL_RE='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?:[[:space:]].+'
|
|
23
|
+
|
|
24
|
+
for p in "${DANGEROUS_PATTERNS[@]}"; do
|
|
25
|
+
if echo "$COMMAND" | grep -qE "$p"; then
|
|
26
|
+
printf '{"decision":"block","reason":"BLOCKED: dangerous pattern %q"}\n' "$p"
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
if [[ "$COMMAND" =~ git[[:space:]]+commit ]]; then
|
|
32
|
+
MSG=""
|
|
33
|
+
if [[ "$COMMAND" =~ -m[[:space:]]+"([^"]+)" ]]; then MSG="${BASH_REMATCH[1]}"; fi
|
|
34
|
+
if [ -n "$MSG" ] && [[ ! "$MSG" =~ $CONVENTIONAL_RE ]]; then
|
|
35
|
+
echo '{"decision":"block","reason":"BLOCKED: Conventional Commits required"}'
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
39
|
+
for b in "${PROTECTED_BRANCHES[@]}"; do
|
|
40
|
+
if [[ "$BRANCH" == "$b" ]]; then
|
|
41
|
+
printf '{"decision":"block","reason":"BLOCKED: direct commit to %q"}\n' "$b"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
done
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
exit 0
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bigpowers",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.88.0",
|
|
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
|
+
"omp": {
|
|
7
|
+
"extensions": [
|
|
8
|
+
"extensions/omp-hooks.ts"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
6
11
|
"scripts": {
|
|
7
12
|
"setup": "node bin/setup.js",
|
|
8
13
|
"compliance": "bash scripts/audit-compliance.sh specs/verifications/features && bash scripts/validate-doctrine.sh && python3 scripts/check-skill-links.py",
|
|
Binary file
|
|
Binary file
|
|
@@ -64,6 +64,8 @@ GOLDEN_GATES=(
|
|
|
64
64
|
"epics-wiki-zero-stories:bash scripts/test-generate-epics-wiki-zero-stories.sh:false"
|
|
65
65
|
"golden-g11-worktree-venv:bash scripts/test-golden-g11-worktree-venv.sh:false"
|
|
66
66
|
"install-helpers:bash scripts/test-install-helpers.sh:false"
|
|
67
|
+
"project-runtime:bash scripts/test-project-runtime.sh:false"
|
|
68
|
+
"omp-extension:bash scripts/validate-omp-extension.sh:false"
|
|
67
69
|
"import-boundaries:bash scripts/check-import-boundaries.sh:false"
|
|
68
70
|
"skill-catalog:bash scripts/validate-skill-catalog.sh:false"
|
|
69
71
|
"specs-parse:bash scripts/validate-specs-yaml.sh:false"
|
|
@@ -36,7 +36,11 @@ function assertReplaceable(dst) {
|
|
|
36
36
|
}
|
|
37
37
|
if (st.isSymbolicLink()) {
|
|
38
38
|
const target = fs.readlinkSync(dst);
|
|
39
|
-
|
|
39
|
+
// Path-separator boundary: a string-prefix sibling of the package root
|
|
40
|
+
// (e.g. <ROOT>-evil) is NOT managed by this package.
|
|
41
|
+
if (target === REPO_ROOT || target.startsWith(REPO_ROOT + path.sep)) {
|
|
42
|
+
return; // managed by bigpowers — safe to re-link
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
throw new Error(
|
|
42
46
|
`Refusing to replace ${dst}: it exists and is not a bigpowers-managed symlink. ` +
|
|
@@ -416,6 +420,52 @@ function installLocal(tool, repoRoot) {
|
|
|
416
420
|
}
|
|
417
421
|
}
|
|
418
422
|
|
|
423
|
+
// ── Project provisioning (bigpowers init) ────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
// Provision the CURRENT project (cwd) with the package's scripts/ tree and the
|
|
426
|
+
// specs/ scaffolding skill bodies assume. Package installs (pi, npm -g, npx)
|
|
427
|
+
// register skills but pi's package contract has no resource type for project
|
|
428
|
+
// files and no lifecycle script runs — so `bash scripts/*.sh` invocations and
|
|
429
|
+
// `→ verify:` gates in SKILL.md files fail outside a bigpowers checkout. (#116)
|
|
430
|
+
function initProject(repoRoot) {
|
|
431
|
+
const cwd = process.cwd();
|
|
432
|
+
if (path.resolve(cwd) === path.resolve(repoRoot)) {
|
|
433
|
+
return { skipped: 'cwd-is-package' };
|
|
434
|
+
}
|
|
435
|
+
linkDir(path.join(repoRoot, 'scripts'), path.join(cwd, 'scripts'));
|
|
436
|
+
const created = [];
|
|
437
|
+
for (const rel of [path.join('specs', 'bugs'), path.join('specs', 'verifications')]) {
|
|
438
|
+
const dir = path.join(cwd, rel);
|
|
439
|
+
if (!fs.existsSync(dir)) {
|
|
440
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
441
|
+
created.push(rel);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return { skipped: null, scriptsLinked: true, created };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Inverse of initProject: remove ONLY the managed scripts symlink and specs/
|
|
448
|
+
// scaffolding dirs we created (and only while untouched — user content survives).
|
|
449
|
+
function initProjectRemove(repoRoot) {
|
|
450
|
+
const cwd = process.cwd();
|
|
451
|
+
const scriptsLink = path.join(cwd, 'scripts');
|
|
452
|
+
try {
|
|
453
|
+
if (fs.lstatSync(scriptsLink).isSymbolicLink()
|
|
454
|
+
&& fs.readlinkSync(scriptsLink) === path.join(repoRoot, 'scripts')) {
|
|
455
|
+
fs.unlinkSync(scriptsLink);
|
|
456
|
+
}
|
|
457
|
+
} catch {}
|
|
458
|
+
for (const rel of [path.join('specs', 'bugs'), path.join('specs', 'verifications'), 'specs']) {
|
|
459
|
+
const dir = path.join(cwd, rel);
|
|
460
|
+
try {
|
|
461
|
+
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()
|
|
462
|
+
&& fs.readdirSync(dir).length === 0) {
|
|
463
|
+
fs.rmdirSync(dir);
|
|
464
|
+
}
|
|
465
|
+
} catch {}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
419
469
|
// ── Uninstall per tool ───────────────────────────────────────────────────────
|
|
420
470
|
|
|
421
471
|
function uninstallTool(toolId, repoRoot) {
|
|
@@ -694,6 +744,8 @@ module.exports = {
|
|
|
694
744
|
linkHook,
|
|
695
745
|
installGlobal,
|
|
696
746
|
installLocal,
|
|
747
|
+
initProject,
|
|
748
|
+
initProjectRemove,
|
|
697
749
|
uninstallTool,
|
|
698
750
|
removeSymlink,
|
|
699
751
|
detectExistingInstall,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e82s01
|
|
3
|
+
# Resolve consumer-project runtime settings without external YAML dependencies.
|
|
4
|
+
|
|
5
|
+
if [[ -n "${BIGPOWERS_PROJECT_RUNTIME_LOADED:-}" ]]; then
|
|
6
|
+
return 0
|
|
7
|
+
fi
|
|
8
|
+
BIGPOWERS_PROJECT_RUNTIME_LOADED=1
|
|
9
|
+
|
|
10
|
+
_bp_config_value() {
|
|
11
|
+
local project_root="$1" key="$2"
|
|
12
|
+
local config_file="$project_root/.bigpowers/config.yaml"
|
|
13
|
+
[[ -f "$config_file" ]] || return 1
|
|
14
|
+
|
|
15
|
+
awk -v key="$key" '
|
|
16
|
+
/^[[:space:]]*#/ { next }
|
|
17
|
+
{
|
|
18
|
+
separator = index($0, ":")
|
|
19
|
+
if (!separator) next
|
|
20
|
+
candidate = substr($0, 1, separator - 1)
|
|
21
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", candidate)
|
|
22
|
+
if (candidate != key) next
|
|
23
|
+
value = substr($0, separator + 1)
|
|
24
|
+
sub(/[[:space:]]+#.*$/, "", value)
|
|
25
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
|
26
|
+
if ((substr(value, 1, 1) == "\"" && substr(value, length(value), 1) == "\"") ||
|
|
27
|
+
(substr(value, 1, 1) == "\047" && substr(value, length(value), 1) == "\047")) {
|
|
28
|
+
value = substr(value, 2, length(value) - 2)
|
|
29
|
+
}
|
|
30
|
+
print value
|
|
31
|
+
found = 1
|
|
32
|
+
exit
|
|
33
|
+
}
|
|
34
|
+
END { if (!found) exit 1 }
|
|
35
|
+
' "$config_file"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_bp_specs_dir_value() {
|
|
39
|
+
local project_root="$1"
|
|
40
|
+
if [[ -n "${BIGPOWERS_SPECS_DIR+x}" ]]; then
|
|
41
|
+
printf '%s\n' "$BIGPOWERS_SPECS_DIR"
|
|
42
|
+
elif _bp_config_value "$project_root" specs_dir; then
|
|
43
|
+
return 0
|
|
44
|
+
else
|
|
45
|
+
printf '%s\n' specs
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
bp_specs_dir() {
|
|
50
|
+
local project_root="${1:-$PWD}"
|
|
51
|
+
local specs_dir
|
|
52
|
+
specs_dir="$(_bp_specs_dir_value "$project_root")"
|
|
53
|
+
if [[ -z "$specs_dir" ]]; then
|
|
54
|
+
echo "bigpowers config: specs_dir must not be empty; set it to specs, .specs, or another directory" >&2
|
|
55
|
+
return 2
|
|
56
|
+
fi
|
|
57
|
+
printf '%s\n' "$specs_dir"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
bp_specs_path() {
|
|
61
|
+
local project_root="${1:-$PWD}" relative_path="${2:-}"
|
|
62
|
+
local specs_dir
|
|
63
|
+
specs_dir="$(bp_specs_dir "$project_root")" || return
|
|
64
|
+
|
|
65
|
+
if [[ "$specs_dir" == /* ]]; then
|
|
66
|
+
if [[ -n "$relative_path" ]]; then
|
|
67
|
+
printf '%s/%s\n' "${specs_dir%/}" "${relative_path#/}"
|
|
68
|
+
else
|
|
69
|
+
printf '%s\n' "${specs_dir%/}"
|
|
70
|
+
fi
|
|
71
|
+
elif [[ -n "$relative_path" ]]; then
|
|
72
|
+
printf '%s/%s/%s\n' "${project_root%/}" "${specs_dir%/}" "${relative_path#/}"
|
|
73
|
+
else
|
|
74
|
+
printf '%s/%s\n' "${project_root%/}" "${specs_dir%/}"
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_bp_vcs_value() {
|
|
79
|
+
local project_root="$1"
|
|
80
|
+
if [[ -n "${BIGPOWERS_VCS+x}" ]]; then
|
|
81
|
+
printf '%s\n' "$BIGPOWERS_VCS"
|
|
82
|
+
elif _bp_config_value "$project_root" vcs; then
|
|
83
|
+
return 0
|
|
84
|
+
else
|
|
85
|
+
printf '%s\n' auto
|
|
86
|
+
fi
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
bp_vcs_kind() {
|
|
90
|
+
local project_root="${1:-$PWD}"
|
|
91
|
+
local configured_vcs
|
|
92
|
+
configured_vcs="$(_bp_vcs_value "$project_root")"
|
|
93
|
+
|
|
94
|
+
case "$configured_vcs" in
|
|
95
|
+
git|jj) printf '%s\n' "$configured_vcs" ;;
|
|
96
|
+
auto)
|
|
97
|
+
if [[ -d "$project_root/.jj" ]]; then
|
|
98
|
+
printf '%s\n' jj
|
|
99
|
+
elif [[ -e "$project_root/.git" ]]; then
|
|
100
|
+
printf '%s\n' git
|
|
101
|
+
else
|
|
102
|
+
printf '%s\n' none
|
|
103
|
+
fi
|
|
104
|
+
;;
|
|
105
|
+
*)
|
|
106
|
+
echo "bigpowers config: invalid vcs '$configured_vcs'. Expected one of: auto, git, jj" >&2
|
|
107
|
+
return 2
|
|
108
|
+
;;
|
|
109
|
+
esac
|
|
110
|
+
}
|